@ethora/chat-component-rn 26.5.11 → 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 +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 +17 -1
- 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/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 +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 +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/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/ChatRoom.js +18 -2
- 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/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 +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 +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/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/ChatRoom.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.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/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 +13 -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 +21 -0
- package/src/components/MainComponents/MediaMessage.tsx +295 -5
- 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 +6 -2
- 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/roomStore/roomsSlice.ts +7 -0
- package/src/types/models/modal.model.ts +3 -0
- package/src/types/types.ts +13 -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
|
|
|
@@ -227,8 +228,28 @@ const ChatRoom: React.FC<ChatRoomProps> = React.memo(
|
|
|
227
228
|
const inputDockPaddingBottom = getInputDockPaddingBottom({
|
|
228
229
|
platform: Platform.OS,
|
|
229
230
|
bottomInset: insets.bottom,
|
|
231
|
+
keyboardVisible,
|
|
230
232
|
});
|
|
231
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
|
+
|
|
232
253
|
return (
|
|
233
254
|
<KeyboardAvoidingView
|
|
234
255
|
style={{ flex: 1 }}
|
|
@@ -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
|
+
});
|
|
@@ -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',
|
|
@@ -240,6 +240,30 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
240
240
|
);
|
|
241
241
|
case activeFile.mimetype.startsWith('video/'):
|
|
242
242
|
return <ModalVideo uri={activeFile.fileURL} />;
|
|
243
|
+
case activeFile.mimetype.includes('application/octet-stream'):
|
|
244
|
+
return (
|
|
245
|
+
<View
|
|
246
|
+
style={{
|
|
247
|
+
width: '100%',
|
|
248
|
+
padding: 20,
|
|
249
|
+
gap: 12,
|
|
250
|
+
backgroundColor: '#FFF8ED',
|
|
251
|
+
borderRadius: 16,
|
|
252
|
+
}}
|
|
253
|
+
>
|
|
254
|
+
<Text style={{ fontSize: 16, fontWeight: '600' }}>
|
|
255
|
+
Voice message
|
|
256
|
+
</Text>
|
|
257
|
+
<AudioMessage
|
|
258
|
+
src={activeFile.fileURL}
|
|
259
|
+
mimeType={activeFile.mimetype}
|
|
260
|
+
fileName={activeFile.fileName}
|
|
261
|
+
originalName={activeFile.originalName}
|
|
262
|
+
duration={activeFile.duration}
|
|
263
|
+
waveForm={activeFile.waveForm}
|
|
264
|
+
/>
|
|
265
|
+
</View>
|
|
266
|
+
);
|
|
243
267
|
// Mirrors the MediaMessage heuristic: treat octet-stream voicemails
|
|
244
268
|
// with audio-shaped filenames / URLs as audio so the preview shows
|
|
245
269
|
// the player instead of an "Unsupported" card. Customer-reported
|
|
@@ -247,7 +271,12 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
247
271
|
case isLikelyAudio(
|
|
248
272
|
activeFile.mimetype,
|
|
249
273
|
activeFile.fileName,
|
|
250
|
-
activeFile.fileURL
|
|
274
|
+
activeFile.fileURL,
|
|
275
|
+
{
|
|
276
|
+
duration: activeFile.duration,
|
|
277
|
+
waveForm: activeFile.waveForm,
|
|
278
|
+
originalName: activeFile.originalName,
|
|
279
|
+
}
|
|
251
280
|
):
|
|
252
281
|
return (
|
|
253
282
|
<View
|
|
@@ -261,11 +290,18 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
261
290
|
>
|
|
262
291
|
<Text style={{ fontSize: 16, fontWeight: '600' }}>
|
|
263
292
|
{ensureFilenameHasExtension(
|
|
264
|
-
activeFile.fileName,
|
|
293
|
+
activeFile.originalName || activeFile.fileName,
|
|
265
294
|
activeFile.mimetype
|
|
266
295
|
)}
|
|
267
296
|
</Text>
|
|
268
|
-
<AudioMessage
|
|
297
|
+
<AudioMessage
|
|
298
|
+
src={activeFile.fileURL}
|
|
299
|
+
mimeType={activeFile.mimetype}
|
|
300
|
+
fileName={activeFile.fileName}
|
|
301
|
+
originalName={activeFile.originalName}
|
|
302
|
+
duration={activeFile.duration}
|
|
303
|
+
waveForm={activeFile.waveForm}
|
|
304
|
+
/>
|
|
269
305
|
</View>
|
|
270
306
|
);
|
|
271
307
|
case activeFile.mimetype === 'application/pdf':
|