@ethora/chat-component-rn 26.6.10 → 26.6.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 +19 -0
- package/lib/commonjs/components/MainComponents/MediaMessage.js +8 -2
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js +1 -1
- package/lib/commonjs/components/MessageBubble/InteractionsOverlay.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js +1 -1
- package/lib/commonjs/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +7 -4
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js +6 -3
- package/lib/commonjs/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/commonjs/components/styled/MediaFilePreview.js +1 -1
- package/lib/commonjs/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +111 -61
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/hooks/useSendMessage.js +2 -2
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/networking/api-requests/auth.api.js +81 -52
- package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +8 -2
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MessageBubble/InteractionsOverlay.js +1 -1
- package/lib/module/components/MessageBubble/InteractionsOverlay.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +1 -1
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js +1 -1
- package/lib/module/components/MessageBubble/MessageInteractions.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +7 -4
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js +1 -1
- package/lib/module/components/Modals/FilePreviewModal/GViewWebView.js.map +1 -1
- package/lib/module/components/VideoCalls/VideoCallSession.js +6 -3
- package/lib/module/components/VideoCalls/VideoCallSession.js.map +1 -1
- package/lib/module/components/styled/MediaFilePreview.js +1 -1
- package/lib/module/components/styled/MediaFilePreview.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +112 -62
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/hooks/useSendMessage.js +3 -3
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/networking/api-requests/auth.api.js +80 -51
- package/lib/module/networking/api-requests/auth.api.js.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts +2 -0
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/hooks/useCustomTypingIndicator.d.ts +16 -4
- package/lib/typescript/hooks/useCustomTypingIndicator.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts +27 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
- package/package.json +29 -28
- package/src/components/MainComponents/MediaMessage.tsx +6 -1
- package/src/components/MessageBubble/InteractionsOverlay.tsx +1 -1
- package/src/components/MessageBubble/Message.tsx +1 -1
- package/src/components/MessageBubble/MessageInteractions.tsx +1 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +6 -3
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +1 -1
- package/src/components/VideoCalls/VideoCallSession.tsx +8 -3
- package/src/components/styled/MediaFilePreview.tsx +1 -1
- package/src/components/styled/VideoMessage.tsx +115 -54
- package/src/hooks/useSendMessage.tsx +3 -3
- package/src/networking/api-requests/auth.api.ts +79 -44
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
|
-
import { Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
-
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
2
|
+
import { Image, Pressable, StyleSheet, View } from 'react-native';
|
|
4
3
|
import { useDispatch } from 'react-redux';
|
|
5
4
|
import {
|
|
6
5
|
setActiveFile,
|
|
@@ -18,72 +17,134 @@ interface CustomMessageVideoProps {
|
|
|
18
17
|
fileName: string;
|
|
19
18
|
fileURL: string;
|
|
20
19
|
mimetype: string;
|
|
20
|
+
/** The backend-generated poster frame (message.locationPreview). */
|
|
21
|
+
previewURL?: string;
|
|
21
22
|
}
|
|
22
23
|
|
|
24
|
+
const PlayBadge: React.FC = () => (
|
|
25
|
+
<View style={styles.overlay} pointerEvents="none">
|
|
26
|
+
<View style={styles.playButton}>
|
|
27
|
+
<PlayIcon width={28} height={28} />
|
|
28
|
+
</View>
|
|
29
|
+
</View>
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Poster path — taken whenever the message carries a `locationPreview`.
|
|
34
|
+
*
|
|
35
|
+
* A chat row only ever needs a still and a play affordance; actual
|
|
36
|
+
* playback happens in the full-screen preview. Drawing that still with a
|
|
37
|
+
* plain <Image> keeps the whole bubble inside the RN view hierarchy, so
|
|
38
|
+
* the play badge is guaranteed to composite on top of it.
|
|
39
|
+
*/
|
|
40
|
+
const PosterVideo: React.FC<{
|
|
41
|
+
previewURL: string;
|
|
42
|
+
onOpen: () => void;
|
|
43
|
+
onPosterError: () => void;
|
|
44
|
+
}> = ({ previewURL, onOpen, onPosterError }) => {
|
|
45
|
+
const [dims, setDims] = useState<MediaDims>(defaultMediaDims());
|
|
46
|
+
|
|
47
|
+
useEffect(() => {
|
|
48
|
+
if (!previewURL) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
let active = true;
|
|
52
|
+
Image.getSize(
|
|
53
|
+
previewURL,
|
|
54
|
+
(w, h) => {
|
|
55
|
+
if (active) {
|
|
56
|
+
setDims(fitMediaDimensions(w, h));
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
() => {
|
|
60
|
+
/* keep the default box if the size probe fails */
|
|
61
|
+
}
|
|
62
|
+
);
|
|
63
|
+
return () => {
|
|
64
|
+
active = false;
|
|
65
|
+
};
|
|
66
|
+
}, [previewURL]);
|
|
67
|
+
|
|
68
|
+
return (
|
|
69
|
+
<Pressable
|
|
70
|
+
onPress={onOpen}
|
|
71
|
+
style={[styles.wrapper, { width: dims.width, height: dims.height }]}
|
|
72
|
+
accessibilityRole="button"
|
|
73
|
+
accessibilityLabel="Play video"
|
|
74
|
+
>
|
|
75
|
+
<Image
|
|
76
|
+
source={{ uri: previewURL }}
|
|
77
|
+
style={styles.video}
|
|
78
|
+
resizeMode="cover"
|
|
79
|
+
// Not every backend returns a real still — some set
|
|
80
|
+
// locationPreview to the video URL itself, which decodes to
|
|
81
|
+
// nothing. Rather than show an empty box, fall back to the
|
|
82
|
+
// static placeholder branch.
|
|
83
|
+
onError={onPosterError}
|
|
84
|
+
/>
|
|
85
|
+
<PlayBadge />
|
|
86
|
+
</Pressable>
|
|
87
|
+
);
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Fallback for messages with no usable poster frame: a static dark
|
|
92
|
+
* placeholder behind the play badge.
|
|
93
|
+
*
|
|
94
|
+
* This branch used to mount an expo-video player purely to paint the
|
|
95
|
+
* first frame (`VideoView` + `surfaceType="textureView"`). That never
|
|
96
|
+
* composited reliably: on Android (Pixel, Android 16, New Architecture)
|
|
97
|
+
* the native video surface draws ABOVE sibling RN views, swallowing the
|
|
98
|
+
* play badge — the video looked like a plain photo. There is no
|
|
99
|
+
* surfaceType/zIndex combination that guarantees ordering across SDK 54
|
|
100
|
+
* and 57, so we don't fight it: no live surface in the bubble at all.
|
|
101
|
+
* The badge is plain RN views over a plain RN background, which cannot
|
|
102
|
+
* be occluded, and we drop the per-row player instance as a bonus.
|
|
103
|
+
* Actual playback (and the real first frame) happens in the full-screen
|
|
104
|
+
* preview modal.
|
|
105
|
+
*/
|
|
106
|
+
const PlaceholderVideo: React.FC<{
|
|
107
|
+
onOpen: () => void;
|
|
108
|
+
}> = ({ onOpen }) => {
|
|
109
|
+
const dims = defaultMediaDims();
|
|
110
|
+
|
|
111
|
+
return (
|
|
112
|
+
<Pressable
|
|
113
|
+
onPress={onOpen}
|
|
114
|
+
style={[styles.wrapper, { width: dims.width, height: dims.height }]}
|
|
115
|
+
accessibilityRole="button"
|
|
116
|
+
accessibilityLabel="Play video"
|
|
117
|
+
>
|
|
118
|
+
<PlayBadge />
|
|
119
|
+
</Pressable>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
23
123
|
const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
24
124
|
fileName,
|
|
25
125
|
fileURL,
|
|
26
126
|
mimetype,
|
|
127
|
+
previewURL,
|
|
27
128
|
}) => {
|
|
28
129
|
const dispatch = useDispatch();
|
|
29
|
-
const [
|
|
30
|
-
|
|
31
|
-
const player = useVideoPlayer(fileURL, (p) => {
|
|
32
|
-
p.muted = true;
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
useEffect(() => {
|
|
36
|
-
const sub = player.addListener('statusChange', () => {
|
|
37
|
-
const size = player.videoTrack?.size;
|
|
38
|
-
if (size?.width && size?.height) {
|
|
39
|
-
setDims(fitMediaDimensions(size.width, size.height));
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
return () => sub.remove();
|
|
43
|
-
}, [player]);
|
|
130
|
+
const [posterFailed, setPosterFailed] = useState(false);
|
|
44
131
|
|
|
45
132
|
const handleOpen = () => {
|
|
46
133
|
dispatch(setActiveFile({ fileName, fileURL, mimetype }));
|
|
47
134
|
dispatch(setActiveModal(MODAL_TYPES.FILE_PREVIEW));
|
|
48
135
|
};
|
|
49
136
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
|
61
|
-
style={[styles.wrapper, { width: dims.width, height: dims.height }]}
|
|
62
|
-
>
|
|
63
|
-
<VideoView
|
|
64
|
-
player={player}
|
|
65
|
-
style={styles.video}
|
|
66
|
-
contentFit="cover"
|
|
67
|
-
nativeControls={false}
|
|
68
|
-
surfaceType="textureView"
|
|
69
|
-
pointerEvents="none"
|
|
137
|
+
if (previewURL && !posterFailed) {
|
|
138
|
+
return (
|
|
139
|
+
<PosterVideo
|
|
140
|
+
previewURL={previewURL}
|
|
141
|
+
onOpen={handleOpen}
|
|
142
|
+
onPosterError={() => setPosterFailed(true)}
|
|
70
143
|
/>
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
</View>
|
|
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>
|
|
86
|
-
);
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
return <PlaceholderVideo onOpen={handleOpen} />;
|
|
87
148
|
};
|
|
88
149
|
|
|
89
150
|
export default CustomMessageVideo;
|
|
@@ -99,7 +160,7 @@ const styles = StyleSheet.create({
|
|
|
99
160
|
height: '100%',
|
|
100
161
|
},
|
|
101
162
|
overlay: {
|
|
102
|
-
...StyleSheet.
|
|
163
|
+
...StyleSheet.absoluteFill,
|
|
103
164
|
justifyContent: 'center',
|
|
104
165
|
alignItems: 'center',
|
|
105
166
|
},
|
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
clearMessageFailure,
|
|
10
10
|
markMessageFailed,
|
|
11
11
|
} from '../roomStore/roomHeapSlice';
|
|
12
|
-
import {
|
|
12
|
+
import { uploadFileMultipart } from '../networking/api-requests/auth.api';
|
|
13
13
|
import { enqueueOutboundSend } from '../networking/outboundQueue';
|
|
14
14
|
import { RootState, store } from '../roomStore';
|
|
15
15
|
import { getGlobalXmppClient } from '../utils/clientRegistry';
|
|
@@ -479,7 +479,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
479
479
|
const fd = new FormData();
|
|
480
480
|
fd.append('files', fileBlob as any);
|
|
481
481
|
try {
|
|
482
|
-
return await
|
|
482
|
+
return await uploadFileMultipart(fd);
|
|
483
483
|
} catch (err: any) {
|
|
484
484
|
if (err?.response?.status === 500) {
|
|
485
485
|
console.warn(
|
|
@@ -488,7 +488,7 @@ export const useSendMessage = (_configOverride?: IConfig) => {
|
|
|
488
488
|
);
|
|
489
489
|
const fd2 = new FormData();
|
|
490
490
|
fd2.append('file', fileBlob as any);
|
|
491
|
-
return await
|
|
491
|
+
return await uploadFileMultipart(fd2);
|
|
492
492
|
}
|
|
493
493
|
throw err;
|
|
494
494
|
}
|
|
@@ -144,55 +144,90 @@ export function uploadFile(formData: FormData) {
|
|
|
144
144
|
});
|
|
145
145
|
}
|
|
146
146
|
|
|
147
|
-
|
|
147
|
+
const UPLOAD_PATH = '/files/';
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Multipart upload of a picked file, sent over `XMLHttpRequest`.
|
|
151
|
+
*
|
|
152
|
+
* The transport matters here, and both obvious alternatives are wrong:
|
|
153
|
+
*
|
|
154
|
+
* - `fetch`. Expo SDK 54 shipped a WinterCG-compliant fetch and SDK 57
|
|
155
|
+
* installs it as the GLOBAL fetch. It accepts only `Blob` / `File` /
|
|
156
|
+
* string form parts, so React Native's own `{ uri, type, name }` part
|
|
157
|
+
* — which is exactly what an image/document picker hands us — is
|
|
158
|
+
* rejected with "Unsupported FormDataPart implementation", surfacing
|
|
159
|
+
* to the caller as ERR_NETWORK. A host can set
|
|
160
|
+
* `EXPO_PUBLIC_USE_RN_FETCH=1`, but that opts their whole app out of
|
|
161
|
+
* Expo's fetch just to satisfy this SDK; not a cost we get to impose
|
|
162
|
+
* on a consumer.
|
|
163
|
+
* - axios. It forces its own `Content-Type: multipart/form-data`
|
|
164
|
+
* WITHOUT a boundary, so the server mis-parses one file as many and
|
|
165
|
+
* answers HTTP 413 TOO_MANY_FILES (bug #10 — this is why the upload
|
|
166
|
+
* was moved off axios in the first place).
|
|
167
|
+
*
|
|
168
|
+
* `XMLHttpRequest` avoids both. Expo replaces `fetch`, not `XHR`, so the
|
|
169
|
+
* request still goes through React Native's native networking module,
|
|
170
|
+
* which understands the `{ uri, type, name }` part, streams the file
|
|
171
|
+
* straight from disk (a 50MB video never lands in JS memory) and writes
|
|
172
|
+
* its own `Content-Type` with a valid boundary — which is precisely why
|
|
173
|
+
* we must NOT set that header ourselves below.
|
|
174
|
+
*/
|
|
175
|
+
export function uploadFileMultipart(formData: FormData): Promise<{ data: any }> {
|
|
148
176
|
const token = store.getState().chatSettingStore?.user?.token ?? '';
|
|
149
177
|
const baseUrl = getCurrentBaseURL();
|
|
150
|
-
const url = `${baseUrl.replace(/\/$/, '')}
|
|
178
|
+
const url = `${baseUrl.replace(/\/$/, '')}${UPLOAD_PATH}`;
|
|
151
179
|
|
|
152
|
-
pushLog('http', `→ POST ${url} (
|
|
180
|
+
pushLog('http', `→ POST ${url} (xhr)`, {
|
|
153
181
|
headers: { Authorization: token ? token.slice(0, 16) + '…' : '[empty]', Accept: '*/*' },
|
|
154
182
|
});
|
|
155
183
|
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
184
|
+
return new Promise((resolve, reject) => {
|
|
185
|
+
const xhr = new XMLHttpRequest();
|
|
186
|
+
|
|
187
|
+
const failNetwork = (reason: string) => {
|
|
188
|
+
pushLog('http', `← ERR xhr ${UPLOAD_PATH}`, { reason });
|
|
189
|
+
const err: any = new Error(reason);
|
|
190
|
+
err.code = 'ERR_NETWORK';
|
|
191
|
+
err.config = { url: UPLOAD_PATH };
|
|
192
|
+
reject(err);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
xhr.onload = () => {
|
|
196
|
+
const status = xhr.status;
|
|
197
|
+
let body: any = null;
|
|
198
|
+
try {
|
|
199
|
+
body = JSON.parse(xhr.responseText);
|
|
200
|
+
} catch {
|
|
201
|
+
body = xhr.responseText || null;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (status < 200 || status >= 300) {
|
|
205
|
+
pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, body);
|
|
206
|
+
const err: any = new Error(`Upload failed: ${status}`);
|
|
207
|
+
// Shaped like an axios error so callers' `err.response.status`
|
|
208
|
+
// checks keep working — notably useSendMessage's retry with the
|
|
209
|
+
// singular "file" field on 500.
|
|
210
|
+
err.response = { status, statusText: xhr.statusText, data: body };
|
|
211
|
+
err.config = { url: UPLOAD_PATH };
|
|
212
|
+
reject(err);
|
|
213
|
+
return;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
pushLog('http', `← ${status} POST ${UPLOAD_PATH} (xhr)`, {
|
|
217
|
+
resultsCount: Array.isArray(body?.results) ? body.results.length : 'n/a',
|
|
218
|
+
});
|
|
219
|
+
resolve({ data: body });
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
xhr.onerror = () => failNetwork('Network request failed');
|
|
223
|
+
xhr.ontimeout = () => failNetwork('Upload timed out');
|
|
224
|
+
xhr.onabort = () => failNetwork('Upload aborted');
|
|
225
|
+
|
|
226
|
+
xhr.open('POST', url);
|
|
227
|
+
xhr.setRequestHeader('Authorization', token);
|
|
228
|
+
xhr.setRequestHeader('Accept', '*/*');
|
|
229
|
+
// Deliberately no Content-Type — see the note above: RN fills it in
|
|
230
|
+
// along with the multipart boundary it generated.
|
|
231
|
+
xhr.send(formData as any);
|
|
196
232
|
});
|
|
197
|
-
return { data };
|
|
198
233
|
}
|