* Handle FileWillBeDownloaded plugin hook rejections - Add WebSocket handler for file_download_rejected events - Show rejected files as file cards instead of broken thumbnails - Display plugin rejection message in snackbar - Store rejection reason in EphemeralStore for later retrieval - Re-render file components when rejection events arrive - Remove blurred preview to prevent visual blink on rejection * chore: lint * chore: lint and test * fix: removed event from useEffect
248 lines
9 KiB
TypeScript
248 lines
9 KiB
TypeScript
// Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
|
|
// See LICENSE.txt for license information.
|
|
|
|
import {deleteAsync} from 'expo-file-system';
|
|
import {useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
|
import {useIntl} from 'react-intl';
|
|
import {Platform, StatusBar, type StatusBarStyle} from 'react-native';
|
|
import FileViewer from 'react-native-file-viewer';
|
|
import tinyColor from 'tinycolor2';
|
|
|
|
import {getLocalFileInfo} from '@actions/local/file';
|
|
import {buildFilePreviewUrl, buildFileUrl, downloadFile} from '@actions/remote/file';
|
|
import {useServerUrl} from '@context/server';
|
|
import {useTheme} from '@context/theme';
|
|
import EphemeralStore from '@store/ephemeral_store';
|
|
import {alertDownloadFailed, alertFailedToOpenDocument, alertOnlyPDFSupported} from '@utils/document';
|
|
import {getFullErrorMessage, isErrorWithMessage} from '@utils/errors';
|
|
import {fileExists, getLocalFilePathFromFile, isAudio, isGif, isImage, isPdf, isVideo} from '@utils/file';
|
|
import {getImageSize} from '@utils/gallery';
|
|
import {logDebug} from '@utils/log';
|
|
import {previewPdf} from '@utils/navigation';
|
|
|
|
import type {ClientResponse, ProgressPromise} from '@mattermost/react-native-network-client';
|
|
import type ChannelBookmarkModel from '@typings/database/models/servers/channel_bookmark';
|
|
|
|
const getFileInfo = async (serverUrl: string, bookmarks: ChannelBookmarkModel[], cb: (files: FileInfo[]) => void) => {
|
|
const fileInfos: FileInfo[] = [];
|
|
for await (const b of bookmarks) {
|
|
if (b.fileId) {
|
|
const res = await getLocalFileInfo(serverUrl, b.fileId);
|
|
if (res.file) {
|
|
const fileInfo = res.file.toFileInfo(b.ownerId);
|
|
const imageFile = isImage(fileInfo);
|
|
const videoFile = isVideo(fileInfo);
|
|
|
|
let uri;
|
|
if (imageFile || videoFile) {
|
|
if (fileInfo.localPath) {
|
|
uri = fileInfo.localPath;
|
|
} else {
|
|
uri = (isGif(fileInfo) || (imageFile && !fileInfo.has_preview_image) || videoFile) ? buildFileUrl(serverUrl, fileInfo.id!) : buildFilePreviewUrl(serverUrl, fileInfo.id!);
|
|
}
|
|
} else {
|
|
uri = fileInfo.localPath || buildFileUrl(serverUrl, fileInfo.id!);
|
|
}
|
|
|
|
let {width, height} = fileInfo;
|
|
if (imageFile && !width) {
|
|
const size = await getImageSize(serverUrl, uri, b.fileId);
|
|
width = size.width;
|
|
height = size.height;
|
|
}
|
|
|
|
fileInfos.push({...fileInfo, uri, width, height});
|
|
}
|
|
}
|
|
}
|
|
|
|
if (fileInfos.length) {
|
|
cb(fileInfos);
|
|
}
|
|
};
|
|
|
|
export const useImageAttachments = (filesInfo: FileInfo[]) => {
|
|
const serverUrl = useServerUrl();
|
|
return useMemo(() => {
|
|
return filesInfo.reduce(({images, nonImages}: {images: FileInfo[]; nonImages: FileInfo[]}, file) => {
|
|
const imageFile = isImage(file);
|
|
const videoFile = isVideo(file);
|
|
const audioFile = isAudio(file);
|
|
|
|
// Check if file is rejected by plugin - treat rejected files as non-images
|
|
// so they display as file cards instead of broken image previews
|
|
const isRejected = file.id && EphemeralStore.isFileRejected(file.id);
|
|
|
|
if ((imageFile || videoFile || audioFile) && !isRejected) {
|
|
let uri;
|
|
if (file.localPath) {
|
|
uri = file.localPath;
|
|
} else {
|
|
// If no local path and no id, we skip the image
|
|
if (!file.id) {
|
|
return {images, nonImages};
|
|
}
|
|
uri = (isGif(file) || videoFile || audioFile) ? buildFileUrl(serverUrl, file.id) : buildFilePreviewUrl(serverUrl, file.id);
|
|
}
|
|
images.push({...file, uri});
|
|
} else {
|
|
nonImages.push({...file});
|
|
}
|
|
return {images, nonImages};
|
|
}, {images: [], nonImages: []});
|
|
}, [filesInfo, serverUrl]);
|
|
};
|
|
|
|
export const useChannelBookmarkFiles = (bookmarks: ChannelBookmarkModel[]) => {
|
|
const serverUrl = useServerUrl();
|
|
const [files, setFiles] = useState<FileInfo[]>([]);
|
|
|
|
useEffect(() => {
|
|
getFileInfo(serverUrl, bookmarks, setFiles);
|
|
}, [serverUrl, bookmarks]);
|
|
|
|
return files;
|
|
};
|
|
|
|
export const useDownloadFileAndPreview = (enableSecureFilePreview: boolean) => {
|
|
const serverUrl = useServerUrl();
|
|
const intl = useIntl();
|
|
const theme = useTheme();
|
|
const downloadTask = useRef<ProgressPromise<ClientResponse>>();
|
|
|
|
const [progress, setProgress] = useState<number>(0);
|
|
|
|
const [preview, setPreview] = useState(false);
|
|
const [downloading, setDownloading] = useState(false);
|
|
const [didCancel, setDidCancel] = useState(false);
|
|
|
|
const setStatusBarColor = useCallback((style: StatusBarStyle = 'light-content') => {
|
|
if (Platform.OS === 'ios') {
|
|
if (style) {
|
|
StatusBar.setBarStyle(style, true);
|
|
} else {
|
|
const headerColor = tinyColor(theme.sidebarHeaderBg);
|
|
let barStyle: StatusBarStyle = 'light-content';
|
|
if (headerColor.isLight() && Platform.OS === 'ios') {
|
|
barStyle = 'dark-content';
|
|
}
|
|
StatusBar.setBarStyle(barStyle, true);
|
|
}
|
|
}
|
|
}, [theme.sidebarHeaderBg]);
|
|
|
|
const onDonePreviewingFile = useCallback(() => {
|
|
setProgress(0);
|
|
setDownloading(false);
|
|
setPreview(false);
|
|
setStatusBarColor();
|
|
}, [setStatusBarColor]);
|
|
|
|
const openDocument = useCallback(async (file: FileInfo) => {
|
|
if (!didCancel && !preview) {
|
|
let path = decodeURIComponent(file.localPath || '');
|
|
let exists = false;
|
|
if (path) {
|
|
exists = await fileExists(path);
|
|
}
|
|
|
|
if (!exists) {
|
|
path = getLocalFilePathFromFile(serverUrl, file);
|
|
}
|
|
|
|
if (enableSecureFilePreview) {
|
|
if (isPdf(file)) {
|
|
previewPdf(file, path, theme, onDonePreviewingFile);
|
|
} else {
|
|
alertOnlyPDFSupported(intl);
|
|
}
|
|
return;
|
|
}
|
|
|
|
setPreview(true);
|
|
setStatusBarColor('dark-content');
|
|
FileViewer.open(path!.replace('file://', ''), {
|
|
displayName: decodeURIComponent(file.name),
|
|
onDismiss: onDonePreviewingFile,
|
|
showOpenWithDialog: true,
|
|
showAppsSuggestions: true,
|
|
}).then(() => {
|
|
setDownloading(false);
|
|
setProgress(0);
|
|
}).catch(() => {
|
|
alertFailedToOpenDocument(file, intl);
|
|
onDonePreviewingFile();
|
|
|
|
if (path) {
|
|
deleteAsync(path, {idempotent: true});
|
|
}
|
|
});
|
|
}
|
|
}, [didCancel, preview, enableSecureFilePreview, setStatusBarColor, onDonePreviewingFile, serverUrl, theme, intl]);
|
|
|
|
const downloadAndPreviewFile = useCallback(async (file: FileInfo) => {
|
|
setDidCancel(false);
|
|
let path;
|
|
let exists = false;
|
|
|
|
try {
|
|
path = decodeURIComponent(file.localPath || '');
|
|
if (path) {
|
|
exists = await fileExists(path);
|
|
}
|
|
|
|
if (!exists) {
|
|
path = getLocalFilePathFromFile(serverUrl, file);
|
|
exists = await fileExists(path);
|
|
}
|
|
|
|
if (exists) {
|
|
openDocument(file);
|
|
} else {
|
|
setDownloading(true);
|
|
downloadTask.current = downloadFile(serverUrl, file.id!, path!);
|
|
downloadTask.current?.progress?.(setProgress);
|
|
|
|
await downloadTask.current;
|
|
setProgress(1);
|
|
openDocument(file);
|
|
}
|
|
} catch (error) {
|
|
if (path) {
|
|
deleteAsync(path, {idempotent: true});
|
|
}
|
|
setDownloading(false);
|
|
setProgress(0);
|
|
|
|
if (!isErrorWithMessage(error) || error.message !== 'cancelled') {
|
|
logDebug('error on downloadAndPreviewFile', getFullErrorMessage(error));
|
|
alertDownloadFailed(intl);
|
|
}
|
|
}
|
|
}, [intl, openDocument, serverUrl]);
|
|
|
|
const toggleDownloadAndPreview = useCallback((file: FileInfo) => {
|
|
if (downloading && progress < 1) {
|
|
cancelDownload();
|
|
} else if (downloading) {
|
|
setProgress(0);
|
|
cancelDownload();
|
|
setDownloading(false);
|
|
} else {
|
|
downloadAndPreviewFile(file);
|
|
}
|
|
}, [downloading, progress, downloadAndPreviewFile]);
|
|
|
|
const cancelDownload = () => {
|
|
setDidCancel(true);
|
|
if (downloadTask.current?.cancel) {
|
|
downloadTask.current.cancel();
|
|
}
|
|
};
|
|
|
|
return {
|
|
downloading,
|
|
progress,
|
|
toggleDownloadAndPreview,
|
|
};
|
|
};
|