@ethora/chat-component-rn 26.5.11 → 26.5.12

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 (135) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -0
  3. package/lib/commonjs/assets/icons.js +7 -1
  4. package/lib/commonjs/assets/icons.js.map +1 -1
  5. package/lib/commonjs/components/MainComponents/ChatRoom.js +17 -1
  6. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/MediaMessage.js +288 -5
  8. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  9. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
  10. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  11. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
  12. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  13. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  14. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
  16. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  17. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
  18. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  19. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
  20. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  21. package/lib/commonjs/components/styled/AudioMessage.js +643 -66
  22. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  23. package/lib/commonjs/components/styled/Button.js +9 -3
  24. package/lib/commonjs/components/styled/Button.js.map +1 -1
  25. package/lib/commonjs/components/styled/SendInput.js +239 -40
  26. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  27. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  28. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  29. package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
  30. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  31. package/lib/commonjs/context/xmppProvider.js +5 -2
  32. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  33. package/lib/commonjs/helpers/keyboardLayout.js +5 -3
  34. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  35. package/lib/commonjs/helpers/mimeToExtension.js +70 -3
  36. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  37. package/lib/commonjs/hooks/useEventHandlers.js +1 -1
  38. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  39. package/lib/commonjs/hooks/useSendMessage.js +6 -3
  40. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  41. package/lib/commonjs/roomStore/roomsSlice.js +6 -0
  42. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  43. package/lib/module/assets/icons.js +7 -1
  44. package/lib/module/assets/icons.js.map +1 -1
  45. package/lib/module/components/MainComponents/ChatRoom.js +18 -2
  46. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  47. package/lib/module/components/MainComponents/MediaMessage.js +290 -7
  48. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  49. package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
  50. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  51. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
  52. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  53. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  54. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  55. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
  56. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  57. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
  58. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  59. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
  60. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  61. package/lib/module/components/styled/AudioMessage.js +644 -67
  62. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  63. package/lib/module/components/styled/Button.js +9 -3
  64. package/lib/module/components/styled/Button.js.map +1 -1
  65. package/lib/module/components/styled/SendInput.js +240 -42
  66. package/lib/module/components/styled/SendInput.js.map +1 -1
  67. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  68. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  69. package/lib/module/components/styled/UnsupportedType.js +47 -5
  70. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  71. package/lib/module/context/xmppProvider.js +5 -2
  72. package/lib/module/context/xmppProvider.js.map +1 -1
  73. package/lib/module/helpers/keyboardLayout.js +4 -2
  74. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  75. package/lib/module/helpers/mimeToExtension.js +67 -3
  76. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  77. package/lib/module/hooks/useEventHandlers.js +1 -1
  78. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  79. package/lib/module/hooks/useSendMessage.js +6 -3
  80. package/lib/module/hooks/useSendMessage.js.map +1 -1
  81. package/lib/module/roomStore/roomsSlice.js +6 -0
  82. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  83. package/lib/typescript/assets/icons.d.ts.map +1 -1
  84. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  85. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  86. package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
  87. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
  88. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
  89. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  90. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
  91. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
  92. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  93. package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
  94. package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
  95. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  96. package/lib/typescript/components/styled/Button.d.ts.map +1 -1
  97. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  98. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
  99. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
  100. package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
  101. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  102. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  103. package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
  104. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  105. package/lib/typescript/helpers/mimeToExtension.d.ts +31 -1
  106. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  107. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  108. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  109. package/lib/typescript/types/models/modal.model.d.ts +3 -0
  110. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  111. package/lib/typescript/types/types.d.ts +13 -0
  112. package/lib/typescript/types/types.d.ts.map +1 -1
  113. package/package.json +5 -1
  114. package/src/assets/icons.tsx +30 -25
  115. package/src/components/MainComponents/ChatRoom.tsx +21 -0
  116. package/src/components/MainComponents/MediaMessage.tsx +295 -5
  117. package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
  118. package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
  119. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
  120. package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
  121. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
  122. package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
  123. package/src/components/styled/AudioMessage.tsx +724 -59
  124. package/src/components/styled/Button.tsx +20 -11
  125. package/src/components/styled/SendInput.tsx +255 -46
  126. package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
  127. package/src/components/styled/UnsupportedType.tsx +60 -6
  128. package/src/context/xmppProvider.tsx +6 -2
  129. package/src/helpers/keyboardLayout.ts +5 -1
  130. package/src/helpers/mimeToExtension.ts +116 -2
  131. package/src/hooks/useEventHandlers.tsx +1 -1
  132. package/src/hooks/useSendMessage.tsx +12 -6
  133. package/src/roomStore/roomsSlice.ts +7 -0
  134. package/src/types/models/modal.model.ts +3 -0
  135. package/src/types/types.ts +13 -0
