turbodesk-livechat-react-native 0.1.0-alpha.0 → 0.1.0-alpha.10
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/provider/LiveChatProvider.d.ts.map +1 -1
- package/dist/provider/LiveChatProvider.js +2 -1
- package/dist/provider/LiveChatProvider.js.map +1 -1
- package/dist/ui/components/ConversationScreen.d.ts.map +1 -1
- package/dist/ui/components/ConversationScreen.js +6 -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 -25
- package/dist/ui/components/MessageComposer.js.map +1 -1
- package/package.json +11 -5
- package/src/hooks/use-send-message.ts +169 -159
- package/src/provider/LiveChatProvider.tsx +4 -1
- package/src/ui/components/ConversationScreen.tsx +6 -3
- package/src/ui/components/HomeScreen.tsx +12 -3
- package/src/ui/components/MessageComposer.tsx +97 -30
|
@@ -81,7 +81,6 @@ function AttachmentMenu({ onPick, disabled, attachmentCount, color }: Attachment
|
|
|
81
81
|
let picked: PickedAttachment | null = null;
|
|
82
82
|
|
|
83
83
|
if (kind === "image") {
|
|
84
|
-
// Try react-native-image-picker
|
|
85
84
|
const ImagePicker = require("react-native-image-picker");
|
|
86
85
|
await new Promise<void>((resolve) => {
|
|
87
86
|
ImagePicker.launchImageLibrary({ mediaType: "photo", quality: 0.8 }, (res: any) => {
|
|
@@ -224,13 +223,15 @@ function FilePreviews({
|
|
|
224
223
|
|
|
225
224
|
// ── upload helper ─────────────────────────────────────────────────────────────
|
|
226
225
|
|
|
226
|
+
// Throws on any failure so the caller can surface the error and preserve the draft.
|
|
227
227
|
async function uploadAttachment(
|
|
228
228
|
pending: PendingAttachment,
|
|
229
229
|
visitorQueryParams: Record<string, string> | null
|
|
230
|
-
): Promise<UploadedAttachment
|
|
230
|
+
): Promise<UploadedAttachment> {
|
|
231
|
+
const ext = pending.name.split(".").pop() ?? "";
|
|
232
|
+
let presigned: any;
|
|
231
233
|
try {
|
|
232
|
-
|
|
233
|
-
const presigned = await fileApi.getFileUrl(
|
|
234
|
+
presigned = await fileApi.getFileUrl(
|
|
234
235
|
{
|
|
235
236
|
operation: "putObject",
|
|
236
237
|
name: pending.name,
|
|
@@ -239,27 +240,43 @@ async function uploadAttachment(
|
|
|
239
240
|
fileMode: "app",
|
|
240
241
|
},
|
|
241
242
|
visitorQueryParams ? { params: visitorQueryParams } : undefined
|
|
242
|
-
)
|
|
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}"`);
|
|
243
250
|
|
|
244
|
-
|
|
251
|
+
let putRes: Response;
|
|
252
|
+
try {
|
|
253
|
+
putRes = await fetch(signedUrl, {
|
|
245
254
|
method: "PUT",
|
|
246
255
|
headers: { "Content-Type": pending.mimeType },
|
|
247
256
|
body: { uri: pending.uri, type: pending.mimeType, name: pending.name } as any,
|
|
248
257
|
});
|
|
249
|
-
|
|
250
|
-
const uploaded = presigned?.data?.file;
|
|
251
|
-
return {
|
|
252
|
-
id: pending.id,
|
|
253
|
-
fileId: uploaded._id,
|
|
254
|
-
fileUrl: uploaded.url,
|
|
255
|
-
fileName: uploaded.name,
|
|
256
|
-
fileExtension: uploaded.extension,
|
|
257
|
-
fileType: pending.mimeType,
|
|
258
|
-
type: pending.kind,
|
|
259
|
-
};
|
|
260
258
|
} catch {
|
|
261
|
-
|
|
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}"`);
|
|
262
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
|
+
};
|
|
263
280
|
}
|
|
264
281
|
|
|
265
282
|
// ── main composer ─────────────────────────────────────────────────────────────
|
|
@@ -273,10 +290,17 @@ export function MessageComposer({
|
|
|
273
290
|
const [text, setText] = useState("");
|
|
274
291
|
const [attachments, setAttachments] = useState<PendingAttachment[]>([]);
|
|
275
292
|
const [isSending, setIsSending] = useState(false);
|
|
293
|
+
const [sendError, setSendError] = useState<string | null>(null);
|
|
276
294
|
const inputRef = useRef<TextInput>(null);
|
|
277
295
|
|
|
296
|
+
const isConnecting = !visitorQueryParams;
|
|
278
297
|
const trimmed = text.trim();
|
|
279
|
-
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]);
|
|
280
304
|
|
|
281
305
|
const handlePick = useCallback((item: PickedAttachment) => {
|
|
282
306
|
setAttachments((prev) => {
|
|
@@ -291,19 +315,34 @@ export function MessageComposer({
|
|
|
291
315
|
|
|
292
316
|
const handleSend = useCallback(async () => {
|
|
293
317
|
if (!canSend) return;
|
|
318
|
+
|
|
294
319
|
setIsSending(true);
|
|
295
|
-
|
|
320
|
+
setSendError(null);
|
|
321
|
+
|
|
322
|
+
// Capture current values — text/attachments must NOT be cleared until send succeeds
|
|
296
323
|
const sendText = text.trim();
|
|
324
|
+
const snapshot = [...attachments];
|
|
325
|
+
|
|
297
326
|
try {
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
)
|
|
301
|
-
|
|
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)
|
|
302
337
|
await onSend(sendText, uploaded);
|
|
338
|
+
|
|
339
|
+
// Only reached on confirmed success — safe to clear
|
|
303
340
|
setText("");
|
|
304
341
|
setAttachments([]);
|
|
305
|
-
|
|
306
|
-
|
|
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.");
|
|
307
346
|
} finally {
|
|
308
347
|
setIsSending(false);
|
|
309
348
|
}
|
|
@@ -311,6 +350,17 @@ export function MessageComposer({
|
|
|
311
350
|
|
|
312
351
|
return (
|
|
313
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
|
+
|
|
314
364
|
<View style={[styles.inner, { borderColor: t.colors.composerBorder }]}>
|
|
315
365
|
<FilePreviews items={attachments} onRemove={handleRemove} borderColor={t.colors.border} />
|
|
316
366
|
|
|
@@ -319,11 +369,11 @@ export function MessageComposer({
|
|
|
319
369
|
style={[styles.input, { color: t.colors.inputText, fontSize: t.fontSizes.md }]}
|
|
320
370
|
value={text}
|
|
321
371
|
onChangeText={setText}
|
|
322
|
-
placeholder={placeholder}
|
|
372
|
+
placeholder={isConnecting ? "Connecting…" : placeholder}
|
|
323
373
|
placeholderTextColor={t.colors.textMuted}
|
|
324
374
|
multiline
|
|
325
375
|
maxLength={4000}
|
|
326
|
-
editable={!disabled}
|
|
376
|
+
editable={!disabled && !isConnecting}
|
|
327
377
|
returnKeyType="default"
|
|
328
378
|
blurOnSubmit={false}
|
|
329
379
|
/>
|
|
@@ -331,7 +381,7 @@ export function MessageComposer({
|
|
|
331
381
|
<View style={styles.toolbar}>
|
|
332
382
|
<AttachmentMenu
|
|
333
383
|
onPick={handlePick}
|
|
334
|
-
disabled={disabled || isSending}
|
|
384
|
+
disabled={disabled || isSending || isConnecting}
|
|
335
385
|
attachmentCount={attachments.length}
|
|
336
386
|
color={t.colors.textMuted}
|
|
337
387
|
/>
|
|
@@ -344,10 +394,12 @@ export function MessageComposer({
|
|
|
344
394
|
styles.sendBtn,
|
|
345
395
|
{ backgroundColor: canSend ? t.colors.sendButtonBackground : t.colors.border },
|
|
346
396
|
]}
|
|
347
|
-
accessibilityLabel="Send message"
|
|
397
|
+
accessibilityLabel={isConnecting ? "Connecting, please wait" : "Send message"}
|
|
348
398
|
>
|
|
349
399
|
{isSending ? (
|
|
350
400
|
<ActivityIndicator size="small" color={t.colors.sendButtonIcon} />
|
|
401
|
+
) : isConnecting ? (
|
|
402
|
+
<ActivityIndicator size="small" color={t.colors.textMuted} />
|
|
351
403
|
) : (
|
|
352
404
|
<SendPlaneIcon size={18} color={canSend ? t.colors.sendButtonIcon : t.colors.textMuted} />
|
|
353
405
|
)}
|
|
@@ -372,6 +424,21 @@ const styles = StyleSheet.create({
|
|
|
372
424
|
paddingTop: 10,
|
|
373
425
|
paddingBottom: 6,
|
|
374
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
|
+
},
|
|
375
442
|
input: {
|
|
376
443
|
minHeight: 30,
|
|
377
444
|
maxHeight: 200,
|