Skip to content

Commit

Permalink
fix(teaching): detect corrupted files, refresh on click (#444)
Browse files Browse the repository at this point in the history
* fix(teaching): detect corrupted files, refresh on click

* feat(teaching): prevent downloaded files from opening after leaving page
  • Loading branch information
umbopepato authored Mar 11, 2024
1 parent 7a6b859 commit 9c240f4
Show file tree
Hide file tree
Showing 4 changed files with 112 additions and 59 deletions.
16 changes: 14 additions & 2 deletions lib/ui/components/FileListItem.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { Pie as ProgressIndicator } from 'react-native-progress';
import { IconDefinition } from '@fortawesome/fontawesome-svg-core';
import {
faCheckCircle,
faExclamationCircle,
faFile,
faFileAudio,
faFileCode,
Expand Down Expand Up @@ -69,13 +70,15 @@ interface Props {
downloadProgress?: number;
containerStyle?: StyleProp<ViewStyle>;
mimeType?: string;
isCorrupted?: boolean;
}

export const FileListItem = ({
isDownloaded = false,
downloadProgress,
subtitle,
mimeType,
isCorrupted = false,
...rest
}: ListItemProps & Props) => {
const { palettes, fontSizes } = useTheme();
Expand All @@ -101,15 +104,24 @@ export const FileListItem = ({
/>
</View>
) : (
isDownloaded && (
isDownloaded &&
(!isCorrupted ? (
<View style={styles.downloadedIconContainer}>
<Icon
icon={faCheckCircle}
size={12}
color={palettes.secondary[600]}
/>
</View>
)
) : (
<View style={styles.downloadedIconContainer}>
<Icon
icon={faExclamationCircle}
size={12}
color={palettes.danger[600]}
/>
</View>
))
)}
</View>
}
Expand Down
113 changes: 60 additions & 53 deletions src/core/hooks/useDownloadCourseFile.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,53 +88,58 @@ export const useDownloadCourseFile = (
updateDownload,
]);

const startDownload = useCallback(async () => {
if (!download.isDownloaded && download.downloadProgress == null) {
updateDownload({ downloadProgress: 0 });
try {
await mkdir(dirname(toFile));
const { jobId, promise } = downloadFile({
fromUrl,
toFile,
headers: {
Authorization: `Bearer ${token}`,
},
progressInterval: 200,
begin: () => {
/* Needed for progress updates to work */
},
progress: ({ bytesWritten, contentLength }) => {
updateDownload({ downloadProgress: bytesWritten / contentLength });
},
});
updateDownload({ jobId });
const result = await promise;
if (result.statusCode !== 200) {
// noinspection ExceptionCaughtLocallyJS
throw new Error(t('common.downloadError'));
const startDownload = useCallback(
async (force = false) => {
if (
force ||
(!download.isDownloaded && download.downloadProgress == null)
) {
updateDownload({ downloadProgress: 0 });
try {
await mkdir(dirname(toFile));

const { jobId, promise } = downloadFile({
fromUrl,
toFile,
headers: {
Authorization: `Bearer ${token}`,
},
progressInterval: 200,
begin: () => {
/* Needed for progress updates to work */
},
progress: ({ bytesWritten, contentLength }) => {
updateDownload({
downloadProgress: bytesWritten / contentLength,
});
},
});
updateDownload({ jobId });
const result = await promise;
if (result.statusCode !== 200) {
// noinspection ExceptionCaughtLocallyJS
throw new Error(t('common.downloadError'));
}
updateDownload({
isDownloaded: true,
downloadProgress: undefined,
});
} catch (e) {
if (!(e as Error).message?.includes('aborted')) {
Alert.alert(
t('common.error'),
t('courseScreen.fileDownloadFailed'),
);
}
updateDownload({
isDownloaded: false,
downloadProgress: undefined,
});
}
updateDownload({
isDownloaded: true,
downloadProgress: undefined,
});
} catch (e) {
Alert.alert(t('common.error'), t('courseScreen.fileDownloadFailed'));
updateDownload({
isDownloaded: false,
downloadProgress: undefined,
});
throw e;
}
}
}, [
download.downloadProgress,
download.isDownloaded,
fromUrl,
t,
toFile,
token,
updateDownload,
]);
},
[download, fromUrl, t, toFile, token, updateDownload],
);

const stopDownload = useCallback(() => {
const jobId = download.jobId;
Expand All @@ -157,7 +162,7 @@ export const useDownloadCourseFile = (
isDownloaded: false,
downloadProgress: undefined,
});
return startDownload();
return startDownload(true);
}, [download.isDownloaded, startDownload, toFile, updateDownload]);

const removeDownload = useCallback(async () => {
Expand All @@ -169,13 +174,15 @@ export const useDownloadCourseFile = (
});
}, [toFile, updateDownload]);

const openFile = useCallback(() => {
return open(toFile).catch(async e => {
if (e.message === 'No app associated with this mime type') {
throw new UnsupportedFileTypeError(`Cannot open file ${fromUrl}`);
}
});
}, [fromUrl, toFile]);
const openFile = useCallback(
() =>
open(toFile).catch(async e => {
if (e.message === 'No app associated with this mime type') {
throw new UnsupportedFileTypeError(`Cannot open file ${fromUrl}`);
}
}),
[fromUrl, toFile],
);

return {
...(download ?? {}),
Expand Down
40 changes: 37 additions & 3 deletions src/features/courses/components/CourseFileListItem.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { memo, useCallback, useMemo } from 'react';
import { memo, useCallback, useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert, Platform } from 'react-native';
import { stat } from 'react-native-fs';
import { extension, lookup } from 'react-native-mime-types';

import {
Expand All @@ -15,6 +16,7 @@ import { useTheme } from '@lib/ui/hooks/useTheme';
import { BASE_PATH, CourseFileOverview } from '@polito/api-client';
import { MenuView } from '@react-native-menu/menu';
import { MenuComponentProps } from '@react-native-menu/menu/src/types';
import { useNavigation } from '@react-navigation/native';

import { IS_IOS } from '../../../core/constants';
import { useDownloadCourseFile } from '../../../core/hooks/useDownloadCourseFile';
Expand Down Expand Up @@ -94,6 +96,7 @@ export const CourseFileListItem = memo(
...rest
}: Props) => {
const { t } = useTranslation();
const navigation = useNavigation();
const { colors, fontSizes, spacing } = useTheme();
const iconProps = useMemo(
() => ({
Expand All @@ -109,6 +112,7 @@ export const CourseFileListItem = memo(
() => ['teaching', 'courses', courseId.toString(), 'files', item.id],
[courseId, item.id],
);
const [isCorrupted, setIsCorrupted] = useState(false);
const fileUrl = `${BASE_PATH}/courses/${courseId}/files/${item.id}`;
const cachedFilePath = useMemo(() => {
let ext: string | null = extension(item.mimeType!);
Expand Down Expand Up @@ -137,6 +141,21 @@ export const CourseFileListItem = memo(
openFile,
} = useDownloadCourseFile(fileUrl, cachedFilePath, item.id);

useEffect(() => {
(async () => {
if (!isDownloaded) {
setIsCorrupted(false);
return;
}
const fileStats = await stat(cachedFilePath);
setIsCorrupted(
Math.abs(fileStats.size - item.sizeInKiloBytes * 1024) /
Math.max(fileStats.size, item.sizeInKiloBytes * 1024) >
0.1,
);
})();
}, [cachedFilePath, isDownloaded, item.sizeInKiloBytes]);

const metrics = useMemo(
() =>
[
Expand All @@ -161,12 +180,26 @@ export const CourseFileListItem = memo(

const downloadFile = useCallback(async () => {
if (downloadProgress == null) {
if (isCorrupted) {
await refreshDownload();
return;
}
if (!isDownloaded) {
await startDownload();
}
openDownloadedFile();
if (navigation.isFocused()) {
openDownloadedFile();
}
}
}, [downloadProgress, isDownloaded, openDownloadedFile, startDownload]);
}, [
downloadProgress,
isCorrupted,
isDownloaded,
navigation,
openDownloadedFile,
refreshDownload,
startDownload,
]);

const trailingItem = useMemo(
() =>
Expand Down Expand Up @@ -246,6 +279,7 @@ export const CourseFileListItem = memo(
trailingItem={trailingItem}
mimeType={item.mimeType}
unread={!!getUnreadsCount(fileNotificationScope)}
isCorrupted={isCorrupted}
/>
);

Expand Down
2 changes: 1 addition & 1 deletion src/features/transcript/screens/ProvisionalGradeScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -228,7 +228,7 @@ const createStyles = ({
fontWeight: fontWeights.semibold,
},
longGradeText: {
fontSize: fontSizes['md'],
fontSize: fontSizes.md,
fontWeight: fontWeights.semibold,
},
rejectionTime: {
Expand Down

0 comments on commit 9c240f4

Please sign in to comment.