@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,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 {};
|