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

1
.husky/.gitignore vendored
View file

@ -1 +0,0 @@
_

View file

@ -1,4 +1,4 @@
#!/bin/sh #!/bin/sh
. "$(dirname "$0")/_/husky.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 ## 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 ```bash
npm run dev npm run dev

View file

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

View file

@ -1,13 +1,12 @@
import * as Sentry from '@sentry/nextjs'; import * as Sentry from '@sentry/nextjs';
import { getSentryTunnelUrl } from 'utils/common/apiUtil'; 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_DSN = process.env.NEXT_PUBLIC_SENTRY_DSN ?? 'https://860186db60c54c7fbacfe255124958e8@errors.ente.io/4';
const SENTRY_ENV = process.env.NEXT_PUBLIC_SENTRY_ENV ?? 'development'; 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({ Sentry.init({
dsn: SENTRY_DSN, dsn: SENTRY_DSN,
enabled: SENTRY_ENV !== 'development', enabled: SENTRY_ENV !== 'development',

View file

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

View file

@ -31,26 +31,27 @@ export const IconButton = styled.button`
align-items: center; align-items: center;
justify-content: center; justify-content: center;
&:focus, &:hover { &:focus,
&:hover {
background-color: rgba(255, 255, 255, 0.2); background-color: rgba(255, 255, 255, 0.2);
} }
`; `;
export const Row = styled.div` export const Row = styled.div`
display:flex; display: flex;
align-items:center; align-items: center;
margin-bottom:20px; margin-bottom: 20px;
flex:1 flex: 1;
`; `;
export const Label = styled.div <{ width?: string }> ` export const Label = styled.div<{ width?: string }>`
width:${(props) => props.width ?? '70%'}; width: ${(props) => props.width ?? '70%'};
`; `;
export const Value = styled.div <{ width?: string }> ` export const Value = styled.div<{ width?: string }>`
display:flex; display: flex;
justify-content:flex-start; justify-content: flex-start;
align-items:center; align-items: center;
width:${(props) => props.width ?? '30%'}; width: ${(props) => props.width ?? '30%'};
text-align: center; text-align: center;
color: #ddd; color: #ddd;
`; `;

View file

@ -20,8 +20,7 @@ export default function DeleteBtn(props) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
viewBox={props.viewBox} viewBox={props.viewBox}
width={props.width} width={props.width}>
>
<path d="M0 0h24v24H0z" fill="none" /> <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" /> <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> </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 { Label, Row, Value } from './Container';
import { ComfySpan } from './ExportInProgress'; import { ComfySpan } from './ExportInProgress';
interface Props { interface Props {
show: boolean show: boolean;
onHide: () => void onHide: () => void;
exportFolder: string exportFolder: string;
exportSize: string exportSize: string;
lastExportTime: number lastExportTime: number;
exportStats: ExportStats exportStats: ExportStats;
updateExportFolder: (newFolder: string) => void; updateExportFolder: (newFolder: string) => void;
exportFiles: () => void; exportFiles: () => void;
retryFailed: () => void; retryFailed: () => void;
@ -23,30 +22,69 @@ export default function ExportFinished(props: Props) {
const totalFiles = props.exportStats.failed + props.exportStats.success; const totalFiles = props.exportStats.failed + props.exportStats.success;
return ( return (
<> <>
<div style={{ borderBottom: '1px solid #444', marginBottom: '20px', padding: '0 5%' }}> <div
style={{
borderBottom: '1px solid #444',
marginBottom: '20px',
padding: '0 5%',
}}>
<Row> <Row>
<Label width="40%">{constants.LAST_EXPORT_TIME}</Label> <Label width="40%">{constants.LAST_EXPORT_TIME}</Label>
<Value width="60%">{formatDateTime(props.lastExportTime)}</Value> <Value width="60%">
</Row> {formatDateTime(props.lastExportTime)}
<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> </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>
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}> <div
<Button block variant={'outline-secondary'} onClick={props.onHide}>{constants.CLOSE}</Button> style={{
width: '100%',
display: 'flex',
justifyContent: 'space-around',
}}>
<Button
block
variant={'outline-secondary'}
onClick={props.onHide}>
{constants.CLOSE}
</Button>
<div style={{ width: '30px' }} /> <div style={{ width: '30px' }} />
{props.exportStats.failed !== 0 ? {props.exportStats.failed !== 0 ? (
<Button block variant={'outline-danger'} onClick={props.retryFailed}>{constants.RETRY_EXPORT_}</Button> : <Button
<Button block variant={'outline-success'} onClick={props.exportFiles}>{constants.EXPORT_AGAIN}</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> </div>
</> </>
); );

View file

@ -5,42 +5,82 @@ import styled from 'styled-components';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export const ComfySpan = styled.span` export const ComfySpan = styled.span`
word-spacing:1rem; word-spacing: 1rem;
color:#ddd; color: #ddd;
`; `;
interface Props { interface Props {
show: boolean show: boolean;
onHide: () => void onHide: () => void;
exportFolder: string exportFolder: string;
exportSize: string exportSize: string;
exportStage: ExportStage exportStage: ExportStage;
exportProgress: ExportProgress exportProgress: ExportProgress;
resumeExport: () => void; resumeExport: () => void;
cancelExport: () => void cancelExport: () => void;
pauseExport: () => void; pauseExport: () => void;
} }
export default function ExportInProgress(props: Props) { export default function ExportInProgress(props: Props) {
return ( 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' }}> <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>
<div style={{ width: '100%', marginBottom: '30px' }}> <div style={{ width: '100%', marginBottom: '30px' }}>
<ProgressBar <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)} animated={!(props.exportStage === ExportStage.PAUSED)}
variant="upload-progress-bar" variant="upload-progress-bar"
/> />
</div> </div>
<div style={{ width: '100%', display: 'flex', justifyContent: 'space-around' }}> <div
{props.exportStage === ExportStage.PAUSED ? style={{
<Button block variant={'outline-secondary'} onClick={props.resumeExport}>{constants.RESUME}</Button> : width: '100%',
<Button block variant={'outline-secondary'} onClick={props.pauseExport}>{constants.PAUSE}</Button> 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' }} /> <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>
</div> </div>
</> </>

View file

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

View file

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

View file

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

View file

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

View file

@ -5,9 +5,9 @@ import CrossIcon from './icons/CrossIcon';
const CloseButtonWrapper = styled.div` const CloseButtonWrapper = styled.div`
position: absolute; position: absolute;
top:10px; top: 10px;
right:10px; right: 10px;
cursor:pointer; cursor: pointer;
`; `;
const DropDiv = styled.div` const DropDiv = styled.div`
flex: 1; flex: 1;
@ -62,14 +62,10 @@ export default function FullScreenDropZone(props: Props) {
e.preventDefault(); e.preventDefault();
props.showCollectionSelector(); props.showCollectionSelector();
}, },
})} })}>
>
<input {...props.getInputProps()} /> <input {...props.getInputProps()} />
{isDragActive && ( {isDragActive && (
<Overlay <Overlay onDrop={onDragLeave} onDragLeave={onDragLeave}>
onDrop={onDragLeave}
onDragLeave={onDragLeave}
>
<CloseButtonWrapper onClick={onDragLeave}> <CloseButtonWrapper onClick={onDragLeave}>
<CrossIcon /> <CrossIcon />
</CloseButtonWrapper> </CloseButtonWrapper>

View file

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

View file

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

View file

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

View file

@ -15,11 +15,17 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
color: #eee; color: #eee;
z-index: 1; z-index: 1;
position: absolute; position: absolute;
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'margin-right: 10px;' : 'margin-left: 10px;')} ${(props) =>
${(props) => (props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;')} props.direction === SCROLL_DIRECTION.LEFT
? 'margin-right: 10px;'
: 'margin-left: 10px;'}
${(props) =>
props.direction === SCROLL_DIRECTION.LEFT ? 'left: 0;' : 'right: 0;'}
& > svg { & > svg {
${(props) => props.direction === SCROLL_DIRECTION.LEFT && 'transform:rotate(180deg);'} ${(props) =>
props.direction === SCROLL_DIRECTION.LEFT &&
'transform:rotate(180deg);'}
border-radius: 50%; border-radius: 50%;
height: 30px; height: 30px;
width: 30px; width: 30px;
@ -30,25 +36,33 @@ const Wrapper = styled.button<{ direction: SCROLL_DIRECTION }>`
} }
&:hover { &:hover {
color:#fff; color: #fff;
} }
&::after { &::after {
content: ' '; 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; position: absolute;
top: 0; top: 0;
width: 40px; width: 40px;
height: 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 }) => ( const NavigationButton = ({ scrollDirection, ...rest }) => (
<Wrapper <Wrapper direction={scrollDirection} {...rest}>
direction={scrollDirection}
{...rest}
>
<NavigateNext /> <NavigateNext />
</Wrapper> </Wrapper>
); );

View file

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

View file

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

View file

@ -12,11 +12,11 @@ export const CodeBlock = styled.div<{ height: number }>`
justify-content: center; justify-content: center;
background: #1a1919; background: #1a1919;
height: ${(props) => props.height}px; height: ${(props) => props.height}px;
padding-left:30px; padding-left: 30px;
padding-right:20px; padding-right: 20px;
color: white; color: white;
margin: 20px 0; margin: 20px 0;
width:100%; width: 100%;
`; `;
export const FreeFlowText = styled.div` export const FreeFlowText = styled.div`
@ -71,20 +71,17 @@ function RecoveryKeyModal({ somethingWentWrong, ...props }: Props) {
disabled: !recoveryKey, disabled: !recoveryKey,
variant: 'success', variant: 'success',
}, },
}} }}>
>
<p>{constants.RECOVERY_KEY_DESCRIPTION}</p> <p>{constants.RECOVERY_KEY_DESCRIPTION}</p>
<CodeBlock height={150}> <CodeBlock height={150}>
{recoveryKey ? ( {recoveryKey ? (
<FreeFlowText> <FreeFlowText>{recoveryKey}</FreeFlowText>
{recoveryKey}
</FreeFlowText>
) : ( ) : (
<EnteSpinner /> <EnteSpinner />
)} )}
</CodeBlock> </CodeBlock>
<p>{constants.KEY_NOT_STORED_DISCLAIMER}</p> <p>{constants.KEY_NOT_STORED_DISCLAIMER}</p>
</MessageDialog > </MessageDialog>
); );
} }
export default RecoveryKeyModal; export default RecoveryKeyModal;

View file

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

View file

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

View file

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

View file

@ -36,7 +36,7 @@ export default function SignUp(props: SignUpProps) {
const registerUser = async ( const registerUser = async (
{ email, passphrase, confirm }: FormValues, { email, passphrase, confirm }: FormValues,
{ setFieldError }: FormikHelpers<FormValues>, { setFieldError }: FormikHelpers<FormValues>
) => { ) => {
setLoading(true); setLoading(true);
try { try {
@ -47,12 +47,13 @@ export default function SignUp(props: SignUpProps) {
} }
try { try {
if (passphrase === confirm) { if (passphrase === confirm) {
const { keyAttributes, masterKey } = await generateKeyAttributes(passphrase); const { keyAttributes, masterKey } =
await generateKeyAttributes(passphrase);
setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes); setData(LS_KEYS.ORIGINAL_KEY_ATTRIBUTES, keyAttributes);
await generateAndSaveIntermediateKeyAttributes( await generateAndSaveIntermediateKeyAttributes(
passphrase, passphrase,
keyAttributes, keyAttributes,
masterKey, masterKey
); );
await setSessionKeys(masterKey); await setSessionKeys(masterKey);
@ -68,113 +69,110 @@ export default function SignUp(props: SignUpProps) {
setLoading(false); setLoading(false);
}; };
return (<> return (
<Card.Title style={{ marginBottom: '32px' }}> <>
<LogoImg src="/icon.svg" /> <Card.Title style={{ marginBottom: '32px' }}>
{constants.SIGN_UP} <LogoImg src="/icon.svg" />
</Card.Title> {constants.SIGN_UP}
<Formik<FormValues> </Card.Title>
initialValues={{ <Formik<FormValues>
email: '', initialValues={{
passphrase: '', email: '',
confirm: '', passphrase: '',
}} confirm: '',
validationSchema={Yup.object().shape({ }}
email: Yup.string() validationSchema={Yup.object().shape({
.email(constants.EMAIL_ERROR) email: Yup.string()
.required(constants.REQUIRED), .email(constants.EMAIL_ERROR)
passphrase: Yup.string().required( .required(constants.REQUIRED),
constants.REQUIRED, passphrase: Yup.string().required(constants.REQUIRED),
), confirm: Yup.string().required(constants.REQUIRED),
confirm: Yup.string().required(constants.REQUIRED), })}
})} validateOnChange={false}
validateOnChange={false} validateOnBlur={false}
validateOnBlur={false} onSubmit={registerUser}>
onSubmit={registerUser} {({
> values,
{({ errors,
values, touched,
errors, handleChange,
touched, handleSubmit,
handleChange, }): JSX.Element => (
handleSubmit, <Form noValidate onSubmit={handleSubmit}>
}): JSX.Element => ( <Form.Group controlId="registrationForm.email">
<Form noValidate onSubmit={handleSubmit}> <Form.Control
<Form.Group controlId="registrationForm.email"> type="email"
<Form.Control placeholder={constants.ENTER_EMAIL}
type="email" value={values.email}
placeholder={constants.ENTER_EMAIL} onChange={handleChange('email')}
value={values.email} isInvalid={Boolean(
onChange={handleChange('email')} touched.email && errors.email
isInvalid={Boolean( )}
touched.email && errors.email, autoFocus
)} disabled={loading}
autoFocus />
disabled={loading} <FormControl.Feedback type="invalid">
{errors.email}
</FormControl.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
type="password"
placeholder={constants.PASSPHRASE_HINT}
value={values.passphrase}
onChange={handleChange('passphrase')}
isInvalid={Boolean(
touched.passphrase && errors.passphrase
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
type="password"
placeholder={constants.RE_ENTER_PASSPHRASE}
value={values.confirm}
onChange={handleChange('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.confirm}
</Form.Control.Feedback>
</Form.Group>
<Form.Group
style={{
marginBottom: '0',
textAlign: 'left',
}}
controlId="formBasicCheckbox-1">
<Form.Check
checked={acceptTerms}
onChange={(e) =>
setAcceptTerms(e.target.checked)
}
type="checkbox"
label={constants.TERMS_AND_CONDITIONS()}
/>
</Form.Group>
<br />
<SubmitButton
buttonText={constants.SUBMIT}
loading={loading}
disabled={!acceptTerms}
/> />
<FormControl.Feedback type="invalid"> <br />
{errors.email} <Button block variant="link" onClick={props.login}>
</FormControl.Feedback> {constants.ACCOUNT_EXISTS}
</Form.Group> </Button>
<Form.Group> </Form>
<Form.Control )}
type="password" </Formik>
placeholder={constants.PASSPHRASE_HINT} </>
value={values.passphrase} );
onChange={handleChange('passphrase')}
isInvalid={Boolean(
touched.passphrase &&
errors.passphrase,
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group>
<Form.Group>
<Form.Control
type="password"
placeholder={
constants.RE_ENTER_PASSPHRASE
}
value={values.confirm}
onChange={handleChange('confirm')}
isInvalid={Boolean(
touched.confirm && errors.confirm,
)}
disabled={loading}
/>
<Form.Control.Feedback type="invalid">
{errors.confirm}
</Form.Control.Feedback>
</Form.Group>
<Form.Group
style={{
marginBottom: '0',
textAlign: 'left',
}}
controlId="formBasicCheckbox-1"
>
<Form.Check
checked={acceptTerms}
onChange={(e) => setAcceptTerms(e.target.checked)}
type="checkbox"
label={constants.TERMS_AND_CONDITIONS()}
/>
</Form.Group>
<br />
<SubmitButton
buttonText={constants.SUBMIT}
loading={loading}
disabled={!acceptTerms}
/>
<br />
<Button block variant="link" onClick={props.login}>
{constants.ACCOUNT_EXISTS}
</Button>
</Form>
)}
</Formik>
</>);
} }

View file

@ -4,6 +4,9 @@ import { Form } from 'react-bootstrap';
import { Formik, FormikHelpers } from 'formik'; import { Formik, FormikHelpers } from 'formik';
import * as Yup from 'yup'; import * as Yup from 'yup';
import SubmitButton from './SubmitButton'; import SubmitButton from './SubmitButton';
import styled from 'styled-components';
import Visibility from './icons/Visibility';
import VisibilityOff from './icons/VisibilityOff';
interface formValues { interface formValues {
passphrase: string; passphrase: string;
@ -15,11 +18,29 @@ interface Props {
buttonText: string; 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) { export default function SingleInputForm(props: Props) {
const [loading, SetLoading] = useState(false); const [loading, SetLoading] = useState(false);
const [showPassword, setShowPassword] = useState(false);
const submitForm = async ( const submitForm = async (
values: formValues, values: formValues,
{ setFieldError }: FormikHelpers<formValues>, { setFieldError }: FormikHelpers<formValues>
) => { ) => {
SetLoading(true); SetLoading(true);
await props.callback(values.passphrase, setFieldError); await props.callback(values.passphrase, setFieldError);
@ -33,27 +54,39 @@ export default function SingleInputForm(props: Props) {
passphrase: Yup.string().required(constants.REQUIRED), passphrase: Yup.string().required(constants.REQUIRED),
})} })}
validateOnChange={false} validateOnChange={false}
validateOnBlur={false} validateOnBlur={false}>
> {({ values, touched, errors, handleChange, handleSubmit }) => (
{({
values, touched, errors, handleChange, handleSubmit,
}) => (
<Form noValidate onSubmit={handleSubmit}> <Form noValidate onSubmit={handleSubmit}>
<Form.Group> <Form.Group>
<Form.Control <Group>
type={props.fieldType} <Form.Control
placeholder={props.placeholder} type={showPassword ? 'text' : props.fieldType}
value={values.passphrase} placeholder={props.placeholder}
onChange={handleChange('passphrase')} value={values.passphrase}
isInvalid={Boolean( onChange={handleChange('passphrase')}
touched.passphrase && errors.passphrase, isInvalid={Boolean(
touched.passphrase && errors.passphrase
)}
disabled={loading}
autoFocus
/>
{props.fieldType === 'password' && (
<Button
type="button"
onClick={() =>
setShowPassword(!showPassword)
}>
{showPassword ? (
<VisibilityOff />
) : (
<Visibility />
)}
</Button>
)} )}
disabled={loading} <Form.Control.Feedback type="invalid">
autoFocus {errors.passphrase}
/> </Form.Control.Feedback>
<Form.Control.Feedback type="invalid"> </Group>
{errors.passphrase}
</Form.Control.Feedback>
</Form.Group> </Form.Group>
<SubmitButton <SubmitButton
buttonText={props.buttonText} buttonText={props.buttonText}

View file

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

View file

@ -1,6 +1,6 @@
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { DeadCenter, SetLoading } from 'pages/gallery'; 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 React, { useContext, useEffect, useState } from 'react';
import { Button, Row } from 'react-bootstrap'; import { Button, Row } from 'react-bootstrap';
import { disableTwoFactor, getTwoFactorStatus } from 'services/userService'; import { disableTwoFactor, getTwoFactorStatus } from 'services/userService';
@ -13,7 +13,7 @@ interface Props {
show: boolean; show: boolean;
onHide: () => void; onHide: () => void;
setDialogMessage: SetDialogMessage; setDialogMessage: SetDialogMessage;
setLoading: SetLoading setLoading: SetLoading;
closeSidebar: () => void; closeSidebar: () => void;
} }
@ -26,12 +26,16 @@ function TwoFactorModal(props: Props) {
if (!props.show) { if (!props.show) {
return; return;
} }
const isTwoFactorEnabled = getData(LS_KEYS.USER).isTwoFactorEnabled ?? false; const isTwoFactorEnabled =
getData(LS_KEYS.USER).isTwoFactorEnabled ?? false;
setTwoFactorStatus(isTwoFactorEnabled); setTwoFactorStatus(isTwoFactorEnabled);
const main = async () => { const main = async () => {
const isTwoFactorEnabled = await getTwoFactorStatus(); const isTwoFactorEnabled = await getTwoFactorStatus();
setTwoFactorStatus(isTwoFactorEnabled); setTwoFactorStatus(isTwoFactorEnabled);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), isTwoFactorEnabled: false }); setData(LS_KEYS.USER, {
...getData(LS_KEYS.USER),
isTwoFactorEnabled: false,
});
}; };
main(); main();
}, [props.show]); }, [props.show]);
@ -51,12 +55,21 @@ function TwoFactorModal(props: Props) {
const twoFactorDisable = async () => { const twoFactorDisable = async () => {
try { try {
await disableTwoFactor(); 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.onHide();
props.closeSidebar(); 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) { } 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 () => { const warnTwoFactorReconfigure = async () => {
@ -82,38 +95,60 @@ function TwoFactorModal(props: Props) {
attributes={{ attributes={{
title: constants.TWO_FACTOR_AUTHENTICATION, title: constants.TWO_FACTOR_AUTHENTICATION,
staticBackdrop: true, staticBackdrop: true,
}} }}>
<div
> {...(!isTwoFactorEnabled
<div {...(!isTwoFactorEnabled ? { style: { padding: '10px 10px 30px 10px' } } : { style: { padding: '10px' } })}> ? { style: { padding: '10px 10px 30px 10px' } }
{ : { style: { padding: '10px' } })}>
isTwoFactorEnabled ? {isTwoFactorEnabled ? (
<> <>
<Row> <Row>
<Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label> <Label>{constants.UPDATE_TWO_FACTOR_HINT}</Label>
<Value> <Value>
<Button variant={'outline-success'} onClick={warnTwoFactorReconfigure}>{constants.RECONFIGURE}</Button> <Button
</Value> variant={'outline-success'}
</Row> onClick={warnTwoFactorReconfigure}>
<Row> {constants.RECONFIGURE}
<Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label> </Button>
<Value> </Value>
<Button variant={'outline-danger'} onClick={warnTwoFactorDisable}>{constants.DISABLE}</Button> </Row>
</Value> <Row>
</Row> <Label>{constants.DISABLE_TWO_FACTOR_HINT} </Label>
<Value>
</> : ( <Button
<DeadCenter> variant={'outline-danger'}
<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> onClick={warnTwoFactorDisable}>
<p /> {constants.DISABLE}
<p>{constants.TWO_FACTOR_INFO}</p> </Button>
<div style={{ height: '10px' }} /> </Value>
<Button variant="outline-success" onClick={() => router.push('/two-factor/setup')}>{constants.ENABLE_TWO_FACTOR}</Button> </Row>
</DeadCenter> </>
) ) : (
} <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>
<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>
</DeadCenter>
)}
</div> </div>
</MessageDialog > </MessageDialog>
); );
} }
export default TwoFactorModal; export default TwoFactorModal;

View file

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

View file

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

View file

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

View file

@ -7,8 +7,7 @@ export default function CloudUpload(props) {
height={props.height} height={props.height}
viewBox={props.viewBox} viewBox={props.viewBox}
width={props.width} width={props.width}
fill="currentColor" fill="currentColor">
>
<path d="M0 0h24v24H0V0z" fill="none" /> <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" /> <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> </svg>

View file

@ -6,8 +6,7 @@ export default function DateIcon(props) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
viewBox={props.viewBox} 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" /> <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> </svg>
); );

View file

@ -6,8 +6,7 @@ export default function DateIcon(props) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
viewBox={props.viewBox} 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" /> <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> </svg>
); );

View file

@ -7,8 +7,7 @@ export default function DeleteIcon(props) {
height={props.height} height={props.height}
viewBox={props.viewBox} viewBox={props.viewBox}
width={props.width} width={props.width}
fill='currentColor' fill="currentColor">
>
<path d="M0 0h24v24H0z" fill="none" /> <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" /> <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> </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,9 +1,7 @@
import React from 'react'; import React from 'react';
export default function FolderIcon(props) { export default function FolderIcon(props) {
return ( return (
<div > <div>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
@ -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" /> <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> </svg>
</div> </div>
); );
} }

View file

@ -4,16 +4,16 @@ import styled from 'styled-components';
const Rotate = styled.div<{ disabled }>` const Rotate = styled.div<{ disabled }>`
width: 24px; width: 24px;
height: 27px; height: 27px;
${(props) => !props.disabled && '-webkit-animation: rotation 1s infinite linear'}; ${(props) =>
cursor:${(props) => props.disabled && 'pointer'}; !props.disabled && '-webkit-animation: rotation 1s infinite linear'};
cursor: ${(props) => props.disabled && 'pointer'};
transition-duration: 0.8s; transition-duration: 0.8s;
transition-property: transform; transition-property: transform;
&:hover { &:hover {
color:#fff; color: #fff;
transform: rotate(90deg); transform: rotate(90deg);
-webkit-transform: rotate(90deg); -webkit-transform: rotate(90deg);
} }
`; `;
export default function InProgressIcon(props) { export default function InProgressIcon(props) {
return ( return (
@ -24,9 +24,10 @@ export default function InProgressIcon(props) {
viewBox={props.viewBox} viewBox={props.viewBox}
width={props.width} width={props.width}
fill="#000000"> 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> </svg>
</ Rotate> </Rotate>
); );
} }
InProgressIcon.defaultProps = { InProgressIcon.defaultProps = {
@ -35,4 +36,3 @@ InProgressIcon.defaultProps = {
width: 24, width: 24,
viewBox: '0 0 24 24', viewBox: '0 0 24 24',
}; };

View file

@ -6,8 +6,7 @@ export default function LocationIcon(props) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
viewBox={props.viewBox} 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" /> <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" /> <circle cx="12" cy="9" r="2.5" />
</svg> </svg>

View file

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

View file

@ -6,8 +6,7 @@ export default function PlayCircleOutline(props) {
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height={props.height} height={props.height}
viewBox={props.viewBox} viewBox={props.viewBox}
width={props.width} width={props.width}>
>
<path d="M0 0h24v24H0z" fill="none" /> <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" /> <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> </svg>

View file

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

View file

@ -1,16 +1,31 @@
import React from 'react'; import React from 'react';
import Alert from 'react-bootstrap/Alert'; 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 ( return (
<Alert <Alert
variant="danger" variant={props.variant ?? 'danger'}
style={{ style={{
display: bannerMessage ? 'block' : 'none', display:
props.bannerMessage || props.children ? 'block' : 'none',
textAlign: 'center', textAlign: 'center',
}}
> border: 'none',
{bannerMessage} 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> </Alert>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -16,17 +16,18 @@ const OptionIcon = ({ onClick }: Props) => (
onClick(); onClick();
e.stopPropagation(); e.stopPropagation();
}} }}
style={{ marginBottom: '2px' }} style={{ marginBottom: '2px' }}>
>
<svg <svg
xmlns="http://www.w3.org/2000/svg" xmlns="http://www.w3.org/2000/svg"
height="20px" height="20px"
width="24px" width="24px"
viewBox="0 0 24 24" viewBox="0 0 24 24"
fill="#000000" fill="#000000">
>
<path d="M0 0h24v24H0V0z" fill="none" /> <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> </svg>
</OptionIconWrapper> </OptionIconWrapper>
); );

View file

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

View file

@ -25,7 +25,7 @@ const Check = styled.input`
opacity: 0; opacity: 0;
outline: none; outline: none;
cursor: pointer; cursor: pointer;
@media(pointer: coarse) { @media (pointer: coarse) {
pointer-events: none; pointer-events: none;
} }
@ -183,8 +183,7 @@ export default function PreviewCard(props: IProps) {
onClick={handleClick} onClick={handleClick}
disabled={!forcedEnable && !file?.msrc && !imgSrc} disabled={!forcedEnable && !file?.msrc && !imgSrc}
selected={selected} selected={selected}
{...(selectable ? useLongPress(longPressCallback, 500) : {})} {...(selectable ? useLongPress(longPressCallback, 500) : {})}>
>
{selectable && ( {selectable && (
<Check <Check
type="checkbox" type="checkbox"

View file

@ -7,7 +7,7 @@ import DeleteIcon from 'components/icons/DeleteIcon';
import CrossIcon from 'components/icons/CrossIcon'; import CrossIcon from 'components/icons/CrossIcon';
import AddIcon from 'components/icons/AddIcon'; import AddIcon from 'components/icons/AddIcon';
import { IconButton } from 'components/Container'; import { IconButton } from 'components/Container';
import constants from 'utils/strings/englishConstants'; import constants from 'utils/strings/constants';
interface Props { interface Props {
addToCollectionHelper: (collectionName, collection) => void; addToCollectionHelper: (collectionName, collection) => void;
@ -42,32 +42,42 @@ const SelectedFileOptions = ({
count, count,
clearSelection, clearSelection,
}: Props) => { }: Props) => {
const addToCollection = () => setCollectionSelectorAttributes({ const addToCollection = () =>
callback: (collection) => addToCollectionHelper(null, collection), setCollectionSelectorAttributes({
showNextModal: showCreateCollectionModal, callback: (collection) => addToCollectionHelper(null, collection),
title: constants.ADD_TO_COLLECTION, showNextModal: showCreateCollectionModal,
}); title: constants.ADD_TO_COLLECTION,
});
const deleteHandler = () => setDialogMessage({ const deleteHandler = () =>
title: constants.CONFIRM_DELETE_FILE, setDialogMessage({
content: constants.DELETE_FILE_MESSAGE, title: constants.CONFIRM_DELETE_FILE,
staticBackdrop: true, content: constants.DELETE_FILE_MESSAGE,
proceed: { staticBackdrop: true,
action: deleteFileHelper, proceed: {
text: constants.DELETE, action: deleteFileHelper,
variant: 'danger', text: constants.DELETE,
}, variant: 'danger',
close: { text: constants.CANCEL }, },
}); close: { text: constants.CANCEL },
});
return ( return (
<SelectionBar> <SelectionBar>
<SelectionContainer> <SelectionContainer>
<IconButton onClick={clearSelection}><CrossIcon /></IconButton> <IconButton onClick={clearSelection}>
<div>{count} {constants.SELECTED}</div> <CrossIcon />
</IconButton>
<div>
{count} {constants.SELECTED}
</div>
</SelectionContainer> </SelectionContainer>
<IconButton onClick={addToCollection}><AddIcon /></IconButton> <IconButton onClick={addToCollection}>
<IconButton onClick={deleteHandler}><DeleteIcon /></IconButton> <AddIcon />
</IconButton>
<IconButton onClick={deleteHandler}>
<DeleteIcon />
</IconButton>
</SelectionBar> </SelectionBar>
); );
}; };

View file

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

View file

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

View file

@ -1,10 +1,13 @@
import React from 'react'; import ExpandLess from 'components/icons/ExpandLess';
import { import ExpandMore from 'components/icons/ExpandMore';
Alert, Button, Modal, ProgressBar, import React, { useState } from 'react';
} from 'react-bootstrap'; import { Button, Modal, ProgressBar } from 'react-bootstrap';
import { FileRejection } from 'react-dropzone'; 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 constants from 'utils/strings/constants';
import AlertBanner from './AlertBanner';
interface Props { interface Props {
fileCounter; fileCounter;
@ -14,106 +17,222 @@ interface Props {
retryFailed; retryFailed;
fileProgress: Map<string, number>; fileProgress: Map<string, number>;
show; show;
fileRejections:FileRejection[] fileRejections: FileRejection[];
uploadResult: Map<string, number>;
} }
interface FileProgressStatuses{ interface FileProgresses {
fileName:string; fileName: string;
progress:number; 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) { 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) { if (props.fileProgress) {
for (const [fileName, progress] of props.fileProgress) { for (const [fileName, progress] of props.fileProgress) {
fileProgressStatuses.push({ fileName, progress }); 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 ( return (
<Modal <Modal
show={props.show} show={props.show}
onHide={ onHide={
props.uploadStage !== UPLOAD_STAGES.FINISH ? props.uploadStage !== UPLOAD_STAGES.FINISH
() => null : ? () => null
props.closeModal : props.closeModal
} }
aria-labelledby="contained-modal-title-vcenter" aria-labelledby="contained-modal-title-vcenter"
centered centered
backdrop={ backdrop={fileProgressStatuses?.length !== 0 ? 'static' : 'true'}>
fileProgressStatuses?.length !== 0 ? 'static' : 'true'
}
>
<Modal.Header <Modal.Header
style={{ display: 'flex', justifyContent: 'center', textAlign: 'center', borderBottom: 'none', paddingTop: '30px', paddingBottom: '0px' }} style={{
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH} display: 'flex',
> justifyContent: 'center',
textAlign: 'center',
borderBottom: 'none',
paddingTop: '30px',
paddingBottom: '0px',
}}
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}>
<h4 style={{ width: '100%' }}> <h4 style={{ width: '100%' }}>
{props.uploadStage === UPLOAD_STAGES.UPLOADING ? {props.uploadStage === UPLOAD_STAGES.UPLOADING
constants.UPLOAD[props.uploadStage]( ? constants.UPLOAD[props.uploadStage](props.fileCounter)
props.fileCounter, : constants.UPLOAD[props.uploadStage]}
) :
constants.UPLOAD[props.uploadStage]}
</h4> </h4>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{props.uploadStage===UPLOAD_STAGES.FINISH ? ( {(props.uploadStage ===
fileProgressStatuses.length !== 0 && ( UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
<Alert variant="warning"> props.uploadStage === UPLOAD_STAGES.UPLOADING) && (
{constants.FAILED_UPLOAD_FILE_LIST} <ProgressBar
</Alert> now={props.now}
) animated
) : variant="upload-progress-bar"
(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES || />
props.uploadStage === UPLOAD_STAGES.UPLOADING) && )}
( {fileProgressStatuses.length > 0 && (
< ProgressBar <FileList>
now={props.now}
animated
variant="upload-progress-bar"
/>
)}
{fileProgressStatuses?.length > 0 && (
<ul
style={{
marginTop: '10px',
overflow: 'auto',
maxHeight: '250px',
}}
>
{fileProgressStatuses.map(({ fileName, progress }) => ( {fileProgressStatuses.map(({ fileName, progress }) => (
<li key={fileName} style={{ marginTop: '12px' }}> <li key={fileName} style={{ marginTop: '12px' }}>
{props.uploadStage===UPLOAD_STAGES.FINISH ? {props.uploadStage === UPLOAD_STAGES.FINISH
fileName : ? fileName
constants.FILE_UPLOAD_PROGRESS( : constants.FILE_UPLOAD_PROGRESS(
fileName, fileName,
progress, progress
)} )}
</li> </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 && ( {props.uploadStage === UPLOAD_STAGES.FINISH && (
<Modal.Footer style={{ border: 'none' }}> <Modal.Footer style={{ border: 'none' }}>
{props.uploadStage===UPLOAD_STAGES.FINISH && (fileProgressStatuses?.length === 0 ? ( {props.uploadStage === UPLOAD_STAGES.FINISH &&
<Button (fileUploadResultMap?.get(FileUploadResults.FAILED)
variant="outline-secondary" ?.length > 0 ||
style={{ width: '100%' }} fileUploadResultMap?.get(FileUploadResults.BLOCKED)
onClick={props.closeModal} ?.length > 0 ? (
> <Button
{constants.CLOSE} variant="outline-success"
</Button>) : ( style={{ width: '100%' }}
<Button onClick={props.retryFailed}>
variant="outline-success" {constants.RETRY_FAILED}
style={{ width: '100%' }} </Button>
onClick={props.retryFailed} ) : (
> <Button
{constants.RETRY} variant="outline-secondary"
</Button>))} style={{ width: '100%' }}
onClick={props.closeModal}>
{constants.CLOSE}
</Button>
))}
</Modal.Footer> </Modal.Footer>
)} )}
</Modal.Body> </Modal.Body>

View file

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

View file

@ -1,7 +1,5 @@
import React from 'react'; import React from 'react';
import Document, { import Document, { Html, Head, Main, NextScript } from 'next/document';
Html, Head, Main, NextScript,
} from 'next/document';
import { ServerStyleSheet } from 'styled-components'; import { ServerStyleSheet } from 'styled-components';
export default class MyDocument extends Document { export default class MyDocument extends Document {
@ -10,9 +8,11 @@ export default class MyDocument extends Document {
const originalRenderPage = ctx.renderPage; const originalRenderPage = ctx.renderPage;
try { try {
ctx.renderPage = () => originalRenderPage({ ctx.renderPage = () =>
enhanceApp: (App) => (props) => sheet.collectStyles(<App {...props} />), originalRenderPage({
}); enhanceApp: (App) => (props) =>
sheet.collectStyles(<App {...props} />),
});
const initialProps = await Document.getInitialProps(ctx); const initialProps = await Document.getInitialProps(ctx);
return { return {
@ -37,13 +37,24 @@ export default class MyDocument extends Document {
name="description" name="description"
content="ente is a privacy focussed photo storage service that offers end-to-end encryption." 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="manifest" href="manifest.json" />
<link rel="apple-touch-icon" href="/images/ente-512.png" /> <link rel="apple-touch-icon" href="/images/ente-512.png" />
<meta name="theme-color" content="#111" /> <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-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> </Head>
<body> <body>
<Main /> <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({ export default createProxyMiddleware({
target: API_ENDPOINT, 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 constants from 'utils/strings/constants';
import { getData, LS_KEYS, setData } from 'utils/storage/localStorage'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
import { useRouter } from 'next/router'; import { useRouter } from 'next/router';
import { B64EncryptionResult } from 'services/uploadService';
import CryptoWorker, { import CryptoWorker, {
setSessionKeys, setSessionKeys,
generateAndSaveIntermediateKeyAttributes, generateAndSaveIntermediateKeyAttributes,
B64EncryptionResult,
} from 'utils/crypto'; } from 'utils/crypto';
import { getActualKey } from 'utils/common/key'; import { getActualKey } from 'utils/common/key';
import { setKeys, UpdatedKey } from 'services/userService'; import { setKeys, UpdatedKey } from 'services/userService';
@ -45,7 +45,8 @@ export default function Generate() {
setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED); setFieldError('confirm', constants.PASSWORD_GENERATION_FAILED);
return; return;
} }
const encryptedKeyAttributes: B64EncryptionResult = await cryptoWorker.encryptToB64(key, kek.key); const encryptedKeyAttributes: B64EncryptionResult =
await cryptoWorker.encryptToB64(key, kek.key);
const updatedKey: UpdatedKey = { const updatedKey: UpdatedKey = {
kekSalt, kekSalt,
encryptedKey: encryptedKeyAttributes.encryptedData, encryptedKey: encryptedKeyAttributes.encryptedData,
@ -60,7 +61,7 @@ export default function Generate() {
await generateAndSaveIntermediateKeyAttributes( await generateAndSaveIntermediateKeyAttributes(
passphrase, passphrase,
updatedKeyAttributes, updatedKeyAttributes,
key, key
); );
setSessionKeys(key); setSessionKeys(key);
@ -75,9 +76,9 @@ export default function Generate() {
callback={onSubmit} callback={onSubmit}
buttonText={constants.CHANGE_PASSWORD} buttonText={constants.CHANGE_PASSWORD}
back={ back={
getData(LS_KEYS.SHOW_BACK_BUTTON)?.value ? getData(LS_KEYS.SHOW_BACK_BUTTON)?.value
redirectToGallery : ? redirectToGallery
null : null
} }
/> />
); );

View file

@ -29,7 +29,10 @@ export default function Credentials() {
const user = getData(LS_KEYS.USER); const user = getData(LS_KEYS.USER);
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
if ((!user?.token && !user?.encryptedToken) || !keyAttributes?.memLimit) { if (
(!user?.token && !user?.encryptedToken) ||
!keyAttributes?.memLimit
) {
clearData(); clearData();
router.push('/'); router.push('/');
} else if (!keyAttributes) { } else if (!keyAttributes) {
@ -51,7 +54,7 @@ export default function Credentials() {
passphrase, passphrase,
keyAttributes.kekSalt, keyAttributes.kekSalt,
keyAttributes.opsLimit, keyAttributes.opsLimit,
keyAttributes.memLimit, keyAttributes.memLimit
); );
} catch (e) { } catch (e) {
console.error('failed to deriveKey ', e.message); console.error('failed to deriveKey ', e.message);
@ -61,13 +64,13 @@ export default function Credentials() {
const key: string = await cryptoWorker.decryptB64( const key: string = await cryptoWorker.decryptB64(
keyAttributes.encryptedKey, keyAttributes.encryptedKey,
keyAttributes.keyDecryptionNonce, keyAttributes.keyDecryptionNonce,
kek, kek
); );
if (isFirstLogin()) { if (isFirstLogin()) {
await generateAndSaveIntermediateKeyAttributes( await generateAndSaveIntermediateKeyAttributes(
passphrase, passphrase,
keyAttributes, keyAttributes,
key, key
); );
} }
await setSessionKeys(key); await setSessionKeys(key);
@ -81,7 +84,7 @@ export default function Credentials() {
} catch (e) { } catch (e) {
setFieldError( setFieldError(
'passphrase', 'passphrase',
`${constants.UNKNOWN_ERROR} ${e.message}`, `${constants.UNKNOWN_ERROR} ${e.message}`
); );
console.error('failed to verifyPassphrase ', e.message); console.error('failed to verifyPassphrase ', e.message);
} }
@ -90,13 +93,10 @@ export default function Credentials() {
return ( return (
<> <>
<Container> <Container>
<Card <Card style={{ minWidth: '320px' }} className="text-center">
style={{ minWidth: '320px' }}
className="text-center"
>
<Card.Body style={{ padding: '40px 30px' }}> <Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}> <Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' /> <LogoImg src="/icon.svg" />
{constants.PASSWORD} {constants.PASSWORD}
</Card.Title> </Card.Title>
<SingleInputForm <SingleInputForm
@ -110,12 +110,10 @@ export default function Credentials() {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
marginTop: '12px', marginTop: '12px',
}} }}>
>
<Button <Button
variant="link" variant="link"
onClick={() => router.push('/recover')} onClick={() => router.push('/recover')}>
>
{constants.FORGOT_PASSWORD} {constants.FORGOT_PASSWORD}
</Button> </Button>
<Button variant="link" onClick={logoutUser}> <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 { useRouter } from 'next/router';
import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { clearKeys, getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { import {
@ -39,7 +45,6 @@ import { LoadingOverlay } from 'components/LoadingOverlay';
import PhotoFrame from 'components/PhotoFrame'; import PhotoFrame from 'components/PhotoFrame';
import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file'; import { getSelectedFileIds, sortFilesIntoCollections } from 'utils/file';
import { addFilesToCollection } from 'utils/collection'; import { addFilesToCollection } from 'utils/collection';
import { errorCodes } from 'utils/common/errorUtil';
import SearchBar, { DateValue } from 'components/SearchBar'; import SearchBar, { DateValue } from 'components/SearchBar';
import { Bbox } from 'services/searchService'; import { Bbox } from 'services/searchService';
import SelectedFileOptions from 'components/pages/gallery/SelectedFileOptions'; 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 Upload from 'components/pages/gallery/Upload';
import Collections from 'components/pages/gallery/Collections'; import Collections from 'components/pages/gallery/Collections';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import { CustomError, ServerErrorCodes } from 'utils/common/errorUtil';
export enum FILE_TYPE {
IMAGE,
VIDEO,
OTHERS,
}
export const DeadCenter = styled.div` export const DeadCenter = styled.div`
flex: 1; flex: 1;
@ -98,19 +98,22 @@ export interface SearchStats {
type GalleryContextType = { type GalleryContextType = {
thumbs: Map<number, string>; thumbs: Map<number, string>;
files: Map<number, string>; files: Map<number, string>;
} };
const defaultGalleryContext: GalleryContextType = { const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(), thumbs: new Map(),
files: new Map(), files: new Map(),
}; };
export const GalleryContext = createContext<GalleryContextType>(defaultGalleryContext); export const GalleryContext = createContext<GalleryContextType>(
defaultGalleryContext
);
export default function Gallery() { export default function Gallery() {
const router = useRouter(); const router = useRouter();
const [collections, setCollections] = useState<Collection[]>([]); const [collections, setCollections] = useState<Collection[]>([]);
const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] = useState<CollectionAndItsLatestFile[]>([]); const [collectionsAndTheirLatestFile, setCollectionsAndTheirLatestFile] =
useState<CollectionAndItsLatestFile[]>([]);
const [files, setFiles] = useState<File[]>(null); const [files, setFiles] = useState<File[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>(); const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [bannerMessage, setBannerMessage] = useState<string>(null); const [bannerMessage, setBannerMessage] = useState<string>(null);
@ -121,9 +124,11 @@ export default function Gallery() {
const [dialogView, setDialogView] = useState(false); const [dialogView, setDialogView] = useState(false);
const [planModalView, setPlanModalView] = useState(false); const [planModalView, setPlanModalView] = useState(false);
const [loading, setLoading] = 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 [collectionSelectorView, setCollectionSelectorView] = useState(false);
const [collectionNamerAttributes, setCollectionNamerAttributes] = useState<CollectionNamerAttributes>(null); const [collectionNamerAttributes, setCollectionNamerAttributes] =
useState<CollectionNamerAttributes>(null);
const [collectionNamerView, setCollectionNamerView] = useState(false); const [collectionNamerView, setCollectionNamerView] = useState(false);
const [search, setSearch] = useState<Search>({ const [search, setSearch] = useState<Search>({
date: null, date: null,
@ -150,7 +155,8 @@ export default function Gallery() {
const resync = useRef(false); const resync = useRef(false);
const [deleted, setDeleted] = useState<number[]>([]); const [deleted, setDeleted] = useState<number[]>([]);
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const [collectionFilesCount, setCollectionFilesCount] = useState<Map<number, number>>(); const [collectionFilesCount, setCollectionFilesCount] =
useState<Map<number, number>>();
useEffect(() => { useEffect(() => {
const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY);
@ -181,50 +187,46 @@ export default function Gallery() {
}, []); }, []);
useEffect(() => setDialogView(true), [dialogMessage]); useEffect(() => setDialogView(true), [dialogMessage]);
useEffect( useEffect(() => {
() => { if (collectionSelectorAttributes) {
if (collectionSelectorAttributes) { setCollectionSelectorView(true);
setCollectionSelectorView(true); }
} }, [collectionSelectorAttributes]);
},
[collectionSelectorAttributes],
);
useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]); useEffect(() => setCollectionNamerView(true), [collectionNamerAttributes]);
const syncWithRemote = async (force = false, silent=false) => { const syncWithRemote = async (force = false, silent = false) => {
if (syncInProgress.current && !force) { if (syncInProgress.current && !force) {
resync.current= true; resync.current = true;
return; return;
} }
syncInProgress.current=true; syncInProgress.current = true;
try { try {
checkConnectivity(); checkConnectivity();
if (!(await isTokenValid())) { if (!(await isTokenValid())) {
throw new Error(errorCodes.ERR_SESSION_EXPIRED); throw new Error(ServerErrorCodes.SESSION_EXPIRED);
} }
!silent && loadingBar.current?.continuousStart(); !silent && loadingBar.current?.continuousStart();
await billingService.updatePlans();
await billingService.syncSubscription(); await billingService.syncSubscription();
const collections = await syncCollections(); const collections = await syncCollections();
const { files } = await syncFiles(collections, setFiles); const { files } = await syncFiles(collections, setFiles);
await initDerivativeState(collections, files); await initDerivativeState(collections, files);
} catch (e) { } catch (e) {
switch (e.message) { switch (e.message) {
case errorCodes.ERR_SESSION_EXPIRED: case ServerErrorCodes.SESSION_EXPIRED:
setBannerMessage(constants.SESSION_EXPIRED_MESSAGE); setBannerMessage(constants.SESSION_EXPIRED_MESSAGE);
setDialogMessage({ setDialogMessage({
title: constants.SESSION_EXPIRED, title: constants.SESSION_EXPIRED,
content: constants.SESSION_EXPIRED_MESSAGE, content: constants.SESSION_EXPIRED_MESSAGE,
staticBackdrop: true, staticBackdrop: true,
nonClosable: true,
proceed: { proceed: {
text: constants.LOGIN, text: constants.LOGIN,
action: logoutUser, action: logoutUser,
variant: 'success', variant: 'success',
}, },
nonClosable: true,
}); });
break; break;
case errorCodes.ERR_KEY_MISSING: case CustomError.KEY_MISSING:
clearKeys(); clearKeys();
router.push('/credentials'); router.push('/credentials');
break; break;
@ -232,22 +234,17 @@ export default function Gallery() {
} finally { } finally {
!silent && loadingBar.current?.complete(); !silent && loadingBar.current?.complete();
} }
syncInProgress.current=false; syncInProgress.current = false;
if (resync.current) { if (resync.current) {
resync.current=false; resync.current = false;
syncWithRemote(); syncWithRemote();
} }
}; };
const initDerivativeState = async (collections, files) => { const initDerivativeState = async (collections, files) => {
const nonEmptyCollections = getNonEmptyCollections( const nonEmptyCollections = getNonEmptyCollections(collections, files);
collections, const collectionsAndTheirLatestFile =
files, await getCollectionsAndTheirLatestFile(nonEmptyCollections, files);
);
const collectionsAndTheirLatestFile = await getCollectionsAndTheirLatestFile(
nonEmptyCollections,
files,
);
const collectionWiseFiles = sortFilesIntoCollections(files); const collectionWiseFiles = sortFilesIntoCollections(files);
const collectionFilesCount = new Map<number, number>(); const collectionFilesCount = new Map<number, number>();
for (const [id, files] of collectionWiseFiles) { for (const [id, files] of collectionWiseFiles) {
@ -274,7 +271,7 @@ export default function Gallery() {
} }
const addToCollectionHelper = ( const addToCollectionHelper = (
collectionName: string, collectionName: string,
collection: Collection, collection: Collection
) => { ) => {
loadingBar.current?.continuousStart(); loadingBar.current?.continuousStart();
addFilesToCollection( addFilesToCollection(
@ -285,31 +282,29 @@ export default function Gallery() {
syncWithRemote, syncWithRemote,
selectCollection, selectCollection,
collectionName, collectionName,
collection, collection
); );
}; };
const showCreateCollectionModal = () => setCollectionNamerAttributes({ const showCreateCollectionModal = () =>
title: constants.CREATE_COLLECTION, setCollectionNamerAttributes({
buttonText: constants.CREATE, title: constants.CREATE_COLLECTION,
autoFilledName: '', buttonText: constants.CREATE,
callback: (collectionName) => addToCollectionHelper(collectionName, null), autoFilledName: '',
}); callback: (collectionName) =>
addToCollectionHelper(collectionName, null),
});
const deleteFileHelper = async () => { const deleteFileHelper = async () => {
loadingBar.current?.continuousStart(); loadingBar.current?.continuousStart();
try { try {
const fileIds = getSelectedFileIds(selected); const fileIds = getSelectedFileIds(selected);
await deleteFiles( await deleteFiles(fileIds, clearSelection, syncWithRemote);
fileIds,
clearSelection,
syncWithRemote,
);
setDeleted([...deleted, ...fileIds]); setDeleted([...deleted, ...fileIds]);
} catch (e) { } catch (e) {
loadingBar.current.complete(); loadingBar.current.complete();
switch (e.status?.toString()) { switch (e.status?.toString()) {
case errorCodes.ERR_FORBIDDEN: case ServerErrorCodes.FORBIDDEN:
setDialogMessage({ setDialogMessage({
title: constants.ERROR, title: constants.ERROR,
staticBackdrop: true, staticBackdrop: true,
@ -333,7 +328,6 @@ export default function Gallery() {
setSearchStats(null); setSearchStats(null);
}; };
const closeCollectionSelector = (closeBtnClick?: boolean) => { const closeCollectionSelector = (closeBtnClick?: boolean) => {
if (closeBtnClick === true) { if (closeBtnClick === true) {
appContext.resetSharedFiles(); appContext.resetSharedFiles();
@ -346,8 +340,10 @@ export default function Gallery() {
<FullScreenDropZone <FullScreenDropZone
getRootProps={getRootProps} getRootProps={getRootProps}
getInputProps={getInputProps} getInputProps={getInputProps}
showCollectionSelector={setCollectionSelectorView.bind(null, true)} showCollectionSelector={setCollectionSelectorView.bind(
> null,
true
)}>
{loading && ( {loading && (
<LoadingOverlay> <LoadingOverlay>
<EnteSpinner /> <EnteSpinner />
@ -399,24 +395,33 @@ export default function Gallery() {
attributes={collectionNamerAttributes} attributes={collectionNamerAttributes}
/> />
<CollectionSelector <CollectionSelector
show={collectionSelectorView && !(collectionsAndTheirLatestFile?.length === 0)} show={
collectionSelectorView &&
!(collectionsAndTheirLatestFile?.length === 0)
}
onHide={closeCollectionSelector} onHide={closeCollectionSelector}
collectionsAndTheirLatestFile={collectionsAndTheirLatestFile} collectionsAndTheirLatestFile={
collectionsAndTheirLatestFile
}
directlyShowNextModal={ directlyShowNextModal={
collectionsAndTheirLatestFile?.length === 0 collectionsAndTheirLatestFile?.length === 0
} }
attributes={collectionSelectorAttributes} attributes={collectionSelectorAttributes}
syncWithRemote={syncWithRemote}
/> />
<Upload <Upload
syncWithRemote={syncWithRemote} syncWithRemote={syncWithRemote}
setBannerMessage={setBannerMessage} setBannerMessage={setBannerMessage}
acceptedFiles={acceptedFiles} acceptedFiles={acceptedFiles}
showCollectionSelector={setCollectionSelectorView.bind(null, true)} showCollectionSelector={setCollectionSelectorView.bind(
setCollectionSelectorAttributes={setCollectionSelectorAttributes} null,
true
)}
setCollectionSelectorAttributes={
setCollectionSelectorAttributes
}
closeCollectionSelector={setCollectionSelectorView.bind( closeCollectionSelector={setCollectionSelectorView.bind(
null, null,
false, false
)} )}
setLoading={setLoading} setLoading={setLoading}
setCollectionNamerAttributes={setCollectionNamerAttributes} setCollectionNamerAttributes={setCollectionNamerAttributes}
@ -431,7 +436,10 @@ export default function Gallery() {
setLoading={setLoading} setLoading={setLoading}
showPlanSelectorModal={() => setPlanModalView(true)} showPlanSelectorModal={() => setPlanModalView(true)}
/> />
<UploadButton isFirstFetch={isFirstFetch} openFileUploader={openFileUploader} /> <UploadButton
isFirstFetch={isFirstFetch}
openFileUploader={openFileUploader}
/>
<PhotoFrame <PhotoFrame
files={files} files={files}
setFiles={setFiles} setFiles={setFiles}

View file

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

View file

@ -11,6 +11,7 @@ import SignUp from 'components/SignUp';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import localForage from 'utils/storage/localForage'; import localForage from 'utils/storage/localForage';
import IncognitoWarning from 'components/IncognitoWarning'; import IncognitoWarning from 'components/IncognitoWarning';
import { logError } from 'utils/sentry';
const Container = styled.div` const Container = styled.div`
display: flex; display: flex;
@ -19,7 +20,7 @@ const Container = styled.div`
justify-content: center; justify-content: center;
background-color: #000; background-color: #000;
@media(max-width: 1024px) { @media (max-width: 1024px) {
flex-direction: column; flex-direction: column;
} }
`; `;
@ -32,7 +33,7 @@ const SlideContainer = styled.div`
justify-content: center; justify-content: center;
text-align: center; text-align: center;
@media(max-width: 1024px) { @media (max-width: 1024px) {
flex-grow: 0; flex-grow: 0;
} }
`; `;
@ -46,7 +47,7 @@ const DesktopBox = styled.div`
justify-content: center; justify-content: center;
background-color: #242424; background-color: #242424;
@media(max-width: 1024px) { @media (max-width: 1024px) {
display: none; display: none;
} }
`; `;
@ -54,7 +55,7 @@ const DesktopBox = styled.div`
const MobileBox = styled.div` const MobileBox = styled.div`
display: none; display: none;
@media(max-width: 1024px) { @media (max-width: 1024px) {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
padding: 40px 10px; padding: 40px 10px;
@ -90,7 +91,7 @@ const Img = styled.img`
height: 250px; height: 250px;
object-fit: contain; object-fit: contain;
@media(max-width: 400px) { @media (max-width: 400px) {
height: 180px; height: 180px;
} }
`; `;
@ -110,6 +111,7 @@ export default function LandingPage() {
try { try {
await localForage.ready(); await localForage.ready();
} catch (e) { } catch (e) {
logError(e, 'usage in incognito mode tried');
setBlockUsage(true); setBlockUsage(true);
} }
setLoading(false); setLoading(false);
@ -121,56 +123,73 @@ export default function LandingPage() {
const signUp = () => setShowLogin(false); const signUp = () => setShowLogin(false);
const login = () => setShowLogin(true); const login = () => setShowLogin(true);
return <Container> return (
{loading ? <EnteSpinner /> : <Container>
(<> {loading ? (
<SlideContainer> <EnteSpinner />
<UpperText> ) : (
{constants.HERO_HEADER()} <>
</UpperText> <SlideContainer>
<Carousel controls={false}> <UpperText>{constants.HERO_HEADER()}</UpperText>
<Carousel.Item> <Carousel controls={false}>
<Img src="/images/slide-1.png" /> <Carousel.Item>
<FeatureText>{constants.HERO_SLIDE_1_TITLE}</FeatureText> <Img src="/images/slide-1.png" />
<TextContainer>{constants.HERO_SLIDE_1}</TextContainer> <FeatureText>
</Carousel.Item> {constants.HERO_SLIDE_1_TITLE}
<Carousel.Item> </FeatureText>
<Img src="/images/slide-2.png" /> <TextContainer>
<FeatureText>{constants.HERO_SLIDE_2_TITLE}</FeatureText> {constants.HERO_SLIDE_1}
<TextContainer>{constants.HERO_SLIDE_2}</TextContainer> </TextContainer>
</Carousel.Item> </Carousel.Item>
<Carousel.Item> <Carousel.Item>
<Img src="/images/slide-3.png" /> <Img src="/images/slide-2.png" />
<FeatureText>{constants.HERO_SLIDE_3_TITLE}</FeatureText> <FeatureText>
<TextContainer>{constants.HERO_SLIDE_3}</TextContainer> {constants.HERO_SLIDE_2_TITLE}
</Carousel.Item> </FeatureText>
</Carousel> <TextContainer>
</SlideContainer> {constants.HERO_SLIDE_2}
<MobileBox> </TextContainer>
<Button </Carousel.Item>
variant="outline-success" <Carousel.Item>
size="lg" <Img src="/images/slide-3.png" />
style={{ color: '#fff', padding: '10px 50px' }} <FeatureText>
onClick={() => router.push('signup')} {constants.HERO_SLIDE_3_TITLE}
> </FeatureText>
{constants.SIGN_UP} <TextContainer>
</Button> {constants.HERO_SLIDE_3}
<br /> </TextContainer>
<Button </Carousel.Item>
variant="link" </Carousel>
size="lg" </SlideContainer>
style={{ color: '#fff', padding: '10px 50px' }} <MobileBox>
onClick={() => router.push('login')} <Button
> variant="outline-success"
{constants.SIGN_IN} size="lg"
</Button> style={{ color: '#fff', padding: '10px 50px' }}
</MobileBox> onClick={() => router.push('signup')}>
<DesktopBox> {constants.SIGN_UP}
<SideBox> </Button>
{showLogin ? <Login signUp={signUp} /> : <SignUp login={login} />} <br />
</SideBox> <Button
</DesktopBox> variant="link"
{blockUsage && <IncognitoWarning />} size="lg"
</>)} style={{ color: '#fff', padding: '10px 50px' }}
</Container>; onClick={() => router.push('login')}>
{constants.SIGN_IN}
</Button>
</MobileBox>
<DesktopBox>
<SideBox>
{showLogin ? (
<Login signUp={signUp} />
) : (
<SignUp login={login} />
)}
</SideBox>
</DesktopBox>
{blockUsage && <IncognitoWarning />}
</>
)}
</Container>
);
} }

View file

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

View file

@ -38,7 +38,7 @@ export default function Recover() {
const masterKey: string = await cryptoWorker.decryptB64( const masterKey: string = await cryptoWorker.decryptB64(
keyAttributes.masterKeyEncryptedWithRecoveryKey, keyAttributes.masterKeyEncryptedWithRecoveryKey,
keyAttributes.masterKeyDecryptionNonce, keyAttributes.masterKeyDecryptionNonce,
await cryptoWorker.fromHex(recoveryKey), await cryptoWorker.fromHex(recoveryKey)
); );
setSessionKeys(masterKey); setSessionKeys(masterKey);
router.push('/changePassword'); router.push('/changePassword');
@ -51,13 +51,10 @@ export default function Recover() {
return ( return (
<> <>
<Container> <Container>
<Card <Card style={{ minWidth: '320px' }} className="text-center">
style={{ minWidth: '320px' }}
className="text-center"
>
<Card.Body style={{ padding: '40px 30px' }}> <Card.Body style={{ padding: '40px 30px' }}>
<Card.Title style={{ marginBottom: '32px' }}> <Card.Title style={{ marginBottom: '32px' }}>
<LogoImg src='/icon.svg' /> <LogoImg src="/icon.svg" />
{constants.RECOVER_ACCOUNT} {constants.RECOVER_ACCOUNT}
</Card.Title> </Card.Title>
<SingleInputForm <SingleInputForm
@ -71,12 +68,10 @@ export default function Recover() {
display: 'flex', display: 'flex',
flexDirection: 'column', flexDirection: 'column',
marginTop: '12px', marginTop: '12px',
}} }}>
>
<Button <Button
variant="link" variant="link"
onClick={() => SetMessageDialogView(true)} onClick={() => SetMessageDialogView(true)}>
>
{constants.NO_RECOVERY_KEY} {constants.NO_RECOVERY_KEY}
</Button> </Button>
<Button variant="link" onClick={router.back}> <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 { getData, LS_KEYS } from 'utils/storage/localStorage';
import SignUp from 'components/SignUp'; import SignUp from 'components/SignUp';
export default function SignUpPage() { export default function SignUpPage() {
const router = useRouter(); const router = useRouter();
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
@ -29,14 +28,16 @@ export default function SignUpPage() {
}; };
return ( return (
<Container>{ <Container>
loading ? <EnteSpinner /> : {loading ? (
<EnteSpinner />
) : (
<Card style={{ minWidth: '320px' }} className="text-center"> <Card style={{ minWidth: '320px' }} className="text-center">
<Card.Body style={{ padding: '40px 30px' }}> <Card.Body style={{ padding: '40px 30px' }}>
<SignUp login={login} /> <SignUp login={login} />
</Card.Body> </Card.Body>
</Card> </Card>
} )}
</Container> </Container>
); );
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -7,7 +7,7 @@ import CryptoWorker from 'utils/crypto';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import { getPublicKey, User } from './userService'; import { getPublicKey, User } from './userService';
import { B64EncryptionResult } from './uploadService'; import { B64EncryptionResult } from 'utils/crypto';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File } from './fileService'; import { File } from './fileService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -52,7 +52,7 @@ export interface CollectionAndItsLatestFile {
const getCollectionWithSecrets = async ( const getCollectionWithSecrets = async (
collection: Collection, collection: Collection,
masterKey: string, masterKey: string
) => { ) => {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const userID = getData(LS_KEYS.USER).id; const userID = getData(LS_KEYS.USER).id;
@ -61,26 +61,27 @@ const getCollectionWithSecrets = async (
decryptedKey = await worker.decryptB64( decryptedKey = await worker.decryptB64(
collection.encryptedKey, collection.encryptedKey,
collection.keyDecryptionNonce, collection.keyDecryptionNonce,
masterKey, masterKey
); );
} else { } else {
const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES); const keyAttributes = getData(LS_KEYS.KEY_ATTRIBUTES);
const secretKey = await worker.decryptB64( const secretKey = await worker.decryptB64(
keyAttributes.encryptedSecretKey, keyAttributes.encryptedSecretKey,
keyAttributes.secretKeyDecryptionNonce, keyAttributes.secretKeyDecryptionNonce,
masterKey, masterKey
); );
decryptedKey = await worker.boxSealOpen( decryptedKey = await worker.boxSealOpen(
collection.encryptedKey, collection.encryptedKey,
keyAttributes.publicKey, keyAttributes.publicKey,
secretKey, secretKey
); );
} }
collection.name = collection.name || collection.name =
collection.name ||
(await worker.decryptToUTF8( (await worker.decryptToUTF8(
collection.encryptedName, collection.encryptedName,
collection.nameDecryptionNonce, collection.nameDecryptionNonce,
decryptedKey, decryptedKey
)); ));
return { return {
...collection, ...collection,
@ -91,7 +92,7 @@ const getCollectionWithSecrets = async (
const getCollections = async ( const getCollections = async (
token: string, token: string,
sinceTime: number, sinceTime: number,
key: string, key: string
): Promise<Collection[]> => { ): Promise<Collection[]> => {
try { try {
const resp = await HTTPService.get( const resp = await HTTPService.get(
@ -99,7 +100,7 @@ const getCollections = async (
{ {
sinceTime, sinceTime,
}, },
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
const promises: Promise<Collection>[] = resp.data.collections.map( const promises: Promise<Collection>[] = resp.data.collections.map(
async (collection: Collection) => { async (collection: Collection) => {
@ -110,16 +111,16 @@ const getCollections = async (
try { try {
collectionWithSecrets = await getCollectionWithSecrets( collectionWithSecrets = await getCollectionWithSecrets(
collection, collection,
key, key
); );
return collectionWithSecrets; return collectionWithSecrets;
} catch (e) { } catch (e) {
logError( logError(
e, e,
`decryption failed for collection with id=${collection.id}`, `decryption failed for collection with id=${collection.id}`
); );
} }
}, }
); );
return await Promise.all(promises); return await Promise.all(promises);
} catch (e) { } catch (e) {
@ -129,18 +130,21 @@ const getCollections = async (
}; };
export const getLocalCollections = async (): Promise<Collection[]> => { export const getLocalCollections = async (): Promise<Collection[]> => {
const collections: Collection[] = (await localForage.getItem(COLLECTIONS)) ?? []; const collections: Collection[] =
(await localForage.getItem(COLLECTIONS)) ?? [];
return 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 () => { export const syncCollections = async () => {
const localCollections = await getLocalCollections(); const localCollections = await getLocalCollections();
const lastCollectionUpdationTime = await getCollectionUpdationTime(); const lastCollectionUpdationTime = await getCollectionUpdationTime();
const token = getToken(); const token = getToken();
const key = await getActualKey(); const key = await getActualKey();
const updatedCollections = (await getCollections(token, lastCollectionUpdationTime, key)) ?? []; const updatedCollections =
(await getCollections(token, lastCollectionUpdationTime, key)) ?? [];
if (updatedCollections.length === 0) { if (updatedCollections.length === 0) {
return localCollections; return localCollections;
} }
@ -153,7 +157,7 @@ export const syncCollections = async () => {
if ( if (
!latestCollectionsInstances.has(collection.id) || !latestCollectionsInstances.has(collection.id) ||
latestCollectionsInstances.get(collection.id).updationTime < latestCollectionsInstances.get(collection.id).updationTime <
collection.updationTime collection.updationTime
) { ) {
latestCollectionsInstances.set(collection.id, collection); latestCollectionsInstances.set(collection.id, collection);
} }
@ -161,7 +165,7 @@ export const syncCollections = async () => {
const collections: Collection[] = []; const collections: Collection[] = [];
let updationTime = await localForage.getItem<number>( let updationTime = await localForage.getItem<number>(
COLLECTION_UPDATION_TIME, COLLECTION_UPDATION_TIME
); );
// eslint-disable-next-line @typescript-eslint/no-unused-vars // eslint-disable-next-line @typescript-eslint/no-unused-vars
for (const [_, collection] of latestCollectionsInstances) { for (const [_, collection] of latestCollectionsInstances) {
@ -179,7 +183,7 @@ export const syncCollections = async () => {
export const getCollectionsAndTheirLatestFile = ( export const getCollectionsAndTheirLatestFile = (
collections: Collection[], collections: Collection[],
files: File[], files: File[]
): CollectionAndItsLatestFile[] => { ): CollectionAndItsLatestFile[] => {
const latestFile = new Map<number, File>(); const latestFile = new Map<number, File>();
@ -213,15 +217,16 @@ export const getFavItemIds = async (files: File[]): Promise<Set<number>> => {
return new Set( return new Set(
files files
.filter((file) => file.collectionID === favCollection.id) .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 ( export const createCollection = async (
collectionName: string, collectionName: string,
type: CollectionType, type: CollectionType
): Promise<Collection> => { ): Promise<Collection> => {
try { try {
const existingCollections = await syncCollections(); const existingCollections = await syncCollections();
@ -239,14 +244,14 @@ export const createCollection = async (
nonce: keyDecryptionNonce, nonce: keyDecryptionNonce,
}: B64EncryptionResult = await worker.encryptToB64( }: B64EncryptionResult = await worker.encryptToB64(
collectionKey, collectionKey,
encryptionKey, encryptionKey
); );
const { const {
encryptedData: encryptedName, encryptedData: encryptedName,
nonce: nameDecryptionNonce, nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8( }: B64EncryptionResult = await worker.encryptUTF8(
collectionName, collectionName,
collectionKey, collectionKey
); );
const newCollection: Collection = { const newCollection: Collection = {
id: null, id: null,
@ -263,11 +268,11 @@ export const createCollection = async (
}; };
let createdCollection: Collection = await postCollection( let createdCollection: Collection = await postCollection(
newCollection, newCollection,
token, token
); );
createdCollection = await getCollectionWithSecrets( createdCollection = await getCollectionWithSecrets(
createdCollection, createdCollection,
encryptionKey, encryptionKey
); );
return createdCollection; return createdCollection;
} catch (e) { } catch (e) {
@ -278,14 +283,14 @@ export const createCollection = async (
const postCollection = async ( const postCollection = async (
collectionData: Collection, collectionData: Collection,
token: string, token: string
): Promise<Collection> => { ): Promise<Collection> => {
try { try {
const response = await HTTPService.post( const response = await HTTPService.post(
`${ENDPOINT}/collections`, `${ENDPOINT}/collections`,
collectionData, collectionData,
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
return response.data.collection; return response.data.collection;
} catch (e) { } catch (e) {
@ -298,7 +303,7 @@ export const addToFavorites = async (file: File) => {
if (!favCollection) { if (!favCollection) {
favCollection = await createCollection( favCollection = await createCollection(
'Favorites', 'Favorites',
CollectionType.favorites, CollectionType.favorites
); );
await localForage.setItem(FAV_COLLECTION, favCollection); await localForage.setItem(FAV_COLLECTION, favCollection);
} }
@ -312,7 +317,7 @@ export const removeFromFavorites = async (file: File) => {
export const addToCollection = async ( export const addToCollection = async (
collection: Collection, collection: Collection,
files: File[], files: File[]
) => { ) => {
try { try {
const params = {}; const params = {};
@ -322,7 +327,8 @@ export const addToCollection = async (
await Promise.all( await Promise.all(
files.map(async (file) => { files.map(async (file) => {
file.collectionID = collection.id; 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.encryptedKey = newEncryptedKey.encryptedData;
file.keyDecryptionNonce = newEncryptedKey.nonce; file.keyDecryptionNonce = newEncryptedKey.nonce;
if (params['files'] === undefined) { if (params['files'] === undefined) {
@ -334,13 +340,13 @@ export const addToCollection = async (
keyDecryptionNonce: file.keyDecryptionNonce, keyDecryptionNonce: file.keyDecryptionNonce,
}); });
return file; return file;
}), })
); );
await HTTPService.post( await HTTPService.post(
`${ENDPOINT}/collections/add-files`, `${ENDPOINT}/collections/add-files`,
params, params,
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
} catch (e) { } catch (e) {
logError(e, 'Add to collection Failed '); logError(e, 'Add to collection Failed ');
@ -357,13 +363,13 @@ const removeFromCollection = async (collection: Collection, files: File[]) => {
params['fileIDs'] = []; params['fileIDs'] = [];
} }
params['fileIDs'].push(file.id); params['fileIDs'].push(file.id);
}), })
); );
await HTTPService.post( await HTTPService.post(
`${ENDPOINT}/collections/remove-files`, `${ENDPOINT}/collections/remove-files`,
params, params,
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
} catch (e) { } catch (e) {
logError(e, 'remove from collection failed '); logError(e, 'remove from collection failed ');
@ -374,7 +380,7 @@ export const deleteCollection = async (
collectionID: number, collectionID: number,
syncWithRemote: () => Promise<void>, syncWithRemote: () => Promise<void>,
redirectToAll: () => void, redirectToAll: () => void,
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -383,7 +389,7 @@ export const deleteCollection = async (
`${ENDPOINT}/collections/${collectionID}`, `${ENDPOINT}/collections/${collectionID}`,
null, null,
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
await syncWithRemote(); await syncWithRemote();
redirectToAll(); redirectToAll();
@ -399,7 +405,7 @@ export const deleteCollection = async (
export const renameCollection = async ( export const renameCollection = async (
collection: Collection, collection: Collection,
newCollectionName: string, newCollectionName: string
) => { ) => {
const token = getToken(); const token = getToken();
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
@ -408,7 +414,7 @@ export const renameCollection = async (
nonce: nameDecryptionNonce, nonce: nameDecryptionNonce,
}: B64EncryptionResult = await worker.encryptUTF8( }: B64EncryptionResult = await worker.encryptUTF8(
newCollectionName, newCollectionName,
collection.key, collection.key
); );
const collectionRenameRequest = { const collectionRenameRequest = {
collectionID: collection.id, collectionID: collection.id,
@ -421,12 +427,12 @@ export const renameCollection = async (
null, null,
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
}; };
export const shareCollection = async ( export const shareCollection = async (
collection: Collection, collection: Collection,
withUserEmail: string, withUserEmail: string
) => { ) => {
try { try {
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
@ -435,7 +441,7 @@ export const shareCollection = async (
const publicKey: string = await getPublicKey(withUserEmail); const publicKey: string = await getPublicKey(withUserEmail);
const encryptedKey: string = await worker.boxSeal( const encryptedKey: string = await worker.boxSeal(
collection.key, collection.key,
publicKey, publicKey
); );
const shareCollectionRequest = { const shareCollectionRequest = {
collectionID: collection.id, collectionID: collection.id,
@ -448,7 +454,7 @@ export const shareCollection = async (
null, null,
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
} catch (e) { } catch (e) {
logError(e, 'share collection failed '); logError(e, 'share collection failed ');
@ -458,7 +464,7 @@ export const shareCollection = async (
export const unshareCollection = async ( export const unshareCollection = async (
collection: Collection, collection: Collection,
withUserEmail: string, withUserEmail: string
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -472,7 +478,7 @@ export const unshareCollection = async (
null, null,
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
} catch (e) { } catch (e) {
logError(e, 'unshare collection failed '); logError(e, 'unshare collection failed ');
@ -492,11 +498,13 @@ export const getFavCollection = async () => {
export const getNonEmptyCollections = ( export const getNonEmptyCollections = (
collections: Collection[], collections: Collection[],
files: File[], files: File[]
) => { ) => {
const nonEmptyCollectionsIds = new Set<number>(); const nonEmptyCollectionsIds = new Set<number>();
for (const file of files) { for (const file of files) {
nonEmptyCollectionsIds.add(file.collectionID); 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 { getToken } from 'utils/common/key';
import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil'; import { getFileUrl, getThumbnailUrl } from 'utils/common/apiUtil';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file'; import {
fileIsHEIC,
convertHEIC2JPEG,
fileNameWithoutExtension,
generateStreamFromArrayBuffer,
} from 'utils/file';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { File } from './fileService'; import { File, FILE_TYPE } from './fileService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { decodeMotionPhoto } from './motionPhotoService';
class DownloadManager { class DownloadManager {
private fileDownloads = new Map<string, string>(); private fileDownloads = new Map<string, string>();
@ -36,36 +42,50 @@ class DownloadManager {
getThumbnailUrl(file.id), getThumbnailUrl(file.id),
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }, { responseType: 'arraybuffer' }
); );
const worker = await new CryptoWorker(); const worker = await new CryptoWorker();
const decrypted: any = await worker.decryptThumbnail( const decrypted: any = await worker.decryptThumbnail(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.thumbnail.decryptionHeader), await worker.fromB64(file.thumbnail.decryptionHeader),
file.key, file.key
); );
try { try {
await cache.put( await cache.put(
file.id.toString(), file.id.toString(),
new Response(new Blob([decrypted])), new Response(new Blob([decrypted]))
); );
} catch (e) { } catch (e) {
// TODO: handle storage full exception. // TODO: handle storage full exception.
} }
return URL.createObjectURL(new Blob([decrypted])); return URL.createObjectURL(new Blob([decrypted]));
} };
getFile = async (file: File, forPreview=false) => { getFile = async (file: File, forPreview = false) => {
try { try {
if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) { if (!this.fileDownloads.get(`${file.id}_${forPreview}`)) {
// unzip motion photo and return fileBlob of the image for preview
const fileStream = await this.downloadFile(file); const fileStream = await this.downloadFile(file);
let fileBlob= await new Response(fileStream).blob(); let fileBlob = await new Response(fileStream).blob();
if (forPreview) { 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)) { if (fileIsHEIC(file.metadata.title)) {
fileBlob = await convertHEIC2JPEG(fileBlob); 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}`); return this.fileDownloads.get(`${file.id}_${forPreview}`);
} catch (e) { } catch (e) {
@ -79,25 +99,22 @@ class DownloadManager {
if (!token) { if (!token) {
return null; 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( const resp = await HTTPService.get(
getFileUrl(file.id), getFileUrl(file.id),
null, null,
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token },
{ responseType: 'arraybuffer' }, { responseType: 'arraybuffer' }
); );
const decrypted: any = await worker.decryptFile( const decrypted: any = await worker.decryptFile(
new Uint8Array(resp.data), new Uint8Array(resp.data),
await worker.fromB64(file.file.decryptionHeader), await worker.fromB64(file.file.decryptionHeader),
file.key, file.key
); );
return generateStreamFromArrayBuffer(decrypted);
return new ReadableStream({
async start(controller: ReadableStreamDefaultController) {
controller.enqueue(decrypted);
controller.close();
},
});
} }
const resp = await fetch(getFileUrl(file.id), { const resp = await fetch(getFileUrl(file.id), {
headers: { headers: {
@ -108,13 +125,11 @@ class DownloadManager {
const stream = new ReadableStream({ const stream = new ReadableStream({
async start(controller) { async start(controller) {
const decryptionHeader = await worker.fromB64( const decryptionHeader = await worker.fromB64(
file.file.decryptionHeader, file.file.decryptionHeader
); );
const fileKey = await worker.fromB64(file.key); const fileKey = await worker.fromB64(file.key);
const { const { pullState, decryptionChunkSize } =
pullState, await worker.initDecryption(decryptionHeader, fileKey);
decryptionChunkSize,
} = await worker.initDecryption(decryptionHeader, fileKey);
let data = new Uint8Array(); let data = new Uint8Array();
// The following function handles each data chunk // The following function handles each data chunk
function push() { function push() {
@ -123,24 +138,20 @@ class DownloadManager {
// Is there more data to read? // Is there more data to read?
if (!done) { if (!done) {
const buffer = new Uint8Array( const buffer = new Uint8Array(
data.byteLength + value.byteLength, data.byteLength + value.byteLength
); );
buffer.set(new Uint8Array(data), 0); buffer.set(new Uint8Array(data), 0);
buffer.set( buffer.set(new Uint8Array(value), data.byteLength);
new Uint8Array(value),
data.byteLength,
);
if (buffer.length > decryptionChunkSize) { if (buffer.length > decryptionChunkSize) {
const fileData = buffer.slice( const fileData = buffer.slice(
0, 0,
decryptionChunkSize, decryptionChunkSize
);
const {
decryptedData,
} = await worker.decryptChunk(
fileData,
pullState,
); );
const { decryptedData } =
await worker.decryptChunk(
fileData,
pullState
);
controller.enqueue(decryptedData); controller.enqueue(decryptedData);
data = buffer.slice(decryptionChunkSize); data = buffer.slice(decryptionChunkSize);
} else { } else {
@ -149,12 +160,8 @@ class DownloadManager {
push(); push();
} else { } else {
if (data) { if (data) {
const { const { decryptedData } =
decryptedData, await worker.decryptChunk(data, pullState);
} = await worker.decryptChunk(
data,
pullState,
);
controller.enqueue(decryptedData); controller.enqueue(decryptedData);
data = null; data = null;
} }

View file

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

View file

@ -19,6 +19,13 @@ export interface fileAttribute {
decryptionHeader: string; decryptionHeader: string;
} }
export enum FILE_TYPE {
IMAGE,
VIDEO,
LIVE_PHOTO,
OTHERS,
}
export interface File { export interface File {
id: number; id: number;
collectionID: number; collectionID: number;
@ -43,7 +50,10 @@ export const getLocalFiles = async () => {
return files; 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(); const localFiles = await getLocalFiles();
let files = await removeDeletedCollectionFiles(collections, localFiles); let files = await removeDeletedCollectionFiles(collections, localFiles);
if (files.length !== localFiles.length) { if (files.length !== localFiles.length) {
@ -54,11 +64,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
if (!getToken()) { if (!getToken()) {
continue; 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) { if (collection.updationTime === lastSyncTime) {
continue; continue;
} }
const fetchedFiles = (await getFiles(collection, lastSyncTime, DIFF_LIMIT, files, setFiles)) ?? []; const fetchedFiles =
(await getFiles(
collection,
lastSyncTime,
DIFF_LIMIT,
files,
setFiles
)) ?? [];
files.push(...fetchedFiles); files.push(...fetchedFiles);
const latestVersionFiles = new Map<string, File>(); const latestVersionFiles = new Map<string, File>();
files.forEach((file) => { files.forEach((file) => {
@ -78,17 +96,19 @@ export const syncFiles = async (collections: Collection[], setFiles: (files: Fil
} }
files.push(file); files.push(file);
} }
files=sortFiles(files); files = sortFiles(files);
await localForage.setItem('files', files); await localForage.setItem('files', files);
await localForage.setItem( await localForage.setItem(
`${collection.id}-time`, `${collection.id}-time`,
collection.updationTime, collection.updationTime
);
setFiles(
files.map((item) => ({
...item,
w: window.innerWidth,
h: window.innerHeight,
}))
); );
setFiles(files.map((item) => ({
...item,
w: window.innerWidth,
h: window.innerHeight,
})));
} }
return { return {
files: files.map((item) => ({ files: files.map((item) => ({
@ -104,11 +124,12 @@ export const getFiles = async (
sinceTime: number, sinceTime: number,
limit: number, limit: number,
files: File[], files: File[],
setFiles: (files: File[]) => void, setFiles: (files: File[]) => void
): Promise<File[]> => { ): Promise<File[]> => {
try { try {
const decryptedFiles: File[] = []; const decryptedFiles: File[] = [];
let time = sinceTime || let time =
sinceTime ||
(await localForage.getItem<number>(`${collection.id}-time`)) || (await localForage.getItem<number>(`${collection.id}-time`)) ||
0; 0;
let resp; let resp;
@ -126,7 +147,7 @@ export const getFiles = async (
}, },
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
decryptedFiles.push( decryptedFiles.push(
@ -136,16 +157,21 @@ export const getFiles = async (
file = await decryptFile(file, collection); file = await decryptFile(file, collection);
} }
return file; return file;
}) as Promise<File>[], }) as Promise<File>[]
)), ))
); );
if (resp.data.diff.length) { if (resp.data.diff.length) {
time = resp.data.diff.slice(-1)[0].updationTime; time = resp.data.diff.slice(-1)[0].updationTime;
} }
setFiles([...(files || []), ...decryptedFiles].filter((item) => !item.isDeleted).sort( setFiles(
(a, b) => b.metadata.creationTime - a.metadata.creationTime, [...(files || []), ...decryptedFiles]
)); .filter((item) => !item.isDeleted)
.sort(
(a, b) =>
b.metadata.creationTime - a.metadata.creationTime
)
);
} while (resp.data.diff.length === limit); } while (resp.data.diff.length === limit);
return decryptedFiles; return decryptedFiles;
} catch (e) { } catch (e) {
@ -155,7 +181,7 @@ export const getFiles = async (
const removeDeletedCollectionFiles = async ( const removeDeletedCollectionFiles = async (
collections: Collection[], collections: Collection[],
files: File[], files: File[]
) => { ) => {
const syncedCollectionIds = new Set<number>(); const syncedCollectionIds = new Set<number>();
for (const collection of collections) { for (const collection of collections) {
@ -168,7 +194,7 @@ const removeDeletedCollectionFiles = async (
export const deleteFiles = async ( export const deleteFiles = async (
filesToDelete: number[], filesToDelete: number[],
clearSelection: Function, clearSelection: Function,
syncWithRemote: Function, syncWithRemote: Function
) => { ) => {
try { try {
const token = getToken(); const token = getToken();
@ -181,7 +207,7 @@ export const deleteFiles = async (
null, null,
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
clearSelection(); clearSelection();
syncWithRemote(); 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.reverse();
} }
return dates; return dates;
} if (date1) { }
if (date1) {
return [{ month: date1.getMonth() }]; return [{ month: date1.getMonth() }];
} }
return []; return [];
} }
export async function searchLocation( export async function searchLocation(
searchPhrase: string, searchPhrase: string
): Promise<LocationSearchResponse[]> { ): Promise<LocationSearchResponse[]> {
const resp = await HTTPService.get( const resp = await HTTPService.get(
`${ENDPOINT}/search/location`, `${ENDPOINT}/search/location`,
@ -48,7 +49,7 @@ export async function searchLocation(
}, },
{ {
'X-Auth-Token': getToken(), 'X-Auth-Token': getToken(),
}, }
); );
return resp.data.results; return resp.data.results;
} }
@ -75,7 +76,9 @@ export function getHolidaySuggestion(searchPhrase: string): Suggestion[] {
value: { month: 11, date: 31 }, value: { month: 11, date: 31 },
type: SuggestionType.DATE, 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[] { export function getYearSuggestion(searchPhrase: string): Suggestion[] {

View file

@ -1,15 +1,11 @@
import { getEndpoint } from 'utils/common/apiUtil'; import { getEndpoint } from 'utils/common/apiUtil';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import EXIF from 'exif-js'; import EXIF from 'exif-js';
import { File, fileAttribute } from './fileService'; import { File, fileAttribute, FILE_TYPE } from './fileService';
import { Collection } from './collectionService'; import { Collection } from './collectionService';
import { FILE_TYPE, SetFiles } from 'pages/gallery'; import { SetFiles } from 'pages/gallery';
import { retryAsyncFunction, sleep } from 'utils/common'; import { retryAsyncFunction, sleep } from 'utils/common';
import { import { handleError, parseError, CustomError } from 'utils/common/errorUtil';
handleError,
parseError,
THUMBNAIL_GENERATION_FAILED,
} from 'utils/common/errorUtil';
import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto'; import { ComlinkWorker, getDedicatedCryptoWorker } from 'utils/crypto';
import * as convert from 'xml-js'; import * as convert from 'xml-js';
import { ENCRYPTION_CHUNK_SIZE } from 'types'; import { ENCRYPTION_CHUNK_SIZE } from 'types';
@ -46,10 +42,12 @@ const FILE_UPLOAD_COMPLETED = 100;
const EDITED_FILE_SUFFIX = '-edited'; const EDITED_FILE_SUFFIX = '-edited';
const TwoSecondInMillSeconds = 2000; const TwoSecondInMillSeconds = 2000;
export enum FileUploadErrorCode { export enum FileUploadResults {
FAILED = -1, FAILED = -1,
SKIPPED = -2, SKIPPED = -2,
UNSUPPORTED = -3, UNSUPPORTED = -3,
BLOCKED = -4,
UPLOADED = 100,
} }
interface Location { interface Location {
@ -119,7 +117,7 @@ interface ProcessedFile {
metadata: fileAttribute; metadata: fileAttribute;
filename: string; filename: string;
} }
interface BackupedFile extends Omit<ProcessedFile, 'filename'> { } interface BackupedFile extends Omit<ProcessedFile, 'filename'> {}
interface uploadFile extends BackupedFile { interface uploadFile extends BackupedFile {
collectionID: number; collectionID: number;
@ -142,31 +140,34 @@ class UploadService {
private filesCompleted: number; private filesCompleted: number;
private totalFileCount: number; private totalFileCount: number;
private fileProgress: Map<string, number>; private fileProgress: Map<string, number>;
private uploadResult: Map<string, number>;
private metadataMap: Map<string, Object>; private metadataMap: Map<string, Object>;
private filesToBeUploaded: FileWithCollection[]; private filesToBeUploaded: FileWithCollection[];
private progressBarProps; private progressBarProps;
private failedFiles: FileWithCollection[]; private failedFiles: FileWithCollection[];
private existingFilesCollectionWise: Map<number, File[]>; private existingFilesCollectionWise: Map<number, File[]>;
private existingFiles: File[]; private existingFiles: File[];
private setFiles:SetFiles; private setFiles: SetFiles;
public async uploadFiles( public async uploadFiles(
filesWithCollectionToUpload: FileWithCollection[], filesWithCollectionToUpload: FileWithCollection[],
existingFiles: File[], existingFiles: File[],
progressBarProps, progressBarProps,
setFiles:SetFiles, setFiles: SetFiles
) { ) {
try { try {
progressBarProps.setUploadStage(UPLOAD_STAGES.START); progressBarProps.setUploadStage(UPLOAD_STAGES.START);
this.filesCompleted = 0; this.filesCompleted = 0;
this.fileProgress = new Map<string, number>(); this.fileProgress = new Map<string, number>();
this.uploadResult = new Map<string, number>();
this.failedFiles = []; this.failedFiles = [];
this.metadataMap = new Map<string, object>(); this.metadataMap = new Map<string, object>();
this.progressBarProps = progressBarProps; this.progressBarProps = progressBarProps;
this.existingFiles=existingFiles; this.existingFiles = existingFiles;
this.existingFilesCollectionWise = sortFilesIntoCollections(existingFiles); this.existingFilesCollectionWise =
sortFilesIntoCollections(existingFiles);
this.updateProgressBarUI(); this.updateProgressBarUI();
this.setFiles=setFiles; this.setFiles = setFiles;
const metadataFiles: globalThis.File[] = []; const metadataFiles: globalThis.File[] = [];
const actualFiles: FileWithCollection[] = []; const actualFiles: FileWithCollection[] = [];
filesWithCollectionToUpload.forEach((fileWithCollection) => { filesWithCollectionToUpload.forEach((fileWithCollection) => {
@ -184,7 +185,7 @@ class UploadService {
this.filesToBeUploaded = actualFiles; this.filesToBeUploaded = actualFiles;
progressBarProps.setUploadStage( progressBarProps.setUploadStage(
UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES, UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES
); );
this.totalFileCount = metadataFiles.length; this.totalFileCount = metadataFiles.length;
this.perFileProgress = 100 / metadataFiles.length; this.perFileProgress = 100 / metadataFiles.length;
@ -211,20 +212,16 @@ class UploadService {
} }
} }
const uploadProcesses = []; const uploadProcesses = [];
for ( for (let i = 0; i < MAX_CONCURRENT_UPLOADS; i++) {
let i = 0; if (this.filesToBeUploaded.length > 0) {
i < MAX_CONCURRENT_UPLOADS; const fileWithCollection = this.filesToBeUploaded.pop();
i++
) {
if (this.filesToBeUploaded.length>0) {
const fileWithCollection= this.filesToBeUploaded.pop();
this.cryptoWorkers[i] = getDedicatedCryptoWorker(); this.cryptoWorkers[i] = getDedicatedCryptoWorker();
uploadProcesses.push( uploadProcesses.push(
this.uploader( this.uploader(
await new this.cryptoWorkers[i].comlink(), await new this.cryptoWorkers[i].comlink(),
new FileReader(), new FileReader(),
fileWithCollection, fileWithCollection
), )
); );
} }
} }
@ -246,75 +243,97 @@ class UploadService {
private async uploader( private async uploader(
worker: any, worker: any,
reader: FileReader, reader: FileReader,
fileWithCollection: FileWithCollection, fileWithCollection: FileWithCollection
) { ) {
const { file: rawFile, collection } = fileWithCollection; const { file: rawFile, collection } = fileWithCollection;
this.fileProgress.set(rawFile.name, 0); this.fileProgress.set(rawFile.name, 0);
this.updateProgressBarUI(); this.updateProgressBarUI();
let file: FileInMemory = null;
let encryptedFile: EncryptedFile = null;
try { 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)) { if (this.fileAlreadyInCollection(file, collection)) {
// set progress to -2 indicating that file upload was skipped // 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(); this.updateProgressBarUI();
await sleep(TwoSecondInMillSeconds); await sleep(TwoSecondInMillSeconds);
// remove completed files for file progress list
this.fileProgress.delete(rawFile.name);
} else { } else {
let encryptedFile: EncryptedFile = encryptedFile = await this.encryptFile(
await this.encryptFile(worker, file, collection.key); worker,
file,
collection.key
);
let backupedFile: BackupedFile = await this.uploadToBucket( const backupedFile: BackupedFile = await this.uploadToBucket(
encryptedFile.file, encryptedFile.file
); );
let uploadFile: uploadFile = this.getUploadFile( let uploadFile: uploadFile = this.getUploadFile(
collection, collection,
backupedFile, backupedFile,
encryptedFile.fileKey, encryptedFile.fileKey
); );
encryptedFile = null; const uploadedFile = await this.uploadFile(uploadFile);
backupedFile = null; const decryptedFile = await decryptFile(
uploadedFile,
const uploadedFile =await this.uploadFile(uploadFile); collection
const decryptedFile=await decryptFile(uploadedFile, collection); );
this.existingFiles.push(decryptedFile); this.existingFiles.push(decryptedFile);
this.existingFiles=sortFiles(this.existingFiles); this.existingFiles = sortFiles(this.existingFiles);
await localForage.setItem('files', removeUnneccessaryFileProps(this.existingFiles)); await localForage.setItem(
'files',
removeUnneccessaryFileProps(this.existingFiles)
);
this.setFiles(this.existingFiles); this.setFiles(this.existingFiles);
file = null;
uploadFile = null; uploadFile = null;
this.fileProgress.set(rawFile.name, FileUploadResults.UPLOADED);
this.fileProgress.delete(rawFile.name);
this.filesCompleted++; this.filesCompleted++;
} }
} catch (e) { } catch (e) {
logError(e, 'file upload failed'); 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); 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(); this.updateProgressBarUI();
if (this.filesToBeUploaded.length > 0) { if (this.filesToBeUploaded.length > 0) {
await this.uploader( await this.uploader(worker, reader, this.filesToBeUploaded.pop());
worker,
reader,
this.filesToBeUploaded.pop(),
);
} }
} }
async retryFailedFiles(localFiles:File[]) { 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() { private updateProgressBarUI() {
const { setPercentComplete, setFileCounter, setFileProgress } = const {
this.progressBarProps; setPercentComplete,
setFileCounter,
setFileProgress,
setUploadResult,
} = this.progressBarProps;
setFileCounter({ setFileCounter({
finished: this.filesCompleted, finished: this.filesCompleted,
total: this.totalFileCount, total: this.totalFileCount,
@ -332,11 +351,12 @@ class UploadService {
} }
setPercentComplete(percentComplete); setPercentComplete(percentComplete);
setFileProgress(this.fileProgress); setFileProgress(this.fileProgress);
setUploadResult(this.uploadResult);
} }
private fileAlreadyInCollection( private fileAlreadyInCollection(
newFile: FileInMemory, newFile: FileInMemory,
collection: Collection, collection: Collection
): boolean { ): boolean {
const collectionFiles = const collectionFiles =
this.existingFilesCollectionWise.get(collection.id) ?? []; this.existingFilesCollectionWise.get(collection.id) ?? [];
@ -349,7 +369,7 @@ class UploadService {
} }
private areFilesSame( private areFilesSame(
existingFile: MetadataObject, existingFile: MetadataObject,
newFile: MetadataObject, newFile: MetadataObject
): boolean { ): boolean {
if ( if (
existingFile.fileType === newFile.fileType && existingFile.fileType === newFile.fileType &&
@ -365,10 +385,8 @@ class UploadService {
private async readFile(reader: FileReader, receivedFile: globalThis.File) { private async readFile(reader: FileReader, receivedFile: globalThis.File) {
try { try {
const { thumbnail, hasStaticThumbnail } = await this.generateThumbnail( const { thumbnail, hasStaticThumbnail } =
reader, await this.generateThumbnail(reader, receivedFile);
receivedFile,
);
let fileType: FILE_TYPE; let fileType: FILE_TYPE;
switch (receivedFile.type.split('/')[0]) { switch (receivedFile.type.split('/')[0]) {
@ -392,13 +410,13 @@ class UploadService {
const { location, creationTime } = await this.getExifData( const { location, creationTime } = await this.getExifData(
reader, reader,
receivedFile, receivedFile,
fileType, fileType
); );
let receivedFileOriginalName = receivedFile.name; let receivedFileOriginalName = receivedFile.name;
if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) { if (receivedFile.name.endsWith(EDITED_FILE_SUFFIX)) {
receivedFileOriginalName = receivedFile.name.slice( receivedFileOriginalName = receivedFile.name.slice(
0, 0,
-1 * EDITED_FILE_SUFFIX.length, -1 * EDITED_FILE_SUFFIX.length
); );
} }
const metadata = Object.assign( const metadata = Object.assign(
@ -411,15 +429,15 @@ class UploadService {
longitude: location?.latitude, longitude: location?.latitude,
fileType, fileType,
}, },
this.metadataMap.get(receivedFileOriginalName), this.metadataMap.get(receivedFileOriginalName)
); );
if (hasStaticThumbnail) { if (hasStaticThumbnail) {
metadata['hasStaticThumbnail'] = hasStaticThumbnail; metadata['hasStaticThumbnail'] = hasStaticThumbnail;
} }
const filedata = const filedata =
receivedFile.size > MIN_STREAM_FILE_SIZE ? receivedFile.size > MIN_STREAM_FILE_SIZE
this.getFileStream(reader, receivedFile) : ? this.getFileStream(reader, receivedFile)
await this.getUint8ArrayView(reader, receivedFile); : await this.getUint8ArrayView(reader, receivedFile);
return { return {
filedata, filedata,
@ -435,13 +453,13 @@ class UploadService {
private async encryptFile( private async encryptFile(
worker: any, worker: any,
file: FileInMemory, file: FileInMemory,
encryptionKey: string, encryptionKey: string
): Promise<EncryptedFile> { ): Promise<EncryptedFile> {
try { try {
const { key: fileKey, file: encryptedFiledata }: EncryptionResult = const { key: fileKey, file: encryptedFiledata }: EncryptionResult =
isDataStream(file.filedata) ? isDataStream(file.filedata)
await this.encryptFileStream(worker, file.filedata) : ? await this.encryptFileStream(worker, file.filedata)
await worker.encryptFile(file.filedata); : await worker.encryptFile(file.filedata);
const { file: encryptedThumbnail }: EncryptionResult = const { file: encryptedThumbnail }: EncryptionResult =
await worker.encryptThumbnail(file.thumbnail, fileKey); await worker.encryptThumbnail(file.thumbnail, fileKey);
@ -450,7 +468,7 @@ class UploadService {
const encryptedKey: B64EncryptionResult = await worker.encryptToB64( const encryptedKey: B64EncryptionResult = await worker.encryptToB64(
fileKey, fileKey,
encryptionKey, encryptionKey
); );
const result: EncryptedFile = { const result: EncryptedFile = {
@ -481,7 +499,7 @@ class UploadService {
const encryptedFileChunk = await worker.encryptFileChunk( const encryptedFileChunk = await worker.encryptFileChunk(
value, value,
pushState, pushState,
ref.pullCount === chunkCount, ref.pullCount === chunkCount
); );
controller.enqueue(encryptedFileChunk); controller.enqueue(encryptedFileChunk);
if (ref.pullCount === chunkCount) { if (ref.pullCount === chunkCount) {
@ -505,30 +523,30 @@ class UploadService {
if (isDataStream(file.file.encryptedData)) { if (isDataStream(file.file.encryptedData)) {
const { chunkCount, stream } = file.file.encryptedData; const { chunkCount, stream } = file.file.encryptedData;
const uploadPartCount = Math.ceil( const uploadPartCount = Math.ceil(
chunkCount / CHUNKS_COMBINED_FOR_UPLOAD, chunkCount / CHUNKS_COMBINED_FOR_UPLOAD
); );
const filePartUploadURLs = await this.fetchMultipartUploadURLs( const filePartUploadURLs = await this.fetchMultipartUploadURLs(
uploadPartCount, uploadPartCount
); );
fileObjectKey = await this.putFileInParts( fileObjectKey = await this.putFileInParts(
filePartUploadURLs, filePartUploadURLs,
stream, stream,
file.filename, file.filename,
uploadPartCount, uploadPartCount
); );
} else { } else {
const fileUploadURL = await this.getUploadURL(); const fileUploadURL = await this.getUploadURL();
fileObjectKey = await this.putFile( fileObjectKey = await this.putFile(
fileUploadURL, fileUploadURL,
file.file.encryptedData, file.file.encryptedData,
file.filename, file.filename
); );
} }
const thumbnailUploadURL = await this.getUploadURL(); const thumbnailUploadURL = await this.getUploadURL();
const thumbnailObjectKey = await this.putFile( const thumbnailObjectKey = await this.putFile(
thumbnailUploadURL, thumbnailUploadURL,
file.thumbnail.encryptedData as Uint8Array, file.thumbnail.encryptedData as Uint8Array,
null, null
); );
const backupedFile: BackupedFile = { const backupedFile: BackupedFile = {
@ -552,7 +570,7 @@ class UploadService {
private getUploadFile( private getUploadFile(
collection: Collection, collection: Collection,
backupedFile: BackupedFile, backupedFile: BackupedFile,
fileKey: B64EncryptionResult, fileKey: B64EncryptionResult
): uploadFile { ): uploadFile {
const uploadFile: uploadFile = { const uploadFile: uploadFile = {
collectionID: collection.id, collectionID: collection.id,
@ -564,20 +582,17 @@ class UploadService {
return uploadFile; return uploadFile;
} }
private async uploadFile(uploadFile: uploadFile):Promise<File> { private async uploadFile(uploadFile: uploadFile): Promise<File> {
try { try {
const token = getToken(); const token = getToken();
if (!token) { if (!token) {
return; return;
} }
const response = await retryAsyncFunction(()=>HTTPService.post( const response = await retryAsyncFunction(() =>
`${ENDPOINT}/files`, HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
uploadFile,
null,
{
'X-Auth-Token': token, 'X-Auth-Token': token,
}, })
)); );
return response.data; return response.data;
} catch (e) { } catch (e) {
logError(e, 'upload Files Failed'); logError(e, 'upload Files Failed');
@ -590,17 +605,19 @@ class UploadService {
const metadataJSON: object = await new Promise( const metadataJSON: object = await new Promise(
(resolve, reject) => { (resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onabort = () => reject(Error('file reading was aborted')); reader.onabort = () =>
reader.onerror = () => reject(Error('file reading has failed')); reject(Error('file reading was aborted'));
reader.onerror = () =>
reject(Error('file reading has failed'));
reader.onload = () => { reader.onload = () => {
const result = const result =
typeof reader.result !== 'string' ? typeof reader.result !== 'string'
new TextDecoder().decode(reader.result) : ? new TextDecoder().decode(reader.result)
reader.result; : reader.result;
resolve(JSON.parse(result)); resolve(JSON.parse(result));
}; };
reader.readAsText(receivedFile); reader.readAsText(receivedFile);
}, }
); );
const metaDataObject = {}; const metaDataObject = {};
@ -647,8 +664,8 @@ class UploadService {
} }
private async generateThumbnail( private async generateThumbnail(
reader: FileReader, reader: FileReader,
file: globalThis.File, file: globalThis.File
): Promise<{ thumbnail: Uint8Array, hasStaticThumbnail: boolean }> { ): Promise<{ thumbnail: Uint8Array; hasStaticThumbnail: boolean }> {
try { try {
let hasStaticThumbnail = false; let hasStaticThumbnail = false;
const canvas = document.createElement('canvas'); const canvas = document.createElement('canvas');
@ -662,7 +679,7 @@ class UploadService {
file = new globalThis.File( file = new globalThis.File(
[await convertHEIC2JPEG(file)], [await convertHEIC2JPEG(file)],
null, null,
null, null
); );
} }
let image = new Image(); let image = new Image();
@ -672,7 +689,8 @@ class UploadService {
image.onload = () => { image.onload = () => {
try { try {
const thumbnailWidth = const thumbnailWidth =
(image.width * THUMBNAIL_HEIGHT) / image.height; (image.width * THUMBNAIL_HEIGHT) /
image.height;
canvas.width = thumbnailWidth; canvas.width = thumbnailWidth;
canvas.height = THUMBNAIL_HEIGHT; canvas.height = THUMBNAIL_HEIGHT;
canvas_CTX.drawImage( canvas_CTX.drawImage(
@ -680,7 +698,7 @@ class UploadService {
0, 0,
0, 0,
thumbnailWidth, thumbnailWidth,
THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT
); );
image = null; image = null;
clearTimeout(timeout); clearTimeout(timeout);
@ -688,15 +706,23 @@ class UploadService {
} catch (e) { } catch (e) {
reject(e); reject(e);
logError(e); logError(e);
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`)); reject(
Error(
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
)
);
} }
}; };
timeout = setTimeout( timeout = setTimeout(
() => () =>
reject( 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 { } else {
@ -718,7 +744,7 @@ class UploadService {
0, 0,
0, 0,
thumbnailWidth, thumbnailWidth,
THUMBNAIL_HEIGHT, THUMBNAIL_HEIGHT
); );
video = null; video = null;
clearTimeout(timeout); clearTimeout(timeout);
@ -726,16 +752,26 @@ class UploadService {
} catch (e) { } catch (e) {
reject(e); reject(e);
logError(e); logError(e);
reject(Error(`${THUMBNAIL_GENERATION_FAILED} err: ${e}`)); reject(
Error(
`${CustomError.THUMBNAIL_GENERATION_FAILED} err: ${e}`
)
);
} }
}); });
video.preload = 'metadata'; video.preload = 'metadata';
video.src = imageURL; video.src = imageURL;
video.currentTime = 3; video.currentTime = 3;
setTimeout( timeout = setTimeout(
() => () =>
reject(Error(`wait time exceeded for format ${file.name.split('.').slice(-1)[0]}`)), reject(
WAIT_TIME_THUMBNAIL_GENERATION, Error(
`wait time exceeded for format ${
file.name.split('.').slice(-1)[0]
}`
)
),
WAIT_TIME_THUMBNAIL_GENERATION
); );
}); });
} }
@ -758,7 +794,7 @@ class UploadService {
resolve(blob); resolve(blob);
}, },
'image/jpeg', 'image/jpeg',
quality, quality
); );
}); });
thumbnailBlob = thumbnailBlob ?? new Blob([]); thumbnailBlob = thumbnailBlob ?? new Blob([]);
@ -768,7 +804,7 @@ class UploadService {
); );
const thumbnail = await this.getUint8ArrayView( const thumbnail = await this.getUint8ArrayView(
reader, reader,
thumbnailBlob, thumbnailBlob
); );
return { thumbnail, hasStaticThumbnail }; return { thumbnail, hasStaticThumbnail };
} catch (e) { } catch (e) {
@ -781,7 +817,7 @@ class UploadService {
const self = this; const self = this;
const fileChunkReader = (async function* fileChunkReaderMaker( const fileChunkReader = (async function* fileChunkReaderMaker(
fileSize, fileSize,
self, self
) { ) {
let offset = 0; let offset = 0;
while (offset < fileSize) { while (offset < fileSize) {
@ -809,18 +845,19 @@ class UploadService {
private async getUint8ArrayView( private async getUint8ArrayView(
reader: FileReader, reader: FileReader,
file: Blob, file: Blob
): Promise<Uint8Array> { ): Promise<Uint8Array> {
try { try {
return await new Promise((resolve, reject) => { 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.onerror = () => reject(Error('file reading has failed'));
reader.onload = () => { reader.onload = () => {
// Do whatever you want with the file contents // Do whatever you want with the file contents
const result = const result =
typeof reader.result === 'string' ? typeof reader.result === 'string'
new TextEncoder().encode(reader.result) : ? new TextEncoder().encode(reader.result)
new Uint8Array(reader.result); : new Uint8Array(reader.result);
resolve(result); resolve(result);
}; };
reader.readAsArrayBuffer(file); reader.readAsArrayBuffer(file);
@ -851,10 +888,10 @@ class UploadService {
{ {
count: Math.min( count: Math.min(
MAX_URL_REQUESTS, 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; const response = await this.uploadURLFetchInProgress;
this.uploadURLs.push(...response.data['urls']); this.uploadURLs.push(...response.data['urls']);
@ -870,7 +907,7 @@ class UploadService {
} }
private async fetchMultipartUploadURLs( private async fetchMultipartUploadURLs(
count: number, count: number
): Promise<MultipartUploadURLs> { ): Promise<MultipartUploadURLs> {
try { try {
const token = getToken(); const token = getToken();
@ -882,7 +919,7 @@ class UploadService {
{ {
count, count,
}, },
{ 'X-Auth-Token': token }, { 'X-Auth-Token': token }
); );
return response.data['urls']; return response.data['urls'];
@ -895,17 +932,17 @@ class UploadService {
private async putFile( private async putFile(
fileUploadURL: UploadURL, fileUploadURL: UploadURL,
file: Uint8Array, file: Uint8Array,
filename: string, filename: string
): Promise<string> { ): Promise<string> {
try { try {
await retryAsyncFunction(()=> await retryAsyncFunction(() =>
HTTPService.put( HTTPService.put(
fileUploadURL.url, fileUploadURL.url,
file, file,
null, null,
null, null,
this.trackUploadProgress(filename), this.trackUploadProgress(filename)
), )
); );
return fileUploadURL.objectKey; return fileUploadURL.objectKey;
} catch (e) { } catch (e) {
@ -918,12 +955,12 @@ class UploadService {
multipartUploadURLs: MultipartUploadURLs, multipartUploadURLs: MultipartUploadURLs,
file: ReadableStream<Uint8Array>, file: ReadableStream<Uint8Array>,
filename: string, filename: string,
uploadPartCount: number, uploadPartCount: number
) { ) {
try { try {
const streamEncryptedFileReader = file.getReader(); const streamEncryptedFileReader = file.getReader();
const percentPerPart = Math.round( const percentPerPart = Math.round(
RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount, RANDOM_PERCENTAGE_PROGRESS_FOR_PUT() / uploadPartCount
); );
const resParts = []; const resParts = [];
for (const [ for (const [
@ -942,15 +979,25 @@ class UploadService {
} }
} }
const uploadChunk = Uint8Array.from(combinedChunks); const uploadChunk = Uint8Array.from(combinedChunks);
const response = await retryAsyncFunction(()=> const response = await retryAsyncFunction(async () => {
HTTPService.put( const resp = await HTTPService.put(
fileUploadURL, fileUploadURL,
uploadChunk, uploadChunk,
null, null,
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({ resParts.push({
PartNumber: index + 1, PartNumber: index + 1,
ETag: response.headers.etag, ETag: response.headers.etag,
@ -959,12 +1006,12 @@ class UploadService {
const options = { compact: true, ignoreComment: true, spaces: 4 }; const options = { compact: true, ignoreComment: true, spaces: 4 };
const body = convert.js2xml( const body = convert.js2xml(
{ CompleteMultipartUpload: { Part: resParts } }, { CompleteMultipartUpload: { Part: resParts } },
options, options
); );
await retryAsyncFunction(()=> await retryAsyncFunction(() =>
HTTPService.post(multipartUploadURLs.completeURL, body, null, { HTTPService.post(multipartUploadURLs.completeURL, body, null, {
'content-type': 'text/xml', 'content-type': 'text/xml',
}), })
); );
return multipartUploadURLs.objectKey; return multipartUploadURLs.objectKey;
} catch (e) { } catch (e) {
@ -976,15 +1023,15 @@ class UploadService {
private trackUploadProgress( private trackUploadProgress(
filename, filename,
percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(), percentPerPart = RANDOM_PERCENTAGE_PROGRESS_FOR_PUT(),
index = 0, index = 0
) { ) {
const cancel={ exec: null }; const cancel = { exec: null };
let timeout=null; let timeout = null;
const resetTimeout=()=>{ const resetTimeout = () => {
if (timeout) { if (timeout) {
clearTimeout(timeout); clearTimeout(timeout);
} }
timeout=setTimeout(()=>cancel.exec(), 30*1000); timeout = setTimeout(() => cancel.exec(), 30 * 1000);
}; };
return { return {
cancel, cancel,
@ -995,14 +1042,14 @@ class UploadService {
Math.min( Math.min(
Math.round( Math.round(
percentPerPart * index + percentPerPart * index +
(percentPerPart * event.loaded) / (percentPerPart * event.loaded) /
event.total, event.total
), ),
98, 98
), )
); );
this.updateProgressBarUI(); this.updateProgressBarUI();
if (event.loaded===event.total) { if (event.loaded === event.total) {
clearTimeout(timeout); clearTimeout(timeout);
} else { } else {
resetTimeout(); resetTimeout();
@ -1013,7 +1060,7 @@ class UploadService {
private async getExifData( private async getExifData(
reader: FileReader, reader: FileReader,
receivedFile: globalThis.File, receivedFile: globalThis.File,
fileType: FILE_TYPE, fileType: FILE_TYPE
): Promise<ParsedEXIFData> { ): Promise<ParsedEXIFData> {
try { try {
if (fileType === FILE_TYPE.VIDEO) { if (fileType === FILE_TYPE.VIDEO) {
@ -1039,15 +1086,16 @@ class UploadService {
} }
} }
private getUNIXTime(exifData: any) { private getUNIXTime(exifData: any) {
const dateString: string = exifData.DateTimeOriginal || exifData.DateTime; const dateString: string =
if (!dateString || dateString==='0000:00:00 00:00:00') { exifData.DateTimeOriginal || exifData.DateTime;
if (!dateString || dateString === '0000:00:00 00:00:00') {
return null; return null;
} }
const parts = dateString.split(' ')[0].split(':'); const parts = dateString.split(' ')[0].split(':');
const date = new Date( const date = new Date(
Number(parts[0]), Number(parts[0]),
Number(parts[1]) - 1, Number(parts[1]) - 1,
Number(parts[2]), Number(parts[2])
); );
return date.getTime() * 1000; return date.getTime() * 1000;
} }
@ -1072,14 +1120,14 @@ class UploadService {
latDegree, latDegree,
latMinute, latMinute,
latSecond, latSecond,
latDirection, latDirection
); );
const lonFinal = this.convertDMSToDD( const lonFinal = this.convertDMSToDD(
lonDegree, lonDegree,
lonMinute, lonMinute,
lonSecond, lonSecond,
lonDirection, lonDirection
); );
return { latitude: latFinal * 1.0, longitude: lonFinal * 1.0 }; 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 localForage from 'utils/storage/localForage';
import { getToken } from 'utils/common/key'; import { getToken } from 'utils/common/key';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { B64EncryptionResult } from './uploadService'; import { B64EncryptionResult } from 'utils/crypto';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { Subscription } from './billingService';
export interface UpdatedKey { export interface UpdatedKey {
kekSalt: string; kekSalt: string;
@ -35,7 +36,7 @@ export interface EmailVerificationResponse {
keyAttributes?: KeyAttributes; keyAttributes?: KeyAttributes;
encryptedToken?: string; encryptedToken?: string;
token?: string; token?: string;
twoFactorSessionID: string twoFactorSessionID: string;
} }
export interface TwoFactorVerificationResponse { export interface TwoFactorVerificationResponse {
@ -46,19 +47,28 @@ export interface TwoFactorVerificationResponse {
} }
export interface TwoFactorSecret { export interface TwoFactorSecret {
secretCode: string secretCode: string;
qrCode: string qrCode: string;
} }
export interface TwoFactorRecoveryResponse { export interface TwoFactorRecoveryResponse {
encryptedSecret: string encryptedSecret: string;
secretDecryptionNonce: string secretDecryptionNonce: string;
} }
export const getOtt = (email: string) => HTTPService.get(`${ENDPOINT}/users/ott`, { export interface UserDetails {
email, email: string;
client: 'web', usage: number;
}); fileCount: number;
sharedCollectionCount: number;
subscription: Subscription;
}
export const getOtt = (email: string) =>
HTTPService.get(`${ENDPOINT}/users/ott`, {
email,
client: 'web',
});
export const getPublicKey = async (email: string) => { export const getPublicKey = async (email: string) => {
const token = getToken(); const token = getToken();
@ -67,7 +77,7 @@ export const getPublicKey = async (email: string) => {
{ email }, { email },
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
return resp.data.publicKey; return resp.data.publicKey;
}; };
@ -80,34 +90,28 @@ export const getPaymentToken = async () => {
null, null,
{ {
'X-Auth-Token': token, 'X-Auth-Token': token,
}, }
); );
return resp.data['paymentToken']; 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( export const putAttributes = (token: string, keyAttributes: KeyAttributes) =>
`${ENDPOINT}/users/attributes`, HTTPService.put(`${ENDPOINT}/users/attributes`, { keyAttributes }, null, {
{ keyAttributes },
null,
{
'X-Auth-Token': token, 'X-Auth-Token': token,
}, });
);
export const setKeys = (token: string, updatedKey: UpdatedKey) => HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, { export const setKeys = (token: string, updatedKey: UpdatedKey) =>
'X-Auth-Token': token, HTTPService.put(`${ENDPOINT}/users/keys`, updatedKey, null, {
});
export const setRecoveryKey = (token: string, recoveryKey: RecoveryKey) => HTTPService.put(
`${ENDPOINT}/users/recovery-key`,
recoveryKey,
null,
{
'X-Auth-Token': token, '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 () => { export const logoutUser = async () => {
// ignore server logout result as logoutUser can be triggered before sign up or on token expiry // 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 () => { export const setupTwoFactor = async () => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/setup`, null, null, { const resp = await HTTPService.post(
'X-Auth-Token': getToken(), `${ENDPOINT}/users/two-factor/setup`,
}); null,
null,
{
'X-Auth-Token': getToken(),
}
);
return resp.data as TwoFactorSecret; return resp.data as TwoFactorSecret;
}; };
export const enableTwoFactor = async (code: string, recoveryEncryptedTwoFactorSecret: B64EncryptionResult) => { export const enableTwoFactor = async (
await HTTPService.post(`${ENDPOINT}/users/two-factor/enable`, { code: string,
code, recoveryEncryptedTwoFactorSecret: B64EncryptionResult
encryptedTwoFactorSecret: recoveryEncryptedTwoFactorSecret.encryptedData, ) => {
twoFactorSecretDecryptionNonce: recoveryEncryptedTwoFactorSecret.nonce, await HTTPService.post(
}, null, { `${ENDPOINT}/users/two-factor/enable`,
'X-Auth-Token': getToken(), {
}); code,
encryptedTwoFactorSecret:
recoveryEncryptedTwoFactorSecret.encryptedData,
twoFactorSecretDecryptionNonce:
recoveryEncryptedTwoFactorSecret.nonce,
},
null,
{
'X-Auth-Token': getToken(),
}
);
}; };
export const verifyTwoFactor = async (code: string, sessionID: string) => { export const verifyTwoFactor = async (code: string, sessionID: string) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/verify`, { const resp = await HTTPService.post(
code, sessionID, `${ENDPOINT}/users/two-factor/verify`,
}, null); {
code,
sessionID,
},
null
);
return resp.data as TwoFactorVerificationResponse; return resp.data as TwoFactorVerificationResponse;
}; };
@ -167,7 +191,8 @@ export const recoverTwoFactor = async (sessionID: string) => {
export const removeTwoFactor = async (sessionID: string, secret: string) => { export const removeTwoFactor = async (sessionID: string, secret: string) => {
const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, { const resp = await HTTPService.post(`${ENDPOINT}/users/two-factor/remove`, {
sessionID, secret, sessionID,
secret,
}); });
return resp.data as TwoFactorVerificationResponse; return resp.data as TwoFactorVerificationResponse;
}; };
@ -179,9 +204,13 @@ export const disableTwoFactor = async () => {
}; };
export const getTwoFactorStatus = async () => { export const getTwoFactorStatus = async () => {
const resp = await HTTPService.get(`${ENDPOINT}/users/two-factor/status`, null, { const resp = await HTTPService.get(
'X-Auth-Token': getToken(), `${ENDPOINT}/users/two-factor/status`,
}); null,
{
'X-Auth-Token': getToken(),
}
);
return resp.data['status']; return resp.data['status'];
}; };
@ -197,3 +226,40 @@ export const _logout = async () => {
return false; 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 GAP_BTW_TILES = 4;
export const DATE_CONTAINER_HEIGHT = 48; export const DATE_CONTAINER_HEIGHT = 48;
export const IMAGE_CONTAINER_MAX_HEIGHT = 200; 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 MIN_COLUMNS = 4;
export const SPACE_BTW_DATES = 44; export const SPACE_BTW_DATES = 44;

View file

@ -9,7 +9,7 @@ import { NextRouter } from 'next/router';
import { SetDialogMessage } from 'components/MessageDialog'; import { SetDialogMessage } from 'components/MessageDialog';
import { SetLoading } from 'pages/gallery'; import { SetLoading } from 'pages/gallery';
import { getData, LS_KEYS } from './storage/localStorage'; import { getData, LS_KEYS } from './storage/localStorage';
import { SUBSCRIPTION_VERIFICATION_ERROR } from './common/errorUtil'; import { CustomError } from './common/errorUtil';
const STRIPE = 'stripe'; const STRIPE = 'stripe';
@ -17,14 +17,14 @@ export function convertBytesToGBs(bytes, precision?): string {
return (bytes / (1024 * 1024 * 1024)).toFixed(precision ?? 2); return (bytes / (1024 * 1024 * 1024)).toFixed(precision ?? 2);
} }
export function convertToHumanReadable(bytes:number, precision=2): string { export function convertToHumanReadable(bytes: number, precision = 2): string {
if (bytes===0) { if (bytes === 0) {
return '0 MB'; return '0 MB';
} }
const i = Math.floor(Math.log(bytes) / Math.log(1024)); const i = Math.floor(Math.log(bytes) / Math.log(1024));
const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
return (bytes / Math.pow(1024, i)).toFixed(precision)+ ' ' + sizes[i]; return (bytes / Math.pow(1024, i)).toFixed(precision) + ' ' + sizes[i];
} }
export function hasPaidSubscription(subscription?: Subscription) { export function hasPaidSubscription(subscription?: Subscription) {
@ -89,7 +89,7 @@ export async function updateSubscription(
plan: Plan, plan: Plan,
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage,
setLoading: SetLoading, setLoading: SetLoading,
closePlanSelectorModal: () => null, closePlanSelectorModal: () => null
) { ) {
try { try {
setLoading(true); setLoading(true);
@ -99,7 +99,7 @@ export async function updateSubscription(
setDialogMessage({ setDialogMessage({
title: constants.SUCCESS, title: constants.SUCCESS,
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS( content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
getUserSubscription().expiryTime, getUserSubscription().expiryTime
), ),
close: { variant: 'success' }, close: { variant: 'success' },
}); });
@ -117,13 +117,13 @@ export async function updateSubscription(
null, null,
setDialogMessage, setDialogMessage,
setLoading, setLoading
), ),
}, },
close: { text: constants.CANCEL }, close: { text: constants.CANCEL },
}); });
break; break;
case SUBSCRIPTION_VERIFICATION_ERROR: case CustomError.SUBSCRIPTION_VERIFICATION_ERROR:
setDialogMessage({ setDialogMessage({
title: constants.ERROR, title: constants.ERROR,
content: constants.SUBSCRIPTION_VERIFICATION_FAILED, content: constants.SUBSCRIPTION_VERIFICATION_FAILED,
@ -146,7 +146,7 @@ export async function updateSubscription(
export async function cancelSubscription( export async function cancelSubscription(
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage,
closePlanSelectorModal: () => null, closePlanSelectorModal: () => null,
setLoading: SetLoading, setLoading: SetLoading
) { ) {
try { try {
setLoading(true); setLoading(true);
@ -171,7 +171,7 @@ export async function cancelSubscription(
export async function activateSubscription( export async function activateSubscription(
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage,
closePlanSelectorModal: () => null, closePlanSelectorModal: () => null,
setLoading: SetLoading, setLoading: SetLoading
) { ) {
try { try {
setLoading(true); setLoading(true);
@ -195,7 +195,7 @@ export async function activateSubscription(
export async function updatePaymentMethod( export async function updatePaymentMethod(
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage,
setLoading: SetLoading, setLoading: SetLoading
) { ) {
try { try {
setLoading(true); setLoading(true);
@ -213,7 +213,7 @@ export async function updatePaymentMethod(
export async function checkSubscriptionPurchase( export async function checkSubscriptionPurchase(
setDialogMessage: SetDialogMessage, setDialogMessage: SetDialogMessage,
router: NextRouter, router: NextRouter
) { ) {
try { try {
const urlParams = new URLSearchParams(window.location.search); const urlParams = new URLSearchParams(window.location.search);
@ -227,19 +227,19 @@ export async function checkSubscriptionPurchase(
} else if (sessionId) { } else if (sessionId) {
try { try {
const subscription = await billingService.verifySubscription( const subscription = await billingService.verifySubscription(
sessionId, sessionId
); );
setDialogMessage({ setDialogMessage({
title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE, title: constants.SUBSCRIPTION_PURCHASE_SUCCESS_TITLE,
close: { variant: 'success' }, close: { variant: 'success' },
content: constants.SUBSCRIPTION_PURCHASE_SUCCESS( content: constants.SUBSCRIPTION_PURCHASE_SUCCESS(
subscription?.expiryTime, subscription?.expiryTime
), ),
}); });
} catch (e) { } catch (e) {
setDialogMessage({ setDialogMessage({
title: constants.ERROR, title: constants.ERROR,
content: SUBSCRIPTION_VERIFICATION_ERROR, content: CustomError.SUBSCRIPTION_VERIFICATION_ERROR,
close: {}, close: {},
}); });
} }
@ -250,3 +250,18 @@ export async function checkSubscriptionPurchase(
router.push('gallery', undefined, { shallow: true }); 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>, syncWithRemote: () => Promise<void>,
selectCollection: (id: number) => void, selectCollection: (id: number) => void,
collectionName: string, collectionName: string,
existingCollection: Collection, existingCollection: Collection
) { ) {
setCollectionSelectorView(false); setCollectionSelectorView(false);
let collection; let collection;
if (!existingCollection) { if (!existingCollection) {
collection = await createCollection( collection = await createCollection(
collectionName, collectionName,
CollectionType.album, CollectionType.album
); );
} else { } else {
collection = existingCollection; collection = existingCollection;

View file

@ -1,18 +1,25 @@
export const getEndpoint = () => { 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; return endPoint;
}; };
export const getFileUrl = (id: number) => { export const getFileUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) { 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}`; return `https://files.ente.workers.dev/?fileID=${id}`;
}; };
export const getThumbnailUrl = (id: number) => { export const getThumbnailUrl = (id: number) => {
if (process.env.NEXT_PUBLIC_ENTE_ENDPOINT !== undefined) { 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}`; return `https://thumbnails.ente.workers.dev/?fileID=${id}`;
}; };

View file

@ -1,45 +1,43 @@
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export const errorCodes = { export const ServerErrorCodes = {
ERR_STORAGE_LIMIT_EXCEEDED: '426', SESSION_EXPIRED: '401',
ERR_NO_ACTIVE_SUBSCRIPTION: '402', NO_ACTIVE_SUBSCRIPTION: '402',
ERR_NO_INTERNET_CONNECTION: '1', FORBIDDEN: '403',
ERR_SESSION_EXPIRED: '401', STORAGE_LIMIT_EXCEEDED: '426',
ERR_KEY_MISSING: '2',
ERR_FORBIDDEN: '403',
}; };
export const CustomError = {
export const SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed'; SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
export const THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed'; VIDEO_PLAYBACK_FAILED: 'video playback failed',
export const 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) { export function parseError(error) {
let errorMessage = null; let parsedMessage = null;
if (error?.status) { if (error?.status) {
const errorCode = error.status.toString(); const errorCode = error.status.toString();
switch (errorCode) { switch (errorCode) {
case errorCodes.ERR_NO_ACTIVE_SUBSCRIPTION: case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
errorMessage = constants.SUBSCRIPTION_EXPIRED; parsedMessage = constants.SUBSCRIPTION_EXPIRED;
break; break;
case errorCodes.ERR_STORAGE_LIMIT_EXCEEDED: case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
errorMessage = constants.STORAGE_QUOTA_EXCEEDED; parsedMessage = constants.STORAGE_QUOTA_EXCEEDED;
break; break;
case errorCodes.ERR_NO_INTERNET_CONNECTION: case ServerErrorCodes.SESSION_EXPIRED:
errorMessage = constants.NO_INTERNET_CONNECTION; parsedMessage = constants.SESSION_EXPIRED_MESSAGE;
break;
case errorCodes.ERR_SESSION_EXPIRED:
errorMessage = constants.SESSION_EXPIRED_MESSAGE;
break; break;
} }
} }
if (errorMessage) { if (parsedMessage) {
return { parsedError: new Error(errorMessage), parsed: true }; return { parsedError: new Error(parsedMessage), parsed: true };
} else { } else {
return ({ return {
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`), parsed: false, parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`),
}); parsed: false,
};
} }
} }
@ -48,6 +46,6 @@ export function handleError(error) {
if (parsed) { if (parsed) {
throw parsedError; throw parsedError;
} else { } 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'; 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]; const retrySleepTime = [2000, 5000, 10000];
@ -32,7 +33,10 @@ export function reverseString(title: string) {
.reduce((reversedString, currWord) => `${currWord} ${reversedString}`); .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 { try {
const resp = await func(); const resp = await func();
return resp; return resp;

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