@ethora/chat-component-rn 26.5.11 → 26.5.13

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 (154) 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 +58 -22
  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/MainComponents/ReduxWrapper.js +24 -15
  10. package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
  12. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  13. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
  14. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  15. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  16. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  17. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
  18. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  19. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
  20. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  21. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
  22. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  23. package/lib/commonjs/components/styled/AudioMessage.js +643 -66
  24. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  25. package/lib/commonjs/components/styled/Button.js +9 -3
  26. package/lib/commonjs/components/styled/Button.js.map +1 -1
  27. package/lib/commonjs/components/styled/SendInput.js +239 -40
  28. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  29. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  30. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  31. package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
  32. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  33. package/lib/commonjs/context/xmppProvider.js +68 -2
  34. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  35. package/lib/commonjs/helpers/keyboardLayout.js +5 -3
  36. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  37. package/lib/commonjs/helpers/mimeToExtension.js +70 -3
  38. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  39. package/lib/commonjs/hooks/useEventHandlers.js +1 -1
  40. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  41. package/lib/commonjs/hooks/useSendMessage.js +6 -3
  42. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  43. package/lib/commonjs/networking/xmppClient.js +26 -1
  44. package/lib/commonjs/networking/xmppClient.js.map +1 -1
  45. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
  46. package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  47. package/lib/commonjs/roomStore/roomsSlice.js +65 -4
  48. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  49. package/lib/module/assets/icons.js +7 -1
  50. package/lib/module/assets/icons.js.map +1 -1
  51. package/lib/module/components/MainComponents/ChatRoom.js +59 -23
  52. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  53. package/lib/module/components/MainComponents/MediaMessage.js +290 -7
  54. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  55. package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
  56. package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
  57. package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
  58. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  59. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
  60. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  61. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +36 -3
  62. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  63. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
  64. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  65. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
  66. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  67. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
  68. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  69. package/lib/module/components/styled/AudioMessage.js +644 -67
  70. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  71. package/lib/module/components/styled/Button.js +9 -3
  72. package/lib/module/components/styled/Button.js.map +1 -1
  73. package/lib/module/components/styled/SendInput.js +240 -42
  74. package/lib/module/components/styled/SendInput.js.map +1 -1
  75. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  76. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  77. package/lib/module/components/styled/UnsupportedType.js +47 -5
  78. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  79. package/lib/module/context/xmppProvider.js +70 -4
  80. package/lib/module/context/xmppProvider.js.map +1 -1
  81. package/lib/module/helpers/keyboardLayout.js +4 -2
  82. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  83. package/lib/module/helpers/mimeToExtension.js +67 -3
  84. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  85. package/lib/module/hooks/useEventHandlers.js +1 -1
  86. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  87. package/lib/module/hooks/useSendMessage.js +6 -3
  88. package/lib/module/hooks/useSendMessage.js.map +1 -1
  89. package/lib/module/networking/xmppClient.js +26 -1
  90. package/lib/module/networking/xmppClient.js.map +1 -1
  91. package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
  92. package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
  93. package/lib/module/roomStore/roomsSlice.js +63 -3
  94. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  95. package/lib/typescript/assets/icons.d.ts.map +1 -1
  96. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  97. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  98. package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
  99. package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
  100. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
  101. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
  102. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  103. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
  104. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
  105. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  106. package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
  107. package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
  108. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  109. package/lib/typescript/components/styled/Button.d.ts.map +1 -1
  110. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  111. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
  112. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
  113. package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
  114. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  115. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  116. package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
  117. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  118. package/lib/typescript/helpers/mimeToExtension.d.ts +31 -1
  119. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  120. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  121. package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
  122. package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
  123. package/lib/typescript/roomStore/roomsSlice.d.ts +18 -1
  124. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  125. package/lib/typescript/types/models/modal.model.d.ts +3 -0
  126. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  127. package/lib/typescript/types/types.d.ts +15 -0
  128. package/lib/typescript/types/types.d.ts.map +1 -1
  129. package/package.json +5 -1
  130. package/src/assets/icons.tsx +30 -25
  131. package/src/components/MainComponents/ChatRoom.tsx +61 -23
  132. package/src/components/MainComponents/MediaMessage.tsx +295 -5
  133. package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
  134. package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
  135. package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
  136. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
  137. package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
  138. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
  139. package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
  140. package/src/components/styled/AudioMessage.tsx +724 -59
  141. package/src/components/styled/Button.tsx +20 -11
  142. package/src/components/styled/SendInput.tsx +255 -46
  143. package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
  144. package/src/components/styled/UnsupportedType.tsx +60 -6
  145. package/src/context/xmppProvider.tsx +61 -3
  146. package/src/helpers/keyboardLayout.ts +5 -1
  147. package/src/helpers/mimeToExtension.ts +116 -2
  148. package/src/hooks/useEventHandlers.tsx +1 -1
  149. package/src/hooks/useSendMessage.tsx +12 -6
  150. package/src/networking/xmppClient.ts +24 -1
  151. package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
  152. package/src/roomStore/roomsSlice.ts +66 -2
  153. package/src/types/models/modal.model.ts +3 -0
  154. package/src/types/types.ts +32 -0
