@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/src/api/client.ts
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
* The token itself lives in `SecureStorage`.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
+
import { measureFileSize } from '../attachments/file-size';
|
|
11
12
|
import { SecureStorage, TOKEN_KEY } from '../storage';
|
|
12
13
|
import type {
|
|
13
14
|
AgentUiAction,
|
|
@@ -40,26 +41,51 @@ export class RepzoWidgetError extends Error {
|
|
|
40
41
|
|
|
41
42
|
readonly code: RepzoWidgetErrorCode;
|
|
42
43
|
|
|
43
|
-
|
|
44
|
+
/**
|
|
45
|
+
* Seconds Workstation asked to wait before trying again, on a 429
|
|
46
|
+
* (`Retry-After`). Undefined when it didn't say.
|
|
47
|
+
* @since 1.5.0
|
|
48
|
+
*/
|
|
49
|
+
readonly retryAfter?: number;
|
|
50
|
+
|
|
51
|
+
constructor(message: string, status: number, code: RepzoWidgetErrorCode = 'http', retryAfter?: number) {
|
|
44
52
|
super(message);
|
|
45
53
|
this.name = 'RepzoWidgetError';
|
|
46
54
|
this.status = status;
|
|
47
55
|
this.code = code;
|
|
56
|
+
if (retryAfter !== undefined) this.retryAfter = retryAfter;
|
|
48
57
|
}
|
|
49
58
|
|
|
50
59
|
/** Builds the error for a non-2xx `{ message, code }` body. */
|
|
51
|
-
static fromResponse(status: number, body: unknown): RepzoWidgetError {
|
|
60
|
+
static fromResponse(status: number, body: unknown, retryAfterHeader?: string | null): RepzoWidgetError {
|
|
52
61
|
const payload = (body && typeof body === 'object' ? body : {}) as {
|
|
53
62
|
message?: unknown;
|
|
54
63
|
code?: unknown;
|
|
64
|
+
retryAfter?: unknown;
|
|
55
65
|
};
|
|
56
66
|
const message = typeof payload.message === 'string' && payload.message ? payload.message : null;
|
|
57
67
|
const code =
|
|
58
68
|
status === 403 && typeof payload.code === 'string' && isTerminalErrorCode(payload.code)
|
|
59
69
|
? (payload.code as RepzoWidgetErrorCode)
|
|
60
70
|
: 'http';
|
|
61
|
-
|
|
71
|
+
const retryAfter = status === 429 ? parseRetryAfter(retryAfterHeader, payload.retryAfter) : undefined;
|
|
72
|
+
return new RepzoWidgetError(message || `HTTP ${status}`, status, code, retryAfter);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Seconds from a `Retry-After` header (seconds or an HTTP date), falling back
|
|
78
|
+
* to the `retryAfter` seconds Workstation also puts in the 429 body.
|
|
79
|
+
*/
|
|
80
|
+
function parseRetryAfter(header: string | null | undefined, bodyValue: unknown): number | undefined {
|
|
81
|
+
const value = header?.trim();
|
|
82
|
+
if (value) {
|
|
83
|
+
if (/^\d+(\.\d+)?$/.test(value)) return Number(value);
|
|
84
|
+
const at = Date.parse(value);
|
|
85
|
+
if (Number.isFinite(at)) return Math.max(0, (at - Date.now()) / 1000);
|
|
62
86
|
}
|
|
87
|
+
if (typeof bodyValue === 'number' && Number.isFinite(bodyValue) && bodyValue >= 0) return bodyValue;
|
|
88
|
+
return undefined;
|
|
63
89
|
}
|
|
64
90
|
|
|
65
91
|
/** A Send message part asked for on its own, which may share one request with others. */
|
|
@@ -84,6 +110,60 @@ interface RequestOptions {
|
|
|
84
110
|
authToken?: string | null;
|
|
85
111
|
}
|
|
86
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Sends a local file as the request body straight from disk and resolves the
|
|
115
|
+
* HTTP status. React Native streams a `{ uri }` body natively; `fetch` would
|
|
116
|
+
* stringify it, so this goes through XMLHttpRequest.
|
|
117
|
+
*/
|
|
118
|
+
function sendFileFromDisk(
|
|
119
|
+
url: string,
|
|
120
|
+
method: string,
|
|
121
|
+
headers: Record<string, string>,
|
|
122
|
+
file: { uri: string; contentType: string },
|
|
123
|
+
signal?: AbortSignal
|
|
124
|
+
): Promise<number> {
|
|
125
|
+
return new Promise((resolve, reject) => {
|
|
126
|
+
const xhr = new XMLHttpRequest();
|
|
127
|
+
const cancel = () => xhr.abort();
|
|
128
|
+
const settle = (finish: () => void) => {
|
|
129
|
+
signal?.removeEventListener('abort', cancel);
|
|
130
|
+
finish();
|
|
131
|
+
};
|
|
132
|
+
xhr.onload = () => settle(() => resolve(xhr.status));
|
|
133
|
+
xhr.onerror = () => settle(() => reject(new RepzoWidgetError('Network request failed', 0, 'network')));
|
|
134
|
+
xhr.ontimeout = xhr.onerror;
|
|
135
|
+
xhr.onabort = () => settle(() => reject(new RepzoWidgetError('Upload cancelled', 0, 'network')));
|
|
136
|
+
xhr.open(method, url);
|
|
137
|
+
Object.entries(headers).forEach(([name, value]) => xhr.setRequestHeader(name, value));
|
|
138
|
+
// Android refuses a `{ uri }` body without one.
|
|
139
|
+
if (!Object.keys(headers).some((name) => name.toLowerCase() === 'content-type')) {
|
|
140
|
+
xhr.setRequestHeader('Content-Type', file.contentType);
|
|
141
|
+
}
|
|
142
|
+
signal?.addEventListener('abort', cancel);
|
|
143
|
+
xhr.send({ uri: file.uri } as unknown as XMLHttpRequestBodyInit);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* How long Workstation keeps an upload for a message to name, when its answer
|
|
149
|
+
* has no usable `expiresAt` (deployments without it, or a phone clock that
|
|
150
|
+
* reads it as already past).
|
|
151
|
+
*/
|
|
152
|
+
const UPLOAD_LIFETIME_MS = 60 * 60 * 1000;
|
|
153
|
+
|
|
154
|
+
/** A file in storage, waiting for the message that names it. */
|
|
155
|
+
export interface UploadedFile {
|
|
156
|
+
uploadToken: string;
|
|
157
|
+
/** When Workstation stops taking `uploadToken`, on this phone's clock (ms). */
|
|
158
|
+
expiresAt: number;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function uploadExpiry(expiresAt: unknown, now: number): number {
|
|
162
|
+
const at =
|
|
163
|
+
typeof expiresAt === 'string' ? Date.parse(expiresAt) : typeof expiresAt === 'number' ? expiresAt : NaN;
|
|
164
|
+
return at > now ? at : now + UPLOAD_LIFETIME_MS;
|
|
165
|
+
}
|
|
166
|
+
|
|
87
167
|
export class WidgetApiClient {
|
|
88
168
|
private baseUrl = '';
|
|
89
169
|
|
|
@@ -187,7 +267,11 @@ export class WidgetApiClient {
|
|
|
187
267
|
|
|
188
268
|
if (!response.ok) {
|
|
189
269
|
const body = await response.json().catch(() => null);
|
|
190
|
-
const error = RepzoWidgetError.fromResponse(
|
|
270
|
+
const error = RepzoWidgetError.fromResponse(
|
|
271
|
+
response.status,
|
|
272
|
+
body,
|
|
273
|
+
response.headers?.get?.('Retry-After')
|
|
274
|
+
);
|
|
191
275
|
// skipReauth calls (init, logout-time) are handled by their caller,
|
|
192
276
|
// which knows whether the result is still current.
|
|
193
277
|
if (isTerminalErrorCode(error.code) && !options.skipReauth) this.onTerminalError?.(error);
|
|
@@ -360,54 +444,81 @@ export class WidgetApiClient {
|
|
|
360
444
|
/**
|
|
361
445
|
* Puts a local file in storage ahead of the message, which then names it by
|
|
362
446
|
* the returned upload token. The target is presigned storage, or a path on
|
|
363
|
-
* the server where its storage can't presign. The
|
|
364
|
-
*
|
|
365
|
-
*
|
|
447
|
+
* the server where its storage can't presign. The upload URL is signed for
|
|
448
|
+
* exactly the size asked for: a `file://` file whose size `expo-file-system`
|
|
449
|
+
* can read streams from disk, anything else is read as a native blob and
|
|
450
|
+
* measured. `signal` cancels the transfer (the visitor removed the file, or
|
|
451
|
+
* left the conversation). Resolves the token and when it expires.
|
|
366
452
|
*/
|
|
367
|
-
async uploadAttachment(
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
453
|
+
async uploadAttachment(
|
|
454
|
+
file: {
|
|
455
|
+
uri: string;
|
|
456
|
+
name: string;
|
|
457
|
+
contentType: string;
|
|
458
|
+
size: number;
|
|
459
|
+
},
|
|
460
|
+
signal?: AbortSignal
|
|
461
|
+
): Promise<UploadedFile> {
|
|
462
|
+
// A file removed early is never read, nor given a slot in storage.
|
|
463
|
+
const stopIfCancelled = () => {
|
|
464
|
+
if (signal?.aborted) throw new RepzoWidgetError('Upload cancelled', 0, 'network');
|
|
465
|
+
};
|
|
466
|
+
stopIfCancelled();
|
|
467
|
+
// Streaming matters on Android: React Native also copies a blob it reads
|
|
468
|
+
// into a Base64 string for its network inspector, so a file past ~30 MB
|
|
469
|
+
// runs the app out of memory. Only file:// gives an exact Content-Length.
|
|
470
|
+
const streamedSize = urlScheme(file.uri) === 'file:' ? await measureFileSize(file.uri) : null;
|
|
471
|
+
let blob: Blob | null = null;
|
|
472
|
+
if (streamedSize === null) {
|
|
473
|
+
try {
|
|
474
|
+
blob = await (await fetch(file.uri, { signal })).blob();
|
|
475
|
+
} catch (error) {
|
|
476
|
+
throw new RepzoWidgetError(
|
|
477
|
+
error instanceof Error ? error.message : 'Could not read the file',
|
|
478
|
+
0,
|
|
479
|
+
'network'
|
|
480
|
+
);
|
|
481
|
+
}
|
|
382
482
|
}
|
|
483
|
+
stopIfCancelled();
|
|
383
484
|
const target = await this.request<{
|
|
384
485
|
uploadUrl: string;
|
|
385
486
|
method?: string;
|
|
386
487
|
headers?: Record<string, string>;
|
|
387
488
|
uploadToken: string;
|
|
489
|
+
expiresAt?: string | number;
|
|
388
490
|
}>('/api/widget/uploads', {
|
|
389
491
|
method: 'POST',
|
|
390
|
-
body: {
|
|
492
|
+
body: {
|
|
493
|
+
fileName: file.name,
|
|
494
|
+
contentType: file.contentType,
|
|
495
|
+
size: streamedSize ?? (blob?.size || file.size),
|
|
496
|
+
},
|
|
391
497
|
});
|
|
498
|
+
// Counted from when the token was issued, not from when the file is up.
|
|
499
|
+
const expiresAt = uploadExpiry(target.expiresAt, Date.now());
|
|
392
500
|
const uploadUrl = this.resolveUrl(target.uploadUrl);
|
|
393
501
|
if (!uploadUrl) throw new RepzoWidgetError('Invalid upload target', 0, 'http');
|
|
502
|
+
stopIfCancelled();
|
|
394
503
|
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
body: blob,
|
|
401
|
-
})
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
504
|
+
const method = target.method ?? 'PUT';
|
|
505
|
+
const headers = target.headers ?? { 'Content-Type': file.contentType };
|
|
506
|
+
let status: number;
|
|
507
|
+
if (blob) {
|
|
508
|
+
try {
|
|
509
|
+
status = (await fetch(uploadUrl, { method, headers, body: blob, signal })).status;
|
|
510
|
+
} catch (error) {
|
|
511
|
+
throw new RepzoWidgetError(
|
|
512
|
+
error instanceof Error ? error.message : 'Upload failed',
|
|
513
|
+
0,
|
|
514
|
+
'network'
|
|
515
|
+
);
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
status = await sendFileFromDisk(uploadUrl, method, headers, file, signal);
|
|
408
519
|
}
|
|
409
|
-
if (
|
|
410
|
-
return target.uploadToken;
|
|
520
|
+
if (status < 200 || status >= 300) throw new RepzoWidgetError('Upload failed', status, 'http');
|
|
521
|
+
return { uploadToken: target.uploadToken, expiresAt };
|
|
411
522
|
}
|
|
412
523
|
|
|
413
524
|
listConversations(): Promise<{ data: Conversation[] }> {
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local file sizes from the host's `expo-file-system` (optional, like the
|
|
3
|
+
* pickers): read from the file's metadata, never from its contents. Shared by
|
|
4
|
+
* the pickers and the upload client, so the client doesn't pull in pickers.
|
|
5
|
+
*
|
|
6
|
+
* No `react-native` import on purpose: it stays testable in plain Node.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
let fileSystem: any = null;
|
|
10
|
+
try {
|
|
11
|
+
fileSystem = require('expo-file-system') ?? null;
|
|
12
|
+
} catch {
|
|
13
|
+
fileSystem = null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** A positive, finite byte count, or null. */
|
|
17
|
+
export function validSize(size: unknown): number | null {
|
|
18
|
+
return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* A local file's exact size in bytes, from its metadata without reading it.
|
|
23
|
+
* Null without `expo-file-system`, or when it can't tell.
|
|
24
|
+
*/
|
|
25
|
+
export async function measureFileSize(uri: string): Promise<number | null> {
|
|
26
|
+
if (typeof fileSystem?.File === 'function') {
|
|
27
|
+
try {
|
|
28
|
+
const size = validSize(new fileSystem.File(uri).size);
|
|
29
|
+
if (size !== null) return size;
|
|
30
|
+
} catch {
|
|
31
|
+
// Fall through to the next reader.
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
if (typeof fileSystem?.getInfoAsync === 'function') {
|
|
35
|
+
try {
|
|
36
|
+
return validSize((await fileSystem.getInfoAsync(uri, { size: true }))?.size);
|
|
37
|
+
} catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The phone's copy of each file the visitor sent, by the URL of the stored
|
|
3
|
+
* copy that replaced it in the thread. Right after a send the stored copy
|
|
4
|
+
* may not load yet (Workstation can still be storing it), so a photo keeps
|
|
5
|
+
* showing from the phone instead of turning into a file row. Kept in memory
|
|
6
|
+
* only: a later session shows the stored copy.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const MAX_PREVIEWS = 50;
|
|
10
|
+
const previews = new Map<string, string>();
|
|
11
|
+
|
|
12
|
+
export function rememberLocalPreview(storedUrl: string, localUrl: string): void {
|
|
13
|
+
if (!storedUrl || !localUrl) return;
|
|
14
|
+
previews.delete(storedUrl);
|
|
15
|
+
previews.set(storedUrl, localUrl);
|
|
16
|
+
// Oldest first: a Map iterates in insertion order.
|
|
17
|
+
if (previews.size > MAX_PREVIEWS) previews.delete(previews.keys().next().value as string);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function localPreviewFor(storedUrl: string): string | null {
|
|
21
|
+
return (storedUrl && previews.get(storedUrl)) || null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** One that failed to load (the OS cleared the picker's cache): every view moves to the stored copy. */
|
|
25
|
+
export function forgetLocalPreview(storedUrl: string): void {
|
|
26
|
+
previews.delete(storedUrl);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Signing out forgets them: the next visitor on the phone doesn't inherit the paths. */
|
|
30
|
+
export function clearLocalPreviews(): void {
|
|
31
|
+
previews.clear();
|
|
32
|
+
}
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Visitor attachments: picking photos/files for `POST /api/widget/messages`.
|
|
3
3
|
*
|
|
4
4
|
* A server that takes uploads names its cap (`maxAttachmentBytes`): a picked
|
|
5
|
-
* file then stays a local URI
|
|
5
|
+
* file then stays a local URI, which the composer uploads right away. An
|
|
6
6
|
* older server only takes files inline, as base64 data URLs within the
|
|
7
7
|
* 1.5 MB / 6 MB caps below.
|
|
8
8
|
*
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import type { AttachmentSource, RepzoPickedFile, RepzoWidgetAttachmentOptions } from '../types';
|
|
27
|
+
import { measureFileSize, validSize } from './file-size';
|
|
27
28
|
|
|
28
29
|
/** The server keeps the first five and silently drops the rest. */
|
|
29
30
|
export const MAX_ATTACHMENTS = 5;
|
|
@@ -48,10 +49,19 @@ export interface DraftAttachment {
|
|
|
48
49
|
size: number;
|
|
49
50
|
/** `data:<mime>;base64,<…>` for an inline file; empty when `fileUri` is uploaded instead. */
|
|
50
51
|
dataUrl: string;
|
|
51
|
-
/** The local file uploaded
|
|
52
|
+
/** The local file uploaded ahead of the message. */
|
|
52
53
|
fileUri?: string;
|
|
53
54
|
/** What `<Image>` renders locally: the picked file's URI, else the data URL. */
|
|
54
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;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/** Why some (or all) of a pick didn't make it into the composer. */
|
|
@@ -119,6 +129,11 @@ export function setAttachmentOptions(options: RepzoWidgetAttachmentOptions | nul
|
|
|
119
129
|
hostOptions = options;
|
|
120
130
|
}
|
|
121
131
|
|
|
132
|
+
/** Whether a picked file starts uploading before Send (`attachments.uploadOnPick`). */
|
|
133
|
+
export function uploadsOnPick(): boolean {
|
|
134
|
+
return hostOptions?.uploadOnPick !== false;
|
|
135
|
+
}
|
|
136
|
+
|
|
122
137
|
const ALL_SOURCES: AttachmentSource[] = ['photo', 'camera', 'file'];
|
|
123
138
|
|
|
124
139
|
/**
|
|
@@ -356,7 +371,7 @@ async function toDraft(file: RepzoPickedFile): Promise<DraftAttachment> {
|
|
|
356
371
|
};
|
|
357
372
|
}
|
|
358
373
|
|
|
359
|
-
/** A file that's uploaded
|
|
374
|
+
/** A file that's uploaded on its own, ahead of the message: only its size is read here. */
|
|
360
375
|
async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promise<DraftAttachment> {
|
|
361
376
|
const uri = file.uri!;
|
|
362
377
|
const size = validSize(file.size) ?? (await readFileSize(uri));
|
|
@@ -375,23 +390,9 @@ async function toUploadDraft(file: RepzoPickedFile, uploadLimit: number): Promis
|
|
|
375
390
|
|
|
376
391
|
/** A local file's size in bytes, or 0 when nothing can tell. */
|
|
377
392
|
async function readFileSize(uri: string): Promise<number> {
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
if (size !== null) return size;
|
|
382
|
-
} catch {
|
|
383
|
-
// Fall through to the next reader.
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
if (typeof fileSystem?.getInfoAsync === 'function') {
|
|
387
|
-
try {
|
|
388
|
-
const size = validSize((await fileSystem.getInfoAsync(uri, { size: true }))?.size);
|
|
389
|
-
if (size !== null) return size;
|
|
390
|
-
} catch {
|
|
391
|
-
// Fall through to fetch.
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
// A native blob: its size is known without reading it into JS.
|
|
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).
|
|
395
396
|
const blob = await (await fetch(uri)).blob();
|
|
396
397
|
return validSize(blob.size) ?? 0;
|
|
397
398
|
}
|
|
@@ -524,10 +525,6 @@ export function isImageAttachment(contentType: string | null | undefined): boole
|
|
|
524
525
|
return !!contentType && contentType.toLowerCase().startsWith('image/');
|
|
525
526
|
}
|
|
526
527
|
|
|
527
|
-
function validSize(size: unknown): number | null {
|
|
528
|
-
return typeof size === 'number' && Number.isFinite(size) && size > 0 ? size : null;
|
|
529
|
-
}
|
|
530
|
-
|
|
531
528
|
function fileNameFromUri(uri: string | undefined): string | null {
|
|
532
529
|
if (!uri) return null;
|
|
533
530
|
const last = uri.split(/[?#]/)[0]?.split('/').pop();
|
|
@@ -563,7 +560,8 @@ const CONTENT_TYPES: Record<string, string> = {
|
|
|
563
560
|
mov: 'video/quicktime',
|
|
564
561
|
};
|
|
565
562
|
|
|
566
|
-
|
|
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 {
|
|
567
565
|
const extension = /\.([a-z0-9]+)$/i.exec(nameOrUri?.split(/[?#]/)[0] ?? '')?.[1];
|
|
568
566
|
return extension ? (CONTENT_TYPES[extension.toLowerCase()] ?? null) : null;
|
|
569
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,6 +92,8 @@ 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',
|
|
@@ -93,13 +108,17 @@ export const DEFAULT_LABELS = {
|
|
|
93
108
|
preChatSubtitle: 'Let us know a bit about you so we can help you better.',
|
|
94
109
|
preChatSkip: 'Skip',
|
|
95
110
|
preChatSubmit: 'Start chat',
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
homeViewAllCount: 'View all ({count})',
|
|
111
|
+
// Heading of Home's card that shows the newest conversation.
|
|
112
|
+
homeRecentMessageTitle: 'Recent message',
|
|
99
113
|
homeNoConversations: 'Your conversations will show up here',
|
|
100
114
|
homeStatusOnline: "We're online",
|
|
101
115
|
homeStatusAway: "We're away",
|
|
102
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}',
|
|
103
122
|
team: 'Team',
|
|
104
123
|
home: 'Home',
|
|
105
124
|
close: 'Close',
|
package/src/index.tsx
CHANGED
|
@@ -91,6 +91,11 @@ export type RepzoWidgetType = {
|
|
|
91
91
|
/**
|
|
92
92
|
* Attach custom attributes to the visitor's identity so agents see them on
|
|
93
93
|
* the conversation (max 30 keys, 64-char keys, 512-char values).
|
|
94
|
+
*
|
|
95
|
+
* Since 1.5.0 calls are paced (see `BootOptions.throttleUpdateUser`):
|
|
96
|
+
* values Workstation already has are skipped, and once more than 20 updates
|
|
97
|
+
* go out in 30 minutes the rest are merged and sent later. Those calls
|
|
98
|
+
* resolve right away. A call sent straight away still rejects when it fails.
|
|
94
99
|
*/
|
|
95
100
|
updateUser(attributes: WidgetCustomAttributes): Promise<void>;
|
|
96
101
|
|
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,12 +33,16 @@ 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: 'تعذر الإرسال',
|
|
@@ -48,13 +56,14 @@ const ar: LabelMap = {
|
|
|
48
56
|
preChatSubtitle: 'أخبرنا قليلاً عنك حتى نتمكن من مساعدتك بشكل أفضل.',
|
|
49
57
|
preChatSkip: 'تخطٍ',
|
|
50
58
|
preChatSubmit: 'ابدأ المحادثة',
|
|
51
|
-
|
|
52
|
-
homeRecentTitle: 'الأحدث',
|
|
53
|
-
homeViewAllCount: 'عرض الكل ({count})',
|
|
59
|
+
homeRecentMessageTitle: 'أحدث رسالة',
|
|
54
60
|
homeNoConversations: 'ستظهر محادثاتك هنا',
|
|
55
61
|
homeStatusOnline: 'نحن متصلون',
|
|
56
62
|
homeStatusAway: 'نحن غير متصلين',
|
|
57
63
|
unreadBadge: '{count} غير مقروءة',
|
|
64
|
+
lastMessagePhoto: 'صورة',
|
|
65
|
+
lastMessageFile: 'ملف',
|
|
66
|
+
lastMessageYou: 'أنت: {message}',
|
|
58
67
|
team: 'الفريق',
|
|
59
68
|
home: 'الرئيسية',
|
|
60
69
|
close: 'إغلاق',
|