@ethora/chat-component-rn 26.6.11 → 26.6.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 +24 -0
- package/README.md +2 -2
- package/lib/commonjs/components/AuthForms/Login.js +2 -1
- package/lib/commonjs/components/AuthForms/Login.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +4 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatWrapper.js +14 -13
- package/lib/commonjs/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/LoginWrapper.js +2 -1
- package/lib/commonjs/components/MainComponents/LoginWrapper.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +18 -7
- 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/ChatProfileModal/ChatProfileModal.js +4 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +15 -11
- 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/Modals/FilePreviewModal/PdfView.js +2 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js +5 -1
- package/lib/commonjs/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js +10 -2
- package/lib/commonjs/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.js +4 -1
- package/lib/commonjs/components/RoomComponents/styled/LastMessageVideo.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/AudioMessage.js +2 -1
- package/lib/commonjs/components/styled/AudioMessage.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/MessageImage.js +34 -8
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +36 -64
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +44 -18
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/initXmppRooms.js +5 -1
- package/lib/commonjs/helpers/initXmppRooms.js.map +1 -1
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +126 -12
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/commonjs/helpers/secureFileUrl.js +101 -0
- package/lib/commonjs/helpers/secureFileUrl.js.map +1 -0
- package/lib/commonjs/hooks/useChatWrapperInit.js +3 -3
- package/lib/commonjs/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/commonjs/hooks/useFileToken.js +10 -0
- package/lib/commonjs/hooks/useFileToken.js.map +1 -0
- package/lib/commonjs/hooks/useSendMessage.js +23 -13
- package/lib/commonjs/hooks/useSendMessage.js.map +1 -1
- package/lib/commonjs/main.js +25 -0
- package/lib/commonjs/main.js.map +1 -1
- package/lib/commonjs/networking/api-requests/auth.api.js +99 -54
- package/lib/commonjs/networking/api-requests/auth.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/roomMembers.api.js +1 -1
- package/lib/commonjs/networking/api-requests/roomMembers.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +9 -9
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/api-requests/user.api.js +9 -9
- package/lib/commonjs/networking/api-requests/user.api.js.map +1 -1
- package/lib/commonjs/networking/apiClient.js +62 -112
- package/lib/commonjs/networking/apiClient.js.map +1 -1
- package/lib/commonjs/networking/authRefresh.js +323 -0
- package/lib/commonjs/networking/authRefresh.js.map +1 -0
- package/lib/commonjs/networking/xmppClient.js +33 -5
- package/lib/commonjs/networking/xmppClient.js.map +1 -1
- package/lib/commonjs/roomStore/chatSettingsSlice.js +20 -2
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/components/AuthForms/Login.js +2 -1
- package/lib/module/components/AuthForms/Login.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +4 -1
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/ChatWrapper.js +14 -13
- package/lib/module/components/MainComponents/ChatWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/LoginWrapper.js +2 -1
- package/lib/module/components/MainComponents/LoginWrapper.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +18 -7
- 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/ChatProfileModal/ChatProfileModal.js +4 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +15 -11
- 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/Modals/FilePreviewModal/PdfView.js +2 -1
- package/lib/module/components/Modals/FilePreviewModal/PdfView.js.map +1 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js +5 -1
- package/lib/module/components/RoomComponents/ChatRoomItem.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js +10 -2
- package/lib/module/components/RoomComponents/styled/LastMessageFile.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js +4 -1
- package/lib/module/components/RoomComponents/styled/LastMessagePhoto.js.map +1 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.js +4 -1
- package/lib/module/components/RoomComponents/styled/LastMessageVideo.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/AudioMessage.js +2 -1
- package/lib/module/components/styled/AudioMessage.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/MessageImage.js +34 -8
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +36 -64
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +43 -18
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/initXmppRooms.js +5 -1
- package/lib/module/helpers/initXmppRooms.js.map +1 -1
- package/lib/module/helpers/resolveInitBeforeLoadUser.js +126 -11
- package/lib/module/helpers/resolveInitBeforeLoadUser.js.map +1 -1
- package/lib/module/helpers/secureFileUrl.js +91 -0
- package/lib/module/helpers/secureFileUrl.js.map +1 -0
- package/lib/module/hooks/useChatWrapperInit.js +3 -3
- package/lib/module/hooks/useChatWrapperInit.js.map +1 -1
- package/lib/module/hooks/useFileToken.js +5 -0
- package/lib/module/hooks/useFileToken.js.map +1 -0
- package/lib/module/hooks/useSendMessage.js +24 -14
- package/lib/module/hooks/useSendMessage.js.map +1 -1
- package/lib/module/main.js +12 -1
- package/lib/module/main.js.map +1 -1
- package/lib/module/networking/api-requests/auth.api.js +96 -53
- package/lib/module/networking/api-requests/auth.api.js.map +1 -1
- package/lib/module/networking/api-requests/roomMembers.api.js +1 -1
- package/lib/module/networking/api-requests/roomMembers.api.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +9 -9
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/api-requests/user.api.js +8 -7
- package/lib/module/networking/api-requests/user.api.js.map +1 -1
- package/lib/module/networking/apiClient.js +61 -113
- package/lib/module/networking/apiClient.js.map +1 -1
- package/lib/module/networking/authRefresh.js +312 -0
- package/lib/module/networking/authRefresh.js.map +1 -0
- package/lib/module/networking/xmppClient.js +33 -5
- package/lib/module/networking/xmppClient.js.map +1 -1
- package/lib/module/roomStore/chatSettingsSlice.js +20 -2
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/typescript/components/AuthForms/Login.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/LoginWrapper.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.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/FilePreviewModal/PdfView.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/ChatRoomItem.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageFile.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessagePhoto.d.ts.map +1 -1
- package/lib/typescript/components/RoomComponents/styled/LastMessageVideo.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/VideoCallSession.d.ts.map +1 -1
- package/lib/typescript/components/styled/AudioMessage.d.ts.map +1 -1
- package/lib/typescript/components/styled/MessageImage.d.ts +8 -0
- package/lib/typescript/components/styled/MessageImage.d.ts.map +1 -1
- package/lib/typescript/components/styled/VideoMessage.d.ts.map +1 -1
- package/lib/typescript/context/xmppProvider.d.ts.map +1 -1
- package/lib/typescript/helpers/initXmppRooms.d.ts.map +1 -1
- package/lib/typescript/helpers/resolveInitBeforeLoadUser.d.ts.map +1 -1
- package/lib/typescript/helpers/secureFileUrl.d.ts +7 -0
- package/lib/typescript/helpers/secureFileUrl.d.ts.map +1 -0
- package/lib/typescript/hooks/useCustomTypingIndicator.d.ts +16 -4
- package/lib/typescript/hooks/useCustomTypingIndicator.d.ts.map +1 -1
- package/lib/typescript/hooks/useFileToken.d.ts +2 -0
- package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
- package/lib/typescript/hooks/useSendMessage.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +6 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/auth.api.d.ts +3 -27
- package/lib/typescript/networking/api-requests/auth.api.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/user.api.d.ts.map +1 -1
- package/lib/typescript/networking/apiClient.d.ts +12 -6
- package/lib/typescript/networking/apiClient.d.ts.map +1 -1
- package/lib/typescript/networking/authRefresh.d.ts +92 -0
- package/lib/typescript/networking/authRefresh.d.ts.map +1 -0
- package/lib/typescript/networking/xmppClient.d.ts +6 -0
- package/lib/typescript/networking/xmppClient.d.ts.map +1 -1
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts +16 -0
- package/lib/typescript/roomStore/chatSettingsSlice.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +17 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/package.json +29 -28
- package/src/components/AuthForms/Login.tsx +1 -0
- package/src/components/MainComponents/ChatHeader.tsx +4 -1
- package/src/components/MainComponents/ChatWrapper.tsx +14 -19
- package/src/components/MainComponents/LoginWrapper.tsx +1 -0
- package/src/components/MainComponents/MediaMessage.tsx +15 -5
- 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/ChatProfileModal/ChatProfileModal.tsx +4 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +14 -10
- package/src/components/Modals/FilePreviewModal/GViewWebView.tsx +1 -1
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
- package/src/components/RoomComponents/ChatRoomItem.tsx +9 -1
- package/src/components/RoomComponents/styled/LastMessageFile.tsx +14 -2
- package/src/components/RoomComponents/styled/LastMessagePhoto.tsx +4 -1
- package/src/components/RoomComponents/styled/LastMessageVideo.tsx +4 -1
- package/src/components/VideoCalls/VideoCallSession.tsx +8 -3
- package/src/components/styled/AudioMessage.tsx +2 -1
- package/src/components/styled/MediaFilePreview.tsx +1 -1
- package/src/components/styled/MessageImage.tsx +45 -6
- package/src/components/styled/VideoMessage.tsx +36 -60
- package/src/context/xmppProvider.tsx +45 -20
- package/src/helpers/initXmppRooms.tsx +5 -1
- package/src/helpers/resolveInitBeforeLoadUser.ts +123 -11
- package/src/helpers/secureFileUrl.ts +103 -0
- package/src/hooks/useChatWrapperInit.ts +3 -3
- package/src/hooks/useFileToken.ts +7 -0
- package/src/hooks/useSendMessage.tsx +28 -13
- package/src/main.ts +22 -1
- package/src/networking/api-requests/auth.api.ts +125 -52
- package/src/networking/api-requests/roomMembers.api.ts +1 -1
- package/src/networking/api-requests/rooms.api.ts +9 -9
- package/src/networking/api-requests/user.api.ts +8 -7
- package/src/networking/apiClient.ts +68 -123
- package/src/networking/authRefresh.ts +435 -0
- package/src/networking/xmppClient.ts +37 -5
- package/src/roomStore/chatSettingsSlice.ts +28 -3
- package/src/types/types.ts +17 -0
|
@@ -345,9 +345,14 @@ export const VideoCallSession: FC<VideoCallSessionProps> = ({
|
|
|
345
345
|
if (!runtime) {return;}
|
|
346
346
|
try {
|
|
347
347
|
if (Platform.OS === 'android') {
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
348
|
+
// NB: not `setAndroidAudioConfiguration` — that method has never
|
|
349
|
+
// existed on AudioSession (the old `?.` call silently no-oped, so
|
|
350
|
+
// the speaker icon toggled while audio stayed on the earpiece).
|
|
351
|
+
// `selectAudioOutput` routes directly; on Android its device ids
|
|
352
|
+
// are the literal strings 'speaker' / 'earpiece'.
|
|
353
|
+
await runtime.AudioSession.selectAudioOutput(
|
|
354
|
+
next ? 'speaker' : 'earpiece'
|
|
355
|
+
);
|
|
351
356
|
} else {
|
|
352
357
|
await runtime.AudioSession.setAppleAudioConfiguration?.({
|
|
353
358
|
audioCategory: 'playAndRecord',
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { withFileToken } from '../../helpers/secureFileUrl';
|
|
2
3
|
import {
|
|
3
4
|
View,
|
|
4
5
|
TouchableOpacity,
|
|
@@ -425,7 +426,7 @@ const AudioMessage = ({
|
|
|
425
426
|
const partInfo = await FileSystem.getInfoAsync(part);
|
|
426
427
|
if (!partInfo.exists) {
|
|
427
428
|
const download = await withTimeout(
|
|
428
|
-
FileSystem.downloadAsync(src, part),
|
|
429
|
+
FileSystem.downloadAsync(withFileToken(src), part),
|
|
429
430
|
12000,
|
|
430
431
|
'audio_download_timeout'
|
|
431
432
|
);
|
|
@@ -110,7 +110,7 @@ const styles = StyleSheet.create({
|
|
|
110
110
|
backgroundColor: '#1a1a1a',
|
|
111
111
|
},
|
|
112
112
|
playOverlay: {
|
|
113
|
-
...StyleSheet.
|
|
113
|
+
...StyleSheet.absoluteFill,
|
|
114
114
|
justifyContent: 'center',
|
|
115
115
|
alignItems: 'center',
|
|
116
116
|
backgroundColor: 'rgba(0,0,0,0.3)',
|
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
isSecureFileUrl,
|
|
4
|
+
requestFileTokenRecovery,
|
|
5
|
+
} from '../../helpers/secureFileUrl';
|
|
6
|
+
import { pushLog } from '../../utils/devLogger';
|
|
2
7
|
import { Container } from './StyledInputComponents/MediaComponents';
|
|
3
8
|
import { useDispatch } from 'react-redux';
|
|
4
9
|
import {
|
|
@@ -14,11 +19,23 @@ import {
|
|
|
14
19
|
} from '../../helpers/mediaDimensions';
|
|
15
20
|
|
|
16
21
|
interface CustomMessageImageProps {
|
|
22
|
+
/** Full-size file. Opened in the preview modal, NOT rendered inline. */
|
|
17
23
|
fileURL: string;
|
|
18
24
|
fileName: string;
|
|
19
25
|
mimetype: string;
|
|
26
|
+
/**
|
|
27
|
+
* Server-generated thumbnail — what the bubble actually renders, same
|
|
28
|
+
* as the web client. The full-size original can be many megabytes and,
|
|
29
|
+
* for secure uploads, is served from a different (membership-gated)
|
|
30
|
+
* route; rendering it inline is what left the bubbles blank.
|
|
31
|
+
*/
|
|
32
|
+
locationPreview?: string;
|
|
20
33
|
}
|
|
21
34
|
|
|
35
|
+
/** Keep the personal file token out of the logs. */
|
|
36
|
+
const maskToken = (url?: string) =>
|
|
37
|
+
(url || '').replace(/([?&]ft=)[^&]*/i, '$1<redacted>');
|
|
38
|
+
|
|
22
39
|
const FALLBACK_IMAGE =
|
|
23
40
|
'https://as2.ftcdn.net/v2/jpg/02/51/95/53/1000_F_251955356_FAQH0U1y1TZw3ZcdPGybwUkH90a3VAhb.jpg';
|
|
24
41
|
|
|
@@ -26,7 +43,12 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
26
43
|
fileURL,
|
|
27
44
|
fileName,
|
|
28
45
|
mimetype,
|
|
46
|
+
locationPreview,
|
|
29
47
|
}) => {
|
|
48
|
+
// Preview first, original only as a fallback — mirrors the web client,
|
|
49
|
+
// where the <img> src is the preview and `fileURL` is reserved for the
|
|
50
|
+
// modal.
|
|
51
|
+
const displayURL = locationPreview || fileURL;
|
|
30
52
|
const [loading, setLoading] = useState(true);
|
|
31
53
|
// Start with a neutral box; swap to the real aspect ratio once the
|
|
32
54
|
// image's natural dimensions are known so it's shown scaled, not cropped.
|
|
@@ -35,25 +57,31 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
35
57
|
const dispatch = useDispatch();
|
|
36
58
|
|
|
37
59
|
useEffect(() => {
|
|
38
|
-
if (!
|
|
60
|
+
if (!displayURL) {
|
|
39
61
|
return;
|
|
40
62
|
}
|
|
41
63
|
let active = true;
|
|
42
64
|
Image.getSize(
|
|
43
|
-
|
|
65
|
+
displayURL,
|
|
44
66
|
(w, h) => {
|
|
45
67
|
if (active) {
|
|
46
68
|
setDims(fitMediaDimensions(w, h));
|
|
47
69
|
}
|
|
48
70
|
},
|
|
49
|
-
() => {
|
|
50
|
-
|
|
71
|
+
(error) => {
|
|
72
|
+
// Keep the default box — but say WHICH url failed. A blank
|
|
73
|
+
// bubble is otherwise indistinguishable between "bad url",
|
|
74
|
+
// "403 without a token" and "host unreachable".
|
|
75
|
+
pushLog('warn', 'image: getSize failed', {
|
|
76
|
+
url: maskToken(displayURL),
|
|
77
|
+
error: (error as any)?.message || String(error),
|
|
78
|
+
});
|
|
51
79
|
}
|
|
52
80
|
);
|
|
53
81
|
return () => {
|
|
54
82
|
active = false;
|
|
55
83
|
};
|
|
56
|
-
}, [
|
|
84
|
+
}, [displayURL]);
|
|
57
85
|
|
|
58
86
|
const handleOpen = () => {
|
|
59
87
|
dispatch(setActiveFile({ fileName, fileURL, mimetype }));
|
|
@@ -78,7 +106,7 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
78
106
|
</View>
|
|
79
107
|
)}
|
|
80
108
|
<Image
|
|
81
|
-
source={{ uri:
|
|
109
|
+
source={{ uri: displayURL || FALLBACK_IMAGE }}
|
|
82
110
|
style={{
|
|
83
111
|
width: dims.width,
|
|
84
112
|
height: dims.height,
|
|
@@ -86,6 +114,17 @@ const CustomMessageImage: React.FC<CustomMessageImageProps> = ({
|
|
|
86
114
|
backgroundColor: 'rgba(0, 0, 0, 0)',
|
|
87
115
|
}}
|
|
88
116
|
onLoadEnd={() => setLoading(false)}
|
|
117
|
+
onError={(event) => {
|
|
118
|
+
setLoading(false);
|
|
119
|
+
pushLog('warn', 'image: load failed', {
|
|
120
|
+
url: maskToken(displayURL),
|
|
121
|
+
usingPreview: !!locationPreview,
|
|
122
|
+
error: (event as any)?.nativeEvent?.error,
|
|
123
|
+
});
|
|
124
|
+
if (isSecureFileUrl(displayURL)) {
|
|
125
|
+
requestFileTokenRecovery();
|
|
126
|
+
}
|
|
127
|
+
}}
|
|
89
128
|
// The box already matches the natural aspect ratio, so 'cover'
|
|
90
129
|
// fills it edge-to-edge with no actual cropping or letterboxing.
|
|
91
130
|
resizeMode="cover"
|
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import React, { useEffect, useState } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
isSecureFileUrl,
|
|
4
|
+
requestFileTokenRecovery,
|
|
5
|
+
} from '../../helpers/secureFileUrl';
|
|
2
6
|
import { Image, Pressable, StyleSheet, View } from 'react-native';
|
|
3
|
-
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
4
7
|
import { useDispatch } from 'react-redux';
|
|
5
8
|
import {
|
|
6
9
|
setActiveFile,
|
|
@@ -37,12 +40,6 @@ const PlayBadge: React.FC = () => (
|
|
|
37
40
|
* playback happens in the full-screen preview. Drawing that still with a
|
|
38
41
|
* plain <Image> keeps the whole bubble inside the RN view hierarchy, so
|
|
39
42
|
* the play badge is guaranteed to composite on top of it.
|
|
40
|
-
*
|
|
41
|
-
* The VideoView path below cannot make that promise: expo-video draws
|
|
42
|
-
* into a native surface, and depending on platform, expo-video version
|
|
43
|
-
* and surfaceType that surface can end up above its sibling RN views —
|
|
44
|
-
* swallowing the badge and leaving the video looking like a plain photo.
|
|
45
|
-
* It is also cheaper: no video player instance per row in a long list.
|
|
46
43
|
*/
|
|
47
44
|
const PosterVideo: React.FC<{
|
|
48
45
|
previewURL: string;
|
|
@@ -85,8 +82,8 @@ const PosterVideo: React.FC<{
|
|
|
85
82
|
resizeMode="cover"
|
|
86
83
|
// Not every backend returns a real still — some set
|
|
87
84
|
// locationPreview to the video URL itself, which decodes to
|
|
88
|
-
// nothing. Rather than show an empty box,
|
|
89
|
-
//
|
|
85
|
+
// nothing. Rather than show an empty box, fall back to the
|
|
86
|
+
// static placeholder branch.
|
|
90
87
|
onError={onPosterError}
|
|
91
88
|
/>
|
|
92
89
|
<PlayBadge />
|
|
@@ -95,61 +92,35 @@ const PosterVideo: React.FC<{
|
|
|
95
92
|
};
|
|
96
93
|
|
|
97
94
|
/**
|
|
98
|
-
* Fallback for messages with no usable poster frame:
|
|
99
|
-
*
|
|
95
|
+
* Fallback for messages with no usable poster frame: a static dark
|
|
96
|
+
* placeholder behind the play badge.
|
|
97
|
+
*
|
|
98
|
+
* This branch used to mount an expo-video player purely to paint the
|
|
99
|
+
* first frame (`VideoView` + `surfaceType="textureView"`). That never
|
|
100
|
+
* composited reliably: on Android (Pixel, Android 16, New Architecture)
|
|
101
|
+
* the native video surface draws ABOVE sibling RN views, swallowing the
|
|
102
|
+
* play badge — the video looked like a plain photo. There is no
|
|
103
|
+
* surfaceType/zIndex combination that guarantees ordering across SDK 54
|
|
104
|
+
* and 57, so we don't fight it: no live surface in the bubble at all.
|
|
105
|
+
* The badge is plain RN views over a plain RN background, which cannot
|
|
106
|
+
* be occluded, and we drop the per-row player instance as a bonus.
|
|
107
|
+
* Actual playback (and the real first frame) happens in the full-screen
|
|
108
|
+
* preview modal.
|
|
100
109
|
*/
|
|
101
|
-
const
|
|
102
|
-
fileURL: string;
|
|
110
|
+
const PlaceholderVideo: React.FC<{
|
|
103
111
|
onOpen: () => void;
|
|
104
|
-
}> = ({
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
const player = useVideoPlayer(fileURL, (p) => {
|
|
108
|
-
p.muted = true;
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
useEffect(() => {
|
|
112
|
-
const sub = player.addListener('statusChange', () => {
|
|
113
|
-
const size = player.videoTrack?.size;
|
|
114
|
-
if (size?.width && size?.height) {
|
|
115
|
-
setDims(fitMediaDimensions(size.width, size.height));
|
|
116
|
-
}
|
|
117
|
-
});
|
|
118
|
-
return () => sub.remove();
|
|
119
|
-
}, [player]);
|
|
112
|
+
}> = ({ onOpen }) => {
|
|
113
|
+
const dims = defaultMediaDims();
|
|
120
114
|
|
|
121
115
|
return (
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
// top owns the tap. Previously we wrapped VideoView in a
|
|
125
|
-
// TouchableOpacity and set `pointerEvents="none"` on the inner
|
|
126
|
-
// VideoView — but on some expo-video versions / iOS the native
|
|
127
|
-
// VideoView still intercepted gestures despite that hint, so
|
|
128
|
-
// tapping the poster did nothing. Customer-reported #9 (video
|
|
129
|
-
// preview unresponsive). The Pressable now guarantees the tap
|
|
130
|
-
// reaches `onOpen`.
|
|
131
|
-
<View
|
|
116
|
+
<Pressable
|
|
117
|
+
onPress={onOpen}
|
|
132
118
|
style={[styles.wrapper, { width: dims.width, height: dims.height }]}
|
|
119
|
+
accessibilityRole="button"
|
|
120
|
+
accessibilityLabel="Play video"
|
|
133
121
|
>
|
|
134
|
-
<VideoView
|
|
135
|
-
player={player}
|
|
136
|
-
style={styles.video}
|
|
137
|
-
contentFit="cover"
|
|
138
|
-
nativeControls={false}
|
|
139
|
-
surfaceType="textureView"
|
|
140
|
-
pointerEvents="none"
|
|
141
|
-
/>
|
|
142
122
|
<PlayBadge />
|
|
143
|
-
|
|
144
|
-
onPress={onOpen}
|
|
145
|
-
// Sits ON TOP of VideoView + the play-button overlay; transparent
|
|
146
|
-
// so the poster + play icon show through. Captures the tap and
|
|
147
|
-
// routes to onOpen → setActiveModal(FILE_PREVIEW).
|
|
148
|
-
style={StyleSheet.absoluteFill}
|
|
149
|
-
accessibilityRole="button"
|
|
150
|
-
accessibilityLabel="Play video"
|
|
151
|
-
/>
|
|
152
|
-
</View>
|
|
123
|
+
</Pressable>
|
|
153
124
|
);
|
|
154
125
|
};
|
|
155
126
|
|
|
@@ -172,12 +143,17 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
172
143
|
<PosterVideo
|
|
173
144
|
previewURL={previewURL}
|
|
174
145
|
onOpen={handleOpen}
|
|
175
|
-
onPosterError={() =>
|
|
146
|
+
onPosterError={() => {
|
|
147
|
+
if (isSecureFileUrl(previewURL)) {
|
|
148
|
+
requestFileTokenRecovery();
|
|
149
|
+
}
|
|
150
|
+
setPosterFailed(true);
|
|
151
|
+
}}
|
|
176
152
|
/>
|
|
177
153
|
);
|
|
178
154
|
}
|
|
179
155
|
|
|
180
|
-
return <
|
|
156
|
+
return <PlaceholderVideo onOpen={handleOpen} />;
|
|
181
157
|
};
|
|
182
158
|
|
|
183
159
|
export default CustomMessageVideo;
|
|
@@ -193,7 +169,7 @@ const styles = StyleSheet.create({
|
|
|
193
169
|
height: '100%',
|
|
194
170
|
},
|
|
195
171
|
overlay: {
|
|
196
|
-
...StyleSheet.
|
|
172
|
+
...StyleSheet.absoluteFill,
|
|
197
173
|
justifyContent: 'center',
|
|
198
174
|
alignItems: 'center',
|
|
199
175
|
},
|
|
@@ -14,7 +14,7 @@ import { VideoCallOverlay } from '../components/VideoCalls/VideoCallOverlay';
|
|
|
14
14
|
import XmppClient, {
|
|
15
15
|
XmppCredentialsProvider,
|
|
16
16
|
} from '../networking/xmppClient';
|
|
17
|
-
import {
|
|
17
|
+
import { refreshAuthTokensQuietly } from '../networking/authRefresh';
|
|
18
18
|
import {
|
|
19
19
|
IConfig,
|
|
20
20
|
ProviderBootstrapStatus,
|
|
@@ -107,23 +107,25 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
107
107
|
// the XmppClient at least doesn't crash on `undefined.password`.
|
|
108
108
|
const credentialsProvider = useMemo<XmppCredentialsProvider>(() => {
|
|
109
109
|
return async () => {
|
|
110
|
-
// 1.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
// 1. Make sure the REST tokens are fresh before re-minting XMPP
|
|
111
|
+
// creds. This used to call `config.refreshTokens.refreshFunction`
|
|
112
|
+
// directly and then fall into step 2, which refreshes again —
|
|
113
|
+
// two rotations back to back, which the new backend scheme
|
|
114
|
+
// reads as a race at best and token reuse at worst. One call to
|
|
115
|
+
// the shared rotation point covers both the consumer-supplied
|
|
116
|
+
// function and the built-in endpoint.
|
|
117
|
+
if (config?.refreshTokens?.enabled) {
|
|
118
|
+
const rotated = await refreshAuthTokensQuietly();
|
|
119
|
+
|
|
120
|
+
if (rotated?.xmppPassword) {
|
|
121
|
+
const rotatedUser = store.getState().chatSettingStore.user;
|
|
122
|
+
return {
|
|
123
|
+
username:
|
|
124
|
+
rotatedUser?.xmppUsername ||
|
|
125
|
+
rotatedUser?.defaultWallet?.walletAddress ||
|
|
126
|
+
'',
|
|
127
|
+
password: rotated.xmppPassword,
|
|
128
|
+
};
|
|
127
129
|
}
|
|
128
130
|
}
|
|
129
131
|
|
|
@@ -163,7 +165,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
163
165
|
}, [
|
|
164
166
|
config?.jwtLogin?.enabled,
|
|
165
167
|
config?.jwtLogin?.token,
|
|
166
|
-
config?.refreshTokens?.
|
|
168
|
+
config?.refreshTokens?.enabled,
|
|
167
169
|
config?.userLogin?.enabled,
|
|
168
170
|
config?.customLogin?.enabled,
|
|
169
171
|
config?.initBeforeLoadAuth?.myEndpoint,
|
|
@@ -353,6 +355,29 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
353
355
|
store.dispatch(setConfig(config));
|
|
354
356
|
}
|
|
355
357
|
|
|
358
|
+
// Rotate BEFORE the first XMPP connect. On the rotating backend
|
|
359
|
+
// the xmpp password is a short-lived JWT that only the newest
|
|
360
|
+
// rotation knows, and each rotation invalidates its predecessors
|
|
361
|
+
// — so a snapshot's password (a host's cached `userLogin.user`,
|
|
362
|
+
// a persisted session) is usually already dead at boot. Without
|
|
363
|
+
// this, the first SASL attempt fails `not-authorized`, all three
|
|
364
|
+
// bootstrap retries re-present the same dead password, and the
|
|
365
|
+
// user dead-ends on the Connection error modal even though one
|
|
366
|
+
// rotation away a perfectly good session existed. The rotation
|
|
367
|
+
// also lands the fileToken pre-render, so secure media never
|
|
368
|
+
// paints blank first. Quiet + deduped; when it can't rotate
|
|
369
|
+
// (no refresh material, non-rotating backend) we just connect
|
|
370
|
+
// with what the snapshot has, exactly as before.
|
|
371
|
+
// jwtLogin is skipped: /users/client just minted fresh creds.
|
|
372
|
+
let connectPassword = resolved.xmppPassword;
|
|
373
|
+
if (config?.refreshTokens?.enabled && !config?.jwtLogin?.enabled) {
|
|
374
|
+
const rotated = await refreshAuthTokensQuietly();
|
|
375
|
+
if (cancelled) {return;}
|
|
376
|
+
if (rotated?.xmppPassword) {
|
|
377
|
+
connectPassword = rotated.xmppPassword;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
356
381
|
// Kick off REST /chats/my; allRoomPresences below needs the
|
|
357
382
|
// room list to be in redux, otherwise it joins 0 MUCs and the
|
|
358
383
|
// user receives zero realtime messages until they manually tap
|
|
@@ -362,7 +387,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
362
387
|
|
|
363
388
|
const c = await initializeClient(
|
|
364
389
|
resolved.xmppUsername!,
|
|
365
|
-
|
|
390
|
+
connectPassword,
|
|
366
391
|
config?.xmppSettings
|
|
367
392
|
);
|
|
368
393
|
if (cancelled) {return;}
|
|
@@ -4,6 +4,7 @@ import { initRoomsPresence } from './initRoomsPresence';
|
|
|
4
4
|
import XmppClient from '../networking/xmppClient';
|
|
5
5
|
import { IConfig, IRoom, User } from '../types/types';
|
|
6
6
|
import { store } from '../roomStore';
|
|
7
|
+
import { refreshAuthTokensQuietly } from '../networking/authRefresh';
|
|
7
8
|
|
|
8
9
|
const initXmppRooms = async (
|
|
9
10
|
user: User,
|
|
@@ -59,7 +60,10 @@ const initXmppRooms = async (
|
|
|
59
60
|
}
|
|
60
61
|
|
|
61
62
|
if (config?.refreshTokens?.enabled) {
|
|
62
|
-
|
|
63
|
+
// Was a bare call to the consumer-supplied refreshFunction, i.e. a
|
|
64
|
+
// rotation completely outside the SDK's lock. Routed through the
|
|
65
|
+
// one rotation point, which calls that same function internally.
|
|
66
|
+
refreshAuthTokensQuietly();
|
|
63
67
|
}
|
|
64
68
|
} catch (error) {
|
|
65
69
|
console.error(error);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { setBaseURL } from '../networking/apiClient';
|
|
2
|
+
import { refreshAuthTokens, isRefreshFatalError } from '../networking/authRefresh';
|
|
2
3
|
import { loginViaJwt } from '../networking/api-requests/auth.api';
|
|
3
4
|
import { getMyUser } from '../networking/api-requests/user.api';
|
|
4
5
|
import { IConfig, User } from '../types/types';
|
|
@@ -46,15 +47,109 @@ const normalizeUserForXmpp = (user?: User | null): User | null => {
|
|
|
46
47
|
return { ...user, xmppUsername: normalizedXmppUsername };
|
|
47
48
|
};
|
|
48
49
|
|
|
50
|
+
// Hermes has no atob/Buffer; hand-decode just enough base64url to read a
|
|
51
|
+
// JWT's `iat`. Claims are ASCII so the byte-per-char string is fine.
|
|
52
|
+
const B64_ALPHABET =
|
|
53
|
+
'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
|
|
54
|
+
|
|
55
|
+
const readJwtIat = (token?: string | null): number => {
|
|
56
|
+
if (!token || typeof token !== 'string') {return 0;}
|
|
57
|
+
const part = token.split('.')[1];
|
|
58
|
+
if (!part) {return 0;}
|
|
59
|
+
try {
|
|
60
|
+
const clean = part.replace(/-/g, '+').replace(/_/g, '/');
|
|
61
|
+
let out = '';
|
|
62
|
+
let buffer = 0;
|
|
63
|
+
let bits = 0;
|
|
64
|
+
for (let i = 0; i < clean.length; i++) {
|
|
65
|
+
const idx = B64_ALPHABET.indexOf(clean[i]);
|
|
66
|
+
if (idx === -1) {continue;}
|
|
67
|
+
buffer = (buffer << 6) | idx;
|
|
68
|
+
bits += 6;
|
|
69
|
+
if (bits >= 8) {
|
|
70
|
+
bits -= 8;
|
|
71
|
+
out += String.fromCharCode((buffer >> bits) & 0xff);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const iat = JSON.parse(out)?.iat;
|
|
75
|
+
return typeof iat === 'number' ? iat : 0;
|
|
76
|
+
} catch {
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* A `userLogin.user` is typically a snapshot the host captured at ITS
|
|
83
|
+
* login time and re-presents verbatim on every mount, while this SDK
|
|
84
|
+
* keeps rotating the session and persisting the newest pair to
|
|
85
|
+
* `ETHORA_USER`. Taking the snapshot as-is therefore discards the live
|
|
86
|
+
* session in favour of an ever-aging copy: media goes blank the moment
|
|
87
|
+
* the snapshot has no `fileToken` (secure-files URLs 401 without it),
|
|
88
|
+
* and once the snapshot's refresh token ages out entirely the account
|
|
89
|
+
* looks dead even though the SDK held a working session all along.
|
|
90
|
+
*
|
|
91
|
+
* So, for the SAME account only, prefer whichever refresh pair was
|
|
92
|
+
* minted later (JWT `iat` decides — a host that genuinely re-logged-in
|
|
93
|
+
* hands us newer tokens and wins), and fill in a missing `fileToken`
|
|
94
|
+
* from the persisted copy either way.
|
|
95
|
+
*/
|
|
96
|
+
const adoptFresherPersistedSession = async (candidate: User): Promise<User> => {
|
|
97
|
+
try {
|
|
98
|
+
const stored = await asyncLocalStorage<User>(
|
|
99
|
+
localStorageConstants.ETHORA_USER
|
|
100
|
+
).get();
|
|
101
|
+
if (!stored) {return candidate;}
|
|
102
|
+
|
|
103
|
+
const sameUser =
|
|
104
|
+
(candidate._id && stored._id && candidate._id === stored._id) ||
|
|
105
|
+
(candidate.xmppUsername &&
|
|
106
|
+
stored.xmppUsername &&
|
|
107
|
+
candidate.xmppUsername === stored.xmppUsername);
|
|
108
|
+
if (!sameUser) {return candidate;}
|
|
109
|
+
|
|
110
|
+
const merged = { ...candidate };
|
|
111
|
+
|
|
112
|
+
if (
|
|
113
|
+
stored.refreshToken &&
|
|
114
|
+
readJwtIat(stored.refreshToken) > readJwtIat(candidate.refreshToken)
|
|
115
|
+
) {
|
|
116
|
+
merged.refreshToken = stored.refreshToken;
|
|
117
|
+
if (stored.token) {merged.token = stored.token;}
|
|
118
|
+
if (stored.fileToken) {merged.fileToken = stored.fileToken;}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (!merged.fileToken && stored.fileToken) {
|
|
122
|
+
merged.fileToken = stored.fileToken;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
return merged;
|
|
126
|
+
} catch {
|
|
127
|
+
return candidate;
|
|
128
|
+
}
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Bootstrap rotation.
|
|
133
|
+
*
|
|
134
|
+
* Was a second, independent `/users/login/refresh` caller that bypassed
|
|
135
|
+
* every lock in the SDK — and, worse, only wrote the rotated token to
|
|
136
|
+
* the store on the happy path, so several of the early-return branches
|
|
137
|
+
* below used to drop it. Under the backend's reuse detection a dropped
|
|
138
|
+
* rotation means the next launch presents a burned token and the
|
|
139
|
+
* session is killed.
|
|
140
|
+
*
|
|
141
|
+
* `refreshAuthTokens` persists the new pair before it resolves, so by
|
|
142
|
+
* the time this returns the rotation is safe no matter which branch the
|
|
143
|
+
* caller takes afterwards. The explicit token is required here: during
|
|
144
|
+
* bootstrap the candidate session isn't in the store yet.
|
|
145
|
+
*/
|
|
49
146
|
const refreshWithToken = async (refreshToken: string) => {
|
|
50
|
-
const
|
|
51
|
-
'/users/login/refresh',
|
|
52
|
-
{},
|
|
53
|
-
{ headers: { Authorization: refreshToken } }
|
|
54
|
-
);
|
|
147
|
+
const result = await refreshAuthTokens({ refreshToken });
|
|
55
148
|
return {
|
|
56
|
-
token:
|
|
57
|
-
refreshToken:
|
|
149
|
+
token: result.token,
|
|
150
|
+
refreshToken: result.refreshToken || refreshToken,
|
|
151
|
+
xmppPassword: result.xmppPassword || '',
|
|
152
|
+
fileToken: result.fileToken || '',
|
|
58
153
|
};
|
|
59
154
|
};
|
|
60
155
|
|
|
@@ -87,11 +182,15 @@ const tryHydrateViaMy = async (
|
|
|
87
182
|
|
|
88
183
|
let workingToken = candidate?.token || '';
|
|
89
184
|
let workingRefresh = candidate?.refreshToken || '';
|
|
185
|
+
let rotatedXmppPassword = '';
|
|
186
|
+
let workingFileToken = candidate?.fileToken || '';
|
|
90
187
|
|
|
91
188
|
const candidateWithCurrentTokens = (): User => ({
|
|
92
189
|
...candidate,
|
|
93
190
|
token: workingToken || candidate.token,
|
|
94
191
|
refreshToken: workingRefresh || candidate.refreshToken,
|
|
192
|
+
xmppPassword: rotatedXmppPassword || candidate.xmppPassword,
|
|
193
|
+
fileToken: workingFileToken || candidate.fileToken,
|
|
95
194
|
});
|
|
96
195
|
|
|
97
196
|
const fallbackWithCreds = (): User | null => {
|
|
@@ -107,6 +206,8 @@ const tryHydrateViaMy = async (
|
|
|
107
206
|
if (merged) {
|
|
108
207
|
merged.token = workingToken || merged.token;
|
|
109
208
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
209
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
210
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
110
211
|
}
|
|
111
212
|
return merged;
|
|
112
213
|
} catch (error) {
|
|
@@ -127,6 +228,8 @@ const tryHydrateViaMy = async (
|
|
|
127
228
|
const refreshed = await refreshWithToken(workingRefresh);
|
|
128
229
|
workingToken = refreshed.token;
|
|
129
230
|
workingRefresh = refreshed.refreshToken;
|
|
231
|
+
rotatedXmppPassword = refreshed.xmppPassword || rotatedXmppPassword;
|
|
232
|
+
workingFileToken = refreshed.fileToken || workingFileToken;
|
|
130
233
|
|
|
131
234
|
try {
|
|
132
235
|
const myUser = await getMyUser({ token: workingToken, endpoint: myEndpoint });
|
|
@@ -136,6 +239,8 @@ const tryHydrateViaMy = async (
|
|
|
136
239
|
if (merged) {
|
|
137
240
|
merged.token = workingToken || merged.token;
|
|
138
241
|
merged.refreshToken = workingRefresh || merged.refreshToken;
|
|
242
|
+
merged.xmppPassword = rotatedXmppPassword || merged.xmppPassword;
|
|
243
|
+
merged.fileToken = workingFileToken || merged.fileToken;
|
|
139
244
|
}
|
|
140
245
|
return merged;
|
|
141
246
|
} catch (myError) {
|
|
@@ -147,6 +252,11 @@ const tryHydrateViaMy = async (
|
|
|
147
252
|
}
|
|
148
253
|
} catch (error) {
|
|
149
254
|
if (signal?.aborted || isAbortError(error)) {return null;}
|
|
255
|
+
// A fatal refresh verdict (reuse detected / token not found / stale
|
|
256
|
+
// with nothing newer around) carries no HTTP response, so it would
|
|
257
|
+
// otherwise fall through to `throw` — the session is simply dead,
|
|
258
|
+
// which for bootstrap means "nothing to restore".
|
|
259
|
+
if (isRefreshFatalError(error)) {return null;}
|
|
150
260
|
if (isAuthError(error)) {return null;}
|
|
151
261
|
throw error;
|
|
152
262
|
}
|
|
@@ -162,13 +272,15 @@ export const resolveInitBeforeLoadUser = async (
|
|
|
162
272
|
setBaseURL(config.baseUrl, config.customAppToken);
|
|
163
273
|
}
|
|
164
274
|
|
|
165
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
275
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
166
276
|
|
|
167
277
|
// Priority 1: explicit userLogin
|
|
168
278
|
const explicitUser = config?.userLogin?.enabled ? config?.userLogin?.user : null;
|
|
169
279
|
if (explicitUser) {
|
|
170
280
|
const candidate = normalizeUserForXmpp(explicitUser);
|
|
171
|
-
if (candidate && hasXmppCredentials(candidate)) {
|
|
281
|
+
if (candidate && hasXmppCredentials(candidate)) {
|
|
282
|
+
return await adoptFresherPersistedSession(candidate);
|
|
283
|
+
}
|
|
172
284
|
|
|
173
285
|
const hydrated = await tryHydrateViaMy(explicitUser, myEndpoint, signal).catch(
|
|
174
286
|
() => null
|
|
@@ -275,7 +387,7 @@ export const refreshUserCredentialsForXmpp = async (
|
|
|
275
387
|
}
|
|
276
388
|
}
|
|
277
389
|
|
|
278
|
-
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/users/my';
|
|
390
|
+
const myEndpoint = config?.initBeforeLoadAuth?.myEndpoint || '/v1/users/my';
|
|
279
391
|
|
|
280
392
|
// Priority 2: redux store user (covers userLogin + customLogin +
|
|
281
393
|
// anything previously persisted into chatSettingStore.user). The
|