@ethora/chat-component-rn 26.6.12 → 26.7.1
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 +12 -0
- package/README.md +2 -2
- package/lib/commonjs/components/AuthForms/Login.js +4 -4
- 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 +17 -6
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.js +9 -8
- package/lib/commonjs/components/MainComponents/ProfileImagePlaceholder.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 +8 -7
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.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/Modals/NewChatModal/ModalContent.js +2 -1
- package/lib/commonjs/components/Modals/NewChatModal/ModalContent.js.map +1 -1
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
- package/lib/commonjs/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.js +7 -21
- package/lib/commonjs/components/Modals/UserProfileModal/EditUserModal.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/styled/AudioMessage.js +2 -1
- package/lib/commonjs/components/styled/AudioMessage.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 +7 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/xmppProvider.js +46 -21
- package/lib/commonjs/context/xmppProvider.js.map +1 -1
- package/lib/commonjs/helpers/ensureScopedChatCache.js +2 -2
- package/lib/commonjs/helpers/ensureScopedChatCache.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/persistCrypto.js +122 -0
- package/lib/commonjs/helpers/persistCrypto.js.map +1 -0
- package/lib/commonjs/helpers/pickImageAsset.js +68 -0
- package/lib/commonjs/helpers/pickImageAsset.js.map +1 -0
- package/lib/commonjs/helpers/resolveInitBeforeLoadUser.js +130 -17
- 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/helpers/secureKeyValue.js +83 -0
- package/lib/commonjs/helpers/secureKeyValue.js.map +1 -0
- package/lib/commonjs/helpers/secureStoreRuntime.js +59 -0
- package/lib/commonjs/helpers/secureStoreRuntime.js.map +1 -0
- package/lib/commonjs/helpers/secureUserStorage.js +75 -0
- package/lib/commonjs/helpers/secureUserStorage.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/useLogout.js +15 -7
- package/lib/commonjs/hooks/useLogout.js.map +1 -1
- 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 +322 -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 +23 -6
- package/lib/commonjs/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/commonjs/roomStore/persistence.js +29 -15
- package/lib/commonjs/roomStore/persistence.js.map +1 -1
- package/lib/commonjs/roomStore/roomsSlice.js +2 -0
- package/lib/commonjs/roomStore/roomsSlice.js.map +1 -1
- package/lib/module/components/AuthForms/Login.js +4 -4
- 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 +17 -6
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.js +9 -8
- package/lib/module/components/MainComponents/ProfileImagePlaceholder.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 +8 -7
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.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/Modals/NewChatModal/ModalContent.js +2 -1
- package/lib/module/components/Modals/NewChatModal/ModalContent.js.map +1 -1
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js +8 -0
- package/lib/module/components/Modals/NewChatModal/UpdNewChatModal.js.map +1 -1
- package/lib/module/components/Modals/UserProfileModal/EditUserModal.js +9 -23
- package/lib/module/components/Modals/UserProfileModal/EditUserModal.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/styled/AudioMessage.js +2 -1
- package/lib/module/components/styled/AudioMessage.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 +7 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/xmppProvider.js +45 -21
- package/lib/module/context/xmppProvider.js.map +1 -1
- package/lib/module/helpers/ensureScopedChatCache.js +2 -2
- package/lib/module/helpers/ensureScopedChatCache.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/persistCrypto.js +115 -0
- package/lib/module/helpers/persistCrypto.js.map +1 -0
- package/lib/module/helpers/pickImageAsset.js +63 -0
- package/lib/module/helpers/pickImageAsset.js.map +1 -0
- package/lib/module/helpers/resolveInitBeforeLoadUser.js +130 -16
- 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/helpers/secureKeyValue.js +75 -0
- package/lib/module/helpers/secureKeyValue.js.map +1 -0
- package/lib/module/helpers/secureStoreRuntime.js +52 -0
- package/lib/module/helpers/secureStoreRuntime.js.map +1 -0
- package/lib/module/helpers/secureUserStorage.js +72 -0
- package/lib/module/helpers/secureUserStorage.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/useLogout.js +15 -7
- package/lib/module/hooks/useLogout.js.map +1 -1
- 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 +311 -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 +23 -6
- package/lib/module/roomStore/chatSettingsSlice.js.map +1 -1
- package/lib/module/roomStore/persistence.js +30 -15
- package/lib/module/roomStore/persistence.js.map +1 -1
- package/lib/module/roomStore/roomsSlice.js +2 -0
- package/lib/module/roomStore/roomsSlice.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/MainComponents/ProfileImagePlaceholder.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/Modals/NewChatModal/ModalContent.d.ts +4 -2
- package/lib/typescript/components/Modals/NewChatModal/ModalContent.d.ts.map +1 -1
- package/lib/typescript/components/Modals/NewChatModal/UpdNewChatModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/UserProfileModal/EditUserModal.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/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/persistCrypto.d.ts +13 -0
- package/lib/typescript/helpers/persistCrypto.d.ts.map +1 -0
- package/lib/typescript/helpers/pickImageAsset.d.ts +27 -0
- package/lib/typescript/helpers/pickImageAsset.d.ts.map +1 -0
- 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/helpers/secureKeyValue.d.ts +13 -0
- package/lib/typescript/helpers/secureKeyValue.d.ts.map +1 -0
- package/lib/typescript/helpers/secureStoreRuntime.d.ts +29 -0
- package/lib/typescript/helpers/secureStoreRuntime.d.ts.map +1 -0
- package/lib/typescript/helpers/secureUserStorage.d.ts +23 -0
- package/lib/typescript/helpers/secureUserStorage.d.ts.map +1 -0
- package/lib/typescript/hooks/useFileToken.d.ts +2 -0
- package/lib/typescript/hooks/useFileToken.d.ts.map +1 -0
- package/lib/typescript/hooks/useLogout.d.ts.map +1 -1
- 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/roomStore/persistence.d.ts.map +1 -1
- package/lib/typescript/roomStore/roomsSlice.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 +8 -1
- package/src/components/AuthForms/Login.tsx +3 -3
- 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 +14 -4
- package/src/components/MainComponents/ProfileImagePlaceholder.tsx +16 -9
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +4 -1
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +8 -7
- package/src/components/Modals/FilePreviewModal/PdfView.tsx +2 -1
- package/src/components/Modals/NewChatModal/ModalContent.tsx +6 -3
- package/src/components/Modals/NewChatModal/UpdNewChatModal.tsx +12 -1
- package/src/components/Modals/UserProfileModal/EditUserModal.tsx +9 -25
- 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/styled/AudioMessage.tsx +2 -1
- package/src/components/styled/MessageImage.tsx +45 -6
- package/src/components/styled/VideoMessage.tsx +10 -1
- package/src/context/xmppProvider.tsx +47 -23
- package/src/helpers/ensureScopedChatCache.ts +2 -2
- package/src/helpers/initXmppRooms.tsx +5 -1
- package/src/helpers/persistCrypto.ts +127 -0
- package/src/helpers/pickImageAsset.ts +68 -0
- package/src/helpers/resolveInitBeforeLoadUser.ts +125 -18
- package/src/helpers/secureFileUrl.ts +103 -0
- package/src/helpers/secureKeyValue.ts +80 -0
- package/src/helpers/secureStoreRuntime.ts +62 -0
- package/src/helpers/secureUserStorage.ts +80 -0
- package/src/hooks/useChatWrapperInit.ts +3 -3
- package/src/hooks/useFileToken.ts +7 -0
- package/src/hooks/useLogout.tsx +15 -9
- 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 +430 -0
- package/src/networking/xmppClient.ts +37 -5
- package/src/roomStore/chatSettingsSlice.ts +31 -9
- package/src/roomStore/persistence.ts +39 -19
- package/src/roomStore/roomsSlice.ts +2 -0
- package/src/types/types.ts +17 -0
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { useMemo } from 'react';
|
|
2
|
+
import { useFileToken } from '../../hooks/useFileToken';
|
|
3
|
+
import { appendFileToken } from '../../helpers/secureFileUrl';
|
|
2
4
|
import { IConfig, IRoom } from '../../types/types';
|
|
3
5
|
import { ProfileImagePlaceholder } from '../MainComponents/ProfileImagePlaceholder';
|
|
4
6
|
import {
|
|
@@ -26,6 +28,8 @@ const ChatRoomItem: React.FC<ChatRoomItemProps> = ({
|
|
|
26
28
|
isDriver,
|
|
27
29
|
config,
|
|
28
30
|
}) => {
|
|
31
|
+
// Room icons can live on the membership-gated secure-files host.
|
|
32
|
+
const fileToken = useFileToken();
|
|
29
33
|
const displayName = String(chat?.title || chat?.name || '').trim();
|
|
30
34
|
|
|
31
35
|
const lastMessage = useMemo(() => {
|
|
@@ -86,7 +90,11 @@ const ChatRoomItem: React.FC<ChatRoomItemProps> = ({
|
|
|
86
90
|
|
|
87
91
|
return (
|
|
88
92
|
<ChatItem key={index}>
|
|
89
|
-
<ProfileImagePlaceholder
|
|
93
|
+
<ProfileImagePlaceholder
|
|
94
|
+
name={displayName}
|
|
95
|
+
icon={appendFileToken(chat?.icon, fileToken)}
|
|
96
|
+
active={false}
|
|
97
|
+
/>
|
|
90
98
|
<View
|
|
91
99
|
style={{
|
|
92
100
|
flexDirection: 'column',
|
|
@@ -8,6 +8,12 @@ import {
|
|
|
8
8
|
} from './StyledRoomComponents';
|
|
9
9
|
import { FileIcon } from '../../../assets/icons';
|
|
10
10
|
import { Image, View } from 'react-native';
|
|
11
|
+
import { useFileToken } from '../../../hooks/useFileToken';
|
|
12
|
+
import {
|
|
13
|
+
appendFileToken,
|
|
14
|
+
isSecureFileUrl,
|
|
15
|
+
requestFileTokenRecovery,
|
|
16
|
+
} from '../../../helpers/secureFileUrl';
|
|
11
17
|
|
|
12
18
|
interface LastMessageFileProps
|
|
13
19
|
extends Pick<LastMessage, 'user' | 'originalName' | 'locationPreview'> {}
|
|
@@ -21,6 +27,7 @@ const LastMessageFile: FC<LastMessageFileProps> = ({
|
|
|
21
27
|
locationPreview,
|
|
22
28
|
}) => {
|
|
23
29
|
const [imgSrc, setImgSrc] = useState(locationPreview);
|
|
30
|
+
const fileToken = useFileToken();
|
|
24
31
|
|
|
25
32
|
return (
|
|
26
33
|
<LastRoomMessageContainer>
|
|
@@ -34,13 +41,18 @@ const LastMessageFile: FC<LastMessageFileProps> = ({
|
|
|
34
41
|
>
|
|
35
42
|
{imgSrc ? (
|
|
36
43
|
<Image
|
|
37
|
-
source={{ uri: imgSrc }}
|
|
44
|
+
source={{ uri: appendFileToken(imgSrc, fileToken) }}
|
|
38
45
|
style={{
|
|
39
46
|
borderRadius: 16,
|
|
40
47
|
width: 20,
|
|
41
48
|
height: 20,
|
|
42
49
|
}}
|
|
43
|
-
onError={() =>
|
|
50
|
+
onError={() => {
|
|
51
|
+
if (isSecureFileUrl(imgSrc)) {
|
|
52
|
+
requestFileTokenRecovery();
|
|
53
|
+
}
|
|
54
|
+
setImgSrc(fallbackImage);
|
|
55
|
+
}}
|
|
44
56
|
/>
|
|
45
57
|
) : (
|
|
46
58
|
<FileIcon style={{ width: '20px', height: '20px' }} />
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { FC } from 'react';
|
|
2
|
+
import { useFileToken } from '../../../hooks/useFileToken';
|
|
3
|
+
import { appendFileToken } from '../../../helpers/secureFileUrl';
|
|
2
4
|
import styled from 'styled-components/native';
|
|
3
5
|
import { LastMessage } from '../../../types/types';
|
|
4
6
|
import {
|
|
@@ -30,6 +32,7 @@ const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
|
|
|
30
32
|
locationPreview,
|
|
31
33
|
originalName,
|
|
32
34
|
}) => {
|
|
35
|
+
const fileToken = useFileToken();
|
|
33
36
|
return (
|
|
34
37
|
<LastRoomMessageContainer>
|
|
35
38
|
<LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
|
|
@@ -42,7 +45,7 @@ const LastMessagePhoto: FC<LastMessagePhotoProps> = ({
|
|
|
42
45
|
>
|
|
43
46
|
<PhotoContainer>
|
|
44
47
|
<ShadeWrapper>
|
|
45
|
-
<LastMessageImg src={locationPreview} />
|
|
48
|
+
<LastMessageImg src={appendFileToken(locationPreview, fileToken)} />
|
|
46
49
|
</ShadeWrapper>
|
|
47
50
|
</PhotoContainer>
|
|
48
51
|
<LastRoomMessageText>{originalName || 'file'}</LastRoomMessageText>
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import React, { FC } from 'react';
|
|
2
|
+
import { useFileToken } from '../../../hooks/useFileToken';
|
|
3
|
+
import { appendFileToken } from '../../../helpers/secureFileUrl';
|
|
2
4
|
import styled from 'styled-components/native';
|
|
3
5
|
import { LastMessage } from '../../../types/types';
|
|
4
6
|
import {
|
|
@@ -45,6 +47,7 @@ const LastMessageVideo: FC<LastMessageVideoProps> = ({
|
|
|
45
47
|
location,
|
|
46
48
|
originalName,
|
|
47
49
|
}) => {
|
|
50
|
+
const fileToken = useFileToken();
|
|
48
51
|
return (
|
|
49
52
|
<LastRoomMessageContainer>
|
|
50
53
|
<LastRoomMessageName>{user?.name || ''}:</LastRoomMessageName>
|
|
@@ -57,7 +60,7 @@ const LastMessageVideo: FC<LastMessageVideoProps> = ({
|
|
|
57
60
|
>
|
|
58
61
|
<VideoContainer>
|
|
59
62
|
<ShadeWrapper>
|
|
60
|
-
<Thumbnail src={location} />
|
|
63
|
+
<Thumbnail src={appendFileToken(location, fileToken)} />
|
|
61
64
|
</ShadeWrapper>
|
|
62
65
|
<PlayButton><Text>▶</Text></PlayButton>
|
|
63
66
|
</VideoContainer>
|
|
@@ -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
|
);
|
|
@@ -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,4 +1,8 @@
|
|
|
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
7
|
import { useDispatch } from 'react-redux';
|
|
4
8
|
import {
|
|
@@ -139,7 +143,12 @@ const CustomMessageVideo: React.FC<CustomMessageVideoProps> = ({
|
|
|
139
143
|
<PosterVideo
|
|
140
144
|
previewURL={previewURL}
|
|
141
145
|
onOpen={handleOpen}
|
|
142
|
-
onPosterError={() =>
|
|
146
|
+
onPosterError={() => {
|
|
147
|
+
if (isSecureFileUrl(previewURL)) {
|
|
148
|
+
requestFileTokenRecovery();
|
|
149
|
+
}
|
|
150
|
+
setPosterFailed(true);
|
|
151
|
+
}}
|
|
143
152
|
/>
|
|
144
153
|
);
|
|
145
154
|
}
|
|
@@ -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,
|
|
@@ -49,8 +49,7 @@ import {
|
|
|
49
49
|
setUnreadSyncing,
|
|
50
50
|
} from '../roomStore/roomsSlice';
|
|
51
51
|
import { runHistoryPreloadScheduler } from '../helpers/historyPreloadScheduler';
|
|
52
|
-
import {
|
|
53
|
-
import { localStorageConstants } from '../helpers/constants/LOCAL_STORAGE';
|
|
52
|
+
import { secureUserStorage } from '../helpers/secureUserStorage';
|
|
54
53
|
import { clearPersistedState } from '../roomStore/persistence';
|
|
55
54
|
import { pushLog as devPushLog } from '../utils/devLogger';
|
|
56
55
|
|
|
@@ -107,23 +106,25 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
107
106
|
// the XmppClient at least doesn't crash on `undefined.password`.
|
|
108
107
|
const credentialsProvider = useMemo<XmppCredentialsProvider>(() => {
|
|
109
108
|
return async () => {
|
|
110
|
-
// 1.
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
109
|
+
// 1. Make sure the REST tokens are fresh before re-minting XMPP
|
|
110
|
+
// creds. This used to call `config.refreshTokens.refreshFunction`
|
|
111
|
+
// directly and then fall into step 2, which refreshes again —
|
|
112
|
+
// two rotations back to back, which the new backend scheme
|
|
113
|
+
// reads as a race at best and token reuse at worst. One call to
|
|
114
|
+
// the shared rotation point covers both the consumer-supplied
|
|
115
|
+
// function and the built-in endpoint.
|
|
116
|
+
if (config?.refreshTokens?.enabled) {
|
|
117
|
+
const rotated = await refreshAuthTokensQuietly();
|
|
118
|
+
|
|
119
|
+
if (rotated?.xmppPassword) {
|
|
120
|
+
const rotatedUser = store.getState().chatSettingStore.user;
|
|
121
|
+
return {
|
|
122
|
+
username:
|
|
123
|
+
rotatedUser?.xmppUsername ||
|
|
124
|
+
rotatedUser?.defaultWallet?.walletAddress ||
|
|
125
|
+
'',
|
|
126
|
+
password: rotated.xmppPassword,
|
|
127
|
+
};
|
|
127
128
|
}
|
|
128
129
|
}
|
|
129
130
|
|
|
@@ -163,7 +164,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
163
164
|
}, [
|
|
164
165
|
config?.jwtLogin?.enabled,
|
|
165
166
|
config?.jwtLogin?.token,
|
|
166
|
-
config?.refreshTokens?.
|
|
167
|
+
config?.refreshTokens?.enabled,
|
|
167
168
|
config?.userLogin?.enabled,
|
|
168
169
|
config?.customLogin?.enabled,
|
|
169
170
|
config?.initBeforeLoadAuth?.myEndpoint,
|
|
@@ -322,7 +323,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
322
323
|
devPushLog('rn', 'initBeforeLoad: clearStoreBeforeInit');
|
|
323
324
|
store.dispatch(setLogoutState());
|
|
324
325
|
store.dispatch(logout());
|
|
325
|
-
await
|
|
326
|
+
await secureUserStorage().remove().catch(() => undefined);
|
|
326
327
|
}
|
|
327
328
|
|
|
328
329
|
await ensureScopedChatCache(config);
|
|
@@ -353,6 +354,29 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
353
354
|
store.dispatch(setConfig(config));
|
|
354
355
|
}
|
|
355
356
|
|
|
357
|
+
// Rotate BEFORE the first XMPP connect. On the rotating backend
|
|
358
|
+
// the xmpp password is a short-lived JWT that only the newest
|
|
359
|
+
// rotation knows, and each rotation invalidates its predecessors
|
|
360
|
+
// — so a snapshot's password (a host's cached `userLogin.user`,
|
|
361
|
+
// a persisted session) is usually already dead at boot. Without
|
|
362
|
+
// this, the first SASL attempt fails `not-authorized`, all three
|
|
363
|
+
// bootstrap retries re-present the same dead password, and the
|
|
364
|
+
// user dead-ends on the Connection error modal even though one
|
|
365
|
+
// rotation away a perfectly good session existed. The rotation
|
|
366
|
+
// also lands the fileToken pre-render, so secure media never
|
|
367
|
+
// paints blank first. Quiet + deduped; when it can't rotate
|
|
368
|
+
// (no refresh material, non-rotating backend) we just connect
|
|
369
|
+
// with what the snapshot has, exactly as before.
|
|
370
|
+
// jwtLogin is skipped: /users/client just minted fresh creds.
|
|
371
|
+
let connectPassword = resolved.xmppPassword;
|
|
372
|
+
if (config?.refreshTokens?.enabled && !config?.jwtLogin?.enabled) {
|
|
373
|
+
const rotated = await refreshAuthTokensQuietly();
|
|
374
|
+
if (cancelled) {return;}
|
|
375
|
+
if (rotated?.xmppPassword) {
|
|
376
|
+
connectPassword = rotated.xmppPassword;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
|
|
356
380
|
// Kick off REST /chats/my; allRoomPresences below needs the
|
|
357
381
|
// room list to be in redux, otherwise it joins 0 MUCs and the
|
|
358
382
|
// user receives zero realtime messages until they manually tap
|
|
@@ -362,7 +386,7 @@ export const XmppProvider: React.FC<XmppProviderProps> = ({ children, config, is
|
|
|
362
386
|
|
|
363
387
|
const c = await initializeClient(
|
|
364
388
|
resolved.xmppUsername!,
|
|
365
|
-
|
|
389
|
+
connectPassword,
|
|
366
390
|
config?.xmppSettings
|
|
367
391
|
);
|
|
368
392
|
if (cancelled) {return;}
|
|
@@ -3,7 +3,7 @@ import { store } from '../roomStore';
|
|
|
3
3
|
import { logout } from '../roomStore/chatSettingsSlice';
|
|
4
4
|
import { setLogoutState } from '../roomStore/roomsSlice';
|
|
5
5
|
import { asyncLocalStorage } from '../hooks/useLocalStorage';
|
|
6
|
-
import {
|
|
6
|
+
import { secureUserStorage } from './secureUserStorage';
|
|
7
7
|
import { clearRoomsRestCache } from '../networking/api-requests/rooms.api';
|
|
8
8
|
import { clearPersistedState } from '../roomStore/persistence';
|
|
9
9
|
import {
|
|
@@ -53,7 +53,7 @@ export async function ensureScopedChatCache(config?: IConfig): Promise<void> {
|
|
|
53
53
|
clearRoomsRestCache();
|
|
54
54
|
store.dispatch(setLogoutState());
|
|
55
55
|
store.dispatch(logout());
|
|
56
|
-
await
|
|
56
|
+
await secureUserStorage().remove();
|
|
57
57
|
await clearPersistedState();
|
|
58
58
|
}
|
|
59
59
|
|
|
@@ -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);
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import CryptoJS from 'crypto-js';
|
|
2
|
+
import { secureGet, secureSet } from './secureKeyValue';
|
|
3
|
+
|
|
4
|
+
// AES-256-CBC at rest for the bulk persisted state (message history +
|
|
5
|
+
// room list) that `roomStore/persistence.ts` writes to AsyncStorage.
|
|
6
|
+
// This is envelope encryption, not a SecureStore write of the payload
|
|
7
|
+
// itself: expo-secure-store has historically capped a single value's
|
|
8
|
+
// size on Android (chunking exists in newer releases, but the payload
|
|
9
|
+
// here is unbounded — up to MESSAGE_LIMIT messages per room, times
|
|
10
|
+
// every room), so only the small symmetric KEY goes through the
|
|
11
|
+
// platform Keychain/Keystore (`secureKeyValue.ts`); the ciphertext
|
|
12
|
+
// itself stays in plain AsyncStorage, unreadable without that key.
|
|
13
|
+
const CIPHER_KEY_STORE_KEY = 'ethora_history_cipher_key';
|
|
14
|
+
const ENVELOPE_VERSION = 1;
|
|
15
|
+
const KEY_BYTES = 32; // AES-256
|
|
16
|
+
const IV_BYTES = 16; // one AES block
|
|
17
|
+
|
|
18
|
+
interface Envelope {
|
|
19
|
+
v: number;
|
|
20
|
+
iv: string; // base64
|
|
21
|
+
ct: string; // base64 ciphertext
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* `crypto-js`'s own `WordArray.random()` falls back to `Math.random()`
|
|
26
|
+
* when it can't detect a browser `window.crypto` — not safe for key/IV
|
|
27
|
+
* material. `react-native-get-random-values` (a REQUIRED peer
|
|
28
|
+
* dependency, imported at the top of `main.ts`/`index.js` before
|
|
29
|
+
* anything else in the SDK runs) polyfills the real thing onto
|
|
30
|
+
* `global.crypto.getRandomValues` instead; source bytes from that and
|
|
31
|
+
* hand them to crypto-js as a WordArray rather than trust its default.
|
|
32
|
+
*/
|
|
33
|
+
const randomWordArray = (byteLength: number): CryptoJS.lib.WordArray => {
|
|
34
|
+
const bytes = new Uint8Array(byteLength);
|
|
35
|
+
(global as any).crypto.getRandomValues(bytes);
|
|
36
|
+
const words: number[] = [];
|
|
37
|
+
for (let i = 0; i < bytes.length; i += 4) {
|
|
38
|
+
words.push(
|
|
39
|
+
((bytes[i] || 0) << 24) |
|
|
40
|
+
((bytes[i + 1] || 0) << 16) |
|
|
41
|
+
((bytes[i + 2] || 0) << 8) |
|
|
42
|
+
(bytes[i + 3] || 0)
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
return CryptoJS.lib.WordArray.create(words, byteLength);
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
let cachedKeyPromise: Promise<CryptoJS.lib.WordArray> | null = null;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* One AES-256 key per install, generated on first use and persisted
|
|
52
|
+
* through the SecureStore/Keychain bridge. Reused across logins on
|
|
53
|
+
* purpose: the ciphertext it protects is deleted wholesale on logout
|
|
54
|
+
* (`clearPersistedState`), so there is no cross-account data for a
|
|
55
|
+
* stable key to expose — only the encrypted blob of whoever is
|
|
56
|
+
* currently logged in ever exists on disk at a given time.
|
|
57
|
+
*/
|
|
58
|
+
const loadOrCreateKey = (): Promise<CryptoJS.lib.WordArray> => {
|
|
59
|
+
if (cachedKeyPromise) {return cachedKeyPromise;}
|
|
60
|
+
cachedKeyPromise = (async () => {
|
|
61
|
+
const existing = await secureGet(CIPHER_KEY_STORE_KEY);
|
|
62
|
+
if (existing) {
|
|
63
|
+
return CryptoJS.enc.Base64.parse(existing);
|
|
64
|
+
}
|
|
65
|
+
const key = randomWordArray(KEY_BYTES);
|
|
66
|
+
await secureSet(CIPHER_KEY_STORE_KEY, CryptoJS.enc.Base64.stringify(key));
|
|
67
|
+
return key;
|
|
68
|
+
})();
|
|
69
|
+
return cachedKeyPromise;
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
/** Encrypt a JSON-serialisable payload for storage. Returns the string to persist. */
|
|
73
|
+
export async function encryptForPersist(plaintext: string): Promise<string> {
|
|
74
|
+
const key = await loadOrCreateKey();
|
|
75
|
+
const iv = randomWordArray(IV_BYTES);
|
|
76
|
+
const encrypted = CryptoJS.AES.encrypt(plaintext, key, {
|
|
77
|
+
iv,
|
|
78
|
+
mode: CryptoJS.mode.CBC,
|
|
79
|
+
padding: CryptoJS.pad.Pkcs7,
|
|
80
|
+
});
|
|
81
|
+
const envelope: Envelope = {
|
|
82
|
+
v: ENVELOPE_VERSION,
|
|
83
|
+
iv: CryptoJS.enc.Base64.stringify(iv),
|
|
84
|
+
ct: encrypted.ciphertext.toString(CryptoJS.enc.Base64),
|
|
85
|
+
};
|
|
86
|
+
return JSON.stringify(envelope);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Decrypt a value previously written by `encryptForPersist`. Returns
|
|
91
|
+
* null (never throws) for anything that isn't a valid envelope for the
|
|
92
|
+
* current key — including a plaintext blob from a pre-encryption
|
|
93
|
+
* install: the app just re-hydrates that room/history from the server,
|
|
94
|
+
* same as any other empty-cache cold start.
|
|
95
|
+
*/
|
|
96
|
+
export async function decryptFromPersist(
|
|
97
|
+
stored: string
|
|
98
|
+
): Promise<string | null> {
|
|
99
|
+
try {
|
|
100
|
+
const envelope = JSON.parse(stored) as Partial<Envelope>;
|
|
101
|
+
if (
|
|
102
|
+
!envelope ||
|
|
103
|
+
envelope.v !== ENVELOPE_VERSION ||
|
|
104
|
+
!envelope.iv ||
|
|
105
|
+
!envelope.ct
|
|
106
|
+
) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
const key = await loadOrCreateKey();
|
|
110
|
+
const iv = CryptoJS.enc.Base64.parse(envelope.iv);
|
|
111
|
+
const ciphertext = CryptoJS.enc.Base64.parse(envelope.ct);
|
|
112
|
+
const decrypted = CryptoJS.AES.decrypt(
|
|
113
|
+
{ ciphertext } as CryptoJS.lib.CipherParams,
|
|
114
|
+
key,
|
|
115
|
+
{ iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7 }
|
|
116
|
+
);
|
|
117
|
+
const plaintext = decrypted.toString(CryptoJS.enc.Utf8);
|
|
118
|
+
return plaintext || null;
|
|
119
|
+
} catch {
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Test seam only. */
|
|
125
|
+
export const __resetPersistCryptoKeyForTests = (): void => {
|
|
126
|
+
cachedKeyPromise = null;
|
|
127
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Alert, Linking } from 'react-native';
|
|
2
|
+
import * as ImagePicker from 'expo-image-picker';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* A picked image in the shape React Native's FormData understands.
|
|
6
|
+
*
|
|
7
|
+
* NOT a web `File`: RN streams a multipart part straight from a `file://`
|
|
8
|
+
* uri and has no usable Blob/File/atob path. Screens ported from the web
|
|
9
|
+
* client used to carry base64 -> Blob -> File helpers that could never
|
|
10
|
+
* have run here.
|
|
11
|
+
*/
|
|
12
|
+
export interface PickedImage {
|
|
13
|
+
uri: string;
|
|
14
|
+
type: string;
|
|
15
|
+
name: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Ask for library permission, open the picker, and normalise the result.
|
|
20
|
+
*
|
|
21
|
+
* Returns `null` for every non-error outcome — permission refused, user
|
|
22
|
+
* cancelled — so call sites can simply keep their current value.
|
|
23
|
+
*
|
|
24
|
+
* Exists because the avatar pickers are wired to `onPress`, which hands
|
|
25
|
+
* the callback a SyntheticEvent. Storing that event as "the image" (what
|
|
26
|
+
* two screens used to do) rendered a pooled event as an <Image source>
|
|
27
|
+
* and then crashed with "Property is not configurable" when React tried
|
|
28
|
+
* to release it — on top of never letting the user pick anything.
|
|
29
|
+
*/
|
|
30
|
+
export const pickImageAsset = async (): Promise<PickedImage | null> => {
|
|
31
|
+
try {
|
|
32
|
+
// expo-image-picker owns the permission prompt internally and
|
|
33
|
+
// returns a `granted` flag.
|
|
34
|
+
const perm = await ImagePicker.requestMediaLibraryPermissionsAsync();
|
|
35
|
+
if (!perm.granted) {
|
|
36
|
+
Alert.alert(
|
|
37
|
+
'Permission required',
|
|
38
|
+
'Photo library permission is needed to select images.',
|
|
39
|
+
[
|
|
40
|
+
{ text: 'Cancel', style: 'cancel' },
|
|
41
|
+
{ text: 'Open Settings', onPress: () => Linking.openSettings() },
|
|
42
|
+
]
|
|
43
|
+
);
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const result = await ImagePicker.launchImageLibraryAsync({
|
|
48
|
+
mediaTypes: ['images'],
|
|
49
|
+
allowsEditing: true,
|
|
50
|
+
aspect: [1, 1],
|
|
51
|
+
quality: 0.8,
|
|
52
|
+
});
|
|
53
|
+
if (result.canceled || !result.assets?.[0]) {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const asset = result.assets[0];
|
|
58
|
+
const originalName = asset.fileName || asset.uri.split('/').pop();
|
|
59
|
+
return {
|
|
60
|
+
uri: asset.uri,
|
|
61
|
+
type: asset.mimeType || 'image/jpeg',
|
|
62
|
+
name: originalName || `image_${Date.now()}.jpg`,
|
|
63
|
+
};
|
|
64
|
+
} catch (error) {
|
|
65
|
+
console.error('Failed to pick an image:', error);
|
|
66
|
+
return null;
|
|
67
|
+
}
|
|
68
|
+
};
|