@ethora/chat-component-rn 26.10.8 → 26.10.10
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 +7 -0
- package/README.md +1 -1
- package/lib/commonjs/components/MainComponents/AlbumGrid.js +6 -2
- package/lib/commonjs/components/MainComponents/AlbumGrid.js.map +1 -1
- package/lib/commonjs/components/MainComponents/ChatHeader.js +16 -11
- package/lib/commonjs/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/commonjs/components/MainComponents/MediaMessage.js +13 -0
- package/lib/commonjs/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/commonjs/components/MainComponents/RoomList.js +7 -16
- package/lib/commonjs/components/MainComponents/RoomList.js.map +1 -1
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/commonjs/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/commonjs/components/MessageBubble/Message.js +27 -1
- package/lib/commonjs/components/MessageBubble/Message.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js +159 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js +203 -134
- package/lib/commonjs/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js +180 -0
- package/lib/commonjs/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -162
- package/lib/commonjs/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js +824 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js +179 -0
- package/lib/commonjs/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
- package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js +22 -5
- package/lib/commonjs/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
- package/lib/commonjs/components/VideoCalls/useCallKeep.js +1 -3
- package/lib/commonjs/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/commonjs/components/styled/MessageImage.js +4 -1
- package/lib/commonjs/components/styled/MessageImage.js.map +1 -1
- package/lib/commonjs/components/styled/VideoMessage.js +5 -1
- package/lib/commonjs/components/styled/VideoMessage.js.map +1 -1
- package/lib/commonjs/context/MediaSourceContext.js +18 -0
- package/lib/commonjs/context/MediaSourceContext.js.map +1 -0
- package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js +3 -1
- package/lib/commonjs/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
- package/lib/commonjs/helpers/constants/MODAL_TYPES.js +2 -0
- package/lib/commonjs/helpers/constants/MODAL_TYPES.js.map +1 -1
- package/lib/commonjs/helpers/publicChatsEntry.js +8 -6
- package/lib/commonjs/helpers/publicChatsEntry.js.map +1 -1
- package/lib/commonjs/helpers/roomMedia.js +118 -0
- package/lib/commonjs/helpers/roomMedia.js.map +1 -0
- package/lib/commonjs/i18n/strings.js +36 -0
- package/lib/commonjs/i18n/strings.js.map +1 -1
- package/lib/commonjs/networking/api-requests/rooms.api.js +9 -13
- package/lib/commonjs/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/commonjs/networking/callTokenStanza.js +13 -1
- package/lib/commonjs/networking/callTokenStanza.js.map +1 -1
- package/lib/commonjs/push/sealedPushTask.js +20 -1
- package/lib/commonjs/push/sealedPushTask.js.map +1 -1
- package/lib/commonjs/version.js +1 -1
- package/lib/commonjs/version.js.map +1 -1
- package/lib/module/components/MainComponents/AlbumGrid.js +6 -2
- package/lib/module/components/MainComponents/AlbumGrid.js.map +1 -1
- package/lib/module/components/MainComponents/ChatHeader.js +18 -13
- package/lib/module/components/MainComponents/ChatHeader.js.map +1 -1
- package/lib/module/components/MainComponents/MediaMessage.js +13 -0
- package/lib/module/components/MainComponents/MediaMessage.js.map +1 -1
- package/lib/module/components/MainComponents/RoomList.js +7 -16
- package/lib/module/components/MainComponents/RoomList.js.map +1 -1
- package/lib/module/components/Menu/HeaderRoomListMenu.js +3 -3
- package/lib/module/components/Menu/HeaderRoomListMenu.js.map +1 -1
- package/lib/module/components/MessageBubble/Message.js +27 -1
- package/lib/module/components/MessageBubble/Message.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js +154 -0
- package/lib/module/components/Modals/ChatProfileModal/ChatMediaGrid.js.map +1 -0
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js +204 -136
- package/lib/module/components/Modals/ChatProfileModal/ChatProfileModal.js.map +1 -1
- package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js +175 -0
- package/lib/module/components/Modals/ChatProfileModal/LeaveChatModal.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js +100 -163
- package/lib/module/components/Modals/FilePreviewModal/FilePreviewModal.js.map +1 -1
- package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js +819 -0
- package/lib/module/components/Modals/FilePreviewModal/MediaViewer.js.map +1 -0
- package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js +172 -0
- package/lib/module/components/Modals/FilePreviewModal/useSaveFile.js.map +1 -0
- package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js +20 -4
- package/lib/module/components/Modals/MessageSearchModal/MessageSearchModal.js.map +1 -1
- package/lib/module/components/VideoCalls/useCallKeep.js +2 -4
- package/lib/module/components/VideoCalls/useCallKeep.js.map +1 -1
- package/lib/module/components/styled/MessageImage.js +4 -1
- package/lib/module/components/styled/MessageImage.js.map +1 -1
- package/lib/module/components/styled/VideoMessage.js +5 -1
- package/lib/module/components/styled/VideoMessage.js.map +1 -1
- package/lib/module/context/MediaSourceContext.js +13 -0
- package/lib/module/context/MediaSourceContext.js.map +1 -0
- package/lib/module/helpers/constants/MODAL_COMPONENTS.js +2 -1
- package/lib/module/helpers/constants/MODAL_COMPONENTS.js.map +1 -1
- package/lib/module/helpers/constants/MODAL_TYPES.js +2 -0
- package/lib/module/helpers/constants/MODAL_TYPES.js.map +1 -1
- package/lib/module/helpers/publicChatsEntry.js +8 -6
- package/lib/module/helpers/publicChatsEntry.js.map +1 -1
- package/lib/module/helpers/roomMedia.js +112 -0
- package/lib/module/helpers/roomMedia.js.map +1 -0
- package/lib/module/i18n/strings.js +36 -0
- package/lib/module/i18n/strings.js.map +1 -1
- package/lib/module/networking/api-requests/rooms.api.js +9 -13
- package/lib/module/networking/api-requests/rooms.api.js.map +1 -1
- package/lib/module/networking/callTokenStanza.js +13 -1
- package/lib/module/networking/callTokenStanza.js.map +1 -1
- package/lib/module/push/sealedPushTask.js +20 -1
- package/lib/module/push/sealedPushTask.js.map +1 -1
- package/lib/module/version.js +1 -1
- package/lib/module/version.js.map +1 -1
- package/lib/typescript/components/MainComponents/AlbumGrid.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/ChatHeader.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts +3 -1
- package/lib/typescript/components/MainComponents/MediaMessage.d.ts.map +1 -1
- package/lib/typescript/components/MainComponents/RoomList.d.ts.map +1 -1
- package/lib/typescript/components/Menu/HeaderRoomListMenu.d.ts.map +1 -1
- package/lib/typescript/components/MessageBubble/Message.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts +16 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatMediaGrid.d.ts.map +1 -0
- package/lib/typescript/components/Modals/ChatProfileModal/ChatProfileModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts +18 -0
- package/lib/typescript/components/Modals/ChatProfileModal/LeaveChatModal.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts +16 -0
- package/lib/typescript/components/Modals/FilePreviewModal/FilePreviewModal.d.ts.map +1 -1
- package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts +27 -0
- package/lib/typescript/components/Modals/FilePreviewModal/MediaViewer.d.ts.map +1 -0
- package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts +9 -0
- package/lib/typescript/components/Modals/FilePreviewModal/useSaveFile.d.ts.map +1 -0
- package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts +8 -0
- package/lib/typescript/components/Modals/MessageSearchModal/MessageSearchModal.d.ts.map +1 -1
- package/lib/typescript/components/VideoCalls/useCallKeep.d.ts.map +1 -1
- 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/MediaSourceContext.d.ts +4 -0
- package/lib/typescript/context/MediaSourceContext.d.ts.map +1 -0
- package/lib/typescript/helpers/constants/MODAL_COMPONENTS.d.ts.map +1 -1
- package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts +2 -0
- package/lib/typescript/helpers/constants/MODAL_TYPES.d.ts.map +1 -1
- package/lib/typescript/helpers/publicChatsEntry.d.ts +7 -5
- package/lib/typescript/helpers/publicChatsEntry.d.ts.map +1 -1
- package/lib/typescript/helpers/roomMedia.d.ts +26 -0
- package/lib/typescript/helpers/roomMedia.d.ts.map +1 -0
- package/lib/typescript/i18n/strings.d.ts.map +1 -1
- package/lib/typescript/main.d.ts +1 -1
- package/lib/typescript/main.d.ts.map +1 -1
- package/lib/typescript/networking/api-requests/rooms.api.d.ts +6 -5
- package/lib/typescript/networking/api-requests/rooms.api.d.ts.map +1 -1
- package/lib/typescript/networking/callTokenStanza.d.ts.map +1 -1
- package/lib/typescript/push/sealedPushTask.d.ts +13 -0
- package/lib/typescript/push/sealedPushTask.d.ts.map +1 -1
- package/lib/typescript/types/models/modal.model.d.ts +18 -0
- package/lib/typescript/types/models/modal.model.d.ts.map +1 -1
- package/lib/typescript/types/types.d.ts +18 -0
- package/lib/typescript/types/types.d.ts.map +1 -1
- package/lib/typescript/version.d.ts +1 -1
- package/lib/typescript/version.d.ts.map +1 -1
- package/package.json +9 -2
- package/plugins/notification-service/NotificationService.swift +105 -19
- package/plugins/withNotificationServiceExtension.js +44 -4
- package/src/components/MainComponents/AlbumGrid.tsx +5 -1
- package/src/components/MainComponents/ChatHeader.tsx +16 -11
- package/src/components/MainComponents/MediaMessage.tsx +13 -2
- package/src/components/MainComponents/RoomList.tsx +8 -19
- package/src/components/Menu/HeaderRoomListMenu.tsx +13 -8
- package/src/components/MessageBubble/Message.tsx +24 -1
- package/src/components/Modals/ChatProfileModal/ChatMediaGrid.tsx +174 -0
- package/src/components/Modals/ChatProfileModal/ChatProfileModal.tsx +119 -48
- package/src/components/Modals/ChatProfileModal/LeaveChatModal.tsx +191 -0
- package/src/components/Modals/FilePreviewModal/FilePreviewModal.tsx +115 -185
- package/src/components/Modals/FilePreviewModal/MediaViewer.tsx +880 -0
- package/src/components/Modals/FilePreviewModal/useSaveFile.ts +207 -0
- package/src/components/Modals/MessageSearchModal/MessageSearchModal.tsx +56 -30
- package/src/components/VideoCalls/useCallKeep.ts +2 -4
- package/src/components/styled/MessageImage.tsx +3 -1
- package/src/components/styled/VideoMessage.tsx +5 -1
- package/src/context/MediaSourceContext.tsx +14 -0
- package/src/helpers/constants/MODAL_COMPONENTS.tsx +4 -1
- package/src/helpers/constants/MODAL_TYPES.ts +2 -0
- package/src/helpers/publicChatsEntry.ts +8 -7
- package/src/helpers/roomMedia.ts +126 -0
- package/src/i18n/strings.ts +36 -0
- package/src/main.ts +1 -1
- package/src/networking/api-requests/rooms.api.ts +13 -12
- package/src/networking/callTokenStanza.ts +10 -1
- package/src/push/sealedPushTask.ts +34 -1
- package/src/types/models/modal.model.ts +16 -0
- package/src/types/types.ts +16 -0
- package/src/version.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
|
1
|
+
import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
|
2
2
|
import { withFileToken } from '../../../helpers/secureFileUrl';
|
|
3
3
|
import styled from 'styled-components/native';
|
|
4
4
|
import {
|
|
@@ -10,26 +10,28 @@ import ModalHeaderComponent from '../ModalHeaderComponent';
|
|
|
10
10
|
import { useChatSettingState } from '../../../hooks/useChatSettingState';
|
|
11
11
|
import { useTheme } from '../../../hooks/useTheme';
|
|
12
12
|
import { chatTextStyle } from '../../../helpers/typography';
|
|
13
|
-
import { useDispatch, useSelector } from 'react-redux';
|
|
13
|
+
import { useDispatch, useSelector, useStore } from 'react-redux';
|
|
14
14
|
import Button from '../../styled/Button';
|
|
15
15
|
import { RootState } from '../../../roomStore';
|
|
16
16
|
import { FullScreenImage } from '../../styled/StyledInputComponents/MediaComponents';
|
|
17
17
|
import { setActiveFile } from '../../../roomStore/chatSettingsSlice';
|
|
18
|
+
import type { ModalFile } from '../../../types/types';
|
|
19
|
+
import MediaViewer from './MediaViewer';
|
|
20
|
+
import { isLocalFile, useSaveFile } from './useSaveFile';
|
|
21
|
+
import { withChatSiblings } from '../../../helpers/roomMedia';
|
|
22
|
+
import { loadRoomHistory } from '../../../helpers/loadRoomHistory';
|
|
23
|
+
import { getGlobalXmppClient } from '../../../utils/clientRegistry';
|
|
24
|
+
import { useFileToken } from '../../../hooks/useFileToken';
|
|
18
25
|
import {
|
|
19
26
|
ActivityIndicator,
|
|
20
|
-
Alert,
|
|
21
27
|
Text,
|
|
22
28
|
View,
|
|
23
29
|
Platform,
|
|
24
30
|
TouchableOpacity,
|
|
25
31
|
StyleSheet,
|
|
26
|
-
Share,
|
|
27
32
|
} from 'react-native';
|
|
28
33
|
import { useVideoPlayer, VideoView } from 'expo-video';
|
|
29
34
|
import { PlayIcon } from '../../../assets/icons';
|
|
30
|
-
import * as FileSystem from 'expo-file-system/legacy';
|
|
31
|
-
import { getMediaLibrary } from '../../../helpers/mediaLibraryRuntime';
|
|
32
|
-
import { useToast } from '../../../context/ToastContext';
|
|
33
35
|
import { useT } from '../../../i18n/useT';
|
|
34
36
|
import PdfViewer from './PdfView';
|
|
35
37
|
import DocumentViewer from './DocumentViewer';
|
|
@@ -44,7 +46,6 @@ import {
|
|
|
44
46
|
} from '../../../helpers/videoCache';
|
|
45
47
|
import {
|
|
46
48
|
getDisplayFileName,
|
|
47
|
-
getUniqueFileName,
|
|
48
49
|
sanitizeFileNameForPath,
|
|
49
50
|
} from '../../../helpers/getDisplayFileName';
|
|
50
51
|
|
|
@@ -62,13 +63,12 @@ const GVIEW_PREVIEWABLE = new Set<string>([
|
|
|
62
63
|
'application/rtf',
|
|
63
64
|
]);
|
|
64
65
|
|
|
65
|
-
const
|
|
66
|
-
|
|
66
|
+
const isMedia = (mime: string | undefined | null) =>
|
|
67
|
+
!!mime && (mime.startsWith('image/') || mime.startsWith('video/'));
|
|
67
68
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
: FileSystem.downloadAsync(withFileToken(url), cachePath);
|
|
69
|
+
/** Older history pages the viewer asked for; one at a time per room. */
|
|
70
|
+
const historyInFlight = new Set<string>();
|
|
71
|
+
const HISTORY_PAGE_SIZE = 30;
|
|
72
72
|
|
|
73
73
|
const isGviewPreviewable = (mime: string | undefined | null) => {
|
|
74
74
|
if (!mime) {return false;}
|
|
@@ -246,11 +246,94 @@ interface FilePreviewModalProps {
|
|
|
246
246
|
handleCloseModal: any;
|
|
247
247
|
}
|
|
248
248
|
|
|
249
|
+
/** The viewer on the store's `activeFile`, as opened from a message bubble. */
|
|
249
250
|
const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
250
251
|
handleCloseModal,
|
|
251
252
|
}) => {
|
|
252
253
|
const dispatch = useDispatch();
|
|
253
|
-
const
|
|
254
|
+
const store = useStore<RootState>();
|
|
255
|
+
const { activeFile } = useSelector(
|
|
256
|
+
(state: RootState) => state.chatSettingStore
|
|
257
|
+
);
|
|
258
|
+
const { user, config } = useChatSettingState();
|
|
259
|
+
const fileToken = useFileToken();
|
|
260
|
+
const roomJid = activeFile?.source?.roomJid;
|
|
261
|
+
const room = useSelector((state: RootState) =>
|
|
262
|
+
roomJid ? state.rooms.rooms?.[roomJid] : undefined
|
|
263
|
+
);
|
|
264
|
+
const messages = room?.messages;
|
|
265
|
+
|
|
266
|
+
// A picture or video opened from a bubble pages through the whole chat;
|
|
267
|
+
// anything not in the loaded history keeps what it was opened with.
|
|
268
|
+
const file = useMemo(() => {
|
|
269
|
+
if (!activeFile?.fileURL || !isMedia(activeFile.mimetype)) {return activeFile;}
|
|
270
|
+
return (
|
|
271
|
+
withChatSiblings(activeFile, messages, {
|
|
272
|
+
me: user,
|
|
273
|
+
fileToken,
|
|
274
|
+
interactive: !config?.disableInteractions,
|
|
275
|
+
repliesOn: !config?.disableReplies,
|
|
276
|
+
}) ?? activeFile
|
|
277
|
+
);
|
|
278
|
+
}, [activeFile, messages, user, fileToken, config?.disableInteractions, config?.disableReplies]);
|
|
279
|
+
|
|
280
|
+
// On the oldest loaded file, ask for an older page: what it brings in
|
|
281
|
+
// shows up to the left of it.
|
|
282
|
+
const atOldest = !!file?.siblings && file.index === 0 && !!roomJid;
|
|
283
|
+
const historyComplete = !!room?.historyComplete;
|
|
284
|
+
useEffect(() => {
|
|
285
|
+
if (!atOldest || historyComplete || !roomJid) {return;}
|
|
286
|
+
loadRoomHistory({
|
|
287
|
+
client: getGlobalXmppClient(),
|
|
288
|
+
room: store.getState().rooms.rooms?.[roomJid],
|
|
289
|
+
inFlight: historyInFlight,
|
|
290
|
+
chatJID: roomJid,
|
|
291
|
+
max: HISTORY_PAGE_SIZE,
|
|
292
|
+
});
|
|
293
|
+
}, [atOldest, historyComplete, roomJid, store]);
|
|
294
|
+
|
|
295
|
+
if (!file?.fileURL) {return null;}
|
|
296
|
+
|
|
297
|
+
const closeModal = () => {
|
|
298
|
+
dispatch(
|
|
299
|
+
setActiveFile({
|
|
300
|
+
fileName: '',
|
|
301
|
+
fileURL: '',
|
|
302
|
+
mimetype: '',
|
|
303
|
+
})
|
|
304
|
+
);
|
|
305
|
+
handleCloseModal?.();
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
return (
|
|
309
|
+
<FilePreview
|
|
310
|
+
file={file}
|
|
311
|
+
onClose={closeModal}
|
|
312
|
+
onChangeFile={(next) => dispatch(setActiveFile(next))}
|
|
313
|
+
/>
|
|
314
|
+
);
|
|
315
|
+
};
|
|
316
|
+
|
|
317
|
+
interface FilePreviewProps {
|
|
318
|
+
file: ModalFile;
|
|
319
|
+
onClose: () => void;
|
|
320
|
+
/** Another file of the album (siblings and index included). */
|
|
321
|
+
onChangeFile: (file: ModalFile) => void;
|
|
322
|
+
/** After the media viewer's Reply opened a thread. */
|
|
323
|
+
onReplyOpened?: () => void;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* One file, full screen: pictures and videos in the Telegram-style
|
|
328
|
+
* MediaViewer, everything else (documents, PDFs, voice notes) on the
|
|
329
|
+
* plain preview page with the save button in its header.
|
|
330
|
+
*/
|
|
331
|
+
export const FilePreview: React.FC<FilePreviewProps> = ({
|
|
332
|
+
file: activeFile,
|
|
333
|
+
onClose,
|
|
334
|
+
onChangeFile,
|
|
335
|
+
onReplyOpened,
|
|
336
|
+
}) => {
|
|
254
337
|
const t = useT();
|
|
255
338
|
const { config } = useChatSettingState();
|
|
256
339
|
const theme = useTheme();
|
|
@@ -258,16 +341,6 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
258
341
|
// dark sits them on the secondary surface.
|
|
259
342
|
const cardBackground = theme.dark ? theme.surfaceSecondary : '#FFF8ED';
|
|
260
343
|
|
|
261
|
-
// Cache the SAF directory the user granted so saving several documents
|
|
262
|
-
// in a row doesn't re-prompt for a folder every time (Android only).
|
|
263
|
-
const safDirUriRef = useRef<string | null>(null);
|
|
264
|
-
|
|
265
|
-
const { activeFile } = useSelector(
|
|
266
|
-
(state: RootState) => state.chatSettingStore
|
|
267
|
-
);
|
|
268
|
-
|
|
269
|
-
if (!activeFile) {return null;}
|
|
270
|
-
|
|
271
344
|
// Bug #40: prefer the sender's original name over the server's stored
|
|
272
345
|
// hash name everywhere this modal shows or saves/shares the file.
|
|
273
346
|
// Sanitized so it's safe to use as an actual path segment (strips path
|
|
@@ -281,154 +354,8 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
281
354
|
})
|
|
282
355
|
);
|
|
283
356
|
|
|
284
|
-
const
|
|
285
|
-
|
|
286
|
-
if (!MediaLibrary?.requestPermissionsAsync) {return false;}
|
|
287
|
-
try {
|
|
288
|
-
const { status } = await MediaLibrary.requestPermissionsAsync();
|
|
289
|
-
return status === 'granted';
|
|
290
|
-
} catch (error) {
|
|
291
|
-
console.error('Error requesting permission:', error);
|
|
292
|
-
return false;
|
|
293
|
-
}
|
|
294
|
-
};
|
|
295
|
-
|
|
296
|
-
const saveToGallery = async () => {
|
|
297
|
-
const hasPermission = await requestStoragePermission();
|
|
298
|
-
if (!hasPermission) {
|
|
299
|
-
Alert.alert(
|
|
300
|
-
'Permission Denied',
|
|
301
|
-
'Storage permission is required to save files to the gallery.'
|
|
302
|
-
);
|
|
303
|
-
return;
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
try {
|
|
307
|
-
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
308
|
-
const download = await fetchToCache(activeFile.fileURL, filePath);
|
|
309
|
-
|
|
310
|
-
if (download.status === 200) {
|
|
311
|
-
await getMediaLibrary()?.saveToLibraryAsync(download.uri);
|
|
312
|
-
showToast({
|
|
313
|
-
id: Date.now().toString(),
|
|
314
|
-
title: t('toast.successTitle'),
|
|
315
|
-
message: t('toast.saveSuccessful'),
|
|
316
|
-
type: 'success',
|
|
317
|
-
});
|
|
318
|
-
} else {
|
|
319
|
-
Alert.alert('Error', 'Failed to save the file.');
|
|
320
|
-
}
|
|
321
|
-
} catch (err) {
|
|
322
|
-
Alert.alert('Error', `Failed to save the file: ${displayFileName}`);
|
|
323
|
-
}
|
|
324
|
-
};
|
|
325
|
-
|
|
326
|
-
// Best-effort existing-name lookup for the Android SAF branch below: SAF
|
|
327
|
-
// hands back content URIs, not plain names, so this decodes each one and
|
|
328
|
-
// takes its last path segment. If the provider's URI shape doesn't match
|
|
329
|
-
// what we expect, we fail open (empty set) rather than block the save -
|
|
330
|
-
// worst case we skip de-duplication, we never crash the save flow.
|
|
331
|
-
const listSafDirectoryNames = async (
|
|
332
|
-
saf: any,
|
|
333
|
-
dirUri: string
|
|
334
|
-
): Promise<Set<string>> => {
|
|
335
|
-
try {
|
|
336
|
-
const uris: string[] = await saf.readDirectoryAsync(dirUri);
|
|
337
|
-
const names = uris.map((uri) => {
|
|
338
|
-
try {
|
|
339
|
-
const decoded = decodeURIComponent(uri);
|
|
340
|
-
const last = decoded.split('/').pop() || '';
|
|
341
|
-
return last.includes(':') ? last.split(':').pop() || '' : last;
|
|
342
|
-
} catch {
|
|
343
|
-
return '';
|
|
344
|
-
}
|
|
345
|
-
});
|
|
346
|
-
return new Set(names.filter(Boolean));
|
|
347
|
-
} catch {
|
|
348
|
-
return new Set<string>();
|
|
349
|
-
}
|
|
350
|
-
};
|
|
351
|
-
|
|
352
|
-
const saveFileToDownloads = async () => {
|
|
353
|
-
try {
|
|
354
|
-
const filePath = FileSystem.cacheDirectory + displayFileName;
|
|
355
|
-
const download = await fetchToCache(activeFile.fileURL, filePath);
|
|
356
|
-
|
|
357
|
-
if (download.status !== 200) {
|
|
358
|
-
Alert.alert('Error', 'Failed to save the file.');
|
|
359
|
-
return;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
if (Platform.OS === 'android') {
|
|
363
|
-
const saf = (FileSystem as any).StorageAccessFramework;
|
|
364
|
-
let dirUri = safDirUriRef.current;
|
|
365
|
-
if (!dirUri) {
|
|
366
|
-
const perm = await saf.requestDirectoryPermissionsAsync();
|
|
367
|
-
if (!perm.granted) {
|
|
368
|
-
Alert.alert(
|
|
369
|
-
'Permission Denied',
|
|
370
|
-
'Storage permission is required to save the file.'
|
|
371
|
-
);
|
|
372
|
-
return;
|
|
373
|
-
}
|
|
374
|
-
dirUri = perm.directoryUri;
|
|
375
|
-
safDirUriRef.current = dirUri;
|
|
376
|
-
}
|
|
377
|
-
|
|
378
|
-
// Avoid silently overwriting a different attachment that happens
|
|
379
|
-
// to share the same display name (common now that we prefer the
|
|
380
|
-
// human-picked name over the unique server hash - bug #40) by
|
|
381
|
-
// suffixing " (1)", " (2)", ... on a collision.
|
|
382
|
-
const existingNames = await listSafDirectoryNames(saf, dirUri as string);
|
|
383
|
-
const uniqueName = await getUniqueFileName(
|
|
384
|
-
displayFileName,
|
|
385
|
-
async (candidate) => existingNames.has(candidate)
|
|
386
|
-
);
|
|
387
|
-
const baseName = uniqueName.replace(/\.[^/.]+$/, '');
|
|
388
|
-
const mime = activeFile.mimetype || 'application/octet-stream';
|
|
389
|
-
const base64 = await FileSystem.readAsStringAsync(download.uri, {
|
|
390
|
-
encoding: FileSystem.EncodingType.Base64,
|
|
391
|
-
});
|
|
392
|
-
const destUri = await saf.createFileAsync(dirUri, baseName, mime);
|
|
393
|
-
await FileSystem.writeAsStringAsync(destUri, base64, {
|
|
394
|
-
encoding: FileSystem.EncodingType.Base64,
|
|
395
|
-
});
|
|
396
|
-
|
|
397
|
-
showToast({
|
|
398
|
-
id: Date.now().toString(),
|
|
399
|
-
title: t('toast.successTitle'),
|
|
400
|
-
message: t('toast.saveSuccessful'),
|
|
401
|
-
type: 'success',
|
|
402
|
-
});
|
|
403
|
-
return;
|
|
404
|
-
}
|
|
405
|
-
|
|
406
|
-
// iOS (and any non-Android): there is no app-writable "Downloads"
|
|
407
|
-
// folder, and MediaLibrary only accepts images/videos — which is why
|
|
408
|
-
// documents previously hit the success toast without ever being
|
|
409
|
-
// saved. Present the system share sheet so the user can "Save to
|
|
410
|
-
// Files", AirDrop, etc. The sheet itself is the confirmation (the
|
|
411
|
-
// user may cancel), so we don't show a "Save successful" toast here.
|
|
412
|
-
await Share.share({
|
|
413
|
-
url: download.uri,
|
|
414
|
-
title: displayFileName,
|
|
415
|
-
});
|
|
416
|
-
} catch (err) {
|
|
417
|
-
console.error('Error saving file:', err);
|
|
418
|
-
Alert.alert('Error', 'Failed to save the file.');
|
|
419
|
-
}
|
|
420
|
-
};
|
|
421
|
-
|
|
422
|
-
const saveClick = async () => {
|
|
423
|
-
if (
|
|
424
|
-
activeFile.mimetype.startsWith('image/') ||
|
|
425
|
-
activeFile.mimetype.startsWith('video/')
|
|
426
|
-
) {
|
|
427
|
-
await saveToGallery();
|
|
428
|
-
} else {
|
|
429
|
-
await saveFileToDownloads();
|
|
430
|
-
}
|
|
431
|
-
};
|
|
357
|
+
const saveFile = useSaveFile();
|
|
358
|
+
const saveClick = () => saveFile(activeFile);
|
|
432
359
|
|
|
433
360
|
// An album opens with its siblings; the viewer pages through them.
|
|
434
361
|
const albumSize = activeFile.siblings?.length ?? 0;
|
|
@@ -436,21 +363,24 @@ const FilePreviewModal: React.FC<FilePreviewModalProps> = ({
|
|
|
436
363
|
const goToSibling = (next: number) => {
|
|
437
364
|
const target = activeFile.siblings?.[next];
|
|
438
365
|
if (!target) {return;}
|
|
439
|
-
|
|
440
|
-
setActiveFile({ ...target, siblings: activeFile.siblings, index: next })
|
|
441
|
-
);
|
|
366
|
+
onChangeFile({ ...target, siblings: activeFile.siblings, index: next });
|
|
442
367
|
};
|
|
368
|
+
const closeModal = onClose;
|
|
443
369
|
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
370
|
+
if (
|
|
371
|
+
activeFile.mimetype.startsWith('image/') ||
|
|
372
|
+
activeFile.mimetype.startsWith('video/')
|
|
373
|
+
) {
|
|
374
|
+
return (
|
|
375
|
+
<MediaViewer
|
|
376
|
+
file={activeFile}
|
|
377
|
+
onClose={onClose}
|
|
378
|
+
onPage={goToSibling}
|
|
379
|
+
onSave={saveClick}
|
|
380
|
+
onReplyOpened={onReplyOpened}
|
|
381
|
+
/>
|
|
451
382
|
);
|
|
452
|
-
|
|
453
|
-
};
|
|
383
|
+
}
|
|
454
384
|
|
|
455
385
|
// NOTE: plain computed value, NOT useMemo. It sits AFTER the
|
|
456
386
|
// `if (!activeFile) return` guard above, so as a hook it would run
|