Skip to content

Commit

Permalink
feat: added no notifition yet message bar (#493)
Browse files Browse the repository at this point in the history
* feat: added no notifition yet message bar

* refactor: fixed reviewed issues

* refactor: fixed review issues

* refactor: refactor code for ref

* test: added test cases for no notification section

* refactor: remove style and added classes

* refactor: added useContext instead of props drilling

* refactor: memoize notification refs

---------

Co-authored-by: SundasNoreen <sundas.noreen@arbisoft.com>
  • Loading branch information
sundasnoreen12 and SundasNoreen committed Oct 20, 2023
1 parent 3621628 commit 326c522
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 35 deletions.
41 changes: 41 additions & 0 deletions src/Notifications/NotificationEmptySection.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import React, { useContext } from 'react';
import { useIntl } from '@edx/frontend-platform/i18n';
import { Icon, IconButton } from '@edx/paragon';
import { NotificationsNone } from '@edx/paragon/icons';
import NotificationContext from './context';
import messages from './messages';

const EmptyNotifications = () => {
const intl = useIntl();
const { popoverHeaderRef, notificationRef } = useContext(NotificationContext);

return (
<div
className="d-flex flex-column justify-content-center align-items-center"
data-testid="notifications-list-complete"
style={{ height: `${notificationRef.current.clientHeight - popoverHeaderRef.current.clientHeight}px` }}
>
<IconButton
isActive
alt={intl.formatMessage(messages.notificationBellIconAltMessage)}
src={NotificationsNone}
iconAs={Icon}
variant="light"
id="bell-icon"
iconClassNames="text-primary-500"
className="ml-4 mr-1 notification-button notification-lg-bell-icon"
data-testid="notification-bell-icon"
/>
<div className="mx-auto mt-3.5 mb-3 font-size-22 notification-end-title line-height-24">
{intl.formatMessage(messages.noNotificationsYetMessage)}
</div>
<div className="d-flex flex-row mx-auto text-gray-500">
<span className="font-size-14 line-height-normal">
{intl.formatMessage(messages.noNotificationHelpMessage)}
</span>
</div>
</div>
);
};

export default React.memo(EmptyNotifications);
23 changes: 20 additions & 3 deletions src/Notifications/NotificationSections.jsx
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import React, { useCallback, useMemo } from 'react';
import React, { useCallback, useMemo, useContext } from 'react';
import { Button, Icon, Spinner } from '@edx/paragon';
import { AutoAwesome, CheckCircleLightOutline } from '@edx/paragon/icons';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
import isEmpty from 'lodash/isEmpty';
import classNames from 'classnames';
import messages from './messages';
import NotificationRowItem from './NotificationRowItem';
import NotificationEmptySection from './NotificationEmptySection';
import { markAllNotificationsAsRead, fetchNotificationList } from './data/thunks';
import {
selectExpiryDays, selectNotificationsByIds, selectPaginationData,
selectSelectedAppName, selectNotificationListStatus, selectNotificationTabs,
} from './data/selectors';
import { splitNotificationsByTime } from './utils';
import { RequestStatus } from './data/slice';
import NotificationContext from './context';

const NotificationSections = () => {
const intl = useIntl();
Expand All @@ -23,6 +26,7 @@ const NotificationSections = () => {
const { hasMorePages, currentPage } = useSelector(selectPaginationData);
const notificationTabs = useSelector(selectNotificationTabs);
const expiryDays = useSelector(selectExpiryDays);
const { popoverHeaderRef, notificationRef } = useContext(NotificationContext);
const { today = [], earlier = [] } = useMemo(
() => splitNotificationsByTime(notifications),
[notifications],
Expand Down Expand Up @@ -73,8 +77,19 @@ const NotificationSections = () => {
);
};

const shouldRenderEmptyNotifications = notifications.length === 0
&& notificationRequestStatus === RequestStatus.SUCCESSFUL
&& notificationRef?.current
&& popoverHeaderRef?.current;

return (
<div className={`${notificationTabs.length > 1 && 'mt-4'} px-4 pb-3.5`} data-testid="notification-tray-section">
<div
className={classNames('px-4', {
'mt-4': notificationTabs.length > 1,
'pb-3.5': notifications.length > 0,
})}
data-testid="notification-tray-section"
>
{renderNotificationSection('today', today)}
{renderNotificationSection('earlier', earlier)}
{(hasMorePages === undefined || hasMorePages) && notificationRequestStatus === RequestStatus.IN_PROGRESS ? (
Expand All @@ -93,7 +108,7 @@ const NotificationSections = () => {
)
)}
{
!hasMorePages && notificationRequestStatus === RequestStatus.SUCCESSFUL && (
notifications.length > 0 && !hasMorePages && notificationRequestStatus === RequestStatus.SUCCESSFUL && (
<div
className="d-flex flex-column my-5"
data-testid="notifications-list-complete"
Expand All @@ -111,6 +126,8 @@ const NotificationSections = () => {
</div>
)
}

{shouldRenderEmptyNotifications && <NotificationEmptySection />}
</div>
);
};
Expand Down
5 changes: 5 additions & 0 deletions src/Notifications/context.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React from 'react';

const NotificationContext = React.createContext({ popoverHeaderRef: null, notificationRef: null });

export default NotificationContext;
59 changes: 35 additions & 24 deletions src/Notifications/index.jsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/* eslint-disable react-hooks/exhaustive-deps */
import React, {
useCallback, useEffect, useRef, useState,
useCallback, useEffect, useRef, useState, useMemo,
} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { useIntl } from '@edx/frontend-platform/i18n';
Expand All @@ -17,11 +17,13 @@ import { useIsOnLargeScreen, useIsOnMediumScreen } from './data/hook';
import NotificationTabs from './NotificationTabs';
import messages from './messages';
import NotificationTour from './tours/NotificationTour';
import NotificationContext from './context';

const Notifications = () => {
const intl = useIntl();
const dispatch = useDispatch();
const popoverRef = useRef(null);
const headerRef = useRef(null);
const buttonRef = useRef(null);
const [enableNotificationTray, setEnableNotificationTray] = useState(false);
const [isHeaderVisible, setIsHeaderVisible] = useState(true);
Expand Down Expand Up @@ -199,6 +201,11 @@ const Notifications = () => {
window.usabilla_live('click');
}, []);

const notificationRefs = useMemo(
() => ({ popoverHeaderRef: headerRef, notificationRef: popoverRef }),
[headerRef, popoverRef],
);

return (
<>
<OverlayTrigger
Expand All @@ -210,38 +217,42 @@ const Notifications = () => {
overlay={(
<Popover
id="notificationTray"
style={{ height: isHeaderVisible ? '91vh' : '100vh' }}
data-testid="notification-tray"
className={classNames('overflow-auto rounded-0 border-0 position-fixed', {
'w-100': !isOnMediumScreen && !isOnLargeScreen,
'medium-screen': isOnMediumScreen,
'large-screen': isOnLargeScreen,
'popover-margin-top': !isHeaderVisible,
'popover-margin-top height-100vh': !isHeaderVisible,
'height-91vh ': isHeaderVisible,
})}
>
<div ref={popoverRef}>
<Popover.Title
as="h2"
className={`d-flex justify-content-between p-4 m-0 border-0 text-primary-500 zIndex-2 font-size-18
<div ref={popoverRef} className="height-inherit">
<div ref={headerRef}>
<Popover.Title
as="h2"
className={`d-flex justify-content-between p-4 m-0 border-0 text-primary-500 zIndex-2 font-size-18
line-height-24 bg-white position-sticky`}
>
{intl.formatMessage(messages.notificationTitle)}
<Hyperlink
destination={`${getConfig().ACCOUNT_SETTINGS_URL}/notifications`}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon
src={Settings}
className="icon-size-20 text-primary-500"
data-testid="setting-icon"
screenReaderText="preferences settings icon"
/>
</Hyperlink>
</Popover.Title>
{intl.formatMessage(messages.notificationTitle)}
<Hyperlink
destination={`${getConfig().ACCOUNT_SETTINGS_URL}/notifications`}
target="_blank"
rel="noopener noreferrer"
showLaunchIcon={false}
>
<Icon
src={Settings}
className="icon-size-20 text-primary-500"
data-testid="setting-icon"
screenReaderText="preferences settings icon"
/>
</Hyperlink>
</Popover.Title>
</div>
<Popover.Content className="notification-content p-0">
<NotificationTabs />
<NotificationContext.Provider value={notificationRefs}>
<NotificationTabs />
</NotificationContext.Provider>
</Popover.Content>
{getConfig().NOTIFICATION_FEEDBACK_URL && (
<Button
Expand All @@ -260,7 +271,7 @@ const Notifications = () => {
<div ref={buttonRef}>
<IconButton
isActive={enableNotificationTray}
alt="notification bell icon"
alt={intl.formatMessage(messages.notificationBellIconAltMessage)}
onClick={toggleNotificationTray}
src={NotificationsNone}
iconAs={Icon}
Expand Down
15 changes: 15 additions & 0 deletions src/Notifications/messages.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,21 @@ const messages = defineMessages({
defaultMessage: 'Notifications are automatically cleared after {days} days',
description: 'Message showing that expired notifications will be deleted',
},
noNotificationsYetMessage: {
id: 'notification.no.message',
defaultMessage: 'No notifications yet',
description: 'Message visible when there is no notification in the notification tray',
},
noNotificationHelpMessage: {
id: 'notification.no.help.message',
defaultMessage: 'When you receive notifications they’ll show up here',
description: 'Message showing that when you receive notifications they’ll show up here',
},
notificationBellIconAltMessage: {
id: 'notification.bell.icon.alt.message',
defaultMessage: 'Notification bell icon',
description: 'Alt message for notification bell icon',
},
});

export default messages;
37 changes: 34 additions & 3 deletions src/Notifications/notificationSections.test.jsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import React from 'react';

import {
act, fireEvent, render, screen, within,
act, fireEvent, render, screen, waitFor, within,
} from '@testing-library/react';
import MockAdapter from 'axios-mock-adapter';
import { Context as ResponsiveContext } from 'react-responsive';
Expand All @@ -14,14 +14,18 @@ import { AppContext, AppProvider } from '@edx/frontend-platform/react';

import AuthenticatedUserDropdown from '../learning-header/AuthenticatedUserDropdown';
import { initializeStore } from '../store';
import { markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl } from './data/api';
import {
markNotificationAsReadApiUrl, markNotificationsSeenApiUrl, getNotificationsListApiUrl, getNotificationsCountApiUrl,
} from './data/api';
import mockNotificationsResponse from './test-utils';
import { markNotificationsAsSeen, fetchNotificationList } from './data/thunks';
import { markNotificationsAsSeen, fetchNotificationList, fetchAppsNotificationCount } from './data/thunks';
import executeThunk from '../test-utils';
import './data/__factories__';
import { RequestStatus, notificationListStatusRequest } from './data';

const markedAllNotificationsAsReadApiUrl = markNotificationAsReadApiUrl();
const notificationCountsApiUrl = getNotificationsCountApiUrl();
const notificationsApiUrl = getNotificationsListApiUrl();

let axiosMock;
let store;
Expand Down Expand Up @@ -126,4 +130,31 @@ describe('Notification sections test cases.', () => {
expect(screen.queryAllByTestId('notification-contents')).toHaveLength(12);
expect(screen.queryByTestId('notifications-list-complete')).toBeInTheDocument();
});

it('Successfully showed No notification yet message when the notification tray is empty.', async () => {
const notificationCountsMock = {
show_notifications_tray: true,
count: 0,
count_by_app_name: {
discussion: 0,
},
};

axiosMock.onGet(notificationCountsApiUrl).reply(200, notificationCountsMock);
axiosMock.onGet(notificationsApiUrl).reply(200, { results: [] });

await executeThunk(fetchAppsNotificationCount(), store.dispatch, store.getState);
await executeThunk(fetchNotificationList({ appName: 'discussion', page: 1 }), store.dispatch, store.getState);

renderComponent();

const bellIcon = screen.queryByTestId('notification-bell-icon');
await act(async () => { fireEvent.click(bellIcon); });

await waitFor(() => {
const noNotiifcationMsg = screen.queryByText('No notifications yet');

expect(noNotiifcationMsg).toBeInTheDocument();
});
});
});
10 changes: 5 additions & 5 deletions src/Notifications/notificationTabs.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -79,13 +79,13 @@ describe('Notification Tabs test cases.', () => {
const bellIcon = screen.queryByTestId('notification-bell-icon');
await act(async () => { fireEvent.click(bellIcon); });
const notificationTab = screen.getAllByRole('tab');
let selectedTab = screen.queryByTestId('notification-tab-reminders');

await act(async () => { fireEvent.click(notificationTab[0], { dataset: { rbEventKey: 'reminders' } }); });
expect(selectedTab).not.toHaveClass('active');

const tabs = screen.queryAllByRole('tab');
const selectedTab = tabs.find(tab => tab.getAttribute('aria-selected') === 'true');
await act(async () => { fireEvent.click(notificationTab[0], { dataset: { rbEventKey: 'reminders' } }); });
selectedTab = screen.queryByTestId('notification-tab-reminders');

expect(within(selectedTab).queryByText('reminders')).toBeInTheDocument();
expect(within(selectedTab).queryByRole('status')).not.toBeInTheDocument();
expect(selectedTab).toHaveClass('active');
});
});
22 changes: 22 additions & 0 deletions src/index.scss
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,16 @@ $white: #fff;
}
}

.notification-lg-bell-icon {
width: 56px !important;
height: 56px !important;

span:first-child {
width: 32px !important;
height: 32px !important;
}
}

.notification-button.btn-icon-light-active {
background-color: #F2F0EF !important;
}
Expand Down Expand Up @@ -344,3 +354,15 @@ $white: #fff;
.popover-margin-top {
margin-top: -68px !important;
}

.height-inherit {
height: inherit;
}

.height-100vh {
height: 100vh
}

.height-91vh {
height: 91vh
}

0 comments on commit 326c522

Please sign in to comment.