Skip to content
This repository has been archived by the owner on Jun 15, 2022. It is now read-only.

Commit

Permalink
feat: preview files (#580)
Browse files Browse the repository at this point in the history
* fix: when sharing the file on Android, keep it in cache instead of Downloads folder

* feat: preview files

* feat: show an alert when the file is possibly non-previewable

* fix: correct wording

* fix: add period at the end of string

* fix: remove title from the alert modal
  • Loading branch information
vardan-arm committed Mar 31, 2022
1 parent 90a0caf commit b098c15
Show file tree
Hide file tree
Showing 6 changed files with 175 additions and 75 deletions.
215 changes: 153 additions & 62 deletions src/hooks/useFiles.ts
Expand Up @@ -8,6 +8,7 @@ import {
UploadedFileItemActionType,
} from '@Screens/UploadedFilesList/UploadedFileItemAction';
import {
ButtonType,
ChallengeReason,
ContentType,
SNFile,
Expand All @@ -16,13 +17,28 @@ import {
import { useCustomActionSheet } from '@Style/custom_action_sheet';
import { useCallback, useContext, useEffect, useState } from 'react';
import { Platform } from 'react-native';
import FileViewer from 'react-native-file-viewer';
import RNFS, { exists } from 'react-native-fs';
import RNShare from 'react-native-share';
import Toast from 'react-native-toast-message';

type Props = {
note: SNNote;
};
type TDownloadFileAndReturnLocalPathParams = {
file: SNFile;
saveInTempLocation?: boolean;
};

export const isFileTypePreviewable = (fileType: string) => {
const isImage = fileType.startsWith('image/');
const isVideo = fileType.startsWith('video/');
const isAudio = fileType.startsWith('audio/');
const isPdf = fileType === 'application/pdf';
const isText = fileType === 'text/plain';

return isImage || isVideo || isAudio || isPdf || isText;
};

export const useFiles = ({ note }: Props) => {
const application = useContext(ApplicationContext);
Expand Down Expand Up @@ -66,49 +82,11 @@ export const useFiles = ({ note }: Props) => {
} catch (err) {}
}, []);

const shareFile = useCallback(
(file: SNFile, path: string) => {
if (!application) {
return;
}
application
.getAppState()
.performActionWithoutStateChangeImpact(async () => {
try {
// On Android this response always returns {success: false}, there is an open issue for that:
// https://github.com/react-native-share/react-native-share/issues/1059
const shareDialogResponse = await RNShare.open({
url: `file://${path}`,
failOnCancel: false,
});

// On iOS the user can store files locally from "Share" screen, so we don't show "Download" option there.
// For Android the user has a separate "Download" action for the file, therefore after the file is shared,
// it's not needed anymore and we remove it from the storage.
if (Platform.OS === 'android') {
await deleteFileAtPath(path);
}
if (shareDialogResponse.success) {
Toast.show({
type: Success,
text1: 'Successfully exported file',
onPress: Toast.hide,
});
}
} catch (error) {
Toast.show({
type: Error,
text1: 'An error occurred while trying to share this file',
onPress: Toast.hide,
});
}
});
},
[Error, Success, application, deleteFileAtPath]
);

const downloadFile = useCallback(
async (file: SNFile, showShareScreen = false) => {
const downloadFileAndReturnLocalPath = useCallback(
async ({
file,
saveInTempLocation = false,
}: TDownloadFileAndReturnLocalPathParams): Promise<string | undefined> => {
if (isDownloading || !application) {
return;
}
Expand All @@ -129,28 +107,22 @@ export const useFiles = ({ note }: Props) => {
onPress: Toast.hide,
});

const path = filesService.getDestinationPath(
file.name,
showShareScreen
);
const path = filesService.getDestinationPath({
fileName: file.name,
saveInTempLocation,
});

await deleteFileAtPath(path);

await filesService.downloadFileInChunks(file, path);

Toast.hide();

if (showShareScreen) {
await shareFile(file, path);
return;
}

Toast.show({
type: Success,
text1: 'Success',
text2: 'Successfully downloaded file',
onPress: Toast.hide,
});

return path;
} catch (error) {
Toast.show({
type: Error,
Expand All @@ -162,14 +134,68 @@ export const useFiles = ({ note }: Props) => {
setIsDownloading(false);
}
},
[Error, Info, Success, application, deleteFileAtPath, isDownloading]
);

const cleanupTempFileOnAndroid = useCallback(
async (downloadedFilePath: string) => {
if (Platform.OS === 'android') {
await deleteFileAtPath(downloadedFilePath);
}
},
[deleteFileAtPath]
);

const shareFile = useCallback(
async (file: SNFile) => {
if (!application) {
return;
}
const downloadedFilePath = await downloadFileAndReturnLocalPath({
file,
saveInTempLocation: true,
});
if (!downloadedFilePath) {
return;
}
application
.getAppState()
.performActionWithoutStateChangeImpact(async () => {
try {
// On Android this response always returns {success: false}, there is an open issue for that:
// https://github.com/react-native-share/react-native-share/issues/1059
const shareDialogResponse = await RNShare.open({
url: `file://${downloadedFilePath}`,
failOnCancel: false,
});

// On iOS the user can store files locally from "Share" screen, so we don't show "Download" option there.
// For Android the user has a separate "Download" action for the file, therefore after the file is shared,
// it's not needed anymore and we remove it from the storage.
await cleanupTempFileOnAndroid(downloadedFilePath);

if (shareDialogResponse.success) {
Toast.show({
type: Success,
text1: 'Successfully exported file',
onPress: Toast.hide,
});
}
} catch (error) {
Toast.show({
type: Error,
text1: 'An error occurred while trying to share this file',
onPress: Toast.hide,
});
}
});
},
[
Error,
Info,
Success,
application,
deleteFileAtPath,
isDownloading,
shareFile,
cleanupTempFileOnAndroid,
downloadFileAndReturnLocalPath,
]
);

Expand Down Expand Up @@ -250,6 +276,55 @@ export const useFiles = ({ note }: Props) => {
[application]
);

const previewFile = useCallback(
async (file: SNFile) => {
if (!application) {
return;
}

let downloadedFilePath: string | undefined = '';
try {
const isPreviewable = isFileTypePreviewable(file.mimeType);

if (!isPreviewable) {
const tryToPreview = await application.alertService.confirm(
'This file may not be previewable. Do you wish to try anyway?',
'',
'Try to preview',
ButtonType.Info,
'Cancel'
);
if (!tryToPreview) {
return;
}
}

downloadedFilePath = await downloadFileAndReturnLocalPath({
file,
saveInTempLocation: true,
});

if (!downloadedFilePath) {
return;
}
await FileViewer.open(downloadedFilePath, {
onDismiss: async () => {
await cleanupTempFileOnAndroid(downloadedFilePath as string);
},
});

return true;
} catch (error) {
await cleanupTempFileOnAndroid(downloadedFilePath as string);
await application.alertService.alert(
'An error occurred while previewing the file.'
);

return false;
}
},
[application, cleanupTempFileOnAndroid, downloadFileAndReturnLocalPath]
);
const handleFileAction = useCallback(
async (action: UploadedFileItemAction) => {
if (!application) {
Expand Down Expand Up @@ -284,10 +359,10 @@ export const useFiles = ({ note }: Props) => {
await detachFileFromNote(file);
break;
case UploadedFileItemActionType.ShareFile:
await downloadFile(file, true);
await shareFile(file);
break;
case UploadedFileItemActionType.DownloadFile:
await downloadFile(file);
await downloadFileAndReturnLocalPath({ file });
break;
case UploadedFileItemActionType.ToggleFileProtection: {
await toggleFileProtection(file);
Expand All @@ -296,6 +371,11 @@ export const useFiles = ({ note }: Props) => {
case UploadedFileItemActionType.RenameFile:
await renameFile(file, action.payload.name);
break;
case UploadedFileItemActionType.PreviewFile:
await previewFile(file);
break;
default:
break;
}

application.sync.sync();
Expand All @@ -306,8 +386,10 @@ export const useFiles = ({ note }: Props) => {
attachFileToNote,
authorizeProtectedActionForFile,
detachFileFromNote,
downloadFile,
downloadFileAndReturnLocalPath,
previewFile,
renameFile,
shareFile,
toggleFileProtection,
]
);
Expand Down Expand Up @@ -360,6 +442,15 @@ export const useFiles = ({ note }: Props) => {
});
},
},
{
text: 'Preview',
callback: () => {
handleFileAction({
type: UploadedFileItemActionType.PreviewFile,
payload: file,
});
},
},
{
text: Platform.OS === 'ios' ? 'Export' : 'Share',
callback: () => {
Expand Down
22 changes: 14 additions & 8 deletions src/lib/files_service.ts
Expand Up @@ -2,23 +2,29 @@ import { ApplicationService, SNFile } from '@standardnotes/snjs';
import { Buffer } from 'buffer';
import { PermissionsAndroid, Platform } from 'react-native';
import RNFS, {
CachesDirectoryPath,
DocumentDirectoryPath,
DownloadDirectoryPath,
} from 'react-native-fs';

type TGetFileDestinationPath = {
fileName: string;
saveInTempLocation?: boolean;
};

export class FilesService extends ApplicationService {
getDestinationPath(fileName: string, showShareScreen: boolean): string {
getDestinationPath({
fileName,
saveInTempLocation = false,
}: TGetFileDestinationPath): string {
let directory = DocumentDirectoryPath;
let tmpInFileName = '';

if (Platform.OS === 'android') {
directory = DownloadDirectoryPath;

if (showShareScreen) {
tmpInFileName = 'tmp-';
}
directory = saveInTempLocation
? CachesDirectoryPath
: DownloadDirectoryPath;
}
return `${directory}/${tmpInFileName}${fileName}`;
return `${directory}/${fileName}`;
}

async hasStoragePermissionOnAndroid(): Promise<boolean> {
Expand Down
2 changes: 1 addition & 1 deletion src/screens/Compose/Compose.tsx
Expand Up @@ -494,7 +494,7 @@ export class Compose extends React.Component<{}, State> {
text = 'The local component server has an error.';
break;
case ComponentLoadingError.Unknown:
text = 'An unknown error occured.';
text = 'An unknown error occurred.';
break;
default:
break;
Expand Down
8 changes: 5 additions & 3 deletions src/screens/ManageSessions/ManageSessions.tsx
Expand Up @@ -33,15 +33,15 @@ const useSessions = (): [
const response = await application?.getSessions();

if (!response) {
setErrorMessage('An unknown error occured while loading sessions.');
setErrorMessage('An unknown error occurred while loading sessions.');
return;
}

if ('error' in response || !response.data) {
if (response.error?.message) {
setErrorMessage(response.error.message);
} else {
setErrorMessage('An unknown error occured while loading sessions.');
setErrorMessage('An unknown error occurred while loading sessions.');
}
} else {
const newSessions = response.data as RemoteSession[];
Expand All @@ -66,7 +66,9 @@ const useSessions = (): [
if (response.error?.message) {
setErrorMessage(response.error?.message);
} else {
setErrorMessage('An unknown error occured while revoking the session.');
setErrorMessage(
'An unknown error occurred while revoking the session.'
);
}
} else {
setSessions(sessions.filter(session => session.uuid !== uuid));
Expand Down
2 changes: 1 addition & 1 deletion src/screens/SideMenu/Files.tsx
Expand Up @@ -48,7 +48,7 @@ export const Files: FC<Props> = ({ note }) => {
);

return (
<FileItemContainer>
<FileItemContainer key={file.uuid}>
<SideMenuCellStyled
text={file.name}
key={file.uuid}
Expand Down

0 comments on commit b098c15

Please sign in to comment.