diff --git a/src/components/Notification.tsx b/src/components/Notification.tsx new file mode 100644 index 000000000..405ba85cf --- /dev/null +++ b/src/components/Notification.tsx @@ -0,0 +1,93 @@ +import CloseIcon from '@mui/icons-material/Close'; +import { + Box, + Button, + ButtonProps, + IconButton, + Paper, + Snackbar, + Stack, + Typography, +} from '@mui/material'; +import React from 'react'; +import { NotificationAttributes } from 'types/Notification'; + +import InfoIcon from '@mui/icons-material/Info'; + +interface Iprops { + open: boolean; + onClose: () => void; + attributes: NotificationAttributes; +} + +export default function Notification({ open, onClose, attributes }: Iprops) { + if (!attributes) { + return <>; + } + + const handleClose: ButtonProps['onClick'] = (event) => { + onClose(); + event.stopPropagation(); + }; + + const handleClick = () => { + attributes.action?.callback(); + onClose(); + }; + return ( + + + + + {attributes?.icon ?? } + + + + {attributes.message}{' '} + + {attributes?.action && ( + + {attributes?.action.text} + + )} + + + + + + + + + + ); +} diff --git a/src/components/PhotoSwipe/index.tsx b/src/components/PhotoSwipe/index.tsx index 4bf37183c..c54f9b2e4 100644 --- a/src/components/PhotoSwipe/index.tsx +++ b/src/components/PhotoSwipe/index.tsx @@ -20,7 +20,6 @@ import { FILE_TYPE } from 'constants/file'; import { sleep } from 'utils/common'; import { playVideo, pauseVideo } from 'utils/photoFrame'; import { PublicCollectionGalleryContext } from 'utils/publicCollectionGallery'; -import { GalleryContext } from 'pages/gallery'; import { AppContext } from 'pages/_app'; import { InfoModal } from './InfoDialog'; import { defaultLivePhotoDefaultOptions } from 'constants/photoswipe'; @@ -58,7 +57,6 @@ function PhotoSwipe(props: Iprops) { const publicCollectionGalleryContext = useContext( PublicCollectionGalleryContext ); - const galleryContext = useContext(GalleryContext); const appContext = useContext(AppContext); useEffect(() => { @@ -237,8 +235,6 @@ function PhotoSwipe(props: Iprops) { videoTag.pause(); } handleCloseInfo(); - // BE_AWARE: this will clear any notification set, even if they were not set in/by the photoswipe component - galleryContext.setNotificationAttributes(null); }; const isInFav = (file) => { const { favItemIds } = props; diff --git a/src/components/ToastNotification.tsx b/src/components/ToastNotification.tsx deleted file mode 100644 index a885fc4ab..000000000 --- a/src/components/ToastNotification.tsx +++ /dev/null @@ -1,59 +0,0 @@ -import React, { useEffect, useState } from 'react'; -import { Toast } from 'react-bootstrap'; -import styled from 'styled-components'; -import { NotificationAttributes } from 'types/gallery'; - -const Wrapper = styled.div` - position: absolute; - top: 60px; - right: 10px; - z-index: 1501; - min-height: 100px; -`; -const AUTO_HIDE_TIME_IN_MILLISECONDS = 3000; - -interface Iprops { - attributes: NotificationAttributes; - clearAttributes: () => void; -} - -export default function ToastNotification({ - attributes, - clearAttributes, -}: Iprops) { - const [show, setShow] = useState(false); - const closeToast = () => { - setShow(false); - clearAttributes(); - }; - useEffect(() => { - if (!attributes) { - setShow(false); - } else { - setShow(true); - } - }, [attributes]); - return ( - - - {attributes?.title && ( - -
{attributes.title}
-
- )} - {attributes?.message && ( - {attributes.message} - )} -
-
- ); -} diff --git a/src/components/pages/gallery/Upload.tsx b/src/components/pages/gallery/Upload.tsx index cccaa9556..7ee93d9cc 100644 --- a/src/components/pages/gallery/Upload.tsx +++ b/src/components/pages/gallery/Upload.tsx @@ -16,7 +16,7 @@ import uploadManager from 'services/upload/uploadManager'; import ImportService from 'services/importService'; import isElectron from 'is-electron'; import { METADATA_FOLDER_NAME } from 'constants/export'; -import { getUserFacingErrorMessage } from 'utils/error'; +import { CustomError } from 'utils/error'; import { Collection } from 'types/collection'; import { SetLoading, SetFiles } from 'types/gallery'; import { FileUploadResults, UPLOAD_STAGES } from 'constants/upload'; @@ -25,6 +25,8 @@ import UploadTypeSelector from '../../UploadTypeSelector'; import Router from 'next/router'; import { isCanvasBlocked } from 'utils/upload/isCanvasBlocked'; import { downloadApp } from 'utils/common'; +import DiscFullIcon from '@mui/icons-material/DiscFull'; +import { NotificationAttributes } from 'types/Notification'; const FIRST_ALBUM_NAME = 'My First Album'; @@ -47,6 +49,7 @@ interface Props { setElectronFiles: (files: ElectronFile[]) => void; uploadTypeSelectorView: boolean; setUploadTypeSelectorView: (open: boolean) => void; + showSessionExpiredMessage: () => void; } enum UPLOAD_STRATEGY { @@ -350,11 +353,7 @@ export default function Upload(props: Props) { collections ); } catch (err) { - const message = getUserFacingErrorMessage( - err.message, - galleryContext.showPlanSelectorModal - ); - props.setBannerMessage(message); + showUserFacingError(err.message); setProgressView(false); throw err; } finally { @@ -362,6 +361,7 @@ export default function Upload(props: Props) { props.syncWithRemote(); } }; + const retryFailed = async () => { try { props.setUploadInProgress(true); @@ -369,12 +369,8 @@ export default function Upload(props: Props) { await props.syncWithRemote(true, true); await uploadManager.retryFailedFiles(); } catch (err) { - const message = getUserFacingErrorMessage( - err.message, - galleryContext.showPlanSelectorModal - ); - appContext.resetSharedFiles(); - props.setBannerMessage(message); + showUserFacingError(err.message); + setProgressView(false); } finally { props.setUploadInProgress(false); @@ -382,6 +378,41 @@ export default function Upload(props: Props) { } }; + function showUserFacingError(err: CustomError) { + let notification: NotificationAttributes; + switch (err) { + case CustomError.SESSION_EXPIRED: + return props.showSessionExpiredMessage(); + case CustomError.SUBSCRIPTION_EXPIRED: + notification = { + variant: 'danger', + message: constants.SUBSCRIPTION_EXPIRED, + action: { + text: constants.UPGRADE_NOW, + callback: galleryContext.showPlanSelectorModal, + }, + }; + break; + case CustomError.STORAGE_QUOTA_EXCEEDED: + notification = { + variant: 'danger', + message: constants.STORAGE_QUOTA_EXCEEDED, + action: { + text: constants.RENEW_NOW, + callback: galleryContext.showPlanSelectorModal, + }, + icon: , + }; + break; + default: + notification = { + variant: 'danger', + message: constants.UNKNOWN_ERROR, + }; + } + galleryContext.setNotificationAttributes(notification); + } + const uploadToSingleNewCollection = (collectionName: string) => { if (collectionName) { uploadFilesToNewCollections( diff --git a/src/pages/gallery/index.tsx b/src/pages/gallery/index.tsx index edb03daf9..760f3f439 100644 --- a/src/pages/gallery/index.tsx +++ b/src/pages/gallery/index.tsx @@ -96,19 +96,16 @@ import { CollectionSummary, } from 'types/collection'; import { EnteFile } from 'types/file'; -import { - GalleryContextType, - SelectedState, - NotificationAttributes, -} from 'types/gallery'; +import { GalleryContextType, SelectedState } from 'types/gallery'; import { VISIBILITY_STATE } from 'types/magicMetadata'; -import ToastNotification from 'components/ToastNotification'; +import Notification from 'components/Notification'; import { ElectronFile } from 'types/upload'; import importService from 'services/importService'; import Collections from 'components/Collections'; import { GalleryNavbar } from 'components/pages/gallery/Navbar'; import { Search } from 'types/search'; import SearchResultInfo from 'components/Search/SearchResultInfo'; +import { NotificationAttributes } from 'types/Notification'; export const DeadCenter = styled.div` flex: 1; @@ -147,9 +144,7 @@ export default function Gallery() { const [files, setFiles] = useState(null); const [favItemIds, setFavItemIds] = useState>(); - const [bannerMessage, setBannerMessage] = useState( - null - ); + const [bannerMessage, setBannerMessage] = useState(); const [isFirstLoad, setIsFirstLoad] = useState(false); const [isFirstFetch, setIsFirstFetch] = useState(false); const [selected, setSelected] = useState({ @@ -194,6 +189,10 @@ export default function Gallery() { const [fixCreationTimeAttributes, setFixCreationTimeAttributes] = useState(null); + const [notificationView, setNotificationView] = useState(false); + + const closeNotification = () => setNotificationView(false); + const [notificationAttributes, setNotificationAttributes] = useState(null); @@ -202,8 +201,6 @@ export default function Gallery() { const showPlanSelectorModal = () => setPlanModalView(true); - const clearNotificationAttributes = () => setNotificationAttributes(null); - const [electronFiles, setElectronFiles] = useState(null); const [uploadTypeSelectorView, setUploadTypeSelectorView] = useState(false); @@ -213,6 +210,19 @@ export default function Gallery() { const openSidebar = () => setSidebarView(true); const [droppedFiles, setDroppedFiles] = useState([]); + const showSessionExpiredMessage = () => + setDialogMessage({ + title: constants.SESSION_EXPIRED, + content: constants.SESSION_EXPIRED_MESSAGE, + staticBackdrop: true, + nonClosable: true, + proceed: { + text: constants.LOGIN, + action: logoutUser, + variant: 'success', + }, + }); + useEffect(() => { appContext.showNavBar(false); const key = getKey(SESSION_KEYS.ENCRYPTION_KEY); @@ -259,6 +269,11 @@ export default function Gallery() { [fixCreationTimeAttributes] ); + useEffect( + () => notificationAttributes && setNotificationView(true), + [notificationAttributes] + ); + useEffect(() => setDroppedFiles(acceptedFiles), [acceptedFiles]); useEffect(() => { @@ -315,18 +330,7 @@ export default function Gallery() { logError(e, 'syncWithRemote failed'); switch (e.message) { case ServerErrorCodes.SESSION_EXPIRED: - setBannerMessage(constants.SESSION_EXPIRED_MESSAGE); - setDialogMessage({ - title: constants.SESSION_EXPIRED, - content: constants.SESSION_EXPIRED_MESSAGE, - staticBackdrop: true, - nonClosable: true, - proceed: { - text: constants.LOGIN, - action: logoutUser, - variant: 'success', - }, - }); + showSessionExpiredMessage(); break; case CustomError.KEY_MISSING: clearKeys(); @@ -602,9 +606,10 @@ export default function Gallery() { setLoading={setBlockingLoad} /> - diff --git a/src/types/Notification/index.tsx b/src/types/Notification/index.tsx new file mode 100644 index 000000000..22ce43982 --- /dev/null +++ b/src/types/Notification/index.tsx @@ -0,0 +1,12 @@ +import { ButtonProps } from '@mui/material/Button'; +import { ReactNode } from 'react'; + +export interface NotificationAttributes { + icon?: ReactNode; + variant: ButtonProps['color']; + message: JSX.Element | string; + action?: { + text: string; + callback: () => void; + }; +} diff --git a/src/types/gallery/index.ts b/src/types/gallery/index.ts index 1dc568518..7b8662d04 100644 --- a/src/types/gallery/index.ts +++ b/src/types/gallery/index.ts @@ -1,5 +1,6 @@ import { Collection } from 'types/collection'; import { EnteFile } from 'types/file'; +import { NotificationAttributes } from 'types/Notification'; import { Search, SearchResultSummary } from 'types/search'; export type SelectedState = { @@ -26,8 +27,3 @@ export type GalleryContextType = { sidebarView: boolean; closeSidebar: () => void; }; - -export interface NotificationAttributes { - message: string; - title: string; -} diff --git a/src/utils/error/index.ts b/src/utils/error/index.ts index 21e52cf76..620474c96 100644 --- a/src/utils/error/index.ts +++ b/src/utils/error/index.ts @@ -26,7 +26,7 @@ export enum CustomError { FILE_TOO_LARGE = 'file too large', SUBSCRIPTION_EXPIRED = 'subscription expired', STORAGE_QUOTA_EXCEEDED = 'storage quota exceeded', - SESSION_EXPIRED_MESSAGE = 'session expired', + SESSION_EXPIRED = 'session expired', TYPE_DETECTION_FAILED = 'type detection failed', SIGNUP_FAILED = 'signup failed', FAV_COLLECTION_MISSING = 'favorite collection missing', @@ -57,7 +57,7 @@ function parseUploadErrorCodes(error) { parsedMessage = CustomError.STORAGE_QUOTA_EXCEEDED; break; case ServerErrorCodes.SESSION_EXPIRED: - parsedMessage = CustomError.SESSION_EXPIRED_MESSAGE; + parsedMessage = CustomError.SESSION_EXPIRED; break; case ServerErrorCodes.FILE_TOO_LARGE: parsedMessage = CustomError.FILE_TOO_LARGE; @@ -78,28 +78,12 @@ export function handleUploadError(error): Error { switch (parsedError.message) { case CustomError.SUBSCRIPTION_EXPIRED: case CustomError.STORAGE_QUOTA_EXCEEDED: - case CustomError.SESSION_EXPIRED_MESSAGE: + case CustomError.SESSION_EXPIRED: 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; - } -} - export function errorWithContext(originalError: Error, context: string) { const errorWithContext = new Error(context); errorWithContext.stack = diff --git a/src/utils/strings/englishConstants.tsx b/src/utils/strings/englishConstants.tsx index b90454342..fa51bb948 100644 --- a/src/utils/strings/englishConstants.tsx +++ b/src/utils/strings/englishConstants.tsx @@ -27,14 +27,6 @@ const Logo = styled.img` margin-top: -3px; `; -const Trigger = styled.span` - :hover { - text-decoration: underline; - cursor: pointer; - } - color: #51cd7c; -`; - const englishConstants = { ENTE: 'ente', HERO_HEADER: () => ( @@ -79,7 +71,7 @@ const englishConstants = { ENTER_OTT: 'verification code', RESEND_MAIL: 'Resend code', VERIFY: 'verify', - UNKNOWN_ERROR: 'something went wrong, please try again', + UNKNOWN_ERROR: 'Something went wrong, please try again', INVALID_CODE: 'invalid verification code', EXPIRED_CODE: 'your verification code has expired', SENDING: 'sending...', @@ -132,18 +124,8 @@ const englishConstants = { }, UPLOADING_FILES: 'file upload', FILE_NOT_UPLOADED_LIST: 'the following files were not uploaded', - SUBSCRIPTION_EXPIRED: (action) => ( - <> - your subscription has expired, please a{' '} - renew - - ), - STORAGE_QUOTA_EXCEEDED: (action) => ( - <> - you have exceeded your storage quota, please{' '} - upgrade your plan - - ), + SUBSCRIPTION_EXPIRED: 'Subscription expired', + STORAGE_QUOTA_EXCEEDED: 'Storage limit exceeded', INITIAL_LOAD_DELAY_WARNING: 'the first load may take some time', USER_DOES_NOT_EXIST: 'sorry, could not find a user with that email', UPLOAD_BUTTON_TEXT: 'upload', @@ -173,7 +155,7 @@ const englishConstants = { UPLOAD_STRATEGY_COLLECTION_PER_FOLDER: 'separate albums', SESSION_EXPIRED_MESSAGE: 'your session has expired, please login again to continue', - SESSION_EXPIRED: 'session expired', + SESSION_EXPIRED: 'Session expired', SYNC_FAILED: 'failed to sync with server, please refresh this page', PASSWORD_GENERATION_FAILED: "your browser was unable to generate a strong key that meets ente's encryption standards, please try using the mobile app or another browser", @@ -789,6 +771,8 @@ const englishConstants = {

), + UPGRADE_NOW: 'Upgrade now', + RENEW_NOW: 'Renew now', }; export default englishConstants;