@repzo/widget-react-native 1.3.1 → 1.5.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 +173 -5
- package/README.md +99 -13
- package/lib/commonjs/api/client.js +125 -28
- 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 +20 -29
- package/lib/commonjs/attachments/pickers.js.map +1 -1
- package/lib/commonjs/i18n.js +22 -3
- package/lib/commonjs/i18n.js.map +1 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/locales.js +12 -3
- package/lib/commonjs/locales.js.map +1 -1
- package/lib/commonjs/state/user-update-queue.js +168 -0
- package/lib/commonjs/state/user-update-queue.js.map +1 -0
- package/lib/commonjs/state/widget-store.js +303 -33
- 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 +33 -13
- 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 +229 -24
- 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/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 +651 -165
- package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/commonjs/ui/screens/chat/ChatView.styles.js +138 -1
- 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/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 +123 -27
- 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 +15 -25
- package/lib/module/attachments/pickers.js.map +1 -1
- package/lib/module/i18n.js +22 -3
- package/lib/module/i18n.js.map +1 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/locales.js +12 -3
- package/lib/module/locales.js.map +1 -1
- package/lib/module/state/user-update-queue.js +163 -0
- package/lib/module/state/user-update-queue.js.map +1 -0
- package/lib/module/state/widget-store.js +305 -35
- 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 +33 -13
- 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 +231 -26
- 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/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 +655 -169
- package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
- package/lib/module/ui/screens/chat/ChatView.styles.js +140 -2
- 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/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 +20 -6
- 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 +15 -2
- package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/commonjs/i18n.d.ts +12 -3
- package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
- package/lib/typescript/commonjs/index.d.ts +5 -0
- package/lib/typescript/commonjs/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/locales.d.ts.map +1 -1
- package/lib/typescript/commonjs/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/commonjs/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/commonjs/state/widget-store.d.ts +44 -1
- 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 +53 -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/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 +144 -0
- 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 +26 -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/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 +20 -6
- 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 +15 -2
- package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
- package/lib/typescript/module/i18n.d.ts +12 -3
- package/lib/typescript/module/i18n.d.ts.map +1 -1
- package/lib/typescript/module/index.d.ts +5 -0
- package/lib/typescript/module/index.d.ts.map +1 -1
- package/lib/typescript/module/locales.d.ts.map +1 -1
- package/lib/typescript/module/state/user-update-queue.d.ts +62 -0
- package/lib/typescript/module/state/user-update-queue.d.ts.map +1 -0
- package/lib/typescript/module/state/widget-store.d.ts +44 -1
- 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 +53 -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/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 +144 -0
- 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 +26 -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/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 +149 -38
- package/src/attachments/file-size.ts +42 -0
- package/src/attachments/local-previews.ts +32 -0
- package/src/attachments/pickers.ts +23 -25
- package/src/i18n.ts +22 -3
- package/src/index.tsx +5 -0
- package/src/locales.ts +12 -3
- package/src/state/user-update-queue.ts +178 -0
- package/src/state/widget-store.ts +346 -51
- package/src/storage.ts +2 -0
- package/src/types.ts +53 -0
- package/src/ui/WidgetProvider.tsx +35 -8
- package/src/ui/components/AttachmentChips.tsx +100 -29
- package/src/ui/components/ConversationRow.tsx +90 -9
- package/src/ui/components/ImageViewer.tsx +242 -24
- package/src/ui/components/RatingCard.tsx +5 -2
- package/src/ui/components/ZoomableImage.tsx +58 -5
- package/src/ui/hooks/use-retained-state.ts +35 -0
- package/src/ui/screens/chat/ChatView.styles.ts +139 -1
- package/src/ui/screens/chat/ChatView.tsx +735 -198
- package/src/ui/screens/chat/ChatView.types.ts +26 -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/utils/html.ts +26 -0
- package/src/ui/utils/image-load-queue.ts +108 -0
- package/src/ui/utils/inline-attachments.ts +71 -0
package/CHANGELOG.md
CHANGED
|
@@ -12,20 +12,188 @@ All notable changes to `@repzo/widget-react-native`, newest first.
|
|
|
12
12
|
|
|
13
13
|
## Unreleased
|
|
14
14
|
|
|
15
|
+
## 1.5.0 — 2026-11-06
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- **`updateUser()` is paced, like other messengers' SDKs.** An app that calls
|
|
20
|
+
it on a timer or on every location fix no longer sends every call. Values
|
|
21
|
+
Workstation already has aren't sent again. Up to 20 updates go out at once,
|
|
22
|
+
then one every 90 seconds (20 per 30 minutes). Calls in between are merged,
|
|
23
|
+
newest value per key, and sent on the next allowed update, so the latest
|
|
24
|
+
value of every attribute still arrives. If Workstation answers that it's
|
|
25
|
+
busy (HTTP 429), the SDK waits as long as it asks and sends again; after a
|
|
26
|
+
failed connection or a server error it tries again a minute later. New
|
|
27
|
+
`boot({ throttleUpdateUser })` option (default `true`). See **Upgrading**.
|
|
28
|
+
- **`RepzoWidgetError.retryAfter`.** On an HTTP 429, the seconds Workstation
|
|
29
|
+
asked to wait, when it said.
|
|
30
|
+
|
|
31
|
+
### Fixed
|
|
32
|
+
|
|
33
|
+
- **A message goes out once, however many times Send is tapped.** On a slow
|
|
34
|
+
or busy phone the chat could take a moment to react to Send. Every extra tap
|
|
35
|
+
in that moment sent the message again, so your team got it several times.
|
|
36
|
+
Now only the first tap sends it. Nothing to set up.
|
|
37
|
+
|
|
38
|
+
### Upgrading
|
|
39
|
+
|
|
40
|
+
- **`updateUser()` calls may be merged and sent later.** A call that's held
|
|
41
|
+
back resolves right away instead of waiting for, or failing on, the
|
|
42
|
+
request. A call that's sent straight away still rejects when the request
|
|
43
|
+
fails, except for a 429, which the SDK now retries itself. If your app
|
|
44
|
+
calls `updateUser()` often, you can drop your own retry logic. To send
|
|
45
|
+
every call straight away, as before:
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
await RepzoWidget.boot({ channelAccountId: '<widget id>', throttleUpdateUser: false });
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
## 1.4.0 — 2026-11-02
|
|
52
|
+
|
|
53
|
+
### Added
|
|
54
|
+
|
|
55
|
+
- **Several photos in one message show as an album,** like WhatsApp: one
|
|
56
|
+
square holding them all. Two sit side by side, three as one tall beside two
|
|
57
|
+
stacked, four in a grid, and with more than four the last tile shows how
|
|
58
|
+
many more there are ("+3"). Tapping any tile opens the photos at that one.
|
|
59
|
+
Other files in the message still show below. New `boot({ photoAlbums })`
|
|
60
|
+
option (default `true`): `false` shows each photo on its own, as before.
|
|
61
|
+
See **Upgrading**.
|
|
62
|
+
- **Swipe between a message's photos.** When a message has several photos,
|
|
63
|
+
sent or received, tapping one opens them all in the full-screen viewer.
|
|
64
|
+
Swipe left or right to move between them (mirrored in Arabic); the header
|
|
65
|
+
shows which one is open, like "2 / 4", and screen readers move between them
|
|
66
|
+
with the adjust gesture. The next photo is already drawn as it slides in
|
|
67
|
+
and stays on screen while its full-size copy loads, so a swipe never shows
|
|
68
|
+
a black frame. Pinch and double-tap zoom work as before. Nothing to set up.
|
|
69
|
+
- **Photos in the chat show a loader until they've loaded.** A small spinner
|
|
70
|
+
sits over each photo, and each album tile, while it downloads or waits its
|
|
71
|
+
turn, instead of an empty box. A photo that loads straight away (from the
|
|
72
|
+
cache) never shows it. Nothing to set up.
|
|
73
|
+
- **Choose when picked files upload.** New
|
|
74
|
+
`boot({ attachments: { uploadOnPick } })` option (default `true`). Set it to
|
|
75
|
+
`false` to upload files only when the visitor taps Send, so nothing leaves
|
|
76
|
+
the phone for a file they pick and then remove.
|
|
77
|
+
- **"You:" before the visitor's own last message.** On Home and in Messages,
|
|
78
|
+
a conversation whose newest message is the visitor's shows "You: " before
|
|
79
|
+
it ("You: Photo" for a photo), so it's clear who spoke last. Arabic:
|
|
80
|
+
"أنت: ". Workspaces can change it with the `lastMessageYou` translation key,
|
|
81
|
+
which keeps `{message}`. Workstation's list doesn't say who sent the newest
|
|
82
|
+
message, so the SDK remembers it from what this device sent, received or
|
|
83
|
+
loaded, including across launches; a conversation last written in
|
|
84
|
+
elsewhere shows no "You:". `Conversation` has a new optional
|
|
85
|
+
`lastMessageFromVisitor` field; a Workstation list that sends it is used
|
|
86
|
+
as is. New `boot({ youPrefix })` option (default `true`): `false` shows the
|
|
87
|
+
message without "You:", as before. See **Upgrading**.
|
|
88
|
+
|
|
15
89
|
### Changed
|
|
16
90
|
|
|
91
|
+
- **A conversation opens on its newest message.** The thread starts at the
|
|
92
|
+
bottom, with the latest messages drawn first, instead of rendering from the
|
|
93
|
+
top and then jumping down. Older messages load on their own as the visitor
|
|
94
|
+
scrolls up, without moving what they're reading, so there's no need to tap
|
|
95
|
+
**Load earlier messages**. That row stays at the top while a page loads, or
|
|
96
|
+
to try again if one didn't. A reply that arrives while they read further up
|
|
97
|
+
doesn't move the text either. Once the visitor has scrolled up, a button in
|
|
98
|
+
the corner takes them back to the newest message and shows how many
|
|
99
|
+
teammate replies arrived in the meantime. Nothing to set up.
|
|
17
100
|
- **Visitors can attach files up to 50 MB.** With a Workstation server that
|
|
18
|
-
takes uploads, a picked file is uploaded
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
101
|
+
takes uploads, a picked file is uploaded on its own instead of being sent
|
|
102
|
+
inside the message, so the 1.5 MB cap per file (6 MB per message) is gone;
|
|
103
|
+
the server sets the cap (50 MB a file, up to five files). With
|
|
104
|
+
`expo-file-system` installed, a file is sent straight from the phone's
|
|
105
|
+
storage as it uploads, so even a 50 MB file doesn't fill the app's memory.
|
|
106
|
+
Without it the file is read into memory first, which can fail on Android
|
|
107
|
+
for files over about 30 MB. The "File is too large" notice shows the
|
|
108
|
+
server's cap. Nothing else to set up.
|
|
109
|
+
- **Files upload as soon as they're picked.** A spinner shows on each file
|
|
110
|
+
in the message field while it uploads, and the send button turns on once
|
|
111
|
+
every file is up, so the message goes out at once. The upload happens while
|
|
112
|
+
the visitor writes. A file that fails to upload shows **Tap to retry**, and
|
|
113
|
+
removing a file cancels its upload. If the visitor waits so long to send
|
|
114
|
+
that Workstation no longer keeps the upload (about an hour), the file
|
|
115
|
+
uploads again as the message goes out. With an older Workstation that
|
|
116
|
+
doesn't take uploads, nothing changes. To keep uploading on Send, see
|
|
117
|
+
**Upgrading**.
|
|
118
|
+
- **Home shows the latest conversation in a "Recent message" card.** The
|
|
119
|
+
newest conversation now sits in its own card above **Send us a message**:
|
|
120
|
+
who replied, how long ago and the last thing said, with the unread count
|
|
121
|
+
when there is one. Tapping it opens the conversation. It replaces the
|
|
122
|
+
"Recent" list of two conversations and its **View all** link that sat under
|
|
123
|
+
the start card; every conversation is still one tap away in the Messages
|
|
124
|
+
tab. The card follows the workspace's "show previous conversations" setting
|
|
125
|
+
like the list did. Arabic: "أحدث رسالة". Workspaces can change the title
|
|
126
|
+
with the `homeRecentMessageTitle` translation key. Nothing to set up.
|
|
127
|
+
|
|
128
|
+
### Fixed
|
|
129
|
+
|
|
130
|
+
- **Images in a team reply show where the teammate put them.** A reply with
|
|
131
|
+
text, an image, more text and another image now shows them in that order,
|
|
132
|
+
like the web Messenger, instead of all the text first and every image after
|
|
133
|
+
it. Links and formatting in the text between images are kept.
|
|
134
|
+
- **A photo the visitor sends stays a photo.** Right after sending, a photo
|
|
135
|
+
could turn into a file row until the conversation was opened again. It now
|
|
136
|
+
keeps showing from the phone until the next time the conversation loads.
|
|
137
|
+
An attachment that arrives without a file type shows as what its name says,
|
|
138
|
+
so a photo shows as a photo.
|
|
139
|
+
- **Photos keep showing in long conversations on Android.** A conversation
|
|
140
|
+
with a few large photos could use up the memory Android sets aside for
|
|
141
|
+
images. Later photos, in the chat and elsewhere in your app, then showed as
|
|
142
|
+
file rows or didn't load. Photos in the chat are now decoded at the size
|
|
143
|
+
they're shown, and photos over 10 MB download two at a time, newest first,
|
|
144
|
+
so several of them on screen at once all show instead of all failing.
|
|
145
|
+
Ordinary photos still load straight away. iOS wasn't affected.
|
|
146
|
+
- **Screen readers name the send button.** VoiceOver and TalkBack read
|
|
147
|
+
"Send message" (Arabic: "إرسال الرسالة") for the button beside the message
|
|
148
|
+
field, instead of just "button". Workspaces can change the text with the
|
|
149
|
+
`sendMessage` translation key.
|
|
150
|
+
- **The conversation list shows the reply that just arrived.** On Home and
|
|
151
|
+
in Messages, a reply that came in while the visitor was on those screens
|
|
152
|
+
could leave the row showing the visitor's own last message, beside the
|
|
153
|
+
unread badge. This happened when the reply was only a photo or file, or
|
|
154
|
+
came from Workstation as formatted text only. The row now shows the
|
|
155
|
+
reply's words, or "Photo" or "File" (Arabic: "صورة", "ملف") when it had
|
|
156
|
+
none. Workspaces can change these with the `lastMessagePhoto` and
|
|
157
|
+
`lastMessageFile` translation keys. A conversation the visitor starts with
|
|
158
|
+
only attachments shows the same instead of "1 attachment". For apps that
|
|
159
|
+
read conversations from `MessageDidArrive`, `snippet` stays text: after a
|
|
160
|
+
reply with only photos or files it now says "2 attachments", the way
|
|
161
|
+
Workstation's list words it, instead of keeping the previous message's
|
|
162
|
+
words. A new optional `lastMessageAttachment` field (`'photo'`, `'file'`
|
|
163
|
+
or `null`) says which it was.
|
|
164
|
+
- **The conversation list shows what the visitor just sent.** After replying
|
|
165
|
+
in a conversation and going back to Home or Messages, the row still showed
|
|
166
|
+
the team's last message until the list was pulled to refresh. It now shows
|
|
167
|
+
the visitor's message, and its time, as soon as it's sent.
|
|
23
168
|
|
|
24
169
|
### Older versions
|
|
25
170
|
|
|
26
171
|
- Apps on earlier versions keep the 1.5 MB cap. With an older server, this
|
|
27
172
|
version keeps it too.
|
|
28
173
|
|
|
174
|
+
### Upgrading
|
|
175
|
+
|
|
176
|
+
- **Picked files now upload before Send.** Nothing to do. To upload them only
|
|
177
|
+
when the visitor taps Send, as before:
|
|
178
|
+
|
|
179
|
+
```ts
|
|
180
|
+
await RepzoWidget.boot({ channelAccountId: '<widget id>', attachments: { uploadOnPick: false } });
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
- **Several photos in one message show as an album.** Nothing to do. To keep
|
|
184
|
+
showing each photo on its own, full width:
|
|
185
|
+
|
|
186
|
+
```ts
|
|
187
|
+
await RepzoWidget.boot({ channelAccountId: '<widget id>', photoAlbums: false });
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
- **"You:" before the visitor's own last message** on Home and in Messages.
|
|
191
|
+
Nothing to do. To show the message alone, as before:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
await RepzoWidget.boot({ channelAccountId: '<widget id>', youPrefix: false });
|
|
195
|
+
```
|
|
196
|
+
|
|
29
197
|
## 1.3.1 — 2026-10-29
|
|
30
198
|
|
|
31
199
|
### Added
|
package/README.md
CHANGED
|
@@ -25,7 +25,11 @@ in your Repzo Inbox and agent replies arrive live.
|
|
|
25
25
|
- Sent/Seen receipts, tap to retry a message that didn't send (*1.3.1+*), teammate avatars and presence, deleted-message placeholders
|
|
26
26
|
- Catches up after a dropped connection or a trip to the background — no missed or duplicated replies
|
|
27
27
|
- Agent UI blocks: product cards, slot pickers, lead forms, quotes, receipts
|
|
28
|
-
- **Image viewer** with pinch and double-tap zoom for photos in the conversation
|
|
28
|
+
- **Image viewer** with pinch and double-tap zoom for photos in the conversation; swipe between a message's photos (*1.4.0+*)
|
|
29
|
+
- **Photo albums**: several photos in one message share one square, WhatsApp-style (*1.4.0+*; `photoAlbums: false` turns them off)
|
|
30
|
+
- Conversations open on the newest message, load older ones as the visitor scrolls up, and offer a jump-to-latest button (*1.4.0+*)
|
|
31
|
+
- Team replies keep their images where the teammate put them, between the text (*1.4.0+*)
|
|
32
|
+
- Conversation lists say "You:" before the visitor's own last message, and "Photo" or "File" for one without words (*1.4.0+*; `youPrefix: false` turns "You:" off)
|
|
29
33
|
- **Photo and file attachments** (optional — with `expo-image-picker` / `expo-document-picker`, or your own picker)
|
|
30
34
|
- **Voice messages play in the chat**, with the phone's own audio player — nothing to install in Expo apps
|
|
31
35
|
- **In-app notification cards** for new replies while the messenger is closed
|
|
@@ -46,7 +50,7 @@ in your Repzo Inbox and agent replies arrive live.
|
|
|
46
50
|
| `react-native-safe-area-context` | optional — safe-area insets from the host provider |
|
|
47
51
|
| `expo-image-picker` | optional — "Photo library" and "Take photo" attachments (v16+ / SDK 52+) |
|
|
48
52
|
| `expo-document-picker` | optional — "File" attachments |
|
|
49
|
-
| `expo-file-system` | optional — reads picked files as base64 (falls back to `fetch` without it) |
|
|
53
|
+
| `expo-file-system` | optional — reads picked files as base64 (falls back to `fetch` without it), and (*1.4.0+*) lets large attachments upload straight from storage (recommended) |
|
|
50
54
|
| `expo-image-manipulator` | optional — shrinks photos so camera shots fit the 1.5 MB attachment limit |
|
|
51
55
|
| `expo-audio` | optional (*1.2.2+*) — no longer needed from *1.3.0+* in apps with Expo modules (see "Voice messages"); still used where the SDK's own player isn't built in |
|
|
52
56
|
| `expo-crypto` | optional (*1.1.0+*) — secure random source for the anonymous device id when the runtime has no `crypto` global (see below) |
|
|
@@ -320,7 +324,7 @@ npx expo install expo-image-picker expo-document-picker expo-file-system expo-im
|
|
|
320
324
|
| --- | --- |
|
|
321
325
|
| `expo-image-picker` | Photo library, Take photo |
|
|
322
326
|
| `expo-document-picker` | File |
|
|
323
|
-
| `expo-file-system` | nothing new — reads picked files faster (SDK 53's `readAsStringAsync` and SDK 54+'s `File` both work) |
|
|
327
|
+
| `expo-file-system` | nothing new — reads picked files faster (SDK 53's `readAsStringAsync` and SDK 54+'s `File` both work), and (*1.4.0+*) uploads large files straight from storage. Without it a file is read into memory before it uploads, which can fail on Android for files over about 30 MB (recommended) |
|
|
324
328
|
| `expo-image-manipulator` | nothing new — shrinks photos so camera shots fit the size limit (recommended) |
|
|
325
329
|
|
|
326
330
|
With none of them (and no `pick` below) the button isn't shown. It is also
|
|
@@ -334,15 +338,25 @@ them. Taking photos needs `NSCameraUsageDescription` in `Info.plist` (the
|
|
|
334
338
|
gets a clear message instead of a silently missing file:
|
|
335
339
|
|
|
336
340
|
- 5 files per message
|
|
337
|
-
- 1.
|
|
338
|
-
|
|
339
|
-
|
|
341
|
+
- *1.4.0+.* 50 MB per file, the cap Workstation sets. Each file starts uploading as
|
|
342
|
+
soon as it's picked, with a spinner on its thumbnail, and Send turns on
|
|
343
|
+
once every file is up, so the message then goes out at once. A file that
|
|
344
|
+
fails to upload shows **Tap to retry**; removing it cancels its upload.
|
|
345
|
+
A file picked about an hour before Send uploads again as the message goes
|
|
346
|
+
out, since Workstation stops keeping an unsent upload by then.
|
|
347
|
+
- Before 1.4.0, or with an older Workstation that doesn't take uploads,
|
|
348
|
+
files travel base64-encoded inside the message instead: 1.5 MB per file,
|
|
349
|
+
and 6 MB for all files in one message together.
|
|
350
|
+
|
|
351
|
+
*1.4.0+.* To upload the files only when the visitor taps Send, so nothing
|
|
352
|
+
leaves the phone for a file they pick and then remove, pass
|
|
353
|
+
`attachments: { uploadOnPick: false }`. Send then stays on while files are
|
|
354
|
+
attached, and the message goes out once its files are up.
|
|
340
355
|
|
|
341
356
|
With `expo-image-manipulator` installed, photos are scaled to at most 1600 px
|
|
342
|
-
on the longest edge and saved as 70% JPEG — typically 200–500 KB, so
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
a message. A message can be attachments only,
|
|
357
|
+
on the longest edge and saved as 70% JPEG — typically 200–500 KB, so they
|
|
358
|
+
upload quickly and fit the 1.5 MB cap of an older Workstation. Without it
|
|
359
|
+
they're only compressed. A message can be attachments only,
|
|
346
360
|
with no text. If a send fails, the attachments stay in the composer so the
|
|
347
361
|
visitor can retry.
|
|
348
362
|
|
|
@@ -351,6 +365,19 @@ full-screen viewer right away, from the copy on the phone. A PDF or other file
|
|
|
351
365
|
opens in the phone's viewer once Workstation has stored it, usually a moment
|
|
352
366
|
after it's sent.
|
|
353
367
|
|
|
368
|
+
*1.4.0+.* When a message has several photos, sent or received, they show
|
|
369
|
+
together as an album in one square, WhatsApp-style, with "+N" on the last
|
|
370
|
+
tile when there are more than four. Tapping one opens them all in the
|
|
371
|
+
viewer: swipe sideways to move between them (the direction follows the
|
|
372
|
+
app's language), and the header shows which one is open. To show each photo
|
|
373
|
+
on its own, full width, as before, pass `photoAlbums: false`:
|
|
374
|
+
|
|
375
|
+
```ts
|
|
376
|
+
await RepzoWidget.boot({ channelAccountId: '…', photoAlbums: false });
|
|
377
|
+
```
|
|
378
|
+
|
|
379
|
+
Tapping a photo still opens the message's photos in the viewer.
|
|
380
|
+
|
|
354
381
|
### Your own picker
|
|
355
382
|
|
|
356
383
|
Apps that use `react-native-image-picker`, `react-native-document-picker` or
|
|
@@ -434,6 +461,63 @@ get a plain message and no Retry.
|
|
|
434
461
|
Expired or revoked tokens (401) are handled silently: the SDK re-inits once
|
|
435
462
|
(shared by all concurrent requests) and retries.
|
|
436
463
|
|
|
464
|
+
*1.5.0+.* When Workstation answers that it's busy (HTTP 429), the error's
|
|
465
|
+
`retryAfter` holds the seconds it asked to wait, when it said.
|
|
466
|
+
|
|
467
|
+
## Custom attributes
|
|
468
|
+
|
|
469
|
+
`updateUser()` saves attributes on the visitor so agents see them on the
|
|
470
|
+
conversation, for example a plan, a role or which part of your app they're in:
|
|
471
|
+
|
|
472
|
+
```ts
|
|
473
|
+
await RepzoWidget.updateUser({ plan: 'pro', role: 'manager' });
|
|
474
|
+
```
|
|
475
|
+
|
|
476
|
+
Up to 30 keys, keys up to 64 characters, values (text, numbers or true/false)
|
|
477
|
+
up to 512 characters. Each call only changes the keys it sends; the others
|
|
478
|
+
keep their saved values.
|
|
479
|
+
|
|
480
|
+
*1.5.0+.* Calls are paced so an app can't flood Workstation:
|
|
481
|
+
|
|
482
|
+
- Values Workstation already has aren't sent again.
|
|
483
|
+
- Up to 20 updates go out at once, then one every 90 seconds (20 per 30
|
|
484
|
+
minutes).
|
|
485
|
+
- Calls in between are merged, newest value per key, and sent on the next
|
|
486
|
+
allowed update. They resolve right away. The latest value of every
|
|
487
|
+
attribute still arrives.
|
|
488
|
+
- When Workstation answers that it's busy, the SDK waits as long as it asks
|
|
489
|
+
and sends again. After a failed connection or a server error it tries again
|
|
490
|
+
a minute later, and the call that failed still rejects.
|
|
491
|
+
|
|
492
|
+
Send what agents need to see, not live data. A value that changes every few
|
|
493
|
+
seconds, such as a GPS position, only arrives about every 90 seconds once the
|
|
494
|
+
first 20 updates are used. `boot({ throttleUpdateUser: false })` sends every
|
|
495
|
+
call straight away, as before 1.5.0.
|
|
496
|
+
|
|
497
|
+
## Conversation list
|
|
498
|
+
|
|
499
|
+
*1.4.0+.* On Home and in Messages, each conversation shows its newest
|
|
500
|
+
message. When the visitor sent it, "You: " comes first (Arabic "أنت: "), and a
|
|
501
|
+
message with no words shows as "Photo" or "File" (Arabic "صورة", "ملف").
|
|
502
|
+
Workspaces can change these with the `lastMessageYou` (keeps `{message}`),
|
|
503
|
+
`lastMessagePhoto` and `lastMessageFile` translation keys.
|
|
504
|
+
|
|
505
|
+
Workstation's list doesn't say who sent the newest message, so the SDK
|
|
506
|
+
remembers it from what this device sent, received or loaded, across launches
|
|
507
|
+
too. A conversation last written in on another device shows no "You:". To
|
|
508
|
+
show the message without "You:", as before 1.4.0:
|
|
509
|
+
|
|
510
|
+
```ts
|
|
511
|
+
await RepzoWidget.boot({ channelAccountId: '…', youPrefix: false });
|
|
512
|
+
```
|
|
513
|
+
|
|
514
|
+
Conversations in the `MessageDidArrive` payload carry the same information in
|
|
515
|
+
two optional fields: `lastMessageFromVisitor` (`true` when the visitor sent
|
|
516
|
+
the newest message) and `lastMessageAttachment` (`'photo'` or `'file'` when it
|
|
517
|
+
had no words, otherwise `null`). `snippet` stays text, worded the way
|
|
518
|
+
Workstation's list words it ("2 attachments"). Both fields are filled in
|
|
519
|
+
whether or not `youPrefix` is on.
|
|
520
|
+
|
|
437
521
|
## In-app notifications
|
|
438
522
|
|
|
439
523
|
When a team reply arrives while the app is open but the messenger is closed,
|
|
@@ -686,7 +770,7 @@ sub.remove();
|
|
|
686
770
|
| Event | Payload | Use it for |
|
|
687
771
|
| --- | --- | --- |
|
|
688
772
|
| `UnreadCountDidChange` | `{ count }` | Badge on your own chat button. `count` is the number of unread team replies across all conversations; it drops only when the visitor opens a thread (opening the messenger alone doesn't clear it). Counts are server-backed, so replies that arrived while the app was killed are included after a cold start. |
|
|
689
|
-
| `MessageDidArrive` | `{ conversationId, message, conversation }` | Your own in-app banner while the app is foregrounded (no push is sent then). Fires only for team replies that count as unread — never the visitor's echo, never system rows like "agent joined", never the thread already open. |
|
|
773
|
+
| `MessageDidArrive` | `{ conversationId, message, conversation }` | Your own in-app banner while the app is foregrounded (no push is sent then). Fires only for team replies that count as unread — never the visitor's echo, never system rows like "agent joined", never the thread already open. *1.4.0+:* `conversation.lastMessageFromVisitor` and `conversation.lastMessageAttachment` say who sent the newest message and whether it was only photos or files (see "Conversation list"). |
|
|
690
774
|
| `ConversationDidRead` | `{ conversationId }` | Dismissing that conversation's push notification (example above). |
|
|
691
775
|
| `WindowDidShow` / `WindowDidHide` | — | Pause your own UI (tours, toasts) while the messenger is open. |
|
|
692
776
|
|
|
@@ -694,10 +778,10 @@ sub.remove();
|
|
|
694
778
|
|
|
695
779
|
| Method | Notes |
|
|
696
780
|
| --- | --- |
|
|
697
|
-
| `boot({ channelAccountId, baseUrl?, user?, push?, onOpenUrl?, attachments?, inAppNotifications?, posts? })` | Mints/resumes the session and connects the live stream. Idempotent; safe to call repeatedly. |
|
|
781
|
+
| `boot({ channelAccountId, baseUrl?, user?, push?, onOpenUrl?, attachments?, inAppNotifications?, posts?, photoAlbums?, youPrefix?, throttleUpdateUser? })` | Mints/resumes the session and connects the live stream. Idempotent; safe to call repeatedly. |
|
|
698
782
|
| `loginUserWithUserAttributes({ userId?, email?, name?, phone?, userHash?, account?, signedUpAt? })` | Identifies the visitor; moves anonymous history over. `account: { id, name?, website? }` links them to a CRM account matched by `id`; `signedUpAt` (Date, unix seconds or ISO) shows as "Signed up". A different stored user is signed out first. |
|
|
699
783
|
| `loginUnidentifiedUser()` | Drops the identity (signing out a stored user), fresh anonymous session. |
|
|
700
|
-
| `updateUser(attributes)` | Custom attributes agents see on the conversation (≤30 keys, 64-char keys, 512-char values). |
|
|
784
|
+
| `updateUser(attributes)` | Custom attributes agents see on the conversation (≤30 keys, 64-char keys, 512-char values). *1.5.0+:* paced, see "Custom attributes". |
|
|
701
785
|
| `logout()` | Revokes the token, removes the push registration, wipes visitor data, rotates the device id, resets the UI. Await it on sign-out. |
|
|
702
786
|
| `present()` / `presentSpace('home' \| 'messages')` / `presentConversation(id)` / `hide()` | UI commands. |
|
|
703
787
|
| `getUnreadConversationCount()` | Number of conversations with at least one unread reply. |
|
|
@@ -740,6 +824,8 @@ action from you:
|
|
|
740
824
|
| 1.0.9 or older | Nothing required. Development builds now warn once if the visitor token isn't in secure storage, or if the device has no secure random source — see "Secure token storage" and "Requirements". |
|
|
741
825
|
| 1.1.0 or older | Posts from Workstation now show over the app by default, once one is set live for Mobile (`posts: false` turns them off). They need `<RepzoWidgetProvider>` mounted at the root. |
|
|
742
826
|
| 1.2.2 or older | Nothing required. In apps with Expo modules, the SDK now includes a small native voice player, so build the app again (development or store build) to get it; `expo-audio` can be removed if nothing else in your app uses it. If your `runtimeVersion` uses the `fingerprint` policy, it changes with this update. |
|
|
827
|
+
| 1.3.1 or older | Nothing required. With a Workstation that takes uploads, a picked file now starts uploading right away, before the visitor taps Send (`attachments: { uploadOnPick: false }` uploads on Send instead). Several photos in one message now show as an album (`photoAlbums: false` shows each on its own, as before). Home and Messages put "You:" before the visitor's own last message (`youPrefix: false` turns it off). |
|
|
828
|
+
| 1.4.0 or older | Nothing required. `updateUser()` is now paced: unchanged values are skipped, and past 20 updates in 30 minutes calls are merged and sent later (`throttleUpdateUser: false` sends every call, as before). See "Custom attributes". |
|
|
743
829
|
|
|
744
830
|
## Troubleshooting
|
|
745
831
|
|
|
@@ -5,6 +5,7 @@ Object.defineProperty(exports, "__esModule", {
|
|
|
5
5
|
});
|
|
6
6
|
exports.WidgetApiClient = exports.RepzoWidgetError = void 0;
|
|
7
7
|
exports.isTerminalErrorCode = isTerminalErrorCode;
|
|
8
|
+
var _fileSize = require("../attachments/file-size.js");
|
|
8
9
|
var _storage = require("../storage.js");
|
|
9
10
|
var _safeUrl = require("../ui/utils/safe-url.js");
|
|
10
11
|
/**
|
|
@@ -23,24 +24,91 @@ function isTerminalErrorCode(code) {
|
|
|
23
24
|
return !!code && TERMINAL_CODES.has(code);
|
|
24
25
|
}
|
|
25
26
|
class RepzoWidgetError extends Error {
|
|
26
|
-
|
|
27
|
+
/**
|
|
28
|
+
* Seconds Workstation asked to wait before trying again, on a 429
|
|
29
|
+
* (`Retry-After`). Undefined when it didn't say.
|
|
30
|
+
* @since 1.5.0
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
constructor(message, status, code = 'http', retryAfter) {
|
|
27
34
|
super(message);
|
|
28
35
|
this.name = 'RepzoWidgetError';
|
|
29
36
|
this.status = status;
|
|
30
37
|
this.code = code;
|
|
38
|
+
if (retryAfter !== undefined) this.retryAfter = retryAfter;
|
|
31
39
|
}
|
|
32
40
|
|
|
33
41
|
/** Builds the error for a non-2xx `{ message, code }` body. */
|
|
34
|
-
static fromResponse(status, body) {
|
|
42
|
+
static fromResponse(status, body, retryAfterHeader) {
|
|
35
43
|
const payload = body && typeof body === 'object' ? body : {};
|
|
36
44
|
const message = typeof payload.message === 'string' && payload.message ? payload.message : null;
|
|
37
45
|
const code = status === 403 && typeof payload.code === 'string' && isTerminalErrorCode(payload.code) ? payload.code : 'http';
|
|
38
|
-
|
|
46
|
+
const retryAfter = status === 429 ? parseRetryAfter(retryAfterHeader, payload.retryAfter) : undefined;
|
|
47
|
+
return new RepzoWidgetError(message || `HTTP ${status}`, status, code, retryAfter);
|
|
39
48
|
}
|
|
40
49
|
}
|
|
41
50
|
|
|
42
|
-
/**
|
|
51
|
+
/**
|
|
52
|
+
* Seconds from a `Retry-After` header (seconds or an HTTP date), falling back
|
|
53
|
+
* to the `retryAfter` seconds Workstation also puts in the 429 body.
|
|
54
|
+
*/
|
|
43
55
|
exports.RepzoWidgetError = RepzoWidgetError;
|
|
56
|
+
function parseRetryAfter(header, bodyValue) {
|
|
57
|
+
const value = header?.trim();
|
|
58
|
+
if (value) {
|
|
59
|
+
if (/^\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
60
|
+
const at = Date.parse(value);
|
|
61
|
+
if (Number.isFinite(at)) return Math.max(0, (at - Date.now()) / 1000);
|
|
62
|
+
}
|
|
63
|
+
if (typeof bodyValue === 'number' && Number.isFinite(bodyValue) && bodyValue >= 0) return bodyValue;
|
|
64
|
+
return undefined;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** A Send message part asked for on its own, which may share one request with others. */
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Sends a local file as the request body straight from disk and resolves the
|
|
71
|
+
* HTTP status. React Native streams a `{ uri }` body natively; `fetch` would
|
|
72
|
+
* stringify it, so this goes through XMLHttpRequest.
|
|
73
|
+
*/
|
|
74
|
+
function sendFileFromDisk(url, method, headers, file, signal) {
|
|
75
|
+
return new Promise((resolve, reject) => {
|
|
76
|
+
const xhr = new XMLHttpRequest();
|
|
77
|
+
const cancel = () => xhr.abort();
|
|
78
|
+
const settle = finish => {
|
|
79
|
+
signal?.removeEventListener('abort', cancel);
|
|
80
|
+
finish();
|
|
81
|
+
};
|
|
82
|
+
xhr.onload = () => settle(() => resolve(xhr.status));
|
|
83
|
+
xhr.onerror = () => settle(() => reject(new RepzoWidgetError('Network request failed', 0, 'network')));
|
|
84
|
+
xhr.ontimeout = xhr.onerror;
|
|
85
|
+
xhr.onabort = () => settle(() => reject(new RepzoWidgetError('Upload cancelled', 0, 'network')));
|
|
86
|
+
xhr.open(method, url);
|
|
87
|
+
Object.entries(headers).forEach(([name, value]) => xhr.setRequestHeader(name, value));
|
|
88
|
+
// Android refuses a `{ uri }` body without one.
|
|
89
|
+
if (!Object.keys(headers).some(name => name.toLowerCase() === 'content-type')) {
|
|
90
|
+
xhr.setRequestHeader('Content-Type', file.contentType);
|
|
91
|
+
}
|
|
92
|
+
signal?.addEventListener('abort', cancel);
|
|
93
|
+
xhr.send({
|
|
94
|
+
uri: file.uri
|
|
95
|
+
});
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* How long Workstation keeps an upload for a message to name, when its answer
|
|
101
|
+
* has no usable `expiresAt` (deployments without it, or a phone clock that
|
|
102
|
+
* reads it as already past).
|
|
103
|
+
*/
|
|
104
|
+
const UPLOAD_LIFETIME_MS = 60 * 60 * 1000;
|
|
105
|
+
|
|
106
|
+
/** A file in storage, waiting for the message that names it. */
|
|
107
|
+
|
|
108
|
+
function uploadExpiry(expiresAt, now) {
|
|
109
|
+
const at = typeof expiresAt === 'string' ? Date.parse(expiresAt) : typeof expiresAt === 'number' ? expiresAt : NaN;
|
|
110
|
+
return at > now ? at : now + UPLOAD_LIFETIME_MS;
|
|
111
|
+
}
|
|
44
112
|
class WidgetApiClient {
|
|
45
113
|
baseUrl = '';
|
|
46
114
|
channelAccountId = '';
|
|
@@ -128,7 +196,7 @@ class WidgetApiClient {
|
|
|
128
196
|
}
|
|
129
197
|
if (!response.ok) {
|
|
130
198
|
const body = await response.json().catch(() => null);
|
|
131
|
-
const error = RepzoWidgetError.fromResponse(response.status, body);
|
|
199
|
+
const error = RepzoWidgetError.fromResponse(response.status, body, response.headers?.get?.('Retry-After'));
|
|
132
200
|
// skipReauth calls (init, logout-time) are handled by their caller,
|
|
133
201
|
// which knows whether the result is still current.
|
|
134
202
|
if (isTerminalErrorCode(error.code) && !options.skipReauth) this.onTerminalError?.(error);
|
|
@@ -282,41 +350,70 @@ class WidgetApiClient {
|
|
|
282
350
|
/**
|
|
283
351
|
* Puts a local file in storage ahead of the message, which then names it by
|
|
284
352
|
* the returned upload token. The target is presigned storage, or a path on
|
|
285
|
-
* the server where its storage can't presign. The
|
|
286
|
-
*
|
|
287
|
-
*
|
|
353
|
+
* the server where its storage can't presign. The upload URL is signed for
|
|
354
|
+
* exactly the size asked for: a `file://` file whose size `expo-file-system`
|
|
355
|
+
* can read streams from disk, anything else is read as a native blob and
|
|
356
|
+
* measured. `signal` cancels the transfer (the visitor removed the file, or
|
|
357
|
+
* left the conversation). Resolves the token and when it expires.
|
|
288
358
|
*/
|
|
289
|
-
async uploadAttachment(file) {
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
294
|
-
|
|
359
|
+
async uploadAttachment(file, signal) {
|
|
360
|
+
// A file removed early is never read, nor given a slot in storage.
|
|
361
|
+
const stopIfCancelled = () => {
|
|
362
|
+
if (signal?.aborted) throw new RepzoWidgetError('Upload cancelled', 0, 'network');
|
|
363
|
+
};
|
|
364
|
+
stopIfCancelled();
|
|
365
|
+
// Streaming matters on Android: React Native also copies a blob it reads
|
|
366
|
+
// into a Base64 string for its network inspector, so a file past ~30 MB
|
|
367
|
+
// runs the app out of memory. Only file:// gives an exact Content-Length.
|
|
368
|
+
const streamedSize = (0, _safeUrl.urlScheme)(file.uri) === 'file:' ? await (0, _fileSize.measureFileSize)(file.uri) : null;
|
|
369
|
+
let blob = null;
|
|
370
|
+
if (streamedSize === null) {
|
|
371
|
+
try {
|
|
372
|
+
blob = await (await fetch(file.uri, {
|
|
373
|
+
signal
|
|
374
|
+
})).blob();
|
|
375
|
+
} catch (error) {
|
|
376
|
+
throw new RepzoWidgetError(error instanceof Error ? error.message : 'Could not read the file', 0, 'network');
|
|
377
|
+
}
|
|
295
378
|
}
|
|
379
|
+
stopIfCancelled();
|
|
296
380
|
const target = await this.request('/api/widget/uploads', {
|
|
297
381
|
method: 'POST',
|
|
298
382
|
body: {
|
|
299
383
|
fileName: file.name,
|
|
300
384
|
contentType: file.contentType,
|
|
301
|
-
size: blob
|
|
385
|
+
size: streamedSize ?? (blob?.size || file.size)
|
|
302
386
|
}
|
|
303
387
|
});
|
|
388
|
+
// Counted from when the token was issued, not from when the file is up.
|
|
389
|
+
const expiresAt = uploadExpiry(target.expiresAt, Date.now());
|
|
304
390
|
const uploadUrl = this.resolveUrl(target.uploadUrl);
|
|
305
391
|
if (!uploadUrl) throw new RepzoWidgetError('Invalid upload target', 0, 'http');
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
392
|
+
stopIfCancelled();
|
|
393
|
+
const method = target.method ?? 'PUT';
|
|
394
|
+
const headers = target.headers ?? {
|
|
395
|
+
'Content-Type': file.contentType
|
|
396
|
+
};
|
|
397
|
+
let status;
|
|
398
|
+
if (blob) {
|
|
399
|
+
try {
|
|
400
|
+
status = (await fetch(uploadUrl, {
|
|
401
|
+
method,
|
|
402
|
+
headers,
|
|
403
|
+
body: blob,
|
|
404
|
+
signal
|
|
405
|
+
})).status;
|
|
406
|
+
} catch (error) {
|
|
407
|
+
throw new RepzoWidgetError(error instanceof Error ? error.message : 'Upload failed', 0, 'network');
|
|
408
|
+
}
|
|
409
|
+
} else {
|
|
410
|
+
status = await sendFileFromDisk(uploadUrl, method, headers, file, signal);
|
|
317
411
|
}
|
|
318
|
-
if (
|
|
319
|
-
return
|
|
412
|
+
if (status < 200 || status >= 300) throw new RepzoWidgetError('Upload failed', status, 'http');
|
|
413
|
+
return {
|
|
414
|
+
uploadToken: target.uploadToken,
|
|
415
|
+
expiresAt
|
|
416
|
+
};
|
|
320
417
|
}
|
|
321
418
|
listConversations() {
|
|
322
419
|
return this.request('/api/widget/conversations');
|