turbodesk-livechat-react-native 0.1.0-alpha.4 → 0.1.0-alpha.6

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.
@@ -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 | null> {
230
+ ): Promise<UploadedAttachment> {
231
+ const ext = pending.name.split(".").pop() ?? "";
232
+ let presigned: any;
230
233
  try {
231
- const ext = pending.name.split(".").pop() ?? "";
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
- ) as any;
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
- await fetch(presigned?.data?.signedUrl, {
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
- return null;
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
- const snapshot = [...attachments];
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
- const uploadResults = await Promise.all(
298
- snapshot.map((a) => uploadAttachment(a, visitorQueryParams))
299
- );
300
- const uploaded = uploadResults.filter((r): r is UploadedAttachment => r !== null);
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
- } catch {
305
- /* ignore */
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,