Skip to content

Commit

Permalink
feat(Notifications): Added archived state, `NotificationsPopupWrapper…
Browse files Browse the repository at this point in the history
…` component and enhanced the stories for notifications (#83)
  • Loading branch information
Ruminat authored Aug 9, 2023
1 parent bd4710a commit 877803a
Show file tree
Hide file tree
Showing 20 changed files with 540 additions and 136 deletions.
11 changes: 10 additions & 1 deletion src/components/Notification/Notification.scss
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ $notificationSourceIconSize: 36px;
border-left: 4px solid var(--g-color-line-danger);
}

&_active {
cursor: pointer;
}

&__swipe-wrap {
width: 100%;
overflow: hidden;
Expand Down Expand Up @@ -151,7 +155,7 @@ $notificationSourceIconSize: 36px;
background: var(--g-color-base-misc-light);
}
&__swipe-action_theme_base &__swipe-action-icon {
background: var(--g-color-text-misc-light);
background: var(--g-color-base-misc-heavy-hover);
}
&__swipe-action_theme_base &__swipe-action-text {
color: var(--g-color-text-misc-heavy);
Expand Down Expand Up @@ -186,4 +190,9 @@ $notificationSourceIconSize: 36px;
&__swipe-action-text {
font-size: 16px;
}

&__source-icon {
width: 36px;
height: 36px;
}
}
2 changes: 1 addition & 1 deletion src/components/Notification/Notification.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ export const Notification = React.memo(function Notification(props: Props) {
const {notification} = props;
const {title, content, formattedDate, source, unread, theme} = notification;

const modifiers: CnMods = {unread, theme, mobile};
const modifiers: CnMods = {unread, theme, mobile, active: Boolean(notification.onClick)};

return (
<div
Expand Down
6 changes: 3 additions & 3 deletions src/components/Notification/NotificationWithSwipe.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, {useState} from 'react';
import React from 'react';

import clamp from 'lodash/clamp';
import TinyGesture from 'tinygesture';
Expand Down Expand Up @@ -29,7 +29,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p
const leftAction = swipeActions && 'left' in swipeActions ? swipeActions.left : undefined;
const rightAction = swipeActions && 'right' in swipeActions ? swipeActions.right : undefined;

const [position, setPosition] = useState<'left-action' | 'notification' | 'right-action'>(
const [position, setPosition] = React.useState<'left-action' | 'notification' | 'right-action'>(
'notification',
);

Expand Down Expand Up @@ -122,7 +122,7 @@ export const NotificationWithSwipe = React.memo(function NotificationWithSwipe(p
return () => {
gesture.destroy();
};
}, [position]);
}, [leftAction, position, rightAction, swipeThreshold]);

return (
<div className={b('swipe-wrap')}>
Expand Down
1 change: 1 addition & 0 deletions src/components/Notification/definitions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ export type NotificationProps = {
title?: React.ReactNode;
formattedDate?: React.ReactNode;
unread?: boolean;
archived?: boolean;
source?: NotificationSourceProps;
theme?: NotificationTheme;
className?: string;
Expand Down
91 changes: 91 additions & 0 deletions src/components/Notifications/NotificationWrapper.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import React from 'react';

import {useMobile} from '@gravity-ui/uikit';

import {Notification} from '../Notification';
import {NotificationWithSwipe} from '../Notification/NotificationWithSwipe';
import {NotificationProps} from '../Notification/definitions';
import {block} from '../utils/cn';

import './Notifications.scss';

const b = block('notifications');

export const NotificationWrapper = (props: {
notification: NotificationProps;
swipeThreshold?: number;
}) => {
const ref = React.useRef<HTMLDivElement>(null);

const {notification, swipeThreshold} = props;
const [mobile] = useMobile();
const [wrapperMaxHeight, setWrapperMaxHeight] = React.useState<number | undefined>(undefined);
const [isRemoved, setIsRemoved] = React.useState(false);

React.useEffect(() => {
if (!ref.current) {
if (!notification.archived && isRemoved) {
setIsRemoved(false);
}
return () => {};
}

if (notification.archived) {
const listener = (event: TransitionEvent) => {
if (event.propertyName === 'max-height') {
setIsRemoved(true);
ref.current?.removeEventListener('transitionend', listener);
}
};

ref.current.addEventListener('transitionend', listener);

ref.current.style.transition = 'max-height 0.3s';
setWrapperMaxHeight(0);

return () => {
ref.current?.removeEventListener('transitionend', listener);
};
} else {
setIsRemoved(false);

setTimeout(() => {
if (!ref.current) return;

ref.current.style.transition = 'none';
ref.current.style.maxHeight = 'none';

const maxHeight = ref.current?.getBoundingClientRect().height ?? 0;
setWrapperMaxHeight(maxHeight);
}, 0);

return () => {};
}
}, [ref, notification.archived, isRemoved]);

const style = wrapperMaxHeight === undefined ? {} : {maxHeight: `${wrapperMaxHeight}px`};

if (isRemoved) {
return null;
}

return (
<div
className={b('notification-wrapper', {
archived: notification.archived,
unread: notification.unread,
})}
ref={ref}
style={style}
>
{mobile && notification.swipeActions ? (
<NotificationWithSwipe
notification={notification}
swipeThreshold={swipeThreshold}
/>
) : (
<Notification notification={notification} />
)}
</div>
);
};
10 changes: 7 additions & 3 deletions src/components/Notifications/Notifications.scss
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ $block: '.#{variables.$ns}notifications';
display: flex;
flex-direction: column;
color: var(--g-color-text-primary);
background: var(--g-color-base-background);
height: 100%;
width: 100%;

&__head {
display: flex;
Expand Down Expand Up @@ -73,11 +73,15 @@ $block: '.#{variables.$ns}notifications';
&__notification-wrapper:hover:not(:first-child)::before,
&__notification-wrapper:hover + &__notification-wrapper::before,
// .unread
&__notification-wrapper.unread:not(:first-child)::before,
&__notification-wrapper.unread + &__notification-wrapper::before {
&__notification-wrapper_unread:not(:first-child)::before,
&__notification-wrapper_unread + &__notification-wrapper::before {
content: '';
display: block;
border-top: 1px solid transparent;
margin: 0 12px;
}

&__notification-wrapper {
overflow-y: hidden;
}
}
42 changes: 32 additions & 10 deletions src/components/Notifications/Notifications.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
import React from 'react';

import {InfiniteScroll} from '../InfiniteScroll';
import {block} from '../utils/cn';

import {NotificationsEmptyState} from './NotificationsEmptyState';
import {NotificationsErrorState} from './NotificationsErrorState';
import {NotificationsList} from './NotificationsList';
import {NotificationsLoadingState} from './NotificationsLoadingState';
import {NotificationsProps} from './definitions';
import i18n from './i18n';

Expand All @@ -12,22 +15,41 @@ import './Notifications.scss';
const b = block('notifications');

export const Notifications = React.memo(function Notifications(props: NotificationsProps) {
let content: JSX.Element;

const visibleNotificationsCount = props.notifications.filter((n) => !n.archived).length;
const hasUnloadedNotifications =
!props.areAllNotificationsLoaded && props.onLoadMoreNotifications;

if (props.isLoading) {
content = <NotificationsLoadingState />;
} else if (visibleNotificationsCount > 0 || hasUnloadedNotifications) {
content = (
<InfiniteScroll
onActivate={props.onLoadMoreNotifications ?? noop}
disabled={props.areAllNotificationsLoaded ?? true}
>
<NotificationsList
notifications={props.notifications}
swipeThreshold={props.swipeThreshold}
/>
</InfiniteScroll>
);
} else if (props.errorContent) {
content = <NotificationsErrorState image={props.errorImage} content={props.errorContent} />;
} else {
content = <NotificationsEmptyState image={props.emptyImage} content={props.emptyContent} />;
}

return (
<div className={b()}>
<div className={b('head')}>
<div className={b('head-title')}>{props.title || i18n('title')}</div>
{props.actions ? <div className={b('actions')}>{props.actions}</div> : null}
</div>
<div className={b('body')}>
{props.notifications.length > 0 ? (
<NotificationsList
notifications={props.notifications}
swipeThreshold={props.swipeThreshold}
/>
) : (
<NotificationsEmptyState content={props.emptyMessage} />
)}
</div>
<div className={b('body')}>{content}</div>
</div>
);
});

async function noop() {}
12 changes: 9 additions & 3 deletions src/components/Notifications/NotificationsEmptyState.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import {Icon, useTheme} from '@gravity-ui/uikit';

import {block} from '../utils/cn';

import i18n from './i18n/index';

import './Notifications.scss';

const b = block('notifications');
Expand All @@ -12,16 +14,20 @@ const nothingFoundSvg = `<svg xmlns="http://www.w3.org/2000/svg" fill="none"><pa

const nothingFoundDarkSvg = `<svg xmlns="http://www.w3.org/2000/svg" width="172" height="172" fill="none"><path fill="#E3EBF2" fill-opacity=".9" d="M34.4 46.365c0-8.26 6.697-14.956 14.957-14.956h58.33c8.261 0 14.957 6.696 14.957 14.956v58.331c0 8.26-6.696 14.956-14.957 14.956h-58.33c-8.26 0-14.957-6.696-14.957-14.956v-58.33Z"/><path stroke="#fff" stroke-linejoin="bevel" stroke-width="1.496" d="M105.324 59.991c-13.377 5.885-26.954 2.761-33.69-2.408 14.613 47.822 6.297 71.213-6.237 72.455-19.19 1.902-25.334-40.183-14.268-44.09 11.066-3.908 20.42 34.102-4.389 69.024a94.306 94.306 0 0 1-3.861 5.063"/><g filter="url(#a)"><path fill="#5282FF" fill-opacity=".9" d="M88.992 50.104a8.974 8.974 0 0 1 8.974-8.974h26.922a8.974 8.974 0 0 1 8.974 8.974v26.922A8.974 8.974 0 0 1 124.888 86H97.966a8.974 8.974 0 0 1-8.974-8.974V50.104Z"/></g><g filter="url(#b)"><path fill="#fff" fill-opacity=".8" fill-rule="evenodd" d="M99.91 58.081a2.742 2.742 0 1 0 0-5.484 2.742 2.742 0 0 0 0 5.484Zm6.889-4.25a1.508 1.508 0 1 0 0 3.016h17.481a1.508 1.508 0 1 0 0-3.016h-17.481Zm0 16.452a1.508 1.508 0 1 0 0 3.016h17.481a1.508 1.508 0 1 0 0-3.016h-17.481Zm-1.508-6.718c0-.833.675-1.508 1.508-1.508h17.481a1.508 1.508 0 1 1 0 3.016h-17.481a1.508 1.508 0 0 1-1.508-1.508Zm-2.639 0a2.742 2.742 0 1 1-5.484 0 2.742 2.742 0 0 1 5.484 0ZM99.91 74.533a2.742 2.742 0 1 0 0-5.484 2.742 2.742 0 0 0 0 5.484Z" clip-rule="evenodd"/></g><path fill="#fff" d="m125.404 109.343 7.31-1.755a1.496 1.496 0 0 0 1.147-1.454v-.062c0-.606-.367-1.153-.928-1.383L113.9 96.871a1.588 1.588 0 0 0-2.072 2.073l7.817 19.033c.23.561.777.927 1.383.927h.062c.692 0 1.293-.474 1.454-1.146l1.755-7.31a1.494 1.494 0 0 1 1.105-1.105Z"/><defs><filter id="a" width="50.852" height="50.852" x="86.001" y="38.139" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feGaussianBlur in="BackgroundImageFix" stdDeviation="1.496"/><feComposite in2="SourceAlpha" operator="in" result="effect1_backgroundBlur_157_23037"/><feBlend in="SourceGraphic" in2="effect1_backgroundBlur_157_23037" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation="1.122"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.8 0"/><feBlend in2="shape" result="effect2_innerShadow_157_23037"/></filter><filter id="b" width="28.619" height="21.936" x="97.168" y="52.597" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feColorMatrix in="SourceAlpha" result="hardAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/><feOffset/><feGaussianBlur stdDeviation=".748"/><feComposite in2="hardAlpha" k2="-1" k3="1" operator="arithmetic"/><feColorMatrix values="0 0 0 0 1 0 0 0 0 1 0 0 0 0 1 0 0 0 0.8 0"/><feBlend in2="shape" result="effect1_innerShadow_157_23037"/></filter></defs></svg>`;

type Props = {content: React.ReactNode};
type Props = {image?: React.ReactNode; content: React.ReactNode};

export const NotificationsEmptyState = React.memo(function NotificationsEmptyState(props: Props) {
const theme = useTheme();

return (
<div className={b('empty')}>
<Icon data={theme === 'light' ? nothingFoundSvg : nothingFoundDarkSvg} size={172} />
{props.image ? (
props.image
) : (
<Icon data={theme === 'light' ? nothingFoundSvg : nothingFoundDarkSvg} size={172} />
)}
<div className={b('empty-message')}>
<div className={b('empty-title')}>No notifications</div>
<div className={b('empty-title')}>{i18n('no-notifications')}</div>
{props.content ? (
<div className={b('empty-message-content')}>{props.content}</div>
) : null}
Expand Down
Loading

0 comments on commit 877803a

Please sign in to comment.