refactor: optimize the use of notification center (#3621)
This commit is contained in:
@@ -223,6 +223,21 @@ export const Basic = () => {
|
|||||||
gif
|
gif
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
const key = id++;
|
||||||
|
push({
|
||||||
|
title: `${key} title`,
|
||||||
|
type: 'info',
|
||||||
|
theme: 'default',
|
||||||
|
timeout: 3000,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
default message
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
<NotificationCenter />
|
<NotificationCenter />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -177,21 +177,20 @@ export const notificationContentStyle = style({
|
|||||||
width: '380px',
|
width: '380px',
|
||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
boxShadow: 'var(--affine-shadow-1)',
|
boxShadow: 'var(--affine-shadow-1)',
|
||||||
border: '1px solid var(--affine-border-color)',
|
border: '1px solid var(--affine-black-10)',
|
||||||
background: 'var(--affine-white)',
|
background: 'var(--affine-white)',
|
||||||
transition: 'all 0.3s',
|
transition: 'all 0.3s',
|
||||||
});
|
});
|
||||||
export const notificationTitleContactStyle = style({
|
export const notificationTitleContactStyle = style({
|
||||||
marginRight: '22px',
|
marginRight: '22px',
|
||||||
width: '200px',
|
width: '200px',
|
||||||
whiteSpace: 'nowrap',
|
overflow: 'wrap',
|
||||||
textOverflow: 'ellipsis',
|
lineHeight: '24px',
|
||||||
overflow: 'hidden',
|
fontSize: 'var(--affine-font-base)',
|
||||||
lineHeight: '1.5',
|
|
||||||
});
|
});
|
||||||
export const notificationTitleStyle = style({
|
export const notificationTitleStyle = style({
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
alignItems: 'center',
|
alignItems: 'flex-start',
|
||||||
width: '100%',
|
width: '100%',
|
||||||
justifyContent: 'flex-start',
|
justifyContent: 'flex-start',
|
||||||
});
|
});
|
||||||
@@ -199,6 +198,7 @@ export const notificationDescriptionStyle = style({
|
|||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
color: 'var(--affine-text-secondary-color)',
|
color: 'var(--affine-text-secondary-color)',
|
||||||
marginBottom: '4px',
|
marginBottom: '4px',
|
||||||
|
lineHeight: '22px',
|
||||||
});
|
});
|
||||||
export const notificationTimeStyle = style({
|
export const notificationTimeStyle = style({
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
@@ -234,7 +234,7 @@ export const closeButtonWithMediaStyle = style({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
export const closeButtonColorStyle = style({
|
export const closeButtonColorStyle = style({
|
||||||
color: 'var(--affine-white)',
|
color: 'var(--affine-text-primary-color)',
|
||||||
});
|
});
|
||||||
export const undoButtonStyle = style({
|
export const undoButtonStyle = style({
|
||||||
fontSize: 'var(--affine-font-sm)',
|
fontSize: 'var(--affine-font-sm)',
|
||||||
@@ -296,10 +296,10 @@ export const lightWarningStyle = style({
|
|||||||
borderRadius: '8px',
|
borderRadius: '8px',
|
||||||
});
|
});
|
||||||
export const darkColorStyle = style({
|
export const darkColorStyle = style({
|
||||||
color: 'var(--affine-white)',
|
color: 'var(--affine-pure-white)',
|
||||||
});
|
});
|
||||||
export const lightInfoIconStyle = style({
|
export const lightInfoIconStyle = style({
|
||||||
color: 'var(--affine-processing-color)',
|
color: 'var(--affine-icon-color)',
|
||||||
});
|
});
|
||||||
export const defaultCollapseStyle = styleVariants({
|
export const defaultCollapseStyle = styleVariants({
|
||||||
secondary: {
|
secondary: {
|
||||||
|
|||||||
@@ -1,11 +1,12 @@
|
|||||||
|
import { uuidv4 } from '@blocksuite/store';
|
||||||
import { atom } from 'jotai';
|
import { atom } from 'jotai';
|
||||||
|
|
||||||
export type Notification = {
|
export type Notification = {
|
||||||
key: string;
|
key?: string;
|
||||||
title: string;
|
title: string;
|
||||||
message: string;
|
message?: string;
|
||||||
type: 'success' | 'error' | 'warning' | 'info';
|
type: 'success' | 'error' | 'warning' | 'info';
|
||||||
theme?: 'light' | 'dark';
|
theme?: 'light' | 'dark' | 'default';
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
progressingBar?: boolean;
|
progressingBar?: boolean;
|
||||||
multimedia?: React.ReactNode | JSX.Element | HTMLElement;
|
multimedia?: React.ReactNode | JSX.Element | HTMLElement;
|
||||||
@@ -41,6 +42,7 @@ export const removeNotificationAtom = atom(null, (_, set, key: string) => {
|
|||||||
export const pushNotificationAtom = atom<null, [Notification], void>(
|
export const pushNotificationAtom = atom<null, [Notification], void>(
|
||||||
null,
|
null,
|
||||||
(_, set, newNotification) => {
|
(_, set, newNotification) => {
|
||||||
|
newNotification.key = newNotification.key || uuidv4();
|
||||||
const key = newNotification.key;
|
const key = newNotification.key;
|
||||||
const removeNotification = () =>
|
const removeNotification = () =>
|
||||||
set(notificationsBaseAtom, notifications =>
|
set(notificationsBaseAtom, notifications =>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
// License on the MIT
|
// License on the MIT
|
||||||
// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx
|
// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx
|
||||||
|
|
||||||
import { CloseIcon, InformationIcon } from '@blocksuite/icons';
|
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons';
|
||||||
import * as Toast from '@radix-ui/react-toast';
|
import * as Toast from '@radix-ui/react-toast';
|
||||||
import { IconButton } from '@toeverything/components/button';
|
import { IconButton } from '@toeverything/components/button';
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
@@ -33,7 +33,7 @@ export {
|
|||||||
};
|
};
|
||||||
type Height = {
|
type Height = {
|
||||||
height: number;
|
height: number;
|
||||||
notificationKey: number | string;
|
notificationKey: number | string | undefined;
|
||||||
};
|
};
|
||||||
export type NotificationCardProps = {
|
export type NotificationCardProps = {
|
||||||
notification: Notification;
|
notification: Notification;
|
||||||
@@ -46,24 +46,29 @@ const typeColorMap = {
|
|||||||
info: {
|
info: {
|
||||||
light: styles.lightInfoStyle,
|
light: styles.lightInfoStyle,
|
||||||
dark: styles.darkInfoStyle,
|
dark: styles.darkInfoStyle,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
success: {
|
success: {
|
||||||
light: styles.lightSuccessStyle,
|
light: styles.lightSuccessStyle,
|
||||||
dark: styles.darkSuccessStyle,
|
dark: styles.darkSuccessStyle,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
warning: {
|
warning: {
|
||||||
light: styles.lightWarningStyle,
|
light: styles.lightWarningStyle,
|
||||||
dark: styles.darkWarningStyle,
|
dark: styles.darkWarningStyle,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
error: {
|
error: {
|
||||||
light: styles.lightErrorStyle,
|
light: styles.lightErrorStyle,
|
||||||
dark: styles.darkErrorStyle,
|
dark: styles.darkErrorStyle,
|
||||||
|
default: '',
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
function NotificationCard(props: NotificationCardProps): ReactElement {
|
function NotificationCard(props: NotificationCardProps): ReactElement {
|
||||||
const removeNotification = useSetAtom(removeNotificationAtom);
|
const removeNotification = useSetAtom(removeNotificationAtom);
|
||||||
const { notification, notifications, setHeights, heights, index } = props;
|
const { notification, notifications, setHeights, heights, index } = props;
|
||||||
|
|
||||||
const [expand, setExpand] = useAtom(expandNotificationCenterAtom);
|
const [expand, setExpand] = useAtom(expandNotificationCenterAtom);
|
||||||
// const setNotificationRemoveAnimation = useSetAtom(notificationRemoveAnimationAtom);
|
// const setNotificationRemoveAnimation = useSetAtom(notificationRemoveAnimationAtom);
|
||||||
const [mounted, setMounted] = useState<boolean>(false);
|
const [mounted, setMounted] = useState<boolean>(false);
|
||||||
@@ -89,6 +94,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
const duration = notification.timeout || 3000;
|
const duration = notification.timeout || 3000;
|
||||||
const offset = useRef(0);
|
const offset = useRef(0);
|
||||||
const pointerStartYRef = useRef<number | null>(null);
|
const pointerStartYRef = useRef<number | null>(null);
|
||||||
|
|
||||||
const notificationsHeightBefore = useMemo(() => {
|
const notificationsHeightBefore = useMemo(() => {
|
||||||
return heights.reduce((prev, curr, reducerIndex) => {
|
return heights.reduce((prev, curr, reducerIndex) => {
|
||||||
// Calculate offset up until current notification
|
// Calculate offset up until current notification
|
||||||
@@ -149,7 +155,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
}, [notification.title, notification.key, mounted, setHeights]);
|
}, [notification.title, notification.key, mounted, setHeights]);
|
||||||
|
|
||||||
const typeStyle =
|
const typeStyle =
|
||||||
typeColorMap[notification.type][notification.theme || 'light'];
|
typeColorMap[notification.type][notification.theme || 'dark'];
|
||||||
|
|
||||||
const onClickRemove = useCallback(() => {
|
const onClickRemove = useCallback(() => {
|
||||||
// Save the offset for the exit swipe animation
|
// Save the offset for the exit swipe animation
|
||||||
@@ -159,6 +165,9 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
h.filter(height => height.notificationKey !== notification.key)
|
h.filter(height => height.notificationKey !== notification.key)
|
||||||
);
|
);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
|
if (!notification.key) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
removeNotification(notification.key);
|
removeNotification(notification.key);
|
||||||
}, 200);
|
}, 200);
|
||||||
}, [setHeights, notification.key, removeNotification, offset]);
|
}, [setHeights, notification.key, removeNotification, offset]);
|
||||||
@@ -291,7 +300,7 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx({
|
className={clsx({
|
||||||
[typeStyle]: notification.theme,
|
[typeStyle]: notification.theme !== 'default',
|
||||||
[styles.hasMediaStyle]: notification.multimedia,
|
[styles.hasMediaStyle]: notification.multimedia,
|
||||||
[styles.notificationContentStyle]: !notification.multimedia,
|
[styles.notificationContentStyle]: !notification.multimedia,
|
||||||
})}
|
})}
|
||||||
@@ -306,16 +315,20 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
) : null}
|
) : null}
|
||||||
<Toast.Title
|
<Toast.Title
|
||||||
className={clsx(styles.notificationTitleStyle, {
|
className={clsx(styles.notificationTitleStyle, {
|
||||||
[styles.darkColorStyle]: notification.theme === 'dark',
|
[styles.darkColorStyle]:
|
||||||
|
notification.theme !== 'light' &&
|
||||||
|
notification.theme !== 'default',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.notificationIconStyle, {
|
className={clsx(styles.notificationIconStyle, {
|
||||||
[styles.darkColorStyle]: notification.theme === 'dark',
|
[styles.darkColorStyle]:
|
||||||
[styles.lightInfoIconStyle]: notification.theme !== 'dark',
|
notification.theme !== 'light' &&
|
||||||
|
notification.theme !== 'default',
|
||||||
|
[styles.lightInfoIconStyle]: notification.theme === 'light',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<InformationIcon />
|
<InformationFillDuotoneIcon />
|
||||||
</div>
|
</div>
|
||||||
<div className={styles.notificationTitleContactStyle}>
|
<div className={styles.notificationTitleContactStyle}>
|
||||||
{notification.title}
|
{notification.title}
|
||||||
@@ -323,7 +336,9 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
{notification.undo && (
|
{notification.undo && (
|
||||||
<div
|
<div
|
||||||
className={clsx(styles.undoButtonStyle, {
|
className={clsx(styles.undoButtonStyle, {
|
||||||
[styles.darkColorStyle]: notification.theme === 'dark',
|
[styles.darkColorStyle]:
|
||||||
|
notification.theme !== 'light' &&
|
||||||
|
notification.theme !== 'default',
|
||||||
[styles.undoButtonWithMediaStyle]: notification.multimedia,
|
[styles.undoButtonWithMediaStyle]: notification.multimedia,
|
||||||
})}
|
})}
|
||||||
onClick={onClickUndo}
|
onClick={onClickUndo}
|
||||||
@@ -338,9 +353,10 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
})}
|
})}
|
||||||
style={{
|
style={{
|
||||||
color:
|
color:
|
||||||
notification.theme === 'dark'
|
notification.theme !== 'light' &&
|
||||||
? 'var(--affine-white)'
|
notification.theme !== 'default'
|
||||||
: 'var(--affine-icon-color)',
|
? 'var(--affine-pure-white)'
|
||||||
|
: 'var(--affine-text-primary-color)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CloseIcon onClick={onClickRemove} />
|
<CloseIcon onClick={onClickRemove} />
|
||||||
@@ -349,7 +365,9 @@ function NotificationCard(props: NotificationCardProps): ReactElement {
|
|||||||
</Toast.Title>
|
</Toast.Title>
|
||||||
<Toast.Description
|
<Toast.Description
|
||||||
className={clsx(styles.messageStyle, {
|
className={clsx(styles.messageStyle, {
|
||||||
[styles.darkColorStyle]: notification.theme === 'dark',
|
[styles.darkColorStyle]:
|
||||||
|
notification.theme !== 'light' &&
|
||||||
|
notification.theme !== 'default',
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
{notification.message}
|
{notification.message}
|
||||||
@@ -410,16 +428,18 @@ export function NotificationCenter(): ReactElement {
|
|||||||
if (!notifications.length) return <></>;
|
if (!notifications.length) return <></>;
|
||||||
return (
|
return (
|
||||||
<Toast.Provider swipeDirection="right">
|
<Toast.Provider swipeDirection="right">
|
||||||
{notifications.map((notification, index) => (
|
{notifications.map((notification, index) =>
|
||||||
<NotificationCard
|
notification.key ? (
|
||||||
notification={notification}
|
<NotificationCard
|
||||||
index={index}
|
notification={notification}
|
||||||
key={notification.key}
|
index={index}
|
||||||
notifications={notifications}
|
key={notification.key}
|
||||||
heights={heights}
|
notifications={notifications}
|
||||||
setHeights={setHeights}
|
heights={heights}
|
||||||
/>
|
setHeights={setHeights}
|
||||||
))}
|
/>
|
||||||
|
) : null
|
||||||
|
)}
|
||||||
<Toast.Viewport
|
<Toast.Viewport
|
||||||
tabIndex={-1}
|
tabIndex={-1}
|
||||||
ref={listRef}
|
ref={listRef}
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import {
|
|||||||
ExportToPdfIcon,
|
ExportToPdfIcon,
|
||||||
ExportToPngIcon,
|
ExportToPngIcon,
|
||||||
} from '@blocksuite/icons';
|
} from '@blocksuite/icons';
|
||||||
import { uuidv4 } from '@blocksuite/store';
|
|
||||||
import { useSetAtom } from 'jotai';
|
import { useSetAtom } from 'jotai';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
@@ -41,7 +40,6 @@ export const ExportToPdfMenuItem = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSelect?.({ type: 'pdf' });
|
onSelect?.({ type: 'pdf' });
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.success.title'](),
|
title: t['com.affine.export.success.title'](),
|
||||||
message: t['com.affine.export.success.message'](),
|
message: t['com.affine.export.success.message'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -50,7 +48,6 @@ export const ExportToPdfMenuItem = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.error.title'](),
|
title: t['com.affine.export.error.title'](),
|
||||||
message: t['com.affine.export.error.message'](),
|
message: t['com.affine.export.error.message'](),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -64,7 +61,6 @@ export const ExportToPdfMenuItem = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSelect?.({ type: 'pdf' });
|
onSelect?.({ type: 'pdf' });
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.success.title'](),
|
title: t['com.affine.export.success.title'](),
|
||||||
message: t['com.affine.export.success.message'](),
|
message: t['com.affine.export.success.message'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -73,7 +69,6 @@ export const ExportToPdfMenuItem = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.error.title'](),
|
title: t['com.affine.export.error.title'](),
|
||||||
message: t['com.affine.export.error.message'](),
|
message: t['com.affine.export.error.message'](),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -110,7 +105,6 @@ export const ExportToHtmlMenuItem = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSelect?.({ type: 'html' });
|
onSelect?.({ type: 'html' });
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.success.title'](),
|
title: t['com.affine.export.success.title'](),
|
||||||
message: t['com.affine.export.success.message'](),
|
message: t['com.affine.export.success.message'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -119,7 +113,6 @@ export const ExportToHtmlMenuItem = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.error.title'](),
|
title: t['com.affine.export.error.title'](),
|
||||||
message: t['com.affine.export.error.message'](),
|
message: t['com.affine.export.error.message'](),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -159,7 +152,6 @@ export const ExportToPngMenuItem = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSelect?.({ type: 'png' });
|
onSelect?.({ type: 'png' });
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.success.title'](),
|
title: t['com.affine.export.success.title'](),
|
||||||
message: t['com.affine.export.success.message'](),
|
message: t['com.affine.export.success.message'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -168,7 +160,6 @@ export const ExportToPngMenuItem = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.error.title'](),
|
title: t['com.affine.export.error.title'](),
|
||||||
message: t['com.affine.export.error.message'](),
|
message: t['com.affine.export.error.message'](),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
@@ -206,7 +197,6 @@ export const ExportToMarkdownMenuItem = ({
|
|||||||
.then(() => {
|
.then(() => {
|
||||||
onSelect?.({ type: 'markdown' });
|
onSelect?.({ type: 'markdown' });
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.success.title'](),
|
title: t['com.affine.export.success.title'](),
|
||||||
message: t['com.affine.export.success.message'](),
|
message: t['com.affine.export.success.message'](),
|
||||||
type: 'success',
|
type: 'success',
|
||||||
@@ -215,7 +205,6 @@ export const ExportToMarkdownMenuItem = ({
|
|||||||
.catch(err => {
|
.catch(err => {
|
||||||
console.error(err);
|
console.error(err);
|
||||||
setPushNotification({
|
setPushNotification({
|
||||||
key: uuidv4(),
|
|
||||||
title: t['com.affine.export.error.title'](),
|
title: t['com.affine.export.error.title'](),
|
||||||
message: t['com.affine.export.error.message'](),
|
message: t['com.affine.export.error.message'](),
|
||||||
type: 'error',
|
type: 'error',
|
||||||
|
|||||||
Reference in New Issue
Block a user