Merge pull request #152 from ente-io/move-files

Move files
This commit is contained in:
Abhinav-grd 2021-09-22 12:25:16 +05:30 committed by GitHub
commit 896a450762
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 262 additions and 70 deletions

View file

@ -39,7 +39,6 @@ const Overlay = styled.div`
type Props = React.PropsWithChildren<{
getRootProps: any;
getInputProps: any;
showCollectionSelector;
}>;
export default function FullScreenDropZone(props: Props) {
@ -58,10 +57,6 @@ export default function FullScreenDropZone(props: Props) {
<DropDiv
{...props.getRootProps({
onDragEnter,
onDrop: (e) => {
e.preventDefault();
props.showCollectionSelector();
},
})}>
<input {...props.getInputProps()} />
{isDragActive && (

View file

@ -0,0 +1,23 @@
import React from 'react';
export default function MoveIcon(props) {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}>
<path
d="M13.025 1l-2.847 2.828 6.176 6.176h-16.354v3.992h16.354l-6.176 6.176 2.847 2.828 10.975-11z"
strokeWidth="2"
stroke="black"
/>
</svg>
);
}
MoveIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -1,4 +1,4 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { Card, Modal } from 'react-bootstrap';
import styled from 'styled-components';
import {
@ -23,6 +23,7 @@ export interface CollectionSelectorAttributes {
callback: (collection: Collection) => void;
showNextModal: () => void;
title: string;
fromCollection?: number;
}
export type SetCollectionSelectorAttributes = React.Dispatch<
React.SetStateAction<CollectionSelectorAttributes>
@ -45,30 +46,46 @@ function CollectionSelector({
collectionsAndTheirLatestFile,
...props
}: Props) {
const [collectionToShow, setCollectionToShow] = useState<
CollectionAndItsLatestFile[]
>([]);
useEffect(() => {
if (!attributes) {
return;
}
const collectionsOtherThanFrom = collectionsAndTheirLatestFile?.filter(
(item) => !(item.collection.id === attributes.fromCollection)
);
if (collectionsOtherThanFrom.length === 0) {
props.onHide();
attributes.showNextModal();
} else {
setCollectionToShow(collectionsOtherThanFrom);
}
}, [props.show]);
if (!attributes) {
return <Modal />;
}
const CollectionIcons: JSX.Element[] = collectionsAndTheirLatestFile?.map(
(item) => (
<CollectionIcon
key={item.collection.id}
onClick={() => {
attributes.callback(item.collection);
props.onHide();
}}>
<CollectionCard>
<PreviewCard
file={item.file}
updateUrl={() => {}}
forcedEnable
/>
<Card.Text className="text-center">
{item.collection.name}
</Card.Text>
</CollectionCard>
</CollectionIcon>
)
);
const CollectionIcons: JSX.Element[] = collectionToShow?.map((item) => (
<CollectionIcon
key={item.collection.id}
onClick={() => {
attributes.callback(item.collection);
props.onHide();
}}>
<CollectionCard>
<PreviewCard
file={item.file}
updateUrl={() => {}}
forcedEnable
/>
<Card.Text className="text-center">
{item.collection.name}
</Card.Text>
</CollectionCard>
</CollectionIcon>
));
return (
<Modal

View file

@ -8,15 +8,19 @@ import CrossIcon from 'components/icons/CrossIcon';
import AddIcon from 'components/icons/AddIcon';
import { IconButton } from 'components/Container';
import constants from 'utils/strings/constants';
import MoveIcon from 'components/icons/MoveIcon';
import { COLLECTION_OPS_TYPE } from 'utils/collection';
interface Props {
addToCollectionHelper: (collectionName, collection) => void;
showCreateCollectionModal: () => void;
moveToCollectionHelper: (collectionName, collection) => void;
showCreateCollectionModal: (opsType: COLLECTION_OPS_TYPE) => () => void;
setDialogMessage: SetDialogMessage;
setCollectionSelectorAttributes: SetCollectionSelectorAttributes;
deleteFileHelper: () => void;
count: number;
clearSelection: () => void;
activeCollection: number;
}
const SelectionBar = styled(Navbar)`
@ -35,18 +39,21 @@ const SelectionContainer = styled.div`
const SelectedFileOptions = ({
addToCollectionHelper,
moveToCollectionHelper,
showCreateCollectionModal,
setDialogMessage,
setCollectionSelectorAttributes,
deleteFileHelper,
count,
clearSelection,
activeCollection,
}: Props) => {
const addToCollection = () =>
setCollectionSelectorAttributes({
callback: (collection) => addToCollectionHelper(null, collection),
showNextModal: showCreateCollectionModal,
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.ADD),
title: constants.ADD_TO_COLLECTION,
fromCollection: activeCollection,
});
const deleteHandler = () =>
@ -62,6 +69,15 @@ const SelectedFileOptions = ({
close: { text: constants.CANCEL },
});
const moveToCollection = () => {
setCollectionSelectorAttributes({
callback: (collection) => moveToCollectionHelper(null, collection),
showNextModal: showCreateCollectionModal(COLLECTION_OPS_TYPE.MOVE),
title: constants.MOVE_TO_COLLECTION,
fromCollection: activeCollection,
});
};
return (
<SelectionBar>
<SelectionContainer>
@ -72,6 +88,11 @@ const SelectedFileOptions = ({
{count} {constants.SELECTED}
</div>
</SelectionContainer>
{activeCollection !== 0 && (
<IconButton onClick={moveToCollection}>
<MoveIcon />
</IconButton>
)}
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>

View file

@ -45,7 +45,6 @@ import EnteSpinner from 'components/EnteSpinner';
import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame';
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
import { addFilesToCollection } from 'utils/collection';
import SearchBar, { DateValue } from 'components/SearchBar';
import { Bbox } from 'services/searchService';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
@ -63,6 +62,11 @@ import Collections from 'components/pages/gallery/Collections';
import { AppContext } from 'pages/_app';
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
import { PAGES } from 'types';
import {
copyOrMoveFromCollection,
COLLECTION_OPS_TYPE,
} from 'utils/collection';
import { logError } from 'utils/sentry';
export const DeadCenter = styled.div`
flex: 1;
@ -295,7 +299,8 @@ export default function Gallery() {
) => {
loadingBar.current?.continuousStart();
try {
await addFilesToCollection(
await copyOrMoveFromCollection(
COLLECTION_OPS_TYPE.ADD,
setCollectionSelectorView,
selected,
files,
@ -315,14 +320,68 @@ export default function Gallery() {
}
};
const showCreateCollectionModal = () =>
setCollectionNamerAttributes({
title: constants.CREATE_COLLECTION,
buttonText: constants.CREATE,
autoFilledName: '',
callback: (collectionName) =>
addToCollectionHelper(collectionName, null),
});
const moveToCollectionHelper = async (
collectionName: string,
collection: Collection
) => {
loadingBar.current?.continuousStart();
try {
await copyOrMoveFromCollection(
COLLECTION_OPS_TYPE.MOVE,
setCollectionSelectorView,
selected,
files,
clearSelection,
syncWithRemote,
setActiveCollection,
collectionName,
collection
);
} catch (e) {
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.UNKNOWN_ERROR,
});
}
};
const showCreateCollectionModal = (opsType: COLLECTION_OPS_TYPE) => {
try {
let callback = null;
switch (opsType) {
case COLLECTION_OPS_TYPE.ADD:
callback = (collectionName: string) =>
addToCollectionHelper(collectionName, null);
break;
case COLLECTION_OPS_TYPE.MOVE:
callback = (collectionName: string) =>
moveToCollectionHelper(collectionName, null);
break;
default:
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
}
return () =>
setCollectionNamerAttributes({
title: constants.CREATE_COLLECTION,
buttonText: constants.CREATE,
autoFilledName: '',
callback,
});
} catch (e) {
logError(
e,
'showCreateCollectionModal called with incorrect attributes'
);
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.UNKNOWN_ERROR,
});
}
};
const deleteFileHelper = async () => {
loadingBar.current?.continuousStart();
@ -368,11 +427,7 @@ export default function Gallery() {
<GalleryContext.Provider value={defaultGalleryContext}>
<FullScreenDropZone
getRootProps={getRootProps}
getInputProps={getInputProps}
showCollectionSelector={setCollectionSelectorView.bind(
null,
true
)}>
getInputProps={getInputProps}>
{loading && (
<LoadingOverlay>
<EnteSpinner />
@ -488,6 +543,7 @@ export default function Gallery() {
selected.collectionID === activeCollection && (
<SelectedFileOptions
addToCollectionHelper={addToCollectionHelper}
moveToCollectionHelper={moveToCollectionHelper}
showCreateCollectionModal={
showCreateCollectionModal
}
@ -498,6 +554,7 @@ export default function Gallery() {
deleteFileHelper={deleteFileHelper}
count={selected.count}
clearSelection={clearSelection}
activeCollection={activeCollection}
/>
)}
</FullScreenDropZone>

View file

@ -41,6 +41,23 @@ export interface Collection {
isDeleted: boolean;
}
interface EncryptedFileKey {
id: number;
encryptedKey: string;
keyDecryptionNonce: string;
}
interface AddToCollectionRequest {
collectionID: number;
files: EncryptedFileKey[];
}
interface MoveToCollectionRequest {
fromCollectionID: number;
toCollectionID: number;
files: EncryptedFileKey[];
}
interface collectionAttributes {
encryptedPath?: string;
pathDecryptionNonce?: string;
@ -344,39 +361,78 @@ export const addToCollection = async (
files: File[]
) => {
try {
const params = {};
const worker = await new CryptoWorker();
const token = getToken();
params['collectionID'] = collection.id;
await Promise.all(
files.map(async (file) => {
file.collectionID = collection.id;
const newEncryptedKey: B64EncryptionResult =
await worker.encryptToB64(file.key, collection.key);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
if (params['files'] === undefined) {
params['files'] = [];
}
params['files'].push({
id: file.id,
encryptedKey: file.encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce,
});
return file;
})
);
const fileKeysEncryptedWithNewCollection =
await encryptWithNewCollectionKey(collection, files);
const requestBody: AddToCollectionRequest = {
collectionID: collection.id,
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${ENDPOINT}/collections/add-files`,
params,
requestBody,
null,
{ 'X-Auth-Token': token }
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'Add to collection Failed ');
throw e;
}
};
export const moveToCollection = async (
fromCollectionID: number,
toCollection: Collection,
files: File[]
) => {
try {
const token = getToken();
const fileKeysEncryptedWithNewCollection =
await encryptWithNewCollectionKey(toCollection, files);
const requestBody: MoveToCollectionRequest = {
fromCollectionID: fromCollectionID,
toCollectionID: toCollection.id,
files: fileKeysEncryptedWithNewCollection,
};
await HTTPService.post(
`${ENDPOINT}/collections/move-files`,
requestBody,
null,
{
'X-Auth-Token': token,
}
);
} catch (e) {
logError(e, 'move to collection Failed ');
throw e;
}
};
const encryptWithNewCollectionKey = async (
newCollection: Collection,
files: File[]
): Promise<EncryptedFileKey[]> => {
const fileKeysEncryptedWithNewCollection: EncryptedFileKey[] = [];
const worker = await new CryptoWorker();
for (const file of files) {
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(
file.key,
newCollection.key
);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
fileKeysEncryptedWithNewCollection.push({
id: file.id,
encryptedKey: file.encryptedKey,
keyDecryptionNonce: file.keyDecryptionNonce,
});
}
return fileKeysEncryptedWithNewCollection;
};
const removeFromCollection = async (collection: Collection, files: File[]) => {
try {
const params = {};

View file

@ -3,13 +3,21 @@ import {
Collection,
CollectionType,
createCollection,
moveToCollection,
} from 'services/collectionService';
import { getSelectedFiles } from 'utils/file';
import { File } from 'services/fileService';
import { CustomError } from 'utils/common/errorUtil';
import { SelectedState } from 'pages/gallery';
export async function addFilesToCollection(
export enum COLLECTION_OPS_TYPE {
ADD,
MOVE,
}
export async function copyOrMoveFromCollection(
type: COLLECTION_OPS_TYPE,
setCollectionSelectorView: (value: boolean) => void,
selected: any,
selected: SelectedState,
files: File[],
clearSelection: () => void,
syncWithRemote: () => Promise<void>,
@ -28,7 +36,20 @@ export async function addFilesToCollection(
collection = existingCollection;
}
const selectedFiles = getSelectedFiles(selected, files);
await addToCollection(collection, selectedFiles);
switch (type) {
case COLLECTION_OPS_TYPE.ADD:
await addToCollection(collection, selectedFiles);
break;
case COLLECTION_OPS_TYPE.MOVE:
await moveToCollection(
selected.collectionID,
collection,
selectedFiles
);
break;
default:
throw Error(CustomError.INVALID_COLLECTION_OPERATION);
}
clearSelection();
await syncWithRemote();
setActiveCollection(collection.id);

View file

@ -26,6 +26,7 @@ export enum CustomError {
TYPE_DETECTION_FAILED = 'type detection failed',
SIGNUP_FAILED = 'signup failed',
FAV_COLLECTION_MISSING = 'favorite collection missing',
INVALID_COLLECTION_OPERATION = 'invalid collection operation',
}
function parseUploadError(error: AxiosResponse) {

View file

@ -542,6 +542,7 @@ const englishConstants = {
TOO_LARGE_INFO:
'these files were not uploaded as they exceed the maximum size limit for your storage plan',
UPLOAD_TO_COLLECTION: 'upload to album',
MOVE_TO_COLLECTION: 'move to collection',
};
export default englishConstants;