Merge pull request #584 from ente-io/notifications-redesign
Notifications redesign
This commit is contained in:
commit
25b350323c
93
src/components/Notification.tsx
Normal file
93
src/components/Notification.tsx
Normal 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>
|
||||
);
|
||||
}
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
|
@ -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(
|
||||
|
|
|
@ -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} />
|
||||
|
||||
|
|
12
src/types/Notification/index.tsx
Normal file
12
src/types/Notification/index.tsx
Normal 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;
|
||||
};
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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 =
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue