Merge branch 'master' into upload-refactoring

This commit is contained in:
Abhinav-grd 2021-08-09 14:56:49 +05:30
commit 5f386f3c93
22 changed files with 335 additions and 137 deletions

View file

@ -9,7 +9,7 @@ import router from 'next/router';
import { changeEmail, getOTTForEmailChange } from 'services/userService'; import { changeEmail, getOTTForEmailChange } from 'services/userService';
import styled from 'styled-components'; import styled from 'styled-components';
import { AppContext } from 'pages/_app'; import { AppContext } from 'pages/_app';
import englishConstants from 'utils/strings/englishConstants'; import { getData, LS_KEYS, setData } from 'utils/storage/localStorage';
interface formValues { interface formValues {
email: string; email: string;
@ -72,6 +72,7 @@ function ChangeEmailForm(props:Props) {
try { try {
setLoading(true); setLoading(true);
await changeEmail(email, ott); await changeEmail(email, ott);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email });
appContext.setDisappearingFlashMessage({ message: constants.EMAIL_UDPATE_SUCCESSFUL, severity: 'success' }); appContext.setDisappearingFlashMessage({ message: constants.EMAIL_UDPATE_SUCCESSFUL, severity: 'success' });
router.push('/gallery'); router.push('/gallery');
} catch (e) { } catch (e) {
@ -125,7 +126,7 @@ function ChangeEmailForm(props:Props) {
</Col> </Col>
<Col xs ="4" > <Col xs ="4" >
<Button variant="link" onClick={()=>setShowOttInputVisibility(false)}> <Button variant="link" onClick={()=>setShowOttInputVisibility(false)}>
{englishConstants.CHANGE} {constants.CHANGE}
</Button> </Button>
</Col> </Col>
</EmailRow> </EmailRow>

View file

@ -19,7 +19,7 @@ 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, DATE_CONTAINER_HEIGHT, IMAGE_CONTAINER_MAX_HEIGHT,
IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, SPACE_BTW_DATES, IMAGE_CONTAINER_MAX_WIDTH, MIN_COLUMNS, SPACE_BTW_DATES,
@ -306,7 +306,7 @@ const PhotoFrame = ({
const t = setTimeout( const t = setTimeout(
() => { () => {
reject( reject(
Error(`${VIDEO_PLAYBACK_FAILED} err: wait time exceeded`), Error(`${CustomError.VIDEO_PLAYBACK_FAILED} err: wait time exceeded`),
); );
}, },
WAIT_FOR_VIDEO_PLAYBACK, WAIT_FOR_VIDEO_PLAYBACK,

View file

@ -60,6 +60,8 @@ export default function Sidebar(props: Props) {
setUser({ ...user, email: userDetails.email }); setUser({ ...user, email: userDetails.email });
SetUsage(convertToHumanReadable(userDetails.usage)); SetUsage(convertToHumanReadable(userDetails.usage));
setSubscription(userDetails.subscription); setSubscription(userDetails.subscription);
setData(LS_KEYS.USER, { ...getData(LS_KEYS.USER), email: userDetails.email });
setData(LS_KEYS.SUBSCRIPTION, userDetails.subscription);
}; };
main(); main();
}, [isOpen]); }, [isOpen]);

View file

@ -19,16 +19,18 @@ interface Props {
} }
const Group = styled.div` const Group = styled.div`
display: flex; position: relative;
`; `;
const Button = styled.button` const Button = styled.button`
background: transparent; background: transparent;
border: none; border: none;
width: 36px; width: 46px;
height: 36px; height: 34px;
margin-left: -36px; position: absolute;
display: flex; top: 1px;
right: 1px;
border-radius: 5px;
align-items: center; align-items: center;
`; `;
@ -77,10 +79,10 @@ export default function SingleInputForm(props: Props) {
{showPassword ? <VisibilityOff /> : <Visibility />} {showPassword ? <VisibilityOff /> : <Visibility />}
</Button> </Button>
} }
</Group>
<Form.Control.Feedback type="invalid"> <Form.Control.Feedback type="invalid">
{errors.passphrase} {errors.passphrase}
</Form.Control.Feedback> </Form.Control.Feedback>
</Group>
</Form.Group> </Form.Group>
<SubmitButton <SubmitButton
buttonText={props.buttonText} buttonText={props.buttonText}

View file

@ -0,0 +1,26 @@
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,26 @@
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,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',
borderBottom: '1px solid',
background: 'none',
borderRadius: '0px',
color: getVariantColor(props.variant),
padding: 0,
margin: '0 25px',
marginBottom: '10px',
}} }}
> >
{bannerMessage} {props.bannerMessage?props.bannerMessage:props.children}
</Alert> </Alert>
); );
} }

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,8 +12,7 @@ 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';
@ -20,14 +20,17 @@ export default function LinkButton(props: Props) {
return '#c93f3f'; return '#c93f3f';
case ButtonVariant.secondary: case ButtonVariant.secondary:
return '#858585'; return '#858585';
case ButtonVariant.warning:
return '#D7BB63';
default: default:
return '#d1d1d1'; 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,

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;

View file

@ -38,6 +38,7 @@ 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>(
@ -45,6 +46,7 @@ export default function Upload(props: Props) {
); );
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);
@ -77,6 +79,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);
}; };
@ -209,6 +212,7 @@ export default function Upload(props: Props) {
setFileCounter, setFileCounter,
setUploadStage, setUploadStage,
setFileProgress, setFileProgress,
setUploadResult,
}, },
props.setFiles, props.setFiles,
); );
@ -258,6 +262,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

