From 1285a2231d786526262770c28ca344217af50311 Mon Sep 17 00:00:00 2001 From: Rushikesh Tote Date: Wed, 23 Mar 2022 13:06:24 +0530 Subject: [PATCH] added rendering for deduplication --- src/components/PhotoFrame.tsx | 12 +- src/components/Sidebar.tsx | 7 + src/components/icons/LeftArrow.tsx | 24 ++ .../deduplicate/SelectedDuplicatesOptions.tsx | 105 +++++++ src/constants/pages/index.ts | 1 + src/pages/deduplicate/index.tsx | 297 ++++++++++++++++++ src/pages/gallery/index.tsx | 2 +- src/utils/strings/englishConstants.tsx | 6 +- 8 files changed, 446 insertions(+), 8 deletions(-) create mode 100644 src/components/icons/LeftArrow.tsx create mode 100644 src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx create mode 100644 src/pages/deduplicate/index.tsx diff --git a/src/components/PhotoFrame.tsx b/src/components/PhotoFrame.tsx index 39fd968af..6a12beaf3 100644 --- a/src/components/PhotoFrame.tsx +++ b/src/components/PhotoFrame.tsx @@ -62,10 +62,10 @@ interface Props { ) => void; selected: SelectedState; isFirstLoad; - openFileUploader; + openFileUploader?; isInSearchMode: boolean; - search: Search; - setSearchStats: setSearchStats; + search?: Search; + setSearchStats?: setSearchStats; deleted?: number[]; activeCollection: number; isSharedCollection: boolean; @@ -147,7 +147,7 @@ const PhotoFrame = ({ timeTaken: (Date.now() - startTime) / 1000, }); } - if (search.fileIndex || search.fileIndex === 0) { + if (search?.fileIndex || search?.fileIndex === 0) { const filteredDataIdx = filteredData.findIndex( (data) => data.dataIndex === search.fileIndex ); @@ -186,7 +186,7 @@ const PhotoFrame = ({ return false; } if ( - search.date && + search?.date && !isSameDayAnyYear(search.date)( new Date(item.metadata.creationTime / 1000) ) @@ -194,7 +194,7 @@ const PhotoFrame = ({ return false; } if ( - search.location && + search?.location && !isInsideBox(item.metadata, search.location) ) { return false; diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index b34d1f2c3..ea8d5b013 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -285,6 +285,13 @@ export default function Sidebar(props: Props) { }}> {constants.UPDATE_EMAIL} + { + router.push(PAGES.DEDUPLICATE); + }}> + {constants.DEDUPLICATE_FILES} + <> + + + ); +} + +LeftArrow.defaultProps = { + height: 32, + width: 32, + viewBox: '0 0 16 16', +}; diff --git a/src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx b/src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx new file mode 100644 index 000000000..956d0d088 --- /dev/null +++ b/src/components/pages/deduplicate/SelectedDuplicatesOptions.tsx @@ -0,0 +1,105 @@ +import { IconButton } from 'components/Container'; +import CloseIcon from 'components/icons/CloseIcon'; +import DeleteIcon from 'components/icons/DeleteIcon'; +import { SetDialogMessage } from 'components/MessageDialog'; +import Navbar from 'components/Navbar'; +import React from 'react'; +import { OverlayTrigger } from 'react-bootstrap'; +import styled from 'styled-components'; +import constants from 'utils/strings/constants'; + +interface Props { + setDialogMessage: SetDialogMessage; + deleteFileHelper: () => void; + count: number; + clearSelection: () => void; + clubByTime: boolean; + setClubByTime: (clubByTime: boolean) => void; +} + +const SelectionBar = styled(Navbar)` + position: fixed; + top: 0; + color: #fff; + z-index: 1001; + width: 100%; + padding: 0 16px; +`; + +const SelectionContainer = styled.div` + flex: 1; + align-items: center; + display: flex; +`; + +interface IconWithMessageProps { + children?: any; + message: string; +} +export const IconWithMessage = (props: IconWithMessageProps) => ( + {props.message}

}> + {props.children} +
+); + +const SelectedFileOptions = ({ + setDialogMessage, + deleteFileHelper, + count, + clearSelection, + clubByTime, + setClubByTime, +}: Props) => { + const trashHandler = () => + setDialogMessage({ + title: constants.CONFIRM_DELETE, + content: constants.TRASH_MESSAGE, + staticBackdrop: true, + proceed: { + action: deleteFileHelper, + text: constants.MOVE_TO_TRASH, + variant: 'danger', + }, + close: { text: constants.CANCEL }, + }); + + return ( + + + + + +
+ {count} {constants.SELECTED} +
+
+ <> + { + setClubByTime(!clubByTime); + }}> +
+ {constants.CLUB_BY_CAPTURE_TIME} +
+ + + + + + +
+ ); +}; + +export default SelectedFileOptions; diff --git a/src/constants/pages/index.ts b/src/constants/pages/index.ts index b79051e1d..083ed0ac3 100644 --- a/src/constants/pages/index.ts +++ b/src/constants/pages/index.ts @@ -13,6 +13,7 @@ export enum PAGES { VERIFY = '/verify', ROOT = '/', SHARED_ALBUMS = '/shared-albums', + DEDUPLICATE = '/deduplicate', } export const getAlbumSiteHost = () => process.env.NODE_ENV === 'production' diff --git a/src/pages/deduplicate/index.tsx b/src/pages/deduplicate/index.tsx new file mode 100644 index 000000000..e4d1ae9f0 --- /dev/null +++ b/src/pages/deduplicate/index.tsx @@ -0,0 +1,297 @@ +import EnteSpinner from 'components/EnteSpinner'; +import LeftArrow from 'components/icons/LeftArrow'; +import { LoadingOverlay } from 'components/LoadingOverlay'; +import MessageDialog, { MessageAttributes } from 'components/MessageDialog'; +import SelectedDuplicatesOptions from 'components/pages/deduplicate/SelectedDuplicatesOptions'; +import AlertBanner from 'components/pages/gallery/AlertBanner'; +import PhotoFrame from 'components/PhotoFrame'; +import ToastNotification from 'components/ToastNotification'; +import { ALL_SECTION } from 'constants/collection'; +import { PAGES } from 'constants/pages'; +import { useRouter } from 'next/router'; +import { defaultGalleryContext, GalleryContext } from 'pages/gallery'; +import { AppContext } from 'pages/_app'; +import React, { useContext, useEffect, useRef, useState } from 'react'; +import LoadingBar from 'react-top-loading-bar'; +import { syncCollections } from 'services/collectionService'; +import deduplicationService from 'services/deduplicationService'; +import { getLocalFiles, syncFiles, trashFiles } from 'services/fileService'; +import { getLocalTrash, getTrashedFiles } from 'services/trashService'; +import { isTokenValid, logoutUser } from 'services/userService'; +import { EnteFile } from 'types/file'; +import { NotificationAttributes, SelectedState } from 'types/gallery'; +import { checkConnectivity } from 'utils/common'; +import { CustomError, ServerErrorCodes } from 'utils/error'; +import { getSelectedFiles, mergeMetadata, sortFiles } from 'utils/file'; +import { isFirstLogin, setIsFirstLogin, setJustSignedUp } from 'utils/storage'; +import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; +import constants from 'utils/strings/constants'; + +export default function Deduplicate() { + const router = useRouter(); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [localFiles, setLocalFiles] = useState(null); + const [duplicateFiles, setDuplicateFiles] = useState([]); + const [bannerMessage, setBannerMessage] = useState( + null + ); + const [isFirstLoad, setIsFirstLoad] = useState(false); + const [selected, setSelected] = useState({ + count: 0, + collectionID: 0, + }); + const [dialogMessage, setDialogMessage] = useState(); + const [messageDialogView, setMessageDialogView] = useState(false); + const [blockingLoad, setBlockingLoad] = useState(false); + const loadingBar = useRef(null); + const isLoadingBarRunning = useRef(false); + const syncInProgress = useRef(true); + const resync = useRef(false); + const appContext = useContext(AppContext); + const [clubByTime, setClubByTime] = useState(false); + + const [notificationAttributes, setNotificationAttributes] = + useState(null); + + const closeMessageDialog = () => setMessageDialogView(false); + + const clearNotificationAttributes = () => setNotificationAttributes(null); + + useEffect(() => { + const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); + if (!key) { + appContext.setRedirectURL(router.asPath); + router.push(PAGES.ROOT); + return; + } + const main = async () => { + setIsFirstLoad(isFirstLogin()); + setIsFirstLogin(false); + const localFiles = mergeMetadata(await getLocalFiles()); + const trash = await getLocalTrash(); + const trashedFile = getTrashedFiles(trash); + setLocalFiles(sortFiles([...localFiles, ...trashedFile])); + + await syncWithRemote(true); + setIsFirstLoad(false); + setJustSignedUp(false); + }; + main(); + appContext.showNavBar(true); + }, []); + + useEffect(() => setMessageDialogView(true), [dialogMessage]); + + const syncWithRemote = async (force = false, silent = false) => { + if (syncInProgress.current && !force) { + resync.current = true; + return; + } + syncInProgress.current = true; + try { + checkConnectivity(); + if (!(await isTokenValid())) { + throw new Error(ServerErrorCodes.SESSION_EXPIRED); + } + !silent && startLoading(); + + const collections = await syncCollections(); + await syncFiles(collections, setLocalFiles); + + let duplicates = await deduplicationService.getDuplicateFiles(); + if (clubByTime) { + duplicates = await deduplicationService.clubDuplicatesByTime( + duplicates + ); + } + + let allDuplicateFiles: EnteFile[] = []; + let toSelectFileIDs: number[] = []; + let count = 0; + + for (const dupe of duplicates) { + allDuplicateFiles = allDuplicateFiles.concat(dupe.files); + // select all except first file + toSelectFileIDs = toSelectFileIDs.concat( + dupe.files.slice(1).map((f) => f.id) + ); + count += dupe.files.length - 1; + } + setDuplicateFiles(allDuplicateFiles); + + const selectedFiles = { + count: count, + collectionID: 0, + }; + + for (const fileID of toSelectFileIDs) { + selectedFiles[fileID] = true; + } + setSelected(selectedFiles); + } catch (e) { + switch (e.message) { + case ServerErrorCodes.SESSION_EXPIRED: + setBannerMessage(constants.SESSION_EXPIRED_MESSAGE); + setDialogMessage({ + title: constants.SESSION_EXPIRED, + content: constants.SESSION_EXPIRED_MESSAGE, + staticBackdrop: true, + nonClosable: true, + proceed: { + text: constants.LOGIN, + action: logoutUser, + variant: 'success', + }, + }); + break; + case CustomError.KEY_MISSING: + clearKeys(); + router.push(PAGES.CREDENTIALS); + break; + } + } finally { + !silent && finishLoading(); + } + syncInProgress.current = false; + if (resync.current) { + resync.current = false; + syncWithRemote(); + } + }; + + useEffect(() => { + startLoading(); + const sync = async () => { + await syncWithRemote(); + }; + sync(); + finishLoading(); + }, [clubByTime]); + + const clearSelection = function () { + setSelected({ count: 0, collectionID: 0 }); + }; + + const startLoading = () => { + !isLoadingBarRunning.current && loadingBar.current?.continuousStart(); + isLoadingBarRunning.current = true; + }; + const finishLoading = () => { + isLoadingBarRunning.current && loadingBar.current?.complete(); + isLoadingBarRunning.current = false; + }; + + if (!duplicateFiles) { + return
; + } + + const deleteFileHelper = async () => { + startLoading(); + try { + const selectedFiles = getSelectedFiles(selected, duplicateFiles); + await trashFiles(selectedFiles); + clearSelection(); + } catch (e) { + switch (e.status?.toString()) { + case ServerErrorCodes.FORBIDDEN: + setDialogMessage({ + title: constants.ERROR, + staticBackdrop: true, + close: { variant: 'danger' }, + content: constants.NOT_FILE_OWNER, + }); + } + setDialogMessage({ + title: constants.ERROR, + staticBackdrop: true, + close: { variant: 'danger' }, + content: constants.UNKNOWN_ERROR, + }); + } finally { + await syncWithRemote(false, true); + finishLoading(); + } + }; + + return ( + + {blockingLoad && ( + + + + )} + + + + + + {duplicateFiles.length > 0 ? ( + + ) : ( + + {constants.NO_DUPLICATES_FOUND} + + )} + + {selected.count > 0 ? ( + + ) : ( +
{ + router.push(PAGES.GALLERY); + }}> + +
+ )} +
+ ); +} diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index ec9b413c9..ae3ee9e7a 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -121,7 +121,7 @@ const AlertContainer = styled.div` text-align: center; `; -const defaultGalleryContext: GalleryContextType = { +export const defaultGalleryContext: GalleryContextType = { thumbs: new Map(), files: new Map(), showPlanSelectorModal: () => null, diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index 7cbd7e346..1f3cb0d3d 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -684,10 +684,14 @@ const englishConstants = { LIVE_PHOTO: 'this is a live photo', LIVE: 'LIVE', DISABLE_PASSWORD: 'disable password lock', - DISABLE_PASSWORD_MESSAGE: 'are you sure that you want to disable the password lock?', + DISABLE_PASSWORD_MESSAGE: + 'are you sure that you want to disable the password lock?', PASSWORD_LOCK: 'password lock', LOCK: 'lock', DOWNLOAD_UPLOAD_LOGS: 'debug logs', + DEDUPLICATE_FILES: 'deduplicate files', + NO_DUPLICATES_FOUND: "you've no duplicate files that can be cleared", + CLUB_BY_CAPTURE_TIME: 'club by capture time', }; export default englishConstants;