snapshot of https://github.com/toeverything/AFFiNE.git (canary, 2026-09-16; blobs > 131072B omitted)
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
Some checks failed
Build & Test / Analyze (javascript, affine) (push) Failing after 4s
Build & Test / Analyze (typescript, affine) (push) Failing after 3s
Build & Test / Rust test filter (push) Failing after 4s
Build & Test / Check yarn binary (push) Failing after 3s
Build & Test / E2E BlockSuite Test (3) (push) Failing after 3s
Build & Test / Analyze (javascript, blocksuite) (push) Failing after 3s
Build & Test / Analyze (typescript, blocksuite) (push) Failing after 3s
Build & Test / Mobile native build filter (push) Failing after 3s
Build & Test / E2E BlockSuite Test (1) (push) Failing after 4s
Build & Test / E2E BlockSuite Test (2) (push) Failing after 4s
Build & Test / E2E Mobile Test (1) (push) Failing after 3s
Build & Test / Unit Test (1) (push) Has been skipped
Build & Test / Unit Test (2) (push) Has been skipped
Build & Test / Unit Test (3) (push) Has been skipped
Build & Test / Unit Test (4) (push) Has been skipped
Build & Test / Unit Test (5) (push) Has been skipped
Build & Test / Native Unit Test (push) Has been skipped
Build & Test / E2E BlockSuite Test (4) (push) Failing after 3s
Build & Test / E2E BlockSuite Test (5) (push) Failing after 3s
Build & Test / E2E BlockSuite Cross Browser Test (push) Failing after 3s
Build & Test / E2E Mobile Test (2) (push) Failing after 3s
Build & Test / Build AFFiNE native (x86_64-unknown-linux-gnu) (push) Failing after 3s
Build & Test / Build Server native (push) Failing after 3s
Build & Test / Check Git Status (push) Has been skipped
Build & Test / Server Test (0, 4) (push) Has been skipped
Build & Test / Server Test (1, 4) (push) Has been skipped
Build & Test / Server Test (2, 4) (push) Has been skipped
Build & Test / Server Test (3, 4) (push) Has been skipped
Build & Test / Server Test with Elasticsearch (push) Has been skipped
Build & Test / Build @affine/electron renderer (push) Failing after 3s
Build & Test / Copilot test filter (push) Failing after 4s
Build & Test / Lint Rust (push) Has been skipped
Build & Test / Server Test with Embedded Search (push) Has been skipped
Build & Test / Server Test with Manticore Search (push) Has been skipped
Build & Test / Server E2E Test (push) Has been skipped
Build & Test / Cloud Desktop E2E Test (push) Has been skipped
Build & Test / Cloud E2E Test 1/2 (push) Has been skipped
Build & Test / Cloud E2E Test 2/2 (push) Has been skipped
Build & Test / Build Android app (push) Has been skipped
Build & Test / Test Android app (push) Has been skipped
Build & Test / Run native tests (push) Has been skipped
Build & Test / Server Copilot Api Test (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (1, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (2, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (3, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (4, 5) (push) Has been skipped
Build & Test / Frontend Copilot E2E Test (5, 5) (push) Has been skipped
Release / Canary Gate (push) Has been skipped
Build & Test / Build AFFiNE native (aarch64-apple-darwin) (push) Has been cancelled
Build & Test / Lint (push) Has been cancelled
Build & Test / Typecheck (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-apple-darwin) (push) Has been cancelled
Build & Test / Desktop Test (ubuntu-latest, linux, x64, x86_64-unknown-linux-gnu, true) (push) Has been cancelled
Build & Test / Desktop Test (windows-latest, windows, x64, x86_64-pc-windows-msvc, true) (push) Has been cancelled
Build & Test / E2E Test (1) (push) Has been cancelled
Build & Test / E2E Test (2) (push) Has been cancelled
Build & Test / E2E Test (3) (push) Has been cancelled
Build & Test / E2E Test (4) (push) Has been cancelled
Build & Test / E2E Test (5) (push) Has been cancelled
Build & Test / Build AFFiNE native (aarch64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Build AFFiNE native (x86_64-pc-windows-msvc) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, arm64, aarch64-apple-darwin, true) (push) Has been cancelled
Build & Test / Desktop Test (macos-latest, macos, x64, x86_64-apple-darwin, false) (push) Has been cancelled
Build & Test / 3, 2, 1 Launch (push) Has been cancelled
Build & Test / Build iOS app (push) Has been cancelled
Copilot Test Automatically / Setup Test (push) Failing after 1s
Release / Prepare (push) Failing after 2s
Release / Release Cloud (push) Has been skipped
Release / Release Desktop (push) Has been skipped
Release / Release Docker Image (push) Has been skipped
Release / Release Mobile (push) Has been skipped
This commit is contained in:
@@ -0,0 +1,425 @@
|
||||
// Credits to sonner
|
||||
// License on the MIT
|
||||
// https://github.com/emilkowalski/sonner/blob/5cb703edc108a23fd74979235c2f3c4005edd2a7/src/index.tsx
|
||||
|
||||
import { useI18n } from '@affine/i18n';
|
||||
import { CloseIcon, InformationFillDuotoneIcon } from '@blocksuite/icons/rc';
|
||||
import * as Toast from '@radix-ui/react-toast';
|
||||
import clsx from 'clsx';
|
||||
import { useAtom, useAtomValue, useSetAtom } from 'jotai';
|
||||
import type { ReactNode } from 'react';
|
||||
import {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
|
||||
import { IconButton } from '../../ui/button';
|
||||
import { SuccessIcon } from './icons';
|
||||
import * as styles from './index.css';
|
||||
import type { Notification } from './index.jotai';
|
||||
import {
|
||||
expandNotificationCenterAtom,
|
||||
notificationsAtom,
|
||||
pushNotificationAtom,
|
||||
removeNotificationAtom,
|
||||
} from './index.jotai';
|
||||
|
||||
export {
|
||||
expandNotificationCenterAtom,
|
||||
pushNotificationAtom,
|
||||
removeNotificationAtom,
|
||||
};
|
||||
type Height = {
|
||||
height: number;
|
||||
notificationKey: number | string | undefined;
|
||||
};
|
||||
export type NotificationCardProps = {
|
||||
notification: Notification;
|
||||
notifications: Notification[];
|
||||
index: number;
|
||||
heights: Height[];
|
||||
setHeights: React.Dispatch<React.SetStateAction<Height[]>>;
|
||||
};
|
||||
const typeColorMap = {
|
||||
info: {
|
||||
light: styles.lightInfoStyle,
|
||||
dark: styles.darkInfoStyle,
|
||||
default: '',
|
||||
icon: <InformationFillDuotoneIcon />,
|
||||
},
|
||||
success: {
|
||||
light: styles.lightSuccessStyle,
|
||||
dark: styles.darkSuccessStyle,
|
||||
default: '',
|
||||
icon: <SuccessIcon />,
|
||||
},
|
||||
warning: {
|
||||
light: styles.lightWarningStyle,
|
||||
dark: styles.darkWarningStyle,
|
||||
default: '',
|
||||
icon: <InformationFillDuotoneIcon />,
|
||||
},
|
||||
error: {
|
||||
light: styles.lightErrorStyle,
|
||||
dark: styles.darkErrorStyle,
|
||||
default: '',
|
||||
icon: <InformationFillDuotoneIcon />,
|
||||
},
|
||||
};
|
||||
|
||||
function NotificationCard(props: NotificationCardProps): ReactNode {
|
||||
const t = useI18n();
|
||||
const removeNotification = useSetAtom(removeNotificationAtom);
|
||||
const { notification, notifications, setHeights, heights, index } = props;
|
||||
|
||||
const [expand, setExpand] = useAtom(expandNotificationCenterAtom);
|
||||
// const setNotificationRemoveAnimation = useSetAtom(notificationRemoveAnimationAtom);
|
||||
const [mounted, setMounted] = useState<boolean>(false);
|
||||
const [removed, setRemoved] = useState<boolean>(false);
|
||||
const [offsetBeforeRemove, setOffsetBeforeRemove] = useState<number>(0);
|
||||
const [initialHeight, setInitialHeight] = useState<number>(0);
|
||||
const [animationKey, setAnimationKey] = useState(0);
|
||||
const animationRef = useRef<SVGAnimateElement>(null);
|
||||
const notificationRef = useRef<HTMLLIElement>(null);
|
||||
const timerIdRef = useRef<number | null>(null);
|
||||
const isFront = index === 0;
|
||||
const isVisible = index + 1 <= 3;
|
||||
const progressDuration = notification.timeout || 3000;
|
||||
const heightIndex = useMemo(
|
||||
() =>
|
||||
heights.findIndex(
|
||||
height => height.notificationKey === notification.key
|
||||
) || 0,
|
||||
[heights, notification.key]
|
||||
);
|
||||
const duration = notification.timeout || 3000;
|
||||
const offset = useRef(0);
|
||||
|
||||
const notificationsHeightBefore = useMemo(() => {
|
||||
return heights.reduce((prev, curr, reducerIndex) => {
|
||||
// Calculate offset up until current notification
|
||||
if (reducerIndex >= heightIndex) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
return prev + curr.height;
|
||||
}, 0);
|
||||
}, [heights, heightIndex]);
|
||||
|
||||
offset.current = useMemo(
|
||||
() => heightIndex * 14 + notificationsHeightBefore,
|
||||
[heightIndex, notificationsHeightBefore]
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
// Trigger enter animation without using CSS animation
|
||||
setMounted(true);
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
if (!expand) {
|
||||
animationRef.current?.beginElement();
|
||||
}
|
||||
}, [expand]);
|
||||
|
||||
const resetAnimation = () => {
|
||||
setAnimationKey(prevKey => prevKey + 1);
|
||||
};
|
||||
useLayoutEffect(() => {
|
||||
if (!mounted) return;
|
||||
if (!notificationRef.current) return;
|
||||
const notificationNode = notificationRef.current;
|
||||
const originalHeight = notificationNode.style.height;
|
||||
notificationNode.style.height = 'auto';
|
||||
const newHeight = notificationNode.getBoundingClientRect().height;
|
||||
notificationNode.style.height = originalHeight;
|
||||
setInitialHeight(newHeight);
|
||||
|
||||
setHeights(heights => {
|
||||
const alreadyExists = heights.find(
|
||||
height => height.notificationKey === notification.key
|
||||
);
|
||||
if (!alreadyExists) {
|
||||
return [
|
||||
{ notificationKey: notification.key, height: newHeight },
|
||||
...heights,
|
||||
];
|
||||
} else {
|
||||
return heights.map(height =>
|
||||
height.notificationKey === notification.key
|
||||
? { ...height, height: newHeight }
|
||||
: height
|
||||
);
|
||||
}
|
||||
});
|
||||
}, [notification.title, notification.key, mounted, setHeights]);
|
||||
|
||||
const typeStyle =
|
||||
typeColorMap[notification.type][notification.theme || 'dark'];
|
||||
|
||||
const onClickRemove = useCallback(() => {
|
||||
// Save the offset for the exit swipe animation
|
||||
setRemoved(true);
|
||||
setOffsetBeforeRemove(offset.current);
|
||||
setHeights(h =>
|
||||
h.filter(height => height.notificationKey !== notification.key)
|
||||
);
|
||||
window.setTimeout(() => {
|
||||
if (!notification.key) {
|
||||
return;
|
||||
}
|
||||
removeNotification(notification.key);
|
||||
}, 200);
|
||||
}, [setHeights, notification.key, removeNotification, offset]);
|
||||
|
||||
useEffect(() => {
|
||||
if (timerIdRef.current) {
|
||||
clearTimeout(timerIdRef.current);
|
||||
}
|
||||
if (!expand) {
|
||||
timerIdRef.current = window.setTimeout(() => {
|
||||
onClickRemove();
|
||||
}, duration);
|
||||
}
|
||||
return () => {
|
||||
if (timerIdRef.current) {
|
||||
clearTimeout(timerIdRef.current);
|
||||
}
|
||||
};
|
||||
}, [duration, expand, onClickRemove]);
|
||||
|
||||
const onClickAction = useCallback(() => {
|
||||
if (notification.action) {
|
||||
notification.action().catch(err => {
|
||||
console.error(err);
|
||||
});
|
||||
}
|
||||
return void 0;
|
||||
}, [notification]);
|
||||
|
||||
useEffect(() => {
|
||||
const notificationNode = notificationRef.current;
|
||||
|
||||
if (notificationNode) {
|
||||
const height = notificationNode.getBoundingClientRect().height;
|
||||
|
||||
// Add toast height tot heights array after the toast is mounted
|
||||
setInitialHeight(height);
|
||||
setHeights(h => [{ notificationKey: notification.key, height }, ...h]);
|
||||
|
||||
return () =>
|
||||
setHeights(h =>
|
||||
h.filter(height => height.notificationKey !== notification.key)
|
||||
);
|
||||
}
|
||||
return;
|
||||
}, [notification.key, setHeights]);
|
||||
return (
|
||||
<Toast.Root
|
||||
className={clsx(styles.notificationStyle, {
|
||||
[styles.lightCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
|
||||
!isFront && !expand && notification.theme === 'light',
|
||||
[styles.darkCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
|
||||
!isFront && !expand && notification.theme === 'dark',
|
||||
[styles.defaultCollapseStyle[index === 1 ? 'secondary' : 'tertiary']]:
|
||||
!isFront && !expand && !notification.theme,
|
||||
})}
|
||||
duration={Infinity}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
role="status"
|
||||
tabIndex={0}
|
||||
ref={notificationRef}
|
||||
data-mounted={mounted}
|
||||
data-removed={removed}
|
||||
data-visible={isVisible}
|
||||
data-index={index}
|
||||
data-front={isFront}
|
||||
data-expanded={expand}
|
||||
data-testid="affine-notification"
|
||||
onMouseEnter={() => {
|
||||
setExpand(true);
|
||||
}}
|
||||
onMouseMove={() => {
|
||||
setExpand(true);
|
||||
}}
|
||||
onMouseLeave={() => {
|
||||
setExpand(false);
|
||||
}}
|
||||
onSwipeEnd={event => event.preventDefault()}
|
||||
onSwipeMove={event => event.preventDefault()}
|
||||
style={
|
||||
{
|
||||
'--index': index,
|
||||
'--toasts-before': index,
|
||||
'--z-index': notifications.length - index,
|
||||
'--offset': `${removed ? offsetBeforeRemove : offset.current}px`,
|
||||
'--initial-height': `${initialHeight}px`,
|
||||
userSelect: 'auto',
|
||||
} as React.CSSProperties
|
||||
}
|
||||
>
|
||||
<div
|
||||
className={clsx({
|
||||
[typeStyle]: notification.theme !== 'default',
|
||||
[styles.hasMediaStyle]: notification.multimedia,
|
||||
[styles.notificationContentStyle]: !notification.multimedia,
|
||||
})}
|
||||
>
|
||||
{notification.multimedia ? (
|
||||
<div className={styles.notificationMultimediaStyle}>
|
||||
{notification.multimedia}
|
||||
<IconButton className={styles.closeButtonWithMediaStyle}>
|
||||
<CloseIcon onClick={onClickRemove} />
|
||||
</IconButton>
|
||||
</div>
|
||||
) : null}
|
||||
<Toast.Title
|
||||
className={clsx(styles.notificationTitleStyle, {
|
||||
[styles.darkColorStyle]:
|
||||
notification.theme !== 'light' &&
|
||||
notification.theme !== 'default',
|
||||
})}
|
||||
>
|
||||
<div
|
||||
className={clsx(styles.notificationIconStyle, {
|
||||
[styles.darkColorStyle]:
|
||||
notification.theme !== 'light' &&
|
||||
notification.theme !== 'default',
|
||||
[styles.lightInfoIconStyle]: notification.theme === 'light',
|
||||
})}
|
||||
>
|
||||
{typeColorMap[notification.type]?.icon ?? (
|
||||
<InformationFillDuotoneIcon />
|
||||
)}
|
||||
</div>
|
||||
<div className={styles.notificationTitleContactStyle}>
|
||||
{notification.title}
|
||||
</div>
|
||||
{notification.action && (
|
||||
<div
|
||||
className={clsx(styles.undoButtonStyle, {
|
||||
[styles.darkColorStyle]:
|
||||
notification.theme !== 'light' &&
|
||||
notification.theme !== 'default',
|
||||
[styles.undoButtonWithMediaStyle]: notification.multimedia,
|
||||
})}
|
||||
onClick={onClickAction}
|
||||
>
|
||||
{notification.actionLabel ??
|
||||
t['com.affine.keyboardShortcuts.undo']()}
|
||||
</div>
|
||||
)}
|
||||
{notification.multimedia ? null : (
|
||||
<IconButton
|
||||
className={clsx(styles.closeButtonStyle, {
|
||||
[styles.closeButtonWithoutUndoStyle]: !notification.action,
|
||||
})}
|
||||
style={{
|
||||
color:
|
||||
notification.theme !== 'light' &&
|
||||
notification.theme !== 'default'
|
||||
? 'var(--affine-pure-white)'
|
||||
: 'var(--affine-text-primary-color)',
|
||||
}}
|
||||
>
|
||||
<CloseIcon onClick={onClickRemove} />
|
||||
</IconButton>
|
||||
)}
|
||||
</Toast.Title>
|
||||
<Toast.Description
|
||||
className={clsx(styles.messageStyle, {
|
||||
[styles.darkColorStyle]:
|
||||
notification.theme !== 'light' &&
|
||||
notification.theme !== 'default',
|
||||
})}
|
||||
>
|
||||
{notification.message}
|
||||
</Toast.Description>
|
||||
{notification.progressingBar && (
|
||||
<div className={styles.progressBarStyle}>
|
||||
<svg width="100%" height="4">
|
||||
<rect
|
||||
width="100%"
|
||||
height="4"
|
||||
fill="var(--affine-hover-color)"
|
||||
rx="2"
|
||||
ry="2"
|
||||
/>
|
||||
<rect
|
||||
width="0%"
|
||||
height="4"
|
||||
fill="var(--affine-primary-color)"
|
||||
rx="2"
|
||||
ry="2"
|
||||
>
|
||||
<animate
|
||||
key={animationKey}
|
||||
ref={animationRef}
|
||||
attributeName="width"
|
||||
from="0%"
|
||||
to="100%"
|
||||
dur={(progressDuration - 200) / 1000}
|
||||
fill="freeze"
|
||||
onAnimationEnd={resetAnimation}
|
||||
/>
|
||||
</rect>
|
||||
</svg>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Toast.Root>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated use `import { NotificationCenter } from '@affine/component'` instead
|
||||
*/
|
||||
export function NotificationCenter(): ReactNode {
|
||||
const notifications = useAtomValue(notificationsAtom);
|
||||
const [expand, setExpand] = useAtom(expandNotificationCenterAtom);
|
||||
|
||||
if (notifications.length === 0 && expand) {
|
||||
setExpand(false);
|
||||
}
|
||||
const [heights, setHeights] = useState<Height[]>([]);
|
||||
const listRef = useRef<HTMLOListElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// Ensure expanded is always false when no toasts are present / only one left
|
||||
if (notifications.length <= 1) {
|
||||
setExpand(false);
|
||||
}
|
||||
}, [notifications, setExpand]);
|
||||
|
||||
if (!notifications.length) return null;
|
||||
return (
|
||||
<Toast.Provider swipeDirection="right">
|
||||
{notifications.map((notification, index) =>
|
||||
notification.key ? (
|
||||
<NotificationCard
|
||||
notification={notification}
|
||||
index={index}
|
||||
key={notification.key}
|
||||
notifications={notifications}
|
||||
heights={heights}
|
||||
setHeights={setHeights}
|
||||
/>
|
||||
) : null
|
||||
)}
|
||||
<Toast.Viewport
|
||||
tabIndex={-1}
|
||||
ref={listRef}
|
||||
style={
|
||||
{
|
||||
'--front-toast-height': `${heights[0]?.height}px`,
|
||||
} as React.CSSProperties
|
||||
}
|
||||
className={styles.notificationCenterViewportStyle}
|
||||
/>
|
||||
</Toast.Provider>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user