@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.
- package/CHANGELOG.md +65 -1
- package/README.md +30 -1
- package/lib/commonjs/assets/icons.js +7 -1
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +31 -16
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +34 -4
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +17 -14
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +298 -12
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js +119 -0
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js.map +1 -0
- package/lib/commonjs/components/MessageBubble/Message.js +39 -3
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +87 -39
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js +153 -62
- package/lib/commonjs/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js +5 -44
- package/lib/commonjs/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +40 -3
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +149 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js +4 -17
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/commonjs/components/Modals/ModalHeaderComponent.js +10 -0
- package/lib/commonjs/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/commonjs/components/Modals/styledModalComponents.js +13 -4
- package/lib/commonjs/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js +19 -33
- package/lib/commonjs/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/AudioMessage.js +643 -66
- package/lib/commonjs/components/styled/AudioMessage.js.map +1 -1
- package/lib/commonjs/components/styled/Button.js +9 -3
- package/lib/commonjs/components/styled/Button.js.map +1 -1
- package/lib/commonjs/components/styled/SendInput.js +239 -40
- package/lib/commonjs/components/styled/SendInput.js.map +1 -1
- package/lib/commonjs/components/styled/StyledComponents.js +6 -3
- package/lib/commonjs/components/styled/StyledComponents.js.map +1 -1
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/commonjs/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/commonjs/components/styled/UnsupportedType.js +47 -5
- package/lib/commonjs/components/styled/UnsupportedType.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +44 -25
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +5 -2
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/keyboardLayout.js +5 -3
- package/lib/commonjs/helpers/keyboardLayout.js.map +1 -1
- package/lib/commonjs/helpers/mimeToExtension.js +123 -0
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js +1 -1
- package/lib/commonjs/hooks/useEventHandlers.js.map +1 -1
- package/lib/commonjs/hooks/useLogout.js +33 -8
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +6 -3
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +29 -4
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +6 -0
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/assets/icons.js +7 -1
- package/lib/module/assets/icons.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +31 -16
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +36 -6
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +17 -14
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +301 -15
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MessageBubble/InteractionsOverlay.js +112 -0
- package/lib/module/components/MessageBubble/InteractionsOverlay.js.map +1 -0
- package/lib/module/components/MessageBubble/Message.js +40 -4
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +89 -41
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js +155 -64
- package/lib/module/components/Modals/AttachSheet/AttachSheet.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +20 -5
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js +5 -45
- package/lib/module/components/Modals/FilePreviewModal/DocumentViewer.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +41 -4
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +144 -0
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js +3 -17
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/module/components/Modals/ModalHeaderComponent.js +10 -0
- package/lib/module/components/Modals/ModalHeaderComponent.js.map +1 -1
- package/lib/module/components/Modals/styledModalComponents.js +13 -4
- package/lib/module/components/Modals/styledModalComponents.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js +20 -34
- package/lib/module/components/RoomComponents/styled/LastAudioMessage.js.map +1 -1
- package/lib/module/components/styled/AudioMessage.js +644 -67
- package/lib/module/components/styled/AudioMessage.js.map +1 -1
- package/lib/module/components/styled/Button.js +9 -3
- package/lib/module/components/styled/Button.js.map +1 -1
- package/lib/module/components/styled/SendInput.js +240 -42
- package/lib/module/components/styled/SendInput.js.map +1 -1
- package/lib/module/components/styled/StyledComponents.js +6 -3
- package/lib/module/components/styled/StyledComponents.js.map +1 -1
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js +31 -18
- package/lib/module/components/styled/StyledInputComponents/MediaComponents.js.map +1 -1
- package/lib/module/components/styled/UnsupportedType.js +47 -5
- package/lib/module/components/styled/UnsupportedType.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +45 -26
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +5 -2
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/keyboardLayout.js +4 -2
- package/lib/module/helpers/keyboardLayout.js.map +1 -1
- package/lib/module/helpers/mimeToExtension.js +119 -0
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useEventHandlers.js +1 -1
- package/lib/module/hooks/useEventHandlers.js.map +1 -1
- package/lib/module/hooks/useLogout.js +33 -8
- package/lib/module/hooks/useLogout.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +6 -3
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +29 -4
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +6 -0
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts +13 -0
- package/lib/typescript/components/MessageBubble/InteractionsOverlay.d.ts.map +1 -0
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.d.ts.map +1 -1
- package/lib/typescript/components/Modals/AttachSheet/AttachSheet.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts +0 -7
- package/lib/typescript/components/Modals/FilePreviewModal/DocumentViewer.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts +8 -0
- package/lib/typescript/components/Modals/FilePreviewModal/GViewWebView.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/PdfView.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ModalHeaderComponent.d.ts.map +1 -1
- package/lib/typescript/components/Modals/styledModalComponents.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastAudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/AudioMessage.d.ts +8 -2
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/Button.d.ts.map +1 -1
- package/lib/typescript/components/styled/SendInput.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts +5 -1
- package/lib/typescript/components/styled/StyledInputComponents/MediaComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/UnsupportedType.d.ts +5 -0
- package/lib/typescript/components/styled/UnsupportedType.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts +3 -1
- package/lib/typescript/helpers/keyboardLayout.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +38 -0
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useLogout.d.ts +23 -1
- package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +3 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +29 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +11 -4
- package/src/assets/icons.tsx +30 -25
- package/src/components/MainComponents/ChatHeader.tsx +46 -15
- package/src/components/MainComponents/ChatRoom.tsx +38 -3
- package/src/components/MainComponents/ChatWrapper.tsx +28 -21
- package/src/components/MainComponents/MediaMessage.tsx +304 -11
- package/src/components/MessageBubble/InteractionsOverlay.tsx +121 -0
- package/src/components/MessageBubble/Message.tsx +44 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +103 -52
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +144 -51
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +25 -3
- package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +48 -4
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
- package/src/components/Modals/ModalHeaderComponent.tsx +8 -1
- package/src/components/Modals/styledModalComponents.tsx +13 -4
- package/src/components/RoomComponents/styled/LastAudioMessage.tsx +20 -35
- package/src/components/styled/AudioMessage.tsx +724 -59
- package/src/components/styled/Button.tsx +20 -11
- package/src/components/styled/SendInput.tsx +255 -46
- package/src/components/styled/StyledComponents.ts +6 -3
- package/src/components/styled/StyledInputComponents/MediaComponents.tsx +29 -18
- package/src/components/styled/UnsupportedType.tsx +60 -6
- package/src/components/styled/VideoMessage.tsx +21 -5
- package/src/context/xmppProvider.tsx +6 -2
- package/src/helpers/keyboardLayout.ts +5 -1
- package/src/helpers/mimeToExtension.ts +178 -0
- package/src/hooks/useEventHandlers.tsx +1 -1
- package/src/hooks/useLogout.tsx +33 -10
- package/src/hooks/useSendMessage.tsx +12 -6
- package/src/networking/api-requests/rooms.api.ts +31 -7
- package/src/roomStore/roomsSlice.ts +7 -0
- package/src/types/models/modal.model.ts +3 -0
- package/src/types/types.ts +29 -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
|
|
|
@@ -154,6 +155,19 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
154
155
|
dispatch(setEditAction({ isEdit: false }));
|
|
155
156
|
};
|
|
156
157
|
|
|
158
|
+
// Read the latest client through a ref so this effect's setup phase
|
|
159
|
+
// does NOT re-run on every `client` identity change. With `client` in
|
|
160
|
+
// the deps array, a reconnect or any provider re-render would re-fire
|
|
161
|
+
// setup → `dispatch(setVisibleRoom(...))` ~one tick after the host
|
|
162
|
+
// cleared visibility via the `isVisible` prop (XmppProvider) — racing
|
|
163
|
+
// and clobbering it, so `useUnread()` reported 0 in tab-mounted hosts.
|
|
164
|
+
// Customer-reported #19. The cleanup still needs the live client for
|
|
165
|
+
// flushLastViewedToPrivateStoreStanza, which is what the ref provides.
|
|
166
|
+
const clientRef = useRef(client);
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
clientRef.current = client;
|
|
169
|
+
}, [client]);
|
|
170
|
+
|
|
157
171
|
useEffect(() => {
|
|
158
172
|
if (!activeRoomJID) {
|
|
159
173
|
return;
|
|
@@ -170,8 +184,9 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
170
184
|
}),
|
|
171
185
|
);
|
|
172
186
|
dispatch(clearVisibleRoom());
|
|
173
|
-
|
|
174
|
-
|
|
187
|
+
const liveClient = clientRef.current;
|
|
188
|
+
if (liveClient) {
|
|
189
|
+
liveClient
|
|
175
190
|
.flushLastViewedToPrivateStoreStanza(store.getState().rooms?.rooms, {
|
|
176
191
|
visibleRoomJID: activeRoomJID,
|
|
177
192
|
})
|
|
@@ -180,7 +195,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
180
195
|
dispatch(deleteRoomMessage({ roomJID: activeRoomJID, messageId: 'delimiter-new' }));
|
|
181
196
|
setIsLoadingMore(false);
|
|
182
197
|
};
|
|
183
|
-
}, [activeRoomJID,
|
|
198
|
+
}, [activeRoomJID, dispatch]);
|
|
184
199
|
|
|
185
200
|
// hooks useEffects
|
|
186
201
|
// useRoomUrl(activeRoomJID || "", roomsList, config);
|
|
@@ -213,8 +228,28 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
213
228
|
const inputDockPaddingBottom = getInputDockPaddingBottom({
|
|
214
229
|
platform: Platform.OS,
|
|
215
230
|
bottomInset: insets.bottom,
|
|
231
|
+
keyboardVisible,
|
|
216
232
|
});
|
|
217
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
|
+
|
|
218
253
|
return (
|
|
219
254
|
<KeyboardAvoidingView
|
|
220
255
|
style={{ flex: 1 }}
|
|
@@ -32,6 +32,7 @@ import {useChatSettingState} from '../../hooks/useChatSettingState';
|
|
|
32
32
|
import {DeviceEventEmitter, Pressable, Text, View} from 'react-native';
|
|
33
33
|
import {pushLog as devPushLog} from '../../utils/devLogger';
|
|
34
34
|
import {normalizeRoomJid} from '../../helpers/normalizeRoomJid';
|
|
35
|
+
import {InteractionsOverlayProvider} from '../MessageBubble/InteractionsOverlay';
|
|
35
36
|
|
|
36
37
|
interface ChatWrapperProps {
|
|
37
38
|
token?: string;
|
|
@@ -398,28 +399,34 @@ const ChatWrapper: FC<ChatWrapperProps> = ({
|
|
|
398
399
|
style={{
|
|
399
400
|
...MainComponentStyles,
|
|
400
401
|
}}>
|
|
401
|
-
{
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
}}>
|
|
411
|
-
<ChatRoom
|
|
412
|
-
CustomMessageComponent={CustomMessageComponent || Message}
|
|
413
|
-
handleBackClick={
|
|
414
|
-
roomJID || config?.disableRooms
|
|
415
|
-
? undefined
|
|
416
|
-
: () => {
|
|
417
|
-
dispatch(setCurrentRoom({ roomJID: '' }));
|
|
418
|
-
}
|
|
419
|
-
}
|
|
402
|
+
{/* Host the message context menu in-tree (not a RN Modal) so
|
|
403
|
+
opening it doesn't steal focus / dismiss the keyboard on
|
|
404
|
+
Android. Wraps only the chat area — the global Modal /
|
|
405
|
+
ModalWrapper below stay above it. */}
|
|
406
|
+
<InteractionsOverlayProvider>
|
|
407
|
+
{showRoomList ? (
|
|
408
|
+
<RoomList
|
|
409
|
+
chats={Object.values(rooms)}
|
|
410
|
+
onRoomClick={handleChangeChat}
|
|
420
411
|
/>
|
|
421
|
-
|
|
422
|
-
|
|
412
|
+
) : (
|
|
413
|
+
<ChatWrapperBox
|
|
414
|
+
style={{
|
|
415
|
+
...MainComponentStyles,
|
|
416
|
+
}}>
|
|
417
|
+
<ChatRoom
|
|
418
|
+
CustomMessageComponent={CustomMessageComponent || Message}
|
|
419
|
+
handleBackClick={
|
|
420
|
+
roomJID || config?.disableRooms
|
|
421
|
+
? undefined
|
|
422
|
+
: () => {
|
|
423
|
+
dispatch(setCurrentRoom({ roomJID: '' }));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
/>
|
|
427
|
+
</ChatWrapperBox>
|
|
428
|
+
)}
|
|
429
|
+
</InteractionsOverlayProvider>
|
|
423
430
|
<Modal
|
|
424
431
|
modal={activeModal}
|
|
425
432
|
setOpenModal={(value?: ModalType) =>
|
|
@@ -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 {
|
|
8
|
-
|
|
7
|
+
import {
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
Image,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
Text,
|
|
12
|
+
View,
|
|
13
|
+
} from 'react-native';
|
|
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,16 +187,49 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
47
187
|
/>
|
|
48
188
|
);
|
|
49
189
|
case mimeType.startsWith('audio/'): {
|
|
50
|
-
return
|
|
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
|
-
//
|
|
54
|
-
// application/octet-stream
|
|
55
|
-
//
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
214
|
+
// Some senders (notably the web app's voice-message uploader)
|
|
215
|
+
// ship audio as `application/octet-stream` with no audio file
|
|
216
|
+
// extension. `isLikelyAudio` recognises those via the URL path,
|
|
217
|
+
// filename, or voice-message naming hints (voicemail-,
|
|
218
|
+
// voice-note-, recording-, …) so we still render the AudioMessage
|
|
219
|
+
// player instead of falling through to the generic FileDownload
|
|
220
|
+
// card (where the voicemail rendered as a broken `.bin`
|
|
221
|
+
// attachment). Customer-reported #9 voicemail fix.
|
|
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
|
+
);
|
|
60
233
|
}
|
|
61
234
|
return (
|
|
62
235
|
<FileDownload
|
|
@@ -64,6 +237,9 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
64
237
|
fileName={displayName}
|
|
65
238
|
mimetype={mimeType}
|
|
66
239
|
isUser={isUser}
|
|
240
|
+
originalName={(message as any)?.originalName}
|
|
241
|
+
duration={(message as any)?.duration}
|
|
242
|
+
waveForm={(message as any)?.waveForm}
|
|
67
243
|
/>
|
|
68
244
|
);
|
|
69
245
|
}
|
|
@@ -73,3 +249,120 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
73
249
|
};
|
|
74
250
|
|
|
75
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
|
+
});
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import React, {
|
|
2
|
+
createContext,
|
|
3
|
+
useCallback,
|
|
4
|
+
useContext,
|
|
5
|
+
useMemo,
|
|
6
|
+
useRef,
|
|
7
|
+
useState,
|
|
8
|
+
} from 'react';
|
|
9
|
+
import { StyleSheet, View } from 'react-native';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* In-tree overlay host for the message context menu (MessageInteractions).
|
|
13
|
+
*
|
|
14
|
+
* Why this exists: MessageInteractions used to render inside a React Native
|
|
15
|
+
* `<Modal>`. On Android a Modal opens its own window, which steals focus
|
|
16
|
+
* from the chat input — so opening the menu dismissed the keyboard. Hosting
|
|
17
|
+
* the menu in the SAME view tree (no extra window) keeps the keyboard up.
|
|
18
|
+
*
|
|
19
|
+
* Coordinate note: the menu is positioned from `UIManager.measure` WINDOW
|
|
20
|
+
* coordinates (pageX/pageY), but this host sits below the status bar / any
|
|
21
|
+
* safe-area + header insets, so its own origin is offset from the window.
|
|
22
|
+
* The host measures that offset (`measureInWindow`) and exposes it so the
|
|
23
|
+
* menu can convert window coords → host-local coords. The host is mounted
|
|
24
|
+
* ABOVE ChatRoom's KeyboardAvoidingView, so its origin doesn't move when the
|
|
25
|
+
* keyboard opens (only the measured bubble's pageY does).
|
|
26
|
+
*/
|
|
27
|
+
interface OverlayEntry {
|
|
28
|
+
id: string;
|
|
29
|
+
node: React.ReactNode;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface InteractionsOverlayCtx {
|
|
33
|
+
present: (id: string, node: React.ReactNode) => void;
|
|
34
|
+
dismiss: (id: string) => void;
|
|
35
|
+
originX: number;
|
|
36
|
+
originY: number;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Ctx = createContext<InteractionsOverlayCtx>({
|
|
40
|
+
present: () => {},
|
|
41
|
+
dismiss: () => {},
|
|
42
|
+
originX: 0,
|
|
43
|
+
originY: 0,
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
export const useInteractionsOverlay = () => useContext(Ctx);
|
|
47
|
+
|
|
48
|
+
export const InteractionsOverlayProvider: React.FC<{
|
|
49
|
+
children: React.ReactNode;
|
|
50
|
+
}> = ({ children }) => {
|
|
51
|
+
const [entries, setEntries] = useState<OverlayEntry[]>([]);
|
|
52
|
+
const [origin, setOrigin] = useState({ x: 0, y: 0 });
|
|
53
|
+
const hostRef = useRef<View>(null);
|
|
54
|
+
|
|
55
|
+
const measure = useCallback(() => {
|
|
56
|
+
hostRef.current?.measureInWindow?.((x, y) => {
|
|
57
|
+
if (typeof x !== 'number' || typeof y !== 'number') {return;}
|
|
58
|
+
setOrigin((prev) => (prev.x === x && prev.y === y ? prev : { x, y }));
|
|
59
|
+
});
|
|
60
|
+
}, []);
|
|
61
|
+
|
|
62
|
+
const present = useCallback(
|
|
63
|
+
(id: string, node: React.ReactNode) => {
|
|
64
|
+
// Re-measure on present: the keyboard / layout may have shifted us
|
|
65
|
+
// since the last onLayout.
|
|
66
|
+
measure();
|
|
67
|
+
setEntries((prev) => {
|
|
68
|
+
const next = prev.filter((e) => e.id !== id);
|
|
69
|
+
next.push({ id, node });
|
|
70
|
+
return next;
|
|
71
|
+
});
|
|
72
|
+
},
|
|
73
|
+
[measure]
|
|
74
|
+
);
|
|
75
|
+
|
|
76
|
+
const dismiss = useCallback((id: string) => {
|
|
77
|
+
setEntries((prev) =>
|
|
78
|
+
prev.some((e) => e.id === id) ? prev.filter((e) => e.id !== id) : prev
|
|
79
|
+
);
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
82
|
+
// MUST be memoized. Otherwise every host re-render (e.g. from present()'s
|
|
83
|
+
// setEntries) creates a new context value → all consumers re-render →
|
|
84
|
+
// MessageInteractions' present-effect fires again → setEntries → … →
|
|
85
|
+
// "Maximum update depth exceeded". present/dismiss are stable (useCallback)
|
|
86
|
+
// and origin rarely changes, so this value is stable across entry changes.
|
|
87
|
+
const ctxValue = useMemo(
|
|
88
|
+
() => ({ present, dismiss, originX: origin.x, originY: origin.y }),
|
|
89
|
+
[present, dismiss, origin.x, origin.y]
|
|
90
|
+
);
|
|
91
|
+
|
|
92
|
+
return (
|
|
93
|
+
// `children` is a stable element from the caller, so re-rendering this
|
|
94
|
+
// host (present/dismiss/origin state) does NOT re-render the chat
|
|
95
|
+
// subtree — React reconciliation skips the unchanged children element.
|
|
96
|
+
<View
|
|
97
|
+
ref={hostRef}
|
|
98
|
+
style={styles.host}
|
|
99
|
+
onLayout={measure}
|
|
100
|
+
collapsable={false}
|
|
101
|
+
>
|
|
102
|
+
<Ctx.Provider value={ctxValue}>
|
|
103
|
+
{children}
|
|
104
|
+
{entries.length > 0 && (
|
|
105
|
+
<View style={styles.overlay} pointerEvents="box-none">
|
|
106
|
+
{entries.map((e) => (
|
|
107
|
+
<React.Fragment key={e.id}>{e.node}</React.Fragment>
|
|
108
|
+
))}
|
|
109
|
+
</View>
|
|
110
|
+
)}
|
|
111
|
+
</Ctx.Provider>
|
|
112
|
+
</View>
|
|
113
|
+
);
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const styles = StyleSheet.create({
|
|
117
|
+
host: { flex: 1 },
|
|
118
|
+
overlay: { ...StyleSheet.absoluteFillObject, zIndex: 9999, elevation: 9999 },
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
export default InteractionsOverlayProvider;
|
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
findNodeHandle,
|
|
7
7
|
UIManager,
|
|
8
8
|
Text,
|
|
9
|
+
Keyboard,
|
|
9
10
|
} from 'react-native';
|
|
10
11
|
import { useDispatch, useSelector } from 'react-redux';
|
|
11
12
|
import { RootState } from '../../roomStore';
|
|
@@ -241,8 +242,25 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
241
242
|
// onLayout — more accurate than estimating it here, and it's what keeps
|
|
242
243
|
// the menu adjacent to the message instead of floating far above (#13).
|
|
243
244
|
const handleLongPress = () => {
|
|
244
|
-
const target = bubbleRef.current ?? messageRef.current;
|
|
245
|
-
if (target) {
|
|
245
|
+
const target = (bubbleRef.current ?? messageRef.current) as any;
|
|
246
|
+
if (target?.measureInWindow) {
|
|
247
|
+
// Use measureInWindow — the SAME API the overlay host uses to find
|
|
248
|
+
// its own window origin. Both are then in identical window
|
|
249
|
+
// coordinates, so MessageInteractions' host-local conversion
|
|
250
|
+
// (subtract origin) is exact. Mixing this with UIManager.measure
|
|
251
|
+
// (pageX/pageY) drifted on Android (status-bar offset) and pushed the
|
|
252
|
+
// "below" menu too far from the message.
|
|
253
|
+
target.measureInWindow(
|
|
254
|
+
(x: number, y: number, width: number, height: number) => {
|
|
255
|
+
setContextMenuPosition({
|
|
256
|
+
left: x,
|
|
257
|
+
right: x + width,
|
|
258
|
+
top: y,
|
|
259
|
+
bottom: y + height,
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
);
|
|
263
|
+
} else if (target) {
|
|
246
264
|
const nodeHandle = findNodeHandle(target);
|
|
247
265
|
if (nodeHandle) {
|
|
248
266
|
UIManager.measure(nodeHandle, (_x, _y, width, height, pageX, pageY) => {
|
|
@@ -291,7 +309,19 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
291
309
|
>
|
|
292
310
|
{!isUser && (
|
|
293
311
|
<CustomMessagePhotoContainer
|
|
294
|
-
|
|
312
|
+
// `disableProfilesInteractions` gates every entry point to the
|
|
313
|
+
// user-profile popup: this in-bubble avatar AND the chat-title
|
|
314
|
+
// press in ChatHeader. When disabled we still render the avatar
|
|
315
|
+
// visually but make it non-interactive (no press, no a11y
|
|
316
|
+
// affordance) — consumers who want it gone entirely can return
|
|
317
|
+
// null from a custom message component.
|
|
318
|
+
onPress={
|
|
319
|
+
config?.disableProfilesInteractions
|
|
320
|
+
? undefined
|
|
321
|
+
: () => handleUserAvatarClick(message.user)
|
|
322
|
+
}
|
|
323
|
+
disabled={!!config?.disableProfilesInteractions}
|
|
324
|
+
accessible={!config?.disableProfilesInteractions}
|
|
295
325
|
>
|
|
296
326
|
{message.user?.profileImage ? (
|
|
297
327
|
<CustomMessagePhoto source={{ uri: message.user.profileImage }} />
|
|
@@ -310,6 +340,17 @@ const Message: React.FC<MessageProps> = ({ message, isUser, isReply }) => {
|
|
|
310
340
|
flex: 1,
|
|
311
341
|
alignItems: isUser ? 'flex-end' : 'flex-start',
|
|
312
342
|
}}
|
|
343
|
+
// A plain tap anywhere on a message bubble dismisses the
|
|
344
|
+
// keyboard. The FlatList's keyboardShouldPersistTaps="handled"
|
|
345
|
+
// already dismisses taps on the empty GAPS between bubbles, but
|
|
346
|
+
// the bubble Pressable itself is a touch responder, so taps that
|
|
347
|
+
// landed on a message were "handled" and the keyboard stayed —
|
|
348
|
+
// which is exactly the "closes only every other time" symptom
|
|
349
|
+
// (hit a gap → close, hit a bubble → stay). Explicitly dismissing
|
|
350
|
+
// here makes it reliable across the whole list. onPress does NOT
|
|
351
|
+
// fire when onLongPress fires, so opening MessageInteractions via
|
|
352
|
+
// long-press never triggers this dismiss.
|
|
353
|
+
onPress={() => Keyboard.dismiss()}
|
|
313
354
|
// disableInteractions hides the long-press → context menu
|
|
314
355
|
// (delete / edit / reply / react). Mirrors web's config gate.
|
|
315
356
|
onLongPress={
|