diff --git a/src/components/BackButton.tsx b/src/components/BackButton.tsx deleted file mode 100644 index c357d1013..000000000 --- a/src/components/BackButton.tsx +++ /dev/null @@ -1,20 +0,0 @@ -import React from 'react'; -import { IconButton } from './Container'; -import LeftArrow from './icons/LeftArrow'; - -export default function BackButton({ setIsDeduplicating }) { - return ( - { - setIsDeduplicating(false); - }}> - - - ); -} diff --git a/src/components/ClubDuplicateFilesByTime.tsx b/src/components/ClubDuplicateFilesByTime.tsx deleted file mode 100644 index 2e04e9cac..000000000 --- a/src/components/ClubDuplicateFilesByTime.tsx +++ /dev/null @@ -1,43 +0,0 @@ -import { GalleryContext } from 'pages/gallery'; -import React, { useContext } from 'react'; -import styled from 'styled-components'; -import constants from 'utils/strings/constants'; - -const Wrapper = styled.div` - position: fixed; - display: flex; - align-items: center; - justify-content: center; - top: 0; - z-index: 1002; - min-height: 64px; - right: 64px; -`; - -export default function ClubDuplicateFilesByTime() { - const galleryContext = useContext(GalleryContext); - return ( - - { - galleryContext.setClubSameTimeFilesOnly( - !galleryContext.clubSameTimeFilesOnly - ); - }}> -
- {constants.CLUB_BY_CAPTURE_TIME} -
-
- ); -} diff --git a/src/components/DeleteBtn.tsx b/src/components/DeleteBtn.tsx index a7d2cd6cb..e1a86c45c 100644 --- a/src/components/DeleteBtn.tsx +++ b/src/components/DeleteBtn.tsx @@ -1,7 +1,7 @@ import React from 'react'; import styled from 'styled-components'; import constants from 'utils/strings/constants'; -import { IconWithMessage } from './pages/gallery/SelectedFileOptions'; +import { IconWithMessage } from './pages/gallery/SelectedFileOptions/GalleryOptions'; const Wrapper = styled.button` border: none; diff --git a/src/components/EmptyScreen.tsx b/src/components/EmptyScreen.tsx index bc39642f5..dfada99f9 100644 --- a/src/components/EmptyScreen.tsx +++ b/src/components/EmptyScreen.tsx @@ -2,7 +2,7 @@ import React, { useContext } from 'react'; import { Button } from 'react-bootstrap'; import styled from 'styled-components'; import constants from 'utils/strings/constants'; -import { GalleryContext } from 'pages/gallery'; +import { DeduplicateContext } from 'pages/deduplicate'; const Wrapper = styled.div` display: flex; @@ -18,10 +18,10 @@ const Wrapper = styled.div` `; export default function EmptyScreen({ openFileUploader }) { - const galleryContext = useContext(GalleryContext); + const deduplicateContext = useContext(DeduplicateContext); return ( - {galleryContext.isDeduplicating ? ( + {deduplicateContext.isOnDeduplicatePage ? ( Promise; - favItemIds: Set; + favItemIds?: Set; setSelected: ( selected: SelectedState | ((selected: SelectedState) => SelectedState) ) => void; selected: SelectedState; - isFirstLoad; + isFirstLoad?; openFileUploader?; - isInSearchMode: boolean; + isInSearchMode?: boolean; search?: Search; setSearchStats?: setSearchStats; deleted?: number[]; activeCollection: number; - isSharedCollection: boolean; - enableDownload: boolean; + isSharedCollection?: boolean; + enableDownload?: boolean; } type SourceURL = { @@ -88,6 +89,7 @@ const PhotoFrame = ({ const startTime = Date.now(); const galleryContext = useContext(GalleryContext); const appContext = useContext(AppContext); + const deduplicateContext = useContext(DeduplicateContext); const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext ); @@ -171,7 +173,7 @@ const PhotoFrame = ({ }), })) .filter((item) => { - if (deleted.includes(item.id)) { + if (deleted?.includes(item.id)) { return false; } if ( @@ -550,7 +552,7 @@ const PhotoFrame = ({ showAppDownloadBanner={ files.length < 30 && !isInSearchMode && - !galleryContext.isDeduplicating + !deduplicateContext.isOnDeduplicatePage } resetFetching={resetFetching} /> diff --git a/src/components/PhotoList.tsx b/src/components/PhotoList.tsx index 5814fef71..5f8ebec7c 100644 --- a/src/components/PhotoList.tsx +++ b/src/components/PhotoList.tsx @@ -15,8 +15,8 @@ import constants from 'utils/strings/constants'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; import { ENTE_WEBSITE_LINK } from 'constants/urls'; import { getVariantColor, ButtonVariant } from './pages/gallery/LinkButton'; -import { GalleryContext } from 'pages/gallery'; import { convertBytesToHumanReadable } from 'utils/billing'; +import { DeduplicateContext } from 'pages/deduplicate'; const A_DAY = 24 * 60 * 60 * 1000; const NO_OF_PAGES = 2; @@ -159,7 +159,7 @@ export function PhotoList({ const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext ); - const galleryContext = useContext(GalleryContext); + const deduplicateContext = useContext(DeduplicateContext); let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH); let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT; @@ -178,16 +178,13 @@ export function PhotoList({ useEffect(() => { let timeStampList: TimeStampListItem[] = []; - if (galleryContext.isDeduplicating) { + if (deduplicateContext.isOnDeduplicatePage) { + skipMerge = true; groupByFileSize(timeStampList); } else { groupByTime(timeStampList); } - if (galleryContext.isDeduplicating) { - skipMerge = true; - } - if (!skipMerge) { timeStampList = mergeTimeStampList(timeStampList, columns); } @@ -221,16 +218,16 @@ export function PhotoList({ let index = 0; while (index < filteredData.length) { const file = filteredData[index]; - const currentFileSize = galleryContext.fileSizeMap.get(file.id); + const currentFileSize = deduplicateContext.fileSizeMap.get(file.id); const currentCreationTime = file.metadata.creationTime; let lastFileIndex = index; while (lastFileIndex < filteredData.length) { if ( - galleryContext.fileSizeMap.get( + deduplicateContext.fileSizeMap.get( filteredData[lastFileIndex].id ) !== currentFileSize || - (galleryContext.clubSameTimeFilesOnly && + (deduplicateContext.clubSameTimeFilesOnly && filteredData[lastFileIndex].metadata.creationTime !== currentCreationTime) ) { diff --git a/src/components/Sidebar.tsx b/src/components/Sidebar.tsx index 5e357912d..17a0bf404 100644 --- a/src/components/Sidebar.tsx +++ b/src/components/Sidebar.tsx @@ -290,7 +290,7 @@ export default function Sidebar(props: Props) { { - galleryContext.setIsDeduplicating(true); + router.push(PAGES.DEDUPLICATE); }}> {constants.DEDUPLICATE_FILES} diff --git a/src/components/pages/gallery/CollectionSort.tsx b/src/components/pages/gallery/CollectionSort.tsx index 7f0c52b6b..88ccc68e0 100644 --- a/src/components/pages/gallery/CollectionSort.tsx +++ b/src/components/pages/gallery/CollectionSort.tsx @@ -5,7 +5,7 @@ import { OverlayTrigger } from 'react-bootstrap'; import { COLLECTION_SORT_BY } from 'constants/collection'; import constants from 'utils/strings/constants'; import CollectionSortOptions from './CollectionSortOptions'; -import { IconWithMessage } from './SelectedFileOptions'; +import { IconWithMessage } from './SelectedFileOptions/GalleryOptions'; interface Props { setCollectionSortBy: (sortBy: COLLECTION_SORT_BY) => void; diff --git a/src/components/pages/gallery/SelectedFileOptions/DeduplicateOptions.tsx b/src/components/pages/gallery/SelectedFileOptions/DeduplicateOptions.tsx new file mode 100644 index 000000000..27abc03f7 --- /dev/null +++ b/src/components/pages/gallery/SelectedFileOptions/DeduplicateOptions.tsx @@ -0,0 +1,99 @@ +import { IconButton } from 'components/Container'; +import { + IconWithMessage, + SelectionBar, + SelectionContainer, +} from './GalleryOptions'; +import constants from 'utils/strings/constants'; +import DeleteIcon from 'components/icons/DeleteIcon'; +import React, { useContext } from 'react'; +import styled from 'styled-components'; +import { DeduplicateContext } from 'pages/deduplicate'; +import CloseIcon from 'components/icons/CloseIcon'; +import LeftArrow from 'components/icons/LeftArrow'; +import { SetDialogMessage } from 'components/MessageDialog'; + +const VerticalLine = styled.div` + position: absolute; + width: 1px; + top: 0; + bottom: 0; + background: #303030; +`; + +interface IProps { + deleteFileHelper: () => void; + setDialogMessage: SetDialogMessage; + close: () => void; + count: number; +} + +export default function DeduplicateOptions({ + setDialogMessage, + deleteFileHelper, + close, + count, +}: IProps) { + const deduplicateContext = useContext(DeduplicateContext); + + 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 ( + + + + {deduplicateContext.isOnDeduplicatePage ? ( + + ) : ( + + )} + +
+ {count} {constants.SELECTED} +
+
+ + { + deduplicateContext.setClubSameTimeFilesOnly( + !deduplicateContext.clubSameTimeFilesOnly + ); + }}> +
+ {constants.CLUB_BY_CAPTURE_TIME} +
+
+ +
+ + + + + +
+ ); +} diff --git a/src/components/pages/gallery/SelectedFileOptions/DeduplicatingOptions.tsx b/src/components/pages/gallery/SelectedFileOptions/DeduplicatingOptions.tsx deleted file mode 100644 index a24da218b..000000000 --- a/src/components/pages/gallery/SelectedFileOptions/DeduplicatingOptions.tsx +++ /dev/null @@ -1,29 +0,0 @@ -import { IconButton } from 'components/Container'; -import { IconWithMessage } from '.'; -import constants from 'utils/strings/constants'; -import DeleteIcon from 'components/icons/DeleteIcon'; -import React from 'react'; -import styled from 'styled-components'; - -const VerticalLine = styled.div` - position: absolute; - width: 1px; - top: 0; - bottom: 0; - background: #303030; -`; - -export default function DeduplicatingOptions({ trashHandler }) { - return ( - <> -
- -
- - - - - - - ); -} diff --git a/src/components/pages/gallery/SelectedFileOptions/index.tsx b/src/components/pages/gallery/SelectedFileOptions/GalleryOptions.tsx similarity index 95% rename from src/components/pages/gallery/SelectedFileOptions/index.tsx rename to src/components/pages/gallery/SelectedFileOptions/GalleryOptions.tsx index 47d49dc67..695401ee6 100644 --- a/src/components/pages/gallery/SelectedFileOptions/index.tsx +++ b/src/components/pages/gallery/SelectedFileOptions/GalleryOptions.tsx @@ -1,5 +1,5 @@ import { SetDialogMessage } from 'components/MessageDialog'; -import React, { useEffect, useState, useContext } from 'react'; +import React, { useEffect, useState } from 'react'; import { SetCollectionSelectorAttributes } from '../CollectionSelector'; import styled from 'styled-components'; import Navbar from 'components/Navbar'; @@ -17,7 +17,6 @@ import { TRASH_SECTION, } from 'constants/collection'; import UnArchive from 'components/icons/UnArchive'; -import { OverlayTrigger } from 'react-bootstrap'; import { Collection } from 'types/collection'; import RemoveIcon from 'components/icons/RemoveIcon'; import RestoreIcon from 'components/icons/RestoreIcon'; @@ -26,8 +25,7 @@ import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { FIX_CREATION_TIME_VISIBLE_TO_USER_IDS } from 'constants/user'; import DownloadIcon from 'components/icons/DownloadIcon'; import { User } from 'types/user'; -import { GalleryContext } from 'pages/gallery'; -import DeduplicatingOptions from './DeduplicatingOptions'; +import { OverlayTrigger } from 'react-bootstrap'; interface Props { addToCollectionHelper?: (collection: Collection) => void; @@ -48,7 +46,7 @@ interface Props { isFavoriteCollection?: boolean; } -const SelectionBar = styled(Navbar)` +export const SelectionBar = styled(Navbar)` position: fixed; top: 0; color: #fff; @@ -57,7 +55,7 @@ const SelectionBar = styled(Navbar)` padding: 0 16px; `; -const SelectionContainer = styled.div` +export const SelectionContainer = styled.div` flex: 1; align-items: center; display: flex; @@ -67,6 +65,7 @@ interface IconWithMessageProps { children?: any; message: string; } + export const IconWithMessage = (props: IconWithMessageProps) => ( { const [showFixCreationTime, setShowFixCreationTime] = useState(false); - const galleryContext = useContext(GalleryContext); useEffect(() => { const user: User = getData(LS_KEYS.USER); @@ -177,9 +175,7 @@ const SelectedFileOptions = ({ {count} {constants.SELECTED} - {galleryContext.isDeduplicating ? ( - - ) : activeCollection === TRASH_SECTION ? ( + {activeCollection === TRASH_SECTION ? ( <> 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..475868011 --- /dev/null +++ b/src/pages/deduplicate/index.tsx @@ -0,0 +1,142 @@ +import constants from 'utils/strings/constants'; +import PhotoFrame from 'components/PhotoFrame'; +import { ALL_SECTION } from 'constants/collection'; +import { AppContext } from 'pages/_app'; +import React, { createContext, useContext, useEffect, useState } from 'react'; +import { + getDuplicateFiles, + clubDuplicatesByTime, +} from 'services/deduplicationService'; +import { trashFiles } from 'services/fileService'; +import { EnteFile } from 'types/file'; +import { SelectedState } from 'types/gallery'; + +import { ServerErrorCodes } from 'utils/error'; +import { getSelectedFiles } from 'utils/file'; +import { + DeduplicateContextType, + DefaultDeduplicateContext, +} from 'types/deduplicate'; +import Router from 'next/router'; +import DeduplicateOptions from 'components/pages/gallery/SelectedFileOptions/DeduplicateOptions'; + +export const DeduplicateContext = createContext( + DefaultDeduplicateContext +); + +export default function Deduplicate() { + const { setDialogMessage, startLoading, finishLoading, showNavBar } = + useContext(AppContext); + const [duplicateFiles, setDuplicateFiles] = useState([]); + const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false); + const [fileSizeMap, setFileSizeMap] = useState(new Map()); + const [selected, setSelected] = useState({ + count: 0, + collectionID: 0, + }); + const closeDeduplication = function () { + Router.back(); + setSelected({ count: 0, collectionID: 0 }); + }; + useEffect(() => { + showNavBar(true); + }); + + useEffect(() => { + syncWithRemote(); + }, [clubSameTimeFilesOnly]); + + const syncWithRemote = async () => { + startLoading(); + let duplicates = await getDuplicateFiles(); + if (clubSameTimeFilesOnly) { + duplicates = clubDuplicatesByTime(duplicates); + } + + const currFileSizeMap = new Map(); + + 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; + + for (const file of dupe.files) { + currFileSizeMap.set(file.id, dupe.size); + } + } + setDuplicateFiles(allDuplicateFiles); + setFileSizeMap(currFileSizeMap); + + const selectedFiles = { + count: count, + collectionID: ALL_SECTION, + }; + + for (const fileID of toSelectFileIDs) { + selectedFiles[fileID] = true; + } + setSelected(selectedFiles); + finishLoading(); + }; + + const deleteFileHelper = async () => { + try { + startLoading(); + const selectedFiles = getSelectedFiles(selected, duplicateFiles); + await trashFiles(selectedFiles); + closeDeduplication(); + } 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(); + finishLoading(); + } + }; + + return ( + + + + + ); +} diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index b530a7498..766550206 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -51,7 +51,7 @@ import { sortFilesIntoCollections, } from 'utils/file'; import SearchBar from 'components/Search'; -import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions'; +import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions/GalleryOptions'; import CollectionSelector, { CollectionSelectorAttributes, } from 'components/pages/gallery/CollectionSelector'; @@ -103,12 +103,6 @@ import { import Collections from 'components/pages/gallery/Collections'; import { VISIBILITY_STATE } from 'constants/file'; import ToastNotification from 'components/ToastNotification'; -import { - clubDuplicatesByTime, - getDuplicateFiles, -} from 'services/deduplicationService'; -import ClubDuplicateFilesByTime from 'components/ClubDuplicateFilesByTime'; -import BackButton from 'components/BackButton'; export const DeadCenter = styled.div` flex: 1; @@ -134,11 +128,6 @@ const defaultGalleryContext: GalleryContextType = { setNotificationAttributes: () => null, setBlockingLoad: () => null, - clubSameTimeFilesOnly: false, - setClubSameTimeFilesOnly: null, - fileSizeMap: new Map(), - isDeduplicating: false, - setIsDeduplicating: null, }; export const GalleryContext = createContext( @@ -205,11 +194,6 @@ export default function Gallery() { const [notificationAttributes, setNotificationAttributes] = useState(null); - const [isDeduplicating, setIsDeduplicating] = useState(false); - const [duplicateFiles, setDuplicateFiles] = useState([]); - const [clubSameTimeFilesOnly, setClubSameTimeFilesOnly] = useState(false); - const [fileSizeMap, setFileSizeMap] = useState(new Map()); - const showPlanSelectorModal = () => setPlanModalView(true); const clearNotificationAttributes = () => setNotificationAttributes(null); @@ -246,57 +230,6 @@ export default function Gallery() { appContext.showNavBar(true); }, []); - useEffect(() => { - const main = async () => { - if (isDeduplicating) { - startLoading(); - let duplicates = await getDuplicateFiles(); - if (clubSameTimeFilesOnly) { - duplicates = clubDuplicatesByTime(duplicates); - } - - const currFileSizeMap = new Map(); - - 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; - - for (const file of dupe.files) { - currFileSizeMap.set(file.id, dupe.size); - } - } - setDuplicateFiles(allDuplicateFiles); - setFileSizeMap(currFileSizeMap); - - const selectedFiles = { - count: count, - collectionID: ALL_SECTION, - }; - - for (const fileID of toSelectFileIDs) { - selectedFiles[fileID] = true; - } - setSelected(selectedFiles); - setActiveCollection(ALL_SECTION); - finishLoading(); - } else { - setDuplicateFiles([]); - setFileSizeMap(new Map()); - setClubSameTimeFilesOnly(false); - } - }; - - main(); - }, [isDeduplicating, clubSameTimeFilesOnly]); - useEffect( () => collectionSelectorAttributes && setCollectionSelectorView(true), [collectionSelectorAttributes] @@ -619,11 +552,6 @@ export default function Gallery() { setNotificationAttributes, setBlockingLoad, - clubSameTimeFilesOnly, - setClubSameTimeFilesOnly, - fileSizeMap, - setIsDeduplicating: setIsDeduplicating, - isDeduplicating: isDeduplicating, }}> - {!isDeduplicating && ( - <> - - - - )} + + + + {blockingLoad && ( @@ -722,28 +644,19 @@ export default function Gallery() { attributes={fixCreationTimeAttributes} /> - {isDeduplicating ? ( - <> - - - - ) : ( - <> - - - - )} + + void; + fileSizeMap: Map; + isOnDeduplicatePage: boolean; +}; + +export const DefaultDeduplicateContext = { + clubSameTimeFilesOnly: false, + setClubSameTimeFilesOnly: () => null, + fileSizeMap: new Map(), + isOnDeduplicatePage: false, +}; diff --git a/src/types/gallery/index.ts b/src/types/gallery/index.ts index 5223ad40b..ee727b410 100644 --- a/src/types/gallery/index.ts +++ b/src/types/gallery/index.ts @@ -31,11 +31,6 @@ export type GalleryContextType = { setNotificationAttributes: (attributes: NotificationAttributes) => void; setBlockingLoad: (value: boolean) => void; - clubSameTimeFilesOnly: boolean; - setClubSameTimeFilesOnly: (clubSameTimeFilesOnly: boolean) => void; - fileSizeMap: Map; - isDeduplicating: boolean; - setIsDeduplicating: (value: boolean) => void; }; export interface NotificationAttributes {