@repzo/widget-react-native 1.2.2 → 1.3.1

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.
Files changed (172) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +43 -33
  3. package/android/build.gradle +22 -0
  4. package/android/src/main/java/expo/modules/repzowidget/RepzoVoicePlayer.kt +222 -0
  5. package/android/src/main/java/expo/modules/repzowidget/RepzoWidgetAudioModule.kt +68 -0
  6. package/expo-module.config.json +9 -0
  7. package/ios/RepzoVoicePlayer.swift +175 -0
  8. package/ios/RepzoWidgetAudio.podspec +26 -0
  9. package/ios/RepzoWidgetAudioModule.swift +65 -0
  10. package/lib/commonjs/api/client.js +40 -0
  11. package/lib/commonjs/api/client.js.map +1 -1
  12. package/lib/commonjs/attachments/pickers.js +80 -23
  13. package/lib/commonjs/attachments/pickers.js.map +1 -1
  14. package/lib/commonjs/audio/native-player.js +97 -0
  15. package/lib/commonjs/audio/native-player.js.map +1 -0
  16. package/lib/commonjs/audio/voice.js +34 -7
  17. package/lib/commonjs/audio/voice.js.map +1 -1
  18. package/lib/commonjs/i18n.js +2 -0
  19. package/lib/commonjs/i18n.js.map +1 -1
  20. package/lib/commonjs/locales.js +1 -0
  21. package/lib/commonjs/locales.js.map +1 -1
  22. package/lib/commonjs/state/widget-store.js +238 -40
  23. package/lib/commonjs/state/widget-store.js.map +1 -1
  24. package/lib/commonjs/types.js.map +1 -1
  25. package/lib/commonjs/ui/WidgetProvider.js +8 -6
  26. package/lib/commonjs/ui/WidgetProvider.js.map +1 -1
  27. package/lib/commonjs/ui/components/ImageViewer.js +20 -37
  28. package/lib/commonjs/ui/components/ImageViewer.js.map +1 -1
  29. package/lib/commonjs/ui/components/MessagesSkeleton.js +126 -0
  30. package/lib/commonjs/ui/components/MessagesSkeleton.js.map +1 -0
  31. package/lib/commonjs/ui/components/VoiceNote.js +8 -9
  32. package/lib/commonjs/ui/components/VoiceNote.js.map +1 -1
  33. package/lib/commonjs/ui/components/ZoomableImage.js +299 -0
  34. package/lib/commonjs/ui/components/ZoomableImage.js.map +1 -0
  35. package/lib/commonjs/ui/components/icons.js +22 -16
  36. package/lib/commonjs/ui/components/icons.js.map +1 -1
  37. package/lib/commonjs/ui/screens/chat/ChatView.js +42 -18
  38. package/lib/commonjs/ui/screens/chat/ChatView.js.map +1 -1
  39. package/lib/commonjs/ui/screens/chat/ChatView.styles.js +17 -9
  40. package/lib/commonjs/ui/screens/chat/ChatView.styles.js.map +1 -1
  41. package/lib/commonjs/ui/styles/theme.js +1 -0
  42. package/lib/commonjs/ui/styles/theme.js.map +1 -1
  43. package/lib/commonjs/ui/utils/conversations.js +17 -1
  44. package/lib/commonjs/ui/utils/conversations.js.map +1 -1
  45. package/lib/module/api/client.js +40 -0
  46. package/lib/module/api/client.js.map +1 -1
  47. package/lib/module/attachments/pickers.js +80 -23
  48. package/lib/module/attachments/pickers.js.map +1 -1
  49. package/lib/module/audio/native-player.js +93 -0
  50. package/lib/module/audio/native-player.js.map +1 -0
  51. package/lib/module/audio/voice.js +32 -6
  52. package/lib/module/audio/voice.js.map +1 -1
  53. package/lib/module/i18n.js +2 -0
  54. package/lib/module/i18n.js.map +1 -1
  55. package/lib/module/locales.js +1 -0
  56. package/lib/module/locales.js.map +1 -1
  57. package/lib/module/state/widget-store.js +238 -40
  58. package/lib/module/state/widget-store.js.map +1 -1
  59. package/lib/module/types.js.map +1 -1
  60. package/lib/module/ui/WidgetProvider.js +8 -6
  61. package/lib/module/ui/WidgetProvider.js.map +1 -1
  62. package/lib/module/ui/components/ImageViewer.js +21 -38
  63. package/lib/module/ui/components/ImageViewer.js.map +1 -1
  64. package/lib/module/ui/components/MessagesSkeleton.js +124 -0
  65. package/lib/module/ui/components/MessagesSkeleton.js.map +1 -0
  66. package/lib/module/ui/components/VoiceNote.js +9 -10
  67. package/lib/module/ui/components/VoiceNote.js.map +1 -1
  68. package/lib/module/ui/components/ZoomableImage.js +291 -0
  69. package/lib/module/ui/components/ZoomableImage.js.map +1 -0
  70. package/lib/module/ui/components/icons.js +21 -15
  71. package/lib/module/ui/components/icons.js.map +1 -1
  72. package/lib/module/ui/screens/chat/ChatView.js +44 -20
  73. package/lib/module/ui/screens/chat/ChatView.js.map +1 -1
  74. package/lib/module/ui/screens/chat/ChatView.styles.js +17 -9
  75. package/lib/module/ui/screens/chat/ChatView.styles.js.map +1 -1
  76. package/lib/module/ui/styles/theme.js +1 -0
  77. package/lib/module/ui/styles/theme.js.map +1 -1
  78. package/lib/module/ui/utils/conversations.js +15 -1
  79. package/lib/module/ui/utils/conversations.js.map +1 -1
  80. package/lib/typescript/commonjs/api/client.d.ts +27 -1
  81. package/lib/typescript/commonjs/api/client.d.ts.map +1 -1
  82. package/lib/typescript/commonjs/attachments/pickers.d.ts +22 -10
  83. package/lib/typescript/commonjs/attachments/pickers.d.ts.map +1 -1
  84. package/lib/typescript/commonjs/audio/native-player.d.ts +36 -0
  85. package/lib/typescript/commonjs/audio/native-player.d.ts.map +1 -0
  86. package/lib/typescript/commonjs/audio/voice.d.ts +17 -4
  87. package/lib/typescript/commonjs/audio/voice.d.ts.map +1 -1
  88. package/lib/typescript/commonjs/i18n.d.ts +1 -0
  89. package/lib/typescript/commonjs/i18n.d.ts.map +1 -1
  90. package/lib/typescript/commonjs/locales.d.ts.map +1 -1
  91. package/lib/typescript/commonjs/state/widget-store.d.ts +38 -3
  92. package/lib/typescript/commonjs/state/widget-store.d.ts.map +1 -1
  93. package/lib/typescript/commonjs/types.d.ts +17 -0
  94. package/lib/typescript/commonjs/types.d.ts.map +1 -1
  95. package/lib/typescript/commonjs/ui/WidgetProvider.d.ts.map +1 -1
  96. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts +2 -2
  97. package/lib/typescript/commonjs/ui/components/ImageViewer.d.ts.map +1 -1
  98. package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts +10 -0
  99. package/lib/typescript/commonjs/ui/components/MessagesSkeleton.d.ts.map +1 -0
  100. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts +2 -2
  101. package/lib/typescript/commonjs/ui/components/VoiceNote.d.ts.map +1 -1
  102. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts +45 -0
  103. package/lib/typescript/commonjs/ui/components/ZoomableImage.d.ts.map +1 -0
  104. package/lib/typescript/commonjs/ui/components/icons.d.ts +2 -1
  105. package/lib/typescript/commonjs/ui/components/icons.d.ts.map +1 -1
  106. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts +1 -1
  107. package/lib/typescript/commonjs/ui/screens/chat/ChatView.d.ts.map +1 -1
  108. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts +14 -9
  109. package/lib/typescript/commonjs/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  110. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts +4 -0
  111. package/lib/typescript/commonjs/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  112. package/lib/typescript/commonjs/ui/styles/theme.d.ts +2 -0
  113. package/lib/typescript/commonjs/ui/styles/theme.d.ts.map +1 -1
  114. package/lib/typescript/commonjs/ui/utils/conversations.d.ts +10 -2
  115. package/lib/typescript/commonjs/ui/utils/conversations.d.ts.map +1 -1
  116. package/lib/typescript/module/api/client.d.ts +27 -1
  117. package/lib/typescript/module/api/client.d.ts.map +1 -1
  118. package/lib/typescript/module/attachments/pickers.d.ts +22 -10
  119. package/lib/typescript/module/attachments/pickers.d.ts.map +1 -1
  120. package/lib/typescript/module/audio/native-player.d.ts +36 -0
  121. package/lib/typescript/module/audio/native-player.d.ts.map +1 -0
  122. package/lib/typescript/module/audio/voice.d.ts +17 -4
  123. package/lib/typescript/module/audio/voice.d.ts.map +1 -1
  124. package/lib/typescript/module/i18n.d.ts +1 -0
  125. package/lib/typescript/module/i18n.d.ts.map +1 -1
  126. package/lib/typescript/module/locales.d.ts.map +1 -1
  127. package/lib/typescript/module/state/widget-store.d.ts +38 -3
  128. package/lib/typescript/module/state/widget-store.d.ts.map +1 -1
  129. package/lib/typescript/module/types.d.ts +17 -0
  130. package/lib/typescript/module/types.d.ts.map +1 -1
  131. package/lib/typescript/module/ui/WidgetProvider.d.ts.map +1 -1
  132. package/lib/typescript/module/ui/components/ImageViewer.d.ts +2 -2
  133. package/lib/typescript/module/ui/components/ImageViewer.d.ts.map +1 -1
  134. package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts +10 -0
  135. package/lib/typescript/module/ui/components/MessagesSkeleton.d.ts.map +1 -0
  136. package/lib/typescript/module/ui/components/VoiceNote.d.ts +2 -2
  137. package/lib/typescript/module/ui/components/VoiceNote.d.ts.map +1 -1
  138. package/lib/typescript/module/ui/components/ZoomableImage.d.ts +45 -0
  139. package/lib/typescript/module/ui/components/ZoomableImage.d.ts.map +1 -0
  140. package/lib/typescript/module/ui/components/icons.d.ts +2 -1
  141. package/lib/typescript/module/ui/components/icons.d.ts.map +1 -1
  142. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts +1 -1
  143. package/lib/typescript/module/ui/screens/chat/ChatView.d.ts.map +1 -1
  144. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts +14 -9
  145. package/lib/typescript/module/ui/screens/chat/ChatView.styles.d.ts.map +1 -1
  146. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts +4 -0
  147. package/lib/typescript/module/ui/screens/chat/ChatView.types.d.ts.map +1 -1
  148. package/lib/typescript/module/ui/styles/theme.d.ts +2 -0
  149. package/lib/typescript/module/ui/styles/theme.d.ts.map +1 -1
  150. package/lib/typescript/module/ui/utils/conversations.d.ts +10 -2
  151. package/lib/typescript/module/ui/utils/conversations.d.ts.map +1 -1
  152. package/package.json +10 -3
  153. package/react-native.config.js +12 -0
  154. package/src/api/client.ts +66 -2
  155. package/src/attachments/pickers.ts +87 -25
  156. package/src/audio/native-player.ts +106 -0
  157. package/src/audio/voice.ts +30 -6
  158. package/src/i18n.ts +2 -0
  159. package/src/locales.ts +1 -0
  160. package/src/state/widget-store.ts +233 -33
  161. package/src/types.ts +17 -0
  162. package/src/ui/WidgetProvider.tsx +16 -7
  163. package/src/ui/components/ImageViewer.tsx +21 -36
  164. package/src/ui/components/MessagesSkeleton.tsx +92 -0
  165. package/src/ui/components/VoiceNote.tsx +6 -14
  166. package/src/ui/components/ZoomableImage.tsx +250 -0
  167. package/src/ui/components/icons.tsx +10 -12
  168. package/src/ui/screens/chat/ChatView.styles.ts +17 -9
  169. package/src/ui/screens/chat/ChatView.tsx +49 -25
  170. package/src/ui/screens/chat/ChatView.types.ts +4 -0
  171. package/src/ui/styles/theme.ts +6 -0
  172. package/src/ui/utils/conversations.ts +16 -2