@ -1,10 +1,15 @@
import React from 'react'; import ExpandLess from 'components/icons/ExpandLess';
import ExpandMore from 'components/icons/ExpandMore';
import React, { useState } from 'react';
import { import {
Alert, Button, Modal, ProgressBar, Button, Modal, ProgressBar,
} from 'react-bootstrap'; } from 'react-bootstrap';
import { FileRejection } from 'react-dropzone'; import { FileRejection } from 'react-dropzone';
import { UPLOAD_STAGES, FileUploadErrorCode } from 'services/upload/uploadService'; import { FileUploadResults, UPLOAD_STAGES } from 'services/upload/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;
@ -15,24 +20,95 @@ interface Props {
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 (props.uploadResult) {
if (b.progress !== -1 && a.progress === -1) return 1; 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}
@ -48,7 +124,12 @@ export default function UploadProgress(props: Props) {
} }
> >
<Modal.Header <Modal.Header
style={{ display: 'flex', justifyContent: 'center', textAlign: 'center', borderBottom: 'none', paddingTop: '30px', paddingBottom: '0px' }} style={{ display: 'flex',
justifyContent: 'center',
textAlign: 'center',
borderBottom: 'none',
paddingTop: '30px',
paddingBottom: '0px' }}
closeButton={props.uploadStage === UPLOAD_STAGES.FINISH} closeButton={props.uploadStage === UPLOAD_STAGES.FINISH}
> >
@ -61,14 +142,7 @@ export default function UploadProgress(props: Props) {
</h4> </h4>
</Modal.Header> </Modal.Header>
<Modal.Body> <Modal.Body>
{props.uploadStage===UPLOAD_STAGES.FINISH ? ( {(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
fileProgressStatuses.length !== 0 && (
<Alert variant="warning">
{constants.FAILED_UPLOAD_FILE_LIST}
</Alert>
)
) :
(props.uploadStage === UPLOAD_STAGES.READING_GOOGLE_METADATA_FILES ||
props.uploadStage === UPLOAD_STAGES.UPLOADING) && props.uploadStage === UPLOAD_STAGES.UPLOADING) &&
( (
< ProgressBar < ProgressBar
@ -76,15 +150,10 @@ export default function UploadProgress(props: Props) {
animated animated
variant="upload-progress-bar" variant="upload-progress-bar"
/> />
)} )
{fileProgressStatuses?.length > 0 && ( }
<ul {fileProgressStatuses.length>0 &&
style={{ <FileList>
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 ?
@ -92,28 +161,42 @@ export default function UploadProgress(props: Props) {
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 && ((fileUploadResultMap?.get(FileUploadResults.FAILED)?.length>0 || fileUploadResultMap?.get(FileUploadResults.BLOCKED)?.length>0)? (
<Button
variant="outline-secondary"
style={{ width: '100%' }}
onClick={props.closeModal}
>
{constants.CLOSE}
</Button>) : (
<Button <Button
variant="outline-success" variant="outline-success"
style={{ width: '100%' }} style={{ width: '100%' }}
onClick={props.retryFailed} onClick={props.retryFailed}
> >
{constants.RETRY} {constants.RETRY_FAILED}
</Button>))} </Button>
) : ( <Button
variant="outline-secondary"
style={{ width: '100%' }}
onClick={props.closeModal}
>
{constants.CLOSE}
</Button>
))}
</Modal.Footer> </Modal.Footer>
)} )}
</Modal.Body> </Modal.Body>

