ente/src/pages/gallery/index.tsx

471 lines
18 KiB
TypeScript
Raw Normal View History

2021-06-04 07:57:48 +00:00
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
2021-05-30 16:56:48 +00:00
import { useRouter } from 'next/router';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import {
File,
getLocalFiles,
deleteFiles,
syncFiles,
} from 'services/fileService';
2020-09-20 15:18:35 +00:00
import styled from 'styled-components';
2021-02-08 17:15:13 +00:00
import LoadingBar from 'react-top-loading-bar';
import {
Collection,
2021-02-08 16:15:21 +00:00
syncCollections,
CollectionAndItsLatestFile,
getCollectionsAndTheirLatestFile,
getFavItemIds,
2021-02-08 17:15:13 +00:00
getLocalCollections,
2021-03-15 17:30:49 +00:00
getNonEmptyCollections,
} from 'services/collectionService';
2021-01-24 20:59:58 +00:00
import constants from 'utils/strings/constants';
import billingService from 'services/billingService';
2021-05-30 16:56:48 +00:00
import { checkSubscriptionPurchase } from 'utils/billingUtil';
2020-09-20 15:18:35 +00:00
import FullScreenDropZone from 'components/FullScreenDropZone';
import Sidebar from 'components/Sidebar';
2021-05-30 16:56:48 +00:00
import { checkConnectivity } from 'utils/common';
2021-04-22 14:30:07 +00:00
import {
isFirstLogin,
justSignedUp,
setIsFirstLogin,
setJustSignedUp,
} from 'utils/storage';
2021-05-30 16:56:48 +00:00
import { isTokenValid, logoutUser } from 'services/userService';
import MessageDialog, { MessageAttributes } from 'components/MessageDialog';
import { useDropzone } from 'react-dropzone';
import EnteSpinner from 'components/EnteSpinner';
2021-05-30 16:56:48 +00:00
import { LoadingOverlay } from 'components/LoadingOverlay';
2021-05-29 06:27:52 +00:00
import PhotoFrame from 'components/PhotoFrame';
2021-07-16 13:38:42 +00:00
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
2021-05-30 16:56:48 +00:00
import { addFilesToCollection } from 'utils/collection';
import { errorCodes } from 'utils/common/errorUtil';
import SearchBar, { DateValue } from 'components/SearchBar';
import { Bbox } from 'services/searchService';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions';
import CollectionSelector, {
CollectionSelectorAttributes,
} from 'components/pages/gallery/CollectionSelector';
2021-05-29 06:27:52 +00:00
import CollectionNamer, {
CollectionNamerAttributes,
} from 'components/pages/gallery/CollectionNamer';
import AlertBanner from 'components/pages/gallery/AlertBanner';
import UploadButton from 'components/pages/gallery/UploadButton';
import PlanSelector from 'components/pages/gallery/PlanSelector';
import Upload from 'components/pages/gallery/Upload';
import Collections from 'components/pages/gallery/Collections';
2021-06-04 07:57:48 +00:00
import { AppContext } from 'pages/_app';
2021-01-12 07:01:00 +00:00
export enum FILE_TYPE {
IMAGE,
VIDEO,
2021-02-08 17:15:13 +00:00
OTHERS,
2021-01-12 07:01:00 +00:00
}
2020-11-29 14:48:47 +00:00
2021-04-19 08:29:12 +00:00
export const DeadCenter = styled.div`
flex: 1;
display: flex;
justify-content: center;
align-items: center;
text-align: center;
flex-direction: column;
2020-11-28 18:11:24 +00:00
`;
2021-05-27 08:27:32 +00:00
const AlertContainer = styled.div`
background-color: #111;
padding: 5px 0;
font-size: 14px;
text-align: center;
`;
2020-11-28 18:11:24 +00:00
export type selectedState = {
[k: number]: boolean;
2021-03-20 14:58:12 +00:00
count: number;
};
2021-05-18 14:20:38 +00:00
export type SetFiles = React.Dispatch<React.SetStateAction<File[]>>;
export type SetCollections = React.Dispatch<React.SetStateAction<Collection[]>>;
export type SetLoading = React.Dispatch<React.SetStateAction<Boolean>>;
2021-05-27 08:27:32 +00:00
export type setSearchStats = React.Dispatch<React.SetStateAction<SearchStats>>;
2021-05-26 20:36:02 +00:00
export type Search = {
2021-05-28 16:25:19 +00:00
date?: DateValue;
2021-05-26 20:36:02 +00:00
location?: Bbox;
2021-05-27 08:27:32 +00:00
};
export interface SearchStats {
resultCount: number;
timeTaken: number;
2021-05-26 20:36:02 +00:00
}
2021-03-20 14:58:12 +00:00
2021-05-30 16:35:12 +00:00
type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, string>;
2021-05-30 16:35:12 +00:00
}
const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(),
files: new Map(),
};
export const GalleryContext = createContext<GalleryContextType>(defaultGalleryContext);
export default function Gallery() {
const router = useRouter();
const [collections, setCollections] = useState<Collection[]>([]);
2021-05-29 06:27:52 +00:00
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState<CollectionAndItsLatestFile[]>([]);
const [files, setFiles] = useState<File[]>(null);
2021-01-20 12:05:04 +00:00
const [favItemIds, setFavItemIds] = useState<Set<number>>();
2021-03-29 07:52:20 +00:00
const [bannerMessage, setBannerMessage] = useState<string>(null);
const [isFirstLoad, setIsFirstLoad] = useState(false);
2021-05-26 16:48:29 +00:00
const [isFirstFetch, setIsFirstFetch] = useState(false);
2021-05-30 16:56:48 +00:00
const [selected, setSelected] = useState<selectedState>({ count: 0 });
const [dialogMessage, setDialogMessage] = useState<MessageAttributes>();
const [dialogView, setDialogView] = useState(false);
const [planModalView, setPlanModalView] = useState(false);
const [loading, setLoading] = useState(false);
2021-05-29 06:27:52 +00:00
const [collectionSelectorAttributes, setCollectionSelectorAttributes] = useState<CollectionSelectorAttributes>(null);
2021-04-27 05:35:49 +00:00
const [collectionSelectorView, setCollectionSelectorView] = useState(false);
2021-05-29 06:27:52 +00:00
const [collectionNamerAttributes, setCollectionNamerAttributes] = useState<CollectionNamerAttributes>(null);
2021-04-27 05:35:49 +00:00
const [collectionNamerView, setCollectionNamerView] = useState(false);
2021-05-26 20:36:02 +00:00
const [search, setSearch] = useState<Search>({
date: null,
location: null,
});
const [uploadInProgress, setUploadInProgress] = useState(false);
2021-04-27 05:35:49 +00:00
const {
getRootProps,
getInputProps,
open: openFileUploader,
acceptedFiles,
2021-07-24 14:55:12 +00:00
fileRejections,
2021-04-27 05:35:49 +00:00
} = useDropzone({
noClick: true,
noKeyboard: true,
2021-07-24 15:59:02 +00:00
accept: ['image/*', 'video/*', '.json'],
disabled: uploadInProgress,
2021-04-27 05:35:49 +00:00
});
2021-02-17 08:35:19 +00:00
const loadingBar = useRef(null);
const [searchMode, setSearchMode] = useState(false);
2021-05-27 08:27:32 +00:00
const [searchStats, setSearchStats] = useState(null);
2021-07-24 12:47:11 +00:00
const syncInProgress = useRef<Promise<void>>(null);
2021-07-21 08:47:21 +00:00
const resync = useRef(false);
const [deleted, setDeleted] = useState<number[]>([]);
2021-06-04 07:57:48 +00:00
const appContext = useContext(AppContext);
2021-07-16 13:38:42 +00:00
const [collectionFilesCount, setCollectionFilesCount] = useState<Map<number, number>>();
2021-05-27 08:27:32 +00:00
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if (!key) {
router.push('/');
2021-02-17 09:16:20 +00:00
return;
}
const main = async () => {
setIsFirstLoad(isFirstLogin());
2021-05-26 17:17:12 +00:00
setIsFirstFetch(true);
if (justSignedUp()) {
2021-04-20 09:30:45 +00:00
setPlanModalView(true);
}
setIsFirstLogin(false);
const files = await getLocalFiles();
const collections = await getLocalCollections();
setFiles(files);
setCollections(collections);
await initDerivativeState(collections, files);
await checkSubscriptionPurchase(setDialogMessage, router);
2021-06-06 20:26:31 +00:00
await syncWithRemote(true);
setIsFirstLoad(false);
2021-04-22 14:30:07 +00:00
setJustSignedUp(false);
2021-05-26 16:48:29 +00:00
setIsFirstFetch(false);
};
main();
2021-06-04 07:57:48 +00:00
appContext.showNavBar(true);
}, []);
useEffect(() => setDialogView(true), [dialogMessage]);
2021-05-11 12:12:17 +00:00
useEffect(
() => {
if (collectionSelectorAttributes) {
setCollectionSelectorView(true);
}
},
2021-05-29 06:27:52 +00:00
[collectionSelectorAttributes],
2021-05-11 12:12:17 +00:00
);
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
2021-06-06 20:26:31 +00:00
const syncWithRemote = async (force = false) => {
2021-07-21 08:47:21 +00:00
if (syncInProgress.current && !force) {
resync.current= true;
return;
}
2021-07-24 12:47:11 +00:00
syncInProgress.current=(async ()=>{
try {
checkConnectivity();
if (!(await isTokenValid())) {
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
}
loadingBar.current?.continuousStart();
await billingService.updatePlans();
await billingService.syncSubscription();
const collections = await syncCollections();
const { files } = await syncFiles(collections, setFiles);
await initDerivativeState(collections, files);
} catch (e) {
switch (e.message) {
case errorCodes.ERR_SESSION_EXPIRED:
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
setDialogMessage({
title: constants.SESSION_EXPIRED,
content: constants.SESSION_EXPIRED_MESSAGE,
staticBackdrop: true,
proceed: {
text: constants.LOGIN,
action: logoutUser,
variant: 'success',
},
nonClosable: true,
});
break;
case errorCodes.ERR_KEY_MISSING:
clearKeys();
router.push('/credentials');
break;
}
} finally {
loadingBar.current?.complete();
2021-05-24 13:11:08 +00:00
}
2021-07-24 12:47:11 +00:00
syncInProgress.current=null;
if (resync.current) {
resync.current=false;
syncWithRemote();
}
2021-07-24 12:47:11 +00:00
})();
await syncInProgress.current;
2021-02-08 17:15:13 +00:00
};
const initDerivativeState = async (collections, files) => {
const nonEmptyCollections = getNonEmptyCollections(
collections,
files,
);
const collectionsAndTheirLatestFile = await getCollectionsAndTheirLatestFile(
nonEmptyCollections,
files,
);
const collectionWiseFiles = sortFilesIntoCollections(files);
const collectionFilesCount = new Map<number, number>();
for (const [id, files] of collectionWiseFiles) {
collectionFilesCount.set(id, files.length);
}
setCollections(nonEmptyCollections);
setCollectionsAndTheirLatestFile(collectionsAndTheirLatestFile);
setCollectionFilesCount(collectionFilesCount);
const favItemIds = await getFavItemIds(files);
setFavItemIds(favItemIds);
};
const clearSelection = function () {
2021-05-30 16:56:48 +00:00
setSelected({ count: 0 });
2021-04-22 12:56:06 +00:00
};
2021-03-20 14:58:12 +00:00
2021-02-08 09:20:27 +00:00
const selectCollection = (id?: number) => {
const href = `/gallery?collection=${id || ''}`;
2021-05-30 16:56:48 +00:00
router.push(href, undefined, { shallow: true });
};
if (!files) {
return <div />;
}
2021-05-07 09:37:12 +00:00
const addToCollectionHelper = (
collectionName: string,
2021-05-29 06:27:52 +00:00
collection: Collection,
2021-05-07 09:37:12 +00:00
) => {
2021-05-07 12:11:20 +00:00
loadingBar.current?.continuousStart();
2021-05-07 09:37:12 +00:00
addFilesToCollection(
setCollectionSelectorView,
selected,
files,
clearSelection,
syncWithRemote,
selectCollection,
collectionName,
2021-05-29 06:27:52 +00:00
collection,
2021-05-07 09:37:12 +00:00
);
};
2021-05-29 06:27:52 +00:00
const showCreateCollectionModal = () => setCollectionNamerAttributes({
title: constants.CREATE_COLLECTION,
buttonText: constants.CREATE,
autoFilledName: '',
callback: (collectionName) => addToCollectionHelper(collectionName, null),
});
2021-05-31 10:58:33 +00:00
const deleteFileHelper = async () => {
2021-05-07 12:11:20 +00:00
loadingBar.current?.continuousStart();
2021-05-31 10:58:33 +00:00
try {
const fileIds = getSelectedFileIds(selected);
2021-05-31 10:58:33 +00:00
await deleteFiles(
fileIds,
2021-05-31 10:58:33 +00:00
clearSelection,
syncWithRemote,
);
setDeleted([...deleted, ...fileIds]);
2021-05-31 10:58:33 +00:00
} catch (e) {
loadingBar.current.complete();
switch (e.status?.toString()) {
2021-06-03 08:46:54 +00:00
case errorCodes.ERR_FORBIDDEN:
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.NOT_FILE_OWNER,
});
loadingBar.current.complete();
return;
2021-05-31 10:58:33 +00:00
}
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
close: { variant: 'danger' },
content: constants.UNKNOWN_ERROR,
});
}
2021-05-07 09:37:12 +00:00
};
2021-05-26 20:36:02 +00:00
2021-05-26 20:43:30 +00:00
const updateSearch = (search: Search) => {
2021-05-26 20:36:02 +00:00
setSearch(search);
2021-05-27 08:27:32 +00:00
setSearchStats(null);
};
2021-06-06 20:26:31 +00:00
const closeCollectionSelector = (closeBtnClick?: boolean) => {
if (closeBtnClick === true) {
appContext.resetSharedFiles();
2021-06-06 20:26:31 +00:00
}
setCollectionSelectorView(false);
};
return (
2021-06-01 20:23:54 +00:00
<GalleryContext.Provider value={defaultGalleryContext}>
2021-05-30 16:35:12 +00:00
<FullScreenDropZone
getRootProps={getRootProps}
getInputProps={getInputProps}
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
>
{loading && (
<LoadingOverlay>
<EnteSpinner />
</LoadingOverlay>
)}
<LoadingBar color="#2dc262" ref={loadingBar} />
{isFirstLoad && (
<AlertContainer>
{constants.INITIAL_LOAD_DELAY_WARNING}
</AlertContainer>
)}
2021-05-30 16:35:12 +00:00
<PlanSelector
modalView={planModalView}
closeModal={() => setPlanModalView(false)}
setDialogMessage={setDialogMessage}
setLoading={setLoading}
/>
<AlertBanner bannerMessage={bannerMessage} />
<MessageDialog
size="lg"
show={dialogView}
onHide={() => setDialogView(false)}
attributes={dialogMessage}
/>
<SearchBar
isOpen={searchMode}
setOpen={setSearchMode}
loadingBar={loadingBar}
isFirstFetch={isFirstFetch}
setCollections={setCollections}
setSearch={updateSearch}
files={files}
searchStats={searchStats}
/>
<Collections
collections={collections}
searchMode={searchMode}
selected={Number(router.query.collection)}
selectCollection={selectCollection}
syncWithRemote={syncWithRemote}
setDialogMessage={setDialogMessage}
2021-05-30 16:35:12 +00:00
setCollectionNamerAttributes={setCollectionNamerAttributes}
startLoadingBar={loadingBar.current?.continuousStart}
2021-07-16 13:38:42 +00:00
collectionFilesCount={collectionFilesCount}
2021-05-30 16:35:12 +00:00
/>
<CollectionNamer
show={collectionNamerView}
onHide={setCollectionNamerView.bind(null, false)}
attributes={collectionNamerAttributes}
/>
<CollectionSelector
2021-06-11 07:22:54 +00:00
show={collectionSelectorView && !(collectionsAndTheirLatestFile?.length === 0)}
2021-06-06 20:26:31 +00:00
onHide={closeCollectionSelector}
2021-05-30 16:35:12 +00:00
collectionsAndTheirLatestFile={collectionsAndTheirLatestFile}
directlyShowNextModal={
collectionsAndTheirLatestFile?.length === 0
}
attributes={collectionSelectorAttributes}
/>
<Upload
syncWithRemote={syncWithRemote}
2021-07-24 12:47:11 +00:00
syncInProgress={syncInProgress.current}
2021-05-30 16:35:12 +00:00
setBannerMessage={setBannerMessage}
acceptedFiles={acceptedFiles}
2021-05-30 16:35:12 +00:00
existingFiles={files}
2021-06-06 20:26:31 +00:00
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
setCollectionSelectorAttributes={setCollectionSelectorAttributes}
2021-05-30 16:35:12 +00:00
closeCollectionSelector={setCollectionSelectorView.bind(
null,
false,
)}
setLoading={setLoading}
setCollectionNamerAttributes={setCollectionNamerAttributes}
setDialogMessage={setDialogMessage}
setUploadInProgress={setUploadInProgress}
2021-07-24 14:55:12 +00:00
fileRejections={fileRejections}
2021-07-25 07:56:05 +00:00
setFiles={setFiles}
2021-05-30 16:35:12 +00:00
/>
<Sidebar
collections={collections}
setDialogMessage={setDialogMessage}
2021-06-24 08:32:29 +00:00
setLoading={setLoading}
2021-05-30 16:35:12 +00:00
showPlanSelectorModal={() => setPlanModalView(true)}
/>
2021-05-31 10:30:26 +00:00
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} />
2021-05-30 16:35:12 +00:00
<PhotoFrame
files={files}
setFiles={setFiles}
syncWithRemote={syncWithRemote}
favItemIds={favItemIds}
setSelected={setSelected}
selected={selected}
isFirstLoad={isFirstLoad}
openFileUploader={openFileUploader}
loadingBar={loadingBar}
searchMode={searchMode}
search={search}
setSearchStats={setSearchStats}
deleted={deleted}
setDialogMessage={setDialogMessage}
2021-05-30 16:35:12 +00:00
/>
{selected.count > 0 && (
<SelectedFileOptions
addToCollectionHelper={addToCollectionHelper}
showCreateCollectionModal={showCreateCollectionModal}
setDialogMessage={setDialogMessage}
setCollectionSelectorAttributes={
setCollectionSelectorAttributes
}
deleteFileHelper={deleteFileHelper}
count={selected.count}
clearSelection={clearSelection}
2021-05-30 16:35:12 +00:00
/>
)}
</FullScreenDropZone>
</GalleryContext.Provider>
);
}