@@ -49,19 +49,20 @@ import { ImageViewer } from '../../components/ImageViewer';
49
49
  import { getRatingRequest, RatingCard } from '../../components/RatingCard';
50
50
  import { WidgetErrorBoundary } from '../../components/ErrorBoundary';
51
51
  import { GradientBackground } from '../../components/Gradient';
52
- import { ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PaperclipIcon } from '../../components/icons';
52
+ import { AlertIcon, ArrowUpIcon, ChatOutlineIcon, ChevronIcon, ClockIcon, PlusIcon } from '../../components/icons';
53
53
  import { TeamAvatars } from '../../components/TeamAvatars';
54
54
  import { Markdown } from '../../components/Markdown';
55
55
  import { useReduceMotion } from '../../hooks/use-reduce-motion';
56
56
  import { useTopInset } from '../../hooks/use-top-inset';
57
57
  import { globalStyles } from '../../styles/global';
58
58
  import { contrastingTextColor, mixHex, palette, spacing, withAlpha } from '../../styles/theme';
59
- import { formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
59
+ import { canRetryMessage, formatClockTime, formatDayLabel, isSameDay } from '../../utils/conversations';
60
60
  import { stripHtml } from '../../utils/html';
61
61
  import { isHttpUrl, openExternalUrl } from '../../utils/safe-url';
62
62
  import { isPlayableAudio } from '../../../audio/voice';
63
63
  import { VoiceNote, type VoiceNoteLabels } from '../../components/VoiceNote';
64
64
  import { FileCard } from '../../components/FileCard';
65
+ import { MessagesSkeleton } from '../../components/MessagesSkeleton';
65
66
  import { backDirection, isRTL } from '../../utils/rtl';
66
67
  import { styles } from './ChatView.styles';
67
68
  import type { ChatLabels, ChatViewProps } from './ChatView.types';
@@ -86,6 +87,7 @@ export function ChatView({
86
87
  attachmentSources = [],
87
88
  onPickAttachments,
88
89
  onSendMessage,
90
+ onRetryMessage,
89
91
  onIdentifyVisitor,
90
92
  onRateConversation,
91
93
  defaultLocale,
@@ -267,6 +269,7 @@ export function ChatView({
267
269
  sent.map((draft) => ({
268
270
  name: draft.name,
269
271
  url: draft.dataUrl,
272
+ fileUri: draft.fileUri,
270
273
  contentType: draft.contentType,
271
274
  size: draft.size,
272
275
  previewUrl: draft.previewUri,
@@ -324,6 +327,16 @@ export function ChatView({
324
327
  [onSendMessage]
325
328
  );
326
329
 
330
+ // The retried bubble moves to the end of the thread; follow it there.
331
+ // A second failure shows on the bubble again.
332
+ const handleRetryMessage = useCallback(
333
+ (messageId: string) => {
334
+ onRetryMessage?.(messageId).catch(() => {});
335
+ scrollToBottom();
336
+ },
337
+ [onRetryMessage, scrollToBottom]
338
+ );
339
+
327
340
  // Recomputed only when the list changes — never on a keystroke.
328
341
  const thread = useMemo(() => deriveThread(messages), [messages]);
329
342
  const locales = useMemo(() => [locale, defaultLocale], [locale, defaultLocale]);
@@ -331,6 +344,7 @@ export function ChatView({
331
344
  const headerTextColor = contrastingTextColor(color);
332
345
  const onDarkHeader = headerTextColor === palette.white;
333
346
  const isEmptyThread = !loading && messages.length === 0;
347
+ const isLoadingThread = loading && messages.length === 0;
334
348
  const agentIsActive = agentOnline ?? isOnline;
335
349
  // "Waiting for a teammate" until someone replies: no teammate in the header
336
350
  // and the visitor spoke last (bot replies before that still count as waiting).
@@ -439,6 +453,7 @@ export function ChatView({
439
453
  contentContainerStyle={[
440
454
  styles.messagesContent,
441
455
  isEmptyThread && styles.messagesContentEmpty,
456
+ isLoadingThread && styles.messagesContentLoading,
442
457
  ]}
443
458
  keyboardShouldPersistTaps="handled"
444
459
  // Interactive dismissal drags the keyboard down under the finger while
@@ -455,9 +470,7 @@ export function ChatView({
455
470
  </Pressable>
456
471
  ) : null}
457
472
 
458
- {loading && messages.length === 0 ? (
459
- <Text style={styles.stateText}>{labels.loading}</Text>
460
- ) : null}
473
+ {isLoadingThread ? <MessagesSkeleton color={color} label={labels.loading} /> : null}
461
474
 
462
475
  {isEmptyThread ? (
463
476
  <View style={styles.emptyState}>
@@ -520,6 +533,7 @@ export function ChatView({
520
533
  onAgentUiAction={
521
534
  thread.agentBlocksShown.has(message.id) ? handleAgentUiAction : undefined
522
535
  }
536
+ onRetry={onRetryMessage && canRetryMessage(message) ? handleRetryMessage : undefined}
523
537
  />
524
538
  </WidgetErrorBoundary>
525
539
  </Fragment>
@@ -591,7 +605,7 @@ export function ChatView({
591
605
  styles.attachButton,
592
606
  (pressed || sending || picking) && globalStyles.pressed,
593
607
  ]}>
594
- <PaperclipIcon size={20} color={palette.textMuted} />
608
+ <PlusIcon size={20} color={palette.textMuted} />
595
609
  </Pressable>
596
610
  ) : null}
597
611
  <TextInput
@@ -662,6 +676,7 @@ const MessageBubble = memo(function MessageBubble({
662
676
  onOpenImage,
663
677
  onRateConversation,
664
678
  locales,
679
+ onRetry,
665
680
  }: {
666
681
  message: Message;
667
682
  color: string;
@@ -674,6 +689,8 @@ const MessageBubble = memo(function MessageBubble({
674
689
  onOpenImage: (image: { url: string; name: string }) => void;
675
690
  onRateConversation?: ChatViewProps['onRateConversation'];
676
691
  locales: Array<string | null | undefined>;
692
+ /** Re-sends this message when it failed. */
693
+ onRetry?: (messageId: string) => void;
677
694
  }) {
678
695
  const joinedName = message.metadata?.event === 'agent_joined' ? message.metadata?.agentName : null;
679
696
  if (message.messageType === 'system' && typeof joinedName === 'string' && joinedName) {
@@ -771,16 +788,6 @@ const MessageBubble = memo(function MessageBubble({
771
788
  </View>
772
789
  ) : null}
773
790
 
774
- {message.status === 'failed' ? (
775
- <Text
776
- style={[
777
- styles.failedText,
778
- { color: isVisitor ? withAlpha(palette.white, 0.7) : palette.danger },
779
- ]}>
780
- {labels.failedToSend}
781
- </Text>
782
- ) : null}
783
-
784
791
  {timeLabel ? (
785
792
  <Text
786
793
  style={[
@@ -794,6 +801,22 @@ const MessageBubble = memo(function MessageBubble({
794
801
 
795
802
  {receiptLabel ? <Text style={styles.receiptText}>{receiptLabel}</Text> : null}
796
803
 
804
+ {/* Under the bubble, where Sent/Seen would be: in the danger color it
805
+ reads on any brand color, and the whole line is the retry target. */}
806
+ {message.status === 'failed' ? (
807
+ <Pressable
808
+ onPress={onRetry ? () => onRetry(message.id) : undefined}
809
+ disabled={!onRetry}
810
+ accessibilityRole={onRetry ? 'button' : 'text'}
811
+ hitSlop={8}
812
+ style={({ pressed }) => [styles.failedRow, pressed && globalStyles.pressed]}>
813
+ <AlertIcon size={12} color={palette.danger} />
814
+ <Text style={styles.failedText}>
815
+ {onRetry ? `${labels.failedToSend} · ${labels.tapToRetry}` : labels.failedToSend}
816
+ </Text>
817
+ </Pressable>
818
+ ) : null}
819
+
797
820
  {agentUiBlocks.length > 0 && onAgentUiAction ? (
798
821
  <AgentUiBlocks
799
822
  blocks={agentUiBlocks}
@@ -838,18 +861,20 @@ function AttachmentPreview({
838
861
  onOpenImage: (image: { url: string; name: string }) => void;
839
862
  }) {
840
863
  // `url` is empty when the server sent a non-http(s) URL (see resolveUrl).
841
- // An unsent attachment shows its local file (or data URL): it renders, but
842
- // there is nothing to open until the server copy arrives.
864
+ // The visitor's own attachment points at the picked file until the server
865
+ // copy replaces it (or is empty without one).
843
866
  const [imageFailed, setImageFailed] = useState(false);
844
867
  const [audioFailed, setAudioFailed] = useState(false);
845
868
  const isImage = isImageAttachment(attachment.contentType) && !!attachment.url && !imageFailed;
846
869
  const openable = isHttpUrl(attachment.url);
847
- // Images open in the in-app viewer; other files (PDF, sheets) need the OS.
848
- const open = !openable
849
- ? undefined
850
- : isImage
851
- ? () => onOpenImage({ url: attachment.url, name: attachment.name })
852
- : () => void openExternalUrl(attachment.url);
870
+ // Images open in the in-app viewer, a local photo too since it already
871
+ // renders here. Other files (PDF, sheets) need the OS, which can only open
872
+ // the server copy.
873
+ const open = isImage
874
+ ? () => onOpenImage({ url: attachment.url, name: attachment.name })
875
+ : openable
876
+ ? () => void openExternalUrl(attachment.url)
877
+ : undefined;
853
878
 
854
879
  // Voice notes from the team (and any other audio) play in place, like the
855
880
  // web Messenger — when the app has expo-audio; otherwise the file row below.
@@ -872,7 +897,6 @@ function AttachmentPreview({
872
897
  return (
873
898
  <Pressable
874
899
  onPress={open}
875
- disabled={!openable}
876
900
  accessibilityRole="imagebutton"
877
901
  accessibilityLabel={attachment.name}>
878
902
  <Image
@@ -21,6 +21,8 @@ export interface ChatLabels {
21
21
  emptyConversation: string;
22
22
  messagePlaceholder: string;
23
23
  failedToSend: string;
24
+ /** After `failedToSend` on a bubble that can be re-sent. */
25
+ tapToRetry: string;
24
26
  thinking: string;
25
27
  team: string;
26
28
  statusOnline: string;
@@ -104,6 +106,8 @@ export interface ChatViewProps {
104
106
  attachments?: OutgoingAttachment[],
105
107
  action?: AgentUiAction
106
108
  ) => Promise<void>;
109
+ /** Re-sends a failed bubble; rejects when it fails again. Absent: no retry. */
110
+ onRetryMessage?: (messageId: string) => Promise<void>;
107
111
  onIdentifyVisitor?: (data: { email?: string; name?: string; phone?: string }) => Promise<void>;
108
112
  /** Answers a rating card; rejects with the server error (409 locked, 410 expired). */
109
113
  onRateConversation?: (
@@ -68,6 +68,8 @@ export interface WidgetTheme {
68
68
  allowNewConversations: boolean;
69
69
  showPreviousConversations: boolean;
70
70
  attachmentsEnabled: boolean;
71
+ /** The server's upload cap; null when it only takes files inline in the message. */
72
+ maxAttachmentBytes: number | null;
71
73
  preChatEnabled: boolean;
72
74
  }
73
75
 
@@ -94,6 +96,10 @@ export function buildTheme(config: WidgetConfig | null): WidgetTheme {
94
96
  allowNewConversations: config?.allowNewConversations ?? true,
95
97
  showPreviousConversations: config?.showPreviousConversations ?? true,
96
98
  attachmentsEnabled: config?.attachmentsEnabled ?? true,
99
+ maxAttachmentBytes:
100
+ typeof config?.maxAttachmentBytes === 'number' && config.maxAttachmentBytes > 0
101
+ ? config.maxAttachmentBytes
102
+ : null,
97
103
  preChatEnabled: config?.preChatEnabled ?? false,
98
104
  };
99
105
  }
@@ -1,11 +1,12 @@
1
1
  /**
2
2
  * Conversation selectors and formatting shared by the Home and Messages
3
- * spaces, so the two can never disagree about ordering or wording.
3
+ * spaces (and the store and chat thread, for message helpers), so they can
4
+ * never disagree about ordering or wording.
4
5
  * Pure and free of React so they stay trivial to test.
5
6
  */
6
7
 
7
8
  import { formatLabel } from '../../i18n';
8
- import type { Conversation } from '../../types';
9
+ import type { Conversation, Message } from '../../types';
9
10
 
10
11
  export interface TeamAgent {
11
12
  name: string | null;
@@ -120,3 +121,16 @@ export function formatDayLabel(
120
121
  return date.toDateString();
121
122
  }
122
123
  }
124
+
125
+ /** Optimistic visitor bubbles the server hasn't acknowledged (pending/failed). */
126
+ export function isLocalMessage(message: Message): boolean {
127
+ return message.id.startsWith('temp-');
128
+ }
129
+
130
+ /**
131
+ * The visitor's own bubble that never reached the server. A `failed` status
132
+ * the server sets on any other message isn't something this device can re-send.
133
+ */
134
+ export function canRetryMessage(message: Message): boolean {
135
+ return message.status === 'failed' && message.direction === 'inbound' && isLocalMessage(message);
136
+ }