@@ -70,6 +70,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
70
70
  const insets = useSafeAreaInsets();
71
71
 
72
72
  const [isLoadingMore, setIsLoadingMore] = useState<boolean>(false);
73
+ const [keyboardVisible, setKeyboardVisible] = useState(false);
73
74
 
74
75
  const { user, config: storeConfig } = useChatSettingState();
75
76
 
@@ -227,8 +228,28 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
227
228
  const inputDockPaddingBottom = getInputDockPaddingBottom({
228
229
  platform: Platform.OS,
229
230
  bottomInset: insets.bottom,
231
+ keyboardVisible,
230
232
  });
231
233
 
234
+ useEffect(() => {
235
+ const showEvent =
236
+ Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
237
+ const hideEvent =
238
+ Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
239
+
240
+ const showSub = Keyboard.addListener(showEvent, () => {
241
+ setKeyboardVisible(true);
242
+ });
243
+ const hideSub = Keyboard.addListener(hideEvent, () => {
244
+ setKeyboardVisible(false);
245
+ });
246
+
247
+ return () => {
248
+ showSub.remove();
249
+ hideSub.remove();
250
+ };
251
+ }, []);
252
+
232
253
  return (
233
254
  <KeyboardAvoidingView
234
255
  style={{ flex: 1 }}
@@ -4,8 +4,17 @@ import FileDownload from '../styled/UnsupportedType';
4
4
  import CustomMessageImage from '../styled/MessageImage';
5
5
  import CustomMessageVideo from '../styled/VideoMessage';
6
6
  import AudioMessage from '../styled/AudioMessage';
7
- import { Text } from 'react-native';
7
+ import {
8
+ ActivityIndicator,
9
+ Image,
10
+ StyleSheet,
11
+ Text,
12
+ View,
13
+ } from 'react-native';
8
14
  import { deriveDisplayFilename, isLikelyAudio } from '../../helpers/mimeToExtension';
15
+ import { FileIcon, PlayIcon } from '../../assets/icons';
16
+ import { defaultMediaDims } from '../../helpers/mediaDimensions';
17
+ import { useChatSettingState } from '../../hooks/useChatSettingState';
9
18
 
10
19
  interface MediaMessageProps {
11
20
  mimeType?: string;
@@ -15,6 +24,116 @@ interface MediaMessageProps {
15
24
  isUser: boolean;
16
25
  }
17
26
 
27
+ const pendingImageDims = defaultMediaDims();
28
+
29
+ const getPendingStatusLabel = (mimeType?: string) => {
30
+ if (mimeType?.startsWith('image/')) {
31
+ return 'Uploading image';
32
+ }
33
+ if (mimeType?.startsWith('video/')) {
34
+ return 'Uploading video';
35
+ }
36
+ if (
37
+ mimeType?.startsWith('audio/') ||
38
+ mimeType?.includes('application/octet-stream')
39
+ ) {
40
+ return 'Uploading audio';
41
+ }
42
+ return 'Uploading file';
43
+ };
44
+
45
+ const PendingMediaMessage: React.FC<{
46
+ fileName: string;
47
+ mimeType?: string;
48
+ previewUri?: string;
49
+ isUser: boolean;
50
+ size?: string;
51
+ }> = ({ fileName, mimeType, previewUri, isUser, size }) => {
52
+ const { config } = useChatSettingState();
53
+ const statusLabel = getPendingStatusLabel(mimeType);
54
+ const isImage = mimeType?.startsWith('image/');
55
+ const isVideo = mimeType?.startsWith('video/');
56
+ const isAudio =
57
+ mimeType?.startsWith('audio/') ||
58
+ mimeType?.includes('application/octet-stream');
59
+ const primaryColor = config?.colors?.primary || '#0A84FF';
60
+
61
+ if (isImage && previewUri) {
62
+ return (
63
+ <View style={styles.pendingImageWrapper}>
64
+ <Image
65
+ source={{ uri: previewUri }}
66
+ style={styles.pendingImage}
67
+ resizeMode="cover"
68
+ />
69
+ <View style={styles.pendingImageOverlay}>
70
+ <ActivityIndicator size="small" color="#FFFFFF" />
71
+ <Text style={styles.pendingOverlayText}>{statusLabel}</Text>
72
+ </View>
73
+ </View>
74
+ );
75
+ }
76
+
77
+ if (isVideo) {
78
+ return (
79
+ <View style={styles.pendingVideoCard}>
80
+ <View style={styles.pendingVideoPreview}>
81
+ <View style={styles.pendingVideoIcon}>
82
+ <PlayIcon width={24} height={24} />
83
+ </View>
84
+ <ActivityIndicator size="small" color="#5B6B8C" />
85
+ </View>
86
+ <Text style={styles.pendingVideoLabel}>{fileName}</Text>
87
+ <Text style={styles.pendingVideoStatus}>{statusLabel}</Text>
88
+ </View>
89
+ );
90
+ }
91
+
92
+ if (isAudio) {
93
+ return (
94
+ <View style={styles.pendingAudioCard}>
95
+ <View
96
+ style={[
97
+ styles.pendingAudioButton,
98
+ { backgroundColor: primaryColor },
99
+ ]}
100
+ >
101
+ <PlayIcon width={18} height={18} color="#fff" />
102
+ </View>
103
+ <View style={styles.pendingAudioContent}>
104
+ <View style={styles.pendingAudioTrack}>
105
+ <View
106
+ style={[
107
+ styles.pendingAudioTrackFill,
108
+ { backgroundColor: primaryColor },
109
+ ]}
110
+ />
111
+ </View>
112
+ <View style={styles.pendingAudioMeta}>
113
+ <Text style={styles.pendingAudioTime}>0:00</Text>
114
+ <View style={styles.pendingAudioStatusPill}>
115
+ <ActivityIndicator size="small" color="#5B6B8C" />
116
+ <Text style={styles.pendingAudioStatusText}>{statusLabel}</Text>
117
+ </View>
118
+ </View>
119
+ </View>
120
+ </View>
121
+ );
122
+ }
123
+
124
+ return (
125
+ <FileDownload
126
+ fileURL={previewUri || ''}
127
+ fileName={fileName}
128
+ mimetype={mimeType || 'application/octet-stream'}
129
+ size={size}
130
+ isUser={isUser}
131
+ pending
132
+ placeholderIcon={<FileIcon width={36} height={36} />}
133
+ />
134
+ );
135
+ };
136
+
18
137
  const MediaMessage: React.FC<MediaMessageProps> = ({
19
138
  mimeType,
20
139
  location,
@@ -29,12 +148,33 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
29
148
  url: location || messageText,
30
149
  mime: mimeType,
31
150
  });
151
+ const isAudioPayload = isLikelyAudio(
152
+ mimeType,
153
+ displayName,
154
+ location,
155
+ {
156
+ duration: (message as any)?.duration,
157
+ waveForm: (message as any)?.waveForm,
158
+ originalName: (message as any)?.originalName,
159
+ }
160
+ );
161
+ if (message?.pending) {
162
+ return (
163
+ <PendingMediaMessage
164
+ fileName={displayName}
165
+ mimeType={mimeType}
166
+ previewUri={messageText || location || ''}
167
+ isUser={isUser}
168
+ size={message?.size}
169
+ />
170
+ );
171
+ }
32
172
  switch (true) {
33
173
  case mimeType.startsWith('image/'):
34
174
  return (
35
175
  <CustomMessageImage
36
176
  fileName={displayName}
37
- fileURL={messageText || ''}
177
+ fileURL={location || messageText || ''}
38
178
  mimetype={mimeType}
39
179
  />
40
180
  );
@@ -47,7 +187,28 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
47
187
  />
48
188
  );
49
189
  case mimeType.startsWith('audio/'): {
50
- return <AudioMessage src={location || ''} />;
190
+ return (
191
+ <AudioMessage
192
+ src={location || ''}
193
+ mimeType={mimeType}
194
+ fileName={displayName}
195
+ originalName={(message as any)?.originalName}
196
+ duration={(message as any)?.duration}
197
+ waveForm={(message as any)?.waveForm}
198
+ />
199
+ );
200
+ }
201
+ case mimeType.includes('application/octet-stream'): {
202
+ return (
203
+ <AudioMessage
204
+ src={location || ''}
205
+ mimeType={mimeType}
206
+ fileName={displayName}
207
+ originalName={(message as any)?.originalName}
208
+ duration={(message as any)?.duration}
209
+ waveForm={(message as any)?.waveForm}
210
+ />
211
+ );
51
212
  }
52
213
  default: {
53
214
  // Some senders (notably the web app's voice-message uploader)
@@ -58,8 +219,17 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
58
219
  // player instead of falling through to the generic FileDownload
59
220
  // card (where the voicemail rendered as a broken `.bin`
60
221
  // attachment). Customer-reported #9 voicemail fix.
61
- if (isLikelyAudio(mimeType, displayName, location)) {
62
- return <AudioMessage src={location || ''} />;
222
+ if (isAudioPayload) {
223
+ return (
224
+ <AudioMessage
225
+ src={location || ''}
226
+ mimeType={mimeType}
227
+ fileName={displayName}
228
+ originalName={(message as any)?.originalName}
229
+ duration={(message as any)?.duration}
230
+ waveForm={(message as any)?.waveForm}
231
+ />
232
+ );
63
233
  }
64
234
  return (
65
235
  <FileDownload
@@ -67,6 +237,9 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
67
237
  fileName={displayName}
68
238
  mimetype={mimeType}
69
239
  isUser={isUser}
240
+ originalName={(message as any)?.originalName}
241
+ duration={(message as any)?.duration}
242
+ waveForm={(message as any)?.waveForm}
70
243
  />
71
244
  );
72
245
  }
@@ -76,3 +249,120 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
76
249
  };
77
250
 
78
251
  export default MediaMessage;
252
+
253
+ const styles = StyleSheet.create({
254
+ pendingImageWrapper: {
255
+ width: pendingImageDims.width,
256
+ height: pendingImageDims.height,
257
+ borderRadius: 12,
258
+ overflow: 'hidden',
259
+ backgroundColor: '#EAF0FB',
260
+ },
261
+ pendingImage: {
262
+ width: '100%',
263
+ height: '100%',
264
+ },
265
+ pendingImageOverlay: {
266
+ ...StyleSheet.absoluteFillObject,
267
+ alignItems: 'center',
268
+ justifyContent: 'center',
269
+ gap: 8,
270
+ backgroundColor: 'rgba(17, 24, 39, 0.28)',
271
+ },
272
+ pendingOverlayText: {
273
+ color: '#FFFFFF',
274
+ fontSize: 13,
275
+ fontWeight: '600',
276
+ },
277
+ pendingVideoCard: {
278
+ width: 220,
279
+ padding: 12,
280
+ borderRadius: 12,
281
+ backgroundColor: '#F3F6FC',
282
+ gap: 10,
283
+ },
284
+ pendingVideoPreview: {
285
+ height: 144,
286
+ borderRadius: 10,
287
+ backgroundColor: '#DCE6F7',
288
+ alignItems: 'center',
289
+ justifyContent: 'center',
290
+ gap: 10,
291
+ },
292
+ pendingVideoIcon: {
293
+ width: 48,
294
+ height: 48,
295
+ borderRadius: 24,
296
+ backgroundColor: 'rgba(17, 24, 39, 0.45)',
297
+ alignItems: 'center',
298
+ justifyContent: 'center',
299
+ },
300
+ pendingVideoLabel: {
301
+ color: '#141414',
302
+ fontSize: 14,
303
+ fontWeight: '600',
304
+ },
305
+ pendingVideoStatus: {
306
+ color: '#5B6B8C',
307
+ fontSize: 12,
308
+ fontWeight: '500',
309
+ },
310
+ pendingAudioCard: {
311
+ minWidth: 252,
312
+ maxWidth: 288,
313
+ flexDirection: 'row',
314
+ alignItems: 'center',
315
+ gap: 12,
316
+ paddingHorizontal: 12,
317
+ paddingVertical: 10,
318
+ borderRadius: 12,
319
+ backgroundColor: '#F3F6FC',
320
+ },
321
+ pendingAudioButton: {
322
+ width: 44,
323
+ height: 44,
324
+ borderRadius: 22,
325
+ alignItems: 'center',
326
+ justifyContent: 'center',
327
+ },
328
+ pendingAudioContent: {
329
+ flex: 1,
330
+ gap: 8,
331
+ },
332
+ pendingAudioTrack: {
333
+ height: 6,
334
+ borderRadius: 999,
335
+ overflow: 'hidden',
336
+ backgroundColor: '#D0D7E6',
337
+ },
338
+ pendingAudioTrackFill: {
339
+ width: '32%',
340
+ height: '100%',
341
+ borderRadius: 999,
342
+ },
343
+ pendingAudioMeta: {
344
+ flexDirection: 'row',
345
+ alignItems: 'center',
346
+ justifyContent: 'space-between',
347
+ gap: 12,
348
+ },
349
+ pendingAudioTime: {
350
+ color: '#667085',
351
+ fontSize: 12,
352
+ fontWeight: '500',
353
+ },
354
+ pendingAudioStatusPill: {
355
+ flexDirection: 'row',
356
+ alignItems: 'center',
357
+ gap: 6,
358
+ paddingHorizontal: 10,
359
+ paddingVertical: 5,
360
+ borderRadius: 999,
361
+ backgroundColor: '#FFFFFF',
362
+ },
363
+ pendingAudioStatusText: {
364
+ color: '#5B6B8C',
365
+ fontSize: 12,
366
+ fontWeight: '600',
367
+ },
368
+ });
@@ -13,6 +13,7 @@ import {
13
13
  View,
14
14
  Pressable,
15
15
  Dimensions,
16
+ Keyboard,
16
17
  type LayoutChangeEvent,
17
18
  } from 'react-native';
18
19
  import * as Clipboard from 'expo-clipboard';
@@ -56,6 +57,28 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
56
57
 
57
58
  const [menuSize, setMenuSize] = useState({ width: 0, height: 0 });
58
59
 
60
+ // Live keyboard height so the menu knows the REAL space below the message.
61
+ // Without this the position math used the full screen height and happily
62
+ // opened the menu downward into the area the keyboard now covers, hiding
63
+ // it under the keyboard / behind the input. Seeded from current metrics so
64
+ // an already-open keyboard (the common case: menu opened while typing) is
65
+ // accounted for on first render, not only after the next show event.
66
+ const [keyboardHeight, setKeyboardHeight] = useState(
67
+ () => Keyboard.metrics?.()?.height ?? 0
68
+ );
69
+
70
+ useEffect(() => {
71
+ const onShow = (e: any) =>
72
+ setKeyboardHeight(e?.endCoordinates?.height ?? 0);
73
+ const onHide = () => setKeyboardHeight(0);
74
+ const subs = [
75
+ Keyboard.addListener('keyboardDidShow', onShow),
76
+ Keyboard.addListener('keyboardDidChangeFrame', onShow),
77
+ Keyboard.addListener('keyboardDidHide', onHide),
78
+ ];
79
+ return () => subs.forEach((s) => s.remove());
80
+ }, []);
81
+
59
82
  const handleMenuLayout = (e: LayoutChangeEvent) => {
60
83
  const { width, height } = e.nativeEvent.layout;
61
84
  if (
@@ -107,7 +130,13 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
107
130
  const topReserve = 16;
108
131
  const sideMargin = 8;
109
132
 
110
- const spaceBelow = screenHeight - position.bottom - bottomReserve;
133
+ // The keyboard occludes the bottom `keyboardHeight` px, so the real
134
+ // visible bottom edge is above it. Measuring space below against this
135
+ // (not the full screen) is what makes the menu flip ABOVE the message
136
+ // when the keyboard is open and the message sits near the input.
137
+ const visibleBottom = screenHeight - keyboardHeight;
138
+
139
+ const spaceBelow = visibleBottom - position.bottom - bottomReserve;
111
140
  const spaceAbove = position.top - topReserve;
112
141
 
113
142
  let top: number;
@@ -116,7 +145,10 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
116
145
  } else if (spaceAbove >= menuSize.height) {
117
146
  top = position.top - menuSize.height;
118
147
  } else {
119
- top = Math.max(topReserve, screenHeight - menuSize.height - bottomReserve);
148
+ top = Math.max(
149
+ topReserve,
150
+ visibleBottom - menuSize.height - bottomReserve
151
+ );
120
152
  }
121
153
 
122
154
  const leftMargin = position.left;
@@ -130,7 +162,7 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
130
162
  );
131
163
 
132
164
  return { top, left };
133
- }, [position, menuSize, config?.keyboardVerticalOffset]);
165
+ }, [position, menuSize, config?.keyboardVerticalOffset, keyboardHeight]);
134
166
 
