Stack notifications based on notification type
This commit is contained in:
parent
2437272cce
commit
a7b2a1a98e
@ -27,9 +27,8 @@ export const ToastArea = ({ swipeDirection, children, className }: ToastAreaProp
|
||||
const $ToastArea = styled.aside`
|
||||
// Params
|
||||
--toasts-gap: 0.5rem;
|
||||
|
||||
|
||||
// Rules
|
||||
${layoutMixins.scrollArea}
|
||||
z-index: 1;
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
@ -87,6 +87,8 @@ export type NotificationDisplayData = {
|
||||
slotTitleLeft?: React.ReactNode;
|
||||
slotTitleRight?: React.ReactNode;
|
||||
|
||||
groupKey: string; // Grouping key toast notification stacking
|
||||
|
||||
// Overrides title/body for Notification in NotificationMenu
|
||||
renderCustomBody?: ({
|
||||
isToast,
|
||||
|
||||
@ -81,6 +81,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
|
||||
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
|
||||
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
toastSensitivity: 'foreground',
|
||||
groupKey: abacusNotificationType,
|
||||
renderCustomBody: ({ isToast, notification }) => (
|
||||
<TradeNotification
|
||||
isToast={isToast}
|
||||
@ -103,6 +104,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
|
||||
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
|
||||
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
toastSensitivity: 'foreground',
|
||||
groupKey: abacusNotificationType,
|
||||
renderCustomBody: ({ isToast, notification }) => (
|
||||
<BlockRewardNotification
|
||||
isToast={isToast}
|
||||
@ -125,6 +127,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
|
||||
body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
|
||||
toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
|
||||
toastSensitivity: 'foreground',
|
||||
groupKey: abacusNotificationType,
|
||||
},
|
||||
[abacusNotif.updateTimeInMilliseconds, abacusNotif.data]
|
||||
);
|
||||
@ -220,6 +223,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
|
||||
/>
|
||||
),
|
||||
toastSensitivity: 'foreground',
|
||||
groupKey: NotificationType.SquidTransfer,
|
||||
},
|
||||
[isFinished]
|
||||
);
|
||||
@ -265,6 +269,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
|
||||
},
|
||||
}),
|
||||
toastSensitivity: 'foreground',
|
||||
groupKey: NotificationType.ReleaseUpdates,
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
@ -1,7 +1,11 @@
|
||||
import { useMemo, useState } from 'react';
|
||||
import { useState } from 'react';
|
||||
import styled, { css } from 'styled-components';
|
||||
|
||||
import { NotificationStatus } from '@/constants/notifications';
|
||||
import {
|
||||
type Notification,
|
||||
type NotificationDisplayData,
|
||||
NotificationStatus,
|
||||
} from '@/constants/notifications';
|
||||
import { ButtonShape, ButtonSize } from '@/constants/buttons';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
import { useBreakpoints } from '@/hooks/useBreakpoints';
|
||||
@ -13,54 +17,31 @@ import { Toast } from '@/components/Toast';
|
||||
import { ToastArea } from '@/components/ToastArea';
|
||||
import { ToggleButton } from '@/components/ToggleButton';
|
||||
|
||||
type ElementProps = {
|
||||
notifications: {
|
||||
notification: Notification<any, any>;
|
||||
key: string;
|
||||
displayData: NotificationDisplayData;
|
||||
}[];
|
||||
};
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MAX_TOASTS = 10;
|
||||
|
||||
export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
const [shouldStackNotifications, setshouldStackNotifications] = useState(false);
|
||||
|
||||
const {
|
||||
notifications,
|
||||
getKey,
|
||||
getDisplayData,
|
||||
markUnseen,
|
||||
markSeen,
|
||||
isMenuOpen,
|
||||
onNotificationAction,
|
||||
} = useNotifications();
|
||||
export const NotificationStack = ({ notifications, className }: ElementProps & StyleProps) => {
|
||||
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
|
||||
|
||||
const { markUnseen, markSeen, onNotificationAction } = useNotifications();
|
||||
const { isMobile } = useBreakpoints();
|
||||
const notificationMap = useMemo(() => {
|
||||
return (
|
||||
Object.values(notifications)
|
||||
// Sort by time of first trigger
|
||||
.sort(
|
||||
(n1, n2) =>
|
||||
n1.timestamps[NotificationStatus.Triggered]! -
|
||||
n2.timestamps[NotificationStatus.Triggered]!
|
||||
)
|
||||
.map((notification) => ({
|
||||
notification,
|
||||
key: getKey(notification),
|
||||
displayData: getDisplayData(notification),
|
||||
}))
|
||||
.filter(
|
||||
({ displayData, notification }) =>
|
||||
displayData && notification.status < NotificationStatus.Unseen
|
||||
)
|
||||
.slice(-MAX_TOASTS)
|
||||
);
|
||||
}, [notifications, getKey, getDisplayData]);
|
||||
|
||||
if (isMenuOpen) return null;
|
||||
|
||||
const hasMultipleToasts = notificationMap.length > 1;
|
||||
const hasMultipleToasts = notifications.length > 1;
|
||||
|
||||
return (
|
||||
<StyledToastArea swipeDirection={isMobile ? 'up' : 'right'} className={className}>
|
||||
<StyledToastArea
|
||||
swipeDirection={isMobile ? 'up' : 'right'}
|
||||
className={className}
|
||||
size={notifications.length}
|
||||
>
|
||||
{hasMultipleToasts && (
|
||||
<StyledToggleButton
|
||||
shape={ButtonShape.Pill}
|
||||
@ -72,12 +53,12 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
</StyledToggleButton>
|
||||
)}
|
||||
|
||||
{notificationMap.map(({ notification, key, displayData }, idx) => (
|
||||
{notifications.map(({ notification, key, displayData }, idx) => (
|
||||
<StyledToast
|
||||
key={key}
|
||||
isStacked={shouldStackNotifications}
|
||||
isStacked={idx > 0 && shouldStackNotifications}
|
||||
isOpen={notification.status < NotificationStatus.Unseen}
|
||||
layer={notificationMap.length - 1 - idx}
|
||||
layer={idx}
|
||||
notification={notification}
|
||||
slotIcon={displayData.icon}
|
||||
slotTitle={displayData.title}
|
||||
@ -110,19 +91,13 @@ export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
);
|
||||
};
|
||||
|
||||
const StyledToastArea = styled(ToastArea)`
|
||||
position: absolute;
|
||||
width: min(17.5rem, 100%);
|
||||
inset: 0 0 0 auto;
|
||||
|
||||
padding: 0.75rem 0.75rem 0.75rem 0;
|
||||
|
||||
mask-image: linear-gradient(to left, transparent, white 0.5rem);
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
const StyledToastArea = styled(ToastArea)<{ size: number }>`
|
||||
position: relative;
|
||||
${({ size }) =>
|
||||
size &&
|
||||
css`
|
||||
padding-bottom: calc(${size - 1} * 2px);
|
||||
`}
|
||||
`;
|
||||
|
||||
const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
|
||||
@ -133,7 +108,7 @@ const StyledToast = styled(Toast)<{ isStacked?: boolean; layer: number }>`
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: calc(${layer} * 2px);
|
||||
right: calc(${layer} * -2px);
|
||||
right: 0;
|
||||
`
|
||||
: css`
|
||||
margin-bottom: 0.75rem;
|
||||
@ -145,7 +120,7 @@ const StyledToggleButton = styled(ToggleButton)<{ isPressed: boolean }>`
|
||||
pointer-events: auto;
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 4px;
|
||||
top: -0.5rem;
|
||||
left: calc(50% - 0.75rem);
|
||||
--button-width: 2rem;
|
||||
--button-height: 1rem;
|
||||
71
src/layout/NotificationsToastArea/index.tsx
Normal file
71
src/layout/NotificationsToastArea/index.tsx
Normal file
@ -0,0 +1,71 @@
|
||||
import { useMemo } from 'react';
|
||||
import { groupBy } from 'lodash';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import { breakpoints } from '@/styles';
|
||||
import { layoutMixins } from '@/styles/layoutMixins';
|
||||
|
||||
import { NotificationStatus } from '@/constants/notifications';
|
||||
import { useNotifications } from '@/hooks/useNotifications';
|
||||
|
||||
import { NotificationStack } from './NotifcationStack';
|
||||
|
||||
type StyleProps = {
|
||||
className?: string;
|
||||
};
|
||||
|
||||
const MAX_TOASTS = 10;
|
||||
|
||||
export const NotificationsToastArea = ({ className }: StyleProps) => {
|
||||
const { notifications, getKey, getDisplayData, isMenuOpen } = useNotifications();
|
||||
|
||||
const notificationMapByType = useMemo(() => {
|
||||
const notificationMap = Object.values(notifications)
|
||||
// Sort by time of first trigger
|
||||
.sort(
|
||||
(n1, n2) =>
|
||||
n1.timestamps[NotificationStatus.Triggered]! -
|
||||
n2.timestamps[NotificationStatus.Triggered]!
|
||||
)
|
||||
.map((notification) => ({
|
||||
notification,
|
||||
key: getKey(notification),
|
||||
displayData: getDisplayData(notification),
|
||||
}))
|
||||
.filter(
|
||||
({ displayData, notification }) =>
|
||||
displayData && notification.status < NotificationStatus.Unseen
|
||||
)
|
||||
.slice(-MAX_TOASTS);
|
||||
return groupBy(notificationMap, (notification) => notification.displayData?.groupKey);
|
||||
}, [notifications, getKey, getDisplayData]);
|
||||
|
||||
if (isMenuOpen) return null;
|
||||
|
||||
return (
|
||||
<StyledToastArea className={className}>
|
||||
{Object.keys(notificationMapByType).map((groupKey) => (
|
||||
<NotificationStack notifications={notificationMapByType[groupKey]} key={groupKey} />
|
||||
))}
|
||||
</StyledToastArea>
|
||||
);
|
||||
};
|
||||
|
||||
const StyledToastArea = styled.div`
|
||||
${layoutMixins.scrollArea}
|
||||
|
||||
position: absolute;
|
||||
width: min(17.5rem, 100%);
|
||||
inset: 0 0 0 auto;
|
||||
|
||||
padding: 0.75rem 0.75rem 0.75rem 0;
|
||||
|
||||
mask-image: linear-gradient(to left, transparent, white 0.5rem);
|
||||
|
||||
pointer-events: none;
|
||||
|
||||
@media ${breakpoints.mobile} {
|
||||
width: 100%;
|
||||
position: fixed;
|
||||
}
|
||||
`;
|
||||
Loading…
Reference in New Issue
Block a user