diff --git a/public/dots-background-2.svg b/public/dots-background-2.svg
new file mode 100644
index 0000000..82c1f22
--- /dev/null
+++ b/public/dots-background-2.svg
@@ -0,0 +1,230 @@
+
diff --git a/src/components/Icon.tsx b/src/components/Icon.tsx
index 615df90..f5e44e1 100644
--- a/src/components/Icon.tsx
+++ b/src/components/Icon.tsx
@@ -59,6 +59,7 @@ import {
PriceChartIcon,
PrivacyIcon,
QrIcon,
+ RewardStarIcon,
SearchIcon,
SendIcon,
ShareIcon,
@@ -134,6 +135,7 @@ export enum IconName {
PriceChart = 'PriceChart',
Privacy = 'Privacy',
Qr = 'Qr',
+ RewardStar = 'RewardStar',
Search = 'Search',
Send = 'Send',
Share = 'Share',
@@ -208,6 +210,7 @@ const icons = {
[IconName.PriceChart]: PriceChartIcon,
[IconName.Privacy]: PrivacyIcon,
[IconName.Qr]: QrIcon,
+ [IconName.RewardStar]: RewardStarIcon,
[IconName.Search]: SearchIcon,
[IconName.Send]: SendIcon,
[IconName.Share]: ShareIcon,
diff --git a/src/components/ToastArea.tsx b/src/components/ToastArea.tsx
index 0457db9..5aa9e98 100644
--- a/src/components/ToastArea.tsx
+++ b/src/components/ToastArea.tsx
@@ -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;
diff --git a/src/constants/notifications.ts b/src/constants/notifications.ts
index a455fd1..d6728d1 100644
--- a/src/constants/notifications.ts
+++ b/src/constants/notifications.ts
@@ -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,
diff --git a/src/hooks/useNotificationTypes.tsx b/src/hooks/useNotificationTypes.tsx
index 870e03e..b7be4f3 100644
--- a/src/hooks/useNotificationTypes.tsx
+++ b/src/hooks/useNotificationTypes.tsx
@@ -28,6 +28,7 @@ import { useLocalNotifications } from '@/hooks/useLocalNotifications';
import { AssetIcon } from '@/components/AssetIcon';
import { Icon, IconName } from '@/components/Icon';
+import { BlockRewardNotification } from '@/views/notifications/BlockRewardNotification';
import { TradeNotification } from '@/views/notifications/TradeNotification';
import { TransferStatusNotification } from '@/views/notifications/TransferStatusNotification';
@@ -80,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 }) => (
,
+ title: stringGetter({ key: abacusNotif.title }),
+ body: abacusNotif.text ? stringGetter({ key: abacusNotif.text, params }) : '',
+ toastDuration: DEFAULT_TOAST_AUTO_CLOSE_MS,
+ toastSensitivity: 'foreground',
+ groupKey: abacusNotificationType,
+ renderCustomBody: ({ isToast, notification }) => (
+
+ ),
+ },
+ [abacusNotif.updateTimeInMilliseconds, abacusNotif.data],
+ true
+ );
+ break;
+ }
default:
trigger(
abacusNotif.id,
@@ -102,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]
);
@@ -197,6 +223,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
/>
),
toastSensitivity: 'foreground',
+ groupKey: NotificationType.SquidTransfer,
},
[isFinished]
);
@@ -242,6 +269,7 @@ export const notificationTypes: NotificationTypeConfig[] = [
},
}),
toastSensitivity: 'foreground',
+ groupKey: NotificationType.ReleaseUpdates,
},
[]
);
diff --git a/src/icons/index.ts b/src/icons/index.ts
index 8f22631..0dfb854 100644
--- a/src/icons/index.ts
+++ b/src/icons/index.ts
@@ -50,6 +50,7 @@ export { default as PriceChartIcon } from './price-chart.svg';
export { default as PrivacyIcon } from './privacy.svg';
export { default as ProfileIcon } from './profile.svg';
export { default as QrIcon } from './qr.svg';
+export { default as RewardStarIcon } from './reward-star.svg';
export { default as SearchIcon } from './search.svg';
export { default as SendIcon } from './send.svg';
export { default as ShareIcon } from './share.svg';
diff --git a/src/icons/reward-star.svg b/src/icons/reward-star.svg
new file mode 100644
index 0000000..6aab7d7
--- /dev/null
+++ b/src/icons/reward-star.svg
@@ -0,0 +1,9 @@
+
diff --git a/src/layout/NotificationsToastArea.tsx b/src/layout/NotificationsToastArea/NotifcationStack.tsx
similarity index 66%
rename from src/layout/NotificationsToastArea.tsx
rename to src/layout/NotificationsToastArea/NotifcationStack.tsx
index db4e37e..a2f16dd 100644
--- a/src/layout/NotificationsToastArea.tsx
+++ b/src/layout/NotificationsToastArea/NotifcationStack.tsx
@@ -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;
+ key: string;
+ displayData: NotificationDisplayData;
+ }[];
+};
+
type StyleProps = {
className?: string;
};
-const MAX_TOASTS = 10;
-
-export const NotificationsToastArea = ({ className }: StyleProps) => {
+export const NotificationStack = ({ notifications, className }: ElementProps & StyleProps) => {
const [shouldStackNotifications, setshouldStackNotifications] = useState(true);
- const {
- notifications,
- getKey,
- getDisplayData,
- markUnseen,
- markSeen,
- isMenuOpen,
- onNotificationAction,
- } = useNotifications();
-
+ 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 (
-
+
{hasMultipleToasts && (
{
)}
- {notificationMap.map(({ notification, key, displayData }, idx) => (
+ {notifications.map(({ notification, key, displayData }, 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;
diff --git a/src/layout/NotificationsToastArea/index.tsx b/src/layout/NotificationsToastArea/index.tsx
new file mode 100644
index 0000000..fea6004
--- /dev/null
+++ b/src/layout/NotificationsToastArea/index.tsx
@@ -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 (
+
+ {Object.keys(notificationMapByType).map((groupKey) => (
+
+ ))}
+
+ );
+};
+
+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;
+ }
+`;
diff --git a/src/views/notifications/BlockRewardNotification/index.tsx b/src/views/notifications/BlockRewardNotification/index.tsx
new file mode 100644
index 0000000..200522b
--- /dev/null
+++ b/src/views/notifications/BlockRewardNotification/index.tsx
@@ -0,0 +1,78 @@
+import styled, { type AnyStyledComponent } from 'styled-components';
+
+import { STRING_KEYS } from '@/constants/localization';
+import { useStringGetter } from '@/hooks';
+
+import { Details } from '@/components/Details';
+import { Notification, NotificationProps } from '@/components/Notification';
+import { Output, OutputType } from '@/components/Output';
+import { Icon, IconName } from '@/components/Icon';
+
+type ElementProps = {
+ data: {
+ BLOCK_REWARD_AMOUNT: string;
+ BLOCK_REWARD_HEIGHT: string;
+ BLOCK_REWARD_TIME_MILLISECONDS: string;
+ TOKEN_NAME: string;
+ };
+};
+
+export type BlockRewardNotificationProps = NotificationProps & ElementProps;
+
+export const BlockRewardNotification = ({
+ isToast,
+ data,
+ notification,
+}: BlockRewardNotificationProps) => {
+ const stringGetter = useStringGetter();
+ const { BLOCK_REWARD_AMOUNT, TOKEN_NAME } = data;
+
+ return (
+ }
+ slotTitle={stringGetter({ key: STRING_KEYS.TRADING_REWARD_RECEIVED })}
+ slotCustomContent={
+
+ ),
+ },
+ ]}
+ />
+ }
+ />
+ );
+};
+
+const Styled: Record = {};
+
+Styled.Details = styled(Details)`
+ --details-item-height: 1.5rem;
+
+ dd {
+ color: var(--color-text-2);
+ }
+`;
+
+Styled.Notification = styled(Notification)`
+ background-image: url('/dots-background-2.svg');
+ background-size: cover;
+`;
+
+Styled.Output = styled(Output)`
+ &:before {
+ content: '+';
+ color: var(--color-positive);
+ margin-right: 0.5ch;
+ }
+`;