@repzo/widget-react-native 1.3.0 → 1.4.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/CHANGELOG.md +179 -0
- package/README.md +72 -16
- package/lib/commonjs/api/client.js +113 -0
- package/lib/commonjs/api/client.js.map +1 -1
- package/lib/commonjs/attachments/file-size.js +52 -0
- package/lib/commonjs/attachments/file-size.js.map +1 -0
- package/lib/commonjs/attachments/local-previews.js +40 -0
- package/lib/commonjs/attachments/local-previews.js.map +1 -0
- package/lib/commonjs/attachments/pickers.js +76 -28
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +24 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/locales.js +13 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/widget-store.js +486 -63
- package/lib/commonjs/state/widget-store.js.map +1 -1
- package/lib/commonjs/storage.js +3 -1
- package/lib/commonjs/storage.js.map +1 -1
- package/lib/commonjs/types.js.map +1 -1
- package/lib/commonjs/ui/WidgetProvider.js +40 -16
- package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
- package/lib/commonjs/ui/components/AttachmentChips.js +109 -40
- package/lib/commonjs/ui/components/AttachmentChips.js.map +1 -1
- package/lib/commonjs/ui/components/ConversationRow.js +65 -10
- package/lib/commonjs/ui/components/ConversationRow.js.map +1 -1
- package/lib/commonjs/ui/components/ImageViewer.js +231 -25
- package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
- package/lib/commonjs/ui/components/RatingCard.js +5 -2
- package/lib/commonjs/ui/components/RatingCard.js.map +1 -1
- package/lib/commonjs/ui/components/ZoomableImage.js +58 -10
- package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -1
- package/lib/commonjs/ui/components/icons.js +22 -16
- package/lib/commonjs/ui/components/icons.js.map +1 -1
- package/lib/commonjs/ui/hooks/use-retained-state.js +39 -0
- package/lib/commonjs/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/commonjs/ui/screens/chat/ChatView.js +666 -171
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +150 -3
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/commonjs/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.js +38 -55
- package/lib/commonjs/ui/screens/home/HomeView.js.map +1 -1
- package/lib/commonjs/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/commonjs/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/commonjs/ui/styles/theme.js +1 -0
- package/lib/commonjs/ui/styles/theme.js.map +1 -1
- package/lib/commonjs/ui/utils/conversations.js +17 -1
- package/lib/commonjs/ui/utils/conversations.js.map +1 -1
- package/lib/commonjs/ui/utils/html.js +13 -0
- package/lib/commonjs/ui/utils/html.js.map +1 -1
- package/lib/commonjs/ui/utils/image-load-queue.js +114 -0
- package/lib/commonjs/ui/utils/image-load-queue.js.map +1 -0
- package/lib/commonjs/ui/utils/inline-attachments.js +88 -0
- package/lib/commonjs/ui/utils/inline-attachments.js.map +1 -0
- package/lib/module/api/client.js +113 -0
- package/lib/module/api/client.js.map +1 -1
- package/lib/module/attachments/file-size.js +47 -0
- package/lib/module/attachments/file-size.js.map +1 -0
- package/lib/module/attachments/local-previews.js +33 -0
- package/lib/module/attachments/local-previews.js.map +1 -0
- package/lib/module/attachments/pickers.js +74 -27
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +24 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/locales.js +13 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/widget-store.js +488 -65
- package/lib/module/state/widget-store.js.map +1 -1
- package/lib/module/storage.js +2 -0
- package/lib/module/storage.js.map +1 -1
- package/lib/module/types.js.map +1 -1
- package/lib/module/ui/WidgetProvider.js +40 -16
- package/lib/module/ui/WidgetProvider.js.map +1 -1
- package/lib/module/ui/components/AttachmentChips.js +111 -42
- package/lib/module/ui/components/AttachmentChips.js.map +1 -1
- package/lib/module/ui/components/ConversationRow.js +65 -10
- package/lib/module/ui/components/ConversationRow.js.map +1 -1
- package/lib/module/ui/components/ImageViewer.js +233 -27
- package/lib/module/ui/components/ImageViewer.js.map +1 -1
- package/lib/module/ui/components/RatingCard.js +5 -2
- package/lib/module/ui/components/RatingCard.js.map +1 -1
- package/lib/module/ui/components/ZoomableImage.js +58 -10
- package/lib/module/ui/components/ZoomableImage.js.map +1 -1
- package/lib/module/ui/components/icons.js +21 -15
- package/lib/module/ui/components/icons.js.map +1 -1
- package/lib/module/ui/hooks/use-retained-state.js +34 -0
- package/lib/module/ui/hooks/use-retained-state.js.map +1 -0
- package/lib/module/ui/screens/chat/ChatView.js +672 -177
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +152 -4
- package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
- package/lib/module/ui/screens/conversations/ConversationsList.js +1 -0
- package/lib/module/ui/screens/conversations/ConversationsList.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.js +40 -57
- package/lib/module/ui/screens/home/HomeView.js.map +1 -1
- package/lib/module/ui/screens/home/HomeView.styles.js +36 -45
- package/lib/module/ui/screens/home/HomeView.styles.js.map +1 -1
- package/lib/module/ui/styles/theme.js +1 -0
- package/lib/module/ui/styles/theme.js.map +1 -1
- package/lib/module/ui/utils/conversations.js +15 -1
- package/lib/module/ui/utils/conversations.js.map +1 -1
- package/lib/module/ui/utils/html.js +12 -0
- package/lib/module/ui/utils/html.js.map +1 -1
- package/lib/module/ui/utils/image-load-queue.js +109 -0
- package/lib/module/ui/utils/image-load-queue.js.map +1 -0
- package/lib/module/ui/utils/inline-attachments.js +85 -0
- package/lib/module/ui/utils/inline-attachments.js.map +1 -0
- package/lib/typescript/commonjs/api/client.d.ts +35 -1
- package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
- package/lib/typescript/commonjs/attachments/file-size.d.ts +15 -0
- package/lib/typescript/commonjs/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/commonjs/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/commonjs/attachments/pickers.d.ts +35 -10
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +13 -3
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/widget-store.d.ts +67 -4
- package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/commonjs/storage.d.ts +2 -0
- package/lib/typescript/commonjs/storage.d.ts.map +1 -1
- package/lib/typescript/commonjs/types.d.ts +58 -0
- package/lib/typescript/commonjs/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/commonjs/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/commonjs/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
- package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/commonjs/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/commonjs/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/commonjs/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/commonjs/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/commonjs/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/html.d.ts +5 -0
- package/lib/typescript/commonjs/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/commonjs/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/commonjs/ui/utils/inline-attachments.d.ts.map +1 -0
- package/lib/typescript/module/api/client.d.ts +35 -1
- package/lib/typescript/module/api/client.d.ts.map +1 -1
- package/lib/typescript/module/attachments/file-size.d.ts +15 -0
- package/lib/typescript/module/attachments/file-size.d.ts.map +1 -0
- package/lib/typescript/module/attachments/local-previews.d.ts +14 -0
- package/lib/typescript/module/attachments/local-previews.d.ts.map +1 -0
- package/lib/typescript/module/attachments/pickers.d.ts +35 -10
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +13 -3
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/widget-store.d.ts +67 -4
- package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
- package/lib/typescript/module/storage.d.ts +2 -0
- package/lib/typescript/module/storage.d.ts.map +1 -1
- package/lib/typescript/module/types.d.ts +58 -0
- package/lib/typescript/module/types.d.ts.map +1 -1
- package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts +13 -3
- package/lib/typescript/module/ui/components/AttachmentChips.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts +15 -1
- package/lib/typescript/module/ui/components/ConversationRow.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ImageViewer.d.ts +26 -6
- package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/RatingCard.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts +10 -1
- package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -1
- package/lib/typescript/module/ui/components/icons.d.ts +2 -1
- package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts +11 -0
- package/lib/typescript/module/ui/hooks/use-retained-state.d.ts.map +1 -0
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +4 -4
- package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +154 -2
- package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +30 -1
- package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts +2 -0
- package/lib/typescript/module/ui/screens/conversations/ConversationsList.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts +7 -5
- package/lib/typescript/module/ui/screens/home/HomeView.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts +30 -35
- package/lib/typescript/module/ui/screens/home/HomeView.styles.d.ts.map +1 -1
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts +5 -6
- package/lib/typescript/module/ui/screens/home/HomeView.types.d.ts.map +1 -1
- package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
- package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
- package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/html.d.ts +5 -0
- package/lib/typescript/module/ui/utils/html.d.ts.map +1 -1
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts +44 -0
- package/lib/typescript/module/ui/utils/image-load-queue.d.ts.map +1 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts +29 -0
- package/lib/typescript/module/ui/utils/inline-attachments.d.ts.map +1 -0
- package/package.json +1 -1
- package/src/api/client.ts +148 -2
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +90 -30
- package/src/i18n.ts +24 -3
- package/src/locales.ts +13 -3
- package/src/state/widget-store.ts +519 -70
- package/src/storage.ts +2 -0
- package/src/types.ts +58 -0
- package/src/ui/WidgetProvider.tsx +50 -12
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +250 -28
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/components/icons.tsx +10 -12
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +151 -3
- package/src/ui/screens/chat/ChatView.tsx +760 -213
- package/src/ui/screens/chat/ChatView.types.ts +30 -1
- package/src/ui/screens/conversations/ConversationsList.tsx +1 -0
- package/src/ui/screens/conversations/ConversationsList.types.ts +2 -0
- package/src/ui/screens/home/HomeView.styles.ts +36 -45
- package/src/ui/screens/home/HomeView.tsx +54 -70
- package/src/ui/screens/home/HomeView.types.ts +5 -6
- package/src/ui/styles/theme.ts +6 -0
- package/src/ui/utils/conversations.ts +16 -2
- package/src/ui/utils/html.ts +26 -0
- package/src/ui/utils/image-load-queue.ts +108 -0
- package/src/ui/utils/inline-attachments.ts +71 -0
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Visitor attachments: picking photos/files
|
|
3
|
-
*
|
|
2
|
+
* Visitor attachments: picking photos/files for `POST /api/widget/messages`.
|
|
3
|
+
*
|
|
4
|
+
* A server that takes uploads names its cap (`maxAttachmentBytes`): a picked
|
|
5
|
+
* file then stays a local URI, which the composer uploads right away. An
|
|
6
|
+
* older server only takes files inline, as base64 data URLs within the
|
|
7
|
+
* 1.5 MB / 6 MB caps below.
|
|
4
8
|
*
|
|
5
9
|
* The SDK ships no native code, so every picker is borrowed from the host —
|
|
6
10
|
* the same approach as `storage.ts` and `native-gradient.ts`:
|
|
@@ -8,7 +12,7 @@
|
|
|
8
12
|
* - `expo-document-picker` → any file
|
|
9
13
|
* - `expo-file-system` → reads a picked file as base64 (optional; without it
|
|
10
14
|
* the file is read through `fetch` + `FileReader`)
|
|
11
|
-
* - `expo-image-manipulator` → shrinks photos to fit the 1.5 MB cap (optional;
|
|
15
|
+
* - `expo-image-manipulator` → shrinks photos, to fit the 1.5 MB inline cap (optional;
|
|
12
16
|
* without it a full-resolution camera photo is usually refused as too large)
|
|
13
17
|
*
|
|
14
18
|
* A host without those (or with `react-native-image-picker` etc.) plugs its own
|
|
@@ -20,13 +24,14 @@
|
|
|
20
24
|
*/
|
|
21
25
|
|
|
22
26
|
import type { AttachmentSource, RepzoPickedFile, RepzoWidgetAttachmentOptions } from '../types';
|
|
27
|
+
import { measureFileSize, validSize } from './file-size';
|
|
23
28
|
|
|
24
29
|
/** The server keeps the first five and silently drops the rest. */
|
|
25
30
|
export const MAX_ATTACHMENTS = 5;
|
|
26
|
-
/** Per file, decoded. The server silently drops anything larger. */
|
|
31
|
+
/** Per inline file, decoded. The server silently drops anything larger. */
|
|
27
32
|
export const MAX_ATTACHMENT_BYTES = 1_500_000;
|
|
28
33
|
/**
|
|
29
|
-
* All files in one message, decoded. Base64 inside JSON inflates bytes by
|
|
34
|
+
* All inline files in one message, decoded. Base64 inside JSON inflates bytes by
|
|
30
35
|
* ~1.37x and the server caps a request body at 10 MB, so five files at the
|
|
31
36
|
* per-file maximum would come back as a 413.
|
|
32
37
|
*/
|
|
@@ -35,17 +40,28 @@ export const MAX_TOTAL_ATTACHMENT_BYTES = 6_000_000;
|
|
|
35
40
|
/** Photos are re-encoded at this JPEG quality — small enough to fit the cap more often. */
|
|
36
41
|
const IMAGE_QUALITY = 0.7;
|
|
37
42
|
|
|
38
|
-
/** A picked file,
|
|
43
|
+
/** A picked file, ready to send; what the composer shows as a chip. */
|
|
39
44
|
export interface DraftAttachment {
|
|
40
45
|
id: string;
|
|
41
46
|
name: string;
|
|
42
47
|
contentType: string;
|
|
43
48
|
/** Decoded bytes — what the server measures against its cap. */
|
|
44
49
|
size: number;
|
|
45
|
-
/** `data:<mime>;base64,<…>`
|
|
50
|
+
/** `data:<mime>;base64,<…>` for an inline file; empty when `fileUri` is uploaded instead. */
|
|
46
51
|
dataUrl: string;
|
|
52
|
+
/** The local file uploaded ahead of the message. */
|
|
53
|
+
fileUri?: string;
|
|
47
54
|
/** What `<Image>` renders locally: the picked file's URI, else the data URL. */
|
|
48
55
|
previewUri: string;
|
|
56
|
+
/**
|
|
57
|
+
* Where `fileUri`'s upload stands. The composer starts it as soon as the
|
|
58
|
+
* file is picked and holds Send until every file is up. Absent for inline files.
|
|
59
|
+
*/
|
|
60
|
+
uploadStatus?: 'uploading' | 'uploaded' | 'failed';
|
|
61
|
+
/** What the message names the uploaded file by, once `uploadStatus` is `uploaded`. */
|
|
62
|
+
uploadToken?: string;
|
|
63
|
+
/** When `uploadToken` stops working, on this phone's clock (ms). */
|
|
64
|
+
uploadExpiresAt?: number;
|
|
49
65
|
}
|
|
50
66
|
|
|
51
67
|
/** Why some (or all) of a pick didn't make it into the composer. */
|
|
@@ -113,6 +129,11 @@ export function setAttachmentOptions(options: RepzoWidgetAttachmentOptions | nul
|
|
|
113
129
|
hostOptions = options;
|
|
114
130
|
}
|
|
115
131
|
|
|
132
|
+
/** Whether a picked file starts uploading before Send (`attachments.uploadOnPick`). */
|
|
133
|
+
export function uploadsOnPick(): boolean {
|
|
134
|
+
return hostOptions?.uploadOnPick !== false;
|
|
135
|
+
}
|
|
136
|
+
|
|
116
137
|
const ALL_SOURCES: AttachmentSource[] = ['photo', 'camera', 'file'];
|
|
117
138
|
|
|
118
139
|
/**
|
|
@@ -139,11 +160,13 @@ const CAMERA_DENIED = 'cameraDenied' as const;
|
|
|
139
160
|
|
|
140
161
|
/**
|
|
141
162
|
* Opens the picker for `source` and returns what fits alongside `current`.
|
|
163
|
+
* `uploadLimit` is the server's upload cap; without it files go inline.
|
|
142
164
|
* Never throws: a cancelled pick is `{ added: [], problem: null }`.
|
|
143
165
|
*/
|
|
144
166
|
export async function pickAttachments(
|
|
145
167
|
source: AttachmentSource,
|
|
146
|
-
current: DraftAttachment[]
|
|
168
|
+
current: DraftAttachment[],
|
|
169
|
+
uploadLimit: number | null = null
|
|
147
170
|
): Promise<PickResult> {
|
|
148
171
|
const slots = MAX_ATTACHMENTS - current.length;
|
|
149
172
|
if (slots <= 0) return { added: [], problem: 'tooMany' };
|
|
@@ -152,7 +175,7 @@ export async function pickAttachments(
|
|
|
152
175
|
try {
|
|
153
176
|
picked = hostOptions?.pick
|
|
154
177
|
? await hostOptions.pick(source)
|
|
155
|
-
: await launchBuiltInPicker(source, slots);
|
|
178
|
+
: await launchBuiltInPicker(source, slots, !!uploadLimit);
|
|
156
179
|
} catch (error) {
|
|
157
180
|
console.warn('[repzo-widget] attachment picker failed', error);
|
|
158
181
|
return { added: [], problem: 'failed' };
|
|
@@ -170,8 +193,9 @@ export async function pickAttachments(
|
|
|
170
193
|
}
|
|
171
194
|
// A size the picker already knows rules a file out before it's read, so
|
|
172
195
|
// a 40 MB video never lands in memory as a base64 string.
|
|
173
|
-
const
|
|
174
|
-
const
|
|
196
|
+
const uploads = !!uploadLimit && !!file.uri;
|
|
197
|
+
const declared = file.base64 && !uploads ? null : validSize(file.size);
|
|
198
|
+
const early = declared === null ? null : checkAttachmentFits(accepted, declared, uploads ? uploadLimit : null);
|
|
175
199
|
if (early) {
|
|
176
200
|
problem = problem ?? early;
|
|
177
201
|
continue;
|
|
@@ -179,7 +203,7 @@ export async function pickAttachments(
|
|
|
179
203
|
|
|
180
204
|
let draft: DraftAttachment;
|
|
181
205
|
try {
|
|
182
|
-
draft = await toDraft(file);
|
|
206
|
+
draft = uploads ? await toUploadDraft(file, uploadLimit!) : await toDraft(file);
|
|
183
207
|
} catch (error) {
|
|
184
208
|
if (error === TOO_LARGE) {
|
|
185
209
|
problem = problem ?? 'tooLarge';
|
|
@@ -189,7 +213,8 @@ export async function pickAttachments(
|
|
|
189
213
|
problem = problem ?? 'failed';
|
|
190
214
|
continue;
|
|
191
215
|
}
|
|
192
|
-
const fits =
|
|
216
|
+
const fits =
|
|
217
|
+
draft.size > 0 ? checkAttachmentFits(accepted, draft.size, uploads ? uploadLimit : null) : 'failed';
|
|
193
218
|
if (fits) {
|
|
194
219
|
problem = problem ?? fits;
|
|
195
220
|
continue;
|
|
@@ -201,7 +226,8 @@ export async function pickAttachments(
|
|
|
201
226
|
|
|
202
227
|
async function launchBuiltInPicker(
|
|
203
228
|
source: AttachmentSource,
|
|
204
|
-
slots: number
|
|
229
|
+
slots: number,
|
|
230
|
+
uploads: boolean
|
|
205
231
|
): Promise<RepzoPickedFile[] | null | typeof CAMERA_DENIED> {
|
|
206
232
|
if (source === 'file') {
|
|
207
233
|
if (!documentPicker) return null;
|
|
@@ -224,9 +250,10 @@ async function launchBuiltInPicker(
|
|
|
224
250
|
if (!imagePicker) return null;
|
|
225
251
|
const options = {
|
|
226
252
|
quality: IMAGE_QUALITY,
|
|
227
|
-
// With the manipulator the photo is re-encoded below,
|
|
228
|
-
// (full-resolution)
|
|
229
|
-
base64
|
|
253
|
+
// With the manipulator the photo is re-encoded below, and an uploaded
|
|
254
|
+
// photo is read from its file, so the picker's own (full-resolution)
|
|
255
|
+
// base64 would only waste memory.
|
|
256
|
+
base64: !uploads && !imageManipulator,
|
|
230
257
|
exif: false,
|
|
231
258
|
};
|
|
232
259
|
|
|
@@ -246,7 +273,7 @@ async function launchBuiltInPicker(
|
|
|
246
273
|
const assets: any[] = Array.isArray(result?.assets) ? result.assets : [];
|
|
247
274
|
const files: RepzoPickedFile[] = [];
|
|
248
275
|
for (const [index, asset] of assets.slice(0, slots).entries()) {
|
|
249
|
-
const shrunk = await shrinkPhoto(asset);
|
|
276
|
+
const shrunk = await shrinkPhoto(asset, !uploads);
|
|
250
277
|
const type = shrunk
|
|
251
278
|
? 'image/jpeg'
|
|
252
279
|
: asset.mimeType || guessContentType(asset.fileName || asset.uri) || 'image/jpeg';
|
|
@@ -254,7 +281,7 @@ async function launchBuiltInPicker(
|
|
|
254
281
|
files.push({
|
|
255
282
|
name: `${baseName}.${extensionFor(type)}`,
|
|
256
283
|
type,
|
|
257
|
-
// No `size`: `fileSize` is the original's, not the re-encoded
|
|
284
|
+
// No `size`: `fileSize` is the original's, not the re-encoded file's.
|
|
258
285
|
uri: shrunk?.uri ?? asset.uri,
|
|
259
286
|
base64: shrunk?.base64 ?? asset.base64 ?? undefined,
|
|
260
287
|
});
|
|
@@ -282,7 +309,10 @@ async function launchImagePicker(launch: (options: object) => Promise<any>, opti
|
|
|
282
309
|
* MAX_IMAGE_EDGE. Null when the manipulator isn't installed or fails — the
|
|
283
310
|
* caller then falls back to the picker's own output (and the size check).
|
|
284
311
|
*/
|
|
285
|
-
async function shrinkPhoto(
|
|
312
|
+
async function shrinkPhoto(
|
|
313
|
+
asset: any,
|
|
314
|
+
withBase64: boolean
|
|
315
|
+
): Promise<{ uri: string; base64?: string } | null> {
|
|
286
316
|
if (!imageManipulator || !asset?.uri) return null;
|
|
287
317
|
const width = Number(asset.width) || 0;
|
|
288
318
|
const height = Number(asset.height) || 0;
|
|
@@ -296,7 +326,7 @@ async function shrinkPhoto(asset: any): Promise<{ uri: string; base64: string }
|
|
|
296
326
|
const saveOptions = {
|
|
297
327
|
compress: IMAGE_QUALITY,
|
|
298
328
|
format: imageManipulator.SaveFormat?.JPEG ?? 'jpeg',
|
|
299
|
-
base64:
|
|
329
|
+
base64: withBase64,
|
|
300
330
|
};
|
|
301
331
|
try {
|
|
302
332
|
let output: any;
|
|
@@ -308,6 +338,7 @@ async function shrinkPhoto(asset: any): Promise<{ uri: string; base64: string }
|
|
|
308
338
|
const image = await context.renderAsync();
|
|
309
339
|
output = await image.saveAsync(saveOptions);
|
|
310
340
|
}
|
|
341
|
+
if (!withBase64) return typeof output?.uri === 'string' && output.uri ? { uri: output.uri } : null;
|
|
311
342
|
return typeof output?.base64 === 'string' && output.base64
|
|
312
343
|
? { uri: output.uri ?? asset.uri, base64: output.base64 }
|
|
313
344
|
: null;
|
|
@@ -340,6 +371,32 @@ async function toDraft(file: RepzoPickedFile): Promise<DraftAttachment> {
|
|
|
340
371
|
};
|
|
341
372
|
}
|
|
342
373
|
|
|
374
|
+
/** A file that's uploaded on its own, ahead of the message: only its size is read here. */
|
|
375
|
+
async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promise<DraftAttachment> {
|
|
376
|
+
const uri = file.uri!;
|
|
377
|
+
const size = validSize(file.size) ?? (await readFileSize(uri));
|
|
378
|
+
if (size > uploadLimit) throw TOO_LARGE;
|
|
379
|
+
const name = (file.name || fileNameFromUri(uri) || 'file').slice(0, 255);
|
|
380
|
+
return {
|
|
381
|
+
id: `draft-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
|
382
|
+
name,
|
|
383
|
+
contentType: normalizeContentType(file.type) ?? guessContentType(name) ?? 'application/octet-stream',
|
|
384
|
+
size,
|
|
385
|
+
dataUrl: '',
|
|
386
|
+
fileUri: uri,
|
|
387
|
+
previewUri: uri,
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
/** A local file's size in bytes, or 0 when nothing can tell. */
|
|
392
|
+
async function readFileSize(uri: string): Promise<number> {
|
|
393
|
+
const size = await measureFileSize(uri);
|
|
394
|
+
if (size !== null) return size;
|
|
395
|
+
// Last resort: the native blob reads the whole file (on Android, into memory).
|
|
396
|
+
const blob = await (await fetch(uri)).blob();
|
|
397
|
+
return validSize(blob.size) ?? 0;
|
|
398
|
+
}
|
|
399
|
+
|
|
343
400
|
/** Thrown (by identity) when a file is over the per-file cap before it's read. */
|
|
344
401
|
const TOO_LARGE = new Error('File exceeds the attachment cap');
|
|
345
402
|
|
|
@@ -407,14 +464,20 @@ async function readBase64WithFetch(uri: string): Promise<string> {
|
|
|
407
464
|
|
|
408
465
|
// ─── Pure helpers ──────────────────────────────────────────────────────────
|
|
409
466
|
|
|
410
|
-
/**
|
|
467
|
+
/**
|
|
468
|
+
* Null when a file of `size` bytes still fits beside `current`; otherwise the
|
|
469
|
+
* limit it breaks. With `uploadLimit` the file is uploaded on its own, so
|
|
470
|
+
* only its own size counts; inline files also share one request body.
|
|
471
|
+
*/
|
|
411
472
|
export function checkAttachmentFits(
|
|
412
|
-
current: ReadonlyArray<{ size: number }>,
|
|
413
|
-
size: number
|
|
473
|
+
current: ReadonlyArray<{ size: number; fileUri?: string }>,
|
|
474
|
+
size: number,
|
|
475
|
+
uploadLimit: number | null = null
|
|
414
476
|
): AttachmentProblem | null {
|
|
415
477
|
if (current.length >= MAX_ATTACHMENTS) return 'tooMany';
|
|
478
|
+
if (uploadLimit) return size > uploadLimit ? 'tooLarge' : null;
|
|
416
479
|
if (size > MAX_ATTACHMENT_BYTES) return 'tooLarge';
|
|
417
|
-
const total = current.reduce((sum, item) => sum + item.size, 0);
|
|
480
|
+
const total = current.filter((item) => !item.fileUri).reduce((sum, item) => sum + item.size, 0);
|
|
418
481
|
if (total + size > MAX_TOTAL_ATTACHMENT_BYTES) return 'tooLargeTogether';
|
|
419
482
|
return null;
|
|
420
483
|
}
|
|
@@ -462,10 +525,6 @@ export function isImageAttachment(contentType: string | null | undefined): boole
|
|
|
462
525
|
return !!contentType && contentType.toLowerCase().startsWith('image/');
|
|
463
526
|
}
|
|
464
527
|
|
|
465
|
-
function validSize(size: unknown): number | null {
|
|
466
|
-
return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
|
|
467
|
-
}
|
|
468
|
-
|
|
469
528
|
function fileNameFromUri(uri: string | undefined): string | null {
|
|
470
529
|
if (!uri) return null;
|
|
471
530
|
const last = uri.split(/[?#]/)[0]?.split('/').pop();
|
|
@@ -501,7 +560,8 @@ const CONTENT_TYPES: Record<string, string> = {
|
|
|
501
560
|
mov: 'video/quicktime',
|
|
502
561
|
};
|
|
503
562
|
|
|
504
|
-
|
|
563
|
+
/** The type a file name's extension implies, or null for one this table doesn't know. */
|
|
564
|
+
export function guessContentType(nameOrUri: string | null | undefined): string | null {
|
|
505
565
|
const extension = /\.([a-z0-9]+)$/i.exec(nameOrUri?.split(/[?#]/)[0] ?? '')?.[1];
|
|
506
566
|
return extension ? (CONTENT_TYPES[extension.toLowerCase()] ?? null) : null;
|
|
507
567
|
}
|
package/src/i18n.ts
CHANGED
|
@@ -59,6 +59,14 @@ export const DEFAULT_LABELS = {
|
|
|
59
59
|
loadEarlier: 'Load earlier messages',
|
|
60
60
|
emptyConversation: 'Send a message to start the conversation',
|
|
61
61
|
messagePlaceholder: 'Type a message...',
|
|
62
|
+
// Screen-reader label of the send button beside the message field.
|
|
63
|
+
sendMessage: 'Send message',
|
|
64
|
+
// Screen-reader labels of the button that returns to the newest message
|
|
65
|
+
// after scrolling up; the second when teammates replied in the meantime.
|
|
66
|
+
jumpToLatest: 'Jump to latest messages',
|
|
67
|
+
jumpToLatestNew: 'New messages: {count}. Jump to latest',
|
|
68
|
+
// Screen readers, on the last tile of a photo album showing "+{count}".
|
|
69
|
+
morePhotos: '{count} more photos',
|
|
62
70
|
attachFile: 'Attach file',
|
|
63
71
|
// Attach menu, composer chips and the notices under them.
|
|
64
72
|
attachPhotoLibrary: 'Photo library',
|
|
@@ -70,6 +78,11 @@ export const DEFAULT_LABELS = {
|
|
|
70
78
|
attachmentsTooLargeTogether:
|
|
71
79
|
'Attachments are too large to send together — send some in another message',
|
|
72
80
|
attachmentFailed: "Couldn't attach that file",
|
|
81
|
+
// A picked file uploads before Send is enabled. Screen-reader labels for its
|
|
82
|
+
// chip while it uploads and after it failed, then the notice for a failure.
|
|
83
|
+
attachmentUploading: 'Uploading {name}',
|
|
84
|
+
attachmentUploadFailed: "Couldn't upload {name}. Tap to try again",
|
|
85
|
+
attachmentsUploadFailed: "Couldn't upload a file. Tap it to try again",
|
|
73
86
|
// A file in a message; tapping opens it. `{name}` is the file name.
|
|
74
87
|
openAttachment: 'Open {name}',
|
|
75
88
|
// Voice notes played in the chat (with expo-audio).
|
|
@@ -79,9 +92,13 @@ export const DEFAULT_LABELS = {
|
|
|
79
92
|
// Full-screen image viewer.
|
|
80
93
|
imageLoadFailed: "Couldn't load this image",
|
|
81
94
|
openImageExternally: 'Open image',
|
|
95
|
+
// Screen readers: which of a message's photos is open.
|
|
96
|
+
imagePosition: '{current} of {total}',
|
|
82
97
|
cameraPermissionDenied: 'Allow camera access in Settings to take a photo',
|
|
83
98
|
cancel: 'Cancel',
|
|
84
99
|
failedToSend: 'Failed to send',
|
|
100
|
+
// After "Failed to send" under a message the visitor can send again.
|
|
101
|
+
tapToRetry: 'Tap to retry',
|
|
85
102
|
thinking: 'Typing',
|
|
86
103
|
messages: 'Messages',
|
|
87
104
|
noMessages: 'No messages yet',
|
|
@@ -91,13 +108,17 @@ export const DEFAULT_LABELS = {
|
|
|
91
108
|
preChatSubtitle: 'Let us know a bit about you so we can help you better.',
|
|
92
109
|
preChatSkip: 'Skip',
|
|
93
110
|
preChatSubmit: 'Start chat',
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
homeViewAllCount: 'View all ({count})',
|
|
111
|
+
// Heading of Home's card that shows the newest conversation.
|
|
112
|
+
homeRecentMessageTitle: 'Recent message',
|
|
97
113
|
homeNoConversations: 'Your conversations will show up here',
|
|
98
114
|
homeStatusOnline: "We're online",
|
|
99
115
|
homeStatusAway: "We're away",
|
|
100
116
|
unreadBadge: '{count} unread',
|
|
117
|
+
// A conversation row whose newest message was only photos, or other files.
|
|
118
|
+
lastMessagePhoto: 'Photo',
|
|
119
|
+
lastMessageFile: 'File',
|
|
120
|
+
// Before the visitor's own newest message in a conversation row.
|
|
121
|
+
lastMessageYou: 'You: {message}',
|
|
101
122
|
team: 'Team',
|
|
102
123
|
home: 'Home',
|
|
103
124
|
close: 'Close',
|
package/src/locales.ts
CHANGED
|
@@ -20,6 +20,10 @@ const ar: LabelMap = {
|
|
|
20
20
|
loadEarlier: 'تحميل الرسائل السابقة',
|
|
21
21
|
emptyConversation: 'أرسل رسالة لبدء المحادثة',
|
|
22
22
|
messagePlaceholder: 'اكتب رسالة...',
|
|
23
|
+
sendMessage: 'إرسال الرسالة',
|
|
24
|
+
jumpToLatest: 'الانتقال إلى أحدث الرسائل',
|
|
25
|
+
jumpToLatestNew: 'رسائل جديدة: {count}. الانتقال إلى الأحدث',
|
|
26
|
+
morePhotos: 'صور أخرى: {count}',
|
|
23
27
|
attachFile: 'إرفاق ملف',
|
|
24
28
|
attachPhotoLibrary: 'مكتبة الصور',
|
|
25
29
|
attachTakePhoto: 'التقاط صورة',
|
|
@@ -29,15 +33,20 @@ const ar: LabelMap = {
|
|
|
29
33
|
attachmentTooMany: 'يمكنك إرفاق {count} ملفات كحد أقصى',
|
|
30
34
|
attachmentsTooLargeTogether: 'المرفقات كبيرة جدًا لإرسالها معًا — أرسل بعضها في رسالة أخرى',
|
|
31
35
|
attachmentFailed: 'تعذر إرفاق هذا الملف',
|
|
36
|
+
attachmentUploading: 'جارٍ رفع {name}',
|
|
37
|
+
attachmentUploadFailed: 'تعذر رفع {name}. اضغط لإعادة المحاولة',
|
|
38
|
+
attachmentsUploadFailed: 'تعذر رفع ملف. اضغط عليه لإعادة المحاولة',
|
|
32
39
|
openAttachment: 'فتح {name}',
|
|
33
40
|
voicePlay: 'تشغيل الرسالة الصوتية',
|
|
34
41
|
voicePause: 'إيقاف الرسالة الصوتية مؤقتًا',
|
|
35
42
|
voiceSpeed: 'سرعة التشغيل',
|
|
36
43
|
imageLoadFailed: 'تعذر تحميل هذه الصورة',
|
|
37
44
|
openImageExternally: 'فتح الصورة',
|
|
45
|
+
imagePosition: '{current} من {total}',
|
|
38
46
|
cameraPermissionDenied: 'اسمح بالوصول إلى الكاميرا من الإعدادات لالتقاط صورة',
|
|
39
47
|
cancel: 'إلغاء',
|
|
40
48
|
failedToSend: 'تعذر الإرسال',
|
|
49
|
+
tapToRetry: 'اضغط لإعادة المحاولة',
|
|
41
50
|
thinking: 'يكتب',
|
|
42
51
|
messages: 'الرسائل',
|
|
43
52
|
noMessages: 'لا توجد رسائل بعد',
|
|
@@ -47,13 +56,14 @@ const ar: LabelMap = {
|
|
|
47
56
|
preChatSubtitle: 'أخبرنا قليلاً عنك حتى نتمكن من مساعدتك بشكل أفضل.',
|
|
48
57
|
preChatSkip: 'تخطٍ',
|
|
49
58
|
preChatSubmit: 'ابدأ المحادثة',
|
|
50
|
-
|
|
51
|
-
homeRecentTitle: 'الأحدث',
|
|
52
|
-
homeViewAllCount: 'عرض الكل ({count})',
|
|
59
|
+
homeRecentMessageTitle: 'أحدث رسالة',
|
|
53
60
|
homeNoConversations: 'ستظهر محادثاتك هنا',
|
|
54
61
|
homeStatusOnline: 'نحن متصلون',
|
|
55
62
|
homeStatusAway: 'نحن غير متصلين',
|
|
56
63
|
unreadBadge: '{count} غير مقروءة',
|
|
64
|
+
lastMessagePhoto: 'صورة',
|
|
65
|
+
lastMessageFile: 'ملف',
|
|
66
|
+
lastMessageYou: 'أنت: {message}',
|
|
57
67
|
team: 'الفريق',
|
|
58
68
|
home: 'الرئيسية',
|
|
59
69
|
close: 'إغلاق',
|