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.
@@ -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 | null> {
230
+ ): Promise<UploadedAttachment> {
231
+ const ext = pending.name.split(".").pop() ?? "";
232
+ let presigned: any;
231
233
  try {
232
- const ext = pending.name.split(".").pop() ?? "";
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
- ) 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}"`);
243
250
 
244
- await fetch(presigned?.data?.signedUrl, {
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
- 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}"`);
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
- const snapshot = [...attachments];
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
- const uploadResults = await Promise.all(
299
- snapshot.map((a) => uploadAttachment(a, visitorQueryParams))
300
- );
301
- 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)
302
337
  await onSend(sendText, uploaded);
338
+
339
+ // Only reached on confirmed success — safe to clear
303
340
  setText("");
304
341
  setAttachments([]);
305
- } catch {
306
- /* 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.");
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,