Merge pull request #127 from ente-io/first-upload-ux

First upload ux
This commit is contained in:
Abhinav-grd 2021-08-29 14:49:15 +05:30 committed by GitHub
commit 549232c26c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 207 additions and 80 deletions

View file

@ -55,3 +55,9 @@ export const Value = styled.div<{ width?: string }>`
text-align: center; text-align: center;
color: #ddd; color: #ddd;
`; `;
export const FlexWrapper = styled.div`
display: flex;
text-align: center;
justify-content: center;
`;

View file

@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react'; import React, { useContext, useEffect, useState } from 'react';
import { slide as Menu } from 'react-burger-menu'; import { slide as Menu } from 'react-burger-menu';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
@ -8,7 +8,6 @@ import { getEndpoint } from 'utils/common/apiUtil';
import { Button } from 'react-bootstrap'; import { Button } from 'react-bootstrap';
import { import {
isSubscriptionActive, isSubscriptionActive,
convertBytesToGBs,
getUserSubscription, getUserSubscription,
isOnFreePlan, isOnFreePlan,
isSubscriptionCancelled, isSubscriptionCancelled,
@ -28,7 +27,7 @@ import EnteSpinner from './EnteSpinner';
import RecoveryKeyModal from './RecoveryKeyModal'; import RecoveryKeyModal from './RecoveryKeyModal';
import TwoFactorModal from './TwoFactorModal'; import TwoFactorModal from './TwoFactorModal';
import ExportModal from './ExportModal'; import ExportModal from './ExportModal';
import { SetLoading } from 'pages/gallery'; import { GalleryContext, 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'; import { Subscription } from 'services/billingService';
@ -51,6 +50,8 @@ export default function Sidebar(props: Props) {
const [recoverModalView, setRecoveryModalView] = useState(false); const [recoverModalView, setRecoveryModalView] = useState(false);
const [twoFactorModalView, setTwoFactorModalView] = useState(false); const [twoFactorModalView, setTwoFactorModalView] = useState(false);
const [exportModalView, setExportModalView] = useState(false); const [exportModalView, setExportModalView] = useState(false);
const galleryContext = useContext(GalleryContext);
galleryContext.showPlanSelectorModal = props.showPlanSelectorModal;
useEffect(() => { useEffect(() => {
const main = async () => { const main = async () => {
if (!isOpen) { if (!isOpen) {
@ -186,7 +187,7 @@ export default function Sidebar(props: Props) {
{usage ? ( {usage ? (
constants.USAGE_INFO( constants.USAGE_INFO(
usage, usage,
Number(convertBytesToGBs(subscription?.storage)) convertToHumanReadable(subscription?.storage)
) )
) : ( ) : (
<div style={{ textAlign: 'center' }}> <div style={{ textAlign: 'center' }}>

View file

@ -0,0 +1,27 @@
import React from 'react';
export default function WarningIcon(props) {
return (
<div
style={{
color: 'red',
display: 'inline-block',
padding: '0 10px',
}}>
<svg
xmlns="http://www.w3.org/2000/svg"
height={props.height}
viewBox={props.viewBox}
width={props.width}
fill="currentColor">
<path d="M12 2c5.514 0 10 4.486 10 10s-4.486 10-10 10-10-4.486-10-10 4.486-10 10-10zm0-2c-6.627 0-12 5.373-12 12s5.373 12 12 12 12-5.373 12-12-5.373-12-12-12zm-1 6h2v8h-2v-8zm1 12.25c-.69 0-1.25-.56-1.25-1.25s.56-1.25 1.25-1.25 1.25.56 1.25 1.25-.56 1.25-1.25 1.25z" />
</svg>
</div>
);
}
WarningIcon.defaultProps = {
height: 24,
width: 24,
viewBox: '0 0 24 24',
};

View file

@ -1,30 +1,28 @@
import { FlexWrapper } from 'components/Container';
import WarningIcon from 'components/icons/WarningIcon';
import React from 'react'; import React from 'react';
import Alert from 'react-bootstrap/Alert'; import styled from 'styled-components';
import { getVariantColor } from './LinkButton';
interface Props { interface Props {
bannerMessage?: any; bannerMessage?: any;
variant?: string; variant?: string;
children?: any;
style?: any;
} }
const Banner = styled.div`
border: 1px solid #71662e;
border-radius: 8px;
padding: 16px 28px;
color: #eee;
margin-top: 10px;
`;
export default function AlertBanner(props: Props) { export default function AlertBanner(props: Props) {
return ( return props.bannerMessage ? (
<Alert <FlexWrapper>
variant={props.variant ?? 'danger'} <Banner>
style={{ <WarningIcon />
display: {props.bannerMessage && props.bannerMessage}
props.bannerMessage || props.children ? 'block' : 'none', </Banner>
textAlign: 'center', </FlexWrapper>
border: 'none', ) : (
background: 'none', <></>
borderRadius: '0px',
color: getVariantColor(props.variant),
padding: 0,
margin: 0,
...props.style,
}}>
{props.bannerMessage ? props.bannerMessage : props.children}
</Alert>
); );
} }

View file

@ -12,7 +12,7 @@ import UploadProgress from './UploadProgress';
import ChoiceModal from './ChoiceModal'; import ChoiceModal from './ChoiceModal';
import { SetCollectionNamerAttributes } from './CollectionNamer'; import { SetCollectionNamerAttributes } from './CollectionNamer';
import { SetCollectionSelectorAttributes } from './CollectionSelector'; import { SetCollectionSelectorAttributes } from './CollectionSelector';
import { SetFiles, SetLoading } from 'pages/gallery'; import { GalleryContext, 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';
@ -22,6 +22,7 @@ import UploadManager, {
} from 'services/upload/uploadManager'; } from 'services/upload/uploadManager';
import uploadManager from 'services/upload/uploadManager'; import uploadManager from 'services/upload/uploadManager';
import { METADATA_FOLDER_NAME } from 'services/exportService'; import { METADATA_FOLDER_NAME } from 'services/exportService';
import { getUserFacingErrorMessage } from 'utils/common/errorUtil';
const FIRST_ALBUM_NAME = 'My First Album'; const FIRST_ALBUM_NAME = 'My First Album';
@ -75,6 +76,7 @@ export default function Upload(props: Props) {
const [fileAnalysisResult, setFileAnalysisResult] = const [fileAnalysisResult, setFileAnalysisResult] =
useState<AnalysisResult>(null); useState<AnalysisResult>(null);
const appContext = useContext(AppContext); const appContext = useContext(AppContext);
const galleryContext = useContext(GalleryContext);
useEffect(() => { useEffect(() => {
UploadManager.initUploader( UploadManager.initUploader(
@ -280,7 +282,11 @@ export default function Upload(props: Props) {
collections collections
); );
} catch (err) { } catch (err) {
props.setBannerMessage(err.message); const message = getUserFacingErrorMessage(
err.message,
galleryContext.showPlanSelectorModal
);
props.setBannerMessage(message);
setProgressView(false); setProgressView(false);
throw err; throw err;
} finally { } finally {
@ -296,8 +302,12 @@ export default function Upload(props: Props) {
await props.syncWithRemote(true, true); await props.syncWithRemote(true, true);
await uploadManager.retryFailedFiles(); await uploadManager.retryFailedFiles();
} catch (err) { } catch (err) {
const message = getUserFacingErrorMessage(
err.message,
galleryContext.showPlanSelectorModal
);
appContext.resetSharedFiles(); appContext.resetSharedFiles();
props.setBannerMessage(err.message); props.setBannerMessage(message);
setProgressView(false); setProgressView(false);
} finally { } finally {
props.setUploadInProgress(false); props.setUploadInProgress(false);

View file

@ -10,8 +10,8 @@ import {
import styled from 'styled-components'; import styled from 'styled-components';
import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common'; import { DESKTOP_APP_DOWNLOAD_URL } from 'utils/common';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
import AlertBanner from './AlertBanner';
import { Collapse } from 'react-collapse'; import { Collapse } from 'react-collapse';
import { ButtonVariant, getVariantColor } from './LinkButton';
interface Props { interface Props {
fileCounter; fileCounter;
@ -65,6 +65,14 @@ const Content = styled.div`
padding-right: 30px; padding-right: 30px;
`; `;
const NotUploadSectionHeader = styled.div`
margin-top: 30px;
text-align: center;
color: ${getVariantColor(ButtonVariant.warning)};
border-bottom: 1px solid ${getVariantColor(ButtonVariant.warning)};
margin: 0 20px;
`;
interface ResultSectionProps { interface ResultSectionProps {
fileUploadResultMap: Map<FileUploadResults, string[]>; fileUploadResultMap: Map<FileUploadResults, string[]>;
fileUploadResult: FileUploadResults; fileUploadResult: FileUploadResults;
@ -136,12 +144,6 @@ const InProgressSection = (props: InProgressProps) => {
); );
}; };
const NotUploadSectionHeader = () => (
<AlertBanner variant="warning" style={{ marginTop: '30px' }}>
{constants.FILE_NOT_UPLOADED_LIST}
</AlertBanner>
);
export default function UploadProgress(props: Props) { export default function UploadProgress(props: Props) {
const fileProgressStatuses = [] as FileProgresses[]; const fileProgressStatuses = [] as FileProgresses[];
const fileUploadResultMap = new Map<FileUploadResults, string[]>(); const fileUploadResultMap = new Map<FileUploadResults, string[]>();
@ -214,7 +216,11 @@ export default function UploadProgress(props: Props) {
/> />
{props.uploadStage === UPLOAD_STAGES.FINISH && {props.uploadStage === UPLOAD_STAGES.FINISH &&
filesNotUploaded && <NotUploadSectionHeader />} filesNotUploaded && (
<NotUploadSectionHeader>
{constants.FILE_NOT_UPLOADED_LIST}
</NotUploadSectionHeader>
)}
<ResultSection <ResultSection
fileUploadResultMap={fileUploadResultMap} fileUploadResultMap={fileUploadResultMap}
@ -241,6 +247,12 @@ export default function UploadProgress(props: Props) {
sectionTitle={constants.UNSUPPORTED_FILES} sectionTitle={constants.UNSUPPORTED_FILES}
sectionInfo={constants.UNSUPPORTED_INFO} sectionInfo={constants.UNSUPPORTED_INFO}
/> />
<ResultSection
fileUploadResultMap={fileUploadResultMap}
fileUploadResult={FileUploadResults.TOO_LARGE}
sectionTitle={constants.TOO_LARGE_UPLOADS}
sectionInfo={constants.TOO_LARGE_INFO}
/>
</Modal.Body> </Modal.Body>
{props.uploadStage === UPLOAD_STAGES.FINISH && ( {props.uploadStage === UPLOAD_STAGES.FINISH && (
<Modal.Footer style={{ border: 'none' }}> <Modal.Footer style={{ border: 'none' }}>

View file

@ -98,11 +98,13 @@ export interface SearchStats {
type GalleryContextType = { type GalleryContextType = {
thumbs: Map<number, string>; thumbs: Map<number, string>;
files: Map<number, string>; files: Map<number, string>;
showPlanSelectorModal: () => void;
}; };
const defaultGalleryContext: GalleryContextType = { const defaultGalleryContext: GalleryContextType = {
thumbs: new Map(), thumbs: new Map(),
files: new Map(), files: new Map(),
showPlanSelectorModal: () => null,
}; };
export const GalleryContext = createContext<GalleryContextType>( export const GalleryContext = createContext<GalleryContextType>(

View file

@ -4,7 +4,7 @@ import { getToken } from 'utils/common/key';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { UploadFile, UploadURL } from './uploadService'; import { UploadFile, UploadURL } from './uploadService';
import { File } from '../fileService'; import { File } from '../fileService';
import { CustomError } from 'utils/common/errorUtil'; import { CustomError, handleUploadError } from 'utils/common/errorUtil';
import { retryAsyncFunction } from 'utils/network'; import { retryAsyncFunction } from 'utils/network';
import { MultipartUploadURLs } from './multiPartUploadService'; import { MultipartUploadURLs } from './multiPartUploadService';
@ -20,10 +20,12 @@ class UploadHttpClient {
if (!token) { if (!token) {
return; return;
} }
const response = await retryAsyncFunction(() => const response = await retryAsyncFunction(
() =>
HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, { HTTPService.post(`${ENDPOINT}/files`, uploadFile, null, {
'X-Auth-Token': token, 'X-Auth-Token': token,
}) }),
handleUploadError
); );
return response.data; return response.data;
} catch (e) { } catch (e) {

View file

@ -29,6 +29,7 @@ export enum FileUploadResults {
SKIPPED = -2, SKIPPED = -2,
UNSUPPORTED = -3, UNSUPPORTED = -3,
BLOCKED = -4, BLOCKED = -4,
TOO_LARGE = -5,
UPLOADED = 100, UPLOADED = 100,
} }

View file

@ -17,7 +17,7 @@ import { encryptFiledata } from './encryptionService';
import { ENCRYPTION_CHUNK_SIZE } from 'types'; import { ENCRYPTION_CHUNK_SIZE } from 'types';
import { uploadStreamUsingMultipart } from './multiPartUploadService'; import { uploadStreamUsingMultipart } from './multiPartUploadService';
import UIService from './uiService'; import UIService from './uiService';
import { parseError } from 'utils/common/errorUtil'; import { handleUploadError } from 'utils/common/errorUtil';
import { MetadataMap } from './uploadManager'; import { MetadataMap } from './uploadManager';
import { fileIsHEIC } from 'utils/file'; import { fileIsHEIC } from 'utils/file';
@ -258,10 +258,8 @@ class UploadService {
await this.fetchUploadURLs(); await this.fetchUploadURLs();
// checking for any subscription related errors // checking for any subscription related errors
} catch (e) { } catch (e) {
const { parsedError, parsed } = parseError(e); logError(e, 'prefetch uploadURL failed');
if (parsed) { handleUploadError(e);
throw parsedError;
}
} }
} }

View file

@ -1,6 +1,6 @@
import { File, FILE_TYPE } from 'services/fileService'; import { File, FILE_TYPE } from 'services/fileService';
import { sleep } from 'utils/common'; import { sleep } from 'utils/common';
import { handleError, CustomError } from 'utils/common/errorUtil'; import { handleUploadError, CustomError } from 'utils/common/errorUtil';
import { decryptFile } from 'utils/file'; import { decryptFile } from 'utils/file';
import { logError } from 'utils/sentry'; import { logError } from 'utils/sentry';
import { fileAlreadyInCollection } from 'utils/upload'; import { fileAlreadyInCollection } from 'utils/upload';
@ -19,7 +19,7 @@ import uploadService from './uploadService';
import { FileTypeInfo, getFileType } from './readFileService'; import { FileTypeInfo, getFileType } from './readFileService';
const TwoSecondInMillSeconds = 2000; const TwoSecondInMillSeconds = 2000;
const FIVE_GB_IN_BYTES = 5 * 1024 * 1024 * 1024;
interface UploadResponse { interface UploadResponse {
fileUploadResult: FileUploadResults; fileUploadResult: FileUploadResults;
file?: File; file?: File;
@ -40,6 +40,15 @@ export default async function uploader(
let fileWithMetadata: FileWithMetadata = null; let fileWithMetadata: FileWithMetadata = null;
try { try {
if (rawFile.size >= FIVE_GB_IN_BYTES) {
UIService.setFileProgress(
rawFile.name,
FileUploadResults.TOO_LARGE
);
// wait two second before removing the file from the progress in file section
await sleep(TwoSecondInMillSeconds);
return { fileUploadResult: FileUploadResults.TOO_LARGE };
}
fileTypeInfo = await getFileType(worker, rawFile); fileTypeInfo = await getFileType(worker, rawFile);
if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) { if (fileTypeInfo.fileType === FILE_TYPE.OTHERS) {
throw Error(CustomError.UNSUPPORTED_FILE_FORMAT); throw Error(CustomError.UNSUPPORTED_FILE_FORMAT);
@ -94,12 +103,11 @@ export default async function uploader(
file: decryptedFile, file: decryptedFile,
}; };
} catch (e) { } catch (e) {
console.log(e);
const fileFormat = const fileFormat =
fileTypeInfo.exactType ?? rawFile.name.split('.')[-1]; fileTypeInfo.exactType ?? rawFile.name.split('.')[-1];
logError(e, 'file upload failed', { fileFormat }); logError(e, 'file upload failed', { fileFormat });
handleError(e); const error = handleUploadError(e);
switch (e.message) { switch (error.message) {
case CustomError.ETAG_MISSING: case CustomError.ETAG_MISSING:
UIService.setFileProgress( UIService.setFileProgress(
rawFile.name, rawFile.name,
@ -112,6 +120,13 @@ export default async function uploader(
FileUploadResults.UNSUPPORTED FileUploadResults.UNSUPPORTED
); );
return { fileUploadResult: FileUploadResults.UNSUPPORTED }; return { fileUploadResult: FileUploadResults.UNSUPPORTED };
case CustomError.FILE_TOO_LARGE:
UIService.setFileProgress(
rawFile.name,
FileUploadResults.TOO_LARGE
);
return { fileUploadResult: FileUploadResults.TOO_LARGE };
default: default:
UIService.setFileProgress( UIService.setFileProgress(
rawFile.name, rawFile.name,

View file

@ -1,3 +1,4 @@
import { AxiosResponse } from 'axios';
import constants from 'utils/strings/constants'; import constants from 'utils/strings/constants';
export const ServerErrorCodes = { export const ServerErrorCodes = {
@ -8,51 +9,81 @@ export const ServerErrorCodes = {
FILE_TOO_LARGE: '413', FILE_TOO_LARGE: '413',
}; };
export const CustomError = { export enum CustomError {
SUBSCRIPTION_VERIFICATION_ERROR: 'Subscription verification failed', UNKNOWN_ERROR = 'unknown error',
THUMBNAIL_GENERATION_FAILED: 'thumbnail generation failed', SUBSCRIPTION_VERIFICATION_ERROR = 'Subscription verification failed',
VIDEO_PLAYBACK_FAILED: 'video playback failed', THUMBNAIL_GENERATION_FAILED = 'thumbnail generation failed',
ETAG_MISSING: 'no header/etag present in response body', VIDEO_PLAYBACK_FAILED = 'video playback failed',
KEY_MISSING: 'encrypted key missing from localStorage', ETAG_MISSING = 'no header/etag present in response body',
FAILED_TO_LOAD_WEB_WORKER: 'failed to load web worker', KEY_MISSING = 'encrypted key missing from localStorage',
CHUNK_MORE_THAN_EXPECTED: 'chunks more than expected', FAILED_TO_LOAD_WEB_WORKER = 'failed to load web worker',
UNSUPPORTED_FILE_FORMAT: 'unsupported file formats', CHUNK_MORE_THAN_EXPECTED = 'chunks more than expected',
}; UNSUPPORTED_FILE_FORMAT = 'unsupported file formats',
FILE_TOO_LARGE = 'file too large',
SUBSCRIPTION_EXPIRED = 'subscription expired',
STORAGE_QUOTA_EXCEEDED = 'storage quota exceeded',
SESSION_EXPIRED_MESSAGE = 'session expired',
}
export function parseError(error) { function parseUploadError(error: AxiosResponse) {
let parsedMessage = 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 ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION: case ServerErrorCodes.NO_ACTIVE_SUBSCRIPTION:
parsedMessage = constants.SUBSCRIPTION_EXPIRED; parsedMessage = CustomError.SUBSCRIPTION_EXPIRED;
break; break;
case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED: case ServerErrorCodes.STORAGE_LIMIT_EXCEEDED:
parsedMessage = constants.STORAGE_QUOTA_EXCEEDED; parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED;
break; break;
case ServerErrorCodes.SESSION_EXPIRED: case ServerErrorCodes.SESSION_EXPIRED:
parsedMessage = constants.SESSION_EXPIRED_MESSAGE; parsedMessage = CustomError.SESSION_EXPIRED_MESSAGE;
break; break;
case ServerErrorCodes.FILE_TOO_LARGE: case ServerErrorCodes.FILE_TOO_LARGE:
parsedMessage = constants.FILE_TOO_LARGE; parsedMessage = CustomError.FILE_TOO_LARGE;
break; break;
} }
} }
if (parsedMessage) { if (parsedMessage) {
return { parsedError: new Error(parsedMessage), parsed: true }; return {
parsedError: new Error(parsedMessage),
};
} else { } else {
return { return {
parsedError: new Error(`${constants.UNKNOWN_ERROR} ${error}`), parsedError: new Error(CustomError.UNKNOWN_ERROR),
parsed: false,
}; };
} }
} }
export function handleError(error) { export function handleUploadError(error: AxiosResponse | Error): Error {
const { parsedError, parsed } = parseError(error); let parsedError: Error = null;
if (parsed) { if ('status' in error) {
throw parsedError; parsedError = parseUploadError(error).parsedError;
} else { } else {
// swallow error don't break the caller flow parsedError = error;
}
// breaking errors
switch (parsedError.message) {
case CustomError.SUBSCRIPTION_EXPIRED:
case CustomError.STORAGE_QUOTA_EXCEEDED:
case CustomError.SESSION_EXPIRED_MESSAGE:
throw parsedError;
}
return parsedError;
}
export function getUserFacingErrorMessage(
err: CustomError,
action: () => void
) {
switch (err) {
case CustomError.SESSION_EXPIRED_MESSAGE:
return constants.SESSION_EXPIRED_MESSAGE;
case CustomError.SUBSCRIPTION_EXPIRED:
return constants.SUBSCRIPTION_EXPIRED(action);
case CustomError.STORAGE_QUOTA_EXCEEDED:
return constants.STORAGE_QUOTA_EXCEEDED(action);
default:
return constants.UNKNOWN_ERROR;
} }
} }

View file

@ -2,7 +2,10 @@ import { sleep } from 'utils/common';
const retrySleepTimeInMilliSeconds = [2000, 5000, 10000]; const retrySleepTimeInMilliSeconds = [2000, 5000, 10000];
export async function retryAsyncFunction(func: () => Promise<any>) { export async function retryAsyncFunction(
func: () => Promise<any>,
checkForBreakingError?: (error) => void
) {
const retrier = async ( const retrier = async (
func: () => Promise<any>, func: () => Promise<any>,
attemptNumber: number = 0 attemptNumber: number = 0
@ -11,6 +14,9 @@ export async function retryAsyncFunction(func: () => Promise<any>) {
const resp = await func(); const resp = await func();
return resp; return resp;
} catch (e) { } catch (e) {
if (checkForBreakingError) {
checkForBreakingError(e);
}
if (attemptNumber < retrySleepTimeInMilliSeconds.length) { if (attemptNumber < retrySleepTimeInMilliSeconds.length) {
await sleep(retrySleepTimeInMilliSeconds[attemptNumber]); await sleep(retrySleepTimeInMilliSeconds[attemptNumber]);
await retrier(func, attemptNumber + 1); await retrier(func, attemptNumber + 1);

View file

@ -23,6 +23,14 @@ const Logo = styled.img`
margin-top: -3px; margin-top: -3px;
`; `;
const Trigger = styled.span`
:hover {
text-decoration: underline;
cursor: pointer;
}
color: #51cd7c;
`;
const englishConstants = { const englishConstants = {
HERO_HEADER: () => ( HERO_HEADER: () => (
<div> <div>
@ -120,9 +128,18 @@ const englishConstants = {
</span> </span>
</div> </div>
), ),
SUBSCRIPTION_EXPIRED: 'your subscription has expired, please renew', SUBSCRIPTION_EXPIRED: (action) => (
STORAGE_QUOTA_EXCEEDED: <>
'you have exceeded your storage quota, please upgrade your plan', your subscription has expired, please a{' '}
<Trigger onClick={action}>renew</Trigger>
</>
),
STORAGE_QUOTA_EXCEEDED: (action) => (
<>
you have exceeded your storage quota, please{' '}
<Trigger onClick={action}>upgrade</Trigger> your plan
</>
),
INITIAL_LOAD_DELAY_WARNING: 'the first load may take some time', INITIAL_LOAD_DELAY_WARNING: 'the first load may take some time',
USER_DOES_NOT_EXIST: 'sorry, could not find a user with that email', USER_DOES_NOT_EXIST: 'sorry, could not find a user with that email',
UPLOAD_BUTTON_TEXT: 'upload', UPLOAD_BUTTON_TEXT: 'upload',
@ -520,8 +537,9 @@ const englishConstants = {
UNSUPPORTED_INFO: 'ente does not support these file formats yet', UNSUPPORTED_INFO: 'ente does not support these file formats yet',
BLOCKED_UPLOADS: 'blocked uploads', BLOCKED_UPLOADS: 'blocked uploads',
INPROGRESS_UPLOADS: 'uploads in progress', INPROGRESS_UPLOADS: 'uploads in progress',
FILE_TOO_LARGE: TOO_LARGE_UPLOADS: 'large files',
'the file you are trying to upload is larger than the storage available, please upgrade your plan and try again', TOO_LARGE_INFO:
'these files were not uploaded as they exceed the maximum size limit for your storage plan',
UPLOAD_TO_COLLECTION: 'upload to album', UPLOAD_TO_COLLECTION: 'upload to album',
}; };