@ethora/chat-component-rn 26.5.10 → 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 (209) hide show
  1. package/CHANGELOG.md +65 -1
  2. package/README.md +30 -1
  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/ChatHeader.js +31 -16
  6. package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
  7. package/lib/commonjs/components/MainComponents/ChatRoom.js +34 -4
  8. package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
  9. package/lib/commonjs/components/MainComponents/ChatWrapper.js +17 -14
  10. package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
  11. package/lib/commonjs/components/MainComponents/MediaMessage.js +298 -12
  12. package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
  13. package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js +119 -0
  14. package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js.map +1 -0
  15. package/lib/commonjs/components/MessageBubble/Message.js +39 -3
  16. package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
  17. package/lib/commonjs/components/MessageBubble/MessageInteractions.js +87 -39
  18. package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
  19. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +153 -62
  20. package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  21. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
  22. package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  23. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
  24. package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  25. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +40 -3
  26. package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  27. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
  28. package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  29. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
  30. package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  31. package/lib/commonjs/components/Modals/ModalHeaderComponent.js +10 -0
  32. package/lib/commonjs/components/Modals/ModalHeaderComponent.js.map +1 -1
  33. package/lib/commonjs/components/Modals/styledModalComponents.js +13 -4
  34. package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
  35. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
  36. package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  37. package/lib/commonjs/components/styled/AudioMessage.js +643 -66
  38. package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
  39. package/lib/commonjs/components/styled/Button.js +9 -3
  40. package/lib/commonjs/components/styled/Button.js.map +1 -1
  41. package/lib/commonjs/components/styled/SendInput.js +239 -40
  42. package/lib/commonjs/components/styled/SendInput.js.map +1 -1
  43. package/lib/commonjs/components/styled/StyledComponents.js +6 -3
  44. package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
  45. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  46. package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  47. package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
  48. package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
  49. package/lib/commonjs/components/styled/VideoMessage.js +44 -25
  50. package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
  51. package/lib/commonjs/context/xmppProvider.js +5 -2
  52. package/lib/commonjs/context/xmppProvider.js.map +1 -1
  53. package/lib/commonjs/helpers/keyboardLayout.js +5 -3
  54. package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
  55. package/lib/commonjs/helpers/mimeToExtension.js +123 -0
  56. package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
  57. package/lib/commonjs/hooks/useEventHandlers.js +1 -1
  58. package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
  59. package/lib/commonjs/hooks/useLogout.js +33 -8
  60. package/lib/commonjs/hooks/useLogout.js.map +1 -1
  61. package/lib/commonjs/hooks/useSendMessage.js +6 -3
  62. package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
  63. package/lib/commonjs/networking/api-requests/rooms.api.js +29 -4
  64. package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
  65. package/lib/commonjs/roomStore/roomsSlice.js +6 -0
  66. package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
  67. package/lib/module/assets/icons.js +7 -1
  68. package/lib/module/assets/icons.js.map +1 -1
  69. package/lib/module/components/MainComponents/ChatHeader.js +31 -16
  70. package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
  71. package/lib/module/components/MainComponents/ChatRoom.js +36 -6
  72. package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
  73. package/lib/module/components/MainComponents/ChatWrapper.js +17 -14
  74. package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
  75. package/lib/module/components/MainComponents/MediaMessage.js +301 -15
  76. package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
  77. package/lib/module/components/MessageBubble/InteractionsOverlay.js +112 -0
  78. package/lib/module/components/MessageBubble/InteractionsOverlay.js.map +1 -0
  79. package/lib/module/components/MessageBubble/Message.js +40 -4
  80. package/lib/module/components/MessageBubble/Message.js.map +1 -1
  81. package/lib/module/components/MessageBubble/MessageInteractions.js +89 -41
  82. package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
  83. package/lib/module/components/Modals/AttachSheet/AttachSheet.js +155 -64
  84. package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
  85. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
  86. package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
  87. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
  88. package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
  89. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +41 -4
  90. package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
  91. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
  92. package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
  93. package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
  94. package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
  95. package/lib/module/components/Modals/ModalHeaderComponent.js +10 -0
  96. package/lib/module/components/Modals/ModalHeaderComponent.js.map +1 -1
  97. package/lib/module/components/Modals/styledModalComponents.js +13 -4
  98. package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
  99. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
  100. package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
  101. package/lib/module/components/styled/AudioMessage.js +644 -67
  102. package/lib/module/components/styled/AudioMessage.js.map +1 -1
  103. package/lib/module/components/styled/Button.js +9 -3
  104. package/lib/module/components/styled/Button.js.map +1 -1
  105. package/lib/module/components/styled/SendInput.js +240 -42
  106. package/lib/module/components/styled/SendInput.js.map +1 -1
  107. package/lib/module/components/styled/StyledComponents.js +6 -3
  108. package/lib/module/components/styled/StyledComponents.js.map +1 -1
  109. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
  110. package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
  111. package/lib/module/components/styled/UnsupportedType.js +47 -5
  112. package/lib/module/components/styled/UnsupportedType.js.map +1 -1
  113. package/lib/module/components/styled/VideoMessage.js +45 -26
  114. package/lib/module/components/styled/VideoMessage.js.map +1 -1
  115. package/lib/module/context/xmppProvider.js +5 -2
  116. package/lib/module/context/xmppProvider.js.map +1 -1
  117. package/lib/module/helpers/keyboardLayout.js +4 -2
  118. package/lib/module/helpers/keyboardLayout.js.map +1 -1
  119. package/lib/module/helpers/mimeToExtension.js +119 -0
  120. package/lib/module/helpers/mimeToExtension.js.map +1 -1
  121. package/lib/module/hooks/useEventHandlers.js +1 -1
  122. package/lib/module/hooks/useEventHandlers.js.map +1 -1
  123. package/lib/module/hooks/useLogout.js +33 -8
  124. package/lib/module/hooks/useLogout.js.map +1 -1
  125. package/lib/module/hooks/useSendMessage.js +6 -3
  126. package/lib/module/hooks/useSendMessage.js.map +1 -1
  127. package/lib/module/networking/api-requests/rooms.api.js +29 -4
  128. package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
  129. package/lib/module/roomStore/roomsSlice.js +6 -0
  130. package/lib/module/roomStore/roomsSlice.js.map +1 -1
  131. package/lib/typescript/assets/icons.d.ts.map +1 -1
  132. package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
  133. package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
  134. package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
  135. package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
  136. package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts +13 -0
  137. package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts.map +1 -0
  138. package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
  139. package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
  140. package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
  141. package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
  142. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
  143. package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
  144. package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
  145. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
  146. package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
  147. package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
  148. package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts.map +1 -1
  149. package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
  150. package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
  151. package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
  152. package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
  153. package/lib/typescript/components/styled/Button.d.ts.map +1 -1
  154. package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
  155. package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
  156. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
  157. package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
  158. package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
  159. package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
  160. package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
  161. package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
  162. package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
  163. package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
  164. package/lib/typescript/helpers/mimeToExtension.d.ts +38 -0
  165. package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
  166. package/lib/typescript/hooks/useLogout.d.ts +23 -1
  167. package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
  168. package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
  169. package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
  170. package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
  171. package/lib/typescript/types/models/modal.model.d.ts +3 -0
  172. package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
  173. package/lib/typescript/types/types.d.ts +29 -0
  174. package/lib/typescript/types/types.d.ts.map +1 -1
  175. package/package.json +11 -4
  176. package/src/assets/icons.tsx +30 -25
  177. package/src/components/MainComponents/ChatHeader.tsx +46 -15
  178. package/src/components/MainComponents/ChatRoom.tsx +38 -3
  179. package/src/components/MainComponents/ChatWrapper.tsx +28 -21
  180. package/src/components/MainComponents/MediaMessage.tsx +304 -11
  181. package/src/components/MessageBubble/InteractionsOverlay.tsx +121 -0
  182. package/src/components/MessageBubble/Message.tsx +44 -3
  183. package/src/components/MessageBubble/MessageInteractions.tsx +103 -52
  184. package/src/components/Modals/AttachSheet/AttachSheet.tsx +144 -51
  185. package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +25 -3
  186. package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
  187. package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +48 -4
  188. package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
  189. package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
  190. package/src/components/Modals/ModalHeaderComponent.tsx +8 -1
  191. package/src/components/Modals/styledModalComponents.tsx +13 -4
  192. package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
  193. package/src/components/styled/AudioMessage.tsx +724 -59
  194. package/src/components/styled/Button.tsx +20 -11
  195. package/src/components/styled/SendInput.tsx +255 -46
  196. package/src/components/styled/StyledComponents.ts +6 -3
  197. package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
  198. package/src/components/styled/UnsupportedType.tsx +60 -6
  199. package/src/components/styled/VideoMessage.tsx +21 -5
  200. package/src/context/xmppProvider.tsx +6 -2
  201. package/src/helpers/keyboardLayout.ts +5 -1
  202. package/src/helpers/mimeToExtension.ts +178 -0
  203. package/src/hooks/useEventHandlers.tsx +1 -1
  204. package/src/hooks/useLogout.tsx +33 -10
  205. package/src/hooks/useSendMessage.tsx +12 -6
  206. package/src/networking/api-requests/rooms.api.ts +31 -7
  207. package/src/roomStore/roomsSlice.ts +7 -0
  208. package/src/types/models/modal.model.ts +3 -0
  209. package/src/types/types.ts +29 -0
