@ethora/chat-component-rn 26.5.11 → 26.5.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +6 -0
- package/README.md +1 -0
- package/lib/commonjs/assets/icons.js +7 -1
- package/lib/commonjs/assets/icons.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatRoom.js +58 -22
- package/lib/commonjs/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +288 -5
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/commonjs/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +23 -3
- package/lib/commonjs/components/MessageBubble/MessageInteractions.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 +36 -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/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/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/context/xmppProvider.js +68 -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 +70 -3
- 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/useSendMessage.js +6 -3
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/xmppClient.js +26 -1
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/commonjs/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +65 -4
- 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/ChatRoom.js +59 -23
- package/lib/module/components/MainComponents/ChatRoom.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +290 -7
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/ReduxWrapper.js +24 -15
- package/lib/module/components/MainComponents/ReduxWrapper.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +24 -4
- package/lib/module/components/MessageBubble/MessageInteractions.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 +36 -3
- 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/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/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/context/xmppProvider.js +70 -4
- 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 +67 -3
- 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/useSendMessage.js +6 -3
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/xmppClient.js +26 -1
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/Middleware/unreadMidlleware.js +10 -10
- package/lib/module/roomStore/Middleware/unreadMidlleware.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +63 -3
- package/lib/module/roomStore/roomsSlice.js.map +1 -1
- package/lib/typescript/assets/icons.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ReduxWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/MessageInteractions.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/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/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/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 +31 -1
- package/lib/typescript/helpers/mimeToExtension.d.ts.map +1 -1
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/Middleware/unreadMidlleware.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.d.ts +18 -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 +15 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +5 -1
- package/src/assets/icons.tsx +30 -25
- package/src/components/MainComponents/ChatRoom.tsx +61 -23
- package/src/components/MainComponents/MediaMessage.tsx +295 -5
- package/src/components/MainComponents/ReduxWrapper.tsx +23 -9
- package/src/components/MessageBubble/MessageInteractions.tsx +35 -3
- package/src/components/Modals/FilePreviewModal/DocumentViewer.tsx +3 -40
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +39 -3
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +142 -0
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -18
- 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/StyledInputComponents/MediaComponents.tsx +29 -18
- package/src/components/styled/UnsupportedType.tsx +60 -6
- package/src/context/xmppProvider.tsx +61 -3
- package/src/helpers/keyboardLayout.ts +5 -1
- package/src/helpers/mimeToExtension.ts +116 -2
- package/src/hooks/useEventHandlers.tsx +1 -1
- package/src/hooks/useSendMessage.tsx +12 -6
- package/src/networking/xmppClient.ts +24 -1
- package/src/roomStore/Middleware/unreadMidlleware.tsx +11 -8
- package/src/roomStore/roomsSlice.ts +66 -2
- package/src/types/models/modal.model.ts +3 -0
- package/src/types/types.ts +32 -0
|
@@ -32,11 +32,13 @@ import { FlatList } from 'react-native';
|
|
|
32
32
|
import {
|
|
33
33
|
Platform,
|
|
34
34
|
TouchableWithoutFeedback,
|
|
35
|
-
Keyboard,
|
|
36
35
|
View,
|
|
37
36
|
KeyboardEvent,
|
|
38
37
|
} from 'react-native';
|
|
39
|
-
import {
|
|
38
|
+
import {
|
|
39
|
+
KeyboardAvoidingView,
|
|
40
|
+
KeyboardStickyView,
|
|
41
|
+
} from 'react-native-keyboard-controller';
|
|
40
42
|
import useComposing from '../../hooks/useComposing';
|
|
41
43
|
import { store } from '../../roomStore';
|
|
42
44
|
import { useSafeAreaInsets } from 'react-native-safe-area-context';
|
|
@@ -229,25 +231,61 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
229
231
|
bottomInset: insets.bottom,
|
|
230
232
|
});
|
|
231
233
|
|
|
234
|
+
// Keyboard avoidance is delegated to react-native-keyboard-controller's
|
|
235
|
+
// KeyboardAvoidingView. When the HOST app supplies its own keyboard
|
|
236
|
+
// handling (its own KeyboardProvider + KeyboardAvoidingView around
|
|
237
|
+
// <Chat>), this built-in one becomes a SECOND avoider wrapping the same
|
|
238
|
+
// tree; two of them both animating padding on keyboard open is the
|
|
239
|
+
// Android flicker reported in bug #6. `disableKeyboardAvoidingView`
|
|
240
|
+
// swaps this wrapper for a plain View so the host owns it outright
|
|
241
|
+
// (ReduxWrapper drops the built-in KeyboardProvider under the same flag).
|
|
242
|
+
//
|
|
243
|
+
// Bug #6 history (built-in path):
|
|
244
|
+
// - Original: behavior="height" on Android — flicker, because
|
|
245
|
+
// Android's adjustResize already shrinks the window; KAV resizing
|
|
246
|
+
// on top double-resized every keyboard open.
|
|
247
|
+
// - 26.5.6: behavior={undefined} on Android — input got completely
|
|
248
|
+
// blocked when a host disables adjustResize via softInputMode.
|
|
249
|
+
// - 26.5.8: behavior="padding" on BOTH platforms — input lifted above
|
|
250
|
+
// the keyboard regardless of the host's softInputMode.
|
|
251
|
+
// Three keyboard strategies (default = avoidingView, behaviour unchanged):
|
|
252
|
+
// • none ('disableKeyboardAvoidingView'): plain View — the host owns
|
|
253
|
+
// the keyboard entirely (ReduxWrapper also drops the KeyboardProvider).
|
|
254
|
+
// • sticky('keyboardStickyInput'): plain View outer + ONLY the input
|
|
255
|
+
// dock wrapped in <KeyboardStickyView>, so just the input tracks the
|
|
256
|
+
// keyboard and the message list is never resized/reflowed — avoids the
|
|
257
|
+
// Android "messages jump/flash" the padding KAV causes. Best for
|
|
258
|
+
// edge-to-edge hosts (the OS doesn't also resize the window).
|
|
259
|
+
// • avoidingView (default): keyboard-controller KAV, behavior="padding".
|
|
260
|
+
const noKeyboardHandling =
|
|
261
|
+
!!configWithEventHandlers?.disableKeyboardAvoidingView;
|
|
262
|
+
const stickyInput =
|
|
263
|
+
!noKeyboardHandling && !!configWithEventHandlers?.keyboardStickyInput;
|
|
264
|
+
const avoidKeyboard = !noKeyboardHandling && !stickyInput;
|
|
265
|
+
const KeyboardWrapper: React.ComponentType<any> = avoidKeyboard
|
|
266
|
+
? KeyboardAvoidingView
|
|
267
|
+
: View;
|
|
268
|
+
const keyboardWrapperProps = avoidKeyboard
|
|
269
|
+
? {
|
|
270
|
+
style: { flex: 1 },
|
|
271
|
+
behavior: 'padding' as const,
|
|
272
|
+
keyboardVerticalOffset,
|
|
273
|
+
}
|
|
274
|
+
: { style: { flex: 1 } };
|
|
275
|
+
// Input dock: a plain View normally; under the sticky strategy it becomes
|
|
276
|
+
// a KeyboardStickyView so it (and only it) lifts with the keyboard.
|
|
277
|
+
const InputDockTag: React.ComponentType<any> = stickyInput
|
|
278
|
+
? KeyboardStickyView
|
|
279
|
+
: View;
|
|
280
|
+
const inputDockProps: any = {
|
|
281
|
+
style: { paddingBottom: inputDockPaddingBottom, backgroundColor: '#fff' },
|
|
282
|
+
...(stickyInput
|
|
283
|
+
? { offset: { closed: 0, opened: keyboardVerticalOffset } }
|
|
284
|
+
: {}),
|
|
285
|
+
};
|
|
286
|
+
|
|
232
287
|
return (
|
|
233
|
-
<
|
|
234
|
-
style={{ flex: 1 }}
|
|
235
|
-
// Bug #6 history:
|
|
236
|
-
// - Original code: behavior="height" on Android — caused
|
|
237
|
-
// flicker because Android's adjustResize already shrinks
|
|
238
|
-
// the window; KAV resizing on top of that double-resized
|
|
239
|
-
// every keyboard open.
|
|
240
|
-
// - 26.5.6 attempt: behavior={undefined} on Android — input
|
|
241
|
-
// got completely blocked because some host apps disable
|
|
242
|
-
// adjustResize via the activity's softInputMode.
|
|
243
|
-
// - 26.5.8 fix: behavior="padding" on BOTH platforms. Padding
|
|
244
|
-
// adds bottom-padding equal to the keyboard height without
|
|
245
|
-
// changing the layout's height prop — no double-resize on
|
|
246
|
-
// Android, no flicker, and the input is always lifted above
|
|
247
|
-
// the keyboard regardless of the host's softInputMode.
|
|
248
|
-
behavior="padding"
|
|
249
|
-
keyboardVerticalOffset={keyboardVerticalOffset}
|
|
250
|
-
>
|
|
288
|
+
<KeyboardWrapper {...keyboardWrapperProps}>
|
|
251
289
|
<ChatContainer
|
|
252
290
|
style={
|
|
253
291
|
configWithEventHandlers?.chatRoomStyles
|
|
@@ -369,7 +407,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
369
407
|
{editAction && editAction.isEdit && (
|
|
370
408
|
<EditWrapper text={editAction.text || ''} onClose={onCloseEdit} />
|
|
371
409
|
)}
|
|
372
|
-
<
|
|
410
|
+
<InputDockTag {...inputDockProps}>
|
|
373
411
|
{CustomInputComponent ? (
|
|
374
412
|
<CustomInputComponent
|
|
375
413
|
sendMessage={
|
|
@@ -401,7 +439,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
401
439
|
)}
|
|
402
440
|
/>
|
|
403
441
|
)}
|
|
404
|
-
</
|
|
442
|
+
</InputDockTag>
|
|
405
443
|
|
|
406
444
|
{configWithEventHandlers?.customTypingIndicator?.enabled &&
|
|
407
445
|
(configWithEventHandlers.customTypingIndicator.position ===
|
|
@@ -425,7 +463,7 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
425
463
|
/>
|
|
426
464
|
)}
|
|
427
465
|
</ChatContainer>
|
|
428
|
-
</
|
|
466
|
+
</KeyboardWrapper>
|
|
429
467
|
);
|
|
430
468
|
},
|
|
431
469
|
);
|
|
@@ -4,8 +4,17 @@ import FileDownload from '../styled/UnsupportedType';
|
|
|
4
4
|
import CustomMessageImage from '../styled/MessageImage';
|
|
5
5
|
import CustomMessageVideo from '../styled/VideoMessage';
|
|
6
6
|
import AudioMessage from '../styled/AudioMessage';
|
|
7
|
-
import {
|
|
7
|
+
import {
|
|
8
|
+
ActivityIndicator,
|
|
9
|
+
Image,
|
|
10
|
+
StyleSheet,
|
|
11
|
+
Text,
|
|
12
|
+
View,
|
|
13
|
+
} from 'react-native';
|
|
8
14
|
import { deriveDisplayFilename, isLikelyAudio } from '../../helpers/mimeToExtension';
|
|
15
|
+
import { FileIcon, PlayIcon } from '../../assets/icons';
|
|
16
|
+
import { defaultMediaDims } from '../../helpers/mediaDimensions';
|
|
17
|
+
import { useChatSettingState } from '../../hooks/useChatSettingState';
|
|
9
18
|
|
|
10
19
|
interface MediaMessageProps {
|
|
11
20
|
mimeType?: string;
|
|
@@ -15,6 +24,116 @@ interface MediaMessageProps {
|
|
|
15
24
|
isUser: boolean;
|
|
16
25
|
}
|
|
17
26
|
|
|
27
|
+
const pendingImageDims = defaultMediaDims();
|
|
28
|
+
|
|
29
|
+
const getPendingStatusLabel = (mimeType?: string) => {
|
|
30
|
+
if (mimeType?.startsWith('image/')) {
|
|
31
|
+
return 'Uploading image';
|
|
32
|
+
}
|
|
33
|
+
if (mimeType?.startsWith('video/')) {
|
|
34
|
+
return 'Uploading video';
|
|
35
|
+
}
|
|
36
|
+
if (
|
|
37
|
+
mimeType?.startsWith('audio/') ||
|
|
38
|
+
mimeType?.includes('application/octet-stream')
|
|
39
|
+
) {
|
|
40
|
+
return 'Uploading audio';
|
|
41
|
+
}
|
|
42
|
+
return 'Uploading file';
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const PendingMediaMessage: React.FC<{
|
|
46
|
+
fileName: string;
|
|
47
|
+
mimeType?: string;
|
|
48
|
+
previewUri?: string;
|
|
49
|
+
isUser: boolean;
|
|
50
|
+
size?: string;
|
|
51
|
+
}> = ({ fileName, mimeType, previewUri, isUser, size }) => {
|
|
52
|
+
const { config } = useChatSettingState();
|
|
53
|
+
const statusLabel = getPendingStatusLabel(mimeType);
|
|
54
|
+
const isImage = mimeType?.startsWith('image/');
|
|
55
|
+
const isVideo = mimeType?.startsWith('video/');
|
|
56
|
+
const isAudio =
|
|
57
|
+
mimeType?.startsWith('audio/') ||
|
|
58
|
+
mimeType?.includes('application/octet-stream');
|
|
59
|
+
const primaryColor = config?.colors?.primary || '#0A84FF';
|
|
60
|
+
|
|
61
|
+
if (isImage && previewUri) {
|
|
62
|
+
return (
|
|
63
|
+
<View style={styles.pendingImageWrapper}>
|
|
64
|
+
<Image
|
|
65
|
+
source={{ uri: previewUri }}
|
|
66
|
+
style={styles.pendingImage}
|
|
67
|
+
resizeMode="cover"
|
|
68
|
+
/>
|
|
69
|
+
<View style={styles.pendingImageOverlay}>
|
|
70
|
+
<ActivityIndicator size="small" color="#FFFFFF" />
|
|
71
|
+
<Text style={styles.pendingOverlayText}>{statusLabel}</Text>
|
|
72
|
+
</View>
|
|
73
|
+
</View>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
if (isVideo) {
|
|
78
|
+
return (
|
|
79
|
+
<View style={styles.pendingVideoCard}>
|
|
80
|
+
<View style={styles.pendingVideoPreview}>
|
|
81
|
+
<View style={styles.pendingVideoIcon}>
|
|
82
|
+
<PlayIcon width={24} height={24} />
|
|
83
|
+
</View>
|
|
84
|
+
<ActivityIndicator size="small" color="#5B6B8C" />
|
|
85
|
+
</View>
|
|
86
|
+
<Text style={styles.pendingVideoLabel}>{fileName}</Text>
|
|
87
|
+
<Text style={styles.pendingVideoStatus}>{statusLabel}</Text>
|
|
88
|
+
</View>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (isAudio) {
|
|
93
|
+
return (
|
|
94
|
+
<View style={styles.pendingAudioCard}>
|
|
95
|
+
<View
|
|
96
|
+
style={[
|
|
97
|
+
styles.pendingAudioButton,
|
|
98
|
+
{ backgroundColor: primaryColor },
|
|
99
|
+
]}
|
|
100
|
+
>
|
|
101
|
+
<PlayIcon width={18} height={18} color="#fff" />
|
|
102
|
+
</View>
|
|
103
|
+
<View style={styles.pendingAudioContent}>
|
|
104
|
+
<View style={styles.pendingAudioTrack}>
|
|
105
|
+
<View
|
|
106
|
+
style={[
|
|
107
|
+
styles.pendingAudioTrackFill,
|
|
108
|
+
{ backgroundColor: primaryColor },
|
|
109
|
+
]}
|
|
110
|
+
/>
|
|
111
|
+
</View>
|
|
112
|
+
<View style={styles.pendingAudioMeta}>
|
|
113
|
+
<Text style={styles.pendingAudioTime}>0:00</Text>
|
|
114
|
+
<View style={styles.pendingAudioStatusPill}>
|
|
115
|
+
<ActivityIndicator size="small" color="#5B6B8C" />
|
|
116
|
+
<Text style={styles.pendingAudioStatusText}>{statusLabel}</Text>
|
|
117
|
+
</View>
|
|
118
|
+
</View>
|
|
119
|
+
</View>
|
|
120
|
+
</View>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<FileDownload
|
|
126
|
+
fileURL={previewUri || ''}
|
|
127
|
+
fileName={fileName}
|
|
128
|
+
mimetype={mimeType || 'application/octet-stream'}
|
|
129
|
+
size={size}
|
|
130
|
+
isUser={isUser}
|
|
131
|
+
pending
|
|
132
|
+
placeholderIcon={<FileIcon width={36} height={36} />}
|
|
133
|
+
/>
|
|
134
|
+
);
|
|
135
|
+
};
|
|
136
|
+
|
|
18
137
|
const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
19
138
|
mimeType,
|
|
20
139
|
location,
|
|
@@ -29,12 +148,33 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
29
148
|
url: location || messageText,
|
|
30
149
|
mime: mimeType,
|
|
31
150
|
});
|
|
151
|
+
const isAudioPayload = isLikelyAudio(
|
|
152
|
+
mimeType,
|
|
153
|
+
displayName,
|
|
154
|
+
location,
|
|
155
|
+
{
|
|
156
|
+
duration: (message as any)?.duration,
|
|
157
|
+
waveForm: (message as any)?.waveForm,
|
|
158
|
+
originalName: (message as any)?.originalName,
|
|
159
|
+
}
|
|
160
|
+
);
|
|
161
|
+
if (message?.pending) {
|
|
162
|
+
return (
|
|
163
|
+
<PendingMediaMessage
|
|
164
|
+
fileName={displayName}
|
|
165
|
+
mimeType={mimeType}
|
|
166
|
+
previewUri={messageText || location || ''}
|
|
167
|
+
isUser={isUser}
|
|
168
|
+
size={message?.size}
|
|
169
|
+
/>
|
|
170
|
+
);
|
|
171
|
+
}
|
|
32
172
|
switch (true) {
|
|
33
173
|
case mimeType.startsWith('image/'):
|
|
34
174
|
return (
|
|
35
175
|
<CustomMessageImage
|
|
36
176
|
fileName={displayName}
|
|
37
|
-
fileURL={messageText || ''}
|
|
177
|
+
fileURL={location || messageText || ''}
|
|
38
178
|
mimetype={mimeType}
|
|
39
179
|
/>
|
|
40
180
|
);
|
|
@@ -47,7 +187,28 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
47
187
|
/>
|
|
48
188
|
);
|
|
49
189
|
case mimeType.startsWith('audio/'): {
|
|
50
|
-
return
|
|
190
|
+
return (
|
|
191
|
+
<AudioMessage
|
|
192
|
+
src={location || ''}
|
|
193
|
+
mimeType={mimeType}
|
|
194
|
+
fileName={displayName}
|
|
195
|
+
originalName={(message as any)?.originalName}
|
|
196
|
+
duration={(message as any)?.duration}
|
|
197
|
+
waveForm={(message as any)?.waveForm}
|
|
198
|
+
/>
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
case mimeType.includes('application/octet-stream'): {
|
|
202
|
+
return (
|
|
203
|
+
<AudioMessage
|
|
204
|
+
src={location || ''}
|
|
205
|
+
mimeType={mimeType}
|
|
206
|
+
fileName={displayName}
|
|
207
|
+
originalName={(message as any)?.originalName}
|
|
208
|
+
duration={(message as any)?.duration}
|
|
209
|
+
waveForm={(message as any)?.waveForm}
|
|
210
|
+
/>
|
|
211
|
+
);
|
|
51
212
|
}
|
|
52
213
|
default: {
|
|
53
214
|
// Some senders (notably the web app's voice-message uploader)
|
|
@@ -58,8 +219,17 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
58
219
|
// player instead of falling through to the generic FileDownload
|
|
59
220
|
// card (where the voicemail rendered as a broken `.bin`
|
|
60
221
|
// attachment). Customer-reported #9 voicemail fix.
|
|
61
|
-
if (
|
|
62
|
-
return
|
|
222
|
+
if (isAudioPayload) {
|
|
223
|
+
return (
|
|
224
|
+
<AudioMessage
|
|
225
|
+
src={location || ''}
|
|
226
|
+
mimeType={mimeType}
|
|
227
|
+
fileName={displayName}
|
|
228
|
+
originalName={(message as any)?.originalName}
|
|
229
|
+
duration={(message as any)?.duration}
|
|
230
|
+
waveForm={(message as any)?.waveForm}
|
|
231
|
+
/>
|
|
232
|
+
);
|
|
63
233
|
}
|
|
64
234
|
return (
|
|
65
235
|
<FileDownload
|
|
@@ -67,6 +237,9 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
67
237
|
fileName={displayName}
|
|
68
238
|
mimetype={mimeType}
|
|
69
239
|
isUser={isUser}
|
|
240
|
+
originalName={(message as any)?.originalName}
|
|
241
|
+
duration={(message as any)?.duration}
|
|
242
|
+
waveForm={(message as any)?.waveForm}
|
|
70
243
|
/>
|
|
71
244
|
);
|
|
72
245
|
}
|
|
@@ -76,3 +249,120 @@ const MediaMessage: React.FC<MediaMessageProps> = ({
|
|
|
76
249
|
};
|
|
77
250
|
|
|
78
251
|
export default MediaMessage;
|
|
252
|
+
|
|
253
|
+
const styles = StyleSheet.create({
|
|
254
|
+
pendingImageWrapper: {
|
|
255
|
+
width: pendingImageDims.width,
|
|
256
|
+
height: pendingImageDims.height,
|
|
257
|
+
borderRadius: 12,
|
|
258
|
+
overflow: 'hidden',
|
|
259
|
+
backgroundColor: '#EAF0FB',
|
|
260
|
+
},
|
|
261
|
+
pendingImage: {
|
|
262
|
+
width: '100%',
|
|
263
|
+
height: '100%',
|
|
264
|
+
},
|
|
265
|
+
pendingImageOverlay: {
|
|
266
|
+
...StyleSheet.absoluteFillObject,
|
|
267
|
+
alignItems: 'center',
|
|
268
|
+
justifyContent: 'center',
|
|
269
|
+
gap: 8,
|
|
270
|
+
backgroundColor: 'rgba(17, 24, 39, 0.28)',
|
|
271
|
+
},
|
|
272
|
+
pendingOverlayText: {
|
|
273
|
+
color: '#FFFFFF',
|
|
274
|
+
fontSize: 13,
|
|
275
|
+
fontWeight: '600',
|
|
276
|
+
},
|
|
277
|
+
pendingVideoCard: {
|
|
278
|
+
width: 220,
|
|
279
|
+
padding: 12,
|
|
280
|
+
borderRadius: 12,
|
|
281
|
+
backgroundColor: '#F3F6FC',
|
|
282
|
+
gap: 10,
|
|
283
|
+
},
|
|
284
|
+
pendingVideoPreview: {
|
|
285
|
+
height: 144,
|
|
286
|
+
borderRadius: 10,
|
|
287
|
+
backgroundColor: '#DCE6F7',
|
|
288
|
+
alignItems: 'center',
|
|
289
|
+
justifyContent: 'center',
|
|
290
|
+
gap: 10,
|
|
291
|
+
},
|
|
292
|
+
pendingVideoIcon: {
|
|
293
|
+
width: 48,
|
|
294
|
+
height: 48,
|
|
295
|
+
borderRadius: 24,
|
|
296
|
+
backgroundColor: 'rgba(17, 24, 39, 0.45)',
|
|
297
|
+
alignItems: 'center',
|
|
298
|
+
justifyContent: 'center',
|
|
299
|
+
},
|
|
300
|
+
pendingVideoLabel: {
|
|
301
|
+
color: '#141414',
|
|
302
|
+
fontSize: 14,
|
|
303
|
+
fontWeight: '600',
|
|
304
|
+
},
|
|
305
|
+
pendingVideoStatus: {
|
|
306
|
+
color: '#5B6B8C',
|
|
307
|
+
fontSize: 12,
|
|
308
|
+
fontWeight: '500',
|
|
309
|
+
},
|
|
310
|
+
pendingAudioCard: {
|
|
311
|
+
minWidth: 252,
|
|
312
|
+
maxWidth: 288,
|
|
313
|
+
flexDirection: 'row',
|
|
314
|
+
alignItems: 'center',
|
|
315
|
+
gap: 12,
|
|
316
|
+
paddingHorizontal: 12,
|
|
317
|
+
paddingVertical: 10,
|
|
318
|
+
borderRadius: 12,
|
|
319
|
+
backgroundColor: '#F3F6FC',
|
|
320
|
+
},
|
|
321
|
+
pendingAudioButton: {
|
|
322
|
+
width: 44,
|
|
323
|
+
height: 44,
|
|
324
|
+
borderRadius: 22,
|
|
325
|
+
alignItems: 'center',
|
|
326
|
+
justifyContent: 'center',
|
|
327
|
+
},
|
|
328
|
+
pendingAudioContent: {
|
|
329
|
+
flex: 1,
|
|
330
|
+
gap: 8,
|
|
331
|
+
},
|
|
332
|
+
pendingAudioTrack: {
|
|
333
|
+
height: 6,
|
|
334
|
+
borderRadius: 999,
|
|
335
|
+
overflow: 'hidden',
|
|
336
|
+
backgroundColor: '#D0D7E6',
|
|
337
|
+
},
|
|
338
|
+
pendingAudioTrackFill: {
|
|
339
|
+
width: '32%',
|
|
340
|
+
height: '100%',
|
|
341
|
+
borderRadius: 999,
|
|
342
|
+
},
|
|
343
|
+
pendingAudioMeta: {
|
|
344
|
+
flexDirection: 'row',
|
|
345
|
+
alignItems: 'center',
|
|
346
|
+
justifyContent: 'space-between',
|
|
347
|
+
gap: 12,
|
|
348
|
+
},
|
|
349
|
+
pendingAudioTime: {
|
|
350
|
+
color: '#667085',
|
|
351
|
+
fontSize: 12,
|
|
352
|
+
fontWeight: '500',
|
|
353
|
+
},
|
|
354
|
+
pendingAudioStatusPill: {
|
|
355
|
+
flexDirection: 'row',
|
|
356
|
+
alignItems: 'center',
|
|
357
|
+
gap: 6,
|
|
358
|
+
paddingHorizontal: 10,
|
|
359
|
+
paddingVertical: 5,
|
|
360
|
+
borderRadius: 999,
|
|
361
|
+
backgroundColor: '#FFFFFF',
|
|
362
|
+
},
|
|
363
|
+
pendingAudioStatusText: {
|
|
364
|
+
color: '#5B6B8C',
|
|
365
|
+
fontSize: 12,
|
|
366
|
+
fontWeight: '600',
|
|
367
|
+
},
|
|
368
|
+
});
|
|
@@ -33,18 +33,32 @@ export const ReduxWrapper: React.FC<ChatWrapperProps> = React.memo(
|
|
|
33
33
|
return props.config;
|
|
34
34
|
}, [props.config]);
|
|
35
35
|
|
|
36
|
+
// Host apps that own their keyboard handling (their own
|
|
37
|
+
// KeyboardProvider + KeyboardAvoidingView around <Chat>) set
|
|
38
|
+
// `disableKeyboardAvoidingView` — drop the built-in KeyboardProvider
|
|
39
|
+
// here too so there aren't two nested providers (the second is part of
|
|
40
|
+
// the Android keyboard flicker in bug #6; ChatRoom drops the matching
|
|
41
|
+
// KeyboardAvoidingView under the same flag).
|
|
42
|
+
const ownKeyboardHandling = !memoizedConfig?.disableKeyboardAvoidingView;
|
|
43
|
+
|
|
44
|
+
const tree = (
|
|
45
|
+
<XmppProvider config={memoizedConfig} isVisible={props.isVisible}>
|
|
46
|
+
<ToastProvider>
|
|
47
|
+
<MessageNotificationProvider config={memoizedConfig}>
|
|
48
|
+
<LoginWrapper config={memoizedConfig} {...props} />
|
|
49
|
+
</MessageNotificationProvider>
|
|
50
|
+
</ToastProvider>
|
|
51
|
+
</XmppProvider>
|
|
52
|
+
);
|
|
53
|
+
|
|
36
54
|
return (
|
|
37
55
|
<Provider store={store}>
|
|
38
56
|
<SafeAreaProvider>
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
</MessageNotificationProvider>
|
|
45
|
-
</ToastProvider>
|
|
46
|
-
</XmppProvider>
|
|
47
|
-
</KeyboardProvider>
|
|
57
|
+
{ownKeyboardHandling ? (
|
|
58
|
+
<KeyboardProvider>{tree}</KeyboardProvider>
|
|
59
|
+
) : (
|
|
60
|
+
tree
|
|
61
|
+
)}
|
|
48
62
|
</SafeAreaProvider>
|
|
49
63
|
</Provider>
|
|
50
64
|
);
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
View,
|
|
14
14
|
Pressable,
|
|
15
15
|
Dimensions,
|
|
16
|
+
Keyboard,
|
|
16
17
|
type LayoutChangeEvent,
|
|
17
18
|
} from 'react-native';
|
|
18
19
|
import * as Clipboard from 'expo-clipboard';
|
|
@@ -56,6 +57,28 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
56
57
|
|
|
57
58
|
const [menuSize, setMenuSize] = useState({ width: 0, height: 0 });
|
|
58
59
|
|
|
60
|
+
// Live keyboard height so the menu knows the REAL space below the message.
|
|
61
|
+
// Without this the position math used the full screen height and happily
|
|
62
|
+
// opened the menu downward into the area the keyboard now covers, hiding
|
|
63
|
+
// it under the keyboard / behind the input. Seeded from current metrics so
|
|
64
|
+
// an already-open keyboard (the common case: menu opened while typing) is
|
|
65
|
+
// accounted for on first render, not only after the next show event.
|
|
66
|
+
const [keyboardHeight, setKeyboardHeight] = useState(
|
|
67
|
+
() => Keyboard.metrics?.()?.height ?? 0
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
useEffect(() => {
|
|
71
|
+
const onShow = (e: any) =>
|
|
72
|
+
setKeyboardHeight(e?.endCoordinates?.height ?? 0);
|
|
73
|
+
const onHide = () => setKeyboardHeight(0);
|
|
74
|
+
const subs = [
|
|
75
|
+
Keyboard.addListener('keyboardDidShow', onShow),
|
|
76
|
+
Keyboard.addListener('keyboardDidChangeFrame', onShow),
|
|
77
|
+
Keyboard.addListener('keyboardDidHide', onHide),
|
|
78
|
+
];
|
|
79
|
+
return () => subs.forEach((s) => s.remove());
|
|
80
|
+
}, []);
|
|
81
|
+
|
|
59
82
|
const handleMenuLayout = (e: LayoutChangeEvent) => {
|
|
60
83
|
const { width, height } = e.nativeEvent.layout;
|
|
61
84
|
if (
|
|
@@ -107,7 +130,13 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
107
130
|
const topReserve = 16;
|
|
108
131
|
const sideMargin = 8;
|
|
109
132
|
|
|
110
|
-
|
|
133
|
+
// The keyboard occludes the bottom `keyboardHeight` px, so the real
|
|
134
|
+
// visible bottom edge is above it. Measuring space below against this
|
|
135
|
+
// (not the full screen) is what makes the menu flip ABOVE the message
|
|
136
|
+
// when the keyboard is open and the message sits near the input.
|
|
137
|
+
const visibleBottom = screenHeight - keyboardHeight;
|
|
138
|
+
|
|
139
|
+
const spaceBelow = visibleBottom - position.bottom - bottomReserve;
|
|
111
140
|
const spaceAbove = position.top - topReserve;
|
|
112
141
|
|
|
113
142
|
let top: number;
|
|
@@ -116,7 +145,10 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
116
145
|
} else if (spaceAbove >= menuSize.height) {
|
|
117
146
|
top = position.top - menuSize.height;
|
|
118
147
|
} else {
|
|
119
|
-
top = Math.max(
|
|
148
|
+
top = Math.max(
|
|
149
|
+
topReserve,
|
|
150
|
+
visibleBottom - menuSize.height - bottomReserve
|
|
151
|
+
);
|
|
120
152
|
}
|
|
121
153
|
|
|
122
154
|
const leftMargin = position.left;
|
|
@@ -130,7 +162,7 @@ const MessageInteractions: React.FC<MessageInteractionsProps> = ({
|
|
|
130
162
|
);
|
|
131
163
|
|
|
132
164
|
return { top, left };
|
|
133
|
-
}, [position, menuSize, config?.keyboardVerticalOffset]);
|
|
165
|
+
}, [position, menuSize, config?.keyboardVerticalOffset, keyboardHeight]);
|
|
134
166
|
|
|
135
167
|
// Window coords (memoPosition) → host-local coords. The overlay host
|
|
136
168
|
// sits below the status bar / header, so subtract its measured origin.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import React, { useState } from 'react';
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
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',
|