@ethora/chat-component-rn 26.5.10 → 26.5.11
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 +59 -1
- package/README.md +29 -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 +17 -3
- 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 +12 -9
- 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 +64 -36
- 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/FilePreviewModal.js +5 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.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/styled/StyledComponents.js +6 -3
- package/lib/commonjs/components/styled/StyledComponents.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/helpers/mimeToExtension.js +56 -0
- package/lib/commonjs/helpers/mimeToExtension.js.map +1 -1
- package/lib/commonjs/hooks/useLogout.js +33 -8
- package/lib/commonjs/hooks/useLogout.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/module/components/MainComponents/ChatHeader.js +31 -16
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatRoom.js +18 -4
- 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 +13 -10
- 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 +66 -38
- 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/FilePreviewModal.js +6 -2
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.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/styled/StyledComponents.js +6 -3
- package/lib/module/components/styled/StyledComponents.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/helpers/mimeToExtension.js +55 -0
- package/lib/module/helpers/mimeToExtension.js.map +1 -1
- package/lib/module/hooks/useLogout.js +33 -8
- package/lib/module/hooks/useLogout.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/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/FilePreviewModal.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/styled/StyledComponents.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts +8 -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/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +16 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +7 -4
- package/src/components/MainComponents/ChatHeader.tsx +46 -15
- package/src/components/MainComponents/ChatRoom.tsx +17 -3
- package/src/components/MainComponents/ChatWrapper.tsx +28 -21
- package/src/components/MainComponents/MediaMessage.tsx +11 -8
- package/src/components/MessageBubble/InteractionsOverlay.tsx +121 -0
- package/src/components/MessageBubble/Message.tsx +44 -3
- package/src/components/MessageBubble/MessageInteractions.tsx +67 -48
- package/src/components/Modals/AttachSheet/AttachSheet.tsx +144 -51
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +25 -3
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +10 -2
- package/src/components/Modals/ModalHeaderComponent.tsx +8 -1
- package/src/components/Modals/styledModalComponents.tsx +13 -4
- package/src/components/styled/StyledComponents.ts +6 -3
- package/src/components/styled/VideoMessage.tsx +21 -5
- package/src/helpers/mimeToExtension.ts +64 -0
- package/src/hooks/useLogout.tsx +33 -10
- package/src/networking/api-requests/rooms.api.ts +31 -7
- package/src/types/types.ts +16 -0
|
@@ -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={
|
|
@@ -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,7 +8,6 @@ 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,
|
|
@@ -18,6 +17,7 @@ import {
|
|
|
18
17
|
} from 'react-native';
|
|
19
18
|
import * as Clipboard from 'expo-clipboard';
|
|
20
19
|
import { useToast } from '../../context/ToastContext';
|
|
20
|
+
import { useInteractionsOverlay } from './InteractionsOverlay';
|
|
21
21
|
|
|
22
22
|
interface MessageInteractionsProps {
|
|
23
23
|
isReply?: boolean;
|
|
@@ -47,6 +47,8 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
47
47
|
handleEditMessage,
|
|
48
48
|
}) => {
|
|
49
49
|
const { showToast } = useToast();
|
|
50
|
+
const { present, dismiss, originX, originY } = useInteractionsOverlay();
|
|
51
|
+
const overlayId = useId();
|
|
50
52
|
|
|
51
53
|
const config = useSelector(
|
|
52
54
|
(state: RootState) => state.chatSettingStore.config
|
|
@@ -130,54 +132,71 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
130
132
|
return { top, left };
|
|
131
133
|
}, [position, menuSize, config?.keyboardVerticalOffset]);
|
|
132
134
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
{
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
135
|
+
// Window coords (memoPosition) → host-local coords. The overlay host
|
|
136
|
+
// sits below the status bar / header, so subtract its measured origin.
|
|
137
|
+
const localPosition = useMemo(() => {
|
|
138
|
+
if (!memoPosition) {return undefined;}
|
|
139
|
+
return {
|
|
140
|
+
...memoPosition,
|
|
141
|
+
top: (memoPosition as { top: number }).top - originY,
|
|
142
|
+
left: (memoPosition as { left: number }).left - originX,
|
|
143
|
+
};
|
|
144
|
+
}, [memoPosition, originX, originY]);
|
|
145
|
+
|
|
146
|
+
const content =
|
|
147
|
+
config?.disableInteractions || message.isDeleted ? null : (
|
|
148
|
+
<View style={styles.overlayFill}>
|
|
149
|
+
<Pressable style={StyleSheet.absoluteFill} onPress={closeMenu} />
|
|
150
|
+
<View
|
|
151
|
+
style={[styles.contextMenu, localPosition]}
|
|
152
|
+
onLayout={handleMenuLayout}
|
|
153
|
+
>
|
|
154
|
+
<MenuItem onPress={() => handleCopyMessage(message.body!)}>
|
|
155
|
+
<Text style={styles.menuText}>{MESSAGE_INTERACTIONS.COPY}</Text>
|
|
156
|
+
<MESSAGE_INTERACTIONS_ICONS.COPY />
|
|
157
|
+
</MenuItem>
|
|
158
|
+
{isUser && (
|
|
159
|
+
<>
|
|
160
|
+
{/* Edit only for non-media messages (upstream guard) +
|
|
161
|
+
consistent label styling (menuText). */}
|
|
162
|
+
{message?.isMediafile !== 'true' && (
|
|
163
|
+
<>
|
|
164
|
+
<Delimeter />
|
|
165
|
+
<MenuItem onPress={handleEditMessage}>
|
|
166
|
+
<Text style={styles.menuText}>
|
|
167
|
+
{MESSAGE_INTERACTIONS.EDIT}
|
|
168
|
+
</Text>
|
|
169
|
+
<MESSAGE_INTERACTIONS_ICONS.EDIT />
|
|
170
|
+
</MenuItem>
|
|
171
|
+
</>
|
|
172
|
+
)}
|
|
173
|
+
<Delimeter />
|
|
174
|
+
<MenuItem onPress={handleDeleteMessage}>
|
|
175
|
+
<Text style={styles.menuText}>{MESSAGE_INTERACTIONS.DELETE}</Text>
|
|
176
|
+
<MESSAGE_INTERACTIONS_ICONS.DELETE />
|
|
177
|
+
</MenuItem>
|
|
178
|
+
</>
|
|
179
|
+
)}
|
|
176
180
|
</View>
|
|
177
|
-
|
|
181
|
+
</View>
|
|
182
|
+
);
|
|
178
183
|
|
|
179
|
-
|
|
180
|
-
|
|
184
|
+
// Render the menu in the in-tree overlay host instead of a React Native
|
|
185
|
+
// <Modal>. A Modal opens its own window on Android, which steals focus
|
|
186
|
+
// from the chat input and dismisses the keyboard when the menu opens;
|
|
187
|
+
// hosting it in the same view tree keeps the keyboard up. Re-run every
|
|
188
|
+
// render so menuSize/position updates refresh the hosted node.
|
|
189
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
190
|
+
useEffect(() => {
|
|
191
|
+
if (content) {
|
|
192
|
+
present(overlayId, content);
|
|
193
|
+
} else {
|
|
194
|
+
dismiss(overlayId);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
useEffect(() => () => dismiss(overlayId), [dismiss, overlayId]);
|
|
198
|
+
|
|
199
|
+
return null;
|
|
181
200
|
};
|
|
182
201
|
|
|
183
202
|
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
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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={
|
|
79
|
-
|
|
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
|
-
|
|
163
|
+
// Hardware-back / iOS gesture also routes through onClose, which
|
|
164
|
+
// flips `visible` and triggers our exit animation above.
|
|
82
165
|
>
|
|
83
|
-
<
|
|
84
|
-
style={styles.backdrop}
|
|
85
|
-
activeOpacity={1}
|
|
86
|
-
onPress={onClose}
|
|
166
|
+
<Animated.View
|
|
167
|
+
style={[styles.backdrop, { opacity: backdropOpacity }]}
|
|
87
168
|
>
|
|
88
|
-
<TouchableOpacity
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
{
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
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.
|
|
104
|
-
|
|
188
|
+
styles.row,
|
|
189
|
+
idx < rows.length - 1 && styles.rowDivider,
|
|
105
190
|
]}
|
|
191
|
+
onPress={trigger(handler)}
|
|
106
192
|
>
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
</
|
|
123
|
-
</
|
|
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:
|
|
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
|
-
|
|
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}`}
|
|
@@ -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,15 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
240
240
|
);
|
|
241
241
|
case activeFile.mimetype.startsWith('video/'):
|
|
242
242
|
return <ModalVideo uri={activeFile.fileURL} />;
|
|
243
|
-
|
|
243
|
+
// Mirrors the MediaMessage heuristic: treat octet-stream voicemails
|
|
244
|
+
// with audio-shaped filenames / URLs as audio so the preview shows
|
|
245
|
+
// the player instead of an "Unsupported" card. Customer-reported
|
|
246
|
+
// #9 voicemail fix — see isLikelyAudio in mimeToExtension.ts.
|
|
247
|
+
case isLikelyAudio(
|
|
248
|
+
activeFile.mimetype,
|
|
249
|
+
activeFile.fileName,
|
|
250
|
+
activeFile.fileURL
|
|
251
|
+
):
|
|
244
252
|
return (
|
|
245
253
|
<View
|
|
246
254
|
style={{
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
import { BackIcon, MoreIcon, QrIcon } from '../../assets/icons';
|
|
8
8
|
import Button from '../styled/Button';
|
|
9
9
|
import { Text } from 'react-native';
|
|
10
|
+
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
10
11
|
|
|
11
12
|
interface ModalHeaderComponentProps {
|
|
12
13
|
handleCloseModal?: any;
|
|
@@ -21,8 +22,14 @@ const ModalHeaderComponent: React.FC<ModalHeaderComponentProps> = ({
|
|
|
21
22
|
rightMenu,
|
|
22
23
|
leftMenu,
|
|
23
24
|
}) => {
|
|
25
|
+
// The library's full-screen modals are an absolute-fill overlay inside
|
|
26
|
+
// ModalBackground (NOT a native Modal), so the OS doesn't apply any
|
|
27
|
+
// status-bar inset automatically — we have to. SafeAreaProvider is
|
|
28
|
+
// mounted by ReduxWrapper, so this reads the live per-device inset
|
|
29
|
+
// (notch / Dynamic Island / nothing).
|
|
30
|
+
const insets = useSafeAreaInsets();
|
|
24
31
|
return (
|
|
25
|
-
<HeaderContainer>
|
|
32
|
+
<HeaderContainer style={{ paddingTop: insets.top + 8 }}>
|
|
26
33
|
<HeaderLeft>
|
|
27
34
|
{leftMenu ? (
|
|
28
35
|
leftMenu
|
|
@@ -72,10 +72,19 @@ export const HeaderContainer = styled.View`
|
|
|
72
72
|
position: relative;
|
|
73
73
|
top: 0;
|
|
74
74
|
width: 100%;
|
|
75
|
-
padding:
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
/* The previous incarnation had padding-top:62px (hard-coded status-bar
|
|
76
|
+
* offset) clamped to min/max-height:24px — content overflowed the
|
|
77
|
+
* visible band and the back arrow hugged the bottom edge. The earlier
|
|
78
|
+
* fix dropped the clamp but kept a hard-coded 56px top inset, which on
|
|
79
|
+
* top of this modal's absolute-fill overlay (no system inset is auto-
|
|
80
|
+
* applied) made the header eat far more space than it should.
|
|
81
|
+
*
|
|
82
|
+
* Now: compact 8px vertical padding here; the dynamic top inset for
|
|
83
|
+
* the status bar / Dynamic Island is supplied by ModalHeaderComponent
|
|
84
|
+
* via useSafeAreaInsets() — accurate per-device (~59px on a 16 Pro,
|
|
85
|
+
* 20px on an SE). Together that's status-bar + 8px above the back
|
|
86
|
+
* button and 8px below — properly sized and properly centered. */
|
|
87
|
+
padding: 8px 16px;
|
|
79
88
|
background-color: #fff;
|
|
80
89
|
border-bottom-width: 1px;
|
|
81
90
|
border-bottom-color: #f0f0f0;
|
|
@@ -54,11 +54,14 @@ export const ChatContainerHeaderBoxInfo = styled.TouchableOpacity`
|
|
|
54
54
|
flex: 1;
|
|
55
55
|
flex-direction: row;
|
|
56
56
|
align-items: center;
|
|
57
|
-
|
|
57
|
+
/* Left-aligned: the avatar + chat-name row now hugs the left edge of
|
|
58
|
+
* the header instead of centering itself within the available space.
|
|
59
|
+
* Was: justify-content:center + max-width:90% which double-centered
|
|
60
|
+
* with CenterContainer and pushed the title to the middle. */
|
|
61
|
+
justify-content: flex-start;
|
|
62
|
+
width: 100%;
|
|
58
63
|
overflow: hidden;
|
|
59
|
-
max-width: 90%;
|
|
60
64
|
gap: 16px;
|
|
61
|
-
flex: 1;
|
|
62
65
|
`;
|
|
63
66
|
|
|
64
67
|
export const ChatContainerHeaderInfo = styled.View`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import {
|
|
2
|
+
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
3
|
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
4
4
|
import { useDispatch } from 'react-redux';
|
|
5
5
|
import {
|
|
@@ -48,9 +48,16 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
48
48
|
};
|
|
49
49
|
|
|
50
50
|
return (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
// Capture-overlay pattern: VideoView sits underneath without ANY
|
|
52
|
+
// touch participation; a transparent absolute-fill <Pressable> on
|
|
53
|
+
// top owns the tap. Previously we wrapped VideoView in a
|
|
54
|
+
// TouchableOpacity and set `pointerEvents="none"` on the inner
|
|
55
|
+
// VideoView — but on some expo-video versions / iOS the native
|
|
56
|
+
// VideoView still intercepted gestures despite that hint, so
|
|
57
|
+
// tapping the poster did nothing. Customer-reported #9 (video
|
|
58
|
+
// preview unresponsive). The Pressable now guarantees the tap
|
|
59
|
+
// reaches `handleOpen`.
|
|
60
|
+
<View
|
|
54
61
|
style={[styles.wrapper, { width: dims.width, height: dims.height }]}
|
|
55
62
|
>
|
|
56
63
|
<VideoView
|
|
@@ -66,7 +73,16 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
66
73
|
<PlayIcon width={28} height={28} />
|
|
67
74
|
</View>
|
|
68
75
|
</View>
|
|
69
|
-
|
|
76
|
+
<Pressable
|
|
77
|
+
onPress={handleOpen}
|
|
78
|
+
// Sits ON TOP of VideoView + the play-button overlay; transparent
|
|
79
|
+
// so the poster + play icon show through. Captures the tap and
|
|
80
|
+
// routes to handleOpen → setActiveModal(FILE_PREVIEW).
|
|
81
|
+
style={StyleSheet.absoluteFill}
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
accessibilityLabel="Play video"
|
|
84
|
+
/>
|
|
85
|
+
</View>
|
|
70
86
|
);
|
|
71
87
|
};
|
|
72
88
|
|