135
167
  // Window coords (memoPosition) → host-local coords. The overlay host
136
168
  // sits below the status bar / header, so subtract its measured origin.
@@ -1,6 +1,6 @@
1
1
  import React, { useState } from 'react';
2
- import { ActivityIndicator, StyleSheet, Text, View } from 'react-native';
3
- import { WebView } from 'react-native-webview';
2
+ import { StyleSheet, Text, View } from 'react-native';
3
+ import GViewWebView from './GViewWebView';
4
4
 
5
5
  interface DocumentViewerProps {
6
6
  /** Public CDN URL of the doc. Must be reachable from Google's servers. */
@@ -14,28 +14,16 @@ interface DocumentViewerProps {
14
14
  * PPTX / TXT / RTF / CSV) and other non-PDF formats supported by
15
15
  * Google's free gview rendering service.
16
16
  *
17
- * Strategy: embed `https://docs.google.com/gview?url=<encoded>&embedded=true`
18
- * inside a WebView. Google fetches the doc from its public URL and
19
- * renders it to a viewable iframe — no client-side dependency on
20
- * native office libraries, no extra peer dep. Works for any backend
21
- * whose file URLs are publicly reachable (which Ethora's `/files/`
22
- * already are — they're CDN URLs returned in `item.location`).
23
17
  *
24
18
  * Limitations (documented for the integrator):
25
19
  * - Requires public URL — won't work for auth-gated downloads.
26
20
  * - First load can take 2-5s on slow networks.
27
21
  * - Read-only — no editing, no print/share buttons.
28
22
  *
29
- * For PDFs use the existing PdfViewer (downloads then renders locally).
30
23
  */
31
24
  const DocumentViewer: React.FC<DocumentViewerProps> = ({ url, fileName }) => {
32
- const [loaded, setLoaded] = useState(false);
33
25
  const [errored, setErrored] = useState(false);
34
26
 
35
- const viewerUrl = `https://docs.google.com/gview?url=${encodeURIComponent(
36
- url
37
- )}&embedded=true`;
38
-
39
27
  if (errored) {
40
28
  return (
41
29
  <View style={styles.center}>
@@ -50,37 +38,12 @@ const DocumentViewer: React.FC<DocumentViewerProps> = ({ url, fileName }) => {
50
38
  );
51
39
  }
52
40
 
53
- return (
54
- <View style={styles.container}>
55
- <WebView
56
- source={{ uri: viewerUrl }}
57
- style={styles.webview}
58
- originWhitelist={['*']}
59
- startInLoadingState
60
- onLoadEnd={() => setLoaded(true)}
61
- onError={() => setErrored(true)}
62
- onHttpError={() => setErrored(true)}
63
- />
64
- {!loaded && (
65
- <View style={styles.loadingOverlay} pointerEvents="none">
66
- <ActivityIndicator size="large" />
67
- </View>
68
- )}
69
- </View>
70
- );
41
+ return <GViewWebView url={url} onError={() => setErrored(true)} />;
71
42
  };
72
43
 
73
44
  export default DocumentViewer;
74
45
 
75
46
  const styles = StyleSheet.create({
76
- container: { flex: 1, width: '100%', backgroundColor: '#fff' },
77
- webview: { flex: 1, backgroundColor: '#fff' },
78
- loadingOverlay: {
79
- ...StyleSheet.absoluteFillObject,
80
- justifyContent: 'center',
81
- alignItems: 'center',
82
- backgroundColor: '#ffffff',
83
- },
84
47
  center: {
85
48
  flex: 1,
86
49
  alignItems: 'center',
@@ -240,6 +240,30 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
240
240
  );
241
241
  case activeFile.mimetype.startsWith('video/'):
242
242
  return <ModalVideo uri={activeFile.fileURL} />;
243
+ case activeFile.mimetype.includes('application/octet-stream'):
244
+ return (
245
+ <View
246
+ style={{
247
+ width: '100%',
248
+ padding: 20,
249
+ gap: 12,
250
+ backgroundColor: '#FFF8ED',
251
+ borderRadius: 16,
252
+ }}
253
+ >
254
+ <Text style={{ fontSize: 16, fontWeight: '600' }}>
255
+ Voice message
256
+ </Text>
257
+ <AudioMessage
258
+ src={activeFile.fileURL}
259
+ mimeType={activeFile.mimetype}
260
+ fileName={activeFile.fileName}
261
+ originalName={activeFile.originalName}
262
+ duration={activeFile.duration}
263
+ waveForm={activeFile.waveForm}
264
+ />
265
+ </View>
266
+ );
243
267
  // Mirrors the MediaMessage heuristic: treat octet-stream voicemails
244
268
  // with audio-shaped filenames / URLs as audio so the preview shows
245
269
  // the player instead of an "Unsupported" card. Customer-reported
@@ -247,7 +271,12 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
247
271
  case isLikelyAudio(
248
272
  activeFile.mimetype,
249
273
  activeFile.fileName,
250
- activeFile.fileURL
274
+ activeFile.fileURL,
275
+ {
276
+ duration: activeFile.duration,
277
+ waveForm: activeFile.waveForm,
278
+ originalName: activeFile.originalName,
279
+ }
251
280
  ):
252
281
  return (
253
282
  <View
@@ -261,11 +290,18 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
261
290
  >
262
291
  <Text style={{ fontSize: 16, fontWeight: '600' }}>
263
292
  {ensureFilenameHasExtension(
264
- activeFile.fileName,
293
+ activeFile.originalName || activeFile.fileName,
265
294
  activeFile.mimetype
266
295
  )}
267
296
  </Text>
268
- <AudioMessage src={activeFile.fileURL} />
297
+ <AudioMessage
298
+ src={activeFile.fileURL}
299
+ mimeType={activeFile.mimetype}
300
+ fileName={activeFile.fileName}
301
+ originalName={activeFile.originalName}
302
+ duration={activeFile.duration}
303
+ waveForm={activeFile.waveForm}
304
+ />
269
305
  </View>
270
306
  );
271
307
  case activeFile.mimetype === 'application/pdf':