@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,539 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/adapters/expoDefaults.ts
|
|
3
|
+
//
|
|
4
|
+
// BATTERIES-INCLUDED default adapters. These let `<WebChat config={...} />` work
|
|
5
|
+
// with ZERO host adapter code: when the matching Expo peer is installed they
|
|
6
|
+
// auto-wire image/document picking, file->base64 reads, and audio record/play;
|
|
7
|
+
// when the peer is ABSENT each factory returns `undefined` so the existing
|
|
8
|
+
// feature-detect/`?? ` gating degrades gracefully (attach row hidden, recorder
|
|
9
|
+
// suppressed, throwing-reader fallback) exactly as if no prop were passed.
|
|
10
|
+
//
|
|
11
|
+
// Every Expo lib is an OPTIONAL peer (see package.json peerDependenciesMeta) and
|
|
12
|
+
// is required LAZILY (inside a factory, never at module top-level) behind a
|
|
13
|
+
// try/catch — mirroring src/ui/Icon.tsx loadSvg() / src/adapters/webrtc.ts.
|
|
14
|
+
//
|
|
15
|
+
// CRITICAL: the require MUST use a STATIC LITERAL string — `require("expo-font")`,
|
|
16
|
+
// via `safeRequire(() => require("expo-font"), …)` — NOT a dynamic
|
|
17
|
+
// `require(variable)`. Metro only bundles static requires; a dynamic require throws
|
|
18
|
+
// "Dynamic require … not supported by Metro" AT RUNTIME (node/jest tolerate it, so
|
|
19
|
+
// unit tests won't catch it) and silently disables every adapter on-device. Because
|
|
20
|
+
// these are static, Metro bundles the named packages: a batteries-included build
|
|
21
|
+
// must have the Expo peers installed (the example + README do). At runtime a missing
|
|
22
|
+
// NATIVE module is caught here -> the factory returns undefined and the feature
|
|
23
|
+
// degrades gracefully (button hidden / system font / link fallback).
|
|
24
|
+
//
|
|
25
|
+
// Note (bare React Native, no Expo prebuild): these Expo modules require the
|
|
26
|
+
// `expo-modules-core` runtime. In an Expo-managed/dev-client app that is already
|
|
27
|
+
// present; in a bare RN app the consumer must run `npx install-expo-modules`
|
|
28
|
+
// once so the autolinked native side exists. See README / package.json comment.
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.createExpoPicker = createExpoPicker;
|
|
31
|
+
exports.createExpoReadBase64 = createExpoReadBase64;
|
|
32
|
+
exports.createExpoFontLoader = createExpoFontLoader;
|
|
33
|
+
exports.createExpoAudioAdapter = createExpoAudioAdapter;
|
|
34
|
+
/**
|
|
35
|
+
* Best-effort lazy require of an optional Expo peer. Returns `undefined` when the
|
|
36
|
+
* module is not installed (or fails to resolve) so callers feature-detect on the
|
|
37
|
+
* shape rather than crashing on a missing dependency. Mirrors Icon.tsx loadSvg().
|
|
38
|
+
*/
|
|
39
|
+
function safeRequire(load, label) {
|
|
40
|
+
var _a;
|
|
41
|
+
try {
|
|
42
|
+
// `load` MUST contain a STATIC literal require, e.g. () => require("expo-font").
|
|
43
|
+
// Metro only bundles static requires — a dynamic `require(variable)` throws
|
|
44
|
+
// "Dynamic require … not supported by Metro" at runtime, which is what silently
|
|
45
|
+
// disabled EVERY default adapter on-device (node/jest tolerate dynamic require,
|
|
46
|
+
// so the unit tests never caught it).
|
|
47
|
+
return load();
|
|
48
|
+
}
|
|
49
|
+
catch (e) {
|
|
50
|
+
// DEV diagnostic: surface WHY an optional peer didn't resolve (native module
|
|
51
|
+
// missing from the running build, or package not installed). Silent in release.
|
|
52
|
+
adapterLog(`require("${label}") FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}`);
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
/** DEV-only adapter-resolution diagnostic (prints to Metro; silent in release). */
|
|
57
|
+
function adapterLog(...args) {
|
|
58
|
+
if (globalThis.__DEV__ === true) {
|
|
59
|
+
// eslint-disable-next-line no-console
|
|
60
|
+
console.log("[webchat adapters]", ...args);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
// ---------------------------------------------------------------------------
|
|
64
|
+
// Picker (expo-image-picker + expo-document-picker) -> PickerAdapter
|
|
65
|
+
// ---------------------------------------------------------------------------
|
|
66
|
+
/** jpg|png|heic|… inferred from a URI/name tail, lower-cased, query-stripped. */
|
|
67
|
+
function extOf(s) {
|
|
68
|
+
const clean = s.split("?")[0];
|
|
69
|
+
const dot = clean.lastIndexOf(".");
|
|
70
|
+
return dot >= 0 ? clean.slice(dot + 1).toLowerCase() : "";
|
|
71
|
+
}
|
|
72
|
+
/** Infer an image/video mime from a file extension, defaulting to image/jpeg. */
|
|
73
|
+
function imageMimeFromExt(ext) {
|
|
74
|
+
if (ext === "png")
|
|
75
|
+
return "image/png";
|
|
76
|
+
if (ext === "heic" || ext === "heif")
|
|
77
|
+
return "image/heic";
|
|
78
|
+
if (ext === "mp4")
|
|
79
|
+
return "video/mp4";
|
|
80
|
+
if (ext === "m4v")
|
|
81
|
+
return "video/x-m4v";
|
|
82
|
+
return "image/jpeg";
|
|
83
|
+
}
|
|
84
|
+
/**
|
|
85
|
+
* Normalize one `expo-image-picker` asset to the SDK {@link Asset}.
|
|
86
|
+
*
|
|
87
|
+
* expo-image-picker field -> SDK Asset:
|
|
88
|
+
* uri -> uri
|
|
89
|
+
* fileName -> name (synthesized `image_<ts>.<ext>` when null — common on Android/camera-roll)
|
|
90
|
+
* mimeType -> mimeType (inferred from the uri extension when null; `type:'video'` biases mp4)
|
|
91
|
+
* fileSize -> size (0 fallback; the encode-time cap re-checks the real bytes)
|
|
92
|
+
* base64 -> base64 (present because we request `base64:true`, so readBase64 is skipped)
|
|
93
|
+
*/
|
|
94
|
+
function normalizeImageAsset(a) {
|
|
95
|
+
var _a, _b, _c, _d, _e, _f;
|
|
96
|
+
const ext = extOf((_a = a === null || a === void 0 ? void 0 : a.uri) !== null && _a !== void 0 ? _a : "") || ((a === null || a === void 0 ? void 0 : a.type) === "video" ? "mp4" : "jpg");
|
|
97
|
+
const mimeType = (_b = a === null || a === void 0 ? void 0 : a.mimeType) !== null && _b !== void 0 ? _b : ((a === null || a === void 0 ? void 0 : a.type) === "video" ? imageMimeFromExt(ext === "jpg" ? "mp4" : ext) : imageMimeFromExt(ext));
|
|
98
|
+
return {
|
|
99
|
+
uri: (_c = a === null || a === void 0 ? void 0 : a.uri) !== null && _c !== void 0 ? _c : "",
|
|
100
|
+
name: (_d = a === null || a === void 0 ? void 0 : a.fileName) !== null && _d !== void 0 ? _d : `image_${Date.now()}.${ext}`,
|
|
101
|
+
mimeType,
|
|
102
|
+
size: (_e = a === null || a === void 0 ? void 0 : a.fileSize) !== null && _e !== void 0 ? _e : 0,
|
|
103
|
+
base64: (_f = a === null || a === void 0 ? void 0 : a.base64) !== null && _f !== void 0 ? _f : undefined,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/**
|
|
107
|
+
* Normalize one `expo-document-picker` asset to the SDK {@link Asset}.
|
|
108
|
+
*
|
|
109
|
+
* expo-document-picker field -> SDK Asset:
|
|
110
|
+
* uri -> uri (a file:// cache uri because we pass copyToCacheDirectory:true)
|
|
111
|
+
* name -> name (synthesized when null)
|
|
112
|
+
* mimeType -> mimeType (pdf inferred from ext; else application/octet-stream — which
|
|
113
|
+
* validateSelection rejects as unsupported-type, the desired safe default)
|
|
114
|
+
* size -> size (0 fallback)
|
|
115
|
+
* (no base64 — documents carry no eager payload, so readBase64(uri) supplies it)
|
|
116
|
+
*/
|
|
117
|
+
function normalizeDocumentAsset(a) {
|
|
118
|
+
var _a, _b, _c, _d, _e, _f;
|
|
119
|
+
const ext = extOf((_a = a === null || a === void 0 ? void 0 : a.name) !== null && _a !== void 0 ? _a : "") || extOf((_b = a === null || a === void 0 ? void 0 : a.uri) !== null && _b !== void 0 ? _b : "");
|
|
120
|
+
return {
|
|
121
|
+
uri: (_c = a === null || a === void 0 ? void 0 : a.uri) !== null && _c !== void 0 ? _c : "",
|
|
122
|
+
name: (_d = a === null || a === void 0 ? void 0 : a.name) !== null && _d !== void 0 ? _d : `file_${Date.now()}${ext ? "." + ext : ""}`,
|
|
123
|
+
mimeType: (_e = a === null || a === void 0 ? void 0 : a.mimeType) !== null && _e !== void 0 ? _e : (ext === "pdf" ? "application/pdf" : "application/octet-stream"),
|
|
124
|
+
size: (_f = a === null || a === void 0 ? void 0 : a.size) !== null && _f !== void 0 ? _f : 0,
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Default {@link PickerAdapter} backed by Expo. Returns `undefined` when NEITHER
|
|
129
|
+
* expo-image-picker nor expo-document-picker is installed — so the attach button
|
|
130
|
+
* stays hidden (feature-detect parity with the absent-prop case). When only one
|
|
131
|
+
* lib is present the adapter exposes only that method (a partial picker is valid;
|
|
132
|
+
* the Surface omits the missing row).
|
|
133
|
+
*/
|
|
134
|
+
function createExpoPicker() {
|
|
135
|
+
const ImagePicker = safeRequire(() => require("expo-image-picker"), "expo-image-picker");
|
|
136
|
+
const DocumentPicker = safeRequire(() => require("expo-document-picker"), "expo-document-picker");
|
|
137
|
+
// SDK 56's modern file picker (expo-file-system File.pickFileAsync). It is the
|
|
138
|
+
// New-Architecture-native replacement for expo-document-picker, whose
|
|
139
|
+
// getDocumentAsync can HANG on the New Arch (presents nothing, never resolves).
|
|
140
|
+
// Preferred for documents when present; getDocumentAsync is the fallback.
|
|
141
|
+
const FS = safeRequire(() => require("expo-file-system"), "expo-file-system");
|
|
142
|
+
const FilePickCtor = FS === null || FS === void 0 ? void 0 : FS.File;
|
|
143
|
+
const hasFilePick = typeof (FilePickCtor === null || FilePickCtor === void 0 ? void 0 : FilePickCtor.pickFileAsync) === "function";
|
|
144
|
+
const hasImages = typeof (ImagePicker === null || ImagePicker === void 0 ? void 0 : ImagePicker.launchImageLibraryAsync) === "function";
|
|
145
|
+
const hasDocs = typeof (DocumentPicker === null || DocumentPicker === void 0 ? void 0 : DocumentPicker.getDocumentAsync) === "function";
|
|
146
|
+
adapterLog("picker — expo-image-picker:", hasImages ? "READY" : ImagePicker ? "loaded but no launchImageLibraryAsync" : "NOT AVAILABLE (native missing → rebuild)", "| documents:", hasFilePick ? "READY (File.pickFileAsync)" : hasDocs ? "READY (getDocumentAsync)" : "NOT AVAILABLE (native missing → rebuild)");
|
|
147
|
+
if (!hasImages && !hasDocs && !hasFilePick)
|
|
148
|
+
return undefined;
|
|
149
|
+
const adapter = {};
|
|
150
|
+
if (hasImages) {
|
|
151
|
+
adapter.pickImages = async (opts) => {
|
|
152
|
+
var _a, _b, _c;
|
|
153
|
+
// Permission: a denial is treated as a cancel ([] per contract), never a throw.
|
|
154
|
+
const req = ImagePicker.requestMediaLibraryPermissionsAsync;
|
|
155
|
+
if (typeof req === "function") {
|
|
156
|
+
const perm = await req();
|
|
157
|
+
if (perm && perm.granted === false)
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
// mediaTypes: pass the SDK 51+ string-array form directly. Do NOT reference
|
|
161
|
+
// ImagePicker.MediaTypeOptions — passing its enum member fires the SDK 52+
|
|
162
|
+
// deprecation warning (parseMediaTypes warns on `=== a MediaTypeOptions value`).
|
|
163
|
+
const mediaTypes = ["images"];
|
|
164
|
+
let result;
|
|
165
|
+
try {
|
|
166
|
+
result = await ImagePicker.launchImageLibraryAsync({
|
|
167
|
+
mediaTypes,
|
|
168
|
+
allowsMultipleSelection: (_a = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _a !== void 0 ? _a : true,
|
|
169
|
+
base64: true, // eager base64 => toMediaEntry short-circuits readBase64 for images
|
|
170
|
+
quality: (_b = opts === null || opts === void 0 ? void 0 : opts.quality) !== null && _b !== void 0 ? _b : 0.8,
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
// A throw is almost always the NATIVE module missing from the running dev
|
|
175
|
+
// client (the JS exists so this method is wired, but the native call fails).
|
|
176
|
+
// Surface it (the Composer's stagePick otherwise swallows it -> the picker
|
|
177
|
+
// silently never opens) and treat as a cancel. Fix: rebuild the dev client.
|
|
178
|
+
adapterLog(`pickImages FAILED — ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
|
|
179
|
+
return [];
|
|
180
|
+
}
|
|
181
|
+
// Modern API: { canceled: boolean; assets }. `canceled` is spelled with one L.
|
|
182
|
+
if (result === null || result === void 0 ? void 0 : result.canceled)
|
|
183
|
+
return [];
|
|
184
|
+
const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets) ? result.assets : [];
|
|
185
|
+
return assets.map(normalizeImageAsset);
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
if (hasImages) {
|
|
189
|
+
adapter.pickVideos = async (opts) => {
|
|
190
|
+
var _a, _b, _c;
|
|
191
|
+
// Permission: a denial is treated as a cancel ([] per contract), never a throw.
|
|
192
|
+
const req = ImagePicker.requestMediaLibraryPermissionsAsync;
|
|
193
|
+
if (typeof req === "function") {
|
|
194
|
+
const perm = await req();
|
|
195
|
+
if (perm && perm.granted === false)
|
|
196
|
+
return [];
|
|
197
|
+
}
|
|
198
|
+
// mediaTypes: pass the string-array form directly (warning-free on SDK 56).
|
|
199
|
+
const mediaTypes = ["videos"];
|
|
200
|
+
let result;
|
|
201
|
+
try {
|
|
202
|
+
result = await ImagePicker.launchImageLibraryAsync({
|
|
203
|
+
mediaTypes,
|
|
204
|
+
allowsMultipleSelection: (_a = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _a !== void 0 ? _a : true,
|
|
205
|
+
// Videos can be large; do NOT request eager base64 (it would inflate the
|
|
206
|
+
// payload in memory). readBase64(uri) supplies the payload at encode time,
|
|
207
|
+
// exactly like documents. `type:'video'` biases normalizeImageAsset to mp4.
|
|
208
|
+
quality: (_b = opts === null || opts === void 0 ? void 0 : opts.quality) !== null && _b !== void 0 ? _b : 0.8,
|
|
209
|
+
});
|
|
210
|
+
}
|
|
211
|
+
catch (e) {
|
|
212
|
+
adapterLog(`pickVideos FAILED — ${(_c = e === null || e === void 0 ? void 0 : e.message) !== null && _c !== void 0 ? _c : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
|
|
213
|
+
return [];
|
|
214
|
+
}
|
|
215
|
+
// Modern API: { canceled: boolean; assets }. `canceled` is spelled with one L.
|
|
216
|
+
if (result === null || result === void 0 ? void 0 : result.canceled)
|
|
217
|
+
return [];
|
|
218
|
+
const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets) ? result.assets : [];
|
|
219
|
+
return assets.map(normalizeImageAsset);
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
if (hasDocs || hasFilePick) {
|
|
223
|
+
adapter.pickDocuments = async (opts) => {
|
|
224
|
+
var _a, _b, _c, _d;
|
|
225
|
+
// PREFERRED — expo-file-system File.pickFileAsync (SDK 56, New-Arch native).
|
|
226
|
+
// expo-document-picker's getDocumentAsync HANGS on the New Architecture (the OS
|
|
227
|
+
// sheet never presents and the promise never resolves), so try the modern picker
|
|
228
|
+
// first and only fall through to getDocumentAsync if it is unavailable/throws.
|
|
229
|
+
if (hasFilePick) {
|
|
230
|
+
try {
|
|
231
|
+
const res = await FilePickCtor.pickFileAsync((opts === null || opts === void 0 ? void 0 : opts.allowsMultiple)
|
|
232
|
+
? { multipleFiles: true, ...((opts === null || opts === void 0 ? void 0 : opts.mimeTypes) ? { mimeTypes: opts.mimeTypes } : {}) }
|
|
233
|
+
: { ...((opts === null || opts === void 0 ? void 0 : opts.mimeTypes) ? { mimeTypes: opts.mimeTypes } : {}) });
|
|
234
|
+
if (!res || res.canceled)
|
|
235
|
+
return [];
|
|
236
|
+
const files = Array.isArray(res.result)
|
|
237
|
+
? res.result
|
|
238
|
+
: res.result
|
|
239
|
+
? [res.result]
|
|
240
|
+
: [];
|
|
241
|
+
// File implements Blob: uri/name/size + `type` (MIME) → normalize to Asset.
|
|
242
|
+
// `type` may be an EMPTY string (Blob.type unknown); coerce to undefined so
|
|
243
|
+
// normalizeDocumentAsset's extension fallback resolves it (.pdf → application/pdf)
|
|
244
|
+
// — otherwise "" slips past its `?? ` and validateSelection rejects a valid PDF.
|
|
245
|
+
return files.map((f) => normalizeDocumentAsset({
|
|
246
|
+
uri: f === null || f === void 0 ? void 0 : f.uri,
|
|
247
|
+
name: f === null || f === void 0 ? void 0 : f.name,
|
|
248
|
+
mimeType: (typeof (f === null || f === void 0 ? void 0 : f.type) === "string" && f.type) || undefined,
|
|
249
|
+
size: f === null || f === void 0 ? void 0 : f.size,
|
|
250
|
+
}));
|
|
251
|
+
}
|
|
252
|
+
catch (e) {
|
|
253
|
+
adapterLog(`pickDocuments: File.pickFileAsync FAILED — ${(_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : e}${hasDocs ? " — falling back to getDocumentAsync" : ""}`);
|
|
254
|
+
if (!hasDocs)
|
|
255
|
+
return [];
|
|
256
|
+
// fall through to getDocumentAsync
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
let result;
|
|
260
|
+
try {
|
|
261
|
+
result = await DocumentPicker.getDocumentAsync({
|
|
262
|
+
// expo-document-picker accepts a string[] of MIME types directly. iOS maps
|
|
263
|
+
// them to UTIs; Android honors them as EXTRA_MIME_TYPES. Non-standard mimes
|
|
264
|
+
// a chooser ignores are re-enforced post-pick by validateSelection.
|
|
265
|
+
type: (_b = opts === null || opts === void 0 ? void 0 : opts.mimeTypes) !== null && _b !== void 0 ? _b : ["*/*"],
|
|
266
|
+
multiple: (_c = opts === null || opts === void 0 ? void 0 : opts.allowsMultiple) !== null && _c !== void 0 ? _c : false,
|
|
267
|
+
// REQUIRED: yields a stable file:// cache uri readBase64 can read (a raw
|
|
268
|
+
// content:// uri can be revoked before the read runs).
|
|
269
|
+
copyToCacheDirectory: true,
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
catch (e) {
|
|
273
|
+
// This method only exists when getDocumentAsync IS present (hasDocs), so a throw
|
|
274
|
+
// here is a RUNTIME error, NOT a missing module — most often "Different document
|
|
275
|
+
// picking in progress" (a concurrent launch; now guarded by Composer.stagePick),
|
|
276
|
+
// or a permissions/UTI issue. Only if the OS sheet NEVER opens at all is the
|
|
277
|
+
// native module missing → add "expo-document-picker" to plugins and REBUILD.
|
|
278
|
+
adapterLog(`pickDocuments FAILED — ${(_d = e === null || e === void 0 ? void 0 : e.message) !== null && _d !== void 0 ? _d : e} (runtime pick error; if the OS sheet NEVER opens, the native module is missing → rebuild)`);
|
|
279
|
+
return [];
|
|
280
|
+
}
|
|
281
|
+
// Modern API: { canceled: true } on cancel; legacy <=48: { type: 'cancel' }.
|
|
282
|
+
if ((result === null || result === void 0 ? void 0 : result.canceled) || (result === null || result === void 0 ? void 0 : result.type) === "cancel")
|
|
283
|
+
return [];
|
|
284
|
+
// Modern: assets[]; legacy success returned a flat { uri, name, size, mimeType }.
|
|
285
|
+
const assets = Array.isArray(result === null || result === void 0 ? void 0 : result.assets)
|
|
286
|
+
? result.assets
|
|
287
|
+
: (result === null || result === void 0 ? void 0 : result.uri)
|
|
288
|
+
? [result]
|
|
289
|
+
: [];
|
|
290
|
+
return assets.map(normalizeDocumentAsset);
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
return adapter;
|
|
294
|
+
}
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// readBase64 (expo-file-system) -> (uri) => Promise<string> (RAW base64)
|
|
297
|
+
// ---------------------------------------------------------------------------
|
|
298
|
+
/**
|
|
299
|
+
* Default file->base64 reader backed by `expo-file-system`. Returns RAW base64
|
|
300
|
+
* (no `data:` prefix) — `toDataUrl` adds the prefix downstream. Only invoked for
|
|
301
|
+
* assets WITHOUT eager base64 (documents, recorded audio); image-picker assets
|
|
302
|
+
* carry `base64` and short-circuit it.
|
|
303
|
+
*
|
|
304
|
+
* SDK drift: SDK 54 promoted the OO `File`/`Directory` API to the package root and
|
|
305
|
+
* DEPRECATED the classic functions. On SDK 56 the root `readAsStringAsync` is a STUB
|
|
306
|
+
* that THROWS at runtime ("This method will throw in runtime"), so we MUST prefer the
|
|
307
|
+
* OO `new File(uri).base64()` and only fall back to `readAsStringAsync` on OLDER SDKs
|
|
308
|
+
* (<=53) where the `File` class doesn't exist. Returns `undefined` when the lib is
|
|
309
|
+
* absent (provider's throwing reader still only fires if an attach without eager
|
|
310
|
+
* base64 is actually attempted).
|
|
311
|
+
*/
|
|
312
|
+
function createExpoReadBase64() {
|
|
313
|
+
var _a, _b;
|
|
314
|
+
// Static require of the ROOT package only. `expo-file-system/legacy` is
|
|
315
|
+
// intentionally NOT required: a static require of a possibly-absent subpath would
|
|
316
|
+
// FAIL the Metro bundle. The SDK-56 root exposes the new File API (preferred) and
|
|
317
|
+
// a throwing `readAsStringAsync` stub (legacy fallback only) — both handled below.
|
|
318
|
+
const root = safeRequire(() => require("expo-file-system"), "expo-file-system");
|
|
319
|
+
// SDK 54+ OO API FIRST: `new File(uri).base64()` -> Promise<string> (RAW base64).
|
|
320
|
+
// CRITICAL: this MUST come before readAsStringAsync. On SDK 56 the root
|
|
321
|
+
// `readAsStringAsync` is a DEPRECATED STUB that throws at runtime, so preferring it
|
|
322
|
+
// (the previous order) made every NON-image attach fail at encode — recorded audio,
|
|
323
|
+
// picked video, and documents have no eager base64, so they hit this reader and
|
|
324
|
+
// surfaced as WebChatError("send", "media encode failed") -> "[webchat] error send".
|
|
325
|
+
// The File API is the only working root path on current SDKs.
|
|
326
|
+
const FileCtor = root === null || root === void 0 ? void 0 : root.File;
|
|
327
|
+
if (typeof FileCtor === "function") {
|
|
328
|
+
return async (uri) => {
|
|
329
|
+
const out = new FileCtor(uri).base64();
|
|
330
|
+
return typeof (out === null || out === void 0 ? void 0 : out.then) === "function" ? await out : out;
|
|
331
|
+
};
|
|
332
|
+
}
|
|
333
|
+
// Legacy SDKs (<=53): classic readAsStringAsync at the root (works there; on SDK 56
|
|
334
|
+
// it would throw, but on <=53 there is no File class so this is the only path).
|
|
335
|
+
if (typeof (root === null || root === void 0 ? void 0 : root.readAsStringAsync) === "function") {
|
|
336
|
+
const encoding = (_b = (_a = root.EncodingType) === null || _a === void 0 ? void 0 : _a.Base64) !== null && _b !== void 0 ? _b : "base64";
|
|
337
|
+
return (uri) => root.readAsStringAsync(uri, { encoding });
|
|
338
|
+
}
|
|
339
|
+
return undefined;
|
|
340
|
+
}
|
|
341
|
+
/**
|
|
342
|
+
* Stable RN family name for one tenant font entry. Web uses a single CSS family
|
|
343
|
+
* with per-weight `@font-face`; RN cannot share one family across weights via
|
|
344
|
+
* `loadAsync`, so each weight is registered as its OWN family. The name is
|
|
345
|
+
* derived from the entry's `fontName` (preferred — already weight-specific like
|
|
346
|
+
* "Tajawal-Bold") else its `fontWeight`, sanitized to `[A-Za-z0-9_-]`, e.g.
|
|
347
|
+
* `webchat-Tajawal-Bold` or `webchat-700`. Returns `undefined` when neither
|
|
348
|
+
* `fontName` nor `fontWeight` is present (entry is unkeyable -> skipped).
|
|
349
|
+
*/
|
|
350
|
+
function brandFontFamilyName(font) {
|
|
351
|
+
var _a;
|
|
352
|
+
const raw = (_a = font === null || font === void 0 ? void 0 : font.fontName) !== null && _a !== void 0 ? _a : font === null || font === void 0 ? void 0 : font.fontWeight;
|
|
353
|
+
if (raw == null || String(raw).trim() === "")
|
|
354
|
+
return undefined;
|
|
355
|
+
const safe = String(raw).trim().replace(/[^A-Za-z0-9_-]/g, "-");
|
|
356
|
+
return `webchat-${safe}`;
|
|
357
|
+
}
|
|
358
|
+
/**
|
|
359
|
+
* Default brand-font loader backed by `expo-font`. Returns `undefined` (no-op)
|
|
360
|
+
* when expo-font is absent — so Metro never pulls the native module into the
|
|
361
|
+
* import graph and the jest suite (where it is not installed) keeps working,
|
|
362
|
+
* exactly like the other expo defaults.
|
|
363
|
+
*
|
|
364
|
+
* When present it returns `loadBrandFonts(fontFamily)`, an async loader that:
|
|
365
|
+
* - maps each provided font entry to a stable RN family name (`webchat-{…}`),
|
|
366
|
+
* - calls `Font.loadAsync({ [familyName]: url })` for each (in parallel via
|
|
367
|
+
* Promise.all), TOLERATING individual failures (a bad url drops just that
|
|
368
|
+
* weight, never rejects the whole batch),
|
|
369
|
+
* - RESOLVES to a `weight -> familyName` map for the apply phase. The map is
|
|
370
|
+
* keyed by each entry's `fontWeight` (so `resolveFontFamily(map, weight)`
|
|
371
|
+
* can look a weight up); entries without a `fontWeight` are still loaded but
|
|
372
|
+
* are only reachable via the family-level `name` key (added when present).
|
|
373
|
+
*
|
|
374
|
+
* An empty/undefined `fonts` array resolves to `{}` (no fonts loaded).
|
|
375
|
+
*/
|
|
376
|
+
function createExpoFontLoader() {
|
|
377
|
+
const Font = safeRequire(() => require("expo-font"), "expo-font");
|
|
378
|
+
if (!Font || typeof Font.loadAsync !== "function")
|
|
379
|
+
return undefined;
|
|
380
|
+
return async (fontFamily) => {
|
|
381
|
+
const fonts = Array.isArray(fontFamily === null || fontFamily === void 0 ? void 0 : fontFamily.fonts) ? fontFamily.fonts : [];
|
|
382
|
+
const map = {};
|
|
383
|
+
if (fonts.length === 0)
|
|
384
|
+
return map;
|
|
385
|
+
await Promise.all(fonts.map(async (f) => {
|
|
386
|
+
const url = f === null || f === void 0 ? void 0 : f.url;
|
|
387
|
+
const familyName = brandFontFamilyName(f);
|
|
388
|
+
// Skip unkeyable (no fontName/fontWeight) or sourceless entries.
|
|
389
|
+
if (!url || !familyName)
|
|
390
|
+
return;
|
|
391
|
+
try {
|
|
392
|
+
await Font.loadAsync({ [familyName]: url });
|
|
393
|
+
}
|
|
394
|
+
catch {
|
|
395
|
+
// Tolerate a single weight failing to load (bad url, network, decode):
|
|
396
|
+
// the rest of the batch still resolves and that weight just falls back
|
|
397
|
+
// to the system font via resolveFontFamily returning undefined.
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// Only expose successfully-loaded families in the returned map.
|
|
401
|
+
if ((f === null || f === void 0 ? void 0 : f.fontWeight) != null && String(f.fontWeight).trim() !== "") {
|
|
402
|
+
map[String(f.fontWeight)] = familyName;
|
|
403
|
+
}
|
|
404
|
+
// Family-level alias (web `name`) maps to the first loaded family so the
|
|
405
|
+
// apply phase can resolve "the brand font" without a specific weight.
|
|
406
|
+
if ((fontFamily === null || fontFamily === void 0 ? void 0 : fontFamily.name) && !map[fontFamily.name]) {
|
|
407
|
+
map[fontFamily.name] = familyName;
|
|
408
|
+
}
|
|
409
|
+
}));
|
|
410
|
+
return map;
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
// ---------------------------------------------------------------------------
|
|
414
|
+
// Audio playback (expo-audio) -> AudioAdapter (PLAYBACK-CAPABLE default)
|
|
415
|
+
// ---------------------------------------------------------------------------
|
|
416
|
+
// B6 CODEC / ENGINE FLAG — READ BEFORE CHANGING.
|
|
417
|
+
// ------------------------------------------------------------------------
|
|
418
|
+
// The audio engine is `expo-audio` (the dead `expo-av` was removed: 16.x does
|
|
419
|
+
// not build on Expo SDK 56). expo-audio splits its surface in two:
|
|
420
|
+
//
|
|
421
|
+
// • PLAYBACK is IMPERATIVE -> createAudioPlayer(uri) | player.play() |
|
|
422
|
+
// player.pause() | player.remove(). This fits the AudioAdapter object shape,
|
|
423
|
+
// so the DEFAULT adapter implements play()/stop() here.
|
|
424
|
+
// • RECORDING is HOOK-ONLY -> useAudioRecorder(RecordingPresets.HIGH_QUALITY)
|
|
425
|
+
// can ONLY be called from inside a React component (Rules of Hooks). It does
|
|
426
|
+
// NOT fit a plain object method. So the default adapter does NOT record: it
|
|
427
|
+
// delegates recording to the <AudioRecorder> component path (Phase 2), which
|
|
428
|
+
// drives the hook and produces a RecordingResult through the same contract.
|
|
429
|
+
// Here startRecording()/stopRecording() are inert no-op stubs (they keep the
|
|
430
|
+
// AudioAdapter type satisfied) and MUST NOT be used to capture a note.
|
|
431
|
+
//
|
|
432
|
+
// CODEC: expo-audio's HIGH_QUALITY preset records MPEG-4 / AAC (.m4a) on BOTH iOS
|
|
433
|
+
// and Android — it CANNOT produce Ogg/Opus (the SDK's web-parity CANONICAL_AUDIO_MIME).
|
|
434
|
+
// The recorder component (Phase 2) HONESTLY returns the real container mime; we do
|
|
435
|
+
// NOT mislabel m4a bytes as audio/ogg. For the voice note to pass validateSelection,
|
|
436
|
+
// "audio/mp4" / "audio/aac" / "audio/m4a" are in src/core/media.ts ALLOW_LIST.
|
|
437
|
+
//
|
|
438
|
+
// ⚠️ OPEN DEPENDENCY (B6): the WEB widget only ever sent audio/ogg;codecs=opus.
|
|
439
|
+
// Backend/infra acceptance of m4a/aac voice notes MUST BE CONFIRMED. If the backend
|
|
440
|
+
// rejects them, substitute an Opus-capable recorder behind this same AudioAdapter
|
|
441
|
+
// (expo-audio alone cannot do Opus) so mimeType genuinely equals CANONICAL_AUDIO_MIME.
|
|
442
|
+
// Do NOT "solve" this by lying about the mime.
|
|
443
|
+
const RECORDED_AUDIO_MIME = "audio/mp4"; // .m4a / AAC container (iOS + Android)
|
|
444
|
+
/**
|
|
445
|
+
* Default PLAYBACK-CAPABLE {@link AudioAdapter} backed by `expo-audio`. Returns
|
|
446
|
+
* `undefined` when expo-audio is absent (received-voice falls back to a download
|
|
447
|
+
* tile — parity with the absent-prop case).
|
|
448
|
+
*
|
|
449
|
+
* PLAYBACK (imperative, fully implemented here):
|
|
450
|
+
* play(uri) -> createAudioPlayer(uri); player.play(); retain the player.
|
|
451
|
+
* stop() -> retained player.pause() + player.remove() (MUST remove() to free
|
|
452
|
+
* native resources); null-safe; a single active player is retained,
|
|
453
|
+
* so a new play() removes any prior one first.
|
|
454
|
+
*
|
|
455
|
+
* RECORDING (delegated to Phase 2 component): startRecording()/stopRecording() are
|
|
456
|
+
* inert stubs that keep the AudioAdapter type satisfied. expo-audio's recorder is
|
|
457
|
+
* HOOK-ONLY (useAudioRecorder), so the real capture lives in the <AudioRecorder>
|
|
458
|
+
* component (Phase 2) which drives the hook and yields a RecordingResult tagged with
|
|
459
|
+
* the m4a container mime above. The default object adapter cannot call a hook.
|
|
460
|
+
*/
|
|
461
|
+
function createExpoAudioAdapter() {
|
|
462
|
+
const audio = safeRequire(() => require("expo-audio"), "expo-audio");
|
|
463
|
+
adapterLog("audio — expo-audio:", audio ? (typeof audio.createAudioPlayer === "function" ? "READY" : "loaded but no createAudioPlayer") : "NOT AVAILABLE (native missing → rebuild)");
|
|
464
|
+
if (!audio || typeof audio.createAudioPlayer !== "function")
|
|
465
|
+
return undefined;
|
|
466
|
+
// Single retained active player (imperative). play() replaces any prior one;
|
|
467
|
+
// stop() pauses + removes it to free native resources, then clears the ref.
|
|
468
|
+
let player = null;
|
|
469
|
+
// Subscription to expo-audio's "playbackStatusUpdate" (position/duration/finish);
|
|
470
|
+
// removed alongside the player so no stale listener fires after stop/replace.
|
|
471
|
+
let statusSub = null;
|
|
472
|
+
function releasePlayer() {
|
|
473
|
+
var _a, _b, _c;
|
|
474
|
+
try {
|
|
475
|
+
(_a = statusSub === null || statusSub === void 0 ? void 0 : statusSub.remove) === null || _a === void 0 ? void 0 : _a.call(statusSub);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
/* best-effort */
|
|
479
|
+
}
|
|
480
|
+
statusSub = null;
|
|
481
|
+
if (!player)
|
|
482
|
+
return;
|
|
483
|
+
try {
|
|
484
|
+
(_b = player.pause) === null || _b === void 0 ? void 0 : _b.call(player);
|
|
485
|
+
}
|
|
486
|
+
catch {
|
|
487
|
+
/* best-effort: never throw out of release */
|
|
488
|
+
}
|
|
489
|
+
try {
|
|
490
|
+
(_c = player.remove) === null || _c === void 0 ? void 0 : _c.call(player); // REQUIRED to free expo-audio native resources
|
|
491
|
+
}
|
|
492
|
+
catch {
|
|
493
|
+
/* best-effort */
|
|
494
|
+
}
|
|
495
|
+
player = null;
|
|
496
|
+
}
|
|
497
|
+
return {
|
|
498
|
+
// RECORDING is provided by the Phase 2 <AudioRecorder> component (expo-audio
|
|
499
|
+
// useAudioRecorder hook), NOT by this object adapter — these stubs only keep
|
|
500
|
+
// the AudioAdapter type satisfied for the playback-capable default. The flag
|
|
501
|
+
// below tells <AudioRecorder> to drive the hook directly instead of calling
|
|
502
|
+
// these inert stubs (a host-injected imperative recorder leaves it unset and
|
|
503
|
+
// its real startRecording/stopRecording are used as-is).
|
|
504
|
+
recordingDelegated: true,
|
|
505
|
+
async startRecording() {
|
|
506
|
+
/* no-op: default recording is delegated to the AudioRecorder component */
|
|
507
|
+
},
|
|
508
|
+
async stopRecording() {
|
|
509
|
+
// Inert result, tagged with the container mime the Phase 2 recorder emits.
|
|
510
|
+
return { uri: "", mimeType: RECORDED_AUDIO_MIME, durationMs: 0 };
|
|
511
|
+
},
|
|
512
|
+
async play(uri, opts) {
|
|
513
|
+
var _a;
|
|
514
|
+
// Replace any currently-retained player so only one note plays at a time.
|
|
515
|
+
releasePlayer();
|
|
516
|
+
const p = audio.createAudioPlayer(uri);
|
|
517
|
+
player = p;
|
|
518
|
+
// Live progress: subscribe to expo-audio's status events so the player UI can
|
|
519
|
+
// show "current / total" and reset on finish. currentTime/duration are SECONDS
|
|
520
|
+
// in expo-audio → convert to ms. Guarded so engines without addListener degrade
|
|
521
|
+
// gracefully (no progress; the UI keeps showing the recorded total).
|
|
522
|
+
const onStatus = opts === null || opts === void 0 ? void 0 : opts.onStatus;
|
|
523
|
+
if (onStatus && typeof (p === null || p === void 0 ? void 0 : p.addListener) === "function") {
|
|
524
|
+
statusSub = p.addListener("playbackStatusUpdate", (st) => {
|
|
525
|
+
var _a, _b, _c, _d, _e, _f, _g;
|
|
526
|
+
const durationMs = Math.max(0, Math.round(((_b = (_a = st === null || st === void 0 ? void 0 : st.duration) !== null && _a !== void 0 ? _a : p === null || p === void 0 ? void 0 : p.duration) !== null && _b !== void 0 ? _b : 0) * 1000));
|
|
527
|
+
const positionMs = Math.max(0, Math.round(((_d = (_c = st === null || st === void 0 ? void 0 : st.currentTime) !== null && _c !== void 0 ? _c : p === null || p === void 0 ? void 0 : p.currentTime) !== null && _d !== void 0 ? _d : 0) * 1000));
|
|
528
|
+
const isPlaying = (_f = (_e = st === null || st === void 0 ? void 0 : st.playing) !== null && _e !== void 0 ? _e : p === null || p === void 0 ? void 0 : p.playing) !== null && _f !== void 0 ? _f : true;
|
|
529
|
+
const didJustFinish = (_g = st === null || st === void 0 ? void 0 : st.didJustFinish) !== null && _g !== void 0 ? _g : (durationMs > 0 && positionMs >= durationMs);
|
|
530
|
+
onStatus({ positionMs, durationMs, isPlaying, didJustFinish });
|
|
531
|
+
});
|
|
532
|
+
}
|
|
533
|
+
(_a = p === null || p === void 0 ? void 0 : p.play) === null || _a === void 0 ? void 0 : _a.call(p);
|
|
534
|
+
},
|
|
535
|
+
async stop() {
|
|
536
|
+
releasePlayer();
|
|
537
|
+
},
|
|
538
|
+
};
|
|
539
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A normalized file selected from the device, independent of the underlying
|
|
3
|
+
* picker library. `base64` is optional: some pickers return it eagerly, others
|
|
4
|
+
* require a separate read (see {@link toMediaEntry}).
|
|
5
|
+
*/
|
|
6
|
+
export interface Asset {
|
|
7
|
+
/** local file URI (file://… or content://…) */
|
|
8
|
+
uri: string;
|
|
9
|
+
/** display name including extension, e.g. "photo.jpg" */
|
|
10
|
+
name: string;
|
|
11
|
+
/** MIME type, e.g. "image/jpeg", "application/pdf" */
|
|
12
|
+
mimeType: string;
|
|
13
|
+
/** raw byte size before base64 inflation */
|
|
14
|
+
size: number;
|
|
15
|
+
/** eagerly-provided base64 payload (no data: prefix), when available */
|
|
16
|
+
base64?: string;
|
|
17
|
+
}
|
|
18
|
+
export interface PickImagesOptions {
|
|
19
|
+
/** allow selecting more than one image */
|
|
20
|
+
allowsMultiple?: boolean;
|
|
21
|
+
/** 0..1 compression quality hint passed to the native picker */
|
|
22
|
+
quality?: number;
|
|
23
|
+
/** longest-edge resize hint passed to the native picker */
|
|
24
|
+
maxDimension?: number;
|
|
25
|
+
}
|
|
26
|
+
export interface PickDocumentsOptions {
|
|
27
|
+
allowsMultiple?: boolean;
|
|
28
|
+
/** allow-listed MIME types the picker should restrict to */
|
|
29
|
+
mimeTypes?: string[];
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* The injectable picker surface. An empty array means the user cancelled.
|
|
33
|
+
* Both capabilities are OPTIONAL so a host can ship a partial picker (e.g.
|
|
34
|
+
* images only); the Surface feature-detects with `picker?.pickImages` and only
|
|
35
|
+
* offers the corresponding selector row when a method is present.
|
|
36
|
+
*/
|
|
37
|
+
export interface PickerAdapter {
|
|
38
|
+
pickImages?(opts?: PickImagesOptions): Promise<Asset[]>;
|
|
39
|
+
pickVideos?(opts?: PickImagesOptions): Promise<Asset[]>;
|
|
40
|
+
pickDocuments?(opts?: PickDocumentsOptions): Promise<Asset[]>;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The shape of a single `user_uttered.media[]` entry. The payload is a
|
|
44
|
+
* self-describing data URL so the existing base64-in-socket transport carries
|
|
45
|
+
* it unchanged (web parity).
|
|
46
|
+
*/
|
|
47
|
+
export interface MediaEntry {
|
|
48
|
+
/** "data:<mimeType>;base64,<payload>" */
|
|
49
|
+
data: string;
|
|
50
|
+
name: string;
|
|
51
|
+
mimeType: string;
|
|
52
|
+
size: number;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Build a `data:` URL from a MIME type and raw base64 payload.
|
|
56
|
+
* Pure — no I/O.
|
|
57
|
+
*/
|
|
58
|
+
export declare function toDataUrl(mimeType: string, base64: string): string;
|
|
59
|
+
/**
|
|
60
|
+
* Pure transform: turn an {@link Asset} into a {@link MediaEntry} carrying a
|
|
61
|
+
* base64 data URL. Base64 acquisition is injected via `readBase64` so this
|
|
62
|
+
* stays I/O-free and testable with a fake reader.
|
|
63
|
+
*
|
|
64
|
+
* If the asset already carries `base64` it is used directly and `readBase64`
|
|
65
|
+
* is not called; otherwise `readBase64(asset.uri)` supplies the payload.
|
|
66
|
+
*/
|
|
67
|
+
export declare function toMediaEntry(asset: Asset, readBase64: (uri: string) => Promise<string>): Promise<MediaEntry>;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// src/adapters/picker.ts
|
|
3
|
+
//
|
|
4
|
+
// Picker capability behind an injectable interface so the framework-agnostic
|
|
5
|
+
// core never calls a native module directly. Real impls (Expo
|
|
6
|
+
// `expo-image-picker`/`expo-document-picker`, bare `react-native-image-picker`/
|
|
7
|
+
// `react-native-document-picker`) are constructed in the example apps and
|
|
8
|
+
// injected; unit tests use a fake that returns canned `Asset[]`.
|
|
9
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
10
|
+
exports.toDataUrl = toDataUrl;
|
|
11
|
+
exports.toMediaEntry = toMediaEntry;
|
|
12
|
+
/**
|
|
13
|
+
* Build a `data:` URL from a MIME type and raw base64 payload.
|
|
14
|
+
* Pure — no I/O.
|
|
15
|
+
*/
|
|
16
|
+
function toDataUrl(mimeType, base64) {
|
|
17
|
+
return `data:${mimeType};base64,${base64}`;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Pure transform: turn an {@link Asset} into a {@link MediaEntry} carrying a
|
|
21
|
+
* base64 data URL. Base64 acquisition is injected via `readBase64` so this
|
|
22
|
+
* stays I/O-free and testable with a fake reader.
|
|
23
|
+
*
|
|
24
|
+
* If the asset already carries `base64` it is used directly and `readBase64`
|
|
25
|
+
* is not called; otherwise `readBase64(asset.uri)` supplies the payload.
|
|
26
|
+
*/
|
|
27
|
+
async function toMediaEntry(asset, readBase64) {
|
|
28
|
+
const base64 = asset.base64 != null && asset.base64.length > 0
|
|
29
|
+
? asset.base64
|
|
30
|
+
: await readBase64(asset.uri);
|
|
31
|
+
return {
|
|
32
|
+
data: toDataUrl(asset.mimeType, base64),
|
|
33
|
+
name: asset.name,
|
|
34
|
+
mimeType: asset.mimeType,
|
|
35
|
+
size: asset.size,
|
|
36
|
+
};
|
|
37
|
+
}
|