Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(Notifications): Added archived state, NotificationsPopupWrapper component and enhanced the stories for notifications #83

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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(() => {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it is a little bit weird that notification manages it's visibility state by itself

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's because we need to animate the removing effect (archive)
I thought it would be more convenient if we manage it in the NotificationWrapper component

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) {
Ruminat marked this conversation as resolved.
Show resolved Hide resolved
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
Loading