Merge pull request #584 from ente-io/notifications-redesign

Notifications redesign
This commit is contained in:
Abhinav Kumar 2022-06-07 20:41:57 +05:30 committed by GitHub
commit 25b350323c
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 189 additions and 146 deletions

View file

@ -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 (
<Snackbar
open={open}
anchorOrigin={{
horizontal: 'right',
vertical: 'bottom',
}}>
<Paper
component={Button}
color={attributes.variant}
onClick={handleClick}
css={`
width: 320px;
padding: 12px 16px;
`}
sx={{ textAlign: 'left' }}>
<Stack
flex={'1'}
spacing={2}
direction="row"
alignItems={'center'}>
<Box>
{attributes?.icon ?? <InfoIcon fontSize="large" />}
</Box>
<Box sx={{ flex: 1 }}>
<Typography
variant="body2"
color="rgba(255, 255, 255, 0.7)"
mb={0.5}>
{attributes.message}{' '}
</Typography>
{attributes?.action && (
<Typography
mb={0.5}
css={`
font-size: 16px;
font-weight: 600;
line-height: 19px;
`}>
{attributes?.action.text}
</Typography>
)}
</Box>
<Box>
<IconButton
onClick={handleClose}
sx={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
}}>
<CloseIcon />
</IconButton>
</Box>
</Stack>
</Paper>
</Snackbar>
);
}

View file

@ -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;

View file

@ -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 (
<Wrapper>
<Toast
onClose={closeToast}
show={show}
delay={AUTO_HIDE_TIME_IN_MILLISECONDS}
autohide>
{attributes?.title && (
<Toast.Header
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
}}>
<h6 style={{ marginBottom: 0 }}>{attributes.title} </h6>
</Toast.Header>
)}
{attributes?.message && (
<Toast.Body>{attributes.message}</Toast.Body>
)}
</Toast>
</Wrapper>
);
}

View file

@ -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: <DiscFullIcon fontSize="large" />,
};
break;
default:
notification = {
variant: 'danger',
message: constants.UNKNOWN_ERROR,
};
}
galleryContext.setNotificationAttributes(notification);
}
const uploadToSingleNewCollection = (collectionName: string) => {
if (collectionName) {
uploadFilesToNewCollections(

View file

@ -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<EnteFile[]>(null);
const [favItemIds, setFavItemIds] = useState<Set<number>>();
const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>(
null
);
const [bannerMessage, setBannerMessage] = useState<JSX.Element | string>();
const [isFirstLoad, setIsFirstLoad] = useState(false);
const [isFirstFetch, setIsFirstFetch] = useState(false);
const [selected, setSelected] = useState<SelectedState>({
@ -194,6 +189,10 @@ export default function Gallery() {
const [fixCreationTimeAttributes, setFixCreationTimeAttributes] =
useState<FixCreationTimeAttributes>(null);
const [notificationView, setNotificationView] = useState(false);
const closeNotification = () => setNotificationView(false);
const [notificationAttributes, setNotificationAttributes] =
useState<NotificationAttributes>(null);
@ -202,8 +201,6 @@ export default function Gallery() {
const showPlanSelectorModal = () => setPlanModalView(true);
const clearNotificationAttributes = () => setNotificationAttributes(null);
const [electronFiles, setElectronFiles] = useState<ElectronFile[]>(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}
/>
<AlertBanner bannerMessage={bannerMessage} />
<ToastNotification
<Notification
open={notificationView}
onClose={closeNotification}
attributes={notificationAttributes}
clearAttributes={clearNotificationAttributes}
/>
<CollectionNamer
show={collectionNamerView}
@ -676,6 +681,7 @@ export default function Gallery() {
setElectronFiles={setElectronFiles}
uploadTypeSelectorView={uploadTypeSelectorView}
setUploadTypeSelectorView={setUploadTypeSelectorView}
showSessionExpiredMessage={showSessionExpiredMessage}
/>
<Sidebar collectionSummaries={collectionSummaries} />

View file

@ -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;
};
}

View file

@ -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;
}

View file

@ -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 =

View file

@ -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{' '}
<Trigger onClick={action}>renew</Trigger>
</>
),
STORAGE_QUOTA_EXCEEDED: (action) => (
<>
you have exceeded your storage quota, please{' '}
<Trigger onClick={action}>upgrade</Trigger> 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 = {
</p>
</>
),
UPGRADE_NOW: 'Upgrade now',
RENEW_NOW: 'Renew now',
};
export default englishConstants;