@@ -32,11 +32,13 @@ import { FlatList } from 'react-native';
32
32
  import {
33
33
  Platform,
34
34
  TouchableWithoutFeedback,
35
- Keyboard,
36
35
  View,
37
36
  KeyboardEvent,
38
37
  } from 'react-native';
39
- import { KeyboardAvoidingView } from 'react-native-keyboard-controller';
38
+ import {
39
+ KeyboardAvoidingView,
40
+ KeyboardStickyView,
41
+ } from 'react-native-keyboard-controller';
40
42
  import useComposing from '../../hooks/useComposing';
41
43
  import { store } from '../../roomStore';
42
44
  import { useSafeAreaInsets } from 'react-native-safe-area-context';
@@ -229,25 +231,61 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
229
231
  bottomInset: insets.bottom,
230
232
  });
231
233
 
234
+ // Keyboard avoidance is delegated to react-native-keyboard-controller's
235
+ // KeyboardAvoidingView. When the HOST app supplies its own keyboard
236
+ // handling (its own KeyboardProvider + KeyboardAvoidingView around
237
+ // <Chat>), this built-in one becomes a SECOND avoider wrapping the same
238
+ // tree; two of them both animating padding on keyboard open is the
239
+ // Android flicker reported in bug #6. `disableKeyboardAvoidingView`
240
+ // swaps this wrapper for a plain View so the host owns it outright
241
+ // (ReduxWrapper drops the built-in KeyboardProvider under the same flag).
242
+ //
243
+ // Bug #6 history (built-in path):
244
+ // - Original: behavior="height" on Android — flicker, because
245
+ // Android's adjustResize already shrinks the window; KAV resizing
246
+ // on top double-resized every keyboard open.
247
+ // - 26.5.6: behavior={undefined} on Android — input got completely
248
+ // blocked when a host disables adjustResize via softInputMode.
249
+ // - 26.5.8: behavior="padding" on BOTH platforms — input lifted above
250
+ // the keyboard regardless of the host's softInputMode.
251
+ // Three keyboard strategies (default = avoidingView, behaviour unchanged):
252
+ // • none ('disableKeyboardAvoidingView'): plain View — the host owns
253
+ // the keyboard entirely (ReduxWrapper also drops the KeyboardProvider).
254
+ // • sticky('keyboardStickyInput'): plain View outer + ONLY the input
255
+ // dock wrapped in <KeyboardStickyView>, so just the input tracks the
256
+ // keyboard and the message list is never resized/reflowed — avoids the
257
+ // Android "messages jump/flash" the padding KAV causes. Best for
258
+ // edge-to-edge hosts (the OS doesn't also resize the window).
259
+ // • avoidingView (default): keyboard-controller KAV, behavior="padding".
260
+ const noKeyboardHandling =
261
+ !!configWithEventHandlers?.disableKeyboardAvoidingView;
262
+ const stickyInput =
263
+ !noKeyboardHandling && !!configWithEventHandlers?.keyboardStickyInput;
264
+ const avoidKeyboard = !noKeyboardHandling && !stickyInput;
265
+ const KeyboardWrapper: React.ComponentType<any> = avoidKeyboard
266
+ ? KeyboardAvoidingView
267
+ : View;
268
+ const keyboardWrapperProps = avoidKeyboard
269
+ ? {
270
+ style: { flex: 1 },
271
+ behavior: 'padding' as const,
272
+ keyboardVerticalOffset,
273
+ }
274
+ : { style: { flex: 1 } };
275
+ // Input dock: a plain View normally; under the sticky strategy it becomes
276
+ // a KeyboardStickyView so it (and only it) lifts with the keyboard.
277
+ const InputDockTag: React.ComponentType<any> = stickyInput
278
+ ? KeyboardStickyView
279
+ : View;
280
+ const inputDockProps: any = {
281
+ style: { paddingBottom: inputDockPaddingBottom, backgroundColor: '#fff' },
282
+ ...(stickyInput
283
+ ? { offset: { closed: 0, opened: keyboardVerticalOffset } }
284
+ : {}),
285
+ };
286
+
232
287
  return (
233
- <KeyboardAvoidingView
234
- style={{ flex: 1 }}
235
- // Bug #6 history:
236
- // - Original code: behavior="height" on Android — caused
237
- // flicker because Android's adjustResize already shrinks
238
- // the window; KAV resizing on top of that double-resized
239
- // every keyboard open.
240
- // - 26.5.6 attempt: behavior={undefined} on Android — input
241
- // got completely blocked because some host apps disable
242
- // adjustResize via the activity's softInputMode.
243
- // - 26.5.8 fix: behavior="padding" on BOTH platforms. Padding
244
- // adds bottom-padding equal to the keyboard height without
245
- // changing the layout's height prop — no double-resize on
246
- // Android, no flicker, and the input is always lifted above
247
- // the keyboard regardless of the host's softInputMode.
248
- behavior="padding"
249
- keyboardVerticalOffset={keyboardVerticalOffset}
250
- >
288
+ <KeyboardWrapper {...keyboardWrapperProps}>
251
289
  <ChatContainer
252
290
  style={
253
291
  configWithEventHandlers?.chatRoomStyles
@@ -369,7 +407,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
369
407
  {editAction && editAction.isEdit && (
370
408
  <EditWrapper text={editAction.text || ''} onClose={onCloseEdit} />
371
409
  )}
372
- <View style={{ paddingBottom: inputDockPaddingBottom, backgroundColor: '#fff' }}>
410
+ <InputDockTag {...inputDockProps}>
373
411
  {CustomInputComponent ? (
374
412
  <CustomInputComponent
375
413
  sendMessage={
@@ -401,7 +439,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
401
439
  )}
402
440
  />
403
441
  )}
404
- </View>
442
+ </InputDockTag>
405
443
 
406
444
  {configWithEventHandlers?.customTypingIndicator?.enabled &&
407
445
  (configWithEventHandlers.customTypingIndicator.position ===
@@ -425,7 +463,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
425
463
  />
426
464
  )}
427
465
  </ChatContainer>
428
- </KeyboardAvoidingView>
466
+ </KeyboardWrapper>
429
467
  );
