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
30 changes: 30 additions & 0 deletions assets/lang/strings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,7 @@ const translations = {
descrypting: 'Decrypting...',
cancel: 'Cancel',
cancelling: 'Cancelling',
upload: 'Upload',
confirm: 'Confirm',
move: 'Move',
moveHere: 'Move here',
Expand Down Expand Up @@ -477,6 +478,20 @@ const translations = {
duplicateFilesMessage: 'The following files already exist: %s\n\nDo you want to upload them with a new name?',
duplicateFilesAction: 'Upload with new name',
},
nameCollision: {
title: 'Item already exists',
titleMultiple: 'Some items already exist',
messageSingleFolder:
'"{0}" already exists in this location. Do you want to replace the folder or keep both?',
messageSingleFile:
'"{0}" already exists in this location. Do you want to replace the file or keep both?',
messageMultiple:
'More than one element already exists in this location. Do you want to replace them or keep all?',
replaceCurrentItem: 'Replace current item',
keepBoth: 'Keep both',
replaceAll: 'Replace all',
keepAll: 'Keep all',
},
rename: {
title: 'Rename',
label: 'Name',
Expand Down Expand Up @@ -1134,6 +1149,7 @@ const translations = {
descrypting: 'Desencriptando...',
cancel: 'Cancelar',
cancelling: 'Cancelando',
upload: 'Subir',
confirm: 'Confirmar',
move: 'Mover',
save: 'Guardar',
Expand Down Expand Up @@ -1268,6 +1284,20 @@ const translations = {
duplicateFilesMessage: 'Los siguientes archivos ya existen: %s\n\n¿Quieres subirlos con un nuevo nombre?',
duplicateFilesAction: 'Subir con nuevo nombre',
},
nameCollision: {
title: 'El elemento ya existe',
titleMultiple: 'Algunos elementos ya existen',
messageSingleFolder:
'"{0}" ya existe en esta ubicación. ¿Quieres reemplazar la carpeta o conservar ambas?',
messageSingleFile:
'"{0}" ya existe en esta ubicación. ¿Quieres reemplazar el archivo o conservar ambos?',
messageMultiple:
'Más de un elemento ya existe en esta ubicación. ¿Quieres reemplazarlos o conservar todos?',
replaceCurrentItem: 'Reemplazar el elemento actual',
keepBoth: 'Conservar ambos',
replaceAll: 'Reemplazar todos',
keepAll: 'Conservar todos',
},
rename: {
title: 'Renombrar',
label: 'Nombre',
Expand Down
115 changes: 89 additions & 26 deletions src/components/modals/AddModal/hooks/useFolderUpload.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { StorageAccessFramework } from 'expo-file-system/legacy';
import { useRef, useState } from 'react';
import { Platform } from 'react-native';
import uuid from 'react-native-uuid';

Expand All @@ -8,6 +9,7 @@ import errorService from '@internxt-mobile/services/ErrorService';
import { DriveFileData } from '@internxt-mobile/types/drive/file';
import strings from '../../../../../assets/lang/strings';
import analytics, { DriveAnalyticsEvent } from '../../../../services/AnalyticsService';
import { driveFolderService } from '../../../../services/drive/folder/driveFolder.service';
import {
createFolderWithMerge,
getMaxDepth,
Expand All @@ -16,17 +18,44 @@ import {
import { FolderTooLargeError, folderTraversalService } from '../../../../services/drive/folder/folderTraversal.service';
import { folderUploadService } from '../../../../services/drive/folder/folderUpload.service';
import { folderUploadCancellationService } from '../../../../services/drive/folder/folderUploadCancellation.service';
import { getUniqueFolderName } from '../../../../services/drive/folder/utils/getUniqueFolderName';
import { driveTrashService } from '../../../../services/drive/trash/driveTrash.service';
import fileSystemService from '../../../../services/FileSystemService';
import notificationsService from '../../../../services/NotificationsService';
import { useAppDispatch, useAppSelector } from '../../../../store/hooks';
import { driveActions, driveThunks } from '../../../../store/slices/drive';
import { uiActions } from '../../../../store/slices/ui';
import { NotificationType, ProgressCallback } from '../../../../types';
import { FolderTreeNode } from '../../../../types/drive/folderUpload';
import { NameCollisionAction } from '../../NameCollisionModal';

// eslint-disable-next-line @typescript-eslint/no-empty-function
const noopProgress: ProgressCallback = () => {};

const showFolderUploadResult = (
result: { cancelled: boolean; failedFiles: number; uploadedFiles: number; totalFiles: number },
folderName: string,
) => {
if (result.cancelled) {
notificationsService.show({ type: NotificationType.Info, text1: strings.messages.folderUploadCancelled });
} else if (result.failedFiles === 0) {
notificationsService.show({
type: NotificationType.Success,
text1: strings.formatString(strings.messages.folderUploadCompleted, result.uploadedFiles, folderName) as string,
});
} else {
notificationsService.show({
type: NotificationType.Warning,
text1: strings.formatString(
strings.messages.folderUploadPartial,
result.uploadedFiles,
result.totalFiles,
result.failedFiles,
) as string,
});
}
};

const getFileExtensionAndPlainName = (name: string): { extension: string; plainName: string } => {
const lastDot = name.lastIndexOf('.');
if (lastDot <= 0) return { extension: '', plainName: name };
Expand All @@ -43,11 +72,49 @@ type UploadFileEntryFn = (
creationTime?: string,
) => Promise<DriveFileData>;

export interface FolderUploadCollisionModalState {
isOpen: boolean;
folderName: string;
onConfirm: (action: NameCollisionAction) => void;
onClose: () => void;
}

export const useFolderUpload = ({ uploadAndCreateFileEntry }: { uploadAndCreateFileEntry: UploadFileEntryFn }) => {
const dispatch = useAppDispatch();
const { focusedFolder, loadFolderContent } = useDrive();
const folderUploads = useAppSelector((state) => state.drive.folderUploads);

const collisionResolverRef = useRef<((action: NameCollisionAction | null) => void) | null>(null);
const [collisionState, setCollisionState] = useState<{
isOpen: boolean;
folderName: string;
existingFolderUuid: string;
existingFolderId: number;
}>({ isOpen: false, folderName: '', existingFolderUuid: '', existingFolderId: 0 });

const waitForCollisionResolution = (
folderName: string,
existingFolderUuid: string,
existingFolderId: number,
): Promise<NameCollisionAction | null> => {
return new Promise((resolve) => {
collisionResolverRef.current = resolve;
setCollisionState({ isOpen: true, folderName, existingFolderUuid, existingFolderId });
});
};

const closedCollisionState = { isOpen: false, folderName: '', existingFolderUuid: '', existingFolderId: 0 };

const handleCollisionConfirm = (action: NameCollisionAction) => {
setCollisionState(closedCollisionState);
collisionResolverRef.current?.(action);
};

const handleCollisionClose = () => {
setCollisionState(closedCollisionState);
collisionResolverRef.current?.(null);
};

const handleUploadFolder = async () => {
dispatch(uiActions.setShowUploadFileModal(false));

Expand Down Expand Up @@ -102,6 +169,19 @@ export const useFolderUpload = ({ uploadAndCreateFileEntry }: { uploadAndCreateF
throw new Error('No focused folder UUID');
}

// Name collision check
const { existentFolders } = await driveFolderService.checkDuplicatedFolders(focusedFolder.uuid, [picked.name]);
if (existentFolders.length > 0) {
const existing = existentFolders[0];
const action = await waitForCollisionResolution(picked.name, existing.uuid, existing.id);
if (action === null) return;
if (action === 'replace') {
await driveTrashService.moveToTrash([{ uuid: existing.uuid, id: existing.id, type: 'folder' }]);
} else {
picked.name = await getUniqueFolderName(picked.name, focusedFolder.uuid);
}
}

// 4. Create the root folder (merge if already exists)
const rootFolderUuid = await createFolderWithMerge(focusedFolder.uuid, picked.name);
logger.info(`[useFolderUpload][${uploadId}] Root folder "${picked.name}" - ${rootFolderUuid}`);
Expand Down Expand Up @@ -171,31 +251,7 @@ export const useFolderUpload = ({ uploadAndCreateFileEntry }: { uploadAndCreateF
});

// 9. Display result
if (result.cancelled) {
notificationsService.show({
type: NotificationType.Info,
text1: strings.messages.folderUploadCancelled,
});
} else if (result.failedFiles === 0) {
notificationsService.show({
type: NotificationType.Success,
text1: strings.formatString(
strings.messages.folderUploadCompleted,
result.uploadedFiles,
picked.name,
) as string,
});
} else {
notificationsService.show({
type: NotificationType.Warning,
text1: strings.formatString(
strings.messages.folderUploadPartial,
result.uploadedFiles,
result.totalFiles,
result.failedFiles,
) as string,
});
}
showFolderUploadResult(result, picked.name);
} catch (err) {
const error = err as Error;
if (!(err instanceof FolderTooLargeError)) {
Expand All @@ -217,5 +273,12 @@ export const useFolderUpload = ({ uploadAndCreateFileEntry }: { uploadAndCreateF
}
};

return { handleUploadFolder };
const nameCollisionModal: FolderUploadCollisionModalState = {
isOpen: collisionState.isOpen,
folderName: collisionState.folderName,
onConfirm: handleCollisionConfirm,
onClose: handleCollisionClose,
};

return { handleUploadFolder, nameCollisionModal };
};
12 changes: 11 additions & 1 deletion src/components/modals/AddModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import { DocumentPickerFile, UPLOAD_FILE_SIZE_LIMIT, UploadingFile } from '../..
import AppText from '../../AppText';
import BottomModal from '../BottomModal';
import CreateFolderModal from '../CreateFolderModal';
import NameCollisionModal from '../NameCollisionModal';

const MAX_FILES_BULK_UPLOAD = 50;

Expand All @@ -85,7 +86,7 @@ function AddModal(): JSX.Element {
const { limit } = useAppSelector((state) => state.storage);
const usage = useAppSelector(storageSelectors.usage);
const user = useAppSelector((state) => state.auth.user);
const { handleUploadFolder } = useFolderUpload({ uploadAndCreateFileEntry });
const { handleUploadFolder, nameCollisionModal } = useFolderUpload({ uploadAndCreateFileEntry });

async function uploadIOS(file: UploadingFile, fileType: 'document' | 'image', progressCallback: ProgressCallback) {
const name = file.name ?? decodeURI(file.uri).split('/').pop();
Expand Down Expand Up @@ -938,6 +939,15 @@ function AddModal(): JSX.Element {
onFolderCreated={onFolderCreated}
/>
) : null}
<NameCollisionModal
isOpen={nameCollisionModal.isOpen}
itemName={nameCollisionModal.folderName}
collisionCount={1}
itemType="folder"
confirmLabel={strings.buttons.upload}
onClose={nameCollisionModal.onClose}
onConfirm={nameCollisionModal.onConfirm}
/>
</>
);
}
Expand Down
65 changes: 64 additions & 1 deletion src/components/modals/MoveItemsModal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ import { useTailwind } from 'tailwind-rn';
import { checkIsFile, checkIsFolder } from '../../../helpers';
import useGetColor from '../../../hooks/useColor';
import { logger } from '../../../services/common';
import { driveFolderService } from '../../../services/drive/folder/driveFolder.service';
import { getUniqueFolderName } from '../../../services/drive/folder/utils/getUniqueFolderName';
import { driveTrashService } from '../../../services/drive/trash/driveTrash.service';
import notificationsService from '../../../services/NotificationsService';
import { NotificationType } from '../../../types';
import { RootScreenNavigationProp } from '../../../types/navigation';
Expand All @@ -30,6 +33,7 @@ import DriveItemSkinSkeleton from '../../DriveItemSkinSkeleton';
import BottomModal from '../BottomModal';
import ConfirmMoveItemModal from '../ConfirmMoveItemModal';
import CreateFolderModal from '../CreateFolderModal';
import NameCollisionModal, { NameCollisionAction } from '../NameCollisionModal';
import SortModal, { SortMode } from '../SortModal';

const INITIAL_SORT_MODE: SortMode = {
Expand All @@ -45,6 +49,9 @@ function MoveItemsModal(): JSX.Element {
const navigation = useNavigation<RootScreenNavigationProp<'TabExplorer'>>();
const dispatch = useAppDispatch();
const [confirmModalOpen, setConfirmModalOpen] = useState(false);
const [nameCollisionOpen, setNameCollisionOpen] = useState(false);
const [collidingFolderName, setCollidingFolderName] = useState('');
const [existingCollisionFolder, setExistingCollisionFolder] = useState<{ uuid: string; id: number } | null>(null);
const [sortMode, setSortMode] = useState<SortMode>(INITIAL_SORT_MODE);
const [sortModalOpen, setSortModalOpen] = useState(false);
const [createFolderModalOpen, setCreateFolderModalOpen] = useState(false);
Expand Down Expand Up @@ -112,7 +119,7 @@ function MoveItemsModal(): JSX.Element {
return false;
};

const confirmMoveItem = async () => {
const executeMoveItem = async () => {
if (!originFolderContentResponse || !itemToMove || !originFolderId || !destinationFolderContentResponse?.uuid) {
notificationsService.show({
text1: strings.errors.moveError,
Expand Down Expand Up @@ -194,6 +201,53 @@ function MoveItemsModal(): JSX.Element {
}
};

const confirmMoveItem = async () => {
if (isFolder) {
const folderName = itemToMove?.name ?? '';
try {
const { existentFolders } = await driveFolderService.checkDuplicatedFolders(
destinationFolderContentResponse?.uuid ?? '',
[folderName],
);
if (existentFolders.length > 0) {
setConfirmModalOpen(false);
setCollidingFolderName(folderName);
setExistingCollisionFolder({ uuid: existentFolders[0].uuid, id: existentFolders[0].id });
setNameCollisionOpen(true);
return;
}
} catch {
notificationsService.show({ text1: strings.errors.moveError, type: NotificationType.Error });
return;
}
}

await executeMoveItem();
};

const handleNameCollisionConfirm = async (action: NameCollisionAction) => {
setNameCollisionOpen(false);
try {
if (action === 'replace' && existingCollisionFolder) {
await driveTrashService.moveToTrash([
{ uuid: existingCollisionFolder.uuid, id: existingCollisionFolder.id, type: 'folder' },
]);
} else if (action === 'keep-both' && itemToMove?.uuid && destinationFolderContentResponse?.uuid) {
const uniqueName = await getUniqueFolderName(collidingFolderName, destinationFolderContentResponse.uuid);
await driveFolderService.updateMetaData(itemToMove.uuid as string, uniqueName);
}
} catch {
notificationsService.show({ text1: strings.errors.moveError, type: NotificationType.Error });
return;
}
await executeMoveItem();
};

const handleNameCollisionClose = () => {
setNameCollisionOpen(false);
setConfirmModalOpen(true);
};

const onFolderCreated = async () => {
if (destinationFolderContentResponse?.uuid) {
await loadDestinationFolderContent(destinationFolderContentResponse.uuid);
Expand Down Expand Up @@ -453,6 +507,15 @@ function MoveItemsModal(): JSX.Element {
onFolderCreated={onFolderCreated}
/>
) : null}
<NameCollisionModal
isOpen={nameCollisionOpen}
itemName={collidingFolderName}
collisionCount={1}
itemType="folder"
confirmLabel={strings.buttons.move}
onClose={handleNameCollisionClose}
onConfirm={handleNameCollisionConfirm}
/>
</Portal>
);
}
Expand Down
Loading
Loading