Merge branch 'master' into redirect_to_payements

This commit is contained in:
Abhinav-grd 2021-08-14 15:35:48 +05:30
commit ac1a59394e
113 changed files with 4162 additions and 2572 deletions

View file

@ -8,7 +8,8 @@
"plugin:react/recommended",
"eslint:recommended",
"plugin:@typescript-eslint/eslint-recommended",
"google"
"google",
"prettier"
],
"parser": "@typescript-eslint/parser",
"parserOptions": {
@ -23,13 +24,7 @@
"@typescript-eslint"
],
"rules": {
"indent": [
"error",
4,
{
"SwitchCase": 1
}
],
"indent":"off",
"class-methods-use-this": "off",
"react/prop-types": "off",
"react/display-name": "off",
@ -48,7 +43,8 @@
"error",
"always"
],
"space-before-function-paren": "off"
"space-before-function-paren": "off",
"operator-linebreak":["error","after", { "overrides": { "?": "before", ":": "before" } }]
},
"settings": {
"react": {

1
.husky/.gitignore vendored
View file

@ -1 +0,0 @@
_

View file

@ -1,4 +1,4 @@
#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
yarn lint-staged
npx lint-staged

6
.prettierrc.json Normal file
View file

@ -0,0 +1,6 @@
{
"tabWidth": 4,
"trailingComma": "es5",
"singleQuote": true,
"jsxBracketSameLine": true
}

View file

@ -2,7 +2,13 @@ Web application for [ente](https://ente.io) built with lots of ❤️ and a litt
## Getting Started
First, run the development server:
First, pull and install dependencies
```bash
git submodule update --init --recursive
yarn install
```
Then run the development server:
```bash
npm run dev

View file

@ -9,8 +9,7 @@
"build-analyze": "ANALYZE=true next build",
"postbuild": "next export",
"start": "next start",
"lint-staged": "lint-staged",
"postinstall": "husky install"
"prepare": "husky install"
},
"dependencies": {
"@ente-io/next-with-workbox": "^1.0.3",
@ -18,7 +17,7 @@
"@stripe/stripe-js": "^1.13.2",
"@typescript-eslint/eslint-plugin": "^4.25.0",
"@typescript-eslint/parser": "^4.25.0",
"axios": "^0.20.0",
"axios": "^0.21.1",
"bootstrap": "^4.5.2",
"chrono-node": "^2.2.6",
"comlink": "^4.3.0",
@ -32,6 +31,7 @@
"heic2any": "^0.0.3",
"http-proxy-middleware": "^1.0.5",
"is-electron": "^2.2.0",
"jszip": "3.7.1",
"libsodium-wrappers": "^0.7.8",
"localforage": "^1.9.0",
"next": "^10.2.3",
@ -62,7 +62,6 @@
"@next/bundle-analyzer": "^9.5.3",
"@types/debounce-promise": "^3.1.3",
"@types/libsodium-wrappers": "^0.7.8",
"@types/localforage": "^0.0.34",
"@types/node": "^14.6.4",
"@types/photoswipe": "^4.1.1",
"@types/react": "^16.9.49",
@ -74,20 +73,17 @@
"babel-plugin-styled-components": "^1.11.1",
"eslint": "^7.27.0",
"eslint-config-google": "^0.14.0",
"eslint-config-prettier": "^8.3.0",
"eslint-plugin-react": "^7.23.2",
"husky": "^6.0.0",
"lint-staged": "^11.0.0",
"husky": "^7.0.1",
"lint-staged": "^11.1.2",
"prettier": "2.3.2",
"typescript": "^4.1.3"
},
"standard": {
"parser": "babel-eslint"
},
"lint-staged": {
"src/**/*.{js,jsx,ts,tsx}": "eslint"
},
"husky": {
"hooks": {
"pre-commit": "yarn run lint-staged"
}
"src/**/*.{js,jsx,ts,tsx}": ["eslint --fix","prettier --write --ignore-unknown"]
}
}

View file

@ -1,13 +1,12 @@
import * as Sentry from '@sentry/nextjs';
import { getSentryTunnelUrl } from 'utils/common/apiUtil';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { getUserAnonymizedID } from 'utils/user';
const SENTRY_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4';
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development';
const userID = getData(LS_KEYS.USER)?.id;
Sentry.setUser({ id: userID });
Sentry.setUser({ id: getUserAnonymizedID() });
Sentry.init({
dsn: SENTRY_DSN,
enabled: SENTRY_ENV !== 'development',

View file

@ -25,8 +25,7 @@ export default function AddToCollectionBtn(props) {
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
strokeLinejoin="round">
<line x1="12" y1="5" x2="12" y2="19" />
<line x1="5" y1="12" x2="19" y2="12" />
</svg>

View file

@ -0,0 +1,175 @@
import { Formik, FormikHelpers } from 'formik';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Button, Col, Form, FormControl } from 'react-bootstrap';
import * as Yup from 'yup';
import constants from 'utils/strings/constants';
import SubmitButton from 'components/SubmitButton';
import router from 'next/router';
import { changeEmail, getOTTForEmailChange } from 'services/userService';
import styled from 'styled-components';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
interface formValues {
email: string;
ott?: string;
}
const EmailRow = styled.div`
display: flex;
flex-wrap: wrap;
border: 1px solid grey;
margin-bottom: 19px;
align-items: center;
text-align: left;
color: #fff;
`;
interface Props {
showMessage: (value: boolean) => void;
setEmail: (email: string) => void;
}
function ChangeEmailForm(props: Props) {
const [loading, setLoading] = useState(false);
const [ottInputVisible, setShowOttInputVisibility] = useState(false);
const emailInputElement = useRef(null);
const ottInputRef = useRef(null);
const appContext = useContext(AppContext);
useEffect(() => {
setTimeout(() => {
emailInputElement.current?.focus();
}, 250);
}, []);
useEffect(() => {
if (!ottInputVisible) {
props.showMessage(false);
}
}, [ottInputVisible]);
const requestOTT = async (
{ email }: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
try {
setLoading(true);
await getOTTForEmailChange(email);
props.setEmail(email);
setShowOttInputVisibility(true);
props.showMessage(true);
setTimeout(() => {
ottInputRef.current?.focus();
}, 250);
} catch (e) {
setFieldError('email', `${constants.EMAIl_ALREADY_OWNED}`);
}
setLoading(false);
};
const requestEmailChange = async (
{ email, ott }: formValues,
{ setFieldError }: FormikHelpers<formValues>
) => {
try {
setLoading(true);
await changeEmail(email, ott);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
appContext.setDisappearingFlashMessage({
message: constants.EMAIL_UDPATE_SUCCESSFUL,
type: FLASH_MESSAGE_TYPE.SUCCESS,
});
router.push('/gallery');
} catch (e) {
setFieldError('ott', `${constants.INCORRECT_CODE}`);
}
setLoading(false);
};
return (
<Formik<formValues>
initialValues={{ email: '' }}
validationSchema={Yup.object().shape({
email: Yup.string()
.email(constants.EMAIL_ERROR)
.required(constants.REQUIRED),
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={!ottInputVisible ? requestOTT : requestEmailChange}>
{({ values, errors, touched, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
{!ottInputVisible ? (
<Form.Group controlId="formBasicEmail">
<Form.Control
ref={emailInputElement}
type="email"
placeholder={constants.ENTER_EMAIL}
value={values.email}
onChange={handleChange('email')}
isInvalid={Boolean(
touched.email && errors.email
)}
autoFocus
disabled={loading}
/>
<FormControl.Feedback type="invalid">
{errors.email}
</FormControl.Feedback>
</Form.Group>
) : (
<>
<EmailRow>
<Col xs="8">{values.email}</Col>
<Col xs="4">
<Button
variant="link"
onClick={() =>
setShowOttInputVisibility(false)
}>
{constants.CHANGE}
</Button>
</Col>
</EmailRow>
<Form.Group controlId="formBasicEmail">
<Form.Control
ref={ottInputRef}
type="text"
placeholder={constants.ENTER_OTT}
value={values.ott}
onChange={handleChange('ott')}
isInvalid={Boolean(
touched.ott && errors.ott
)}
disabled={loading}
/>
<FormControl.Feedback type="invalid">
{errors.ott}
</FormControl.Feedback>
</Form.Group>
</>
)}
<SubmitButton
buttonText={
!ottInputVisible
? constants.SEND_OTT
: constants.VERIFY
}
loading={loading}
/>
<br />
<Button
block
variant="link"
className="text-center"
onClick={router.back}>
{constants.GO_BACK}
</Button>
</Form>
)}
</Formik>
);
}
export default ChangeEmailForm;

View file

@ -34,7 +34,7 @@ function CollectionShare(props: Props) {
const [loading, setLoading] = useState(false);
const collectionShare = async (
{ email }: formValues,
{ resetForm, setFieldError }: FormikHelpers<formValues>,
{ resetForm, setFieldError }: FormikHelpers<formValues>
) => {
try {
setLoading(true);
@ -89,8 +89,7 @@ function CollectionShare(props: Props) {
fontSize: '1.2em',
fontWeight: 900,
}}
onClick={() => collectionUnshare(sharee)}
>
onClick={() => collectionUnshare(sharee)}>
-
</Button>
</td>
@ -100,8 +99,7 @@ function CollectionShare(props: Props) {
<MessageDialog
show={props.show}
onHide={props.onHide}
attributes={{ title: constants.SHARE_COLLECTION }}
>
attributes={{ title: constants.SHARE_COLLECTION }}>
<DeadCenter style={{ width: '85%', margin: 'auto' }}>
<h6>{constants.SHARE_WITH_PEOPLE}</h6>
<p />
@ -114,8 +112,7 @@ function CollectionShare(props: Props) {
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={collectionShare}
>
onSubmit={collectionShare}>
{({
values,
errors,
@ -128,15 +125,14 @@ function CollectionShare(props: Props) {
<Form.Group
as={Col}
xs={10}
controlId="formHorizontalEmail"
>
controlId="formHorizontalEmail">
<Form.Control
type="email"
placeholder={constants.ENTER_EMAIL}
value={values.email}
onChange={handleChange('email')}
isInvalid={Boolean(
touched.email && errors.email,
touched.email && errors.email
)}
autoFocus
disabled={loading}
@ -148,8 +144,7 @@ function CollectionShare(props: Props) {
<Form.Group
as={Col}
xs={2}
controlId="formHorizontalEmail"
>
controlId="formHorizontalEmail">
<SubmitButton
loading={loading}
inline

View file

@ -31,7 +31,8 @@ export const IconButton = styled.button`
align-items: center;
justify-content: center;
&:focus, &:hover {
&:focus,
&:hover {
background-color: rgba(255, 255, 255, 0.2);
}
`;
@ -40,7 +41,7 @@ export const Row = styled.div`
display: flex;
align-items: center;
margin-bottom: 20px;
flex:1
flex: 1;
`;
export const Label = styled.div<{ width?: string }>`

View file

@ -20,8 +20,7 @@ export default function DeleteBtn(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</svg>

View file

@ -0,0 +1,39 @@
import React from 'react';
import { Card } from 'react-bootstrap';
type Size = 'sm' | 'md' | 'lg';
const EnteCard = ({
size,
children,
style,
}: {
size: Size;
children?: any;
style?: any;
}) => {
let minWidth: string;
let padding: string;
switch (size) {
case 'sm':
minWidth = '320px';
padding = '0px';
break;
case 'md':
minWidth = '460px';
padding = '10px';
break;
default:
minWidth = '480px';
padding = '10px';
break;
}
return (
<Card style={{ minWidth, padding, ...style }} className="text-center">
{children}
</Card>
);
};
export default EnteCard;

View file

@ -6,14 +6,13 @@ import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container';
import { ComfySpan } from './ExportInProgress';
interface Props {
show: boolean
onHide: () => void
exportFolder: string
exportSize: string
lastExportTime: number
exportStats: ExportStats
show: boolean;
onHide: () => void;
exportFolder: string;
exportSize: string;
lastExportTime: number;
exportStats: ExportStats;
updateExportFolder: (newFolder: string) => void;
exportFiles: () => void;
retryFailed: () => void;
@ -23,30 +22,69 @@ export default function ExportFinished(props: Props) {
const totalFiles = props.exportStats.failed + props.exportStats.success;
return (
<>
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
<div
style={{
borderBottom: '1px solid #444',
marginBottom: '20px',
padding: '0 5%',
}}>
<Row>
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
<Value width="60%">{formatDateTime(props.lastExportTime)}</Value>
</Row>
<Row>
<Label width="60%">{constants.SUCCESSFULLY_EXPORTED_FILES}</Label>
<Value width="35%"><ComfySpan>{props.exportStats.success} / {totalFiles}</ComfySpan></Value>
</Row>
{props.exportStats.failed>0 &&
<Row>
<Label width="60%">{constants.FAILED_EXPORTED_FILES}</Label>
<Value width="35%">
<ComfySpan>{props.exportStats.failed} / {totalFiles}</ComfySpan>
<Value width="60%">
{formatDateTime(props.lastExportTime)}
</Value>
</Row>}
</Row>
<Row>
<Label width="60%">
{constants.SUCCESSFULLY_EXPORTED_FILES}
</Label>
<Value width="35%">
<ComfySpan>
{props.exportStats.success} / {totalFiles}
</ComfySpan>
</Value>
</Row>
{props.exportStats.failed > 0 && (
<Row>
<Label width="60%">
{constants.FAILED_EXPORTED_FILES}
</Label>
<Value width="35%">
<ComfySpan>
{props.exportStats.failed} / {totalFiles}
</ComfySpan>
</Value>
</Row>
)}
</div>
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}>
<Button block variant={'outline-secondary'} onClick={props.onHide}>{constants.CLOSE}</Button>
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
<Button
block
variant={'outline-secondary'}
onClick={props.onHide}>
{constants.CLOSE}
</Button>
<div style={{ width: '30px' }} />
{props.exportStats.failed !== 0 ?
<Button block variant={'outline-danger'} onClick={props.retryFailed}>{constants.RETRY_EXPORT_}</Button> :
<Button block variant={'outline-success'} onClick={props.exportFiles}>{constants.EXPORT_AGAIN}</Button>
}
{props.exportStats.failed !== 0 ? (
<Button
block
variant={'outline-danger'}
onClick={props.retryFailed}>
{constants.RETRY_EXPORT_}
</Button>
) : (
<Button
block
variant={'outline-success'}
onClick={props.exportFiles}>
{constants.EXPORT_AGAIN}
</Button>
)}
</div>
</>
);

View file

@ -10,37 +10,77 @@ export const ComfySpan = styled.span`
`;
interface Props {
show: boolean
onHide: () => void
exportFolder: string
exportSize: string
exportStage: ExportStage
exportProgress: ExportProgress
show: boolean;
onHide: () => void;
exportFolder: string;
exportSize: string;
exportStage: ExportStage;
exportProgress: ExportProgress;
resumeExport: () => void;
cancelExport: () => void
cancelExport: () => void;
pauseExport: () => void;
}
export default function ExportInProgress(props: Props) {
return (
<>
<div style={{ marginBottom: '30px', padding: '0 5%', display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
<div
style={{
marginBottom: '30px',
padding: '0 5%',
display: 'flex',
alignItems: 'center',
flexDirection: 'column',
}}>
<div style={{ marginBottom: '10px' }}>
<ComfySpan> {props.exportProgress.current} / {props.exportProgress.total} </ComfySpan> <span style={{ marginLeft: '10px' }}> files exported {props.exportStage === ExportStage.PAUSED && `(paused)`}</span>
<ComfySpan>
{' '}
{props.exportProgress.current} /{' '}
{props.exportProgress.total}{' '}
</ComfySpan>{' '}
<span style={{ marginLeft: '10px' }}>
{' '}
files exported{' '}
{props.exportStage === ExportStage.PAUSED && `(paused)`}
</span>
</div>
<div style={{ width: '100%', marginBottom: '30px' }}>
<ProgressBar
now={Math.round(props.exportProgress.current * 100 / props.exportProgress.total)}
now={Math.round(
(props.exportProgress.current * 100) /
props.exportProgress.total
)}
animated={!(props.exportStage === ExportStage.PAUSED)}
variant="upload-progress-bar"
/>
</div>
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}>
{props.exportStage === ExportStage.PAUSED ?
<Button block variant={'outline-secondary'} onClick={props.resumeExport}>{constants.RESUME}</Button> :
<Button block variant={'outline-secondary'} onClick={props.pauseExport}>{constants.PAUSE}</Button>
}
<div
style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
{props.exportStage === ExportStage.PAUSED ? (
<Button
block
variant={'outline-secondary'}
onClick={props.resumeExport}>
{constants.RESUME}
</Button>
) : (
<Button
block
variant={'outline-secondary'}
onClick={props.pauseExport}>
{constants.PAUSE}
</Button>
)}
<div style={{ width: '30px' }} />
<Button block variant={'outline-danger'} onClick={props.cancelExport}>{constants.CANCEL}</Button>
<Button
block
variant={'outline-danger'}
onClick={props.cancelExport}>
{constants.CANCEL}
</Button>
</div>
</div>
</>

View file

@ -4,13 +4,13 @@ import { Button } from 'react-bootstrap';
import constants from 'utils/strings/constants';
interface Props {
show: boolean
onHide: () => void
show: boolean;
onHide: () => void;
updateExportFolder: (newFolder: string) => void;
exportFolder: string
startExport: () => void
exportFolder: string;
startExport: () => void;
exportSize: string;
selectExportDirectory: () => void
selectExportDirectory: () => void;
}
export default function ExportInit(props: Props) {
return (
@ -26,8 +26,9 @@ export default function ExportInit(props: Props) {
flex: 1,
whiteSpace: 'nowrap',
}}
onClick={props.startExport}
>{constants.START}</Button>
onClick={props.startExport}>
{constants.START}
</Button>
</DeadCenter>
</>
);

View file

@ -1,11 +1,16 @@
import isElectron from 'is-electron';
import React, { useEffect, useState } from 'react';
import { Button } from 'react-bootstrap';
import exportService, { ExportProgress, ExportStage, ExportStats, ExportType } from 'services/exportService';
import exportService, {
ExportProgress,
ExportStage,
ExportStats,
ExportType,
} from 'services/exportService';
import { getLocalFiles } from 'services/fileService';
import styled from 'styled-components';
import { sleep } from 'utils/common';
import { getFileUID } from 'utils/export';
import { getExportRecordFileUID } from 'utils/export';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import constants from 'utils/strings/constants';
import { Label, Row, Value } from './Container';
@ -40,16 +45,22 @@ const ExportFolderPathContainer =styled.span`
`;
interface Props {
show: boolean
onHide: () => void
usage: string
show: boolean;
onHide: () => void;
usage: string;
}
export default function ExportModal(props: Props) {
const [exportStage, setExportStage] = useState(ExportStage.INIT);
const [exportFolder, setExportFolder] = useState('');
const [exportSize, setExportSize] = useState('');
const [exportProgress, setExportProgress] = useState<ExportProgress>({ current: 0, total: 0 });
const [exportStats, setExportStats] = useState<ExportStats>({ failed: 0, success: 0 });
const [exportProgress, setExportProgress] = useState<ExportProgress>({
current: 0,
total: 0,
});
const [exportStats, setExportStats] = useState<ExportStats>({
failed: 0,
success: 0,
});
const [lastExportTime, setLastExportTime] = useState(0);
// ====================
@ -64,7 +75,9 @@ export default function ExportModal(props: Props) {
exportService.ElectronAPIs.registerStopExportListener(stopExport);
exportService.ElectronAPIs.registerPauseExportListener(pauseExport);
exportService.ElectronAPIs.registerResumeExportListener(resumeExport);
exportService.ElectronAPIs.registerRetryFailedExportListener(retryFailedExport);
exportService.ElectronAPIs.registerRetryFailedExportListener(
retryFailedExport
);
}, []);
useEffect(() => {
@ -76,7 +89,10 @@ export default function ExportModal(props: Props) {
setExportStage(exportInfo?.stage ?? ExportStage.INIT);
setLastExportTime(exportInfo?.lastAttemptTimestamp);
setExportProgress(exportInfo?.progress ?? { current: 0, total: 0 });
setExportStats({ success: exportInfo?.exportedFiles?.length ?? 0, failed: exportInfo?.failedFiles?.length ?? 0 });
setExportStats({
success: exportInfo?.exportedFiles?.length ?? 0,
failed: exportInfo?.failedFiles?.length ?? 0,
});
if (exportInfo?.stage === ExportStage.INPROGRESS) {
resumeExport();
}
@ -96,10 +112,22 @@ export default function ExportModal(props: Props) {
const failedFilesCnt = exportRecord.failedFiles.length;
const syncedFilesCnt = localFiles.length;
if (syncedFilesCnt > exportedFileCnt + failedFilesCnt) {
updateExportProgress({ current: exportedFileCnt + failedFilesCnt, total: syncedFilesCnt });
const exportFileUIDs = new Set([...exportRecord.exportedFiles, ...exportRecord.failedFiles]);
const unExportedFiles = localFiles.filter((file) => !exportFileUIDs.has(getFileUID(file)));
exportService.addFilesQueuedRecord(exportFolder, unExportedFiles);
updateExportProgress({
current: exportedFileCnt + failedFilesCnt,
total: syncedFilesCnt,
});
const exportFileUIDs = new Set([
...exportRecord.exportedFiles,
...exportRecord.failedFiles,
]);
const unExportedFiles = localFiles.filter(
(file) =>
!exportFileUIDs.has(getExportRecordFileUID(file))
);
exportService.addFilesQueuedRecord(
exportFolder,
unExportedFiles
);
updateExportStage(ExportStage.PAUSED);
}
}
@ -107,7 +135,6 @@ export default function ExportModal(props: Props) {
main();
}, [props.show]);
useEffect(() => {
setExportSize(props.usage);
}, [props.usage]);
@ -162,7 +189,10 @@ export default function ExportModal(props: Props) {
const startExport = async () => {
await preExportRun();
updateExportProgress({ current: 0, total: 0 });
const { paused } = await exportService.exportFiles(updateExportProgress, ExportType.NEW);
const { paused } = await exportService.exportFiles(
updateExportProgress,
ExportType.NEW
);
await postExportRun(paused);
};
@ -184,13 +214,15 @@ export default function ExportModal(props: Props) {
const pausedStageProgress = exportRecord.progress;
setExportProgress(pausedStageProgress);
const updateExportStatsWithOffset = ((progress: ExportProgress) => updateExportProgress(
{
const updateExportStatsWithOffset = (progress: ExportProgress) =>
updateExportProgress({
current: pausedStageProgress.current + progress.current,
total: pausedStageProgress.current + progress.total,
},
));
const { paused } = await exportService.exportFiles(updateExportStatsWithOffset, ExportType.PENDING);
});
const { paused } = await exportService.exportFiles(
updateExportStatsWithOffset,
ExportType.PENDING
);
await postExportRun(paused);
};
@ -199,7 +231,10 @@ export default function ExportModal(props: Props) {
await preExportRun();
updateExportProgress({ current: 0, total: exportStats.failed });
const { paused } = await exportService.exportFiles(updateExportProgress, ExportType.RETRY_FAILED);
const { paused } = await exportService.exportFiles(
updateExportProgress,
ExportType.RETRY_FAILED
);
await postExportRun(paused);
};
@ -224,7 +259,8 @@ export default function ExportModal(props: Props) {
switch (exportStage) {
case ExportStage.INIT:
return (
<ExportInit {...props}
<ExportInit
{...props}
exportFolder={exportFolder}
exportSize={exportSize}
updateExportFolder={updateExportFolder}
@ -235,7 +271,8 @@ export default function ExportModal(props: Props) {
case ExportStage.INPROGRESS:
case ExportStage.PAUSED:
return (
<ExportInProgress {...props}
<ExportInProgress
{...props}
exportFolder={exportFolder}
exportSize={exportSize}
exportStage={exportStage}
@ -259,7 +296,8 @@ export default function ExportModal(props: Props) {
/>
);
default: return (<></>);
default:
return <></>;
}
};
@ -269,31 +307,47 @@ export default function ExportModal(props: Props) {
onHide={props.onHide}
attributes={{
title: constants.EXPORT_DATA,
}}
>
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}>
}}>
<div
style={{
borderBottom: '1px solid #444',
marginBottom: '20px',
padding: '0 5%',
width: '450px',
}}>
<Row>
<Label width="40%">{constants.DESTINATION}</Label>
<Value width="60%">
{!exportFolder ?
(<Button variant={'outline-success'} size={'sm'} onClick={selectExportDirectory}>{constants.SELECT_FOLDER}</Button>) :
(<>
{!exportFolder ? (
<Button
variant={'outline-success'}
size={'sm'}
onClick={selectExportDirectory}>
{constants.SELECT_FOLDER}
</Button>
) : (
<>
{/* <span style={{ overflow: 'hidden', direction: 'rtl', height: '1.5rem', width: '90%', whiteSpace: 'nowrap' }}> */}
<ExportFolderPathContainer>
{exportFolder}
</ExportFolderPathContainer>
{/* </span> */}
{(exportStage === ExportStage.FINISHED || exportStage === ExportStage.INIT) && (
<FolderIconWrapper onClick={selectExportDirectory} >
{(exportStage === ExportStage.FINISHED ||
exportStage === ExportStage.INIT) && (
<FolderIconWrapper
onClick={selectExportDirectory}>
<FolderIcon />
</FolderIconWrapper>
)}
</>)
}
</>
)}
</Value>
</Row>
<Row>
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label><Value width="60%">{exportSize ? `${exportSize} GB` : <InProgressIcon />}</Value>
<Label width="40%">{constants.TOTAL_EXPORT_SIZE} </Label>
<Value width="60%">
{exportSize ? `${exportSize}` : <InProgressIcon />}
</Value>
</Row>
</div>
<ExportDynamicState />

View file

@ -12,7 +12,8 @@ const HeartUI = styled.button<{
cursor: pointer;
background-size: cover;
border: none;
${({ isClick, size }) => isClick &&
${({ isClick, size }) =>
isClick &&
`background-position: -${
28 * size
}px;transition: background 1s steps(28);`}

View file

@ -2,15 +2,19 @@ import { FlashMessage } from 'pages/_app';
import React from 'react';
import Alert from 'react-bootstrap/Alert';
export default function FlashMessageBar({ flashMessage, onClose }: { flashMessage: FlashMessage, onClose: () => void }) {
export default function FlashMessageBar({
flashMessage,
onClose,
}: {
flashMessage: FlashMessage;
onClose: () => void;
}) {
return (
<Alert
className="flash-message text-center"
variant={flashMessage.severity}
variant={flashMessage.type}
dismissible
onClose={onClose}
>
onClose={onClose}>
<div style={{ maxWidth: '1024px', width: '80%', margin: 'auto' }}>
{flashMessage.message}
</div>

View file

@ -62,14 +62,10 @@ export default function FullScreenDropZone(props: Props) {
e.preventDefault();
props.showCollectionSelector();
},
})}
>
})}>
<input {...props.getInputProps()} />
{isDragActive && (
<Overlay
onDrop={onDragLeave}
onDragLeave={onDragLeave}
>
<Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
<CloseButtonWrapper onClick={onDragLeave}>
<CrossIcon />
</CloseButtonWrapper>

View file

@ -11,11 +11,8 @@ export default function IncognitoWarning() {
title: constants.LOCAL_STORAGE_NOT_ACCESSIBLE,
staticBackdrop: true,
nonClosable: true,
}}
>
<div>
{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}
</div>
}}>
<div>{constants.LOCAL_STORAGE_NOT_ACCESSIBLE_MESSAGE}</div>
</MessageDialog>
);
}

View file

@ -17,7 +17,7 @@ interface formValues {
}
interface LoginProps {
signUp: () => void
signUp: () => void;
}
export default function Login(props: LoginProps) {
@ -39,7 +39,7 @@ export default function Login(props: LoginProps) {
const loginUser = async (
{ email }: formValues,
{ setFieldError }: FormikHelpers<formValues>,
{ setFieldError }: FormikHelpers<formValues>
) => {
try {
setWaiting(true);
@ -73,15 +73,8 @@ export default function Login(props: LoginProps) {
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={loginUser}
>
{({
values,
errors,
touched,
handleChange,
handleSubmit,
}) => (
onSubmit={loginUser}>
{({ values, errors, touched, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group controlId="formBasicEmail">
<Form.Control
@ -91,7 +84,7 @@ export default function Login(props: LoginProps) {
value={values.email}
onChange={handleChange('email')}
isInvalid={Boolean(
touched.email && errors.email,
touched.email && errors.email
)}
autoFocus
disabled={loading}
@ -105,7 +98,11 @@ export default function Login(props: LoginProps) {
loading={waiting}
/>
<br />
<Button block variant="link" className="text-center" onClick={props.signUp}>
<Button
block
variant="link"
className="text-center"
onClick={props.signUp}>
{constants.NO_ACCOUNT}
</Button>
</Form>

View file

@ -7,7 +7,7 @@ export interface MessageAttributes {
staticBackdrop?: boolean;
nonClosable?: boolean;
content?: any;
close?: { text?: string; variant?: string, action?: () => void };
close?: { text?: string; variant?: string; action?: () => void };
proceed?: {
text: string;
action: () => void;
@ -38,21 +38,21 @@ export default function MessageDialog({
{...props}
onHide={attributes.nonClosable ? () => null : props.onHide}
centered
backdrop={attributes.staticBackdrop ? 'static' : 'true'}
>
backdrop={attributes.staticBackdrop ? 'static' : 'true'}>
<Modal.Header
style={{ borderBottom: 'none' }}
closeButton={!attributes.nonClosable}
>
closeButton={!attributes.nonClosable}>
{attributes.title && (
<Modal.Title>
{attributes.title}
</Modal.Title>
<Modal.Title>{attributes.title}</Modal.Title>
)}
</Modal.Header>
{(children || attributes?.content) && (
<Modal.Body style={{ borderTop: '1px solid #444' }}>
{children || <p style={{ fontSize: '1.25rem', marginBottom: 0 }}>{attributes.content}</p>}
{children || (
<p style={{ fontSize: '1.25rem', marginBottom: 0 }}>
{attributes.content}
</p>
)}
</Modal.Body>
)}
{(attributes.close || attributes.proceed) && (
@ -61,13 +61,16 @@ export default function MessageDialog({
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
}}>
{attributes.close && (
<Button
variant={`outline-${attributes.close?.variant ?? 'secondary'}`}
variant={`outline-${
attributes.close?.variant ?? 'secondary'
}`}
onClick={() => {
attributes.close?.action ? attributes.close?.action() : props.onHide();
attributes.close.action &&
attributes.close?.action();
props.onHide();
}}
style={{
padding: '6px 3em',
@ -75,14 +78,15 @@ export default function MessageDialog({
marginBottom: '20px',
flex: 1,
whiteSpace: 'nowrap',
}}
>
}}>
{attributes.close?.text ?? constants.OK}
</Button>
)}
{attributes.proceed && (
<Button
variant={`outline-${attributes.proceed?.variant ?? 'primary'}`}
variant={`outline-${
attributes.proceed?.variant ?? 'primary'
}`}
onClick={() => {
attributes.proceed.action();
props.onHide();
@ -94,8 +98,7 @@ export default function MessageDialog({
flex: 1,
whiteSpace: 'nowrap',
}}
disabled={attributes.proceed.disabled}
>
disabled={attributes.proceed.disabled}>
{attributes.proceed.text}
</Button>
)}

View file

@ -15,11 +15,17 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
color: #eee;
z-index: 1;
position: absolute;
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'margin-right: 10px;' : 'margin-left: 10px;')}
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;')}
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT
? 'margin-right: 10px;'
: 'margin-left: 10px;'}
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
& > svg {
${(props) => props.direction === SCROLL_DIRECTION.LEFT && 'transform:rotate(180deg);'}
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT &&
'transform:rotate(180deg);'}
border-radius: 50%;
height: 30px;
width: 30px;
@ -35,20 +41,28 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
&::after {
content: ' ';
background: linear-gradient(to ${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'right' : 'left')}, #191919 5%, rgba(255, 255, 255, 0) 80%);
background: linear-gradient(
to
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT
? 'right'
: 'left'},
#191919 5%,
rgba(255, 255, 255, 0) 80%
);
position: absolute;
top: 0;
width: 40px;
height: 40px;
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 40px;' : 'right: 40px;')}
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT
? 'left: 40px;'
: 'right: 40px;'}
}
`;
const NavigationButton = ({ scrollDirection, ...rest }) => (
<Wrapper
direction={scrollDirection}
{...rest}
>
<Wrapper direction={scrollDirection} {...rest}>
<NavigateNext />
</Wrapper>
);

View file

@ -1,7 +1,6 @@
import router from 'next/router';
import {
DeadCenter,
FILE_TYPE,
GalleryContext,
Search,
SetFiles,
@ -10,7 +9,7 @@ import {
import PreviewCard from './pages/gallery/PreviewCard';
import React, { useContext, useEffect, useRef, useState } from 'react';
import { Button } from 'react-bootstrap';
import { File } from 'services/fileService';
import { File, FILE_TYPE } from 'services/fileService';
import styled from 'styled-components';
import DownloadManager from 'services/downloadManager';
import constants from 'utils/strings/constants';
@ -19,10 +18,14 @@ import { VariableSizeList as List } from 'react-window';
import PhotoSwipe from 'components/PhotoSwipe/PhotoSwipe';
import { isInsideBox, isSameDay as isSameDayAnyYear } from 'utils/search';
import { SetDialogMessage } from './MessageDialog';
import { VIDEO_PLAYBACK_FAILED } from 'utils/common/errorUtil';
import { CustomError } from 'utils/common/errorUtil';
import {
GAP_BTW_TILES, DATE_CONTAINER_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, SPACE_BTW_DATES,
GAP_BTW_TILES,
DATE_CONTAINER_HEIGHT,
IMAGE_CONTAINER_MAX_HEIGHT,
IMAGE_CONTAINER_MAX_WIDTH,
MIN_COLUMNS,
SPACE_BTW_DATES,
} from 'types';
const NO_OF_PAGES = 2;
@ -68,15 +71,18 @@ const getTemplateColumns = (columns: number, groups?: number[]): string => {
if (sum < columns) {
groups[groups.length - 1] += columns - sum;
}
return groups.map((x) => `repeat(${x}, 1fr)`).join(` ${SPACE_BTW_DATES}px `);
return groups
.map((x) => `repeat(${x}, 1fr)`)
.join(` ${SPACE_BTW_DATES}px `);
} else {
return `repeat(${columns}, 1fr)`;
}
};
const ListContainer = styled.div<{ columns: number, groups?: number[] }>`
const ListContainer = styled.div<{ columns: number; groups?: number[] }>`
display: grid;
grid-template-columns: ${({ columns, groups }) => getTemplateColumns(columns, groups)};
grid-template-columns: ${({ columns, groups }) =>
getTemplateColumns(columns, groups)};
grid-column-gap: ${GAP_BTW_TILES}px;
padding: 0 24px;
width: 100%;
@ -139,7 +145,7 @@ interface Props {
search: Search;
setSearchStats: setSearchStats;
deleted?: number[];
setDialogMessage: SetDialogMessage
setDialogMessage: SetDialogMessage;
}
const PhotoFrame = ({
@ -303,14 +309,13 @@ const PhotoFrame = ({
video.preload = 'metadata';
video.src = url;
video.currentTime = 3;
const t = setTimeout(
() => {
const t = setTimeout(() => {
reject(
Error(`${VIDEO_PLAYBACK_FAILED} err: wait time exceeded`),
);
},
WAIT_FOR_VIDEO_PLAYBACK,
Error(
`${CustomError.VIDEO_PLAYBACK_FAILED} err: wait time exceeded`
)
);
}, WAIT_FOR_VIDEO_PLAYBACK);
});
item.html = `
<video width="320" height="240" controls>
@ -332,7 +337,8 @@ const PhotoFrame = ({
};
setDialogMessage({
title: constants.VIDEO_PLAYBACK_FAILED,
content: constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
content:
constants.VIDEO_PLAYBACK_FAILED_DOWNLOAD_INSTEAD,
staticBackdrop: true,
proceed: {
text: constants.DOWNLOAD,
@ -373,7 +379,7 @@ const PhotoFrame = ({
if (
search.date &&
!isSameDayAnyYear(search.date)(
new Date(item.metadata.creationTime / 1000),
new Date(item.metadata.creationTime / 1000)
)
) {
return false;
@ -397,11 +403,10 @@ const PhotoFrame = ({
return false;
});
const isSameDay = (first, second) => (
const isSameDay = (first, second) =>
first.getFullYear() === second.getFullYear() &&
first.getMonth() === second.getMonth() &&
first.getDate() === second.getDate()
);
first.getDate() === second.getDate();
/**
* Checks and merge multiple dates into a single row.
@ -410,7 +415,10 @@ const PhotoFrame = ({
* @param columns
* @returns
*/
const mergeTimeStampList = (items: TimeStampListItem[], columns: number): TimeStampListItem[] => {
const mergeTimeStampList = (
items: TimeStampListItem[],
columns: number
): TimeStampListItem[] => {
const newList: TimeStampListItem[] = [];
let index = 0;
let newIndex = 0;
@ -423,12 +431,18 @@ const PhotoFrame = ({
// we can add more items to the same list.
if (newList[newIndex]) {
// Check if items can be added to same list
if (newList[newIndex + 1].items.length + items[index + 1].items.length <= columns) {
if (
newList[newIndex + 1].items.length +
items[index + 1].items.length <=
columns
) {
newList[newIndex].dates.push({
date: currItem.date,
span: items[index + 1].items.length,
});
newList[newIndex + 1].items = newList[newIndex + 1].items.concat(items[index + 1].items);
newList[newIndex + 1].items = newList[
newIndex + 1
].items.concat(items[index + 1].items);
index += 2;
} else {
// Adding items would exceed the number of columns.
@ -441,10 +455,12 @@ const PhotoFrame = ({
newList.push({
...currItem,
date: null,
dates: [{
dates: [
{
date: currItem.date,
span: items[index + 1].items.length,
}],
},
],
});
newList.push(items[index + 1]);
index += 2;
@ -474,7 +490,7 @@ const PhotoFrame = ({
<>
{!isFirstLoad && files.length === 0 && !searchMode ? (
<EmptyScreen>
<img height={150} src='/images/gallery.png' />
<img height={150} src="/images/gallery.png" />
<Button
variant="outline-success"
onClick={openFileUploader}
@ -484,8 +500,7 @@ const PhotoFrame = ({
paddingRight: '32px',
paddingTop: '12px',
paddingBottom: '12px',
}}
>
}}>
{constants.UPLOAD_FIRST_PHOTO}
</Button>
</EmptyScreen>
@ -493,7 +508,9 @@ const PhotoFrame = ({
<Container>
<AutoSizer>
{({ height, width }) => {
let columns = Math.floor(width / IMAGE_CONTAINER_MAX_WIDTH);
let columns = Math.floor(
width / IMAGE_CONTAINER_MAX_WIDTH
);
let listItemHeight = IMAGE_CONTAINER_MAX_HEIGHT;
let skipMerge = false;
if (columns < MIN_COLUMNS) {
@ -506,9 +523,18 @@ const PhotoFrame = ({
let listItemIndex = 0;
let currentDate = -1;
filteredData.forEach((item, index) => {
if (!isSameDay(new Date(item.metadata.creationTime / 1000), new Date(currentDate))) {
currentDate = item.metadata.creationTime / 1000;
const dateTimeFormat = new Intl.DateTimeFormat('en-IN', {
if (
!isSameDay(
new Date(
item.metadata.creationTime / 1000
),
new Date(currentDate)
)
) {
currentDate =
item.metadata.creationTime / 1000;
const dateTimeFormat =
new Intl.DateTimeFormat('en-IN', {
weekday: 'short',
year: 'numeric',
month: 'short',
@ -518,16 +544,16 @@ const PhotoFrame = ({
itemType: ITEM_TYPE.TIME,
date: isSameDay(
new Date(currentDate),
new Date(),
) ?
'Today' :
isSameDay(
new Date()
)
? 'Today'
: isSameDay(
new Date(currentDate),
new Date(Date.now() - A_DAY),
) ?
'Yesterday' :
dateTimeFormat.format(
currentDate,
new Date(Date.now() - A_DAY)
)
? 'Yesterday'
: dateTimeFormat.format(
currentDate
),
id: currentDate.toString(),
});
@ -553,7 +579,10 @@ const PhotoFrame = ({
});
if (!skipMerge) {
timeStampList = mergeTimeStampList(timeStampList, columns);
timeStampList = mergeTimeStampList(
timeStampList,
columns
);
}
const getItemSize = (index) => {
@ -574,61 +603,82 @@ const PhotoFrame = ({
}
return sum;
})();
files.length < 30 && !searchMode &&
files.length < 30 &&
!searchMode &&
timeStampList.push({
itemType: ITEM_TYPE.BANNER,
banner: (
<BannerContainer span={columns}>
<p>{constants.INSTALL_MOBILE_APP()}</p>
<p>
{constants.INSTALL_MOBILE_APP()}
</p>
</BannerContainer>
),
id: 'install-banner',
height: Math.max(48, height-photoFrameHeight),
height: Math.max(
48,
height - photoFrameHeight
),
});
const extraRowsToRender = Math.ceil(
(NO_OF_PAGES * height) / IMAGE_CONTAINER_MAX_HEIGHT,
(NO_OF_PAGES * height) /
IMAGE_CONTAINER_MAX_HEIGHT
);
const generateKey = (index) => {
switch (timeStampList[index].itemType) {
case ITEM_TYPE.TILE:
return `${timeStampList[index].items[0].id}-${timeStampList[index].items.slice(-1)[0].id}`;
return `${
timeStampList[index].items[0].id
}-${
timeStampList[index].items.slice(
-1
)[0].id
}`;
default:
return `${timeStampList[index].id}-${index}`;
}
};
const renderListItem = (listItem: TimeStampListItem) => {
const renderListItem = (
listItem: TimeStampListItem
) => {
switch (listItem.itemType) {
case ITEM_TYPE.TIME:
return listItem.dates ?
return listItem.dates ? (
listItem.dates.map((item) => (
<>
<DateContainer key={item.date} span={item.span}>
<DateContainer
key={item.date}
span={item.span}>
{item.date}
</DateContainer>
<div />
</>
)) :
(
))
) : (
<DateContainer span={columns}>
{listItem.date}
</DateContainer>
);
case ITEM_TYPE.BANNER:
return listItem.banner;
default:
{
const ret = (listItem.items.map(
(item, idx) => getThumbnail(
default: {
const ret = listItem.items.map(
(item, idx) =>
getThumbnail(
filteredData,
listItem.itemStartIndex + idx,
),
));
listItem.itemStartIndex +
idx
)
);
if (listItem.groups) {
let sum = 0;
for (let i = 0; i < listItem.groups.length - 1; i++) {
for (
let i = 0;
i < listItem.groups.length - 1;
i++
) {
sum = sum + listItem.groups[i];
ret.splice(sum, 0, <div />);
sum += 1;
@ -648,12 +698,17 @@ const PhotoFrame = ({
width={width}
itemCount={timeStampList.length}
itemKey={generateKey}
overscanCount={extraRowsToRender}
>
overscanCount={extraRowsToRender}>
{({ index, style }) => (
<ListItem style={style}>
<ListContainer columns={columns} groups={timeStampList[index].groups}>
{renderListItem(timeStampList[index])}
<ListContainer
columns={columns}
groups={
timeStampList[index].groups
}>
{renderListItem(
timeStampList[index]
)}
</ListContainer>
</ListItem>
)}

View file

@ -7,7 +7,7 @@ import {
addToFavorites,
removeFromFavorites,
} from 'services/collectionService';
import { File } from 'services/fileService';
import { File, FILE_TYPE } from 'services/fileService';
import constants from 'utils/strings/constants';
import DownloadManger from 'services/downloadManager';
import EXIF from 'exif-js';
@ -16,7 +16,7 @@ import Button from 'react-bootstrap/Button';
import Form from 'react-bootstrap/Form';
import styled from 'styled-components';
import events from './events';
import { formatDateTime } from 'utils/file';
import { fileNameWithoutExtension, formatDateTime } from 'utils/file';
import { FormCheck } from 'react-bootstrap';
interface Iprops {
@ -49,8 +49,12 @@ const Pre = styled.pre`
const renderInfoItem = (label: string, value: string | JSX.Element) => (
<>
<Form.Label column sm="4">{label}</Form.Label>
<Form.Label column sm="8">{value}</Form.Label>
<Form.Label column sm="4">
{label}
</Form.Label>
<Form.Label column sm="8">
{value}
</Form.Label>
</>
);
@ -62,18 +66,37 @@ function ExifData(props: { exif: any }) {
setShowAll(e.target.checked);
};
const renderAllValues = () => (<Pre>{exif.raw}</Pre>);
const renderAllValues = () => <Pre>{exif.raw}</Pre>;
const renderSelectedValues = () => (<>
{exif?.Make && exif?.Model && renderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
{exif?.ImageWidth && exif?.ImageHeight && renderInfoItem(constants.IMAGE_SIZE, `${exif.ImageWidth} x ${exif.ImageHeight}`)}
const renderSelectedValues = () => (
<>
{exif?.Make &&
exif?.Model &&
renderInfoItem(constants.DEVICE, `${exif.Make} ${exif.Model}`)}
{exif?.ImageWidth &&
exif?.ImageHeight &&
renderInfoItem(
constants.IMAGE_SIZE,
`${exif.ImageWidth} x ${exif.ImageHeight}`
)}
{exif?.Flash && renderInfoItem(constants.FLASH, exif.Flash)}
{exif?.FocalLength && renderInfoItem(constants.FOCAL_LENGTH, exif.FocalLength.toString())}
{exif?.ApertureValue && renderInfoItem(constants.APERTURE, exif.ApertureValue.toString())}
{exif?.ISOSpeedRatings && renderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
</>);
{exif?.FocalLength &&
renderInfoItem(
constants.FOCAL_LENGTH,
exif.FocalLength.toString()
)}
{exif?.ApertureValue &&
renderInfoItem(
constants.APERTURE,
exif.ApertureValue.toString()
)}
{exif?.ISOSpeedRatings &&
renderInfoItem(constants.ISO, exif.ISOSpeedRatings.toString())}
</>
);
return (<>
return (
<>
<LegendContainer>
<Legend>{constants.EXIF}</Legend>
<FormCheck>
@ -84,7 +107,8 @@ function ExifData(props: { exif: any }) {
</FormCheck>
</LegendContainer>
{showAll ? renderAllValues() : renderSelectedValues()}
</>);
</>
);
}
function PhotoSwipe(props: Iprops) {
@ -140,8 +164,14 @@ function PhotoSwipe(props: Iprops) {
const ele = document.getElementById(`thumb-${file.id}`);
if (ele) {
const rect = ele.getBoundingClientRect();
const pageYScroll = window.pageYOffset || document.documentElement.scrollTop;
return { x: rect.left, y: rect.top + pageYScroll, w: rect.width };
const pageYScroll =
window.pageYOffset ||
document.documentElement.scrollTop;
return {
x: rect.left,
y: rect.top + pageYScroll,
w: rect.width,
};
}
return null;
} catch (e) {
@ -153,7 +183,7 @@ function PhotoSwipe(props: Iprops) {
pswpElement.current,
PhotoswipeUIDefault,
items,
options,
options
);
events.forEach((event) => {
const callback = props[event];
@ -201,7 +231,8 @@ function PhotoSwipe(props: Iprops) {
const { favItemIds } = props;
if (favItemIds && file) {
return favItemIds.has(file.id);
} return false;
}
return false;
};
const onFavClick = async (file) => {
@ -232,7 +263,9 @@ function PhotoSwipe(props: Iprops) {
const checkExifAvailable = () => {
setExif(null);
setTimeout(() => {
const img = document.querySelector('.pswp__img:not(.pswp__img--placeholder)');
const img = document.querySelector(
'.pswp__img:not(.pswp__img--placeholder)'
);
if (img) {
// @ts-expect-error
EXIF.getData(img, function () {
@ -269,7 +302,11 @@ function PhotoSwipe(props: Iprops) {
loadingBar.current.continuousStart();
a.href = await DownloadManger.getFile(file);
loadingBar.current.complete();
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
a.download = fileNameWithoutExtension(file.metadata.title) + '.zip';
} else {
a.download = file.metadata.title;
}
document.body.appendChild(a);
a.click();
a.remove();
@ -285,8 +322,7 @@ function PhotoSwipe(props: Iprops) {
tabIndex={Number('-1')}
role="dialog"
aria-hidden="true"
ref={pswpElement}
>
ref={pswpElement}>
<div className="pswp__bg" />
<div className="pswp__scroll-wrap">
<div className="pswp__container">
@ -306,7 +342,9 @@ function PhotoSwipe(props: Iprops) {
<button
className="pswp-custom download-btn"
title={constants.DOWNLOAD}
onClick={() => downloadFile(photoSwipe.currItem)}
onClick={() =>
downloadFile(photoSwipe.currItem)
}
/>
<button
@ -363,26 +401,46 @@ function PhotoSwipe(props: Iprops) {
<div>
<Legend>{constants.METADATA}</Legend>
</div>
{renderInfoItem(constants.FILE_ID, items[photoSwipe?.getCurrentIndex()]?.id)}
{metadata?.title && renderInfoItem(constants.FILE_NAME, metadata.title)}
{metadata?.creationTime && renderInfoItem(constants.CREATION_TIME, formatDateTime(metadata.creationTime / 1000))}
{metadata?.modificationTime && renderInfoItem(constants.UPDATED_ON, formatDateTime(metadata.modificationTime / 1000))}
{metadata?.longitude && metadata?.longitude && renderInfoItem(constants.LOCATION, (
<a href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
target='_blank' rel='noopener noreferrer'>
{renderInfoItem(
constants.FILE_ID,
items[photoSwipe?.getCurrentIndex()]?.id
)}
{metadata?.title &&
renderInfoItem(constants.FILE_NAME, metadata.title)}
{metadata?.creationTime &&
renderInfoItem(
constants.CREATION_TIME,
formatDateTime(metadata.creationTime / 1000)
)}
{metadata?.modificationTime &&
renderInfoItem(
constants.UPDATED_ON,
formatDateTime(metadata.modificationTime / 1000)
)}
{metadata?.longitude &&
metadata?.longitude &&
renderInfoItem(
constants.LOCATION,
<a
href={`https://www.openstreetmap.org/?mlat=${metadata.latitude}&mlon=${metadata.longitude}#map=15/${metadata.latitude}/${metadata.longitude}`}
target="_blank"
rel="noopener noreferrer">
{constants.SHOW_MAP}
</a>
))}
)}
{exif && (
<>
<br /><br />
<br />
<br />
<ExifData exif={exif} />
</>
)}
</Form.Group>
</Modal.Body>
<Modal.Footer>
<Button variant="outline-secondary" onClick={handleCloseInfo}>
<Button
variant="outline-secondary"
onClick={handleCloseInfo}>
{constants.CLOSE}
</Button>
</Modal.Footer>

View file

@ -71,14 +71,11 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
disabled: !recoveryKey,
variant: 'success',
},
}}
>
}}>
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
<CodeBlock height={150}>
{recoveryKey ? (
<FreeFlowText>
{recoveryKey}
</FreeFlowText>
<FreeFlowText>{recoveryKey}</FreeFlowText>
) : (
<EnteSpinner />
)}

View file

@ -97,7 +97,9 @@ export default function SearchBar(props: Props) {
}, [props.isOpen]);
useEffect(() => {
window.addEventListener('resize', () => setWindowWidth(window.innerWidth));
window.addEventListener('resize', () =>
setWindowWidth(window.innerWidth)
);
});
// = =========================
// Functionality
@ -119,18 +121,19 @@ export default function SearchBar(props: Props) {
type: SuggestionType.DATE,
value: searchedDate,
label: getFormattedDate(searchedDate),
})),
}))
);
const searchResults = await searchLocation(searchPhrase);
option.push(
...searchResults.map(
(searchResult) => ({
(searchResult) =>
({
type: SuggestionType.LOCATION,
value: searchResult.bbox,
label: searchResult.place,
} as Suggestion),
),
} as Suggestion)
)
);
return option;
};
@ -174,7 +177,8 @@ export default function SearchBar(props: Props) {
// UI
// = =========================
const getIconByType = (type: SuggestionType) => (type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />);
const getIconByType = (type: SuggestionType) =>
type === SuggestionType.DATE ? <DateIcon /> : <LocationIcon />;
const LabelWithIcon = (props: { type: SuggestionType; label: string }) => (
<div style={{ display: 'flex', alignItems: 'center' }}>
@ -198,8 +202,7 @@ export default function SearchBar(props: Props) {
style={{
paddingLeft: '10px',
paddingBottom: '4px',
}}
>
}}>
{props.getValue().length === 0 || props.menuIsOpen ? (
<SearchIcon />
) : props.getValue()[0].type === SuggestionType.DATE ? (
@ -215,13 +218,13 @@ export default function SearchBar(props: Props) {
const customStyles = {
control: (style, { isFocused }) => ({
...style,
'backgroundColor': '#282828',
'color': '#d1d1d1',
'borderColor': isFocused ? '#2dc262' : '#444',
'boxShadow': 'none',
backgroundColor: '#282828',
color: '#d1d1d1',
borderColor: isFocused ? '#2dc262' : '#444',
boxShadow: 'none',
':hover': {
'borderColor': '#2dc262',
'cursor': 'text',
borderColor: '#2dc262',
cursor: 'text',
'&>.icon': { color: '#2dc262' },
},
}),
@ -276,8 +279,7 @@ export default function SearchBar(props: Props) {
style={{
flex: 1,
margin: '10px',
}}
>
}}>
<AsyncSelect
components={{
Option: OptionWithIcon,
@ -297,8 +299,7 @@ export default function SearchBar(props: Props) {
{props.isOpen && (
<div
style={{ cursor: 'pointer' }}
onClick={resetSearch}
>
onClick={resetSearch}>
<CrossIcon />
</div>
)}
@ -307,8 +308,7 @@ export default function SearchBar(props: Props) {
) : (
<SearchButton
isDisabled={props.isFirstFetch}
onClick={() => !props.isFirstFetch && props.setOpen(true)}
>
onClick={() => !props.isFirstFetch && props.setOpen(true)}>
<SearchIcon />
</SearchButton>
)}

View file

@ -21,7 +21,7 @@ function SetPasswordForm(props: Props) {
const [loading, setLoading] = useState(false);
const onSubmit = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>,
{ setFieldError }: FormikHelpers<formValues>
) => {
setLoading(true);
try {
@ -34,7 +34,7 @@ function SetPasswordForm(props: Props) {
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`,
`${constants.UNKNOWN_ERROR} ${e.message}`
);
} finally {
setLoading(false);
@ -46,8 +46,7 @@ function SetPasswordForm(props: Props) {
<Card.Body>
<div
className="text-center"
style={{ marginBottom: '40px' }}
>
style={{ marginBottom: '40px' }}>
<p>{constants.ENTER_ENC_PASSPHRASE}</p>
{constants.PASSPHRASE_DISCLAIMER()}
</div>
@ -55,14 +54,13 @@ function SetPasswordForm(props: Props) {
initialValues={{ passphrase: '', confirm: '' }}
validationSchema={Yup.object().shape({
passphrase: Yup.string().required(
constants.REQUIRED,
constants.REQUIRED
),
confirm: Yup.string().required(constants.REQUIRED),
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={onSubmit}
>
onSubmit={onSubmit}>
{({
values,
touched,
@ -79,7 +77,7 @@ function SetPasswordForm(props: Props) {
onChange={handleChange('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase,
errors.passphrase
)}
autoFocus
disabled={loading}
@ -97,7 +95,7 @@ function SetPasswordForm(props: Props) {
value={values.confirm}
onChange={handleChange('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm,
touched.confirm && errors.confirm
)}
disabled={loading}
/>
@ -115,8 +113,7 @@ function SetPasswordForm(props: Props) {
{props.back && (
<div
className="text-center"
style={{ marginTop: '20px' }}
>
style={{ marginTop: '20px' }}>
<Button variant="link" onClick={props.back}>
{constants.GO_BACK}
</Button>

View file

@ -1,7 +1,6 @@
import React, { useEffect, useState } from 'react';
import { slide as Menu } from 'react-burger-menu';
import billingService, { Subscription } from 'services/billingService';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { getToken } from 'utils/common/key';
@ -14,6 +13,7 @@ import {
isOnFreePlan,
isSubscriptionCancelled,
isSubscribed,
convertToHumanReadable,
} from 'utils/billingUtil';
import isElectron from 'is-electron';
@ -21,7 +21,7 @@ import { Collection } from 'services/collectionService';
import { useRouter } from 'next/router';
import LinkButton from './pages/gallery/LinkButton';
import { downloadApp } from 'utils/common';
import { logoutUser } from 'services/userService';
import { getUserDetails, logoutUser } from 'services/userService';
import { LogoImage } from 'pages/_app';
import { SetDialogMessage } from './MessageDialog';
import EnteSpinner from './EnteSpinner';
@ -31,11 +31,12 @@ import ExportModal from './ExportModal';
import { SetLoading } from 'pages/gallery';
import InProgressIcon from './icons/InProgressIcon';
import exportService from 'services/exportService';
import { Subscription } from 'services/billingService';
interface Props {
collections: Collection[];
setDialogMessage: SetDialogMessage;
setLoading: SetLoading,
setLoading: SetLoading;
showPlanSelectorModal: () => void;
}
export default function Sidebar(props: Props) {
@ -55,16 +56,23 @@ export default function Sidebar(props: Props) {
if (!isOpen) {
return;
}
const usage = await billingService.getUsage();
SetUsage(usage);
setSubscription(getUserSubscription());
const userDetails = await getUserDetails();
setUser({ ...user, email: userDetails.email });
SetUsage(convertToHumanReadable(userDetails.usage));
setSubscription(userDetails.subscription);
setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
email: userDetails.email,
});
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
};
main();
}, [isOpen]);
function openFeedbackURL() {
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(getToken())}`;
const feedbackURL: string = `${getEndpoint()}/users/feedback?token=${encodeURIComponent(
getToken()
)}`;
const win = window.open(feedbackURL, '_blank');
win.focus();
}
@ -105,9 +113,13 @@ export default function Sidebar(props: Props) {
<Menu
isOpen={isOpen}
onStateChange={(state) => setIsOpen(state.isOpen)}
itemListElement="div"
>
<div style={{ display: 'flex', outline: 'none', textAlign: 'center' }}>
itemListElement="div">
<div
style={{
display: 'flex',
outline: 'none',
textAlign: 'center',
}}>
<LogoImage
style={{ height: '24px', padding: '3px' }}
alt="logo"
@ -119,11 +131,16 @@ export default function Sidebar(props: Props) {
outline: 'none',
color: 'rgb(45, 194, 98)',
fontSize: '16px',
}}
>
}}>
{user?.email}
</div>
<div style={{ flex: 1, overflow: 'auto', outline: 'none', paddingTop: '0' }}>
<div
style={{
flex: 1,
overflow: 'auto',
outline: 'none',
paddingTop: '0',
}}>
<div style={{ outline: 'none' }}>
<div style={{ display: 'flex' }}>
<h5 style={{ margin: '4px 0 12px 2px' }}>
@ -134,15 +151,15 @@ export default function Sidebar(props: Props) {
{isSubscriptionActive(subscription) ? (
isOnFreePlan(subscription) ? (
constants.FREE_SUBSCRIPTION_INFO(
subscription?.expiryTime,
subscription?.expiryTime
)
) : isSubscriptionCancelled(subscription) ? (
constants.RENEWAL_CANCELLED_SUBSCRIPTION_INFO(
subscription?.expiryTime,
subscription?.expiryTime
)
) : (
constants.RENEWAL_ACTIVE_SUBSCRIPTION_INFO(
subscription?.expiryTime,
subscription?.expiryTime
)
)
) : (
@ -152,11 +169,10 @@ export default function Sidebar(props: Props) {
variant="outline-success"
block
size="sm"
onClick={onManageClick}
>
{isSubscribed(subscription) ?
constants.MANAGE :
constants.SUBSCRIBE}
onClick={onManageClick}>
{isSubscribed(subscription)
? constants.MANAGE
: constants.SUBSCRIBE}
</Button>
</div>
</div>
@ -169,7 +185,7 @@ export default function Sidebar(props: Props) {
{usage ? (
constants.USAGE_INFO(
usage,
Number(convertBytesToGBs(subscription?.storage)),
Number(convertBytesToGBs(subscription?.storage))
)
) : (
<div style={{ textAlign: 'center' }}>
@ -194,29 +210,28 @@ export default function Sidebar(props: Props) {
/>
<LinkButton
style={{ marginTop: '30px' }}
onClick={openFeedbackURL}
>
onClick={openFeedbackURL}>
{constants.REQUEST_FEATURE}
</LinkButton>
<LinkButton
style={{ marginTop: '30px' }}
onClick={openSupportMail}
>
onClick={openSupportMail}>
{constants.SUPPORT}
</LinkButton>
<>
<RecoveryKeyModal
show={recoverModalView}
onHide={() => setRecoveryModalView(false)}
somethingWentWrong={() => props.setDialogMessage({
somethingWentWrong={() =>
props.setDialogMessage({
title: constants.RECOVER_KEY_GENERATION_FAILED,
close: { variant: 'danger' },
})}
})
}
/>
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => setRecoveryModalView(true)}
>
onClick={() => setRecoveryModalView(true)}>
{constants.DOWNLOAD_RECOVERY_KEY}
</LinkButton>
</>
@ -230,8 +245,7 @@ export default function Sidebar(props: Props) {
/>
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => setTwoFactorModalView(true)}
>
onClick={() => setTwoFactorModalView(true)}>
{constants.TWO_FACTOR}
</LinkButton>
</>
@ -240,18 +254,32 @@ export default function Sidebar(props: Props) {
onClick={() => {
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
router.push('change-password');
}}
>
}}>
{constants.CHANGE_PASSWORD}
</LinkButton>
<LinkButton
style={{ marginTop: '30px' }}
onClick={() => {
setData(LS_KEYS.SHOW_BACK_BUTTON, { value: true });
router.push('change-email');
}}>
{constants.UPDATE_EMAIL}
</LinkButton>
<>
<ExportModal show={exportModalView} onHide={() => setExportModalView(false)} usage={usage} />
<LinkButton style={{ marginTop: '30px' }} onClick={exportFiles}>
<ExportModal
show={exportModalView}
onHide={() => setExportModalView(false)}
usage={usage}
/>
<LinkButton
style={{ marginTop: '30px' }}
onClick={exportFiles}>
<div style={{ display: 'flex' }}>
{constants.EXPORT}<div style={{ width: '20px' }} />
{exportService.isExportInProgress() &&
{constants.EXPORT}
<div style={{ width: '20px' }} />
{exportService.isExportInProgress() && (
<InProgressIcon />
}
)}
</div>
</LinkButton>
</>
@ -266,7 +294,8 @@ export default function Sidebar(props: Props) {
<LinkButton
variant="danger"
style={{ marginTop: '30px' }}
onClick={() => props.setDialogMessage({
onClick={() =>
props.setDialogMessage({
title: `${constants.CONFIRM} ${constants.LOGOUT}`,
content: constants.LOGOUT_MESSAGE,
staticBackdrop: true,
@ -276,8 +305,8 @@ export default function Sidebar(props: Props) {
variant: 'danger',
},
close: { text: constants.CANCEL },
})}
>
})
}>
{constants.LOGOUT}
</LinkButton>
<div

View file

@ -36,7 +36,7 @@ export default function SignUp(props: SignUpProps) {
const registerUser = async (
{ email, passphrase, confirm }: FormValues,
{ setFieldError }: FormikHelpers<FormValues>,
{ setFieldError }: FormikHelpers<FormValues>
) => {
setLoading(true);
try {
@ -47,12 +47,13 @@ export default function SignUp(props: SignUpProps) {
}
try {
if (passphrase === confirm) {
const { keyAttributes, masterKey } = await generateKeyAttributes(passphrase);
const { keyAttributes, masterKey } =
await generateKeyAttributes(passphrase);
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
masterKey,
masterKey
);
await setSessionKeys(masterKey);
@ -68,7 +69,8 @@ export default function SignUp(props: SignUpProps) {
setLoading(false);
};
return (<>
return (
<>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src="/icon.svg" />
{constants.SIGN_UP}
@ -83,15 +85,12 @@ export default function SignUp(props: SignUpProps) {
email: Yup.string()
.email(constants.EMAIL_ERROR)
.required(constants.REQUIRED),
passphrase: Yup.string().required(
constants.REQUIRED,
),
passphrase: Yup.string().required(constants.REQUIRED),
confirm: Yup.string().required(constants.REQUIRED),
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={registerUser}
>
onSubmit={registerUser}>
{({
values,
errors,
@ -107,7 +106,7 @@ export default function SignUp(props: SignUpProps) {
value={values.email}
onChange={handleChange('email')}
isInvalid={Boolean(
touched.email && errors.email,
touched.email && errors.email
)}
autoFocus
disabled={loading}
@ -123,8 +122,7 @@ export default function SignUp(props: SignUpProps) {
value={values.passphrase}
onChange={handleChange('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase,
touched.passphrase && errors.passphrase
)}
disabled={loading}
/>
@ -135,13 +133,11 @@ export default function SignUp(props: SignUpProps) {
<Form.Group>
<Form.Control
type="password"
placeholder={
constants.RE_ENTER_PASSPHRASE
}
placeholder={constants.RE_ENTER_PASSPHRASE}
value={values.confirm}
onChange={handleChange('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm,
touched.confirm && errors.confirm
)}
disabled={loading}
/>
@ -154,11 +150,12 @@ export default function SignUp(props: SignUpProps) {
marginBottom: '0',
textAlign: 'left',
}}
controlId="formBasicCheckbox-1"
>
controlId="formBasicCheckbox-1">
<Form.Check
checked={acceptTerms}
onChange={(e) => setAcceptTerms(e.target.checked)}
onChange={(e) =>
setAcceptTerms(e.target.checked)
}
type="checkbox"
label={constants.TERMS_AND_CONDITIONS()}
/>
@ -176,5 +173,6 @@ export default function SignUp(props: SignUpProps) {
</Form>
)}
</Formik>
</>);
</>
);
}

View file

@ -4,6 +4,9 @@ import { Form } from 'react-bootstrap';
import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup';
import SubmitButton from './SubmitButton';
import styled from 'styled-components';
import Visibility from './icons/Visibility';
import VisibilityOff from './icons/VisibilityOff';
interface formValues {
passphrase: string;
@ -15,11 +18,29 @@ interface Props {
buttonText: string;
}
const Group = styled.div`
position: relative;
`;
const Button = styled.button`
background: transparent;
border: none;
width: 46px;
height: 34px;
position: absolute;
top: 1px;
right: 1px;
border-radius: 5px;
align-items: center;
`;
export default function SingleInputForm(props: Props) {
const [loading, SetLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const submitForm = async (
values: formValues,
{ setFieldError }: FormikHelpers<formValues>,
{ setFieldError }: FormikHelpers<formValues>
) => {
SetLoading(true);
await props.callback(values.passphrase, setFieldError);
@ -33,27 +54,39 @@ export default function SingleInputForm(props: Props) {
passphrase: Yup.string().required(constants.REQUIRED),
})}
validateOnChange={false}
validateOnBlur={false}
>
{({
values, touched, errors, handleChange, handleSubmit,
}) => (
validateOnBlur={false}>
{({ values, touched, errors, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Group>
<Form.Control
type={props.fieldType}
type={showPassword ? 'text' : props.fieldType}
placeholder={props.placeholder}
value={values.passphrase}
onChange={handleChange('passphrase')}
isInvalid={Boolean(
touched.passphrase && errors.passphrase,
touched.passphrase && errors.passphrase
)}
disabled={loading}
autoFocus
/>
{props.fieldType === 'password' && (
<Button
type="button"
onClick={() =>
setShowPassword(!showPassword)
}>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</Button>
)}
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Group>
</Form.Group>
<SubmitButton
buttonText={props.buttonText}

View file

@ -7,22 +7,24 @@ interface Props {
inline?: any;
disabled?: boolean;
}
const SubmitButton = ({
loading, buttonText, inline, disabled,
}: Props) => (
const SubmitButton = ({ loading, buttonText, inline, disabled }: Props) => (
<Button
className="submitButton"
variant="outline-success"
type="submit"
block={!inline}
disabled={loading || disabled}
style={{ padding: '6px 1em' }}
>
style={{ padding: '6px 1em' }}>
{loading ? (
<Spinner
as="span"
animation="border"
style={{ width: '22px', height: '22px', borderWidth: '0.20em', color: '#2dc262' }}
style={{
width: '22px',
height: '22px',
borderWidth: '0.20em',
color: '#2dc262',
}}
/>
) : (
buttonText

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router';
import { DeadCenter, SetLoading } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
import React, { useContext, useEffect, useState } from 'react';
import { Button, Row } from 'react-bootstrap';
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
@ -13,7 +13,7 @@ interface Props {
show: boolean;
onHide: () => void;
setDialogMessage: SetDialogMessage;
setLoading: SetLoading
setLoading: SetLoading;
closeSidebar: () => void;
}
@ -26,12 +26,16 @@ function TwoFactorModal(props: Props) {
if (!props.show) {
return;
}
const isTwoFactorEnabled = getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
const isTwoFactorEnabled =
getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
setTwoFactorStatus(isTwoFactorEnabled);
const main = async () => {
const isTwoFactorEnabled = await getTwoFactorStatus();
setTwoFactorStatus(isTwoFactorEnabled);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
isTwoFactorEnabled: false,
});
};
main();
}, [props.show]);
@ -51,12 +55,21 @@ function TwoFactorModal(props: Props) {
const twoFactorDisable = async () => {
try {
await disableTwoFactor();
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false });
setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
isTwoFactorEnabled: false,
});
props.onHide();
props.closeSidebar();
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_SUCCESS, severity: 'info' });
appContext.setDisappearingFlashMessage({
message: constants.TWO_FACTOR_DISABLE_SUCCESS,
type: FLASH_MESSAGE_TYPE.INFO,
});
} catch (e) {
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_DISABLE_FAILED, severity: 'danger' });
appContext.setDisappearingFlashMessage({
message: constants.TWO_FACTOR_DISABLE_FAILED,
type: FLASH_MESSAGE_TYPE.DANGER,
});
}
};
const warnTwoFactorReconfigure = async () => {
@ -82,36 +95,58 @@ function TwoFactorModal(props: Props) {
attributes={{
title: constants.TWO_FACTOR_AUTHENTICATION,
staticBackdrop: true,
}}
>
<div {...(!isTwoFactorEnabled ? { style: { padding: '10px 10px 30px 10px' } } : { style: { padding: '10px' } })}>
{
isTwoFactorEnabled ?
}}>
<div
{...(!isTwoFactorEnabled
? { style: { padding: '10px 10px 30px 10px' } }
: { style: { padding: '10px' } })}>
{isTwoFactorEnabled ? (
<>
<Row>
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
<Value>
<Button variant={'outline-success'} onClick={warnTwoFactorReconfigure}>{constants.RECONFIGURE}</Button>
<Button
variant={'outline-success'}
onClick={warnTwoFactorReconfigure}>
{constants.RECONFIGURE}
</Button>
</Value>
</Row>
<Row>
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
<Value>
<Button variant={'outline-danger'} onClick={warnTwoFactorDisable}>{constants.DISABLE}</Button>
<Button
variant={'outline-danger'}
onClick={warnTwoFactorDisable}>
{constants.DISABLE}
</Button>
</Value>
</Row>
</> : (
</>
) : (
<DeadCenter>
<svg xmlns="http://www.w3.org/2000/svg" height="36px" viewBox="0 0 24 24" width="36px" fill="#000000"><g fill="none"><path d="M0 0h24v24H0V0z" /><path d="M0 0h24v24H0V0z" opacity=".87" /></g><path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" /></svg>
<svg
xmlns="http://www.w3.org/2000/svg"
height="36px"
viewBox="0 0 24 24"
width="36px"
fill="#000000">
<g fill="none">
<path d="M0 0h24v24H0V0z" />
<path d="M0 0h24v24H0V0z" opacity=".87" />
</g>
<path d="M18 8h-1V6c0-2.76-2.24-5-5-5S7 3.24 7 6v2H6c-1.1 0-2 .9-2 2v10c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V10c0-1.1-.9-2-2-2zM9 6c0-1.66 1.34-3 3-3s3 1.34 3 3v2H9V6zm9 14H6V10h12v10zm-6-3c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2z" />
</svg>
<p />
<p>{constants.TWO_FACTOR_INFO}</p>
<div style={{ height: '10px' }} />
<Button variant="outline-success" onClick={() => router.push('/two-factor/setup')}>{constants.ENABLE_TWO_FACTOR}</Button>
<Button
variant="outline-success"
onClick={() => router.push('/two-factor/setup')}>
{constants.ENABLE_TWO_FACTOR}
</Button>
</DeadCenter>
)
}
)}
</div>
</MessageDialog>
);

View file

@ -11,8 +11,8 @@ interface formValues {
otp: string;
}
interface Props {
onSubmit: any
back: any
onSubmit: any;
back: any;
buttonText: string;
}
@ -21,7 +21,7 @@ export default function VerifyTwoFactor(props: Props) {
const otpInputRef = useRef(null);
const submitForm = async (
{ otp }: formValues,
{ setFieldError, resetForm }: FormikHelpers<formValues>,
{ setFieldError, resetForm }: FormikHelpers<formValues>
) => {
try {
setWaiting(true);
@ -36,7 +36,11 @@ export default function VerifyTwoFactor(props: Props) {
setWaiting(false);
};
const onChange = (otp: string, callback: Function, triggerSubmit: Function) => {
const onChange = (
otp: string,
callback: Function,
triggerSubmit: Function
) => {
callback(otp);
if (otp.length === 6) {
triggerSubmit(otp);
@ -44,13 +48,14 @@ export default function VerifyTwoFactor(props: Props) {
};
return (
<>
<p style={{ marginBottom: '30px' }}>enter the 6-digit code from your authenticator app.</p>
<p style={{ marginBottom: '30px' }}>
enter the 6-digit code from your authenticator app.
</p>
<Formik<formValues>
initialValues={{ otp: '' }}
validateOnChange={false}
validateOnBlur={false}
onSubmit={submitForm}
>
onSubmit={submitForm}>
{({
values,
errors,
@ -58,8 +63,13 @@ export default function VerifyTwoFactor(props: Props) {
handleSubmit,
submitForm,
}) => (
<Form noValidate onSubmit={handleSubmit} style={{ width: '100%' }}>
<Form.Group style={{ marginBottom: '32px' }} controlId="formBasicEmail">
<Form
noValidate
onSubmit={handleSubmit}
style={{ width: '100%' }}>
<Form.Group
style={{ marginBottom: '32px' }}
controlId="formBasicEmail">
<DeadCenter>
<OtpInput
placeholder="123456"
@ -67,16 +77,27 @@ export default function VerifyTwoFactor(props: Props) {
shouldAutoFocus
value={values.otp}
onChange={(otp) => {
onChange(otp, handleChange('otp'), submitForm);
onChange(
otp,
handleChange('otp'),
submitForm
);
}}
numInputs={6}
separator={'-'}
isInputNum
className={'otp-input'}
/>
{errors.otp &&
<div style={{ display: 'block', marginTop: '16px' }} className="invalid-feedback">{constants.INCORRECT_CODE}</div>
}
{errors.otp && (
<div
style={{
display: 'block',
marginTop: '16px',
}}
className="invalid-feedback">
{constants.INCORRECT_CODE}
</div>
)}
</DeadCenter>
</Form.Group>
<SubmitButton
@ -87,10 +108,6 @@ export default function VerifyTwoFactor(props: Props) {
</Form>
)}
</Formik>
</>
);
}

View file

@ -7,9 +7,15 @@ export default function AddIcon(props) {
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill='currentColor'
>
<g><rect fill="none" height="24" width="24"/></g><g><g><path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z"/></g></g>
fill="currentColor">
<g>
<rect fill="none" height="24" width="24" />
</g>
<g>
<g>
<path d="M19,13h-6v6h-2v-6H5v-2h6V5h2v6h6V13z" />
</g>
</g>
</svg>
);
}

View file

@ -7,8 +7,7 @@ export default function ArrowEast(props) {
height={props.height}
viewBox={props.viewBox}
width={props.width}
{...props}
>
{...props}>
<rect fill="none" height="24" width="24" />
<path d="M15,5l-1.41,1.41L18.17,11H2V13h16.17l-4.59,4.59L15,19l7-7L15,5z" />
</svg>

View file

@ -7,8 +7,7 @@ export default function CloudUpload(props) {
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor"
>
fill="currentColor">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M19.35 10.04C18.67 6.59 15.64 4 12 4 9.11 4 6.6 5.64 5.35 8.04 2.34 8.36 0 10.91 0 14c0 3.31 2.69 6 6 6h13c2.76 0 5-2.24 5-5 0-2.64-2.05-4.78-4.65-4.96zM19 18H6c-2.21 0-4-1.79-4-4 0-2.05 1.53-3.76 3.56-3.97l1.07-.11.5-.95C8.08 7.14 9.94 6 12 6c2.62 0 4.88 1.86 5.39 4.43l.3 1.5 1.53.11c1.56.1 2.78 1.41 2.78 2.96 0 1.65-1.35 3-3 3zM8 13h2.55v3h2.9v-3H16l-4-4z" />
</svg>

View file

@ -6,8 +6,7 @@ export default function DateIcon(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M19 6.41L17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12 19 6.41z" />
</svg>
);

View file

@ -6,8 +6,7 @@ export default function DateIcon(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M19 4h-1V2h-2v2H8V2H6v2H5c-1.11 0-1.99.9-1.99 2L3 20a2 2 0 0 0 2 2h14c1.1 0 2-.9 2-2V6c0-1.1-.9-2-2-2zm0 16H5V10h14v10zm-4.5-7a2.5 2.5 0 0 0 0 5 2.5 2.5 0 0 0 0-5z" />
</svg>
);

View file

@ -7,8 +7,7 @@ export default function DeleteIcon(props) {
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill='currentColor'
>
fill="currentColor">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M6 19c0 1.1.9 2 2 2h8c1.1 0 2-.9 2-2V7H6v12zM19 4h-3.5l-1-1h-5l-1 1H5v2h14V4z" />
</svg>

View file

@ -0,0 +1,23 @@
import React from 'react';
export default function ExpandLess(props) {
return (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path d="M12 8l-6 6 1.41 1.41L12 10.83l4.59 4.58L18 14l-6-6z" />
</svg>
</div>
);
}
ExpandLess.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
open: false,
};

View file

@ -0,0 +1,23 @@
import React from 'react';
export default function ExpandMore(props) {
return (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="#000000">
<path d="M24 24H0V0h24v24z" fill="none" opacity=".87" />
<path d="M16.59 8.59L12 13.17 7.41 8.59 6 10l6 6 6-6-1.41-1.41z" />
</svg>
</div>
);
}
ExpandMore.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
open: false,
};

View file

@ -1,5 +1,3 @@
import React from 'react';
export default function FolderIcon(props) {
return (
@ -14,7 +12,6 @@ export default function FolderIcon(props) {
<path d="M20 6h-8l-2-2H4c-1.1 0-1.99.9-1.99 2L2 18c0 1.1.9 2 2 2h16c1.1 0 2-.9 2-2V8c0-1.1-.9-2-2-2zm0 12H4V8h16v10z" />
</svg>
</div>
);
}

View file

@ -4,7 +4,8 @@ import styled from 'styled-components';
const Rotate = styled.div<{ disabled }>`
width: 24px;
height: 27px;
${(props) => !props.disabled && '-webkit-animation: rotation 1s infinite linear'};
${(props) =>
!props.disabled && '-webkit-animation: rotation 1s infinite linear'};
cursor: ${(props) => props.disabled && 'pointer'};
transition-duration: 0.8s;
transition-property: transform;
@ -13,7 +14,6 @@ const Rotate = styled.div<{ disabled }>`
transform: rotate(90deg);
-webkit-transform: rotate(90deg);
}
`;
export default function InProgressIcon(props) {
return (
@ -24,7 +24,8 @@ export default function InProgressIcon(props) {
viewBox={props.viewBox}
width={props.width}
fill="#000000">
<path d="M.01 0h24v24h-24V0z" fill="none" /><path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
<path d="M.01 0h24v24h-24V0z" fill="none" />
<path d="M12 4V1L8 5l4 4V6c3.31 0 6 2.69 6 6 0 1.01-.25 1.97-.7 2.8l1.46 1.46C19.54 15.03 20 13.57 20 12c0-4.42-3.58-8-8-8zm0 14c-3.31 0-6-2.69-6-6 0-1.01.25-1.97.7-2.8L5.24 7.74C4.46 8.97 4 10.43 4 12c0 4.42 3.58 8 8 8v3l4-4-4-4v3z" />
</svg>
</Rotate>
);
@ -35,4 +36,3 @@ InProgressIcon.defaultProps = {
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -6,8 +6,7 @@ export default function LocationIcon(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M12 2C8.13 2 5 5.13 5 9c0 5.25 7 13 7 13s7-7.75 7-13c0-3.87-3.13-7-7-7zM7 9c0-2.76 2.24-5 5-5s5 2.24 5 5c0 2.88-2.88 7.19-5 9.88C9.92 16.21 7 11.85 7 9z" />
<circle cx="12" cy="9" r="2.5" />
</svg>

View file

@ -8,8 +8,7 @@ export default function NavigateNext(props) {
viewBox="0 0 24 24"
width="24px"
fill="currentColor"
{...props}
>
{...props}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M10 6L8.59 7.41 13.17 12l-4.58 4.59L10 18l6-6z" />
</svg>

View file

@ -6,8 +6,7 @@ export default function PlayCircleOutline(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M0 0h24v24H0z" fill="none" />
<path d="M10 16.5l6-4.5-6-4.5v9zM12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm0 18c-4.41 0-8-3.59-8-8s3.59-8 8-8 8 3.59 8 8-3.59 8-8 8z" />
</svg>

View file

@ -6,8 +6,7 @@ export default function SadFace(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M0 0h24v24H0V0z" fill="none" />
<circle cx="15.5" cy="9.5" r="1.5" />
<circle cx="8.5" cy="9.5" r="1.5" />

View file

@ -6,8 +6,7 @@ export default function SearchIcon(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path d="M20.49 19l-5.73-5.73C15.53 12.2 16 10.91 16 9.5A6.5 6.5 0 1 0 9.5 16c1.41 0 2.7-.47 3.77-1.24L19 20.49 20.49 19zM5 9.5C5 7.01 7.01 5 9.5 5S14 7.01 14 9.5 11.99 14 9.5 14 5 11.99 5 9.5z" />
</svg>
);

View file

@ -0,0 +1,23 @@
import React from 'react';
export default function Visibility(props) {
return (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="#000000">
<path d="M0 0h24v24H0z" fill="none" />
<path d="M12 4.5C7 4.5 2.73 7.61 1 12c1.73 4.39 6 7.5 11 7.5s9.27-3.11 11-7.5c-1.73-4.39-6-7.5-11-7.5zM12 17c-2.76 0-5-2.24-5-5s2.24-5 5-5 5 2.24 5 5-2.24 5-5 5zm0-8c-1.66 0-3 1.34-3 3s1.34 3 3 3 3-1.34 3-3-1.34-3-3-3z" />
</svg>
</div>
);
}
Visibility.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
open: false,
};

View file

@ -0,0 +1,26 @@
import React from 'react';
export default function VisibilityOff(props) {
return (
<div>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="#000000">
<path
d="M0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0zm0 0h24v24H0z"
fill="none"
/>
<path d="M12 7c2.76 0 5 2.24 5 5 0 .65-.13 1.26-.36 1.83l2.92 2.92c1.51-1.26 2.7-2.89 3.43-4.75-1.73-4.39-6-7.5-11-7.5-1.4 0-2.74.25-3.98.7l2.16 2.16C10.74 7.13 11.35 7 12 7zM2 4.27l2.28 2.28.46.46C3.08 8.3 1.78 10.02 1 12c1.73 4.39 6 7.5 11 7.5 1.55 0 3.03-.3 4.38-.84l.42.42L19.73 22 21 20.73 3.27 3 2 4.27zM7.53 9.8l1.55 1.55c-.05.21-.08.43-.08.65 0 1.66 1.34 3 3 3 .22 0 .44-.03.65-.08l1.55 1.55c-.67.33-1.41.53-2.2.53-2.76 0-5-2.24-5-5 0-.79.2-1.53.53-2.2zm4.31-.78l3.15 3.15.02-.16c0-1.66-1.34-3-3-3l-.17.01z" />
</svg>
</div>
);
}
VisibilityOff.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
open: false,
};

View file

@ -6,10 +6,12 @@ export default function PowerSettings(props) {
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
>
width={props.width}>
<path fill="none" d="M0 0h24v24H0z" />
<path fill="#ff6666" d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z" />
<path
fill="#ff6666"
d="M13 3h-2v10h2V3zm4.83 2.17l-1.42 1.42C17.99 7.86 19 9.81 19 12c0 3.87-3.13 7-7 7s-7-3.13-7-7c0-2.19 1.01-4.14 2.58-5.42L6.17 5.17C4.23 6.82 3 9.26 3 12c0 4.97 4.03 9 9 9s9-4.03 9-9c0-2.74-1.23-5.18-3.17-6.83z"
/>
</svg>
);
}

View file

@ -1,16 +1,31 @@
import React from 'react';
import Alert from 'react-bootstrap/Alert';
import { getVariantColor } from './LinkButton';
export default function AlertBanner({ bannerMessage }) {
interface Props {
bannerMessage?: any;
variant?: string;
children?: any;
}
export default function AlertBanner(props: Props) {
return (
<Alert
variant="danger"
variant={props.variant ?? 'danger'}
style={{
display: bannerMessage ? 'block' : 'none',
display:
props.bannerMessage || props.children ? 'block' : 'none',
textAlign: 'center',
}}
>
{bannerMessage}
border: 'none',
borderBottom: '1px solid',
background: 'none',
borderRadius: '0px',
color: getVariantColor(props.variant),
padding: 0,
margin: '0 25px',
marginBottom: '10px',
}}>
{props.bannerMessage ? props.bannerMessage : props.children}
</Alert>
);
}

View file

@ -19,15 +19,13 @@ function ChoiceModal({
<MessageDialog
size="lg"
{...props}
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}
>
attributes={{ title: constants.MULTI_FOLDER_UPLOAD }}>
<p>{constants.UPLOAD_STRATEGY_CHOICE}</p>
<div
style={{
display: 'flex',
flexWrap: 'wrap',
}}
>
}}>
<Button
variant="outline-success"
onClick={() => {
@ -38,8 +36,7 @@ function ChoiceModal({
padding: '12px 24px',
flex: 2,
whiteSpace: 'nowrap',
}}
>
}}>
{constants.UPLOAD_STRATEGY_SINGLE_COLLECTION}
</Button>
<div
@ -48,8 +45,7 @@ function ChoiceModal({
textAlign: 'center',
minWidth: '100px',
margin: '2% auto',
}}
>
}}>
<strong>{constants.OR}</strong>
</div>
<Button
@ -62,8 +58,7 @@ function ChoiceModal({
padding: '12px 24px',
flex: 2,
whiteSpace: 'nowrap',
}}
>
}}>
{constants.UPLOAD_STRATEGY_COLLECTION_PER_FOLDER}
</Button>
</div>

View file

@ -51,8 +51,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
size="sm"
attributes={{
title: attributes?.title,
}}
>
}}>
<Formik<formValues>
initialValues={{ albumName: attributes.autoFilledName }}
validationSchema={Yup.object().shape({
@ -60,11 +59,8 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={onSubmit}
>
{({
values, touched, errors, handleChange, handleSubmit,
}) => (
onSubmit={onSubmit}>
{({ values, touched, errors, handleChange, handleSubmit }) => (
<Form noValidate onSubmit={handleSubmit}>
<Form.Group>
<Form.Control
@ -73,7 +69,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
value={values.albumName}
onChange={handleChange('albumName')}
isInvalid={Boolean(
touched.albumName && errors.albumName,
touched.albumName && errors.albumName
)}
placeholder={constants.ENTER_ALBUM_NAME}
ref={collectionNameInputRef}
@ -82,8 +78,7 @@ export default function CollectionNamer({ attributes, ...props }: Props) {
<Form.Control.Feedback
type="invalid"
className="text-center"
>
className="text-center">
{errors.albumName}
</Form.Control.Feedback>
</Form.Group>

View file

@ -24,7 +24,7 @@ interface Props {
const CollectionOptions = (props: Props) => {
const collectionRename = async (
selectedCollection: Collection,
newName: string,
newName: string
) => {
if (selectedCollection.name !== newName) {
await renameCollection(selectedCollection, newName);
@ -37,16 +37,16 @@ const CollectionOptions = (props: Props) => {
buttonText: constants.RENAME,
autoFilledName: getSelectedCollection(
props.selectedCollectionID,
props.collections,
props.collections
)?.name,
callback: (newName) => {
props.startLoadingBar();
collectionRename(
getSelectedCollection(
props.selectedCollectionID,
props.collections,
props.collections
),
newName,
newName
);
},
});
@ -64,7 +64,7 @@ const CollectionOptions = (props: Props) => {
props.selectedCollectionID,
props.syncWithRemote,
props.redirectToAll,
props.setDialogMessage,
props.setDialogMessage
);
},
variant: 'danger',
@ -78,8 +78,7 @@ const CollectionOptions = (props: Props) => {
const MenuLink = (props) => (
<LinkButton
style={{ fontSize: '14px', fontWeight: 700, padding: '8px 1em' }}
{...props}
>
{...props}>
{props.children}
</LinkButton>
);
@ -89,8 +88,7 @@ const CollectionOptions = (props: Props) => {
style={{
background: '#282828',
padding: 0,
}}
>
}}>
{props.children}
</ListGroup.Item>
);
@ -111,8 +109,7 @@ const CollectionOptions = (props: Props) => {
<MenuItem>
<MenuLink
variant="danger"
onClick={confirmDeleteCollection}
>
onClick={confirmDeleteCollection}>
{constants.DELETE}
</MenuLink>
</MenuItem>

View file

@ -37,7 +37,6 @@ interface Props {
directlyShowNextModal: boolean;
collectionsAndTheirLatestFile: CollectionAndItsLatestFile[];
attributes: CollectionSelectorAttributes;
syncWithRemote:(force?: boolean, silent?:boolean)=>Promise<void>;
}
function CollectionSelector({
attributes,
@ -62,8 +61,7 @@ function CollectionSelector({
onClick={() => {
attributes.callback(item.collection);
props.onHide();
}}
>
}}>
<CollectionCard>
<PreviewCard
file={item.file}
@ -75,11 +73,15 @@ function CollectionSelector({
</Card.Text>
</CollectionCard>
</CollectionIcon>
),
)
);
return (
<Modal {...props} size="xl" centered>
<Modal
{...props}
size="xl"
centered
contentClassName="plan-selector-modal-content">
<Modal.Header closeButton onHide={() => props.onHide(true)}>
<Modal.Title>{attributes.title}</Modal.Title>
</Modal.Header>
@ -88,8 +90,7 @@ function CollectionSelector({
display: 'flex',
justifyContent: 'space-around',
flexWrap: 'wrap',
}}
>
}}>
<AddCollectionButton showNextModal={attributes.showNextModal} />
{CollectionIcons}
</Modal.Body>

View file

@ -3,9 +3,7 @@ import { SetDialogMessage } from 'components/MessageDialog';
import NavigationButton, {
SCROLL_DIRECTION,
} from 'components/NavigationButton';
import React, {
useEffect, useRef, useState,
} from 'react';
import React, { useEffect, useRef, useState } from 'react';
import { OverlayTrigger, Tooltip } from 'react-bootstrap';
import { Collection, CollectionType } from 'services/collectionService';
import { User } from 'services/userService';
@ -26,7 +24,7 @@ interface CollectionProps {
setCollectionNamerAttributes: SetCollectionNamerAttributes;
startLoadingBar: () => void;
searchMode: boolean;
collectionFilesCount: Map<number, number>
collectionFilesCount: Map<number, number>;
}
const Container = styled.div`
@ -58,7 +56,8 @@ const Chip = styled.button<{ active: boolean }>`
padding-left: 24px;
margin: 3px;
border: none;
background-color: ${(props) => (props.active ? '#fff' : 'rgba(255, 255, 255, 0.3)')};
background-color: ${(props) =>
props.active ? '#fff' : 'rgba(255, 255, 255, 0.3)'};
outline: none !important;
&:hover {
background-color: ${(props) => !props.active && '#bbbbbb'};
@ -71,9 +70,11 @@ const Chip = styled.button<{ active: boolean }>`
export default function Collections(props: CollectionProps) {
const { selected, collections, selectCollection } = props;
const [selectedCollectionID, setSelectedCollectionID] = useState<number>(null);
const [selectedCollectionID, setSelectedCollectionID] =
useState<number>(null);
const collectionRef = useRef<HTMLDivElement>(null);
const [collectionShareModalView, setCollectionShareModalView] = useState(false);
const [collectionShareModalView, setCollectionShareModalView] =
useState(false);
const [scrollObj, setScrollObj] = useState<{
scrollLeft?: number;
scrollWidth?: number;
@ -82,7 +83,8 @@ export default function Collections(props: CollectionProps) {
const updateScrollObj = () => {
if (collectionRef.current) {
const { scrollLeft, scrollWidth, clientWidth } = collectionRef.current;
const { scrollLeft, scrollWidth, clientWidth } =
collectionRef.current;
setScrollObj({ scrollLeft, scrollWidth, clientWidth });
}
};
@ -126,10 +128,13 @@ export default function Collections(props: CollectionProps) {
const renderTooltip = (collectionID) => {
const fileCount = props.collectionFilesCount?.get(collectionID);
return (
<Tooltip style={{
<Tooltip
style={{
padding: '0',
paddingBottom: '5px',
}} id="button-tooltip" {...props}>
}}
id="button-tooltip"
{...props}>
<div
{...props}
style={{
@ -139,8 +144,7 @@ export default function Collections(props: CollectionProps) {
color: '#ddd',
borderRadius: 3,
fontSize: '12px',
}}
>
}}>
{fileCount} {fileCount > 1 ? 'items' : 'item'}
</div>
</Tooltip>
@ -155,7 +159,7 @@ export default function Collections(props: CollectionProps) {
onHide={() => setCollectionShareModalView(false)}
collection={getSelectedCollection(
selectedCollectionID,
props.collections,
props.collections
)}
syncWithRemote={props.syncWithRemote}
/>
@ -181,35 +185,45 @@ export default function Collections(props: CollectionProps) {
key={item.id}
placement="top"
delay={{ show: 250, hide: 400 }}
overlay={renderTooltip(item.id)}
>
overlay={renderTooltip(item.id)}>
<Chip
active={selected === item.id}
onClick={clickHandler(item)}
>
onClick={clickHandler(item)}>
{item.name}
{item.type !== CollectionType.favorites &&
item.owner.id === user?.id ? (<OverlayTrigger
item.owner.id === user?.id ? (
<OverlayTrigger
rootClose
trigger="click"
placement="bottom"
overlay={collectionOptions}
>
overlay={collectionOptions}>
<OptionIcon
onClick={() => setSelectedCollectionID(item.id)}
onClick={() =>
setSelectedCollectionID(
item.id
)
}
/>
</OverlayTrigger>) : (<div style={{
</OverlayTrigger>
) : (
<div
style={{
display: 'inline-block',
width: '24px',
}}
/>)}
/>
)}
</Chip>
</OverlayTrigger>
))}
</Wrapper>
{scrollObj.scrollLeft <
scrollObj.scrollWidth - scrollObj.clientWidth && (<NavigationButton scrollDirection={SCROLL_DIRECTION.RIGHT} onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)} />)}
scrollObj.scrollWidth - scrollObj.clientWidth && (
<NavigationButton
scrollDirection={SCROLL_DIRECTION.RIGHT}
onClick={scrollCollection(SCROLL_DIRECTION.RIGHT)}
/>
)}
</Container>
</>
)

View file

@ -4,6 +4,7 @@ enum ButtonVariant {
success = 'success',
danger = 'danger',
secondary = 'secondary',
warning = 'warning',
}
type Props = React.PropsWithChildren<{
onClick: any;
@ -11,8 +12,7 @@ type Props = React.PropsWithChildren<{
style?: any;
}>;
export default function LinkButton(props: Props) {
function getButtonColor(variant: string) {
export function getVariantColor(variant: string) {
switch (variant) {
case ButtonVariant.success:
return '#2dc262';
@ -20,20 +20,22 @@ export default function LinkButton(props: Props) {
return '#c93f3f';
case ButtonVariant.secondary:
return '#858585';
case ButtonVariant.warning:
return '#D7BB63';
default:
return '#d1d1d1';
}
}
export default function LinkButton(props: Props) {
return (
<h5
style={{
color: getButtonColor(props.variant),
color: getVariantColor(props.variant),
cursor: 'pointer',
marginBottom: 0,
...props.style,
}}
onClick={props?.onClick ?? (() => null)}
>
onClick={props?.onClick ?? (() => null)}>
{props.children}
</h5>
);

View file

@ -16,17 +16,18 @@ const OptionIcon = ({ onClick }: Props) => (
onClick();
e.stopPropagation();
}}
style={{ marginBottom: '2px' }}
>
style={{ marginBottom: '2px' }}>
<svg
xmlns="http://www.w3.org/2000/svg"
height="20px"
width="24px"
viewBox="0 0 24 24"
fill="#000000"
>
fill="#000000">
<path d="M0 0h24v24H0V0z" fill="none" />
<path fill="#666" d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z" />
<path
fill="#666"
d="M12 8c1.1 0 2-.9 2-2s-.9-2-2-2-2 .9-2 2 .9 2 2 2zm0 2c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2zm0 6c-1.1 0-2 .9-2 2s.9 2 2 2 2-.9 2-2-.9-2-2-2z"
/>
</svg>
</OptionIconWrapper>
);

View file

@ -5,7 +5,6 @@ import styled from 'styled-components';
import billingService, { Plan, Subscription } from 'services/billingService';
import {
convertBytesToGBs,
getPlans,
getUserSubscription,
isUserSubscribedPlan,
isSubscriptionCancelled,
@ -16,6 +15,7 @@ import {
hasStripeSubscription,
hasPaidSubscription,
isOnFreePlan,
planForSubscription,
} from 'utils/billingUtil';
import { reverseString } from 'utils/common';
import { SetDialogMessage } from 'components/MessageDialog';
@ -25,7 +25,7 @@ import { DeadCenter, SetLoading } from 'pages/gallery';
export const PlanIcon = styled.div<{ selected: boolean }>`
border-radius: 20px;
width: 250px;
width: 220px;
border: 2px solid #333;
padding: 30px;
margin: 10px;
@ -55,7 +55,7 @@ export const PlanIcon = styled.div<{ selected: boolean }>`
}
&:hover {
transform: scale(1.2);
transform: scale(1.1);
background-color: #ffffff11;
}
@ -76,18 +76,33 @@ enum PLAN_PERIOD {
}
function PlanSelector(props: Props) {
const subscription: Subscription = getUserSubscription();
const plans = getPlans();
const [plans, setPlans] = useState<Plan[]>(null);
const [planPeriod, setPlanPeriod] = useState<PLAN_PERIOD>(PLAN_PERIOD.YEAR);
const togglePeriod = () => {
setPlanPeriod((prevPeriod) => (prevPeriod === PLAN_PERIOD.MONTH ?
PLAN_PERIOD.YEAR :
PLAN_PERIOD.MONTH));
setPlanPeriod((prevPeriod) =>
prevPeriod === PLAN_PERIOD.MONTH
? PLAN_PERIOD.YEAR
: PLAN_PERIOD.MONTH
);
};
useEffect(() => {
if (!plans && props.modalView) {
if (props.modalView) {
const main = async () => {
props.setLoading(true);
await billingService.updatePlans();
let plans = await billingService.getPlans();
const planNotListed =
plans.filter((plan) =>
isUserSubscribedPlan(plan, subscription)
).length === 0;
if (
subscription &&
!isOnFreePlan(subscription) &&
planNotListed
) {
plans = [planForSubscription(subscription), ...plans];
}
setPlans(plans);
props.setLoading(false);
};
main();
@ -108,7 +123,7 @@ function PlanSelector(props: Props) {
} else if (hasStripeSubscription(subscription)) {
props.setDialogMessage({
title: `${constants.CONFIRM} ${reverseString(
constants.UPDATE_SUBSCRIPTION,
constants.UPDATE_SUBSCRIPTION
)}`,
content: constants.UPDATE_SUBSCRIPTION_MESSAGE,
staticBackdrop: true,
@ -119,7 +134,7 @@ function PlanSelector(props: Props) {
plan,
props.setDialogMessage,
props.setLoading,
props.closeModal,
props.closeModal
),
variant: 'success',
},
@ -148,16 +163,15 @@ function PlanSelector(props: Props) {
key={plan.stripeID}
className="subscription-plan-selector"
selected={isUserSubscribedPlan(plan, subscription)}
>
onClick={async () => await onPlanSelect(plan)}>
<div>
<span
style={{
color: '#ECECEC',
fontWeight: 900,
fontSize: '72px',
lineHeight: '72px',
}}
>
fontSize: '40px',
lineHeight: '40px',
}}>
{convertBytesToGBs(plan.storage, 0)}
</span>
<span
@ -165,27 +179,32 @@ function PlanSelector(props: Props) {
color: '#858585',
fontSize: '24px',
fontWeight: 900,
}}
>
}}>
{' '}
GB
</span>
</div>
<div
className="bold-text"
style={{ color: '#aaa', lineHeight: '36px', fontSize: '20px' }}
>
style={{
color: '#aaa',
lineHeight: '36px',
fontSize: '20px',
}}>
{`${plan.price} / ${plan.period}`}
</div>
<Button
variant="outline-success"
block
style={{ marginTop: '30px' }}
disabled={isUserSubscribedPlan(plan, subscription)}
onClick={async () => (await onPlanSelect(plan))}
>
style={{
marginTop: '20px',
fontSize: '14px',
display: 'flex',
justifyContent: 'center',
}}
disabled={isUserSubscribedPlan(plan, subscription)}>
{constants.CHOOSE_PLAN_BTN}
<ArrowEast style={{ marginLeft: '10px' }} />
<ArrowEast style={{ marginLeft: '5px' }} />
</Button>
</PlanIcon>
));
@ -196,19 +215,18 @@ function PlanSelector(props: Props) {
size="xl"
centered
backdrop={hasPaidSubscription(subscription) ? 'true' : 'static'}
>
contentClassName="plan-selector-modal-content">
<Modal.Header closeButton>
<Modal.Title
style={{
marginLeft: '12px',
width: '100%',
textAlign: 'center',
}}
>
}}>
<span>
{hasPaidSubscription(subscription) ?
constants.MANAGE_PLAN :
constants.CHOOSE_PLAN}
{hasPaidSubscription(subscription)
? constants.MANAGE_PLAN
: constants.CHOOSE_PLAN}
</span>
</Modal.Title>
</Modal.Header>
@ -217,22 +235,23 @@ function PlanSelector(props: Props) {
<div style={{ display: 'flex' }}>
<span
className="bold-text"
style={{ fontSize: '20px' }}
>
style={{ fontSize: '16px' }}>
{constants.MONTHLY}
</span>
<Form.Switch
checked={planPeriod === PLAN_PERIOD.YEAR}
id="plan-period-toggler"
style={{ margin: '-4px 0 20px 15px' }}
style={{
margin: '-4px 0 20px 15px',
fontSize: '10px',
}}
className="custom-switch-md"
onChange={togglePeriod}
/>
<span
className="bold-text"
style={{ fontSize: '20px' }}
>
style={{ fontSize: '16px' }}>
{constants.YEARLY}
</span>
</div>
@ -243,9 +262,8 @@ function PlanSelector(props: Props) {
justifyContent: 'space-around',
flexWrap: 'wrap',
minHeight: '212px',
margin: '24px 0',
}}
>
margin: '5px 0',
}}>
{plans && PlanIcons}
</div>
<DeadCenter style={{ marginBottom: '30px' }}>
@ -254,55 +272,55 @@ function PlanSelector(props: Props) {
{isSubscriptionCancelled(subscription) ? (
<LinkButton
variant="success"
onClick={() => props.setDialogMessage({
title:
constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
content: constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
subscription.expiryTime,
onClick={() =>
props.setDialogMessage({
title: constants.CONFIRM_ACTIVATE_SUBSCRIPTION,
content:
constants.ACTIVATE_SUBSCRIPTION_MESSAGE(
subscription.expiryTime
),
staticBackdrop: true,
proceed: {
text:
constants.ACTIVATE_SUBSCRIPTION,
text: constants.ACTIVATE_SUBSCRIPTION,
action: activateSubscription.bind(
null,
props.setDialogMessage,
props.closeModal,
props.setLoading,
props.setLoading
),
variant: 'success',
},
close: {
text: constants.CANCEL,
},
})}
>
})
}>
{constants.ACTIVATE_SUBSCRIPTION}
</LinkButton>
) : (
<LinkButton
variant="danger"
onClick={() => props.setDialogMessage({
title:
constants.CONFIRM_CANCEL_SUBSCRIPTION,
content: constants.CANCEL_SUBSCRIPTION_MESSAGE(),
onClick={() =>
props.setDialogMessage({
title: constants.CONFIRM_CANCEL_SUBSCRIPTION,
content:
constants.CANCEL_SUBSCRIPTION_MESSAGE(),
staticBackdrop: true,
proceed: {
text:
constants.CANCEL_SUBSCRIPTION,
text: constants.CANCEL_SUBSCRIPTION,
action: cancelSubscription.bind(
null,
props.setDialogMessage,
props.closeModal,
props.setLoading,
props.setLoading
),
variant: 'danger',
},
close: {
text: constants.CANCEL,
},
})}
>
})
}>
{constants.CANCEL_SUBSCRIPTION}
</LinkButton>
)}
@ -311,10 +329,9 @@ function PlanSelector(props: Props) {
onClick={updatePaymentMethod.bind(
null,
props.setDialogMessage,
props.setLoading,
props.setLoading
)}
style={{ marginTop: '20px' }}
>
style={{ marginTop: '20px' }}>
{constants.MANAGEMENT_PORTAL}
</LinkButton>
</>
@ -322,11 +339,13 @@ function PlanSelector(props: Props) {
<LinkButton
variant="primary"
onClick={props.closeModal}
style={{ color: 'rgb(121, 121, 121)', marginTop: '20px' }}
>
{isOnFreePlan(subscription) ?
constants.SKIP :
constants.CLOSE}
style={{
color: 'rgb(121, 121, 121)',
marginTop: '20px',
}}>
{isOnFreePlan(subscription)
? constants.SKIP
: constants.CLOSE}
</LinkButton>
)}
</DeadCenter>

View file

@ -183,8 +183,7 @@ export default function PreviewCard(props: IProps) {
onClick={handleClick}
disabled={!forcedEnable && !file?.msrc && !imgSrc}
selected={selected}
{...(selectable ? useLongPress(longPressCallback, 500) : {})}
>
{...(selectable ? useLongPress(longPressCallback, 500) : {})}>
{selectable && (
<Check
type="checkbox"

View file

@ -7,7 +7,7 @@ import DeleteIcon from 'components/icons/DeleteIcon';
import CrossIcon from 'components/icons/CrossIcon';
import AddIcon from 'components/icons/AddIcon';
import { IconButton } from 'components/Container';
import constants from 'utils/strings/englishConstants';
import constants from 'utils/strings/constants';
interface Props {
addToCollectionHelper: (collectionName, collection) => void;
@ -42,13 +42,15 @@ const SelectedFileOptions = ({
count,
clearSelection,
}: Props) => {
const addToCollection = () => setCollectionSelectorAttributes({
const addToCollection = () =>
setCollectionSelectorAttributes({
callback: (collection) => addToCollectionHelper(null, collection),
showNextModal: showCreateCollectionModal,
title: constants.ADD_TO_COLLECTION,
});
const deleteHandler = () => setDialogMessage({
const deleteHandler = () =>
setDialogMessage({
title: constants.CONFIRM_DELETE_FILE,
content: constants.DELETE_FILE_MESSAGE,
staticBackdrop: true,
@ -63,11 +65,19 @@ const SelectedFileOptions = ({
return (
<SelectionBar>
<SelectionContainer>
<IconButton onClick={clearSelection}><CrossIcon /></IconButton>
<div>{count} {constants.SELECTED}</div>
<IconButton onClick={clearSelection}>
<CrossIcon />
</IconButton>
<div>
{count} {constants.SELECTED}
</div>
</SelectionContainer>
<IconButton onClick={addToCollection}><AddIcon /></IconButton>
<IconButton onClick={deleteHandler}><DeleteIcon /></IconButton>
<IconButton onClick={addToCollection}>
<AddIcon />
</IconButton>
<IconButton onClick={deleteHandler}>
<DeleteIcon />
</IconButton>
</SelectionBar>
);
};

View file

@ -1,5 +1,8 @@
import React, { useContext, useEffect, useState } from 'react';
import UploadService, { FileWithCollection, UPLOAD_STAGES } from 'services/uploadService';
import UploadService, {
FileWithCollection,
UPLOAD_STAGES,
} from 'services/uploadService';
import { createAlbum } from 'services/collectionService';
import { getLocalFiles } from 'services/fileService';
import constants from 'utils/strings/constants';
@ -13,6 +16,7 @@ import { SetFiles, SetLoading } from 'pages/gallery';
import { AppContext } from 'pages/_app';
import { logError } from 'utils/sentry';
import { FileRejection } from 'react-dropzone';
import { METADATA_FOLDER_NAME } from 'services/exportService';
interface Props {
syncWithRemote: (force?: boolean, silent?: boolean) => Promise<void>;
@ -38,20 +42,26 @@ interface AnalysisResult {
suggestedCollectionName: string;
multipleFolders: boolean;
}
export default function Upload(props: Props) {
const [progressView, setProgressView] = useState(false);
const [uploadStage, setUploadStage] = useState<UPLOAD_STAGES>(
UPLOAD_STAGES.START,
UPLOAD_STAGES.START
);
const [fileCounter, setFileCounter] = useState({ current: 0, total: 0 });
const [fileProgress, setFileProgress] = useState(new Map<string, number>());
const [uploadResult, setUploadResult] = useState(new Map<string, number>());
const [percentComplete, setPercentComplete] = useState(0);
const [choiceModalView, setChoiceModalView] = useState(false);
const [fileAnalysisResult, setFileAnalysisResult] = useState<AnalysisResult>(null);
const [fileAnalysisResult, setFileAnalysisResult] =
useState<AnalysisResult>(null);
const appContext = useContext(AppContext);
useEffect(() => {
if (props.acceptedFiles?.length > 0 || appContext.sharedFiles?.length > 0) {
if (
props.acceptedFiles?.length > 0 ||
appContext.sharedFiles?.length > 0
) {
props.setLoading(true);
let fileAnalysisResult;
@ -77,6 +87,7 @@ export default function Upload(props: Props) {
setUploadStage(UPLOAD_STAGES.START);
setFileCounter({ current: 0, total: 0 });
setFileProgress(new Map<string, number>());
setUploadResult(new Map<string, number>());
setPercentComplete(0);
setProgressView(true);
};
@ -89,16 +100,16 @@ export default function Upload(props: Props) {
props.closeCollectionSelector();
await uploadFilesToNewCollections(
UPLOAD_STRATEGY.SINGLE_COLLECTION,
collectionName,
collectionName
);
},
});
};
const nextModal = (fileAnalysisResult: AnalysisResult) => {
fileAnalysisResult?.multipleFolders ?
setChoiceModalView(true) :
showCreateCollectionModal(fileAnalysisResult);
fileAnalysisResult?.multipleFolders
? setChoiceModalView(true)
: showCreateCollectionModal(fileAnalysisResult);
};
function analyseUploadFiles(): AnalysisResult {
@ -118,7 +129,7 @@ export default function Upload(props: Props) {
if (commonPathPrefix) {
commonPathPrefix = commonPathPrefix.substr(
1,
commonPathPrefix.lastIndexOf('/') - 1,
commonPathPrefix.lastIndexOf('/') - 1
);
}
return {
@ -129,10 +140,14 @@ export default function Upload(props: Props) {
function getCollectionWiseFiles() {
const collectionWiseFiles = new Map<string, globalThis.File[]>();
for (const file of props.acceptedFiles) {
const filePath = file['path'];
const folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
const filePath = file['path'] as string;
let folderPath = filePath.substr(0, filePath.lastIndexOf('/'));
if (folderPath.endsWith(METADATA_FOLDER_NAME)) {
folderPath = folderPath.substr(0, folderPath.lastIndexOf('/'));
}
const folderName = folderPath.substr(
folderPath.lastIndexOf('/') + 1,
folderPath.lastIndexOf('/') + 1
);
if (!collectionWiseFiles.has(folderName)) {
collectionWiseFiles.set(folderName, []);
@ -145,7 +160,8 @@ export default function Upload(props: Props) {
const uploadFilesToExistingCollection = async (collection) => {
try {
uploadInit();
const filesWithCollectionToUpload: FileWithCollection[] = props.acceptedFiles.map((file) => ({
const filesWithCollectionToUpload: FileWithCollection[] =
props.acceptedFiles.map((file) => ({
file,
collection,
}));
@ -157,7 +173,7 @@ export default function Upload(props: Props) {
const uploadFilesToNewCollections = async (
strategy: UPLOAD_STRATEGY,
collectionName,
collectionName
) => {
try {
uploadInit();
@ -194,7 +210,7 @@ export default function Upload(props: Props) {
};
const uploadFiles = async (
filesWithCollectionToUpload: FileWithCollection[],
filesWithCollectionToUpload: FileWithCollection[]
) => {
try {
props.setUploadInProgress(true);
@ -209,8 +225,9 @@ export default function Upload(props: Props) {
setFileCounter,
setUploadStage,
setFileProgress,
setUploadResult,
},
props.setFiles,
props.setFiles
);
} catch (err) {
props.setBannerMessage(err.message);
@ -222,8 +239,7 @@ export default function Upload(props: Props) {
props.syncWithRemote();
}
};
const retryFailed = async (
) => {
const retryFailed = async () => {
try {
props.setUploadInProgress(true);
uploadInit();
@ -240,14 +256,15 @@ export default function Upload(props: Props) {
}
};
return (
<>
<ChoiceModal
show={choiceModalView}
onHide={() => setChoiceModalView(false)}
uploadFiles={uploadFilesToNewCollections}
showCollectionCreateModal={() => showCreateCollectionModal(fileAnalysisResult)}
showCollectionCreateModal={() =>
showCreateCollectionModal(fileAnalysisResult)
}
/>
<UploadProgress
now={percentComplete}
@ -258,6 +275,7 @@ export default function Upload(props: Props) {
closeModal={() => setProgressView(false)}
retryFailed={retryFailed}
fileRejections={props.fileRejections}
uploadResult={uploadResult}
/>
</>
);

View file

@ -21,8 +21,7 @@ function UploadButton({ openFileUploader, isFirstFetch }) {
viewBox="0 0 24 24"
fill="green"
width="32px"
height="32px"
>
height="32px">
<path fill="none" d="M0 0h24v24H0z" />
<path
fill="#2dc262"

View file

@ -1,10 +1,13 @@
import React from 'react';
import {
Alert, Button, Modal, ProgressBar,
} from 'react-bootstrap';
import ExpandLess from 'components/icons/ExpandLess';
import ExpandMore from 'components/icons/ExpandMore';
import React, { useState } from 'react';
import { Button, Modal, ProgressBar } from 'react-bootstrap';
import { FileRejection } from 'react-dropzone';
import { UPLOAD_STAGES, FileUploadErrorCode } from 'services/uploadService';
import { FileUploadResults, UPLOAD_STAGES } from 'services/uploadService';
import styled from 'styled-components';
import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common';
import constants from 'utils/strings/constants';
import AlertBanner from './AlertBanner';
interface Props {
fileCounter;
@ -14,106 +17,222 @@ interface Props {
retryFailed;
fileProgress: Map<string, number>;
show;
fileRejections:FileRejection[]
fileRejections: FileRejection[];
uploadResult: Map<string, number>;
}
interface FileProgressStatuses{
interface FileProgresses {
fileName: string;
progress: number;
}
const Content = styled.div<{
collapsed: boolean;
sm?: boolean;
height?: number;
}>`
overflow: hidden;
height: ${(props) => (props.collapsed ? '0px' : props.height + 'px')};
transition: ${(props) => 'height ' + 0.001 * props.height + 's ease-out'};
margin-bottom: 20px;
& > p {
padding-left: 35px;
margin: 0;
}
`;
const FileList = styled.ul`
padding-left: 50px;
margin-top: 5px;
& > li {
padding-left: 10px;
margin-bottom: 10px;
color: #ccc;
}
`;
const SectionTitle = styled.div`
display: flex;
justify-content: space-between;
padding: 0 20px;
color: #eee;
font-size: 20px;
cursor: pointer;
`;
interface ResultSectionProps {
fileUploadResultMap: Map<FileUploadResults, string[]>;
fileUploadResult: FileUploadResults;
sectionTitle;
sectionInfo;
infoHeight: number;
}
const ResultSection = (props: ResultSectionProps) => {
const [listView, setListView] = useState(false);
const fileList = props.fileUploadResultMap?.get(props.fileUploadResult);
if (!fileList?.length) {
return <></>;
}
return (
<>
<SectionTitle onClick={() => setListView(!listView)}>
{' '}
{props.sectionTitle}{' '}
{listView ? <ExpandLess /> : <ExpandMore />}
</SectionTitle>
<Content
collapsed={!listView}
height={fileList.length * 33 + props.infoHeight}>
<p>{props.sectionInfo}</p>
<FileList>
{fileList.map((fileName) => (
<li key={fileName}>{fileName}</li>
))}
</FileList>
</Content>
</>
);
};
export default function UploadProgress(props: Props) {
const fileProgressStatuses = [] as FileProgressStatuses[];
const fileProgressStatuses = [] as FileProgresses[];
const fileUploadResultMap = new Map<FileUploadResults, string[]>();
let filesNotUploaded = false;
if (props.fileProgress) {
for (const [fileName, progress] of props.fileProgress) {
fileProgressStatuses.push({ fileName, progress });
}
for (const { file } of props.fileRejections) {
fileProgressStatuses.push({ fileName: file.name, progress: FileUploadErrorCode.UNSUPPORTED });
}
fileProgressStatuses.sort((a, b) => {
if (b.progress !== -1 && a.progress === -1) return 1;
});
if (props.uploadResult) {
for (const [fileName, progress] of props.uploadResult) {
if (!fileUploadResultMap.has(progress)) {
fileUploadResultMap.set(progress, []);
}
if (progress < 0) {
filesNotUploaded = true;
}
const fileList = fileUploadResultMap.get(progress);
fileUploadResultMap.set(progress, [...fileList, fileName]);
}
}
return (
<Modal
show={props.show}
onHide={
props.uploadStage !== UPLOAD_STAGES.FINISH ?
() => null :
props.closeModal
props.uploadStage !== UPLOAD_STAGES.FINISH
? () => null
: props.closeModal
}
aria-labelledby="contained-modal-title-vcenter"
centered
backdrop={
fileProgressStatuses?.length !== 0 ? 'static' : 'true'
}
>
backdrop={fileProgressStatuses?.length !== 0 ? 'static' : 'true'}>
<Modal.Header
style={{ display: 'flex', justifyContent: 'center', textAlign: 'center', borderBottom: 'none', paddingTop: '30px', paddingBottom: '0px' }}
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}
>
style={{
display: 'flex',
justifyContent: 'center',
textAlign: 'center',
borderBottom: 'none',
paddingTop: '30px',
paddingBottom: '0px',
}}
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}>
<h4 style={{ width: '100%' }}>
{props.uploadStage === UPLOAD_STAGES.UPLOADING ?
constants.UPLOAD[props.uploadStage](
props.fileCounter,
) :
constants.UPLOAD[props.uploadStage]}
{props.uploadStage === UPLOAD_STAGES.UPLOADING
? constants.UPLOAD[props.uploadStage](props.fileCounter)
: constants.UPLOAD[props.uploadStage]}
</h4>
</Modal.Header>
<Modal.Body>
{props.uploadStage===UPLOAD_STAGES.FINISH ? (
fileProgressStatuses.length !== 0 && (
<Alert variant="warning">
{constants.FAILED_UPLOAD_FILE_LIST}
</Alert>
)
) :
(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
props.uploadStage === UPLOAD_STAGES.UPLOADING) &&
(
{(props.uploadStage ===
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
props.uploadStage === UPLOAD_STAGES.UPLOADING) && (
<ProgressBar
now={props.now}
animated
variant="upload-progress-bar"
/>
)}
{fileProgressStatuses?.length > 0 && (
<ul
style={{
marginTop: '10px',
overflow: 'auto',
maxHeight: '250px',
}}
>
{fileProgressStatuses.length > 0 && (
<FileList>
{fileProgressStatuses.map(({ fileName, progress }) => (
<li key={fileName} style={{ marginTop: '12px' }}>
{props.uploadStage===UPLOAD_STAGES.FINISH ?
fileName :
constants.FILE_UPLOAD_PROGRESS(
{props.uploadStage === UPLOAD_STAGES.FINISH
? fileName
: constants.FILE_UPLOAD_PROGRESS(
fileName,
progress,
progress
)}
</li>
))}
</ul>
</FileList>
)}
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.UPLOADED}
sectionTitle={constants.SUCCESSFUL_UPLOADS}
sectionInfo={constants.SUCCESS_INFO}
infoHeight={32}
/>
{props.uploadStage === UPLOAD_STAGES.FINISH &&
filesNotUploaded && (
<AlertBanner variant="warning">
{constants.FILE_NOT_UPLOADED_LIST}
</AlertBanner>
)}
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.BLOCKED}
sectionTitle={constants.BLOCKED_UPLOADS}
sectionInfo={constants.ETAGS_BLOCKED(
DESKTOP_APP_DOWNLOAD_URL
)}
infoHeight={140}
/>
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.FAILED}
sectionTitle={constants.FAILED_UPLOADS}
sectionInfo={constants.FAILED_INFO}
infoHeight={48}
/>
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.SKIPPED}
sectionTitle={constants.SKIPPED_FILES}
sectionInfo={constants.SKIPPED_INFO}
infoHeight={32}
/>
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.UNSUPPORTED}
sectionTitle={constants.UNSUPPORTED_FILES}
sectionInfo={constants.UNSUPPORTED_INFO}
infoHeight={32}
/>
{props.uploadStage === UPLOAD_STAGES.FINISH && (
<Modal.Footer style={{ border: 'none' }}>
{props.uploadStage===UPLOAD_STAGES.FINISH && (fileProgressStatuses?.length === 0 ? (
<Button
variant="outline-secondary"
style={{ width: '100%' }}
onClick={props.closeModal}
>
{constants.CLOSE}
</Button>) : (
{props.uploadStage === UPLOAD_STAGES.FINISH &&
(fileUploadResultMap?.get(FileUploadResults.FAILED)
?.length > 0 ||
fileUploadResultMap?.get(FileUploadResults.BLOCKED)
?.length > 0 ? (
<Button
variant="outline-success"
style={{ width: '100%' }}
onClick={props.retryFailed}
>
{constants.RETRY}
</Button>))}
onClick={props.retryFailed}>
{constants.RETRY_FAILED}
</Button>
) : (
<Button
variant="outline-secondary"
style={{ width: '100%' }}
onClick={props.closeModal}>
{constants.CLOSE}
</Button>
))}
</Modal.Footer>
)}
</Modal.Body>

View file

@ -128,14 +128,21 @@ const GlobalStyles = createGlobalStyle`
.modal-content {
border-radius:15px;
background-color:#202020 !important;
color:#aaa;
}
.modal-dialog{
margin:5% auto;
width:90%;
}
.modal-body{
max-height:80vh;
overflow:auto;
}
.modal-xl{
max-width:960px!important;
max-width:90% !important;
}
.plan-selector-modal-content {
width:auto;
margin:auto;
}
.pswp-custom {
opacity: 0.75;
@ -390,29 +397,35 @@ export interface BannerMessage {
variant: string;
}
type AppContextType = {
showNavBar: (show: boolean) => void;
sharedFiles: File[];
resetSharedFiles: () => void;
setDisappearingFlashMessage: (message: FlashMessage) => void;
}
};
export enum FLASH_MESSAGE_TYPE {
DANGER = 'danger',
INFO = 'info',
SUCCESS = 'success',
WARNING = 'warning',
}
export interface FlashMessage {
message: string;
severity: string
type: FLASH_MESSAGE_TYPE;
}
export const AppContext = createContext<AppContextType>(null);
const redirectMap = {
roadmap: (token: string) => `${getEndpoint()}/users/roadmap?token=${encodeURIComponent(token)}`,
roadmap: (token: string) =>
`${getEndpoint()}/users/roadmap?token=${encodeURIComponent(token)}`,
};
export default function App({ Component, err }) {
const router = useRouter();
const [loading, setLoading] = useState(false);
const [offline, setOffline] = useState(
typeof window !== 'undefined' && !window.navigator.onLine,
typeof window !== 'undefined' && !window.navigator.onLine
);
const [showNavbar, setShowNavBar] = useState(false);
const [sharedFiles, setSharedFiles] = useState<File[]>(null);
@ -444,7 +457,7 @@ export default function App({ Component, err }) {
(error) => {
logError(error);
return Promise.reject(error);
},
}
);
}, []);
@ -455,7 +468,7 @@ export default function App({ Component, err }) {
useEffect(() => {
console.log(
`%c${constants.CONSOLE_WARNING_STOP}`,
'color: red; font-size: 52px;',
'color: red; font-size: 52px;'
);
console.log(`%c${constants.CONSOLE_WARNING_DESC}`, 'font-size: 20px;');
@ -479,7 +492,9 @@ export default function App({ Component, err }) {
if (redirectName) {
const user = getData(LS_KEYS.USER);
if (user?.token) {
window.location.href = redirectMap[redirectName](user.token);
window.location.href = redirectMap[redirectName](
user.token
);
}
}
});
@ -506,24 +521,27 @@ export default function App({ Component, err }) {
<Head>
<title>{constants.TITLE}</title>
{/* Cloudflare Web Analytics */}
{pageRootURL?.hostname && (pageRootURL.hostname === 'photos.ente.io' ?
{pageRootURL?.hostname &&
(pageRootURL.hostname === 'photos.ente.io' ? (
<script
defer
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "6a388287b59c439cb2070f78cc89dde1"}'
/> : pageRootURL.hostname === 'web.ente.io' ?
/>
) : pageRootURL.hostname === 'web.ente.io' ? (
<script
defer
src='https://static.cloudflareinsights.com/beacon.min.js'
data-cf-beacon='{"token": "dfde128b7bb34a618ad34a08f1ba7609"}' /> :
src="https://static.cloudflareinsights.com/beacon.min.js"
data-cf-beacon='{"token": "dfde128b7bb34a618ad34a08f1ba7609"}'
/>
) : (
console.warn('Web analytics is disabled')
)
}
))}
{/* End Cloudflare Web Analytics */}
</Head>
<GlobalStyles />
{
showNavbar && <Navbar>
{showNavbar && (
<Navbar>
<FlexContainer>
<LogoImage
style={{ height: '24px', padding: '3px' }}
@ -532,16 +550,28 @@ export default function App({ Component, err }) {
/>
</FlexContainer>
</Navbar>
}
<MessageContainer>{offline && constants.OFFLINE_MSG}</MessageContainer>
{
sharedFiles &&
(router.pathname === '/gallery' ?
<MessageContainer>{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}</MessageContainer> :
<MessageContainer>{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}</MessageContainer>)
}
{flashMessage && <FlashMessageBar flashMessage={flashMessage} onClose={() => setFlashMessage(null)} />}
<AppContext.Provider value={{
)}
<MessageContainer>
{offline && constants.OFFLINE_MSG}
</MessageContainer>
{sharedFiles &&
(router.pathname === '/gallery' ? (
<MessageContainer>
{constants.FILES_TO_BE_UPLOADED(sharedFiles.length)}
</MessageContainer>
) : (
<MessageContainer>
{constants.LOGIN_TO_UPLOAD_FILES(sharedFiles.length)}
</MessageContainer>
))}
{flashMessage && (
<FlashMessageBar
flashMessage={flashMessage}
onClose={() => setFlashMessage(null)}
/>
)}
<AppContext.Provider
value={{
showNavBar,
sharedFiles,
resetSharedFiles,

View file

@ -1,7 +1,5 @@
import React from 'react';
import Document, {
Html, Head, Main, NextScript,
} from 'next/document';
import Document, { Html, Head, Main, NextScript } from 'next/document';
import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document {
@ -10,8 +8,10 @@ export default class MyDocument extends Document {
const originalRenderPage = ctx.renderPage;
try {
ctx.renderPage = () => originalRenderPage({
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />),
ctx.renderPage = () =>
originalRenderPage({
enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx);
@ -37,13 +37,24 @@ export default class MyDocument extends Document {
name="description"
content="ente is a privacy focussed photo storage service that offers end-to-end encryption."
/>
<link rel="icon" href="/images/favicon.png" type="image/png" />
<link
rel="icon"
href="/images/favicon.png"
type="image/png"
/>
<link rel="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="/images/ente-512.png" />
<meta name="theme-color" content="#111" />
<link rel="icon" type="image/png" href="/images/favicon.png" />
<link
rel="icon"
type="image/png"
href="/images/favicon.png"
/>
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black" />
<meta
name="apple-mobile-web-app-status-bar-style"
content="black"
/>
</Head>
<body>
<Main />

View file

@ -6,7 +6,8 @@ export const config = {
},
};
const API_ENDPOINT = process.env.NEXT_PUBLIC_ENTE_ENDPOINT || 'https://api.staging.ente.io';
const API_ENDPOINT =
process.env.NEXT_PUBLIC_ENTE_ENDPOINT || 'https://api.staging.ente.io';
export default createProxyMiddleware({
target: API_ENDPOINT,

View file

@ -0,0 +1,63 @@
import Container from 'components/Container';
import LogoImg from 'components/LogoImg';
import React, { useEffect, useState } from 'react';
import { Alert, Card } from 'react-bootstrap';
import constants from 'utils/strings/constants';
import router from 'next/router';
import { getToken } from 'utils/common/key';
import EnteSpinner from 'components/EnteSpinner';
import ChangeEmailForm from 'components/ChangeEmail';
import EnteCard from 'components/EnteCard';
function ChangeEmailPage() {
const [email, setEmail] = useState('');
const [waiting, setWaiting] = useState(true);
const [showMessage, setShowMessage] = useState(false);
const [showBigDialog, setShowBigDialog] = useState(false);
useEffect(() => {
const token = getToken();
if (!token) {
router.push('/');
return;
}
setWaiting(false);
}, []);
return (
<Container>
{waiting ? (
<EnteSpinner>
<span className="sr-only">Loading...</span>
</EnteSpinner>
) : (
<EnteCard size={showBigDialog ? 'md' : 'sm'}>
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src="/icon.svg" />
{constants.UPDATE_EMAIL}
</Card.Title>
<Alert
variant="success"
show={showMessage}
style={{ paddingBottom: 0 }}
transition
dismissible
onClose={() => setShowMessage(false)}>
{constants.EMAIL_SENT({ email })}
</Alert>
<ChangeEmailForm
showMessage={(value) => {
setShowMessage(value);
setShowBigDialog(value);
}}
setEmail={setEmail}
/>
</Card.Body>
</EnteCard>
)}
</Container>
);
}
export default ChangeEmailPage;

View file

@ -2,10 +2,10 @@ import React, { useState, useEffect, useContext } from 'react';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import { B64EncryptionResult } from 'services/uploadService';
import CryptoWorker, {
setSessionKeys,
generateAndSaveIntermediateKeyAttributes,
B64EncryptionResult,
} from 'utils/crypto';
import { getActualKey } from 'utils/common/key';
import { setKeys, UpdatedKey } from 'services/userService';
@ -45,7 +45,8 @@ export default function Generate() {
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
return;
}
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(key, kek.key);
const encryptedKeyAttributes: B64EncryptionResult =
await cryptoWorker.encryptToB64(key, kek.key);
const updatedKey: UpdatedKey = {
kekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData,
@ -60,7 +61,7 @@ export default function Generate() {
await generateAndSaveIntermediateKeyAttributes(
passphrase,
updatedKeyAttributes,
key,
key
);
setSessionKeys(key);
@ -75,9 +76,9 @@ export default function Generate() {
callback={onSubmit}
buttonText={constants.CHANGE_PASSWORD}
back={
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ?
redirectToGallery :
null
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value
? redirectToGallery
: null
}
/>
);

View file

@ -29,7 +29,10 @@ export default function Credentials() {
const user = getData(LS_KEYS.USER);
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if ((!user?.token && !user?.encryptedToken) || !keyAttributes?.memLimit) {
if (
(!user?.token && !user?.encryptedToken) ||
!keyAttributes?.memLimit
) {
clearData();
router.push('/');
} else if (!keyAttributes) {
@ -51,7 +54,7 @@ export default function Credentials() {
passphrase,
keyAttributes.kekSalt,
keyAttributes.opsLimit,
keyAttributes.memLimit,
keyAttributes.memLimit
);
} catch (e) {
console.error('failed to deriveKey ', e.message);
@ -61,13 +64,13 @@ export default function Credentials() {
const key: string = await cryptoWorker.decryptB64(
keyAttributes.encryptedKey,
keyAttributes.keyDecryptionNonce,
kek,
kek
);
if (isFirstLogin()) {
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
key,
key
);
}
await setSessionKeys(key);
@ -81,7 +84,7 @@ export default function Credentials() {
} catch (e) {
setFieldError(
'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`,
`${constants.UNKNOWN_ERROR} ${e.message}`
);
console.error('failed to verifyPassphrase ', e.message);
}
@ -90,13 +93,10 @@ export default function Credentials() {
return (
<>
<Container>
<Card
style={{ minWidth: '320px' }}
className="text-center"
>
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.PASSWORD}
</Card.Title>
<SingleInputForm
@ -110,12 +110,10 @@ export default function Credentials() {
display: 'flex',
flexDirection: 'column',
marginTop: '12px',
}}
>
}}>
<Button
variant="link"
onClick={() => router.push('/recover')}
>
onClick={() => router.push('/recover')}>
{constants.FORGOT_PASSWORD}
</Button>
<Button variant="link" onClick={logoutUser}>

View file

@ -1,4 +1,10 @@
import React, { createContext, useContext, useEffect, useRef, useState } from 'react';
import React, {
createContext,
useContext,
useEffect,
useRef,
useState,
} from 'react';
import { useRouter } from 'next/router';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import {
@ -39,7 +45,6 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame';
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
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';
@ -55,12 +60,7 @@ import PlanSelector from 'components/pages/gallery/PlanSelector';
import Upload from 'components/pages/gallery/Upload';
import Collections from 'components/pages/gallery/Collections';
import { AppContext } from 'pages/_app';
export enum FILE_TYPE {
IMAGE,
VIDEO,
OTHERS,
}
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
export const DeadCenter = styled.div`
flex: 1;
@ -98,19 +98,22 @@ export interface SearchStats {
type GalleryContextType = {
thumbs: Map<number, string>;
files: Map<number, string>;
}
};
const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(),
files: new Map(),
};
export const GalleryContext = createContext<GalleryContextType>(defaultGalleryContext);
export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext
);
export default function Gallery() {
const router = useRouter();
const [collections, setCollections] = useState<Collection[]>([]);
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState<CollectionAndItsLatestFile[]>([]);
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
useState<CollectionAndItsLatestFile[]>([]);
const [files, setFiles] = useState<File[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [bannerMessage, setBannerMessage] = useState<string>(null);
@ -121,9 +124,11 @@ export default function Gallery() {
const [dialogView, setDialogView] = useState(false);
const [planModalView, setPlanModalView] = useState(false);
const [loading, setLoading] = useState(false);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] = useState<CollectionSelectorAttributes>(null);
const [collectionSelectorAttributes, setCollectionSelectorAttributes] =
useState<CollectionSelectorAttributes>(null);
const [collectionSelectorView, setCollectionSelectorView] = useState(false);
const [collectionNamerAttributes, setCollectionNamerAttributes] = useState<CollectionNamerAttributes>(null);
const [collectionNamerAttributes, setCollectionNamerAttributes] =
useState<CollectionNamerAttributes>(null);
const [collectionNamerView, setCollectionNamerView] = useState(false);
const [search, setSearch] = useState<Search>({
date: null,
@ -150,7 +155,8 @@ export default function Gallery() {
const resync = useRef(false);
const [deleted, setDeleted] = useState<number[]>([]);
const appContext = useContext(AppContext);
const [collectionFilesCount, setCollectionFilesCount] = useState<Map<number, number>>();
const [collectionFilesCount, setCollectionFilesCount] =
useState<Map<number, number>>();
useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
@ -181,14 +187,11 @@ export default function Gallery() {
}, []);
useEffect(() => setDialogView(true), [dialogMessage]);
useEffect(
() => {
useEffect(() => {
if (collectionSelectorAttributes) {
setCollectionSelectorView(true);
}
},
[collectionSelectorAttributes],
);
}, [collectionSelectorAttributes]);
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
const syncWithRemote = async (force = false, silent = false) => {
@ -200,31 +203,30 @@ export default function Gallery() {
try {
checkConnectivity();
if (!(await isTokenValid())) {
throw new Error(errorCodes.ERR_SESSION_EXPIRED);
throw new Error(ServerErrorCodes.SESSION_EXPIRED);
}
!silent && 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:
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',
},
nonClosable: true,
});
break;
case errorCodes.ERR_KEY_MISSING:
case CustomError.KEY_MISSING:
clearKeys();
router.push('/credentials');
break;
@ -240,14 +242,9 @@ export default function Gallery() {
};
const initDerivativeState = async (collections, files) => {
const nonEmptyCollections = getNonEmptyCollections(
collections,
files,
);
const collectionsAndTheirLatestFile = await getCollectionsAndTheirLatestFile(
nonEmptyCollections,
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) {
@ -274,7 +271,7 @@ export default function Gallery() {
}
const addToCollectionHelper = (
collectionName: string,
collection: Collection,
collection: Collection
) => {
loadingBar.current?.continuousStart();
addFilesToCollection(
@ -285,31 +282,29 @@ export default function Gallery() {
syncWithRemote,
selectCollection,
collectionName,
collection,
collection
);
};
const showCreateCollectionModal = () => setCollectionNamerAttributes({
const showCreateCollectionModal = () =>
setCollectionNamerAttributes({
title: constants.CREATE_COLLECTION,
buttonText: constants.CREATE,
autoFilledName: '',
callback: (collectionName) => addToCollectionHelper(collectionName, null),
callback: (collectionName) =>
addToCollectionHelper(collectionName, null),
});
const deleteFileHelper = async () => {
loadingBar.current?.continuousStart();
try {
const fileIds = getSelectedFileIds(selected);
await deleteFiles(
fileIds,
clearSelection,
syncWithRemote,
);
await deleteFiles(fileIds, clearSelection, syncWithRemote);
setDeleted([...deleted, ...fileIds]);
} catch (e) {
loadingBar.current.complete();
switch (e.status?.toString()) {
case errorCodes.ERR_FORBIDDEN:
case ServerErrorCodes.FORBIDDEN:
setDialogMessage({
title: constants.ERROR,
staticBackdrop: true,
@ -333,7 +328,6 @@ export default function Gallery() {
setSearchStats(null);
};
const closeCollectionSelector = (closeBtnClick?: boolean) => {
if (closeBtnClick === true) {
appContext.resetSharedFiles();
@ -346,8 +340,10 @@ export default function Gallery() {
<FullScreenDropZone
getRootProps={getRootProps}
getInputProps={getInputProps}
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
>
showCollectionSelector={setCollectionSelectorView.bind(
null,
true
)}>
{loading && (
<LoadingOverlay>
<EnteSpinner />
@ -399,24 +395,33 @@ export default function Gallery() {
attributes={collectionNamerAttributes}
/>
<CollectionSelector
show={collectionSelectorView && !(collectionsAndTheirLatestFile?.length === 0)}
show={
collectionSelectorView &&
!(collectionsAndTheirLatestFile?.length === 0)
}
onHide={closeCollectionSelector}
collectionsAndTheirLatestFile={collectionsAndTheirLatestFile}
collectionsAndTheirLatestFile={
collectionsAndTheirLatestFile
}
directlyShowNextModal={
collectionsAndTheirLatestFile?.length === 0
}
attributes={collectionSelectorAttributes}
syncWithRemote={syncWithRemote}
/>
<Upload
syncWithRemote={syncWithRemote}
setBannerMessage={setBannerMessage}
acceptedFiles={acceptedFiles}
showCollectionSelector={setCollectionSelectorView.bind(null, true)}
setCollectionSelectorAttributes={setCollectionSelectorAttributes}
showCollectionSelector={setCollectionSelectorView.bind(
null,
true
)}
setCollectionSelectorAttributes={
setCollectionSelectorAttributes
}
closeCollectionSelector={setCollectionSelectorView.bind(
null,
false,
false
)}
setLoading={setLoading}
setCollectionNamerAttributes={setCollectionNamerAttributes}
@ -431,7 +436,10 @@ export default function Gallery() {
setLoading={setLoading}
showPlanSelectorModal={() => setPlanModalView(true)}
/>
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} />
<UploadButton
isFirstFetch={isFirstFetch}
openFileUploader={openFileUploader}
/>
<PhotoFrame
files={files}
setFiles={setFiles}

View file

@ -35,7 +35,7 @@ export default function Generate() {
setLoading(true);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
const keyAttributes: KeyAttributes = getData(
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES,
LS_KEYS.ORIGINAL_KEY_ATTRIBUTES
);
router.prefetch('/gallery');
const user = getData(LS_KEYS.USER);
@ -64,14 +64,14 @@ export default function Generate() {
const onSubmit = async (passphrase, setFieldError) => {
try {
const { keyAttributes, masterKey } = await generateKeyAttributes(
passphrase,
passphrase
);
await putAttributes(token, keyAttributes);
await generateAndSaveIntermediateKeyAttributes(
passphrase,
keyAttributes,
masterKey,
masterKey
);
await setSessionKeys(masterKey);
setJustSignedUp(true);

View file

@ -11,6 +11,7 @@ import SignUp from 'components/SignUp';
import constants from 'utils/strings/constants';
import localForage from 'utils/storage/localForage';
import IncognitoWarning from 'components/IncognitoWarning';
import { logError } from 'utils/sentry';
const Container = styled.div`
display: flex;
@ -110,6 +111,7 @@ export default function LandingPage() {
try {
await localForage.ready();
} catch (e) {
logError(e, 'usage in incognito mode tried');
setBlockUsage(true);
}
setLoading(false);
@ -121,28 +123,41 @@ export default function LandingPage() {
const signUp = () => setShowLogin(false);
const login = () => setShowLogin(true);
return <Container>
{loading ? <EnteSpinner /> :
(<>
return (
<Container>
{loading ? (
<EnteSpinner />
) : (
<>
<SlideContainer>
<UpperText>
{constants.HERO_HEADER()}
</UpperText>
<UpperText>{constants.HERO_HEADER()}</UpperText>
<Carousel controls={false}>
<Carousel.Item>
<Img src="/images/slide-1.png" />
<FeatureText>{constants.HERO_SLIDE_1_TITLE}</FeatureText>
<TextContainer>{constants.HERO_SLIDE_1}</TextContainer>
<FeatureText>
{constants.HERO_SLIDE_1_TITLE}
</FeatureText>
<TextContainer>
{constants.HERO_SLIDE_1}
</TextContainer>
</Carousel.Item>
<Carousel.Item>
<Img src="/images/slide-2.png" />
<FeatureText>{constants.HERO_SLIDE_2_TITLE}</FeatureText>
<TextContainer>{constants.HERO_SLIDE_2}</TextContainer>
<FeatureText>
{constants.HERO_SLIDE_2_TITLE}
</FeatureText>
<TextContainer>
{constants.HERO_SLIDE_2}
</TextContainer>
</Carousel.Item>
<Carousel.Item>
<Img src="/images/slide-3.png" />
<FeatureText>{constants.HERO_SLIDE_3_TITLE}</FeatureText>
<TextContainer>{constants.HERO_SLIDE_3}</TextContainer>
<FeatureText>
{constants.HERO_SLIDE_3_TITLE}
</FeatureText>
<TextContainer>
{constants.HERO_SLIDE_3}
</TextContainer>
</Carousel.Item>
</Carousel>
</SlideContainer>
@ -151,8 +166,7 @@ export default function LandingPage() {
variant="outline-success"
size="lg"
style={{ color: '#fff', padding: '10px 50px' }}
onClick={() => router.push('signup')}
>
onClick={() => router.push('signup')}>
{constants.SIGN_UP}
</Button>
<br />
@ -160,17 +174,22 @@ export default function LandingPage() {
variant="link"
size="lg"
style={{ color: '#fff', padding: '10px 50px' }}
onClick={() => router.push('login')}
>
onClick={() => router.push('login')}>
{constants.SIGN_IN}
</Button>
</MobileBox>
<DesktopBox>
<SideBox>
{showLogin ? <Login signUp={signUp} /> : <SignUp login={login} />}
{showLogin ? (
<Login signUp={signUp} />
) : (
<SignUp login={login} />
)}
</SideBox>
</DesktopBox>
{blockUsage && <IncognitoWarning />}
</>)}
</Container>;
</>
)}
</Container>
);
}

View file

@ -27,14 +27,19 @@ export default function Home() {
router.push('/signup');
};
return <Container>{loading ?
return (
<Container>
{loading ? (
<EnteSpinner>
<span className="sr-only">Loading...</span>
</EnteSpinner>:
</EnteSpinner>
) : (
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Login signUp={register} />
</Card.Body>
</Card>}
</Container>;
</Card>
)}
</Container>
);
}

View file

@ -38,7 +38,7 @@ export default function Recover() {
const masterKey: string = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey,
keyAttributes.masterKeyDecryptionNonce,
await cryptoWorker.fromHex(recoveryKey),
await cryptoWorker.fromHex(recoveryKey)
);
setSessionKeys(masterKey);
router.push('/changePassword');
@ -51,13 +51,10 @@ export default function Recover() {
return (
<>
<Container>
<Card
style={{ minWidth: '320px' }}
className="text-center"
>
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.RECOVER_ACCOUNT}
</Card.Title>
<SingleInputForm
@ -71,12 +68,10 @@ export default function Recover() {
display: 'flex',
flexDirection: 'column',
marginTop: '12px',
}}
>
}}>
<Button
variant="link"
onClick={() => SetMessageDialogView(true)}
>
onClick={() => SetMessageDialogView(true)}>
{constants.NO_RECOVERY_KEY}
</Button>
<Button variant="link" onClick={router.back}>

View file

@ -7,7 +7,6 @@ import EnteSpinner from 'components/EnteSpinner';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import SignUp from 'components/SignUp';
export default function SignUpPage() {
const router = useRouter();
const appContext = useContext(AppContext);
@ -29,14 +28,16 @@ export default function SignUpPage() {
};
return (
<Container>{
loading ? <EnteSpinner /> :
<Container>
{loading ? (
<EnteSpinner />
) : (
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<SignUp login={login} />
</Card.Body>
</Card>
}
)}
</Container>
);
}

View file

@ -2,20 +2,20 @@ import React, { useEffect, useState } from 'react';
import constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router';
import CryptoWorker from 'utils/crypto';
import CryptoWorker, { B64EncryptionResult } from 'utils/crypto';
import SingleInputForm from 'components/SingleInputForm';
import MessageDialog from 'components/MessageDialog';
import Container from 'components/Container';
import { Card, Button } from 'react-bootstrap';
import LogoImg from 'components/LogoImg';
import { logError } from 'utils/sentry';
import { B64EncryptionResult } from 'services/uploadService';
import { recoverTwoFactor, removeTwoFactor } from 'services/userService';
export default function Recover() {
const router = useRouter();
const [messageDialogView, SetMessageDialogView] = useState(false);
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
const [encryptedTwoFactorSecret, setEncryptedTwoFactorSecret] =
useState<B64EncryptionResult>(null);
const [sessionID, setSessionID] = useState(null);
useEffect(() => {
router.prefetch('/gallery');
@ -41,7 +41,7 @@ export default function Recover() {
const twoFactorSecret: string = await cryptoWorker.decryptB64(
encryptedTwoFactorSecret.encryptedData,
encryptedTwoFactorSecret.nonce,
await cryptoWorker.fromHex(recoveryKey),
await cryptoWorker.fromHex(recoveryKey)
);
const resp = await removeTwoFactor(sessionID, twoFactorSecret);
const { keyAttributes, encryptedToken, token, id } = resp;
@ -63,13 +63,10 @@ export default function Recover() {
return (
<>
<Container>
<Card
style={{ minWidth: '320px' }}
className="text-center"
>
<Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.RECOVER_TWO_FACTOR}
</Card.Title>
<SingleInputForm
@ -83,12 +80,10 @@ export default function Recover() {
display: 'flex',
flexDirection: 'column',
marginTop: '12px',
}}
>
}}>
<Button
variant="link"
onClick={() => SetMessageDialogView(true)}
>
onClick={() => SetMessageDialogView(true)}>
{constants.NO_RECOVERY_KEY}
</Button>
<Button variant="link" onClick={router.back}>

View file

@ -4,17 +4,20 @@ import { CodeBlock, FreeFlowText } from 'components/RecoveryKeyModal';
import { DeadCenter } from 'pages/gallery';
import React, { useContext, useEffect, useState } from 'react';
import { Button, Card } from 'react-bootstrap';
import { enableTwoFactor, setupTwoFactor, TwoFactorSecret } from 'services/userService';
import {
enableTwoFactor,
setupTwoFactor,
TwoFactorSecret,
} from 'services/userService';
import styled from 'styled-components';
import constants from 'utils/strings/constants';
import Container from 'components/Container';
import { useRouter } from 'next/router';
import VerifyTwoFactor from 'components/VerifyTwoFactor';
import { B64EncryptionResult } from 'services/uploadService';
import { B64EncryptionResult } from 'utils/crypto';
import { encryptWithRecoveryKey } from 'utils/crypto';
import { setData, LS_KEYS, getData } from 'utils/storage/localStorage';
import { AppContext } from 'pages/_app';
import { AppContext, FLASH_MESSAGE_TYPE } from 'pages/_app';
enum SetupMode {
QR_CODE,
@ -29,8 +32,12 @@ const QRCode = styled.img`
export default function SetupTwoFactor() {
const [setupMode, setSetupMode] = useState<SetupMode>(SetupMode.QR_CODE);
const [twoFactorSecret, setTwoFactorSecret] = useState<TwoFactorSecret>(null);
const [recoveryEncryptedTwoFactorSecret, setRecoveryEncryptedTwoFactorSecret] = useState<B64EncryptionResult>(null);
const [twoFactorSecret, setTwoFactorSecret] =
useState<TwoFactorSecret>(null);
const [
recoveryEncryptedTwoFactorSecret,
setRecoveryEncryptedTwoFactorSecret,
] = useState<B64EncryptionResult>(null);
const router = useRouter();
const appContext = useContext(AppContext);
useEffect(() => {
@ -40,11 +47,17 @@ export default function SetupTwoFactor() {
const main = async () => {
try {
const twoFactorSecret = await setupTwoFactor();
const recoveryEncryptedTwoFactorSecret = await encryptWithRecoveryKey(twoFactorSecret.secretCode);
const recoveryEncryptedTwoFactorSecret =
await encryptWithRecoveryKey(twoFactorSecret.secretCode);
setTwoFactorSecret(twoFactorSecret);
setRecoveryEncryptedTwoFactorSecret(recoveryEncryptedTwoFactorSecret);
setRecoveryEncryptedTwoFactorSecret(
recoveryEncryptedTwoFactorSecret
);
} catch (e) {
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_FAILED, severity: 'danger' });
appContext.setDisappearingFlashMessage({
message: constants.TWO_FACTOR_SETUP_FAILED,
type: FLASH_MESSAGE_TYPE.DANGER,
});
router.push('/gallery');
}
};
@ -52,8 +65,14 @@ export default function SetupTwoFactor() {
}, []);
const onSubmit = async (otp: string) => {
await enableTwoFactor(otp, recoveryEncryptedTwoFactorSecret);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: true });
appContext.setDisappearingFlashMessage({ message: constants.TWO_FACTOR_SETUP_SUCCESS, severity: 'info' });
setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
isTwoFactorEnabled: true,
});
appContext.setDisappearingFlashMessage({
message: constants.TWO_FACTOR_SETUP_SUCCESS,
type: FLASH_MESSAGE_TYPE.SUCCESS,
});
router.push('/gallery');
};
return (
@ -62,32 +81,64 @@ export default function SetupTwoFactor() {
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
<DeadCenter>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.TWO_FACTOR}
</Card.Title>
{setupMode === SetupMode.QR_CODE ? (
<>
<p>{constants.TWO_FACTOR_QR_INSTRUCTION}</p>
<DeadCenter>
{!twoFactorSecret ? <div style={{ height: '200px', width: '200px', margin: '1rem', display: 'flex', justifyContent: 'center', alignItems: 'center', border: '1px solid #aaa' }}><EnteSpinner /></div> :
<QRCode src={`data:image/png;base64,${twoFactorSecret.qrCode}`} />
}
<Button block variant="link" onClick={() => setSetupMode(SetupMode.MANUAL_CODE)}>
{!twoFactorSecret ? (
<div
style={{
height: '200px',
width: '200px',
margin: '1rem',
display: 'flex',
justifyContent: 'center',
alignItems: 'center',
border: '1px solid #aaa',
}}>
<EnteSpinner />
</div>
) : (
<QRCode
src={`data:image/png;base64,${twoFactorSecret.qrCode}`}
/>
)}
<Button
block
variant="link"
onClick={() =>
setSetupMode(SetupMode.MANUAL_CODE)
}>
{constants.ENTER_CODE_MANUALLY}
</Button>
</DeadCenter>
</>
) : (<>
<p>{constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION}</p>
) : (
<>
<p>
{
constants.TWO_FACTOR_MANUAL_CODE_INSTRUCTION
}
</p>
<CodeBlock height={100}>
{!twoFactorSecret ? <EnteSpinner /> : (
{!twoFactorSecret ? (
<EnteSpinner />
) : (
<FreeFlowText>
{twoFactorSecret.secretCode}
</FreeFlowText>
)}
</CodeBlock>
<Button block variant="link" style={{ marginBottom: '1rem' }} onClick={() => setSetupMode(SetupMode.QR_CODE)}>
<Button
block
variant="link"
style={{ marginBottom: '1rem' }}
onClick={() =>
setSetupMode(SetupMode.QR_CODE)
}>
{constants.SCAN_QR_CODE}
</Button>
</>
@ -99,8 +150,15 @@ export default function SetupTwoFactor() {
width: '100%',
}}
/>
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.ENABLE} />
<Button style={{ marginTop: '16px' }} variant="link-danger" onClick={router.back}>
<VerifyTwoFactor
onSubmit={onSubmit}
back={router.back}
buttonText={constants.ENABLE}
/>
<Button
style={{ marginTop: '16px' }}
variant="link-danger"
onClick={router.back}>
{constants.GO_BACK}
</Button>
</DeadCenter>

View file

@ -52,21 +52,23 @@ export default function Home() {
<Card style={{ minWidth: '300px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px', minHeight: '400px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.TWO_FACTOR}
</Card.Title>
<VerifyTwoFactor onSubmit={onSubmit} back={router.back} buttonText={constants.VERIFY} />
<VerifyTwoFactor
onSubmit={onSubmit}
back={router.back}
buttonText={constants.VERIFY}
/>
<div
style={{
display: 'flex',
flexDirection: 'column',
marginTop: '12px',
}}
>
}}>
<Button
variant="link"
onClick={() => router.push('/two-factor/recover')}
>
onClick={() => router.push('/two-factor/recover')}>
{constants.LOST_DEVICE}
</Button>
<Button variant="link" onClick={logoutUser}>

View file

@ -56,14 +56,24 @@ export default function Verify() {
const onSubmit = async (
{ ott }: formValues,
{ setFieldError }: FormikHelpers<formValues>,
{ setFieldError }: FormikHelpers<formValues>
) => {
try {
setLoading(true);
const resp = await verifyOtt(email, ott);
const { keyAttributes, encryptedToken, token, id, twoFactorSessionID } = resp.data as EmailVerificationResponse;
const {
keyAttributes,
encryptedToken,
token,
id,
twoFactorSessionID,
} = resp.data as EmailVerificationResponse;
if (twoFactorSessionID) {
setData(LS_KEYS.USER, { email, twoFactorSessionID, isTwoFactorEnabled: true });
setData(LS_KEYS.USER, {
email,
twoFactorSessionID,
isTwoFactorEnabled: true,
});
router.push('/two-factor/verify');
return;
}
@ -109,7 +119,7 @@ export default function Verify() {
<Card style={{ minWidth: '300px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' />
<LogoImg src="/icon.svg" />
{constants.VERIFY_EMAIL}
</Card.Title>
{constants.EMAIL_SENT({ email })}
@ -123,8 +133,7 @@ export default function Verify() {
})}
validateOnChange={false}
validateOnBlur={false}
onSubmit={onSubmit}
>
onSubmit={onSubmit}>
{({
values,
touched,
@ -140,7 +149,7 @@ export default function Verify() {
value={values.ott}
onChange={handleChange('ott')}
isInvalid={Boolean(
touched.ott && errors.ott,
touched.ott && errors.ott
)}
placeholder={constants.ENTER_OTT}
disabled={loading}

View file

@ -8,17 +8,25 @@ pageCache();
precacheAndRoute(self.__WB_MANIFEST);
cleanupOutdatedCaches();
registerRoute('/share-target', async ({ event }) => {
event.waitUntil(async function() {
registerRoute(
'/share-target',
async ({ event }) => {
event.waitUntil(
(async function () {
const data = await event.request.formData();
const client = await self.clients.get(event.resultingClientId || event.clientId);
const client = await self.clients.get(
event.resultingClientId || event.clientId
);
const files = data.getAll('files');
setTimeout(() => {
client.postMessage({ files, action: 'upload-files' });
}, 1000);
}());
})()
);
return Response.redirect('./');
}, 'POST');
},
'POST'
);
// Use a stale-while-revalidate strategy for all other requests.
setDefaultHandler(new NetworkOnly());

View file

@ -21,7 +21,7 @@ class HTTPService {
}
const { response } = err;
return Promise.reject(response);
},
}
);
}
@ -77,7 +77,9 @@ class HTTPService {
...config.headers,
};
if (customConfig?.cancel) {
config.cancelToken=new axios.CancelToken((c)=> (customConfig.cancel.exec=c));
config.cancelToken = new axios.CancelToken(
(c) => (customConfig.cancel.exec = c)
);
}
return await axios({ ...config, ...customConfig });
}
@ -89,7 +91,7 @@ class HTTPService {
url: string,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
customConfig?: any
) {
return this.request(
{
@ -98,7 +100,7 @@ class HTTPService {
params,
url,
},
customConfig,
customConfig
);
}
@ -110,7 +112,7 @@ class HTTPService {
data?: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
customConfig?: any
) {
return this.request(
{
@ -120,7 +122,7 @@ class HTTPService {
params,
url,
},
customConfig,
customConfig
);
}
@ -132,7 +134,7 @@ class HTTPService {
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
customConfig?: any
) {
return this.request(
{
@ -142,7 +144,7 @@ class HTTPService {
params,
url,
},
customConfig,
customConfig
);
}
@ -154,7 +156,7 @@ class HTTPService {
data: any,
params?: IQueryPrams,
headers?: IHTTPHeaders,
customConfig?: any,
customConfig?: any
) {
return this.request(
{
@ -164,7 +166,7 @@ class HTTPService {
params,
url,
},
customConfig,
customConfig
);
}
}

View file

@ -17,7 +17,7 @@ export enum PAYMENT_INTENT_STATUS {
}
enum PaymentActionType {
Buy = 'buy',
Update='update'
Update = 'update',
}
export interface Subscription {
id: number;
@ -30,6 +30,8 @@ export interface Subscription {
attributes: {
isCancelled: boolean;
};
price: string;
period: string;
}
export interface Plan {
id: string;
@ -66,11 +68,13 @@ class billingService {
}
}
public async updatePlans() {
public async getPlans(): Promise<Plan[]> {
try {
const response = await HTTPService.get(`${ENDPOINT}/billing/plans`);
const response = await HTTPService.get(
`${ENDPOINT}/billing/plans/v2`
);
const { plans } = response.data;
setData(LS_KEYS.PLANS, plans);
return plans;
} catch (e) {
logError(e, 'failed to get plans');
}
@ -83,12 +87,12 @@ class billingService {
null,
{
'X-Auth-Token': getToken(),
},
}
);
const { subscription } = response.data;
setData(LS_KEYS.SUBSCRIPTION, subscription);
} catch (e) {
logError(e, 'failed to get user\'s subscription details');
logError(e, "failed to get user's subscription details");
}
}
@ -99,7 +103,11 @@ class billingService {
// sessionId: response.data.sessionID,
// });
const paymentToken = await getPaymentToken();
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Buy);
await this.redirectToPayments(
paymentToken,
productID,
PaymentActionType.Buy
);
} catch (e) {
logError(e, 'unable to buy subscription');
throw e;
@ -140,7 +148,11 @@ class billingService {
// break;
// }
const paymentToken = await getPaymentToken();
await this.redirectToPayments(paymentToken, productID, PaymentActionType.Update);
await this.redirectToPayments(
paymentToken,
productID,
PaymentActionType.Update
);
} catch (e) {
logError(e, 'subscription update failed');
throw e;
@ -160,7 +172,7 @@ class billingService {
null,
{
'X-Auth-Token': getToken(),
},
}
);
const { subscription } = response.data;
setData(LS_KEYS.SUBSCRIPTION, subscription);
@ -178,7 +190,7 @@ class billingService {
null,
{
'X-Auth-Token': getToken(),
},
}
);
const { subscription } = response.data;
setData(LS_KEYS.SUBSCRIPTION, subscription);
@ -196,12 +208,12 @@ class billingService {
},
{
'X-Auth-Token': getToken(),
},
}
);
}
public async verifySubscription(
sessionID: string = null,
sessionID: string = null
): Promise<Subscription> {
try {
const response = await HTTPService.post(
@ -214,7 +226,7 @@ class billingService {
null,
{
'X-Auth-Token': getToken(),
},
}
);
const { subscription } = response.data;
setData(LS_KEYS.SUBSCRIPTION, subscription);
@ -225,7 +237,11 @@ class billingService {
}
}
public async redirectToPayments(paymentToken:string, productID:string, action:string) {
public async redirectToPayments(
paymentToken: string,
productID: string,
action: string
) {
try {
window.location.href = `${getPaymentsUrl()}?productID=${productID}&paymentToken=${paymentToken}&action=${action}&rootURL=${
window.location.origin
@ -243,7 +259,7 @@ class billingService {
null,
{
'X-Auth-Token': getToken(),
},
}
);
window.location.href = response.data.url;
} catch (e) {
@ -259,7 +275,7 @@ class billingService {
{ startTime: 0, endTime: Date.now() * 1000 },
{
'X-Auth-Token': getToken(),
},
}
);
return convertToHumanReadable(response.data.usage);
} catch (e) {

View file

@ -7,7 +7,7 @@ import CryptoWorker from 'utils/crypto';
import { SetDialogMessage } from 'components/MessageDialog';
import constants from 'utils/strings/constants';
import { getPublicKey, User } from './userService';
import { B64EncryptionResult } from './uploadService';
import { B64EncryptionResult } from 'utils/crypto';
import HTTPService from './HTTPService';
import { File } from './fileService';
import { logError } from 'utils/sentry';
@ -52,7 +52,7 @@ export interface CollectionAndItsLatestFile {
const getCollectionWithSecrets = async (
collection: Collection,
masterKey: string,
masterKey: string
) => {
const worker = await new CryptoWorker();
const userID = getData(LS_KEYS.USER).id;
@ -61,26 +61,27 @@ const getCollectionWithSecrets = async (
decryptedKey = await worker.decryptB64(
collection.encryptedKey,
collection.keyDecryptionNonce,
masterKey,
masterKey
);
} else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64(
keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce,
masterKey,
masterKey
);
decryptedKey = await worker.boxSealOpen(
collection.encryptedKey,
keyAttributes.publicKey,
secretKey,
secretKey
);
}
collection.name = collection.name ||
collection.name =
collection.name ||
(await worker.decryptToUTF8(
collection.encryptedName,
collection.nameDecryptionNonce,
decryptedKey,
decryptedKey
));
return {
...collection,
@ -91,7 +92,7 @@ const getCollectionWithSecrets = async (
const getCollections = async (
token: string,
sinceTime: number,
key: string,
key: string
): Promise<Collection[]> => {
try {
const resp = await HTTPService.get(
@ -99,7 +100,7 @@ const getCollections = async (
{
sinceTime,
},
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
const promises: Promise<Collection>[] = resp.data.collections.map(
async (collection: Collection) => {
@ -110,16 +111,16 @@ const getCollections = async (
try {
collectionWithSecrets = await getCollectionWithSecrets(
collection,
key,
key
);
return collectionWithSecrets;
} catch (e) {
logError(
e,
`decryption failed for collection with id=${collection.id}`,
`decryption failed for collection with id=${collection.id}`
);
}
},
}
);
return await Promise.all(promises);
} catch (e) {
@ -129,18 +130,21 @@ const getCollections = async (
};
export const getLocalCollections = async (): Promise<Collection[]> => {
const collections: Collection[] = (await localForage.getItem(COLLECTIONS)) ?? [];
const collections: Collection[] =
(await localForage.getItem(COLLECTIONS)) ?? [];
return collections;
};
export const getCollectionUpdationTime = async (): Promise<number> => (await localForage.getItem<number>(COLLECTION_UPDATION_TIME)) ?? 0;
export const getCollectionUpdationTime = async (): Promise<number> =>
(await localForage.getItem<number>(COLLECTION_UPDATION_TIME)) ?? 0;
export const syncCollections = async () => {
const localCollections = await getLocalCollections();
const lastCollectionUpdationTime = await getCollectionUpdationTime();
const token = getToken();
const key = await getActualKey();
const updatedCollections = (await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
const updatedCollections =
(await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
if (updatedCollections.length === 0) {
return localCollections;
}
@ -161,7 +165,7 @@ export const syncCollections = async () => {
const collections: Collection[] = [];
let updationTime = await localForage.getItem<number>(
COLLECTION_UPDATION_TIME,
COLLECTION_UPDATION_TIME
);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, collection] of latestCollectionsInstances) {
@ -179,7 +183,7 @@ export const syncCollections = async () => {
export const getCollectionsAndTheirLatestFile = (
collections: Collection[],
files: File[],
files: File[]
): CollectionAndItsLatestFile[] => {
const latestFile = new Map<number, File>();
@ -213,15 +217,16 @@ export const getFavItemIds = async (files: File[]): Promise<Set<number>> => {
return new Set(
files
.filter((file) => file.collectionID === favCollection.id)
.map((file): number => file.id),
.map((file): number => file.id)
);
};
export const createAlbum = async (albumName: string) => createCollection(albumName, CollectionType.album);
export const createAlbum = async (albumName: string) =>
createCollection(albumName, CollectionType.album);
export const createCollection = async (
collectionName: string,
type: CollectionType,
type: CollectionType
): Promise<Collection> => {
try {
const existingCollections = await syncCollections();
@ -239,14 +244,14 @@ export const createCollection = async (
nonce: keyDecryptionNonce,
}: B64EncryptionResult = await worker.encryptToB64(
collectionKey,
encryptionKey,
encryptionKey
);
const {
encryptedData: encryptedName,
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
collectionName,
collectionKey,
collectionKey
);
const newCollection: Collection = {
id: null,
@ -263,11 +268,11 @@ export const createCollection = async (
};
let createdCollection: Collection = await postCollection(
newCollection,
token,
token
);
createdCollection = await getCollectionWithSecrets(
createdCollection,
encryptionKey,
encryptionKey
);
return createdCollection;
} catch (e) {
@ -278,14 +283,14 @@ export const createCollection = async (
const postCollection = async (
collectionData: Collection,
token: string,
token: string
): Promise<Collection> => {
try {
const response = await HTTPService.post(
`${ENDPOINT}/collections`,
collectionData,
null,
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
return response.data.collection;
} catch (e) {
@ -298,7 +303,7 @@ export const addToFavorites = async (file: File) => {
if (!favCollection) {
favCollection = await createCollection(
'Favorites',
CollectionType.favorites,
CollectionType.favorites
);
await localForage.setItem(FAV_COLLECTION, favCollection);
}
@ -312,7 +317,7 @@ export const removeFromFavorites = async (file: File) => {
export const addToCollection = async (
collection: Collection,
files: File[],
files: File[]
) => {
try {
const params = {};
@ -322,7 +327,8 @@ export const addToCollection = async (
await Promise.all(
files.map(async (file) => {
file.collectionID = collection.id;
const newEncryptedKey: B64EncryptionResult = await worker.encryptToB64(file.key, collection.key);
const newEncryptedKey: B64EncryptionResult =
await worker.encryptToB64(file.key, collection.key);
file.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce;
if (params['files'] === undefined) {
@ -334,13 +340,13 @@ export const addToCollection = async (
keyDecryptionNonce: file.keyDecryptionNonce,
});
return file;
}),
})
);
await HTTPService.post(
`${ENDPOINT}/collections/add-files`,
params,
null,
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
} catch (e) {
logError(e, 'Add to collection Failed ');
@ -357,13 +363,13 @@ const removeFromCollection = async (collection: Collection, files: File[]) => {
params['fileIDs'] = [];
}
params['fileIDs'].push(file.id);
}),
})
);
await HTTPService.post(
`${ENDPOINT}/collections/remove-files`,
params,
null,
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
} catch (e) {
logError(e, 'remove from collection failed ');
@ -374,7 +380,7 @@ export const deleteCollection = async (
collectionID: number,
syncWithRemote: () => Promise<void>,
redirectToAll: () => void,
setDialogMessage: SetDialogMessage,
setDialogMessage: SetDialogMessage
) => {
try {
const token = getToken();
@ -383,7 +389,7 @@ export const deleteCollection = async (
`${ENDPOINT}/collections/${collectionID}`,
null,
null,
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
await syncWithRemote();
redirectToAll();
@ -399,7 +405,7 @@ export const deleteCollection = async (
export const renameCollection = async (
collection: Collection,
newCollectionName: string,
newCollectionName: string
) => {
const token = getToken();
const worker = await new CryptoWorker();
@ -408,7 +414,7 @@ export const renameCollection = async (
nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8(
newCollectionName,
collection.key,
collection.key
);
const collectionRenameRequest = {
collectionID: collection.id,
@ -421,12 +427,12 @@ export const renameCollection = async (
null,
{
'X-Auth-Token': token,
},
}
);
};
export const shareCollection = async (
collection: Collection,
withUserEmail: string,
withUserEmail: string
) => {
try {
const worker = await new CryptoWorker();
@ -435,7 +441,7 @@ export const shareCollection = async (
const publicKey: string = await getPublicKey(withUserEmail);
const encryptedKey: string = await worker.boxSeal(
collection.key,
publicKey,
publicKey
);
const shareCollectionRequest = {
collectionID: collection.id,
@ -448,7 +454,7 @@ export const shareCollection = async (
null,
{
'X-Auth-Token': token,
},
}
);
} catch (e) {
logError(e, 'share collection failed ');
@ -458,7 +464,7 @@ export const shareCollection = async (
export const unshareCollection = async (
collection: Collection,
withUserEmail: string,
withUserEmail: string
) => {
try {
const token = getToken();
@ -472,7 +478,7 @@ export const unshareCollection = async (
null,
{
'X-Auth-Token': token,
},
}
);
} catch (e) {
logError(e, 'unshare collection failed ');
@ -492,11 +498,13 @@ export const getFavCollection = async () => {
export const getNonEmptyCollections = (
collections: Collection[],
files: File[],
files: File[]
) => {
const nonEmptyCollectionsIds = new Set<number>();
for (const file of files) {
nonEmptyCollectionsIds.add(file.collectionID);
}
return collections.filter((collection) => nonEmptyCollectionsIds.has(collection.id));
return collections.filter((collection) =>
nonEmptyCollectionsIds.has(collection.id)
);
};

View file

@ -1,10 +1,16 @@
import { getToken } from 'utils/common/key';
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto';
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file';
import {
fileIsHEIC,
convertHEIC2JPEG,
fileNameWithoutExtension,
generateStreamFromArrayBuffer,
} from 'utils/file';
import HTTPService from './HTTPService';
import { File } from './fileService';
import { File, FILE_TYPE } from './fileService';
import { logError } from 'utils/sentry';
import { decodeMotionPhoto } from './motionPhotoService';
class DownloadManager {
private fileDownloads = new Map<string, string>();
@ -36,36 +42,50 @@ class DownloadManager {
getThumbnailUrl(file.id),
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' },
{ responseType: 'arraybuffer' }
);
const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader),
file.key,
file.key
);
try {
await cache.put(
file.id.toString(),
new Response(new Blob([decrypted])),
new Response(new Blob([decrypted]))
);
} catch (e) {
// TODO: handle storage full exception.
}
return URL.createObjectURL(new Blob([decrypted]));
}
};
getFile = async (file: File, forPreview = false) => {
try {
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
// unzip motion photo and return fileBlob of the image for preview
const fileStream = await this.downloadFile(file);
let fileBlob = await new Response(fileStream).blob();
if (forPreview) {
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
const originalName = fileNameWithoutExtension(
file.metadata.title
);
const motionPhoto = await decodeMotionPhoto(
fileBlob,
originalName
);
fileBlob = new Blob([motionPhoto.image]);
}
if (fileIsHEIC(file.metadata.title)) {
fileBlob = await convertHEIC2JPEG(fileBlob);
}
}
this.fileDownloads.set(`${file.id}_${forPreview}`, URL.createObjectURL(fileBlob));
this.fileDownloads.set(
`${file.id}_${forPreview}`,
URL.createObjectURL(fileBlob)
);
}
return this.fileDownloads.get(`${file.id}_${forPreview}`);
} catch (e) {
@ -79,25 +99,22 @@ class DownloadManager {
if (!token) {
return null;
}
if (file.metadata.fileType === 0) {
if (
file.metadata.fileType === FILE_TYPE.IMAGE ||
file.metadata.fileType === FILE_TYPE.LIVE_PHOTO
) {
const resp = await HTTPService.get(
getFileUrl(file.id),
null,
{ 'X-Auth-Token': token },
{ responseType: 'arraybuffer' },
{ responseType: 'arraybuffer' }
);
const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader),
file.key,
file.key
);
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {
controller.enqueue(decrypted);
controller.close();
},
});
return generateStreamFromArrayBuffer(decrypted);
}
const resp = await fetch(getFileUrl(file.id), {
headers: {
@ -108,13 +125,11 @@ class DownloadManager {
const stream = new ReadableStream({
async start(controller) {
const decryptionHeader = await worker.fromB64(
file.file.decryptionHeader,
file.file.decryptionHeader
);
const fileKey = await worker.fromB64(file.key);
const {
pullState,
decryptionChunkSize,
} = await worker.initDecryption(decryptionHeader, fileKey);
const { pullState, decryptionChunkSize } =
await worker.initDecryption(decryptionHeader, fileKey);
let data = new Uint8Array();
// The following function handles each data chunk
function push() {
@ -123,23 +138,19 @@ class DownloadManager {
// Is there more data to read?
if (!done) {
const buffer = new Uint8Array(
data.byteLength + value.byteLength,
data.byteLength + value.byteLength
);
buffer.set(new Uint8Array(data), 0);
buffer.set(
new Uint8Array(value),
data.byteLength,
);
buffer.set(new Uint8Array(value), data.byteLength);
if (buffer.length > decryptionChunkSize) {
const fileData = buffer.slice(
0,
decryptionChunkSize,
decryptionChunkSize
);
const {
decryptedData,
} = await worker.decryptChunk(
const { decryptedData } =
await worker.decryptChunk(
fileData,
pullState,
pullState
);
controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize);
@ -149,12 +160,8 @@ class DownloadManager {
push();
} else {
if (data) {
const {
decryptedData,
} = await worker.decryptChunk(
data,
pullState,
);
const { decryptedData } =
await worker.decryptChunk(data, pullState);
controller.enqueue(decryptedData);
data = null;
}

View file

@ -1,10 +1,26 @@
import { retryAsyncFunction, runningInBrowser } from 'utils/common';
import { getExportPendingFiles, getExportFailedFiles, getFilesUploadedAfterLastExport, getFileUID, dedupe, getGoogleLikeMetadataFile } from 'utils/export';
import {
getExportPendingFiles,
getExportFailedFiles,
getFilesUploadedAfterLastExport,
getExportRecordFileUID,
dedupe,
getGoogleLikeMetadataFile,
} from 'utils/export';
import {
fileNameWithoutExtension,
generateStreamFromArrayBuffer,
} from 'utils/file';
import { logError } from 'utils/sentry';
import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { Collection, getLocalCollections } from './collectionService';
import {
Collection,
getLocalCollections,
getNonEmptyCollections,
} from './collectionService';
import downloadManager from './downloadManager';
import { File, getLocalFiles } from './fileService';
import { File, FILE_TYPE, getLocalFiles } from './fileService';
import { decodeMotionPhoto } from './motionPhotoService';
export interface ExportProgress {
current: number;
@ -16,7 +32,7 @@ export interface ExportStats {
}
export interface ExportRecord {
stage: ExportStage
stage: ExportStage;
lastAttemptTimestamp: number;
progress: ExportProgress;
queuedFiles: string[];
@ -27,7 +43,7 @@ export enum ExportStage {
INIT,
INPROGRESS,
PAUSED,
FINISHED
FINISHED,
}
enum ExportNotification {
@ -37,26 +53,26 @@ enum ExportNotification {
FAILED = 'export failed',
ABORT = 'export aborted',
PAUSE = 'export paused',
UP_TO_DATE = `no new files to export`
UP_TO_DATE = `no new files to export`,
}
enum RecordType {
SUCCESS = 'success',
FAILED = 'failed'
FAILED = 'failed',
}
export enum ExportType {
NEW,
PENDING,
RETRY_FAILED
RETRY_FAILED,
}
const ExportRecordFileName='export_status.json';
const MetadataFolderName='metadata';
const EXPORT_RECORD_FILE_NAME = 'export_status.json';
export const METADATA_FOLDER_NAME = 'metadata';
class ExportService {
ElectronAPIs: any;
private exportInProgress: Promise<{ paused: boolean; }> = null;
private exportInProgress: Promise<{ paused: boolean }> = null;
private recordUpdateInProgress = Promise.resolve();
private stopExport: boolean = false;
private pauseExport: boolean = false;
@ -73,7 +89,10 @@ class ExportService {
pauseRunningExport() {
this.pauseExport = true;
}
async exportFiles(updateProgress: (progress: ExportProgress) => void, exportType: ExportType) {
async exportFiles(
updateProgress: (progress: ExportProgress) => void,
exportType: ExportType
) {
if (this.exportInProgress) {
this.ElectronAPIs.sendNotification(ExportNotification.IN_PROGRESS);
return this.exportInProgress;
@ -87,25 +106,44 @@ class ExportService {
let filesToExport: File[];
const allFiles = await getLocalFiles();
const collections = await getLocalCollections();
const nonEmptyCollections = getNonEmptyCollections(
collections,
allFiles
);
const exportRecord = await this.getExportRecord(exportDir);
if (exportType === ExportType.NEW) {
filesToExport = await getFilesUploadedAfterLastExport(allFiles, exportRecord);
filesToExport = await getFilesUploadedAfterLastExport(
allFiles,
exportRecord
);
} else if (exportType === ExportType.RETRY_FAILED) {
filesToExport = await getExportFailedFiles(allFiles, exportRecord);
} else {
filesToExport = await getExportPendingFiles(allFiles, exportRecord);
}
this.exportInProgress = this.fileExporter(filesToExport, collections, updateProgress, exportDir);
this.exportInProgress = this.fileExporter(
filesToExport,
nonEmptyCollections,
updateProgress,
exportDir
);
const resp = await this.exportInProgress;
this.exportInProgress = null;
return resp;
}
async fileExporter(files: File[], collections: Collection[], updateProgress: (progress: ExportProgress,) => void, dir: string): Promise<{ paused: boolean }> {
async fileExporter(
files: File[],
collections: Collection[],
updateProgress: (progress: ExportProgress) => void,
dir: string
): Promise<{ paused: boolean }> {
try {
if (!files?.length) {
this.ElectronAPIs.sendNotification(ExportNotification.UP_TO_DATE);
this.ElectronAPIs.sendNotification(
ExportNotification.UP_TO_DATE
);
return { paused: false };
}
this.stopExport = false;
@ -114,22 +152,24 @@ class ExportService {
const failedFileCount = 0;
this.ElectronAPIs.showOnTray({
export_progress:
`0 / ${files.length} files exported`,
export_progress: `0 / ${files.length} files exported`,
});
updateProgress({
current: 0, total: files.length,
current: 0,
total: files.length,
});
this.ElectronAPIs.sendNotification(ExportNotification.START);
const collectionIDMap = new Map<number, string>();
for (const collection of collections) {
const collectionFolderPath = `${dir}/${collection.id}_${this.sanitizeName(collection.name)}`;
const collectionFolderPath = `${dir}/${
collection.id
}_${this.sanitizeName(collection.name)}`;
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
collectionFolderPath,
collectionFolderPath
);
await this.ElectronAPIs.checkExistsAndCreateCollectionDir(
`${collectionFolderPath}/${MetadataFolderName}`,
`${collectionFolderPath}/${METADATA_FOLDER_NAME}`
);
collectionIDMap.set(collection.id, collectionFolderPath);
}
@ -137,8 +177,7 @@ class ExportService {
if (this.stopExport || this.pauseExport) {
if (this.pauseExport) {
this.ElectronAPIs.showOnTray({
export_progress:
`${index} / ${files.length} files exported (paused)`,
export_progress: `${index} / ${files.length} files exported (paused)`,
paused: true,
});
}
@ -147,39 +186,42 @@ class ExportService {
const collectionPath = collectionIDMap.get(file.collectionID);
try {
await this.downloadAndSave(file, collectionPath);
await this.addFileExportRecord(dir, file, RecordType.SUCCESS);
await this.addFileExportRecord(
dir,
file,
RecordType.SUCCESS
);
} catch (e) {
await this.addFileExportRecord(dir, file, RecordType.FAILED);
logError(e, 'download and save failed for file during export');
await this.addFileExportRecord(
dir,
file,
RecordType.FAILED
);
logError(
e,
'download and save failed for file during export'
);
}
this.ElectronAPIs.showOnTray({
export_progress:
`${index + 1} / ${files.length} files exported`,
export_progress: `${index + 1} / ${
files.length
} files exported`,
});
updateProgress({ current: index + 1, total: files.length });
}
if (this.stopExport) {
this.ElectronAPIs.sendNotification(
ExportNotification.ABORT,
);
this.ElectronAPIs.sendNotification(ExportNotification.ABORT);
this.ElectronAPIs.showOnTray();
} else if (this.pauseExport) {
this.ElectronAPIs.sendNotification(
ExportNotification.PAUSE,
);
this.ElectronAPIs.sendNotification(ExportNotification.PAUSE);
return { paused: true };
} else if (failedFileCount > 0) {
this.ElectronAPIs.sendNotification(
ExportNotification.FAILED,
);
this.ElectronAPIs.sendNotification(ExportNotification.FAILED);
this.ElectronAPIs.showOnTray({
retry_export:
`export failed - retry export`,
retry_export: `export failed - retry export`,
});
} else {
this.ElectronAPIs.sendNotification(
ExportNotification.FINISH,
);
this.ElectronAPIs.sendNotification(ExportNotification.FINISH);
this.ElectronAPIs.showOnTray();
}
return { paused: false };
@ -189,20 +231,25 @@ class ExportService {
}
async addFilesQueuedRecord(folder: string, files: File[]) {
const exportRecord = await this.getExportRecord(folder);
exportRecord.queuedFiles = files.map(getFileUID);
exportRecord.queuedFiles = files.map(getExportRecordFileUID);
await this.updateExportRecord(exportRecord, folder);
}
async addFileExportRecord(folder: string, file: File, type: RecordType) {
const fileUID = getFileUID(file);
const fileUID = getExportRecordFileUID(file);
const exportRecord = await this.getExportRecord(folder);
exportRecord.queuedFiles = exportRecord.queuedFiles.filter((queuedFilesUID) => queuedFilesUID !== fileUID);
exportRecord.queuedFiles = exportRecord.queuedFiles.filter(
(queuedFilesUID) => queuedFilesUID !== fileUID
);
if (type === RecordType.SUCCESS) {
if (!exportRecord.exportedFiles) {
exportRecord.exportedFiles = [];
}
exportRecord.exportedFiles.push(fileUID);
exportRecord.failedFiles && (exportRecord.failedFiles = exportRecord.failedFiles.filter((FailedFileUID) => FailedFileUID !== fileUID));
exportRecord.failedFiles &&
(exportRecord.failedFiles = exportRecord.failedFiles.filter(
(FailedFileUID) => FailedFileUID !== fileUID
));
} else {
if (!exportRecord.failedFiles) {
exportRecord.failedFiles = [];
@ -226,7 +273,10 @@ class ExportService {
}
const exportRecord = await this.getExportRecord(folder);
const newRecord = { ...exportRecord, ...newData };
await this.ElectronAPIs.setExportRecord(`${folder}/${ExportRecordFileName}`, JSON.stringify(newRecord, null, 2));
await this.ElectronAPIs.setExportRecord(
`${folder}/${EXPORT_RECORD_FILE_NAME}`,
JSON.stringify(newRecord, null, 2)
);
} catch (e) {
logError(e, 'error updating Export Record');
}
@ -239,7 +289,9 @@ class ExportService {
if (!folder) {
folder = getData(LS_KEYS.EXPORT)?.folder;
}
const recordFile = await this.ElectronAPIs.getExportRecord(`${folder}/${ExportRecordFileName}`);
const recordFile = await this.ElectronAPIs.getExportRecord(
`${folder}/${EXPORT_RECORD_FILE_NAME}`
);
if (recordFile) {
return JSON.parse(recordFile);
} else {
@ -251,14 +303,48 @@ class ExportService {
}
async downloadAndSave(file: File, collectionPath: string) {
const uid = `${file.id}_${this.sanitizeName(
file.metadata.title,
)}`;
const fileStream = await retryAsyncFunction(()=>downloadManager.downloadFile(file));
this.ElectronAPIs.saveStreamToDisk(`${collectionPath}/${uid}`, fileStream);
const uid = `${file.id}_${this.sanitizeName(file.metadata.title)}`;
const fileStream = await retryAsyncFunction(() =>
downloadManager.downloadFile(file)
);
if (file.metadata.fileType === FILE_TYPE.LIVE_PHOTO) {
this.exportMotionPhoto(fileStream, file, collectionPath);
} else {
this.saveMediaFile(collectionPath, uid, fileStream);
this.saveMetadataFile(collectionPath, uid, file.metadata);
}
}
private async exportMotionPhoto(
fileStream: ReadableStream<any>,
file: File,
collectionPath: string
) {
const fileBlob = await new Response(fileStream).blob();
const originalName = fileNameWithoutExtension(file.metadata.title);
const motionPhoto = await decodeMotionPhoto(fileBlob, originalName);
const imageStream = generateStreamFromArrayBuffer(motionPhoto.image);
const imageUID = `${file.id}_${motionPhoto.imageNameTitle}`;
this.saveMediaFile(collectionPath, imageUID, imageStream);
this.saveMetadataFile(collectionPath, imageUID, file.metadata);
const videoStream = generateStreamFromArrayBuffer(motionPhoto.video);
const videoUID = `${file.id}_${motionPhoto.videoNameTitle}`;
this.saveMediaFile(collectionPath, videoUID, videoStream);
this.saveMetadataFile(collectionPath, videoUID, file.metadata);
}
private saveMediaFile(collectionPath, uid, fileStream) {
this.ElectronAPIs.saveStreamToDisk(
`${collectionPath}/${uid}`,
fileStream
);
}
private saveMetadataFile(collectionPath, uid, metadata) {
this.ElectronAPIs.saveFileToDisk(
`${collectionPath}/${MetadataFolderName}/${uid}.json`,
getGoogleLikeMetadataFile(uid, file.metadata),
`${collectionPath}/${METADATA_FOLDER_NAME}/${uid}.json`,
getGoogleLikeMetadataFile(uid, metadata)
);
}
@ -268,6 +354,6 @@ class ExportService {
isExportInProgress = () => {
return this.exportInProgress !== null;
}
};
}
export default new ExportService();

View file

@ -19,6 +19,13 @@ export interface fileAttribute {
decryptionHeader: string;
}
export enum FILE_TYPE {
IMAGE,
VIDEO,
LIVE_PHOTO,
OTHERS,
}
export interface File {
id: number;
collectionID: number;
@ -43,7 +50,10 @@ export const getLocalFiles = async () => {
return files;
};
export const syncFiles = async (collections: Collection[], setFiles: (files: File[]) => void) => {
export const syncFiles = async (
collections: Collection[],
setFiles: (files: File[]) => void
) => {
const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) {
@ -54,11 +64,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
if (!getToken()) {
continue;
}
const lastSyncTime = (await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
const lastSyncTime =
(await localForage.getItem<number>(`${collection.id}-time`)) ?? 0;
if (collection.updationTime === lastSyncTime) {
continue;
}
const fetchedFiles = (await getFiles(collection, lastSyncTime, DIFF_LIMIT, files, setFiles)) ?? [];
const fetchedFiles =
(await getFiles(
collection,
lastSyncTime,
DIFF_LIMIT,
files,
setFiles
)) ?? [];
files.push(...fetchedFiles);
const latestVersionFiles = new Map<string, File>();
files.forEach((file) => {
@ -82,13 +100,15 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
await localForage.setItem('files', files);
await localForage.setItem(
`${collection.id}-time`,
collection.updationTime,
collection.updationTime
);
setFiles(files.map((item) => ({
setFiles(
files.map((item) => ({
...item,
w: window.innerWidth,
h: window.innerHeight,
})));
}))
);
}
return {
files: files.map((item) => ({
@ -104,11 +124,12 @@ export const getFiles = async (
sinceTime: number,
limit: number,
files: File[],
setFiles: (files: File[]) => void,
setFiles: (files: File[]) => void
): Promise<File[]> => {
try {
const decryptedFiles: File[] = [];
let time = sinceTime ||
let time =
sinceTime ||
(await localForage.getItem<number>(`${collection.id}-time`)) ||
0;
let resp;
@ -126,7 +147,7 @@ export const getFiles = async (
},
{
'X-Auth-Token': token,
},
}
);
decryptedFiles.push(
@ -136,16 +157,21 @@ export const getFiles = async (
file = await decryptFile(file, collection);
}
return file;
}) as Promise<File>[],
)),
}) as Promise<File>[]
))
);
if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime;
}
setFiles([...(files || []), ...decryptedFiles].filter((item) => !item.isDeleted).sort(
(a, b) => b.metadata.creationTime - a.metadata.creationTime,
));
setFiles(
[...(files || []), ...decryptedFiles]
.filter((item) => !item.isDeleted)
.sort(
(a, b) =>
b.metadata.creationTime - a.metadata.creationTime
)
);
} while (resp.data.diff.length === limit);
return decryptedFiles;
} catch (e) {
@ -155,7 +181,7 @@ export const getFiles = async (
const removeDeletedCollectionFiles = async (
collections: Collection[],
files: File[],
files: File[]
) => {
const syncedCollectionIds = new Set<number>();
for (const collection of collections) {
@ -168,7 +194,7 @@ const removeDeletedCollectionFiles = async (
export const deleteFiles = async (
filesToDelete: number[],
clearSelection: Function,
syncWithRemote: Function,
syncWithRemote: Function
) => {
try {
const token = getToken();
@ -181,7 +207,7 @@ export const deleteFiles = async (
null,
{
'X-Auth-Token': token,
},
}
);
clearSelection();
syncWithRemote();

View file

@ -0,0 +1,34 @@
import JSZip from 'jszip';
import { fileExtensionWithDot } from 'utils/file';
class MotionPhoto {
image: Uint8Array;
video: Uint8Array;
imageNameTitle: String;
videoNameTitle: String;
}
export const decodeMotionPhoto = async (
zipBlob: Blob,
originalName: string
) => {
const zip = await JSZip.loadAsync(zipBlob, { createFolders: true });
const motionPhoto = new MotionPhoto();
for (const zipFilename in zip.files) {
if (zipFilename.startsWith('image')) {
motionPhoto.imageNameTitle =
originalName + fileExtensionWithDot(zipFilename);
motionPhoto.image = await zip.files[zipFilename].async(
'uint8array'
);
} else if (zipFilename.startsWith('video')) {
motionPhoto.videoNameTitle =
originalName + fileExtensionWithDot(zipFilename);
motionPhoto.video = await zip.files[zipFilename].async(
'uint8array'
);
}
}
return motionPhoto;
};

View file

@ -31,14 +31,15 @@ export function parseHumanDate(humanDate: string): DateValue[] {
return dates.reverse();
}
return dates;
} if (date1) {
}
if (date1) {
return [{ month: date1.getMonth() }];
}
return [];
}
export async function searchLocation(
searchPhrase: string,
searchPhrase: string
): Promise<LocationSearchResponse[]> {
const resp = await HTTPService.get(
`${ENDPOINT}/search/location`,
@ -48,7 +49,7 @@ export async function searchLocation(
},
{
'X-Auth-Token': getToken(),
},
}
);
return resp.data.results;
}
@ -75,7 +76,9 @@ export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
value: { month: 11, date: 31 },
type: SuggestionType.DATE,
},
].filter((suggestion) => suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase()));
].filter((suggestion) =>
suggestion.label.toLowerCase().includes(searchPhrase.toLowerCase())
);
}
export function getYearSuggestion(searchPhrase: string): Suggestion[] {

View file

@ -1,15 +1,11 @@
import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from './HTTPService';
import EXIF from 'exif-js';
import { File, fileAttribute } from './fileService';
import { File, fileAttribute, FILE_TYPE } from './fileService';
import { Collection } from './collectionService';
import { FILE_TYPE, SetFiles } from 'pages/gallery';
import { SetFiles } from 'pages/gallery';
import { retryAsyncFunction, sleep } from 'utils/common';
import {
handleError,
parseError,
THUMBNAIL_GENERATION_FAILED,
} from 'utils/common/errorUtil';
import { handleError, parseError, CustomError } from 'utils/common/errorUtil';
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
import * as convert from 'xml-js';
import { ENCRYPTION_CHUNK_SIZE } from 'types';
@ -46,10 +42,12 @@ const FILE_UPLOAD_COMPLETED = 100;
const EDITED_FILE_SUFFIX = '-edited';
const TwoSecondInMillSeconds = 2000;
export enum FileUploadErrorCode {
export enum FileUploadResults {
FAILED = -1,
SKIPPED = -2,
UNSUPPORTED = -3,
BLOCKED = -4,
UPLOADED = 100,
}
interface Location {
@ -142,6 +140,7 @@ class UploadService {
private filesCompleted: number;
private totalFileCount: number;
private fileProgress: Map<string, number>;
private uploadResult: Map<string, number>;
private metadataMap: Map<string, Object>;
private filesToBeUploaded: FileWithCollection[];
private progressBarProps;
@ -153,18 +152,20 @@ class UploadService {
filesWithCollectionToUpload: FileWithCollection[],
existingFiles: File[],
progressBarProps,
setFiles:SetFiles,
setFiles: SetFiles
) {
try {
progressBarProps.setUploadStage(UPLOAD_STAGES.START);
this.filesCompleted = 0;
this.fileProgress = new Map<string, number>();
this.uploadResult = new Map<string, number>();
this.failedFiles = [];
this.metadataMap = new Map<string, object>();
this.progressBarProps = progressBarProps;
this.existingFiles = existingFiles;
this.existingFilesCollectionWise = sortFilesIntoCollections(existingFiles);
this.existingFilesCollectionWise =
sortFilesIntoCollections(existingFiles);
this.updateProgressBarUI();
this.setFiles = setFiles;
const metadataFiles: globalThis.File[] = [];
@ -184,7 +185,7 @@ class UploadService {
this.filesToBeUploaded = actualFiles;
progressBarProps.setUploadStage(
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES,
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
);
this.totalFileCount = metadataFiles.length;
this.perFileProgress = 100 / metadataFiles.length;
@ -211,11 +212,7 @@ class UploadService {
}
}
const uploadProcesses = [];
for (
let i = 0;
i < MAX_CONCURRENT_UPLOADS;
i++
) {
for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
if (this.filesToBeUploaded.length > 0) {
const fileWithCollection = this.filesToBeUploaded.pop();
this.cryptoWorkers[i] = getDedicatedCryptoWorker();
@ -223,8 +220,8 @@ class UploadService {
this.uploader(
await new this.cryptoWorkers[i].comlink(),
new FileReader(),
fileWithCollection,
),
fileWithCollection
)
);
}
}
@ -246,75 +243,97 @@ class UploadService {
private async uploader(
worker: any,
reader: FileReader,
fileWithCollection: FileWithCollection,
fileWithCollection: FileWithCollection
) {
const { file: rawFile, collection } = fileWithCollection;
this.fileProgress.set(rawFile.name, 0);
this.updateProgressBarUI();
let file: FileInMemory = null;
let encryptedFile: EncryptedFile = null;
try {
let file: FileInMemory = await this.readFile(reader, rawFile);
// read the file into memory
file = await this.readFile(reader, rawFile);
if (this.fileAlreadyInCollection(file, collection)) {
// set progress to -2 indicating that file upload was skipped
this.fileProgress.set(rawFile.name, FileUploadErrorCode.SKIPPED);
this.fileProgress.set(rawFile.name, FileUploadResults.SKIPPED);
this.updateProgressBarUI();
await sleep(TwoSecondInMillSeconds);
// remove completed files for file progress list
this.fileProgress.delete(rawFile.name);
} else {
let encryptedFile: EncryptedFile =
await this.encryptFile(worker, file, collection.key);
encryptedFile = await this.encryptFile(
worker,
file,
collection.key
);
let backupedFile: BackupedFile = await this.uploadToBucket(
encryptedFile.file,
const backupedFile: BackupedFile = await this.uploadToBucket(
encryptedFile.file
);
let uploadFile: uploadFile = this.getUploadFile(
collection,
backupedFile,
encryptedFile.fileKey,
encryptedFile.fileKey
);
encryptedFile = null;
backupedFile = null;
const uploadedFile = await this.uploadFile(uploadFile);
const decryptedFile=await decryptFile(uploadedFile, collection);
const decryptedFile = await decryptFile(
uploadedFile,
collection
);
this.existingFiles.push(decryptedFile);
this.existingFiles = sortFiles(this.existingFiles);
await localForage.setItem('files', removeUnneccessaryFileProps(this.existingFiles));
await localForage.setItem(
'files',
removeUnneccessaryFileProps(this.existingFiles)
);
this.setFiles(this.existingFiles);
file = null;
uploadFile = null;
this.fileProgress.delete(rawFile.name);
this.fileProgress.set(rawFile.name, FileUploadResults.UPLOADED);
this.filesCompleted++;
}
} catch (e) {
logError(e, 'file upload failed');
this.failedFiles.push(fileWithCollection);
// set progress to -1 indicating that file upload failed but keep it to show in the file-upload list progress
this.fileProgress.set(rawFile.name, FileUploadErrorCode.FAILED);
handleError(e);
this.failedFiles.push(fileWithCollection);
if (e.message === CustomError.ETAG_MISSING) {
this.fileProgress.set(rawFile.name, FileUploadResults.BLOCKED);
} else {
this.fileProgress.set(rawFile.name, FileUploadResults.FAILED);
}
} finally {
file = null;
encryptedFile = null;
}
this.uploadResult.set(
rawFile.name,
this.fileProgress.get(rawFile.name)
);
this.fileProgress.delete(rawFile.name);
this.updateProgressBarUI();
if (this.filesToBeUploaded.length > 0) {
await this.uploader(
worker,
reader,
this.filesToBeUploaded.pop(),
);
await this.uploader(worker, reader, this.filesToBeUploaded.pop());
}
}
async retryFailedFiles(localFiles: File[]) {
await this.uploadFiles(this.failedFiles, localFiles, this.progressBarProps, this.setFiles);
await this.uploadFiles(
this.failedFiles,
localFiles,
this.progressBarProps,
this.setFiles
);
}
private updateProgressBarUI() {
const { setPercentComplete, setFileCounter, setFileProgress } =
this.progressBarProps;
const {
setPercentComplete,
setFileCounter,
setFileProgress,
setUploadResult,
} = this.progressBarProps;
setFileCounter({
finished: this.filesCompleted,
total: this.totalFileCount,
@ -332,11 +351,12 @@ class UploadService {
}
setPercentComplete(percentComplete);
setFileProgress(this.fileProgress);
setUploadResult(this.uploadResult);
}
private fileAlreadyInCollection(
newFile: FileInMemory,
collection: Collection,
collection: Collection
): boolean {
const collectionFiles =
this.existingFilesCollectionWise.get(collection.id) ?? [];
@ -349,7 +369,7 @@ class UploadService {
}
private areFilesSame(
existingFile: MetadataObject,
newFile: MetadataObject,
newFile: MetadataObject
): boolean {
if (
existingFile.fileType === newFile.fileType &&
@ -365,10 +385,8 @@ class UploadService {
private async readFile(reader: FileReader, receivedFile: globalThis.File) {
try {
const { thumbnail, hasStaticThumbnail } = await this.generateThumbnail(
reader,
receivedFile,
);
const { thumbnail, hasStaticThumbnail } =
await this.generateThumbnail(reader, receivedFile);
let fileType: FILE_TYPE;
switch (receivedFile.type.split('/')[0]) {
@ -392,13 +410,13 @@ class UploadService {
const { location, creationTime } = await this.getExifData(
reader,
receivedFile,
fileType,
fileType
);
let receivedFileOriginalName = receivedFile.name;
if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) {
receivedFileOriginalName = receivedFile.name.slice(
0,
-1 * EDITED_FILE_SUFFIX.length,
-1 * EDITED_FILE_SUFFIX.length
);
}
const metadata = Object.assign(
@ -411,15 +429,15 @@ class UploadService {
longitude: location?.latitude,
fileType,
},
this.metadataMap.get(receivedFileOriginalName),
this.metadataMap.get(receivedFileOriginalName)
);
if (hasStaticThumbnail) {
metadata['hasStaticThumbnail'] = hasStaticThumbnail;
}
const filedata =
receivedFile.size > MIN_STREAM_FILE_SIZE ?
this.getFileStream(reader, receivedFile) :
await this.getUint8ArrayView(reader, receivedFile);
receivedFile.size > MIN_STREAM_FILE_SIZE
? this.getFileStream(reader, receivedFile)
: await this.getUint8ArrayView(reader, receivedFile);
return {
filedata,
@ -435,13 +453,13 @@ class UploadService {
private async encryptFile(
worker: any,
file: FileInMemory,
encryptionKey: string,
encryptionKey: string
): Promise<EncryptedFile> {
try {
const { key: fileKey, file: encryptedFiledata }: EncryptionResult =
isDataStream(file.filedata) ?
await this.encryptFileStream(worker, file.filedata) :
await worker.encryptFile(file.filedata);
isDataStream(file.filedata)
? await this.encryptFileStream(worker, file.filedata)
: await worker.encryptFile(file.filedata);
const { file: encryptedThumbnail }: EncryptionResult =
await worker.encryptThumbnail(file.thumbnail, fileKey);
@ -450,7 +468,7 @@ class UploadService {
const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
fileKey,
encryptionKey,
encryptionKey
);
const result: EncryptedFile = {
@ -481,7 +499,7 @@ class UploadService {
const encryptedFileChunk = await worker.encryptFileChunk(
value,
pushState,
ref.pullCount === chunkCount,
ref.pullCount === chunkCount
);
controller.enqueue(encryptedFileChunk);
if (ref.pullCount === chunkCount) {
@ -505,30 +523,30 @@ class UploadService {
if (isDataStream(file.file.encryptedData)) {
const { chunkCount, stream } = file.file.encryptedData;
const uploadPartCount = Math.ceil(
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD,
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD
);
const filePartUploadURLs = await this.fetchMultipartUploadURLs(
uploadPartCount,
uploadPartCount
);
fileObjectKey = await this.putFileInParts(
filePartUploadURLs,
stream,
file.filename,
uploadPartCount,
uploadPartCount
);
} else {
const fileUploadURL = await this.getUploadURL();
fileObjectKey = await this.putFile(
fileUploadURL,
file.file.encryptedData,
file.filename,
file.filename
);
}
const thumbnailUploadURL = await this.getUploadURL();
const thumbnailObjectKey = await this.putFile(
thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array,
null,
null
);
const backupedFile: BackupedFile = {
@ -552,7 +570,7 @@ class UploadService {
private getUploadFile(
collection: Collection,
backupedFile: BackupedFile,
fileKey: B64EncryptionResult,
fileKey: B64EncryptionResult
): uploadFile {
const uploadFile: uploadFile = {
collectionID: collection.id,
@ -570,14 +588,11 @@ class UploadService {
if (!token) {
return;
}
const response = await retryAsyncFunction(()=>HTTPService.post(
`${ENDPOINT}/files`,
uploadFile,
null,
{
const response = await retryAsyncFunction(() =>
HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
'X-Auth-Token': token,
},
));
})
);
return response.data;
} catch (e) {
logError(e, 'upload Files Failed');
@ -590,17 +605,19 @@ class UploadService {
const metadataJSON: object = await new Promise(
(resolve, reject) => {
const reader = new FileReader();
reader.onabort = () => reject(Error('file reading was aborted'));
reader.onerror = () => reject(Error('file reading has failed'));
reader.onabort = () =>
reject(Error('file reading was aborted'));
reader.onerror = () =>
reject(Error('file reading has failed'));
reader.onload = () => {
const result =
typeof reader.result !== 'string' ?
new TextDecoder().decode(reader.result) :
reader.result;
typeof reader.result !== 'string'
? new TextDecoder().decode(reader.result)
: reader.result;
resolve(JSON.parse(result));
};
reader.readAsText(receivedFile);
},
}
);
const metaDataObject = {};
@ -647,8 +664,8 @@ class UploadService {
}
private async generateThumbnail(
reader: FileReader,
file: globalThis.File,
): Promise<{ thumbnail: Uint8Array, hasStaticThumbnail: boolean }> {
file: globalThis.File
): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
try {
let hasStaticThumbnail = false;
const canvas = document.createElement('canvas');
@ -662,7 +679,7 @@ class UploadService {
file = new globalThis.File(
[await convertHEIC2JPEG(file)],
null,
null,
null
);
}
let image = new Image();
@ -672,7 +689,8 @@ class UploadService {
image.onload = () => {
try {
const thumbnailWidth =
(image.width * THUMBNAIL_HEIGHT) / image.height;
(image.width * THUMBNAIL_HEIGHT) /
image.height;
canvas.width = thumbnailWidth;
canvas.height = THUMBNAIL_HEIGHT;
canvas_CTX.drawImage(
@ -680,7 +698,7 @@ class UploadService {
0,
0,
thumbnailWidth,
THUMBNAIL_HEIGHT,
THUMBNAIL_HEIGHT
);
image = null;
clearTimeout(timeout);
@ -688,15 +706,23 @@ class UploadService {
} catch (e) {
reject(e);
logError(e);
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
reject(
Error(
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
)
);
}
};
timeout = setTimeout(
() =>
reject(
Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`),
Error(
`wait time exceeded for format ${
file.name.split('.').slice(-1)[0]
}`
)
),
WAIT_TIME_THUMBNAIL_GENERATION,
WAIT_TIME_THUMBNAIL_GENERATION
);
});
} else {
@ -718,7 +744,7 @@ class UploadService {
0,
0,
thumbnailWidth,
THUMBNAIL_HEIGHT,
THUMBNAIL_HEIGHT
);
video = null;
clearTimeout(timeout);
@ -726,16 +752,26 @@ class UploadService {
} catch (e) {
reject(e);
logError(e);
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`));
reject(
Error(
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
)
);
}
});
video.preload = 'metadata';
video.src = imageURL;
video.currentTime = 3;
setTimeout(
timeout = setTimeout(
() =>
reject(Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`)),
WAIT_TIME_THUMBNAIL_GENERATION,
reject(
Error(
`wait time exceeded for format ${
file.name.split('.').slice(-1)[0]
}`
)
),
WAIT_TIME_THUMBNAIL_GENERATION
);
});
}
@ -758,7 +794,7 @@ class UploadService {
resolve(blob);
},
'image/jpeg',
quality,
quality
);
});
thumbnailBlob = thumbnailBlob ?? new Blob([]);
@ -768,7 +804,7 @@ class UploadService {
);
const thumbnail = await this.getUint8ArrayView(
reader,
thumbnailBlob,
thumbnailBlob
);
return { thumbnail, hasStaticThumbnail };
} catch (e) {
@ -781,7 +817,7 @@ class UploadService {
const self = this;
const fileChunkReader = (async function* fileChunkReaderMaker(
fileSize,
self,
self
) {
let offset = 0;
while (offset < fileSize) {
@ -809,18 +845,19 @@ class UploadService {
private async getUint8ArrayView(
reader: FileReader,
file: Blob,
file: Blob
): Promise<Uint8Array> {
try {
return await new Promise((resolve, reject) => {
reader.onabort = () => reject(Error('file reading was aborted'));
reader.onabort = () =>
reject(Error('file reading was aborted'));
reader.onerror = () => reject(Error('file reading has failed'));
reader.onload = () => {
// Do whatever you want with the file contents
const result =
typeof reader.result === 'string' ?
new TextEncoder().encode(reader.result) :
new Uint8Array(reader.result);
typeof reader.result === 'string'
? new TextEncoder().encode(reader.result)
: new Uint8Array(reader.result);
resolve(result);
};
reader.readAsArrayBuffer(file);
@ -851,10 +888,10 @@ class UploadService {
{
count: Math.min(
MAX_URL_REQUESTS,
(this.totalFileCount - this.filesCompleted) * 2,
(this.totalFileCount - this.filesCompleted) * 2
),
},
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
const response = await this.uploadURLFetchInProgress;
this.uploadURLs.push(...response.data['urls']);
@ -870,7 +907,7 @@ class UploadService {
}
private async fetchMultipartUploadURLs(
count: number,
count: number
): Promise<MultipartUploadURLs> {
try {
const token = getToken();
@ -882,7 +919,7 @@ class UploadService {
{
count,
},
{ 'X-Auth-Token': token },
{ 'X-Auth-Token': token }
);
return response.data['urls'];
@ -895,7 +932,7 @@ class UploadService {
private async putFile(
fileUploadURL: UploadURL,
file: Uint8Array,
filename: string,
filename: string
): Promise<string> {
try {
await retryAsyncFunction(() =>
@ -904,8 +941,8 @@ class UploadService {
file,
null,
null,
this.trackUploadProgress(filename),
),
this.trackUploadProgress(filename)
)
);
return fileUploadURL.objectKey;
} catch (e) {
@ -918,12 +955,12 @@ class UploadService {
multipartUploadURLs: MultipartUploadURLs,
file: ReadableStream<Uint8Array>,
filename: string,
uploadPartCount: number,
uploadPartCount: number
) {
try {
const streamEncryptedFileReader = file.getReader();
const percentPerPart = Math.round(
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount,
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount
);
const resParts = [];
for (const [
@ -942,15 +979,25 @@ class UploadService {
}
}
const uploadChunk = Uint8Array.from(combinedChunks);
const response = await retryAsyncFunction(()=>
HTTPService.put(
const response = await retryAsyncFunction(async () => {
const resp = await HTTPService.put(
fileUploadURL,
uploadChunk,
null,
null,
this.trackUploadProgress(filename, percentPerPart, index),
),
this.trackUploadProgress(
filename,
percentPerPart,
index
)
);
if (!resp?.headers?.etag) {
const err = Error(CustomError.ETAG_MISSING);
logError(err);
throw err;
}
return resp;
});
resParts.push({
PartNumber: index + 1,
ETag: response.headers.etag,
@ -959,12 +1006,12 @@ class UploadService {
const options = { compact: true, ignoreComment: true, spaces: 4 };
const body = convert.js2xml(
{ CompleteMultipartUpload: { Part: resParts } },
options,
options
);
await retryAsyncFunction(() =>
HTTPService.post(multipartUploadURLs.completeURL, body, null, {
'content-type': 'text/xml',
}),
})
);
return multipartUploadURLs.objectKey;
} catch (e) {
@ -976,7 +1023,7 @@ class UploadService {
private trackUploadProgress(
filename,
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
index = 0,
index = 0
) {
const cancel = { exec: null };
let timeout = null;
@ -996,10 +1043,10 @@ class UploadService {
Math.round(
percentPerPart * index +
(percentPerPart * event.loaded) /
event.total,
),
98,
event.total
),
98
)
);
this.updateProgressBarUI();
if (event.loaded === event.total) {
@ -1013,7 +1060,7 @@ class UploadService {
private async getExifData(
reader: FileReader,
receivedFile: globalThis.File,
fileType: FILE_TYPE,
fileType: FILE_TYPE
): Promise<ParsedEXIFData> {
try {
if (fileType === FILE_TYPE.VIDEO) {
@ -1039,7 +1086,8 @@ class UploadService {
}
}
private getUNIXTime(exifData: any) {
const dateString: string = exifData.DateTimeOriginal || exifData.DateTime;
const dateString: string =
exifData.DateTimeOriginal || exifData.DateTime;
if (!dateString || dateString === '0000:00:00 00:00:00') {
return null;
}
@ -1047,7 +1095,7 @@ class UploadService {
const date = new Date(
Number(parts[0]),
Number(parts[1]) - 1,
Number(parts[2]),
Number(parts[2])
);
return date.getTime() * 1000;
}
@ -1072,14 +1120,14 @@ class UploadService {
latDegree,
latMinute,
latSecond,
latDirection,
latDirection
);
const lonFinal = this.convertDMSToDD(
lonDegree,
lonMinute,
lonSecond,
lonDirection,
lonDirection
);
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 };
}

View file

@ -6,8 +6,9 @@ import { clearData } from 'utils/storage/localStorage';
import localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key';
import HTTPService from './HTTPService';
import { B64EncryptionResult } from './uploadService';
import { B64EncryptionResult } from 'utils/crypto';
import { logError } from 'utils/sentry';
import { Subscription } from './billingService';
export interface UpdatedKey {
kekSalt: string;
@ -35,7 +36,7 @@ export interface EmailVerificationResponse {
keyAttributes?: KeyAttributes;
encryptedToken?: string;
token?: string;
twoFactorSessionID: string
twoFactorSessionID: string;
}
export interface TwoFactorVerificationResponse {
@ -46,16 +47,25 @@ export interface TwoFactorVerificationResponse {
}
export interface TwoFactorSecret {
secretCode: string
qrCode: string
secretCode: string;
qrCode: string;
}
export interface TwoFactorRecoveryResponse {
encryptedSecret: string
secretDecryptionNonce: string
encryptedSecret: string;
secretDecryptionNonce: string;
}
export const getOtt = (email: string) => HTTPService.get(`${ENDPOINT}/users/ott`, {
export interface UserDetails {
email: string;
usage: number;
fileCount: number;
sharedCollectionCount: number;
subscription: Subscription;
}
export const getOtt = (email: string) =>
HTTPService.get(`${ENDPOINT}/users/ott`, {
email,
client: 'web',
});
@ -67,7 +77,7 @@ export const getPublicKey = async (email: string) => {
{ email },
{
'X-Auth-Token': token,
},
}
);
return resp.data.publicKey;
};
@ -80,34 +90,28 @@ export const getPaymentToken = async () => {
null,
{
'X-Auth-Token': token,
},
}
);
return resp.data['paymentToken'];
};
export const verifyOtt = (email: string, ott: string) => HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
export const verifyOtt = (email: string, ott: string) =>
HTTPService.post(`${ENDPOINT}/users/verify-email`, { email, ott });
export const putAttributes = (token: string, keyAttributes: KeyAttributes) => HTTPService.put(
`${ENDPOINT}/users/attributes`,
{ keyAttributes },
null,
{
'X-Auth-Token': token,
},
);
export const setKeys = (token: string, updatedKey: UpdatedKey) => HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
'X-Auth-Token': token,
});
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) => HTTPService.put(
`${ENDPOINT}/users/recovery-key`,
recoveryKey,
null,
{
export const setKeys = (token: string, updatedKey: UpdatedKey) =>
HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
'X-Auth-Token': token,
},
);
});
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) =>
HTTPService.put(`${ENDPOINT}/users/recovery-key`, recoveryKey, null, {
'X-Auth-Token': token,
});
export const logoutUser = async () => {
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry
@ -135,26 +139,46 @@ export const isTokenValid = async () => {
};
export const setupTwoFactor = async () => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/setup`, null, null, {
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/setup`,
null,
null,
{
'X-Auth-Token': getToken(),
});
}
);
return resp.data as TwoFactorSecret;
};
export const enableTwoFactor = async (code: string, recoveryEncryptedTwoFactorSecret: B64EncryptionResult) => {
await HTTPService.post(`${ENDPOINT}/users/two-factor/enable`, {
export const enableTwoFactor = async (
code: string,
recoveryEncryptedTwoFactorSecret: B64EncryptionResult
) => {
await HTTPService.post(
`${ENDPOINT}/users/two-factor/enable`,
{
code,
encryptedTwoFactorSecret: recoveryEncryptedTwoFactorSecret.encryptedData,
twoFactorSecretDecryptionNonce: recoveryEncryptedTwoFactorSecret.nonce,
}, null, {
encryptedTwoFactorSecret:
recoveryEncryptedTwoFactorSecret.encryptedData,
twoFactorSecretDecryptionNonce:
recoveryEncryptedTwoFactorSecret.nonce,
},
null,
{
'X-Auth-Token': getToken(),
});
}
);
};
export const verifyTwoFactor = async (code: string, sessionID: string) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, {
code, sessionID,
}, null);
const resp = await HTTPService.post(
`${ENDPOINT}/users/two-factor/verify`,
{
code,
sessionID,
},
null
);
return resp.data as TwoFactorVerificationResponse;
};
@ -167,7 +191,8 @@ export const recoverTwoFactor = async (sessionID: string) => {
export const removeTwoFactor = async (sessionID: string, secret: string) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
sessionID, secret,
sessionID,
secret,
});
return resp.data as TwoFactorVerificationResponse;
};
@ -179,9 +204,13 @@ export const disableTwoFactor = async () => {
};
export const getTwoFactorStatus = async () => {
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/status`, null, {
const resp = await HTTPService.get(
`${ENDPOINT}/users/two-factor/status`,
null,
{
'X-Auth-Token': getToken(),
});
}
);
return resp.data['status'];
};
@ -197,3 +226,40 @@ export const _logout = async () => {
return false;
}
};
export const getOTTForEmailChange = async (email: string) => {
if (!getToken()) {
return null;
}
await HTTPService.get(`${ENDPOINT}/users/ott`, {
email,
client: 'web',
purpose: 'change',
});
};
export const changeEmail = async (email: string, ott: string) => {
if (!getToken()) {
return null;
}
await HTTPService.post(
`${ENDPOINT}/users/change-email`,
{
email,
ott,
},
null,
{
'X-Auth-Token': getToken(),
}
);
};
export const getUserDetails = async (): Promise<UserDetails> => {
const token = getToken();
const resp = await HTTPService.get(`${ENDPOINT}/users/details`, null, {
'X-Auth-Token': token,
});
return resp.data['details'];
};

View file

@ -17,6 +17,7 @@ export const ENCRYPTION_CHUNK_SIZE = 4 * 1024 * 1024;
export const GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48;
export const IMAGE_CONTAINER_MAX_HEIGHT = 200;
export const IMAGE_CONTAINER_MAX_WIDTH = IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
export const IMAGE_CONTAINER_MAX_WIDTH =
IMAGE_CONTAINER_MAX_HEIGHT - GAP_BTW_TILES;
export const MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44;

View file

@ -9,7 +9,7 @@ import { NextRouter } from 'next/router';
import { SetDialogMessage } from 'components/MessageDialog';
import { SetLoading } from 'pages/gallery';
import { getData, LS_KEYS } from './storage/localStorage';
import { SUBSCRIPTION_VERIFICATION_ERROR } from './common/errorUtil';
import { CustomError } from './common/errorUtil';
const STRIPE = 'stripe';
@ -89,7 +89,7 @@ export async function updateSubscription(
plan: Plan,
setDialogMessage: SetDialogMessage,
setLoading: SetLoading,
closePlanSelectorModal: () => null,
closePlanSelectorModal: () => null
) {
try {
setLoading(true);
@ -99,7 +99,7 @@ export async function updateSubscription(
setDialogMessage({
title: constants.SUCCESS,
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
getUserSubscription().expiryTime,
getUserSubscription().expiryTime
),
close: { variant: 'success' },
});
@ -117,13 +117,13 @@ export async function updateSubscription(
null,
setDialogMessage,
setLoading,
setLoading
),
},
close: { text: constants.CANCEL },
});
break;
case SUBSCRIPTION_VERIFICATION_ERROR:
case CustomError.SUBSCRIPTION_VERIFICATION_ERROR:
setDialogMessage({
title: constants.ERROR,
content: constants.SUBSCRIPTION_VERIFICATION_FAILED,
@ -146,7 +146,7 @@ export async function updateSubscription(
export async function cancelSubscription(
setDialogMessage: SetDialogMessage,
closePlanSelectorModal: () => null,
setLoading: SetLoading,
setLoading: SetLoading
) {
try {
setLoading(true);
@ -171,7 +171,7 @@ export async function cancelSubscription(
export async function activateSubscription(
setDialogMessage: SetDialogMessage,
closePlanSelectorModal: () => null,
setLoading: SetLoading,
setLoading: SetLoading
) {
try {
setLoading(true);
@ -195,7 +195,7 @@ export async function activateSubscription(
export async function updatePaymentMethod(
setDialogMessage: SetDialogMessage,
setLoading: SetLoading,
setLoading: SetLoading
) {
try {
setLoading(true);
@ -213,7 +213,7 @@ export async function updatePaymentMethod(
export async function checkSubscriptionPurchase(
setDialogMessage: SetDialogMessage,
router: NextRouter,
router: NextRouter
) {
try {
const urlParams = new URLSearchParams(window.location.search);
@ -227,19 +227,19 @@ export async function checkSubscriptionPurchase(
} else if (sessionId) {
try {
const subscription = await billingService.verifySubscription(
sessionId,
sessionId
);
setDialogMessage({
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
close: { variant: 'success' },
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
subscription?.expiryTime,
subscription?.expiryTime
),
});
} catch (e) {
setDialogMessage({
title: constants.ERROR,
content: SUBSCRIPTION_VERIFICATION_ERROR,
content: CustomError.SUBSCRIPTION_VERIFICATION_ERROR,
close: {},
});
}
@ -250,3 +250,18 @@ export async function checkSubscriptionPurchase(
router.push('gallery', undefined, { shallow: true });
}
}
export function planForSubscription(subscription: Subscription) {
if (!subscription) {
return null;
}
return {
id: subscription.productID,
storage: subscription.storage,
price: subscription.price,
period: subscription.period,
stripeID: subscription.productID,
iosID: subscription.productID,
androidID: subscription.productID,
};
}

View file

@ -15,14 +15,14 @@ export async function addFilesToCollection(
syncWithRemote: () => Promise<void>,
selectCollection: (id: number) => void,
collectionName: string,
existingCollection: Collection,
existingCollection: Collection
) {
setCollectionSelectorView(false);
let collection;
if (!existingCollection) {
collection = await createCollection(
collectionName,
CollectionType.album,
CollectionType.album
);
} else {
collection = existingCollection;

View file

@ -1,18 +1,25 @@
export const getEndpoint = () => {
const endPoint = process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? 'https://api.ente.io';
const endPoint =
process.env.NEXT_PUBLIC_ENTE_ENDPOINT ?? 'https://api.ente.io';
return endPoint;
};
export const getFileUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
return `${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/download/${id}` ?? `https://api.ente.io/files/download/${id}`;
return (
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/download/${id}` ??
'https://api.ente.io'
);
}
return `https://files.ente.workers.dev/?fileID=${id}`;
};
export const getThumbnailUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) {
return `${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/preview/${id}` ?? `https://api.ente.io/files/preview/${id}`;
return (
`${process.env.NEXT_PUBLIC_ENTE_ENDPOINT}/files/preview/${id}` ??
'https://api.ente.io'
);
}
return `https://thumbnails.ente.workers.dev/?fileID=${id}`;
};

View file

@ -1,45 +1,43 @@
import constants from 'utils/strings/constants';
export const errorCodes = {
ERR_STORAGE_LIMIT_EXCEEDED: '426',
ERR_NO_ACTIVE_SUBSCRIPTION: '402',
ERR_NO_INTERNET_CONNECTION: '1',
ERR_SESSION_EXPIRED: '401',
ERR_KEY_MISSING: '2',
ERR_FORBIDDEN: '403',
export const ServerErrorCodes = {
SESSION_EXPIRED: '401',
NO_ACTIVE_SUBSCRIPTION: '402',
FORBIDDEN: '403',
STORAGE_LIMIT_EXCEEDED: '426',
};
export const SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed';
export const THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed';
export const VIDEO_PLAYBACK_FAILED = 'video playback failed';
export const CustomError = {
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
VIDEO_PLAYBACK_FAILED: 'video playback failed',
ETAG_MISSING: 'no header/etag present in response body',
KEY_MISSING: 'encrypted key missing from localStorage',
};
export function parseError(error) {
let errorMessage = null;
let parsedMessage = null;
if (error?.status) {
const errorCode = error.status.toString();
switch (errorCode) {
case errorCodes.ERR_NO_ACTIVE_SUBSCRIPTION:
errorMessage = constants.SUBSCRIPTION_EXPIRED;
case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
parsedMessage = constants.SUBSCRIPTION_EXPIRED;
break;
case errorCodes.ERR_STORAGE_LIMIT_EXCEEDED:
errorMessage = constants.STORAGE_QUOTA_EXCEEDED;
case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
parsedMessage = constants.STORAGE_QUOTA_EXCEEDED;
break;
case errorCodes.ERR_NO_INTERNET_CONNECTION:
errorMessage = constants.NO_INTERNET_CONNECTION;
break;
case errorCodes.ERR_SESSION_EXPIRED:
errorMessage = constants.SESSION_EXPIRED_MESSAGE;
case ServerErrorCodes.SESSION_EXPIRED:
parsedMessage = constants.SESSION_EXPIRED_MESSAGE;
break;
}
}
if (errorMessage) {
return { parsedError: new Error(errorMessage), parsed: true };
if (parsedMessage) {
return { parsedError: new Error(parsedMessage), parsed: true };
} else {
return ({
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`), parsed: false,
});
return {
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`),
parsed: false,
};
}
}
@ -48,6 +46,6 @@ export function handleError(error) {
if (parsed) {
throw parsedError;
} else {
// shallow error don't break the caller flow
// swallow error don't break the caller flow
}
}

View file

@ -1,6 +1,7 @@
import constants from 'utils/strings/constants';
const DESKTOP_APP_DOWNLOAD_URL = 'https://github.com/ente-io/bhari-frame/releases/';
export const DESKTOP_APP_DOWNLOAD_URL =
'https://github.com/ente-io/bhari-frame/releases/latest';
const retrySleepTime = [2000, 5000, 10000];
@ -32,7 +33,10 @@ export function reverseString(title: string) {
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`);
}
export async function retryAsyncFunction(func: ()=>Promise<any>, retryCount: number = 3) {
export async function retryAsyncFunction(
func: () => Promise<any>,
retryCount: number = 3
) {
try {
const resp = await func();
return resp;

Some files were not shown because too many files have changed in this diff Show more