@giovchat/react-native 0.0.0-stage → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +333 -2
- package/dist/GiovChat.d.ts +100 -0
- package/dist/GiovChat.d.ts.map +1 -0
- package/dist/GiovChat.js +257 -0
- package/dist/GiovChat.js.map +1 -0
- package/dist/attachments.d.ts +121 -0
- package/dist/attachments.d.ts.map +1 -0
- package/dist/attachments.js +185 -0
- package/dist/attachments.js.map +1 -0
- package/dist/brand-assets.d.ts +6 -0
- package/dist/brand-assets.d.ts.map +1 -0
- package/dist/brand-assets.js +9 -0
- package/dist/brand-assets.js.map +1 -0
- package/dist/client.d.ts +190 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +801 -0
- package/dist/client.js.map +1 -0
- package/dist/color.d.ts +25 -0
- package/dist/color.d.ts.map +1 -0
- package/dist/color.js +78 -0
- package/dist/color.js.map +1 -0
- package/dist/components/Attachments.d.ts +13 -0
- package/dist/components/Attachments.d.ts.map +1 -0
- package/dist/components/Attachments.js +42 -0
- package/dist/components/Attachments.js.map +1 -0
- package/dist/components/Branding.d.ts +6 -0
- package/dist/components/Branding.d.ts.map +1 -0
- package/dist/components/Branding.js +28 -0
- package/dist/components/Branding.js.map +1 -0
- package/dist/components/ChatMarkdown.d.ts +15 -0
- package/dist/components/ChatMarkdown.d.ts.map +1 -0
- package/dist/components/ChatMarkdown.js +72 -0
- package/dist/components/ChatMarkdown.js.map +1 -0
- package/dist/components/GiovBotMark.d.ts +6 -0
- package/dist/components/GiovBotMark.d.ts.map +1 -0
- package/dist/components/GiovBotMark.js +11 -0
- package/dist/components/GiovBotMark.js.map +1 -0
- package/dist/components/GreetingBubble.d.ts +7 -0
- package/dist/components/GreetingBubble.d.ts.map +1 -0
- package/dist/components/GreetingBubble.js +12 -0
- package/dist/components/GreetingBubble.js.map +1 -0
- package/dist/components/Header.d.ts +18 -0
- package/dist/components/Header.d.ts.map +1 -0
- package/dist/components/Header.js +67 -0
- package/dist/components/Header.js.map +1 -0
- package/dist/components/MessageBubble.d.ts +16 -0
- package/dist/components/MessageBubble.d.ts.map +1 -0
- package/dist/components/MessageBubble.js +49 -0
- package/dist/components/MessageBubble.js.map +1 -0
- package/dist/components/PreChatForm.d.ts +11 -0
- package/dist/components/PreChatForm.d.ts.map +1 -0
- package/dist/components/PreChatForm.js +72 -0
- package/dist/components/PreChatForm.js.map +1 -0
- package/dist/components/TypingIndicator.d.ts +9 -0
- package/dist/components/TypingIndicator.d.ts.map +1 -0
- package/dist/components/TypingIndicator.js +18 -0
- package/dist/components/TypingIndicator.js.map +1 -0
- package/dist/components/WidgetStyles.d.ts +3 -0
- package/dist/components/WidgetStyles.d.ts.map +1 -0
- package/dist/components/WidgetStyles.js +9 -0
- package/dist/components/WidgetStyles.js.map +1 -0
- package/dist/components/bubbleStyles.d.ts +47 -0
- package/dist/components/bubbleStyles.d.ts.map +1 -0
- package/dist/components/bubbleStyles.js +19 -0
- package/dist/components/bubbleStyles.js.map +1 -0
- package/dist/components/index.d.ts +11 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/index.js +14 -0
- package/dist/components/index.js.map +1 -0
- package/dist/errors.d.ts +18 -0
- package/dist/errors.d.ts.map +1 -0
- package/dist/errors.js +26 -0
- package/dist/errors.js.map +1 -0
- package/dist/i18n.d.ts +73 -0
- package/dist/i18n.d.ts.map +1 -0
- package/dist/i18n.js +292 -0
- package/dist/i18n.js.map +1 -0
- package/dist/identity.d.ts +41 -0
- package/dist/identity.d.ts.map +1 -0
- package/dist/identity.js +41 -0
- package/dist/identity.js.map +1 -0
- package/dist/index.d.ts +20 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +46 -0
- package/dist/index.js.map +1 -0
- package/dist/markdown.d.ts +56 -0
- package/dist/markdown.d.ts.map +1 -0
- package/dist/markdown.js +219 -0
- package/dist/markdown.js.map +1 -0
- package/dist/outbox.d.ts +68 -0
- package/dist/outbox.d.ts.map +1 -0
- package/dist/outbox.js +168 -0
- package/dist/outbox.js.map +1 -0
- package/dist/protocol.d.ts +9 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +34 -0
- package/dist/protocol.js.map +1 -0
- package/dist/styleOverrides.d.ts +37 -0
- package/dist/styleOverrides.d.ts.map +1 -0
- package/dist/styleOverrides.js +54 -0
- package/dist/styleOverrides.js.map +1 -0
- package/dist/types.d.ts +111 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/dist/unread.d.ts +9 -0
- package/dist/unread.d.ts.map +1 -0
- package/dist/unread.js +49 -0
- package/dist/unread.js.map +1 -0
- package/dist/useChatSession.d.ts +78 -0
- package/dist/useChatSession.d.ts.map +1 -0
- package/dist/useChatSession.js +796 -0
- package/dist/useChatSession.js.map +1 -0
- package/package.json +59 -4
- package/src/GiovChat.tsx +530 -0
- package/src/attachments.ts +218 -0
- package/src/brand-assets.ts +5 -0
- package/src/client.ts +758 -0
- package/src/color.ts +74 -0
- package/src/components/Attachments.tsx +75 -0
- package/src/components/Branding.tsx +35 -0
- package/src/components/ChatMarkdown.tsx +111 -0
- package/src/components/GiovBotMark.tsx +19 -0
- package/src/components/GreetingBubble.tsx +20 -0
- package/src/components/Header.tsx +131 -0
- package/src/components/MessageBubble.tsx +121 -0
- package/src/components/PreChatForm.tsx +183 -0
- package/src/components/TypingIndicator.tsx +34 -0
- package/src/components/WidgetStyles.tsx +5 -0
- package/src/components/bubbleStyles.ts +16 -0
- package/src/components/index.ts +14 -0
- package/src/errors.ts +30 -0
- package/src/i18n.ts +351 -0
- package/src/identity.ts +54 -0
- package/src/index.ts +68 -0
- package/src/markdown.ts +227 -0
- package/src/outbox.ts +175 -0
- package/src/protocol.ts +31 -0
- package/src/styleOverrides.ts +85 -0
- package/src/types.ts +119 -0
- package/src/unread.ts +39 -0
- package/src/useChatSession.ts +695 -0
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Attachments, as the web widget handles them (apps/widget-web/src/lib/attachment-policy.ts,
|
|
3
|
+
* attachment-preview.ts and attachment-access.ts): the agent's policy decides what a visitor may
|
|
4
|
+
* send, previews arrive from the network and are validated field by field, and downloads carry a
|
|
5
|
+
* narrow per-conversation access token. Pure: no React or native modules.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
/** A file the host's picker returned. `uri` is what React Native's FormData uploads (file://, content://). */
|
|
9
|
+
export interface GiovChatPickedFile {
|
|
10
|
+
uri: string;
|
|
11
|
+
name: string;
|
|
12
|
+
/** MIME type, e.g. "image/jpeg". Files without one are rejected by the policy check. */
|
|
13
|
+
type: string;
|
|
14
|
+
/** Bytes, when the picker knows it; the server enforces the limit either way. */
|
|
15
|
+
size?: number;
|
|
16
|
+
/** Image dimensions, when the picker knows them; used for the placeholder box. */
|
|
17
|
+
width?: number;
|
|
18
|
+
height?: number;
|
|
19
|
+
/**
|
|
20
|
+
* Optional low-resolution placeholder, `data:image/jpeg;base64,…` (or webp) under 6,000 characters,
|
|
21
|
+
* e.g. from expo-image-manipulator. With width and height it is stored, so every viewer sees the
|
|
22
|
+
* image's shape before it loads. Without it the image simply loads in place.
|
|
23
|
+
*/
|
|
24
|
+
thumbnail?: string;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Supplied by the host app to enable attachments: open the platform's picker and return what the
|
|
29
|
+
* visitor chose (an empty array when they cancel). The SDK ships no native modules; see the README
|
|
30
|
+
* for expo-document-picker, expo-image-picker and react-native-document-picker adapters.
|
|
31
|
+
*/
|
|
32
|
+
export interface GiovChatAttachmentPicker {
|
|
33
|
+
pick(options: { allowedTypes: string[]; maxFiles: number; maxSizeMb: number }): Promise<GiovChatPickedFile[]>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export type GiovAttachmentPreview = { thumb?: string; width?: number; height?: number };
|
|
37
|
+
export interface GiovAttachment {
|
|
38
|
+
url: string;
|
|
39
|
+
/** Empty when the server did not say; the UI then names it by type. */
|
|
40
|
+
filename: string;
|
|
41
|
+
preview?: GiovAttachmentPreview;
|
|
42
|
+
/** Known only for files sent from this device. */
|
|
43
|
+
contentType?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export interface GiovAttachmentPolicy {
|
|
47
|
+
enabled: boolean;
|
|
48
|
+
allowedTypes: string[];
|
|
49
|
+
maxSizeMb: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// The visitor upload endpoint accepts this bounded set; a wildcard cannot enable other types.
|
|
53
|
+
export const SUPPORTED_ATTACHMENT_TYPES = [
|
|
54
|
+
"image/jpeg", "image/png", "image/gif", "image/webp", "application/pdf", "text/plain", "text/csv",
|
|
55
|
+
"application/msword", "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
|
|
56
|
+
"application/vnd.ms-excel", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
57
|
+
"application/vnd.ms-powerpoint", "application/vnd.openxmlformats-officedocument.presentationml.presentation",
|
|
58
|
+
"audio/mpeg", "audio/mp4", "audio/ogg", "audio/webm", "audio/wav",
|
|
59
|
+
];
|
|
60
|
+
|
|
61
|
+
const LEGACY_IMAGE_TYPES = SUPPORTED_ATTACHMENT_TYPES.filter((type) => type.startsWith("image/"));
|
|
62
|
+
/** Without a published agent policy, visitors may send images (the widget's default). */
|
|
63
|
+
export const DEFAULT_ATTACHMENT_POLICY: GiovAttachmentPolicy = { enabled: true, allowedTypes: LEGACY_IMAGE_TYPES, maxSizeMb: 10 };
|
|
64
|
+
export const MAX_ATTACHED_FILES = 5;
|
|
65
|
+
|
|
66
|
+
export function normalizeAttachmentPolicy(value: unknown): GiovAttachmentPolicy | undefined {
|
|
67
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
68
|
+
const raw = value as Record<string, unknown>;
|
|
69
|
+
const patterns = Array.isArray(raw.allowedTypes)
|
|
70
|
+
? raw.allowedTypes.filter((type): type is string => typeof type === "string").map((type) => type.toLowerCase())
|
|
71
|
+
: LEGACY_IMAGE_TYPES;
|
|
72
|
+
return {
|
|
73
|
+
enabled: raw.enabled !== false,
|
|
74
|
+
allowedTypes: SUPPORTED_ATTACHMENT_TYPES.filter((type) => patterns.some((pattern) =>
|
|
75
|
+
pattern === "*" || pattern === type || (pattern.endsWith("/*") && type.startsWith(pattern.slice(0, -1))),
|
|
76
|
+
)),
|
|
77
|
+
maxSizeMb: typeof raw.maxSizeMb === "number" && Number.isFinite(raw.maxSizeMb)
|
|
78
|
+
? Math.max(0.1, Math.min(10, raw.maxSizeMb)) : 10,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export type AttachmentProblem =
|
|
83
|
+
| { kind: "disabled" }
|
|
84
|
+
| { kind: "type"; name: string }
|
|
85
|
+
| { kind: "size"; name: string; maxSizeMb: number }
|
|
86
|
+
| { kind: "count"; max: number };
|
|
87
|
+
|
|
88
|
+
/** The picker's files checked against the policy; the server remains authoritative. */
|
|
89
|
+
export function validateAttachments<T extends { name: string; type: string; size?: number }>(
|
|
90
|
+
files: T[], existingCount = 0, policy: GiovAttachmentPolicy = DEFAULT_ATTACHMENT_POLICY,
|
|
91
|
+
): { accepted: T[]; problems: AttachmentProblem[] } {
|
|
92
|
+
if (!policy.enabled) return { accepted: [], problems: files.length ? [{ kind: "disabled" }] : [] };
|
|
93
|
+
const accepted: T[] = [];
|
|
94
|
+
const problems: AttachmentProblem[] = [];
|
|
95
|
+
const capacity = Math.max(0, MAX_ATTACHED_FILES - existingCount);
|
|
96
|
+
for (const file of files) {
|
|
97
|
+
if (!policy.allowedTypes.includes(String(file.type).toLowerCase())) problems.push({ kind: "type", name: file.name });
|
|
98
|
+
else if (typeof file.size === "number" && file.size > policy.maxSizeMb * 1024 * 1024) problems.push({ kind: "size", name: file.name, maxSizeMb: policy.maxSizeMb });
|
|
99
|
+
else if (accepted.length >= capacity) { if (!problems.some((problem) => problem.kind === "count")) problems.push({ kind: "count", max: MAX_ATTACHED_FILES }); }
|
|
100
|
+
else accepted.push(file);
|
|
101
|
+
}
|
|
102
|
+
return { accepted, problems };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const MAX_THUMB_LENGTH = 6000;
|
|
106
|
+
const THUMB_PATTERN = /^data:image\/(jpeg|webp);base64,[A-Za-z0-9+/=]+$/;
|
|
107
|
+
const dimension = (value: unknown): number => typeof value === "number" && Number.isFinite(value) && value > 0 ? value : 0;
|
|
108
|
+
|
|
109
|
+
/** Keeps only the parts of an untrusted preview that are safe to render. */
|
|
110
|
+
export function sanitizePreview(value: unknown): GiovAttachmentPreview | undefined {
|
|
111
|
+
if (!value || typeof value !== "object") return undefined;
|
|
112
|
+
const raw = value as Record<string, unknown>;
|
|
113
|
+
const preview: GiovAttachmentPreview = {};
|
|
114
|
+
if (typeof raw.thumb === "string" && raw.thumb.length <= MAX_THUMB_LENGTH && THUMB_PATTERN.test(raw.thumb)) preview.thumb = raw.thumb;
|
|
115
|
+
const width = dimension(raw.width), height = dimension(raw.height);
|
|
116
|
+
// An aspect ratio needs both sides, so a lone dimension is dropped.
|
|
117
|
+
if (width && height) { preview.width = width; preview.height = height; }
|
|
118
|
+
return preview.thumb || preview.width ? preview : undefined;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* `attachmentId` is a JSON-encoded array of URLs (the socket and history both send it that way).
|
|
123
|
+
* `previews` is a real array aligned with it by index; anything else means no previews.
|
|
124
|
+
*/
|
|
125
|
+
export function parseAttachments(attachmentId: unknown, previews?: unknown): GiovAttachment[] {
|
|
126
|
+
if (!attachmentId || typeof attachmentId !== "string") return [];
|
|
127
|
+
try {
|
|
128
|
+
const urls = JSON.parse(attachmentId) as unknown;
|
|
129
|
+
if (!Array.isArray(urls)) return [];
|
|
130
|
+
const list: unknown[] = Array.isArray(previews) ? previews : [];
|
|
131
|
+
return urls.filter((url): url is string => typeof url === "string" && url.length > 0).map((url, index) => {
|
|
132
|
+
const preview = sanitizePreview(list[index]);
|
|
133
|
+
return preview ? { url, filename: "", preview } : { url, filename: "" };
|
|
134
|
+
});
|
|
135
|
+
} catch {
|
|
136
|
+
return [];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Mirrors the widget's attachment image cap. */
|
|
141
|
+
export const MAX_IMAGE_WIDTH = 200;
|
|
142
|
+
export const MAX_IMAGE_HEIGHT = 150;
|
|
143
|
+
|
|
144
|
+
/** Display box for an image, scaled down (never up) to fit the cap. */
|
|
145
|
+
export function previewBoxSize(preview: GiovAttachmentPreview | undefined, maxWidth = MAX_IMAGE_WIDTH, maxHeight = MAX_IMAGE_HEIGHT) {
|
|
146
|
+
if (!preview?.width || !preview.height) return null;
|
|
147
|
+
const scale = Math.min(1, maxWidth / preview.width, maxHeight / preview.height);
|
|
148
|
+
return { width: Math.max(1, Math.round(preview.width * scale)), height: Math.max(1, Math.round(preview.height * scale)) };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const IMAGE_EXTENSION = /\.(jpe?g|png|gif|webp)$/i;
|
|
152
|
+
/** Images render inline; everything else is a file row. */
|
|
153
|
+
export function isImageAttachment(attachment: Pick<GiovAttachment, "url" | "contentType" | "preview">): boolean {
|
|
154
|
+
if (attachment.contentType) return attachment.contentType.startsWith("image/");
|
|
155
|
+
if (attachment.preview) return true;
|
|
156
|
+
try { return IMAGE_EXTENSION.test(decodeURIComponent(new URL(attachment.url).pathname)); } catch { return IMAGE_EXTENSION.test(attachment.url); }
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** A readable name for a file row: the known filename, else the last path segment of the URL. */
|
|
160
|
+
export function attachmentName(attachment: Pick<GiovAttachment, "url" | "filename">): string {
|
|
161
|
+
if (attachment.filename) return attachment.filename;
|
|
162
|
+
try {
|
|
163
|
+
const last = decodeURIComponent(new URL(attachment.url).pathname).split("/").filter(Boolean).pop();
|
|
164
|
+
return last || "";
|
|
165
|
+
} catch {
|
|
166
|
+
return "";
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Narrow credential for downloading the attachments of one conversation; the server keeps it the
|
|
172
|
+
* same for hours, so a URL built with it stays stable and images can be cached. Milliseconds.
|
|
173
|
+
*/
|
|
174
|
+
export type AttachmentAccess = { token: string; expiresAt: number };
|
|
175
|
+
const ACCESS_TOKEN = /^[A-Za-z0-9._-]{16,256}$/;
|
|
176
|
+
/** Stop using a token this long before it expires, so a download never starts with a dying one. */
|
|
177
|
+
export const ATTACHMENT_ACCESS_MARGIN_MS = 5 * 60 * 1000;
|
|
178
|
+
|
|
179
|
+
export function parseAttachmentAccess(value: unknown): AttachmentAccess | null {
|
|
180
|
+
const { token, expiresAt } = (value ?? {}) as { token?: unknown; expiresAt?: unknown };
|
|
181
|
+
return typeof token === "string" && ACCESS_TOKEN.test(token) && typeof expiresAt === "number" && Number.isFinite(expiresAt)
|
|
182
|
+
? { token, expiresAt } : null;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
export function usableAttachmentToken(access: AttachmentAccess | null | undefined, now: number): string | undefined {
|
|
186
|
+
return access && access.expiresAt - now > ATTACHMENT_ACCESS_MARGIN_MS ? access.token : undefined;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/** Keeps the stored value unless the response carried one that expires no earlier. */
|
|
190
|
+
export function newerAttachmentAccess(current: AttachmentAccess | null, incoming: AttachmentAccess | null): AttachmentAccess | null {
|
|
191
|
+
return incoming && !(current && current.expiresAt > incoming.expiresAt) ? incoming : current;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/** Adds exactly one credential: `at` when an access token is usable, else the visitor token, else nothing. */
|
|
195
|
+
export function attachmentDisplayUrl(url: string, accessToken?: string | null, visitorToken?: string | null): string {
|
|
196
|
+
const credential = accessToken ? `at=${accessToken}` : visitorToken ? `token=${encodeURIComponent(visitorToken)}` : "";
|
|
197
|
+
return credential ? `${url}${url.includes("?") ? "&" : "?"}${credential}` : url;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/** The preview the upload can carry: all three fields or none, as the server requires. */
|
|
201
|
+
export function uploadPreview(file: GiovChatPickedFile): Required<GiovAttachmentPreview> | null {
|
|
202
|
+
if (!file.type.startsWith("image/")) return null;
|
|
203
|
+
const preview = sanitizePreview({ thumb: file.thumbnail, width: file.width, height: file.height });
|
|
204
|
+
return preview?.thumb && preview.width && preview.height ? { thumb: preview.thumb, width: preview.width, height: preview.height } : null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/** React Native's FormData takes `{ uri, name, type }` for a file part; the preview fields are optional extras. */
|
|
208
|
+
export function buildUploadForm(file: GiovChatPickedFile): FormData {
|
|
209
|
+
const form = new FormData();
|
|
210
|
+
form.append("file", { uri: file.uri, name: file.name || "file", type: file.type } as unknown as Blob);
|
|
211
|
+
const preview = uploadPreview(file);
|
|
212
|
+
if (preview) {
|
|
213
|
+
form.append("thumbnail", preview.thumb);
|
|
214
|
+
form.append("width", String(Math.round(preview.width)));
|
|
215
|
+
form.append("height", String(Math.round(preview.height)));
|
|
216
|
+
}
|
|
217
|
+
return form;
|
|
218
|
+
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** Native Image fallback, mechanically rendered at 64px from packages/brand/giovbot.svg.
|
|
2
|
+
* No native SVG dependency, extra requests, or remote branding assets are required.
|
|
3
|
+
*/
|
|
4
|
+
export const giovBotMarkSvgSha256 = "10ce5835dfa47fb08d05cdb877187c38f960d1a76d8fd82ba49e59f72197311f";
|
|
5
|
+
export const giovBotMarkDataUri = "data:image/png;base64,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";
|