turbodesk-livechat-react-native 0.1.0-alpha.4 → 0.1.0-alpha.5
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 +10 -0
- package/dist/hooks/use-send-message.d.ts +1 -0
- package/dist/hooks/use-send-message.d.ts.map +1 -1
- package/dist/hooks/use-send-message.js +20 -14
- package/dist/hooks/use-send-message.js.map +1 -1
- package/dist/navigation/LiveChatPanel.d.ts.map +1 -1
- package/dist/navigation/LiveChatPanel.js +1 -1
- package/dist/navigation/LiveChatPanel.js.map +1 -1
- package/dist/ui/components/ConversationScreen.d.ts.map +1 -1
- package/dist/ui/components/ConversationScreen.js +5 -2
- package/dist/ui/components/ConversationScreen.js.map +1 -1
- package/dist/ui/components/HomeScreen.d.ts.map +1 -1
- package/dist/ui/components/HomeScreen.js +8 -2
- package/dist/ui/components/HomeScreen.js.map +1 -1
- package/dist/ui/components/MessageComposer.d.ts.map +1 -1
- package/dist/ui/components/MessageComposer.js +74 -24
- package/dist/ui/components/MessageComposer.js.map +1 -1
- package/package.json +1 -1
- package/src/hooks/use-send-message.ts +169 -159
- package/src/navigation/LiveChatPanel.tsx +7 -3
- package/src/ui/components/ConversationScreen.tsx +6 -9
- package/src/ui/components/HomeScreen.tsx +12 -3
- package/src/ui/components/MessageComposer.tsx +97 -29
|
@@ -223,13 +223,15 @@ function FilePreviews({
|
|
|
223
223
|
|
|
224
224
|
// ── upload helper ─────────────────────────────────────────────────────────────
|
|
225
225
|
|
|
226
|
+
// Throws on any failure so the caller can surface the error and preserve the draft.
|
|
226
227
|
async function uploadAttachment(
|
|
227
228
|
pending: PendingAttachment,
|
|
228
229
|
visitorQueryParams: Record<string, string> | null
|
|
229
|
-
): Promise<UploadedAttachment
|
|
230
|
+
): Promise<UploadedAttachment> {
|
|
231
|
+
const ext = pending.name.split(".").pop() ?? "";
|
|
232
|
+
let presigned: any;
|
|
230
233
|
try {
|
|
231
|
-
|
|
232
|
-
const presigned = await fileApi.getFileUrl(
|
|
234
|
+
presigned = await fileApi.getFileUrl(
|
|
233
235
|
{
|
|
234
236
|
operation: "putObject",
|
|
235
237
|
name: pending.name,
|
|
@@ -238,27 +240,43 @@ async function uploadAttachment(
|
|
|
238
240
|
fileMode: "app",
|
|
239
241
|
},
|
|
240
242
|
visitorQueryParams ? { params: visitorQueryParams } : undefined
|
|
241
|
-
)
|
|
243
|
+
);
|
|
244
|
+
} catch {
|
|
245
|
+
throw new Error(`Failed to prepare upload for "${pending.name}"`);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const signedUrl = presigned?.data?.signedUrl;
|
|
249
|
+
if (!signedUrl) throw new Error(`No upload URL returned for "${pending.name}"`);
|
|
242
250
|
|
|
243
|
-
|
|
251
|
+
let putRes: Response;
|
|
252
|
+
try {
|
|
253
|
+
putRes = await fetch(signedUrl, {
|
|
244
254
|
method: "PUT",
|
|
245
255
|
headers: { "Content-Type": pending.mimeType },
|
|
246
256
|
body: { uri: pending.uri, type: pending.mimeType, name: pending.name } as any,
|
|
247
257
|
});
|
|
248
|
-
|
|
249
|
-
const uploaded = presigned?.data?.file;
|
|
250
|
-
return {
|
|
251
|
-
id: pending.id,
|
|
252
|
-
fileId: uploaded._id,
|
|
253
|
-
fileUrl: uploaded.url,
|
|
254
|
-
fileName: uploaded.name,
|
|
255
|
-
fileExtension: uploaded.extension,
|
|
256
|
-
fileType: pending.mimeType,
|
|
257
|
-
type: pending.kind,
|
|
258
|
-
};
|
|
259
258
|
} catch {
|
|
260
|
-
|
|
259
|
+
throw new Error(`Network error while uploading "${pending.name}"`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
if (!putRes.ok) {
|
|
263
|
+
throw new Error(`Upload failed for "${pending.name}" (HTTP ${putRes.status})`);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const uploaded = presigned?.data?.file;
|
|
267
|
+
if (!uploaded?._id) {
|
|
268
|
+
throw new Error(`Upload succeeded but server returned no file metadata for "${pending.name}"`);
|
|
261
269
|
}
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
id: pending.id,
|
|
273
|
+
fileId: uploaded._id,
|
|
274
|
+
fileUrl: uploaded.url,
|
|
275
|
+
fileName: uploaded.name,
|
|
276
|
+
fileExtension: uploaded.extension,
|
|
277
|
+
fileType: pending.mimeType,
|
|
278
|
+
type: pending.kind,
|
|
279
|
+
};
|
|
262
280
|
}
|
|
263
281
|
|
|
264
282
|
// ── main composer ─────────────────────────────────────────────────────────────
|
|
@@ -272,10 +290,17 @@ export function MessageComposer({
|
|
|
272
290
|
const [text, setText] = useState("");
|
|
273
291
|
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
|
|
274
292
|
const [isSending, setIsSending] = useState(false);
|
|
293
|
+
const [sendError, setSendError] = useState<string | null>(null);
|
|
275
294
|
const inputRef = useRef<TextInput>(null);
|
|
276
295
|
|
|
296
|
+
const isConnecting = !visitorQueryParams;
|
|
277
297
|
const trimmed = text.trim();
|
|
278
|
-
const canSend = (trimmed.length > 0 || attachments.length > 0) && !disabled && !isSending;
|
|
298
|
+
const canSend = (trimmed.length > 0 || attachments.length > 0) && !disabled && !isSending && !isConnecting;
|
|
299
|
+
|
|
300
|
+
// Clear stale error when the user edits the message
|
|
301
|
+
useEffect(() => {
|
|
302
|
+
if (sendError && (text || attachments.length)) setSendError(null);
|
|
303
|
+
}, [text, attachments.length]);
|
|
279
304
|
|
|
280
305
|
const handlePick = useCallback((item: PickedAttachment) => {
|
|
281
306
|
setAttachments((prev) => {
|
|
@@ -290,19 +315,34 @@ export function MessageComposer({
|
|
|
290
315
|
|
|
291
316
|
const handleSend = useCallback(async () => {
|
|
292
317
|
if (!canSend) return;
|
|
318
|
+
|
|
293
319
|
setIsSending(true);
|
|
294
|
-
|
|
320
|
+
setSendError(null);
|
|
321
|
+
|
|
322
|
+
// Capture current values — text/attachments must NOT be cleared until send succeeds
|
|
295
323
|
const sendText = text.trim();
|
|
324
|
+
const snapshot = [...attachments];
|
|
325
|
+
|
|
296
326
|
try {
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
)
|
|
300
|
-
|
|
327
|
+
let uploaded: UploadedAttachment[] = [];
|
|
328
|
+
|
|
329
|
+
if (snapshot.length > 0) {
|
|
330
|
+
// Upload all attachments — throws on the first failure, preserving draft
|
|
331
|
+
uploaded = await Promise.all(
|
|
332
|
+
snapshot.map((a) => uploadAttachment(a, visitorQueryParams))
|
|
333
|
+
);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
// onSend must throw on failure (ConversationScreen propagates throws from useSendMessage)
|
|
301
337
|
await onSend(sendText, uploaded);
|
|
338
|
+
|
|
339
|
+
// Only reached on confirmed success — safe to clear
|
|
302
340
|
setText("");
|
|
303
341
|
setAttachments([]);
|
|
304
|
-
|
|
305
|
-
|
|
342
|
+
setSendError(null);
|
|
343
|
+
} catch (e: any) {
|
|
344
|
+
// Draft preserved — text and attachments state untouched
|
|
345
|
+
setSendError(e?.message ?? "Failed to send. Please try again.");
|
|
306
346
|
} finally {
|
|
307
347
|
setIsSending(false);
|
|
308
348
|
}
|
|
@@ -310,6 +350,17 @@ export function MessageComposer({
|
|
|
310
350
|
|
|
311
351
|
return (
|
|
312
352
|
<View style={[styles.container, { backgroundColor: t.colors.composerBackground, borderTopColor: t.colors.composerBorder }]}>
|
|
353
|
+
{sendError ? (
|
|
354
|
+
<View style={[styles.errorBanner, { backgroundColor: t.colors.surface, borderColor: t.colors.border }]}>
|
|
355
|
+
<Text style={[styles.errorText, { color: t.colors.error ?? "#c0392b" }]} numberOfLines={2}>
|
|
356
|
+
{sendError}
|
|
357
|
+
</Text>
|
|
358
|
+
<TouchableOpacity onPress={() => setSendError(null)} accessibilityLabel="Dismiss error">
|
|
359
|
+
<CloseIcon size={14} color={t.colors.textMuted} />
|
|
360
|
+
</TouchableOpacity>
|
|
361
|
+
</View>
|
|
362
|
+
) : null}
|
|
363
|
+
|
|
313
364
|
<View style={[styles.inner, { borderColor: t.colors.composerBorder }]}>
|
|
314
365
|
<FilePreviews items={attachments} onRemove={handleRemove} borderColor={t.colors.border} />
|
|
315
366
|
|
|
@@ -318,11 +369,11 @@ export function MessageComposer({
|
|
|
318
369
|
style={[styles.input, { color: t.colors.inputText, fontSize: t.fontSizes.md }]}
|
|
319
370
|
value={text}
|
|
320
371
|
onChangeText={setText}
|
|
321
|
-
placeholder={placeholder}
|
|
372
|
+
placeholder={isConnecting ? "Connecting…" : placeholder}
|
|
322
373
|
placeholderTextColor={t.colors.textMuted}
|
|
323
374
|
multiline
|
|
324
375
|
maxLength={4000}
|
|
325
|
-
editable={!disabled}
|
|
376
|
+
editable={!disabled && !isConnecting}
|
|
326
377
|
returnKeyType="default"
|
|
327
378
|
blurOnSubmit={false}
|
|
328
379
|
/>
|
|
@@ -330,7 +381,7 @@ export function MessageComposer({
|
|
|
330
381
|
<View style={styles.toolbar}>
|
|
331
382
|
<AttachmentMenu
|
|
332
383
|
onPick={handlePick}
|
|
333
|
-
disabled={disabled || isSending}
|
|
384
|
+
disabled={disabled || isSending || isConnecting}
|
|
334
385
|
attachmentCount={attachments.length}
|
|
335
386
|
color={t.colors.textMuted}
|
|
336
387
|
/>
|
|
@@ -343,10 +394,12 @@ export function MessageComposer({
|
|
|
343
394
|
styles.sendBtn,
|
|
344
395
|
{ backgroundColor: canSend ? t.colors.sendButtonBackground : t.colors.border },
|
|
345
396
|
]}
|
|
346
|
-
accessibilityLabel="Send message"
|
|
397
|
+
accessibilityLabel={isConnecting ? "Connecting, please wait" : "Send message"}
|
|
347
398
|
>
|
|
348
399
|
{isSending ? (
|
|
349
400
|
<ActivityIndicator size="small" color={t.colors.sendButtonIcon} />
|
|
401
|
+
) : isConnecting ? (
|
|
402
|
+
<ActivityIndicator size="small" color={t.colors.textMuted} />
|
|
350
403
|
) : (
|
|
351
404
|
<SendPlaneIcon size={18} color={canSend ? t.colors.sendButtonIcon : t.colors.textMuted} />
|
|
352
405
|
)}
|
|
@@ -371,6 +424,21 @@ const styles = StyleSheet.create({
|
|
|
371
424
|
paddingTop: 10,
|
|
372
425
|
paddingBottom: 6,
|
|
373
426
|
},
|
|
427
|
+
errorBanner: {
|
|
428
|
+
flexDirection: "row",
|
|
429
|
+
alignItems: "center",
|
|
430
|
+
justifyContent: "space-between",
|
|
431
|
+
borderWidth: 1,
|
|
432
|
+
borderRadius: 8,
|
|
433
|
+
paddingHorizontal: 12,
|
|
434
|
+
paddingVertical: 8,
|
|
435
|
+
marginBottom: 6,
|
|
436
|
+
gap: 8,
|
|
437
|
+
},
|
|
438
|
+
errorText: {
|
|
439
|
+
flex: 1,
|
|
440
|
+
fontSize: 13,
|
|
441
|
+
},
|
|
374
442
|
input: {
|
|
375
443
|
minHeight: 30,
|
|
376
444
|
maxHeight: 200,
|