Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ function DriveGridModeItemComp(props: DriveItemProps): JSX.Element {
useEffect(() => {
if (props.data.thumbnails && props.data.thumbnails.length && !downloadedThumbnail) {
InteractionManager.runAfterInteractions(() => {
// TODO: NEED TO UPDATE SDK TYPES
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
driveFileService.getThumbnail(props.data.thumbnails[0]).then((downloadedThumbnail) => {
setDownloadedThumbnail(downloadedThumbnail);
});
Expand Down
13 changes: 11 additions & 2 deletions src/contexts/Drive/Drive.context.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import errorService from '@internxt-mobile/services/ErrorService';
import { AppStateStatus, NativeEventSubscription } from 'react-native';

import { driveFolderService } from '@internxt-mobile/services/drive/folder';
import { Thumbnail } from '@internxt/sdk/dist/drive/storage/types';

export type DriveFoldersTreeNode = {
name: string;
Expand All @@ -32,7 +33,11 @@ export interface DriveContextType {
toggleViewMode: () => void;
loadFolderContent: (folderUuid: string, options?: LoadFolderContentOptions) => Promise<void>;
focusedFolder: DriveFoldersTreeNode | null;
updateItemInTree: (folderId: string, itemId: number, updates: { name?: string; plainName?: string }) => void;
updateItemInTree: (
folderId: string,
itemId: number,
updates: { name?: string; plainName?: string; thumbnails?: Thumbnail[] },
) => void;
removeItemFromTree: (folderId: string, itemId: number) => void;
addItemToTree: (folderId: string, item: DriveItemData, isFolder: boolean) => void;
}
Expand Down Expand Up @@ -324,7 +329,11 @@ export const DriveContextProvider: React.FC<DriveContextProviderProps> = ({ chil
});
};

const updateItemInTree = (folderId: string, itemId: number, updates: { name?: string; plainName?: string }) => {
const updateItemInTree = (
folderId: string,
itemId: number,
updates: { name?: string; plainName?: string; thumbnails?: Thumbnail[] },
) => {
setDriveFoldersTree((prevTree) => {
const folder = prevTree[folderId];
if (!folder) return prevTree;
Expand Down
30 changes: 30 additions & 0 deletions src/screens/drive/DrivePreviewScreen/DrivePreviewScreen.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fs } from '@internxt-mobile/services/FileSystemService';
import { notifications } from '@internxt-mobile/services/NotificationsService';
import { FileExtension } from '@internxt-mobile/types/drive';
import { RootStackScreenProps } from '@internxt-mobile/types/navigation';
import { Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
import strings from 'assets/lang/strings';
import { WarningCircle } from 'phosphor-react-native';
import React, { useEffect, useRef, useState } from 'react';
Expand All @@ -16,6 +17,7 @@ import AppScreen from 'src/components/AppScreen';
import AppText from 'src/components/AppText';
import { DEFAULT_EASING } from 'src/components/modals/SharedLinkSettingsModal/animations';
import { getFileTypeIcon } from 'src/helpers';
import { useDrive } from 'src/hooks/drive';
import { useAppDispatch, useAppSelector } from 'src/store/hooks';
import { uiActions } from 'src/store/slices/ui';
import { getLineHeight } from 'src/styles/global';
Expand All @@ -25,6 +27,7 @@ import { DriveImagePreview } from './DriveImagePreview';
import { DrivePdfPreview } from './DrivePdfPreview';
import { DRIVE_PREVIEW_HEADER_HEIGHT, DrivePreviewScreenHeader } from './DrivePreviewScreenHeader';
import { DriveVideoPreview } from './DriveVideoPreview';
import { useThumbnailRegeneration } from './hooks/useThumbnailRegeneration';
import AnimatedLoadingDots from './LoadingDots';

const IMAGE_PREVIEW_TYPES = [FileExtension.PNG, FileExtension.JPG, FileExtension.JPEG, FileExtension.HEIC];
Expand All @@ -40,6 +43,7 @@ export const DrivePreviewScreen: React.FC<RootStackScreenProps<'DrivePreview'>>
// REDUX USAGE STARTS
const insets = useSafeAreaInsets();
const dispatch = useAppDispatch();
const driveCtx = useDrive();
// REDUX USAGE ENDS
const { downloadingFile } = useAppSelector((state) => state.drive);
// Use this in order to listen for state changes
Expand All @@ -63,6 +67,32 @@ export const DrivePreviewScreen: React.FC<RootStackScreenProps<'DrivePreview'>>
}
}, [downloadingFile?.downloadedFilePath]);

const updateItemWithNewThumbnail = (thumbnail: Thumbnail) => {
if (!focusedItem?.folderUuid) return;

driveCtx.updateItemInTree(focusedItem.folderUuid, focusedItem.id, {
thumbnails: [thumbnail],
});
dispatch(
driveActions.setFocusedItem({
...focusedItem,
thumbnails: [thumbnail],
}),
);
};

useThumbnailRegeneration(
{
downloadedFilePath: downloadingFile?.downloadedFilePath,
fileExtension: focusedItem?.type,
fileUuid: focusedItem?.uuid,
hasThumbnails: !!(focusedItem?.thumbnails && focusedItem.thumbnails.length > 0),
},
{
onSuccess: updateItemWithNewThumbnail,
},
);

useEffect(() => {
Animated.timing(topbarYPosition, {
toValue: topbarVisible ? 0 : -totalHeaderHeight,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { canGenerateThumbnail, shouldRegenerateThumbnail } from './useThumbnailRegeneration';

describe('useThumbnailRegeneration', () => {
describe('canGenerateThumbnail', () => {
it('should return true for image types', () => {
expect(canGenerateThumbnail('png')).toBe(true);
expect(canGenerateThumbnail('PNG')).toBe(true);
expect(canGenerateThumbnail('jpg')).toBe(true);
expect(canGenerateThumbnail('jpeg')).toBe(true);
expect(canGenerateThumbnail('HEIC')).toBe(true);
});

it('should return true for video types', () => {
expect(canGenerateThumbnail('mp4')).toBe(true);
expect(canGenerateThumbnail('MP4')).toBe(true);
expect(canGenerateThumbnail('mov')).toBe(true);
expect(canGenerateThumbnail('avi')).toBe(true);
});

it('should return true for PDF', () => {
expect(canGenerateThumbnail('pdf')).toBe(true);
expect(canGenerateThumbnail('PDF')).toBe(true);
});

it('should return false for unsupported types', () => {
expect(canGenerateThumbnail('txt')).toBe(false);
expect(canGenerateThumbnail('docx')).toBe(false);
expect(canGenerateThumbnail('zip')).toBe(false);
});
});

describe('shouldRegenerateThumbnail', () => {
it('should return true when file is downloaded, has no thumbnails, and is supported type', () => {
const result = shouldRegenerateThumbnail({
downloadedFilePath: '/path/to/file.jpg',
fileExtension: 'jpg',
fileUuid: 'uuid-123',
hasThumbnails: false,
});
expect(result).toBe(true);
});

it('should return false when file already has thumbnails', () => {
const result = shouldRegenerateThumbnail({
downloadedFilePath: '/path/to/file.jpg',
fileExtension: 'jpg',
fileUuid: 'uuid-123',
hasThumbnails: true,
});
expect(result).toBe(false);
});

it('should return false when file is not downloaded', () => {
const result = shouldRegenerateThumbnail({
downloadedFilePath: undefined,
fileExtension: 'jpg',
fileUuid: 'uuid-123',
hasThumbnails: false,
});
expect(result).toBe(false);
});

it('should return false when file type is unsupported', () => {
const result = shouldRegenerateThumbnail({
downloadedFilePath: '/path/to/file.txt',
fileExtension: 'txt',
fileUuid: 'uuid-123',
hasThumbnails: false,
});
expect(result).toBe(false);
});

it('should return false when file extension is missing', () => {
const result = shouldRegenerateThumbnail({
downloadedFilePath: '/path/to/file',
fileExtension: undefined,
fileUuid: 'uuid-123',
hasThumbnails: false,
});
expect(result).toBe(false);
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import { logger } from '@internxt-mobile/services/common';
import { driveFileService } from '@internxt-mobile/services/drive/file';
import errorService from '@internxt-mobile/services/ErrorService';
import { FileExtension } from '@internxt-mobile/types/drive';
import { Thumbnail } from '@internxt/sdk/dist/drive/storage/types';
import { useEffect, useRef, useState } from 'react';

const IMAGE_PREVIEW_TYPES = new Set([FileExtension.PNG, FileExtension.JPG, FileExtension.JPEG, FileExtension.HEIC]);
const VIDEO_PREVIEW_TYPES = new Set([FileExtension.MP4, FileExtension.MOV, FileExtension.AVI]);
const PDF_PREVIEW_TYPES = new Set([FileExtension.PDF]);

interface ThumbnailRegenerationParams {
downloadedFilePath?: string;
fileExtension?: string;
fileUuid?: string;
hasThumbnails: boolean;
}

interface ThumbnailRegenerationCallbacks {
onSuccess: (thumbnail: Thumbnail) => void;
}

export const canGenerateThumbnail = (fileExtension: string): boolean => {
const extension = fileExtension.toLowerCase() as FileExtension;
return IMAGE_PREVIEW_TYPES.has(extension) || VIDEO_PREVIEW_TYPES.has(extension) || PDF_PREVIEW_TYPES.has(extension);
};

export const shouldRegenerateThumbnail = (params: ThumbnailRegenerationParams): boolean => {
const { downloadedFilePath, fileExtension, hasThumbnails } = params;

if (!downloadedFilePath || hasThumbnails || !fileExtension) {
return false;
}

return canGenerateThumbnail(fileExtension);
};

export const regenerateThumbnail = async (
fileUuid: string,
downloadedFilePath: string,
fileExtension: string,
): Promise<Thumbnail | null> => {
try {
const thumbnail = await driveFileService.regenerateThumbnail(
fileUuid,
downloadedFilePath,
fileExtension.toLowerCase(),
);
logger.info('Thumbnail regenerated successfully', { thumbnail });
return thumbnail;
} catch (error) {
logger.info('Thumbnail regeneration error', { error });
errorService.reportError(error);
return null;
}
};

export const useThumbnailRegeneration = (
params: ThumbnailRegenerationParams,
callbacks: ThumbnailRegenerationCallbacks,
) => {
const [isRegenerating, setIsRegenerating] = useState(false);
const regenerationAttempted = useRef(false);

useEffect(() => {
const { downloadedFilePath, fileExtension, fileUuid } = params;

if (regenerationAttempted.current || isRegenerating) {
return;
}

if (!shouldRegenerateThumbnail(params)) {
return;
}

if (!fileUuid || !downloadedFilePath || !fileExtension) {
return;
}

regenerationAttempted.current = true;
setIsRegenerating(true);

regenerateThumbnail(fileUuid, downloadedFilePath, fileExtension)
.then((thumbnail) => {
if (thumbnail) {
callbacks.onSuccess(thumbnail);
}
})
.finally(() => {
setIsRegenerating(false);
});
}, [params.downloadedFilePath, params.hasThumbnails]);

return { isRegenerating, regenerationAttempted: regenerationAttempted.current };
};
Loading
Loading