@@ -1,4 +1,4 @@
1
- import React, { useMemo, useState } from 'react';
1
+ import React, { useEffect, useId, useMemo, useState } from 'react';
2
2
  import { Delimeter, MenuItem } from '../ContextMenu/ContextMenuComponents';
3
3
  import { useSelector } from 'react-redux';
4
4
  import { RootState } from '../../roomStore';
@@ -8,16 +8,17 @@ import {
8
8
  } from '../../helpers/constants/MESSAGE_INTERACTIONS';
9
9
  import { IMessage } from '../../types/types';
10
10
  import {
11
- Modal,
12
11
  Text,
13
12
  StyleSheet,
14
13
  View,
15
14
  Pressable,
16
15
  Dimensions,
16
+ Keyboard,
17
17
  type LayoutChangeEvent,
18
18
  } from 'react-native';
19
19
  import * as Clipboard from 'expo-clipboard';
20
20
  import { useToast } from '../../context/ToastContext';
21
+ import { useInteractionsOverlay } from './InteractionsOverlay';
21
22
 
22
23
  interface MessageInteractionsProps {
23
24
  isReply?: boolean;
@@ -47,6 +48,8 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
47
48
  handleEditMessage,
48
49
  }) => {
49
50
  const { showToast } = useToast();
51
+ const { present, dismiss, originX, originY } = useInteractionsOverlay();
52
+ const overlayId = useId();
50
53
 
51
54
  const config = useSelector(
52
55
  (state: RootState) => state.chatSettingStore.config
@@ -54,6 +57,28 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
54
57
 
55
58
  const [menuSize, setMenuSize] = useState({ width: 0, height: 0 });
56
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
+
57
82
  const handleMenuLayout = (e: LayoutChangeEvent) => {
58
83
  const { width, height } = e.nativeEvent.layout;
59
84
  if (
@@ -105,7 +130,13 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
105
130
  const topReserve = 16;
106
131
  const sideMargin = 8;
107
132
 
108
- 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;
109
140
  const spaceAbove = position.top - topReserve;
110
141
 
111
142
  let top: number;
@@ -114,7 +145,10 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
114
145
  } else if (spaceAbove >= menuSize.height) {
115
146
  top = position.top - menuSize.height;
116
147
  } else {
117
- top = Math.max(topReserve, screenHeight - menuSize.height - bottomReserve);
148
+ top = Math.max(
149
+ topReserve,
150
+ visibleBottom - menuSize.height - bottomReserve
151
+ );
118
152
  }
119
153
 
120
154
  const leftMargin = position.left;
@@ -128,56 +162,73 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
128
162
  );
129
163
 
130
164
  return { top, left };
131
- }, [position, menuSize, config?.keyboardVerticalOffset]);
132
-
133
- if (config?.disableInteractions) {return null;}
134
-
135
- return (
136
- <Modal
137
- transparent
138
- animationType="fade"
139
- visible={true}
140
- onRequestClose={closeMenu}
141
- >
142
- {!message.isDeleted && (
143
- <View style={styles.overlayFill}>
144
- <Pressable style={StyleSheet.absoluteFill} onPress={closeMenu} />
145
- <View
146
- style={[styles.contextMenu, memoPosition]}
147
- onLayout={handleMenuLayout}
148
- >
149
- <MenuItem onPress={() => handleCopyMessage(message.body!)}>
150
- <Text style={styles.menuText}>{MESSAGE_INTERACTIONS.COPY}</Text>
151
- <MESSAGE_INTERACTIONS_ICONS.COPY />
152
- </MenuItem>
153
- {isUser && (
154
- <>
155
- {/* Edit only for non-media messages (upstream guard) +
156
- consistent label styling (menuText). */}
157
- {message?.isMediafile !== 'true' && (
158
- <>
159
- <Delimeter />
160
- <MenuItem onPress={handleEditMessage}>
161
- <Text style={styles.menuText}>
162
- {MESSAGE_INTERACTIONS.EDIT}
163
- </Text>
164
- <MESSAGE_INTERACTIONS_ICONS.EDIT />
165
- </MenuItem>
166
- </>
167
- )}
168
- <Delimeter />
169
- <MenuItem onPress={handleDeleteMessage}>
170
- <Text style={styles.menuText}>{MESSAGE_INTERACTIONS.DELETE}</Text>
171
- <MESSAGE_INTERACTIONS_ICONS.DELETE />
172
- </MenuItem>
173
- </>
174
- )}
175
- </View>
165
+ }, [position, menuSize, config?.keyboardVerticalOffset, keyboardHeight]);
166
+
167
+ // Window coords (memoPosition) → host-local coords. The overlay host
168
+ // sits below the status bar / header, so subtract its measured origin.
169
+ const localPosition = useMemo(() => {
170
+ if (!memoPosition) {return undefined;}
171
+ return {
172
+ ...memoPosition,
173
+ top: (memoPosition as { top: number }).top - originY,
174
+ left: (memoPosition as { left: number }).left - originX,
175
+ };
176
+ }, [memoPosition, originX, originY]);
177
+
178
+ const content =
179
+ config?.disableInteractions || message.isDeleted ? null : (
180
+ <View style={styles.overlayFill}>
181
+ <Pressable style={StyleSheet.absoluteFill} onPress={closeMenu} />
182
+ <View
183
+ style={[styles.contextMenu, localPosition]}
184
+ onLayout={handleMenuLayout}
185
+ >
186
+ <MenuItem onPress={() => handleCopyMessage(message.body!)}>
187
+ <Text style={styles.menuText}>{MESSAGE_INTERACTIONS.COPY}</Text>
188
+ <MESSAGE_INTERACTIONS_ICONS.COPY />
189
+ </MenuItem>
190
+ {isUser && (
191
+ <>
192
+ {/* Edit only for non-media messages (upstream guard) +
193
+ consistent label styling (menuText). */}
194
+ {message?.isMediafile !== 'true' && (
195
+ <>
196
+ <Delimeter />
197
+ <MenuItem onPress={handleEditMessage}>
198
+ <Text style={styles.menuText}>
199
+ {MESSAGE_INTERACTIONS.EDIT}
200
+ </Text>
201
+ <MESSAGE_INTERACTIONS_ICONS.EDIT />
202
+ </MenuItem>
203
+ </>
204
+ )}
205
+ <Delimeter />
206
+ <MenuItem onPress={handleDeleteMessage}>
207
+ <Text style={styles.menuText}>{MESSAGE_INTERACTIONS.DELETE}</Text>
208
+ <MESSAGE_INTERACTIONS_ICONS.DELETE />
209
+ </MenuItem>
210
+ </>
211
+ )}
176
212
  </View>
177
- )}
213
+ </View>
214
+ );
178
215
 