430
468
  },
431
469
  );
@@ -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
+ });
@@ -33,18 +33,32 @@ export const ReduxWrapper: React.FC<ChatWrapperProps> = React.memo(
33
33
  return props.config;
34
34
  }, [props.config]);
35
35
 
36
+ // Host apps that own their keyboard handling (their own
37
+ // KeyboardProvider + KeyboardAvoidingView around <Chat>) set
38
+ // `disableKeyboardAvoidingView` — drop the built-in KeyboardProvider
39
+ // here too so there aren't two nested providers (the second is part of
40
+ // the Android keyboard flicker in bug #6; ChatRoom drops the matching
41
+ // KeyboardAvoidingView under the same flag).
42
+ const ownKeyboardHandling = !memoizedConfig?.disableKeyboardAvoidingView;
43
+
44
+ const tree = (
45
+ <XmppProvider config={memoizedConfig} isVisible={props.isVisible}>
46
+ <ToastProvider>
47
+ <MessageNotificationProvider config={memoizedConfig}>
48
+ <LoginWrapper config={memoizedConfig} {...props} />
49
+ </MessageNotificationProvider>
50
+ </ToastProvider>
51
+ </XmppProvider>
52
+ );
53
+
36
54
  return (
37
55
  <Provider store={store}>
38
56
  <SafeAreaProvider>
39
- <KeyboardProvider>
40
- <XmppProvider config={memoizedConfig} isVisible={props.isVisible}>
41
- <ToastProvider>
42
- <MessageNotificationProvider config={memoizedConfig}>
43
- <LoginWrapper config={memoizedConfig} {...props} />
44
- </MessageNotificationProvider>
45
- </ToastProvider>
46
- </XmppProvider>
47
- </KeyboardProvider>
57
+ {ownKeyboardHandling ? (
58
+ <KeyboardProvider>{tree}</KeyboardProvider>
59
+ ) : (
60
+ tree
61
+ )}
48
62
  </SafeAreaProvider>
49
63
  </Provider>
50
64
  );
@@ -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',