View file

@ -133,6 +133,10 @@ const GlobalStyles = createGlobalStyle`
margin:5% auto; margin:5% auto;
width:90%; width:90%;
} }
.modal-body{
max-height:80vh;
overflow:auto;
}
.modal-xl{ .modal-xl{
max-width:90% !important; max-width:90% !important;
} }

View file

@ -39,7 +39,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,6 +54,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 { export enum FILE_TYPE {
IMAGE, IMAGE,
@ -200,7 +200,7 @@ export default function Gallery() {
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.syncSubscription(); await billingService.syncSubscription();
@ -209,7 +209,7 @@ export default function Gallery() {
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,
@ -219,11 +219,9 @@ export default function Gallery() {
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;
@ -308,7 +306,7 @@ export default function Gallery() {
} 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,

View file

@ -4,7 +4,7 @@ import { checkConnectivity, runningInBrowser } from 'utils/common/';
import { setData, LS_KEYS } from 'utils/storage/localStorage'; import { setData, LS_KEYS } from 'utils/storage/localStorage';
import { convertToHumanReadable } from 'utils/billingUtil'; import { convertToHumanReadable } from 'utils/billingUtil';
import { loadStripe, Stripe } from '@stripe/stripe-js'; import { loadStripe, Stripe } from '@stripe/stripe-js';
import { SUBSCRIPTION_VERIFICATION_ERROR } from 'utils/common/errorUtil'; import { CustomError } from 'utils/common/errorUtil';
import HTTPService from './HTTPService'; import HTTPService from './HTTPService';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
@ -142,7 +142,7 @@ class billingService {
try { try {
await this.verifySubscription(); await this.verifySubscription();
} catch (e) { } catch (e) {
throw new Error(SUBSCRIPTION_VERIFICATION_ERROR); throw new Error(CustomError.SUBSCRIPTION_VERIFICATION_ERROR);
} }
} }

View file

@ -6,6 +6,7 @@ import { logError } from 'utils/sentry';
import { CHUNKS_COMBINED_FOR_UPLOAD, MultipartUploadURLs, RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, UploadFile } from './uploadService'; import { CHUNKS_COMBINED_FOR_UPLOAD, MultipartUploadURLs, RANDOM_PERCENTAGE_PROGRESS_FOR_PUT, UploadFile } from './uploadService';
import * as convert from 'xml-js'; import * as convert from 'xml-js';
import { File } from '../fileService'; import { File } from '../fileService';
import { CustomError } from 'utils/common/errorUtil';
const ENDPOINT = getEndpoint(); const ENDPOINT = getEndpoint();
const MAX_URL_REQUESTS = 50; const MAX_URL_REQUESTS = 50;
@ -153,7 +154,7 @@ class NetworkClient {
trackUploadProgress(filename, percentPerPart, index), trackUploadProgress(filename, percentPerPart, index),
); );
if (!resp?.headers?.etag) { if (!resp?.headers?.etag) {
const err=Error('no header/etag present in response body'); const err=Error(CustomError.ETAG_MISSING);
logError(err); logError(err);
throw err; throw err;
} }

View file

@ -1,5 +1,5 @@
import { FILE_TYPE } from 'pages/gallery'; import { FILE_TYPE } from 'pages/gallery';
import { THUMBNAIL_GENERATION_FAILED } from 'utils/common/errorUtil'; import { CustomError } from 'utils/common/errorUtil';
import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file'; import { fileIsHEIC, convertHEIC2JPEG } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { getUint8ArrayView } from './readFileService'; import { getUint8ArrayView } from './readFileService';
@ -81,7 +81,7 @@ export async function generateImageThumbnail(file:globalThis.File) {
} 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(
@ -128,7 +128,7 @@ export async function generateVideoThumbnail(file:globalThis.File) {
} 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';

View file

@ -2,6 +2,7 @@ import { File, fileAttribute } from '../fileService';
import { Collection } from '../collectionService'; import { Collection } from '../collectionService';
import { FILE_TYPE, SetFiles } from 'pages/gallery'; import { FILE_TYPE, SetFiles } from 'pages/gallery';
import { import {
CustomError,
handleError, handleError,
parseError, parseError,
} from 'utils/common/errorUtil'; } from 'utils/common/errorUtil';
@ -29,10 +30,12 @@ const TwoSecondInMillSeconds = 2000;
export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random(); export const RANDOM_PERCENTAGE_PROGRESS_FOR_PUT = () => 90 + 10 * Math.random();
export const CHUNKS_COMBINED_FOR_UPLOAD = 5; export const CHUNKS_COMBINED_FOR_UPLOAD = 5;
export enum FileUploadErrorCode { export enum FileUploadResults {
FAILED = -1, FAILED = -1,
SKIPPED = -2, SKIPPED = -2,
UNSUPPORTED = -3, UNSUPPORTED = -3,
BLOCKED=-4,
UPLOADED = 100,
} }
export interface FileWithCollection { export interface FileWithCollection {
@ -114,6 +117,7 @@ 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, ParsedMetaDataJSON>; private metadataMap: Map<string, ParsedMetaDataJSON>;
private filesToBeUploaded: FileWithCollection[]; private filesToBeUploaded: FileWithCollection[];
private progressBarProps; private progressBarProps;
@ -132,6 +136,7 @@ class UploadService {
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, ParsedMetaDataJSON>(); this.metadataMap = new Map<string, ParsedMetaDataJSON>();
this.progressBarProps = progressBarProps; this.progressBarProps = progressBarProps;
@ -233,11 +238,9 @@ class UploadService {
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 {
encryptedFile = await this.encryptFile(worker, file, collection.key); encryptedFile = await this.encryptFile(worker, file, collection.key);
@ -262,19 +265,26 @@ class UploadService {
uploadFile = null; uploadFile = null;
this.fileProgress.delete(rawFile.name); this.fileProgress.set(rawFile.name, FileUploadResults.UPLOADED);
this.filesCompleted++; this.filesCompleted++;
} }
} catch (e) { } catch (e) {
logError(e, 'file upload failed'); logError(e, 'file upload failed');
this.failedFiles.push(fileWithCollection); logError(e, 'file upload failed');
// 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 { } finally {
file=null; file=null;
encryptedFile=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) {
@ -290,7 +300,7 @@ class UploadService {
} }
private updateProgressBarUI() { private updateProgressBarUI() {
const { setPercentComplete, setFileCounter, setFileProgress } = const { setPercentComplete, setFileCounter, setFileProgress, setUploadResult } =
this.progressBarProps; this.progressBarProps;
setFileCounter({ setFileCounter({
finished: this.filesCompleted, finished: this.filesCompleted,
@ -309,6 +319,7 @@ class UploadService {
} }
setPercentComplete(percentComplete); setPercentComplete(percentComplete);
setFileProgress(this.fileProgress); setFileProgress(this.fileProgress);
setUploadResult(this.uploadResult);
} }
async readFile(reader: FileReader, receivedFile: globalThis.File, metadataMap:Map<string, ParsedMetaDataJSON>) { async readFile(reader: FileReader, receivedFile: globalThis.File, metadataMap:Map<string, ParsedMetaDataJSON>) {

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';
@ -123,7 +123,7 @@ export async function updateSubscription(
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,
@ -239,7 +239,7 @@ export async function checkSubscriptionPurchase(
} catch (e) { } catch (e) {
setDialogMessage({ setDialogMessage({
title: constants.ERROR, title: constants.ERROR,
content: SUBSCRIPTION_VERIFICATION_ERROR, content: CustomError.SUBSCRIPTION_VERIFICATION_ERROR,
close: {}, close: {},
}); });
} }

View file

@ -1,41 +1,40 @@
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 SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed'; export const CustomError ={
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed',
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed',
VIDEO_PLAYBACK_FAILED: 'video playback failed',
ETAG_MISSING: 'no header/etag present in response body',
KEY_MISSING: 'encrypted key missing from localStorage',
};
export const THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed';
export const VIDEO_PLAYBACK_FAILED = 'video playback failed';
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 +47,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,6 @@
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];

View file

@ -2,7 +2,7 @@ import { B64EncryptionResult } from 'services/upload/uploadService';
import CryptoWorker from 'utils/crypto'; import CryptoWorker from 'utils/crypto';
import { getData, LS_KEYS } from 'utils/storage/localStorage'; import { getData, LS_KEYS } from 'utils/storage/localStorage';
import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage'; import { getKey, SESSION_KEYS } from 'utils/storage/sessionStorage';
import { errorCodes } from './errorUtil'; import { CustomError } from './errorUtil';
export const getActualKey = async () => { export const getActualKey = async () => {
try { try {
@ -18,7 +18,7 @@ export const getActualKey = async () => {
); );
return key; return key;
} catch (e) { } catch (e) {
throw new Error(errorCodes.ERR_KEY_MISSING); throw new Error(CustomError.KEY_MISSING);
} }
}; };

View file

@ -89,12 +89,11 @@ const englishConstants = {
0: 'preparing to upload', 0: 'preparing to upload',
1: 'reading google metadata files', 1: 'reading google metadata files',
2: (fileCounter) => `${fileCounter.finished} / ${fileCounter.total} files backed up`, 2: (fileCounter) => `${fileCounter.finished} / ${fileCounter.total} files backed up`,
3: 'backup complete!', 3: 'backup complete',
}, },
UPLOADING_FILES: 'file upload', UPLOADING_FILES: 'file upload',
FAILED_UPLOAD_FILE_LIST: 'upload failed for following files', FILE_NOT_UPLOADED_LIST: 'the following files were not uploaded',
UNSUPPORTED_FILE_LIST: 'unsupported files', FILE_UPLOAD_PROGRESS: (name:string, progress:number) => (
FILE_UPLOAD_PROGRESS: (name, progress) => (
<div id={name}> <div id={name}>
<strong>{name}</strong> <strong>{name}</strong>
{' - '} {' - '}
@ -105,7 +104,7 @@ const englishConstants = {
case -2: case -2:
return 'already uploaded, skipping...'; return 'already uploaded, skipping...';
case -3: case -3:
return 'unsupported file format, ignored'; return ',unsupported file format, skipping....';
default: default:
return `${progress}%`; return `${progress}%`;
} }
@ -452,7 +451,30 @@ const englishConstants = {
SEND_OTT: 'send otp', SEND_OTT: 'send otp',
EMAIl_ALREADY_OWNED: 'email already taken', EMAIl_ALREADY_OWNED: 'email already taken',
EMAIL_UDPATE_SUCCESSFUL: 'your email has been udpated successfully', EMAIL_UDPATE_SUCCESSFUL: 'your email has been udpated successfully',
UPLOAD_FAILED: 'upload failed',
ETAGS_BLOCKED: (url:string)=>(<>
<p> we were unable to upload the following files because of your browser configuration.</p>
<p> please disable any addons that might be preventing ente from using <code>eTags</code> to upload large files, or use our
{' '}
<a
href={url} style={{ color: '#2dc262', textDecoration: 'underline' }}
target="_blank" rel="noreferrer">
desktop app
</a>
{' '}
for a more reliable import experience.</p>
</>),
RETRY_FAILED: 'retry failed uploads',
FAILED_UPLOADS: 'failed uploads ',
SKIPPED_FILES: 'duplicate files',
UNSUPPORTED_FILES: 'unsupported files',
SUCCESSFUL_UPLOADS: 'successful uploads',
FAILED_INFO: ' unable to upload these files because of network issue, you can retry upload these files',
SKIPPED_INFO: 'these files already existed in the album',
UNSUPPORTED_INFO: 'these files are currently not supported by ente',
SUCCESS_INFO: 'successfully backed-up memories',
BLOCKED_UPLOADS: 'blocked uploads',
}; };
export default englishConstants; export default englishConstants;