179
- </Modal>
180
- );
216
+ // Render the menu in the in-tree overlay host instead of a React Native
217
+ // <Modal>. A Modal opens its own window on Android, which steals focus
218
+ // from the chat input and dismisses the keyboard when the menu opens;
219
+ // hosting it in the same view tree keeps the keyboard up. Re-run every
220
+ // render so menuSize/position updates refresh the hosted node.
221
+ // eslint-disable-next-line react-hooks/exhaustive-deps
222
+ useEffect(() => {
223
+ if (content) {
224
+ present(overlayId, content);
225
+ } else {
226
+ dismiss(overlayId);
227
+ }
228
+ });
229
+ useEffect(() => () => dismiss(overlayId), [dismiss, overlayId]);
230
+
231
+ return null;
181
232
  };
182
233
 
183
234
  export default MessageInteractions;
@@ -1,7 +1,9 @@
1
1
  /** @format */
2
2
 
3
- import React, { useRef } from 'react';
3
+ import React, { useEffect, useRef, useState } from 'react';
4
4
  import {
5
+ Animated,
6
+ Easing,
5
7
  Modal,
6
8
  Platform,
7
9
  StyleSheet,
@@ -20,6 +22,11 @@ interface AttachSheetProps {
20
22
  primaryColor?: string;
21
23
  }
22
24
 
25
+ // Starting translateY for the sheet. Larger than any realistic sheet
26
+ // height so it sits fully below the viewport before opening; the exact
27
+ // number doesn't matter visually because it animates to 0.
28
+ const SHEET_OFFSCREEN = 600;
29
+
23
30
  const AttachSheet: React.FC<AttachSheetProps> = ({
24
31
  visible,
25
32
  onClose,
@@ -28,22 +35,94 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
28
35
  onDocument,
29
36
  primaryColor = '#0052CD',
30
37
  }) => {
38
+ // Pending handler captured on row tap — runs once the EXIT animation
39
+ // finishes so iOS doesn't try to present an image picker over a
40
+ // still-dismissing modal (was the original reason for the
41
+ // platform-split + Modal.onDismiss dance). The animation completion
42
+ // callback now drives this on both platforms.
31
43
  const pendingRef = useRef<(() => void) | null>(null);
32
44
 
33
- const runPending = () => {
34
- const fn = pendingRef.current;
35
- pendingRef.current = null;
36
- fn?.();
37
- };
45
+ // Local "modal stays mounted while animating out" state. The parent
46
+ // controls `visible`; we mirror it but delay unmount until the exit
47
+ // animation has played so the slide-down is actually seen.
48
+ const [mounted, setMounted] = useState(visible);
38
49
 
39
- const trigger = (handler: () => void) => () => {
40
- if (Platform.OS === 'ios') {
41
- pendingRef.current = handler;
42
- onClose();
43
- } else {
44
- onClose();
45
- setTimeout(handler, 150);
50
+ // Two separate Animated values — the whole point of the rewrite:
51
+ // • backdropOpacity: fades the dim layer in/out (no slide).
52
+ // • sheetTranslateY: slides ONLY the sheet up from below.
53
+ // RN Modal's built-in `animationType="slide"` slides the entire
54
+ // contents (backdrop included) so the dim shade entered from the
55
+ // bottom — what looked "awful". Splitting these gives the standard
56
+ // bottom-sheet feel.
57
+ const backdropOpacity = useRef(new Animated.Value(0)).current;
58
+ const sheetTranslateY = useRef(
59
+ new Animated.Value(SHEET_OFFSCREEN)
60
+ ).current;
61
+
62
+ useEffect(() => {
63
+ if (visible) {
64
+ setMounted(true);
65
+ // Reset to starting positions BEFORE the Modal becomes visible so
66
+ // the first animated frame is below-the-fold (not a flash of the
67
+ // settled sheet).
68
+ backdropOpacity.setValue(0);
69
+ sheetTranslateY.setValue(SHEET_OFFSCREEN);
70
+ // requestAnimationFrame lets the Modal commit its mount before the
71
+ // animation starts; without this the first frame can be skipped.
72
+ const raf = requestAnimationFrame(() => {
73
+ Animated.parallel([
74
+ Animated.timing(backdropOpacity, {
75
+ toValue: 1,
76
+ duration: 220,
77
+ easing: Easing.out(Easing.cubic),
78
+ useNativeDriver: true,
79
+ }),
80
+ Animated.spring(sheetTranslateY, {
81
+ toValue: 0,
82
+ // Tuned for a quick-but-not-bouncy entry. tension is the
83
+ // "stiffness" knob, friction the "damping" knob in RN's
84
+ // spring config.
85
+ tension: 90,
86
+ friction: 14,
87
+ useNativeDriver: true,
88
+ }),
89
+ ]).start();
90
+ });
91
+ return () => cancelAnimationFrame(raf);
46
92
  }
93
+ if (!mounted) {return;}
94
+ // Exit: fade the backdrop, slide the sheet down, then unmount and
95
+ // fire any pending picker handler.
96
+ Animated.parallel([
97
+ Animated.timing(backdropOpacity, {
98
+ toValue: 0,
99
+ duration: 180,
100
+ easing: Easing.in(Easing.cubic),
101
+ useNativeDriver: true,
102
+ }),
103
+ Animated.timing(sheetTranslateY, {
104
+ toValue: SHEET_OFFSCREEN,
105
+ duration: 220,
106
+ easing: Easing.in(Easing.cubic),
107
+ useNativeDriver: true,
108
+ }),
109
+ ]).start(({ finished }) => {
110
+ if (!finished) {return;} // interrupted (rapid re-open) → leave state alone
111
+ setMounted(false);
112
+ const fn = pendingRef.current;
113
+ pendingRef.current = null;
114
+ fn?.();
115
+ });
116
+ return undefined;
117
+ }, [visible]); // eslint-disable-line react-hooks/exhaustive-deps
118
+
119
+ const trigger = (handler: () => void) => () => {
120
+ // Capture the handler and ask parent to close. The exit-animation
121
+ // completion above fires `handler` AFTER the sheet has slid away,
122
+ // so iOS no longer tries to present a picker over a dismissing
123
+ // modal — and Android no longer needs the ad-hoc 150ms setTimeout.
124
+ pendingRef.current = handler;
125
+ onClose();
47
126
  };
48
127
 
49
128
  const rows: {
@@ -75,52 +154,66 @@ const AttachSheet: React.FC<AttachSheetProps> = ({
75
154
  return (
76
155
  <Modal
77
156
  transparent
78
- visible={visible}
79
- animationType="slide"
157
+ visible={mounted}
158
+ // We drive the animation ourselves — disable RN's built-in slide
159
+ // (which slid the backdrop too) so the fade + spring above are the
160
+ // only motion.
161
+ animationType="none"
80
162
  onRequestClose={onClose}
81
- onDismiss={runPending}
163
+ // Hardware-back / iOS gesture also routes through onClose, which
164
+ // flips `visible` and triggers our exit animation above.
82
165
  >
83
- <TouchableOpacity
84
- style={styles.backdrop}
85
- activeOpacity={1}
86
- onPress={onClose}
166
+ <Animated.View
167
+ style={[styles.backdrop, { opacity: backdropOpacity }]}
87
168
  >
88
- <TouchableOpacity activeOpacity={1} style={styles.sheet}>
89
- <View style={styles.grabber} />
90
- <Text style={styles.title}>Attach</Text>
91
- {rows.map(({ label, hint, Icon, handler }, idx) => (
92
- <TouchableOpacity
93
- key={label}
94
- activeOpacity={0.6}
95
- style={[
96
- styles.row,
97
- idx < rows.length - 1 && styles.rowDivider,
98
- ]}
99
- onPress={trigger(handler)}
100
- >
101
- <View
169
+ <TouchableOpacity
170
+ style={StyleSheet.absoluteFill}
171
+ activeOpacity={1}
172
+ onPress={onClose}
173
+ />
174
+ <Animated.View
175
+ style={[
176
+ styles.sheet,
177
+ { transform: [{ translateY: sheetTranslateY }] },
178
+ ]}
179
+ >
180
+ <TouchableOpacity activeOpacity={1}>
181
+ <View style={styles.grabber} />
182
+ <Text style={styles.title}>Attach</Text>
183
+ {rows.map(({ label, hint, Icon, handler }, idx) => (
184
+ <TouchableOpacity
185
+ key={label}
186
+ activeOpacity={0.6}
102
187
  style={[
103
- styles.iconBubble,
104
- { backgroundColor: primaryColor + '14' },
188
+ styles.row,
189
+ idx < rows.length - 1 && styles.rowDivider,
105
190
  ]}
191
+ onPress={trigger(handler)}
106
192
  >
107
- <Icon color={primaryColor} width={20} height={20} />
108
- </View>
109
- <View style={styles.rowText}>
110
- <Text style={styles.rowLabel}>{label}</Text>
111
- <Text style={styles.rowHint}>{hint}</Text>
112
- </View>
193
+ <View
194
+ style={[
195
+ styles.iconBubble,
196
+ { backgroundColor: primaryColor + '14' },
197
+ ]}
198
+ >
199
+ <Icon color={primaryColor} width={20} height={20} />
200
+ </View>
201
+ <View style={styles.rowText}>
202
+ <Text style={styles.rowLabel}>{label}</Text>
203
+ <Text style={styles.rowHint}>{hint}</Text>
204
+ </View>
205
+ </TouchableOpacity>
206
+ ))}
207
+ <TouchableOpacity
208
+ activeOpacity={0.6}
209
+ style={styles.cancelRow}
210
+ onPress={onClose}
211
+ >
212
+ <Text style={styles.cancelLabel}>Cancel</Text>
113
213
  </TouchableOpacity>
114
- ))}
115
- <TouchableOpacity
116
- activeOpacity={0.6}
117
- style={styles.cancelRow}
118
- onPress={onClose}
119
- >
120
- <Text style={styles.cancelLabel}>Cancel</Text>
121
214
  </TouchableOpacity>
122
- </TouchableOpacity>
123
- </TouchableOpacity>
215
+ </Animated.View>
216
+ </Animated.View>
124
217
  </Modal>
125
218
  );
126
219
  };
@@ -293,11 +293,22 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
293
293
  <ProfileImagePlaceholder
294
294
  name={activeRoom.title || activeRoom.name}
295
295
  icon={activeRoom.icon}
296
+ // Two gates control whether the icon is editable:
297
+ // 1. role — only non-participants could edit before.
298
+ // 2. NEW: `disableChatInfo.disableIconEdit` — hard kill from
299
+ // config so a consumer can make the icon read-only for
300
+ // everyone (e.g. branded rooms, patient-facing apps).
301
+ // Both must allow it for the picker/remove affordances to show.
296
302
  upload={{
297
303
  onUpload,
298
- active: activeRoom?.role !== 'participant' ? true : false,
304
+ active:
305
+ !config?.disableChatInfo?.disableIconEdit &&
306
+ activeRoom?.role !== 'participant',
307
+ }}
308
+ remove={{
309
+ enabled: !config?.disableChatInfo?.disableIconEdit,
310
+ onRemoveClick,
299
311
  }}
300
- remove={{ enabled: true, onRemoveClick }}
301
312
  role={activeRoom?.role}
302
313
  size={128}
303
314
  />
@@ -377,7 +388,18 @@ const ChatProfileModal: React.FC<ChatProfileModalProps> = ({
377
388
  >
378
389
  <Pressable
379
390
  style={{ flexDirection: 'row', alignItems: 'center', justifyContent: 'flex-start', gap: 16 }}
380
- onPress={() => handleUserAvatarClick(user)}
391
+ // `disableChatInfo.disableMemberTap` blocks the
392
+ // tap entirely so the user-profile popup never
393
+ // opens. `disableMemberProfileActions` only hid
394
+ // the action buttons INSIDE that popup, leaving
395
+ // the popup itself to open on tap — this gate
396
+ // closes the door. Customer-reported #16.
397
+ onPress={
398
+ config?.disableChatInfo?.disableMemberTap
399
+ ? undefined
400
+ : () => handleUserAvatarClick(user)
401
+ }
402
+ disabled={!!config?.disableChatInfo?.disableMemberTap}
381
403
  >
382
404
  <ProfileImagePlaceholder
383
405
  name={`${user.firstName} ${user.lastName}`}
@@ -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',
@@ -27,7 +27,7 @@ import { useToast } from '../../../context/ToastContext';
27
27
  import PdfViewer from './PdfView';
28
28
  import DocumentViewer from './DocumentViewer';
29
29
  import AudioMessage from '../../styled/AudioMessage';
30
- import { ensureFilenameHasExtension } from '../../../helpers/mimeToExtension';
30
+ import { ensureFilenameHasExtension, isLikelyAudio } from '../../../helpers/mimeToExtension';
31
31
 
32
32
  // MIME types Google's gview embed renders reliably. Everything else
33
33
  // falls through to the info-card so the user can still download.
@@ -240,7 +240,44 @@ 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.startsWith('audio/'):
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
+ );
267
+ // Mirrors the MediaMessage heuristic: treat octet-stream voicemails
268
+ // with audio-shaped filenames / URLs as audio so the preview shows
269
+ // the player instead of an "Unsupported" card. Customer-reported
270
+ // #9 voicemail fix — see isLikelyAudio in mimeToExtension.ts.
271
+ case isLikelyAudio(
272
+ activeFile.mimetype,
273
+ activeFile.fileName,
274
+ activeFile.fileURL,
275
+ {
276
+ duration: activeFile.duration,
277
+ waveForm: activeFile.waveForm,
278
+ originalName: activeFile.originalName,
279
+ }
280
+ ):
244
281
  return (
245
282
  <View
246
283
  style={{
@@ -253,11 +290,18 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
253
290
  >
254
291
  <Text style={{ fontSize: 16, fontWeight: '600' }}>
255
292
  {ensureFilenameHasExtension(
256
- activeFile.fileName,
293
+ activeFile.originalName || activeFile.fileName,
257
294
  activeFile.mimetype
258
295
  )}
259
296
  </Text>
260
- <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
+ />
261
305
  </View>
262
306
  );
263
307
  case activeFile.mimetype === 'application/pdf':