@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.
- package/README.md +254 -0
- package/app.plugin.js +6 -0
- package/lib/adapters/audio.d.ts +74 -0
- package/lib/adapters/audio.js +39 -0
- package/lib/adapters/audioRoute.d.ts +57 -0
- package/lib/adapters/audioRoute.js +77 -0
- package/lib/adapters/expoDefaults.d.ts +77 -0
- package/lib/adapters/expoDefaults.js +539 -0
- package/lib/adapters/picker.d.ts +67 -0
- package/lib/adapters/picker.js +37 -0
- package/lib/adapters/webrtc.d.ts +131 -0
- package/lib/adapters/webrtc.js +70 -0
- package/lib/core/VideoCallClient.d.ts +106 -0
- package/lib/core/VideoCallClient.js +302 -0
- package/lib/core/WebchatClient.d.ts +34 -0
- package/lib/core/WebchatClient.js +132 -0
- package/lib/core/configClient.d.ts +42 -0
- package/lib/core/configClient.js +302 -0
- package/lib/core/greet.d.ts +11 -0
- package/lib/core/greet.js +17 -0
- package/lib/core/ice.d.ts +31 -0
- package/lib/core/ice.js +48 -0
- package/lib/core/linkify.d.ts +11 -0
- package/lib/core/linkify.js +25 -0
- package/lib/core/logger.d.ts +17 -0
- package/lib/core/logger.js +53 -0
- package/lib/core/media.d.ts +52 -0
- package/lib/core/media.js +115 -0
- package/lib/core/mediaType.d.ts +21 -0
- package/lib/core/mediaType.js +66 -0
- package/lib/core/messagesReducer.d.ts +36 -0
- package/lib/core/messagesReducer.js +58 -0
- package/lib/core/persistence.d.ts +45 -0
- package/lib/core/persistence.js +63 -0
- package/lib/core/socketFactory.d.ts +16 -0
- package/lib/core/socketFactory.js +82 -0
- package/lib/core/types.d.ts +320 -0
- package/lib/core/types.js +30 -0
- package/lib/core/unread.d.ts +2 -0
- package/lib/core/unread.js +5 -0
- package/lib/i18n/ar.json +1 -0
- package/lib/i18n/en.json +1 -0
- package/lib/i18n/index.d.ts +7 -0
- package/lib/i18n/index.js +43 -0
- package/lib/index.d.ts +59 -0
- package/lib/index.js +142 -0
- package/lib/plugin/withWebchat.d.ts +53 -0
- package/lib/plugin/withWebchat.js +164 -0
- package/lib/state/WebchatProvider.d.ts +132 -0
- package/lib/state/WebchatProvider.js +906 -0
- package/lib/state/useWebchat.d.ts +1 -0
- package/lib/state/useWebchat.js +12 -0
- package/lib/theme/dir.d.ts +14 -0
- package/lib/theme/dir.js +20 -0
- package/lib/theme/themeFactory.d.ts +219 -0
- package/lib/theme/themeFactory.js +182 -0
- package/lib/ui/AttachButton.d.ts +35 -0
- package/lib/ui/AttachButton.js +26 -0
- package/lib/ui/AudioRecorder.d.ts +25 -0
- package/lib/ui/AudioRecorder.js +228 -0
- package/lib/ui/Bubble.d.ts +1 -0
- package/lib/ui/Bubble.js +265 -0
- package/lib/ui/CallControls.d.ts +27 -0
- package/lib/ui/CallControls.js +92 -0
- package/lib/ui/CallPlaceholder.d.ts +16 -0
- package/lib/ui/CallPlaceholder.js +73 -0
- package/lib/ui/Composer.d.ts +5 -0
- package/lib/ui/Composer.js +272 -0
- package/lib/ui/FileTile.d.ts +9 -0
- package/lib/ui/FileTile.js +31 -0
- package/lib/ui/Header.d.ts +52 -0
- package/lib/ui/Header.js +236 -0
- package/lib/ui/Icon.d.ts +21 -0
- package/lib/ui/Icon.js +110 -0
- package/lib/ui/ImageBubble.d.ts +11 -0
- package/lib/ui/ImageBubble.js +16 -0
- package/lib/ui/MediaUploadMenu.d.ts +23 -0
- package/lib/ui/MediaUploadMenu.js +68 -0
- package/lib/ui/MessageList.d.ts +1 -0
- package/lib/ui/MessageList.js +46 -0
- package/lib/ui/PoweredBy.d.ts +8 -0
- package/lib/ui/PoweredBy.js +14 -0
- package/lib/ui/PrechatForm.d.ts +1 -0
- package/lib/ui/PrechatForm.js +230 -0
- package/lib/ui/QuickReplies.d.ts +1 -0
- package/lib/ui/QuickReplies.js +24 -0
- package/lib/ui/TypingIndicator.d.ts +9 -0
- package/lib/ui/TypingIndicator.js +88 -0
- package/lib/ui/VideoBubble.d.ts +10 -0
- package/lib/ui/VideoBubble.js +130 -0
- package/lib/ui/VideoCall.d.ts +34 -0
- package/lib/ui/VideoCall.js +191 -0
- package/lib/ui/VideoTile.d.ts +25 -0
- package/lib/ui/VideoTile.js +13 -0
- package/lib/ui/VoiceMessage.d.ts +19 -0
- package/lib/ui/VoiceMessage.js +127 -0
- package/lib/ui/WebChat.d.ts +10 -0
- package/lib/ui/WebChat.js +386 -0
- package/lib/ui/openLink.d.ts +1 -0
- package/lib/ui/openLink.js +16 -0
- package/package.json +94 -0
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PrechatForm = PrechatForm;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
// src/ui/PrechatForm.tsx
|
|
6
|
+
//
|
|
7
|
+
// Pre-chat form at web parity (audit Section 3, Tier B #22). Renders the
|
|
8
|
+
// server-configured (or host-supplied) `fields[]` with per-field type support
|
|
9
|
+
// (text / email / phone / dropdown), `{en,ar}` localized label/placeholder/
|
|
10
|
+
// errorMessage, a localized headline + submit label, RTL via `dirStyles(dir)`,
|
|
11
|
+
// an in-form language toggle, inline error-clear on change, and a
|
|
12
|
+
// submitting/disabled state. Validation (required + email/phone regex) is ported
|
|
13
|
+
// verbatim from the web widget's `redux/setting/actions.ts`.
|
|
14
|
+
//
|
|
15
|
+
// On a valid submit it calls `onSubmit(values)` with the collected values keyed
|
|
16
|
+
// by `field.key` — exactly the shape `core/greet.ts` (`buildGreet`) consumes, so
|
|
17
|
+
// the greet plumbing stays untouched.
|
|
18
|
+
const react_1 = require("react");
|
|
19
|
+
const react_native_1 = require("react-native");
|
|
20
|
+
const dir_1 = require("../theme/dir");
|
|
21
|
+
// ---- Validation regexes (PORTED VERBATIM from web actions.ts:84-85) ----
|
|
22
|
+
const EMAIL_RE = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
23
|
+
const PHONE_RE = /^\+?[\d\s\-()]{6,}$/;
|
|
24
|
+
// Localized-text resolver: web reads `text[lang] || text.en`. We additionally
|
|
25
|
+
// accept a plain string (back-compat with hosts/tests that pass `label: "Name"`)
|
|
26
|
+
// and tolerate an absent value.
|
|
27
|
+
function localize(text, lang) {
|
|
28
|
+
if (text == null)
|
|
29
|
+
return "";
|
|
30
|
+
if (typeof text === "string")
|
|
31
|
+
return text;
|
|
32
|
+
return text[lang] || text.en || "";
|
|
33
|
+
}
|
|
34
|
+
// Default per-field error (web parity: actions.ts validatePrechatField). Used
|
|
35
|
+
// only when the field has no `errorMessage[lang]` override.
|
|
36
|
+
function defaultError(kind, lang) {
|
|
37
|
+
if (kind === "email") {
|
|
38
|
+
return lang === "ar" ? "بريد إلكتروني غير صالح" : "Invalid email address";
|
|
39
|
+
}
|
|
40
|
+
if (kind === "phone") {
|
|
41
|
+
return lang === "ar" ? "رقم هاتف غير صالح" : "Invalid phone number";
|
|
42
|
+
}
|
|
43
|
+
return lang === "ar" ? "هذا الحقل مطلوب" : "This field is required";
|
|
44
|
+
}
|
|
45
|
+
// Per-field validation mirroring web validatePrechatField (actions.ts:87-116):
|
|
46
|
+
// required (default true -> required !== false) empty-check first, then the
|
|
47
|
+
// type-specific regex on a non-empty trimmed value. Returns the localized
|
|
48
|
+
// error string or null when valid.
|
|
49
|
+
function validateField(field, value, lang) {
|
|
50
|
+
const trimmed = (value !== null && value !== void 0 ? value : "").trim();
|
|
51
|
+
const required = field.required !== false;
|
|
52
|
+
const custom = (kind) => localize(field.errorMessage, lang) || defaultError(kind, lang);
|
|
53
|
+
if (required && trimmed === "")
|
|
54
|
+
return custom("required");
|
|
55
|
+
if (trimmed === "")
|
|
56
|
+
return null; // optional + empty -> valid
|
|
57
|
+
if (field.type === "email" && !EMAIL_RE.test(trimmed))
|
|
58
|
+
return custom("email");
|
|
59
|
+
if (field.type === "phone" && !PHONE_RE.test(trimmed))
|
|
60
|
+
return custom("phone");
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
// RN keyboardType per field type (web parity: inputMode email/tel/text).
|
|
64
|
+
function keyboardTypeFor(type) {
|
|
65
|
+
if (type === "email")
|
|
66
|
+
return "email-address";
|
|
67
|
+
if (type === "phone")
|
|
68
|
+
return "phone-pad";
|
|
69
|
+
return "default";
|
|
70
|
+
}
|
|
71
|
+
// Custom RN dropdown: a Pressable anchor opening a Modal bottom-sheet listing the
|
|
72
|
+
// localized options (reuses the MediaUploadMenu sheet pattern — NO native picker
|
|
73
|
+
// peer). The selected option's localized label shows in the anchor; the
|
|
74
|
+
// placeholder shows when nothing is selected.
|
|
75
|
+
function Dropdown({ field, value, lang, dir, theme, hasError, onSelect }) {
|
|
76
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
77
|
+
const [open, setOpen] = (0, react_1.useState)(false);
|
|
78
|
+
const d = (0, dir_1.dirStyles)(dir);
|
|
79
|
+
const options = (_a = field.options) !== null && _a !== void 0 ? _a : [];
|
|
80
|
+
const placeholder = localize(field.placeholder, lang);
|
|
81
|
+
const selected = options.find((o) => o.value === value);
|
|
82
|
+
const display = selected ? localize(selected.label, lang) : placeholder;
|
|
83
|
+
const textColor = (_c = (_b = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _b === void 0 ? void 0 : _b.text) !== null && _c !== void 0 ? _c : "#161616";
|
|
84
|
+
const muted = "#9DA4AE";
|
|
85
|
+
const borderColor = hasError ? "#B42318" : "#9DA4AE";
|
|
86
|
+
const surface = (_e = (_d = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _d === void 0 ? void 0 : _d.surface) !== null && _e !== void 0 ? _e : "#FFFFFF";
|
|
87
|
+
return ((0, jsx_runtime_1.jsxs)(jsx_runtime_1.Fragment, { children: [(0, jsx_runtime_1.jsxs)(react_native_1.Pressable, { testID: `prechat-field-${field.key}`, accessibilityRole: "button", accessibilityLabel: localize(field.label, lang), onPress: () => setOpen(true), style: {
|
|
88
|
+
flexDirection: d.rowDirection,
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
justifyContent: "space-between",
|
|
91
|
+
borderWidth: 1,
|
|
92
|
+
borderColor,
|
|
93
|
+
borderRadius: 4,
|
|
94
|
+
minHeight: 40,
|
|
95
|
+
paddingHorizontal: 12,
|
|
96
|
+
backgroundColor: surface,
|
|
97
|
+
}, children: [(0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
|
|
98
|
+
flex: 1,
|
|
99
|
+
color: selected ? textColor : muted,
|
|
100
|
+
textAlign: d.textAlign,
|
|
101
|
+
fontSize: (_g = (_f = theme === null || theme === void 0 ? void 0 : theme.font) === null || _f === void 0 ? void 0 : _f.size) !== null && _g !== void 0 ? _g : 16,
|
|
102
|
+
}, children: display }), (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: muted, marginHorizontal: 4 }, children: "\u25BE" })] }), (0, jsx_runtime_1.jsx)(react_native_1.Modal, { visible: open, transparent: true, animationType: "fade", onRequestClose: () => setOpen(false), children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-dropdown-backdrop-${field.key}`, accessibilityRole: "button", onPress: () => setOpen(false), style: {
|
|
103
|
+
flex: 1,
|
|
104
|
+
backgroundColor: "rgba(0,0,0,0.35)",
|
|
105
|
+
justifyContent: "flex-end",
|
|
106
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-dropdown-sheet-${field.key}`, accessibilityViewIsModal: true, accessibilityRole: "menu", onPress: () => { }, style: {
|
|
107
|
+
backgroundColor: surface,
|
|
108
|
+
borderTopLeftRadius: 12,
|
|
109
|
+
borderTopRightRadius: 12,
|
|
110
|
+
paddingVertical: 8,
|
|
111
|
+
}, children: options.map((opt) => {
|
|
112
|
+
var _a, _b;
|
|
113
|
+
const isSel = opt.value === value;
|
|
114
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: `prechat-option-${field.key}-${opt.value}`, accessibilityRole: "menuitem", accessibilityState: { selected: isSel }, accessibilityLabel: localize(opt.label, lang), onPress: () => {
|
|
115
|
+
onSelect(opt.value);
|
|
116
|
+
setOpen(false);
|
|
117
|
+
}, style: {
|
|
118
|
+
paddingVertical: 14,
|
|
119
|
+
paddingHorizontal: 20,
|
|
120
|
+
backgroundColor: isSel ? "#F3F4F6" : "transparent",
|
|
121
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: {
|
|
122
|
+
color: textColor,
|
|
123
|
+
textAlign: d.textAlign,
|
|
124
|
+
fontSize: (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.font) === null || _a === void 0 ? void 0 : _a.size) !== null && _b !== void 0 ? _b : 16,
|
|
125
|
+
fontWeight: isSel ? "600" : "400",
|
|
126
|
+
}, children: localize(opt.label, lang) }) }, opt.value));
|
|
127
|
+
}) }) }) })] }));
|
|
128
|
+
}
|
|
129
|
+
function PrechatForm({ fields, onSubmit, dir, theme, headline, submitLabel,
|
|
130
|
+
// Initial language: ar when dir is rtl (web derives both from the same source);
|
|
131
|
+
// the in-form toggle then drives `lang` independently of the host dir.
|
|
132
|
+
language, }) {
|
|
133
|
+
var _a, _b, _c, _d, _e, _f, _g, _h;
|
|
134
|
+
const initialLang = language === "ar" || (language == null && dir === "rtl") ? "ar" : "en";
|
|
135
|
+
const [lang, setLang] = (0, react_1.useState)(initialLang);
|
|
136
|
+
const [values, setValues] = (0, react_1.useState)({});
|
|
137
|
+
const [errors, setErrors] = (0, react_1.useState)({});
|
|
138
|
+
const [submitting, setSubmitting] = (0, react_1.useState)(false);
|
|
139
|
+
// The toggle flips the displayed language; direction follows it (web parity —
|
|
140
|
+
// the form direction tracks the form language, not the host dir).
|
|
141
|
+
const effectiveDir = lang === "ar" ? "rtl" : "ltr";
|
|
142
|
+
const d = (0, dir_1.dirStyles)(effectiveDir);
|
|
143
|
+
const fieldList = Array.isArray(fields) ? fields : [];
|
|
144
|
+
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 : "#1B8354";
|
|
145
|
+
const textColor = (_d = (_c = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _c === void 0 ? void 0 : _c.text) !== null && _d !== void 0 ? _d : "#161616";
|
|
146
|
+
const surface = (_f = (_e = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _e === void 0 ? void 0 : _e.surface) !== null && _f !== void 0 ? _f : "#FFFFFF";
|
|
147
|
+
const fontSize = (_h = (_g = theme === null || theme === void 0 ? void 0 : theme.font) === null || _g === void 0 ? void 0 : _g.size) !== null && _h !== void 0 ? _h : 16;
|
|
148
|
+
const headlineText = localize(headline, lang);
|
|
149
|
+
const submitText = localize(submitLabel, lang) || (lang === "ar" ? "ابدأ المحادثة" : "Start chat");
|
|
150
|
+
const toggleLabel = lang === "ar" ? "EN" : "AR";
|
|
151
|
+
const setValue = (key, v) => {
|
|
152
|
+
setValues((prev) => ({ ...prev, [key]: v }));
|
|
153
|
+
// Inline error-clear on change (web ChatContainer parity).
|
|
154
|
+
setErrors((prev) => {
|
|
155
|
+
if (!prev[key])
|
|
156
|
+
return prev;
|
|
157
|
+
const next = { ...prev };
|
|
158
|
+
delete next[key];
|
|
159
|
+
return next;
|
|
160
|
+
});
|
|
161
|
+
};
|
|
162
|
+
const submit = () => {
|
|
163
|
+
if (submitting)
|
|
164
|
+
return;
|
|
165
|
+
const nextErrors = {};
|
|
166
|
+
fieldList.forEach((f) => {
|
|
167
|
+
var _a;
|
|
168
|
+
const err = validateField(f, (_a = values[f.key]) !== null && _a !== void 0 ? _a : "", lang);
|
|
169
|
+
if (err)
|
|
170
|
+
nextErrors[f.key] = err;
|
|
171
|
+
});
|
|
172
|
+
setErrors(nextErrors);
|
|
173
|
+
if (Object.keys(nextErrors).length > 0)
|
|
174
|
+
return; // block submit until valid
|
|
175
|
+
setSubmitting(true);
|
|
176
|
+
onSubmit(values);
|
|
177
|
+
};
|
|
178
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { testID: "prechat-form", style: { padding: 16, flexDirection: "column" }, children: [(0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection, marginBottom: 8 }, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "prechat-language-toggle", accessibilityRole: "button", accessibilityLabel: lang === "ar" ? "Switch to English" : "التبديل إلى العربية", onPress: () => setLang((l) => (l === "ar" ? "en" : "ar")), style: {
|
|
179
|
+
borderWidth: 1,
|
|
180
|
+
borderColor: primary,
|
|
181
|
+
borderRadius: 999,
|
|
182
|
+
paddingVertical: 4,
|
|
183
|
+
paddingHorizontal: 10,
|
|
184
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: primary, fontWeight: "600", fontSize: fontSize - 2 }, children: toggleLabel }) }) }), headlineText ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { testID: "prechat-headline", style: {
|
|
185
|
+
color: textColor,
|
|
186
|
+
fontSize: fontSize + 4,
|
|
187
|
+
fontWeight: "600",
|
|
188
|
+
textAlign: d.textAlign,
|
|
189
|
+
marginBottom: 16,
|
|
190
|
+
}, children: headlineText })) : null, fieldList.map((f) => {
|
|
191
|
+
var _a, _b, _c;
|
|
192
|
+
const label = localize(f.label, lang);
|
|
193
|
+
const placeholder = localize(f.placeholder, lang);
|
|
194
|
+
const required = f.required !== false;
|
|
195
|
+
const error = (_a = errors[f.key]) !== null && _a !== void 0 ? _a : null;
|
|
196
|
+
const isDropdown = f.type === "dropdown";
|
|
197
|
+
return ((0, jsx_runtime_1.jsxs)(react_native_1.View, { style: { marginBottom: 16 }, children: [(0, jsx_runtime_1.jsxs)(react_native_1.Text, { testID: `prechat-label-${f.key}`, style: {
|
|
198
|
+
color: textColor,
|
|
199
|
+
fontSize,
|
|
200
|
+
fontWeight: "500",
|
|
201
|
+
textAlign: d.textAlign,
|
|
202
|
+
marginBottom: 6,
|
|
203
|
+
}, children: [label, required ? (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#B42318" }, children: " *" }) : null] }), isDropdown ? ((0, jsx_runtime_1.jsx)(Dropdown, { field: f, value: (_b = values[f.key]) !== null && _b !== void 0 ? _b : "", lang: lang, dir: effectiveDir, theme: theme, hasError: Boolean(error), onSelect: (v) => setValue(f.key, v) })) : ((0, jsx_runtime_1.jsx)(react_native_1.TextInput, { testID: `prechat-field-${f.key}`, accessibilityLabel: label, value: (_c = values[f.key]) !== null && _c !== void 0 ? _c : "", onChangeText: (t) => setValue(f.key, t), placeholder: placeholder, placeholderTextColor: "#9DA4AE", editable: !submitting, keyboardType: keyboardTypeFor(f.type), autoCapitalize: f.type === "email" ? "none" : "sentences", autoCorrect: f.type !== "email", style: {
|
|
204
|
+
borderWidth: 1,
|
|
205
|
+
borderColor: error ? "#B42318" : "#9DA4AE",
|
|
206
|
+
borderRadius: 4,
|
|
207
|
+
minHeight: 40,
|
|
208
|
+
paddingHorizontal: 12,
|
|
209
|
+
paddingVertical: 8,
|
|
210
|
+
color: textColor,
|
|
211
|
+
fontSize,
|
|
212
|
+
textAlign: d.textAlign,
|
|
213
|
+
backgroundColor: surface,
|
|
214
|
+
writingDirection: effectiveDir,
|
|
215
|
+
} })), error ? ((0, jsx_runtime_1.jsx)(react_native_1.Text, { testID: `prechat-error-${f.key}`, style: {
|
|
216
|
+
color: "#B42318",
|
|
217
|
+
fontSize: fontSize - 2,
|
|
218
|
+
textAlign: d.textAlign,
|
|
219
|
+
marginTop: 4,
|
|
220
|
+
}, children: error })) : null] }, f.key));
|
|
221
|
+
}), (0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection }, children: (0, jsx_runtime_1.jsx)(react_native_1.Pressable, { testID: "prechat-submit", accessibilityRole: "button", accessibilityLabel: submitText, accessibilityState: { disabled: submitting, busy: submitting }, disabled: submitting, onPress: submit, style: {
|
|
222
|
+
backgroundColor: primary,
|
|
223
|
+
borderRadius: 4,
|
|
224
|
+
paddingVertical: 10,
|
|
225
|
+
paddingHorizontal: 16,
|
|
226
|
+
minWidth: 90,
|
|
227
|
+
alignItems: "center",
|
|
228
|
+
opacity: submitting ? 0.6 : 1,
|
|
229
|
+
}, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: "#FFFFFF", fontWeight: "500", fontSize }, children: submitText }) }) })] }));
|
|
230
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function QuickReplies({ items, onSelect, theme, dir }: any): import("react/jsx-runtime").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.QuickReplies = QuickReplies;
|
|
4
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
5
|
+
const react_native_1 = require("react-native");
|
|
6
|
+
const dir_1 = require("../theme/dir");
|
|
7
|
+
const themeFactory_1 = require("../theme/themeFactory");
|
|
8
|
+
// Quick-reply chips for a bot message (audit #3). The row honours the resolved
|
|
9
|
+
// direction (audit #10): rowDirection flips to row-reverse under RTL so the
|
|
10
|
+
// chips read leading-edge first. Selecting a chip calls onSelect(q) — the caller
|
|
11
|
+
// (MessageList) routes it through send({ text: title, payload }).
|
|
12
|
+
function QuickReplies({ items, onSelect, theme, dir = "ltr" }) {
|
|
13
|
+
var _a, _b, _c, _d, _e;
|
|
14
|
+
const d = (0, dir_1.dirStyles)(dir);
|
|
15
|
+
// Chip typography (web quickReplyFontSize / quickReplyFontWeight + defaultFontWeight).
|
|
16
|
+
// Fall back to the global font size / weight when the chat tokens are unset.
|
|
17
|
+
const chatStyle = (_a = theme === null || theme === void 0 ? void 0 : theme.chat) !== null && _a !== void 0 ? _a : {};
|
|
18
|
+
const fontSize = (_b = chatStyle.quickReplyFontSize) !== null && _b !== void 0 ? _b : theme.font.size;
|
|
19
|
+
const fontWeight = (_c = chatStyle.quickReplyFontWeight) !== null && _c !== void 0 ? _c : (_d = theme === null || theme === void 0 ? void 0 : theme.font) === null || _d === void 0 ? void 0 : _d.weight;
|
|
20
|
+
// Tenant custom font (item 28): loaded RN family for the chip weight; undefined =>
|
|
21
|
+
// omit fontFamily (system font).
|
|
22
|
+
const fontFamily = (0, themeFactory_1.resolveFontFamily)((_e = theme === null || theme === void 0 ? void 0 : theme.font) === null || _e === void 0 ? void 0 : _e.familyMap, fontWeight);
|
|
23
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { style: { flexDirection: d.rowDirection, flexWrap: "wrap", gap: 8, marginTop: 6 }, children: items.map((q) => ((0, jsx_runtime_1.jsx)(react_native_1.Pressable, { accessibilityRole: "button", accessibilityLabel: q.title, onPress: () => onSelect(q), style: { borderWidth: 1, borderColor: theme.colors.primary, borderRadius: 15, paddingHorizontal: 12, paddingVertical: 6 }, children: (0, jsx_runtime_1.jsx)(react_native_1.Text, { style: { color: theme.colors.primary, fontSize, fontWeight, fontFamily }, children: q.title }) }, q.payload))) }));
|
|
24
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Dir } from "../theme/dir";
|
|
2
|
+
interface TypingIndicatorProps {
|
|
3
|
+
theme: any;
|
|
4
|
+
dir?: Dir;
|
|
5
|
+
/** testID hook for the container (defaults to "typing-indicator"). */
|
|
6
|
+
testID?: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function TypingIndicator({ theme, dir, testID }: TypingIndicatorProps): import("react/jsx-runtime").JSX.Element;
|
|
9
|
+
export default TypingIndicator;
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.TypingIndicator = TypingIndicator;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
// src/ui/TypingIndicator.tsx
|
|
9
|
+
//
|
|
10
|
+
// Animated three-dot "agent is typing" bubble (web parity — Body/index.tsx:134-191
|
|
11
|
+
// renders an avatar + a white bubble with three pulsing dots while
|
|
12
|
+
// `displayTypingIndication` is on). The web build wires it to a CSS `@keyframes wave`
|
|
13
|
+
// opacity pulse; RN has no CSS keyframes, so we drive the same staggered fade with an
|
|
14
|
+
// Animated.loop over three dots (each offset so the wave reads left → right).
|
|
15
|
+
//
|
|
16
|
+
// Themed: the bubble background + the leading-edge squared "tail" follow the received
|
|
17
|
+
// bubble tokens (theme.bubble.receivedBg / receivedRadius), and the dots take the
|
|
18
|
+
// bubble text color so they read on any tenant palette. Laid out with the logical
|
|
19
|
+
// `dir` so the bubble sits on the leading edge (left in LTR, right in RTL).
|
|
20
|
+
const react_1 = __importDefault(require("react"));
|
|
21
|
+
const react_native_1 = require("react-native");
|
|
22
|
+
const dir_1 = require("../theme/dir");
|
|
23
|
+
/** One pulsing dot — a looped opacity fade with a per-dot start delay. */
|
|
24
|
+
function Dot({ color, delay }) {
|
|
25
|
+
// 0.3 → 1 → 0.3 opacity wave (web uses opacity:0.4 idle); RN drives it with a loop.
|
|
26
|
+
const opacity = react_1.default.useRef(new react_native_1.Animated.Value(0.3)).current;
|
|
27
|
+
react_1.default.useEffect(() => {
|
|
28
|
+
const loop = react_native_1.Animated.loop(react_native_1.Animated.sequence([
|
|
29
|
+
react_native_1.Animated.delay(delay),
|
|
30
|
+
react_native_1.Animated.timing(opacity, {
|
|
31
|
+
toValue: 1,
|
|
32
|
+
duration: 400,
|
|
33
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
34
|
+
useNativeDriver: true,
|
|
35
|
+
}),
|
|
36
|
+
react_native_1.Animated.timing(opacity, {
|
|
37
|
+
toValue: 0.3,
|
|
38
|
+
duration: 400,
|
|
39
|
+
easing: react_native_1.Easing.inOut(react_native_1.Easing.ease),
|
|
40
|
+
useNativeDriver: true,
|
|
41
|
+
}),
|
|
42
|
+
]));
|
|
43
|
+
loop.start();
|
|
44
|
+
return () => loop.stop();
|
|
45
|
+
}, [opacity, delay]);
|
|
46
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.Animated.View, { style: {
|
|
47
|
+
width: 6,
|
|
48
|
+
height: 6,
|
|
49
|
+
borderRadius: 3,
|
|
50
|
+
marginHorizontal: 2,
|
|
51
|
+
backgroundColor: color,
|
|
52
|
+
opacity,
|
|
53
|
+
} }));
|
|
54
|
+
}
|
|
55
|
+
function TypingIndicator({ theme, dir = "ltr", testID = "typing-indicator" }) {
|
|
56
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q;
|
|
57
|
+
const d = (0, dir_1.dirStyles)(dir);
|
|
58
|
+
const bubble = (_a = theme === null || theme === void 0 ? void 0 : theme.bubble) !== null && _a !== void 0 ? _a : {};
|
|
59
|
+
const bg = (_d = (_b = bubble.receivedBg) !== null && _b !== void 0 ? _b : (_c = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _c === void 0 ? void 0 : _c.surface) !== null && _d !== void 0 ? _d : "#FFFFFF";
|
|
60
|
+
// dots take the received text color (web uses black @ 0.4 opacity on the white bubble).
|
|
61
|
+
const dotColor = (_g = (_e = bubble.receivedText) !== null && _e !== void 0 ? _e : (_f = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _f === void 0 ? void 0 : _f.text) !== null && _g !== void 0 ? _g : "#000000";
|
|
62
|
+
const radius = (_j = (_h = bubble.receivedRadius) !== null && _h !== void 0 ? _h : bubble.radius) !== null && _j !== void 0 ? _j : 8;
|
|
63
|
+
const marginX = (_k = bubble.marginX) !== null && _k !== void 0 ? _k : 15;
|
|
64
|
+
// Squared leading-edge tail to match a received bubble (web 0 1em 1em 1em).
|
|
65
|
+
const tail = d.bubbleTail("response");
|
|
66
|
+
const corner = tail === "bottomLeft"
|
|
67
|
+
? { borderBottomLeftRadius: 0 }
|
|
68
|
+
: { borderBottomRightRadius: 0 };
|
|
69
|
+
const shadow = {
|
|
70
|
+
shadowColor: (_l = bubble.shadowColor) !== null && _l !== void 0 ? _l : "#000",
|
|
71
|
+
shadowOpacity: (_m = bubble.shadowOpacity) !== null && _m !== void 0 ? _m : 0.12,
|
|
72
|
+
shadowRadius: (_o = bubble.shadowRadius) !== null && _o !== void 0 ? _o : 6,
|
|
73
|
+
shadowOffset: (_p = bubble.shadowOffset) !== null && _p !== void 0 ? _p : { width: 0, height: 2 },
|
|
74
|
+
elevation: (_q = bubble.elevation) !== null && _q !== void 0 ? _q : 2,
|
|
75
|
+
};
|
|
76
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: testID, accessibilityLabel: "Agent is typing", style: { alignSelf: "flex-start", maxWidth: "85%" }, children: (0, jsx_runtime_1.jsxs)(react_native_1.View, { style: {
|
|
77
|
+
flexDirection: d.rowDirection,
|
|
78
|
+
alignItems: "center",
|
|
79
|
+
backgroundColor: bg,
|
|
80
|
+
borderRadius: radius,
|
|
81
|
+
...corner,
|
|
82
|
+
marginHorizontal: marginX,
|
|
83
|
+
paddingHorizontal: 14,
|
|
84
|
+
paddingVertical: 12,
|
|
85
|
+
...shadow,
|
|
86
|
+
}, children: [(0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 0 }), (0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 150 }), (0, jsx_runtime_1.jsx)(Dot, { color: dotColor, delay: 300 })] }) }));
|
|
87
|
+
}
|
|
88
|
+
exports.default = TypingIndicator;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export interface VideoBubbleProps {
|
|
2
|
+
/** hosted URL (mediaUrl) or local/data URI for the video */
|
|
3
|
+
uri: string;
|
|
4
|
+
/** display name (used by the FileTile fallback) */
|
|
5
|
+
name?: string;
|
|
6
|
+
theme: any;
|
|
7
|
+
testID?: string;
|
|
8
|
+
}
|
|
9
|
+
export declare function VideoBubble({ uri, name, theme, testID }: VideoBubbleProps): import("react/jsx-runtime").JSX.Element;
|
|
10
|
+
export default VideoBubble;
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.VideoBubble = VideoBubble;
|
|
7
|
+
const jsx_runtime_1 = require("react/jsx-runtime");
|
|
8
|
+
// src/ui/VideoBubble.tsx
|
|
9
|
+
// Inline video player for a received/sent message file with a uri/mediaUrl.
|
|
10
|
+
//
|
|
11
|
+
// Drawn with `expo-video` (useVideoPlayer + <VideoView/>). The dep is OPTIONAL at
|
|
12
|
+
// runtime (chat-only consumers may not have the native side, and jest mocks it):
|
|
13
|
+
// we lazy-require it inside a guarded helper (mirroring Icon.tsx loadSvg +
|
|
14
|
+
// VideoTile.tsx). When it is absent — or fails to resolve under metro/jest — the
|
|
15
|
+
// bubble degrades to a <FileTile> download link instead of throwing at render.
|
|
16
|
+
//
|
|
17
|
+
// HOOK-SAFETY: `useVideoPlayer` is a React hook and MUST NOT be called
|
|
18
|
+
// conditionally. So this file never calls it directly. Instead it (a) resolves
|
|
19
|
+
// expo-video once at render; (b) when it is ABSENT renders <FileTile> (a path
|
|
20
|
+
// that calls no hook); (c) when it is PRESENT renders an inner <ExpoVideoPlayer>
|
|
21
|
+
// component that UNCONDITIONALLY calls useVideoPlayer. Because the present/absent
|
|
22
|
+
// decision picks a whole component (never a branch inside one component's body),
|
|
23
|
+
// the hook order in each component stays stable across renders.
|
|
24
|
+
const react_1 = __importDefault(require("react"));
|
|
25
|
+
const react_native_1 = require("react-native");
|
|
26
|
+
const FileTile_1 = require("./FileTile");
|
|
27
|
+
/**
|
|
28
|
+
* Lazily resolve `expo-video`. Returns `null` when the optional dep is not
|
|
29
|
+
* installed (or fails to resolve) so the caller renders the <FileTile> fallback
|
|
30
|
+
* instead of crashing. Mirrors Icon.tsx's loadSvg() / VideoTile's pattern.
|
|
31
|
+
*/
|
|
32
|
+
function loadExpoVideo() {
|
|
33
|
+
var _a;
|
|
34
|
+
try {
|
|
35
|
+
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
|
36
|
+
const ev = require("expo-video");
|
|
37
|
+
if (ev && ev.useVideoPlayer && ev.VideoView) {
|
|
38
|
+
return { useVideoPlayer: ev.useVideoPlayer, VideoView: ev.VideoView };
|
|
39
|
+
}
|
|
40
|
+
if (globalThis.__DEV__ === true) {
|
|
41
|
+
// eslint-disable-next-line no-console
|
|
42
|
+
console.log("[webchat adapters] expo-video loaded but missing useVideoPlayer/VideoView");
|
|
43
|
+
}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
catch (e) {
|
|
47
|
+
// DEV diagnostic: native expo-video absent from the running build (rebuild) -> link fallback.
|
|
48
|
+
if (globalThis.__DEV__ === true) {
|
|
49
|
+
// eslint-disable-next-line no-console
|
|
50
|
+
console.log(`[webchat adapters] require("expo-video") FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e} (native missing → rebuild)`);
|
|
51
|
+
}
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Inner player — only mounted when expo-video resolved, so the useVideoPlayer
|
|
57
|
+
* hook here is ALWAYS called (never conditionally). Sized like ImageBubble:
|
|
58
|
+
* full width up to 100%, a bounded height (~200), rounded, contain-fit.
|
|
59
|
+
*/
|
|
60
|
+
function ExpoVideoPlayer({ useVideoPlayer, VideoView, uri, name, theme, testID, }) {
|
|
61
|
+
var _a, _b;
|
|
62
|
+
const [failed, setFailed] = react_1.default.useState(false);
|
|
63
|
+
const player = useVideoPlayer(uri, (p) => {
|
|
64
|
+
p.loop = false;
|
|
65
|
+
});
|
|
66
|
+
// ASYNC playback failures (an unreachable / non-video URL, an unsupported codec,
|
|
67
|
+
// or a `data:` video URL iOS AVPlayer can't open) surface via the player's
|
|
68
|
+
// 'statusChange' event with status 'error' — they are NOT render throws, so the
|
|
69
|
+
// VideoErrorBoundary can't catch them and the bubble would otherwise sit as a dead
|
|
70
|
+
// empty box. Listen for the error status and degrade to a tappable <FileTile>.
|
|
71
|
+
react_1.default.useEffect(() => {
|
|
72
|
+
if (!(player === null || player === void 0 ? void 0 : player.addListener))
|
|
73
|
+
return;
|
|
74
|
+
const sub = player.addListener("statusChange", (payload) => {
|
|
75
|
+
if ((payload === null || payload === void 0 ? void 0 : payload.status) === "error")
|
|
76
|
+
setFailed(true);
|
|
77
|
+
});
|
|
78
|
+
return () => {
|
|
79
|
+
var _a;
|
|
80
|
+
try {
|
|
81
|
+
(_a = sub === null || sub === void 0 ? void 0 : sub.remove) === null || _a === void 0 ? void 0 : _a.call(sub);
|
|
82
|
+
}
|
|
83
|
+
catch { /* already removed */ }
|
|
84
|
+
};
|
|
85
|
+
}, [player]);
|
|
86
|
+
if (failed) {
|
|
87
|
+
return (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID });
|
|
88
|
+
}
|
|
89
|
+
return ((0, jsx_runtime_1.jsx)(react_native_1.View, { testID: testID, children: (0, jsx_runtime_1.jsx)(VideoView, { testID: testID ? `${testID}-view` : undefined, player: player, nativeControls: true, allowsFullscreen: true, contentFit: "contain", style: {
|
|
90
|
+
// EXPLICIT pixel dimensions (like ImageBubble's 200x150). The previous
|
|
91
|
+
// width:"100%" collapsed to ~0px because the wrapping <View> has NO defined
|
|
92
|
+
// width — a 0-width VideoView is mounted in the tree but invisible on screen
|
|
93
|
+
// (the exact symptom: React DevTools shows VideoView present, nothing renders).
|
|
94
|
+
width: 240,
|
|
95
|
+
height: 180,
|
|
96
|
+
borderRadius: 8,
|
|
97
|
+
backgroundColor: (_b = (_a = theme === null || theme === void 0 ? void 0 : theme.colors) === null || _a === void 0 ? void 0 : _a.surface) !== null && _b !== void 0 ? _b : "#000",
|
|
98
|
+
} }) }));
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Catches a render throw from <ExpoVideoPlayer>. expo-video's JS can be present
|
|
102
|
+
* (Metro bundled it) while the NATIVE module is absent — e.g. a dev client built
|
|
103
|
+
* BEFORE expo-video was bundled. In that case useVideoPlayer/VideoView throw at
|
|
104
|
+
* render; rather than a dead/empty tall box we degrade to the <FileTile> link.
|
|
105
|
+
* After a dev-client rebuild the native module is present and the player renders.
|
|
106
|
+
*/
|
|
107
|
+
class VideoErrorBoundary extends react_1.default.Component {
|
|
108
|
+
constructor() {
|
|
109
|
+
super(...arguments);
|
|
110
|
+
this.state = { failed: false };
|
|
111
|
+
}
|
|
112
|
+
static getDerivedStateFromError() {
|
|
113
|
+
return { failed: true };
|
|
114
|
+
}
|
|
115
|
+
render() {
|
|
116
|
+
return this.state.failed ? this.props.fallback : this.props.children;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function VideoBubble({ uri, name, theme, testID = "video-bubble" }) {
|
|
120
|
+
const ev = loadExpoVideo();
|
|
121
|
+
// Graceful fallback: no expo-video JS at all (chat-only consumer / test without
|
|
122
|
+
// the mock) -> a plain link tile (no native dep, no hook).
|
|
123
|
+
if (!ev) {
|
|
124
|
+
return (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID });
|
|
125
|
+
}
|
|
126
|
+
// expo-video JS is present but the NATIVE module may be missing (dev client not
|
|
127
|
+
// yet rebuilt) -> if the player throws, the boundary degrades to the link tile.
|
|
128
|
+
return ((0, jsx_runtime_1.jsx)(VideoErrorBoundary, { fallback: (0, jsx_runtime_1.jsx)(FileTile_1.FileTile, { uri: uri, name: name, theme: theme, testID: testID }), children: (0, jsx_runtime_1.jsx)(ExpoVideoPlayer, { useVideoPlayer: ev.useVideoPlayer, VideoView: ev.VideoView, uri: uri, name: name, theme: theme, testID: testID }) }));
|
|
129
|
+
}
|
|
130
|
+
exports.default = VideoBubble;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type VideoCallClient, type VideoCallClientOptions, type WebRTCFactory } from "../core/VideoCallClient";
|
|
2
|
+
import type { Dir } from "../theme/dir";
|
|
3
|
+
/** The audio-route surface the screen drives — `AudioRoute` satisfies it. */
|
|
4
|
+
export interface AudioRouteLike {
|
|
5
|
+
start(opts?: {
|
|
6
|
+
media?: "video" | "audio";
|
|
7
|
+
}): void;
|
|
8
|
+
stop(): void;
|
|
9
|
+
setSpeaker?(on: boolean): void;
|
|
10
|
+
}
|
|
11
|
+
export interface VideoCallProps {
|
|
12
|
+
/** existing chat socket (the call rides it; no second connection) */
|
|
13
|
+
socket: VideoCallClientOptions["socket"];
|
|
14
|
+
/** web-parity call scoping (C8): the active chat session id, sent on join */
|
|
15
|
+
getSessionId: () => string;
|
|
16
|
+
/** called once the call ends (leave / inbound hangup / chat-close S6) */
|
|
17
|
+
onEnd?: () => void;
|
|
18
|
+
/** WebRTC seam — defaults to the real react-native-webrtc factory */
|
|
19
|
+
webrtcFactory?: WebRTCFactory;
|
|
20
|
+
/** audio-route seam — defaults to the real react-native-incall-manager wrapper */
|
|
21
|
+
audioRoute?: AudioRouteLike;
|
|
22
|
+
/** client factory seam — defaults to the real createVideoCallClient */
|
|
23
|
+
createClient?: (opts: VideoCallClientOptions) => VideoCallClient;
|
|
24
|
+
/** layout direction (controls mirror under rtl) */
|
|
25
|
+
dir?: Dir;
|
|
26
|
+
/** resolved language — localizes the error copy + control labels (audit #8). */
|
|
27
|
+
language?: string;
|
|
28
|
+
/** agent display name — shown on the "connecting" placeholder before the remote joins */
|
|
29
|
+
agentName?: string;
|
|
30
|
+
/** agent avatar URL — shown on the "connecting" placeholder (falls back to initials) */
|
|
31
|
+
agentAvatarUrl?: string;
|
|
32
|
+
theme: any;
|
|
33
|
+
}
|
|
34
|
+
export declare function VideoCall({ socket, getSessionId, onEnd, webrtcFactory, audioRoute, createClient, dir, language, agentName, agentAvatarUrl, theme, }: VideoCallProps): import("react/jsx-runtime").JSX.Element;
|