Skip to content

Commit 4e718f8

Browse files
refactor: fixed all review points
1 parent a211547 commit 4e718f8

12 files changed

Lines changed: 144 additions & 110 deletions

File tree

src/Notifications/NotificationRowItem.jsx

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { Link } from 'react-router-dom';
77
import * as timeago from 'timeago.js';
88
import { getIconByType } from './utils';
99
import { markNotificationsAsRead } from './data/thunks';
10-
import { messages } from './messages';
10+
import messages from './messages';
1111
import timeLocale from '../common/time-locale';
1212

1313
const NotificationRowItem = ({
@@ -21,15 +21,16 @@ const NotificationRowItem = ({
2121
dispatch(markNotificationsAsRead(id));
2222
}, [dispatch, id]);
2323

24-
const iconComponent = getIconByType(type);
24+
const { icon: iconComponent, class: iconClass } = getIconByType(type);
2525

2626
return (
27-
<Link className="d-flex mb-2 align-items-center text-decoration-none" to={contentUrl} onClick={handleMarkAsRead}>
28-
<Icon
29-
src={iconComponent && iconComponent.icon}
30-
style={{ height: '23.33px', width: '23.33px' }}
31-
className={iconComponent && `${iconComponent.class} mr-4`}
32-
/>
27+
<Link
28+
target="_blank"
29+
className="d-flex mb-2 align-items-center text-decoration-none"
30+
to={contentUrl}
31+
onClick={handleMarkAsRead}
32+
>
33+
<Icon src={iconComponent} className={`${iconClass} mr-4 notification-icon`} />
3334
<div className="d-flex w-100">
3435
<div className="d-flex align-items-center w-100">
3536
<div className="py-10px w-100 px-0 cursor-pointer">

src/Notifications/NotificationSections.jsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { Button } from '@edx/paragon';
33
import { useDispatch, useSelector } from 'react-redux';
44
import { useIntl } from '@edx/frontend-platform/i18n';
55
import isEmpty from 'lodash/isEmpty';
6-
import { messages } from './messages';
6+
import messages from './messages';
77
import NotificationRowItem from './NotificationRowItem';
88
import { markAllNotificationsAsRead } from './data/thunks';
99
import { selectNotificationsByIds, selectPaginationData, selectSelectedAppName } from './data/selectors';
@@ -14,8 +14,8 @@ const NotificationSections = () => {
1414
const intl = useIntl();
1515
const dispatch = useDispatch();
1616
const selectedAppName = useSelector(selectSelectedAppName());
17-
const notifications = useSelector(selectNotificationsByIds);
18-
const paginationData = useSelector(selectPaginationData());
17+
const notifications = useSelector(selectNotificationsByIds(selectedAppName));
18+
const { currentPage, numPages } = useSelector(selectPaginationData());
1919
const { today = [], earlier = [] } = useMemo(
2020
() => splitNotificationsByTime(notifications),
2121
[notifications],
@@ -69,7 +69,7 @@ const NotificationSections = () => {
6969
<div className="mt-4 px-4">
7070
{renderNotificationSection('today', today)}
7171
{renderNotificationSection('earlier', earlier)}
72-
{paginationData.currentPage < paginationData.numPages && (
72+
{currentPage < numPages && (
7373
<Button variant="primary" className="w-100 bg-primary-500" onClick={updatePagination}>
7474
{intl.formatMessage(messages.loadMoreNotifications)}
7575
</Button>

src/Notifications/NotificationTabs.jsx

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
/* eslint-disable react-hooks/exhaustive-deps */
12
import React, { useCallback, useEffect, useMemo } from 'react';
23
import { useDispatch, useSelector } from 'react-redux';
34
import { Tab, Tabs } from '@edx/paragon';
@@ -13,16 +14,16 @@ const NotificationTabs = () => {
1314
const selectedAppName = useSelector(selectSelectedAppName());
1415
const notificationUnseenCounts = useSelector(selectNotificationTabsCount());
1516
const notificationTabs = useSelector(selectNotificationTabs());
16-
const paginationData = useSelector(selectPaginationData());
17+
const { currentPage } = useSelector(selectPaginationData());
1718

1819
useEffect(() => {
19-
dispatch(fetchNotificationList({ appName: selectedAppName, page: paginationData.currentPage, pageSize: 10 }));
20+
dispatch(fetchNotificationList({ appName: selectedAppName, page: currentPage, pageSize: 10 }));
2021
if (selectedAppName) { dispatch(markNotificationsAsSeen(selectedAppName)); }
21-
}, [dispatch, paginationData.currentPage, selectedAppName]);
22+
}, [currentPage, selectedAppName]);
2223

2324
const handleActiveTab = useCallback((appName) => {
2425
dispatch(updateAppNameRequest({ appName }));
25-
}, [dispatch]);
26+
}, []);
2627

2728
const tabArray = useMemo(() => notificationTabs?.map((appName) => (
2829
<Tab

src/Notifications/data/selectors.js

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,12 @@ export const selectSelectedAppNotificationIds = (appName) => state => state.noti
1010

1111
export const selectShowNotificationTray = () => state => state.notifications.showNotificationTray;
1212

13-
export const selectNotifications = () => state => state.notifications.notification;
13+
export const selectNotifications = () => state => state.notifications.notifications;
1414

15-
export const selectNotificationsByIds = createSelector(
16-
state => state.notifications.notifications,
17-
state => state.notifications.apps[state.notifications.appName] || [],
18-
(notifications, notificationIds) => notificationIds.map(notificationId => notifications[notificationId]),
15+
export const selectNotificationsByIds = (appName) => createSelector(
16+
selectNotifications(),
17+
selectSelectedAppNotificationIds(appName),
18+
(notifications, notificationIds) => notificationIds.map((notificationId) => notifications[notificationId]) || [],
1919
);
2020

2121
export const selectSelectedAppName = () => state => state.notifications.appName;

src/Notifications/data/slice.js

Lines changed: 40 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
/* eslint-disable no-param-reassign */
22
import { createSlice } from '@reduxjs/toolkit';
33

4-
export const IDLE = 'idle';
5-
export const LOADING = 'loading';
6-
export const LOADED = 'loaded';
7-
export const FAILED = 'failed';
8-
export const DENIED = 'denied';
4+
export const RequestStatus = {
5+
IDLE: 'idle',
6+
LOADING: 'in-progress',
7+
LOADED: 'successful',
8+
FAILED: 'failed',
9+
DENIED: 'denied',
10+
};
911

1012
const initialState = {
1113
notificationStatus: 'idle',
12-
appName: 'reminders',
14+
appName: 'discussions',
1315
appsId: [],
1416
apps: {},
1517
notifications: {},
@@ -26,65 +28,62 @@ const slice = createSlice({
2628
name: 'notifications',
2729
initialState,
2830
reducers: {
29-
fetchNotificationDenied: (state, { payload }) => {
30-
state.appName = payload.appName;
31-
state.notificationStatus = DENIED;
31+
fetchNotificationDenied: (state) => {
32+
state.notificationStatus = RequestStatus.DENIED;
3233
},
33-
fetchNotificationFailure: (state, { payload }) => {
34-
state.appName = payload.appName;
35-
state.notificationStatus = FAILED;
34+
fetchNotificationFailure: (state) => {
35+
state.notificationStatus = RequestStatus.FAILED;
3636
},
37-
fetchNotificationRequest: (state, { payload }) => {
38-
if (state.appName !== payload.appName) { state.apps[payload.appName] = []; }
39-
state.appName = payload.appName;
40-
state.notificationStatus = LOADING;
37+
fetchNotificationRequest: (state) => {
38+
state.notificationStatus = RequestStatus.LOADING;
4139
},
4240
fetchNotificationSuccess: (state, { payload }) => {
43-
const { notifications, numPages, currentPage } = payload;
44-
const newNotificationIds = notifications.map(notification => notification.id.toString());
41+
const {
42+
newNotificationIds, notificationsKeyValuePair, numPages, currentPage,
43+
} = payload;
4544
const existingNotificationIds = state.apps[state.appName];
46-
const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
47-
const currentAppCount = state.tabsCount[state.appName];
4845

4946
state.apps[state.appName] = Array.from(new Set([...existingNotificationIds, ...newNotificationIds]));
5047
state.notifications = { ...state.notifications, ...notificationsKeyValuePair };
51-
state.tabsCount.count -= currentAppCount;
48+
state.tabsCount.count -= state.tabsCount[state.appName];
5249
state.tabsCount[state.appName] = 0;
53-
state.notificationStatus = LOADED;
50+
state.notificationStatus = RequestStatus.LOADED;
5451
state.pagination.numPages = numPages;
5552
state.pagination.currentPage = currentPage;
5653
},
5754
fetchNotificationsCountDenied: (state) => {
58-
state.notificationStatus = DENIED;
55+
state.notificationStatus = RequestStatus.DENIED;
5956
},
6057
fetchNotificationsCountFailure: (state) => {
61-
state.notificationStatus = FAILED;
58+
state.notificationStatus = RequestStatus.FAILED;
6259
},
6360
fetchNotificationsCountRequest: (state) => {
64-
state.notificationStatus = LOADING;
61+
state.notificationStatus = RequestStatus.LOADING;
6562
},
6663
fetchNotificationsCountSuccess: (state, { payload }) => {
67-
const { countByAppName, count, showNotificationTray } = payload;
64+
const {
65+
countByAppName, appIds, apps, count, showNotificationTray,
66+
} = payload;
6867
state.tabsCount = { count, ...countByAppName };
69-
state.appsId = Object.keys(countByAppName);
70-
state.apps = Object.fromEntries(Object.keys(countByAppName).map(key => [key, []]));
68+
state.appsId = appIds;
69+
state.apps = apps;
7170
state.showNotificationTray = showNotificationTray;
72-
state.notificationStatus = LOADED;
71+
state.notificationStatus = RequestStatus.LOADED;
7372
},
7473
markNotificationsAsSeenRequest: (state) => {
75-
state.notificationStatus = LOADING;
74+
state.notificationStatus = RequestStatus.LOADING;
7675
},
7776
markNotificationsAsSeenSuccess: (state) => {
78-
state.notificationStatus = LOADED;
77+
state.notificationStatus = RequestStatus.LOADED;
7978
},
8079
markNotificationsAsSeenDenied: (state) => {
81-
state.notificationStatus = DENIED;
80+
state.notificationStatus = RequestStatus.DENIED;
8281
},
8382
markNotificationsAsSeenFailure: (state) => {
84-
state.notificationStatus = FAILED;
83+
state.notificationStatus = RequestStatus.FAILED;
8584
},
8685
markAllNotificationsAsReadRequest: (state) => {
87-
state.notificationStatus = LOADING;
86+
state.notificationStatus = RequestStatus.LOADING;
8887
},
8988
markAllNotificationsAsReadSuccess: (state) => {
9089
const updatedNotifications = Object.fromEntries(
@@ -93,27 +92,27 @@ const slice = createSlice({
9392
]),
9493
);
9594
state.notifications = updatedNotifications;
96-
state.notificationStatus = LOADED;
95+
state.notificationStatus = RequestStatus.LOADED;
9796
},
9897
markAllNotificationsAsReadDenied: (state) => {
99-
state.notificationStatus = DENIED;
98+
state.notificationStatus = RequestStatus.DENIED;
10099
},
101100
markAllNotificationsAsReadFailure: (state) => {
102-
state.notificationStatus = FAILED;
101+
state.notificationStatus = RequestStatus.FAILED;
103102
},
104103
markNotificationsAsReadRequest: (state) => {
105-
state.notificationStatus = LOADING;
104+
state.notificationStatus = RequestStatus.LOADING;
106105
},
107106
markNotificationsAsReadSuccess: (state, { payload }) => {
108107
const date = new Date().toISOString();
109108
state.notifications[payload.id] = { ...state.notifications[payload.id], lastRead: date };
110-
state.notificationStatus = LOADED;
109+
state.notificationStatus = RequestStatus.LOADED;
111110
},
112111
markNotificationsAsReadDenied: (state) => {
113-
state.notificationStatus = DENIED;
112+
state.notificationStatus = RequestStatus.DENIED;
114113
},
115114
markNotificationsAsReadFailure: (state) => {
116-
state.notificationStatus = FAILED;
115+
state.notificationStatus = RequestStatus.FAILED;
117116
},
118117
resetNotificationStateRequest: () => initialState,
119118
updateAppNameRequest: (state, { payload }) => {

src/Notifications/data/thunks.js

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { camelCaseObject } from '@edx/frontend-platform';
21
import {
32
fetchNotificationSuccess,
43
fetchNotificationRequest,
@@ -27,14 +26,29 @@ import {
2726
} from './api';
2827
import { getHttpErrorStatus } from '../utils';
2928

30-
export const fetchNotificationList = ({
31-
appName, page, pageSize,
32-
}) => (
29+
const normalizeNotificationCounts = ({ countByAppName, count, showNotificationTray }) => {
30+
const appIds = Object.keys(countByAppName);
31+
const apps = appIds.reduce((acc, appId) => { acc[appId] = []; return acc; }, {});
32+
return {
33+
countByAppName, appIds, apps, count, showNotificationTray,
34+
};
35+
};
36+
37+
const normalizeNotifications = ({ notifications }) => {
38+
const newNotificationIds = notifications.map(notification => notification.id.toString());
39+
const notificationsKeyValuePair = notifications.reduce((acc, obj) => { acc[obj.id] = obj; return acc; }, {});
40+
return {
41+
newNotificationIds, notificationsKeyValuePair,
42+
};
43+
};
44+
45+
export const fetchNotificationList = ({ appName, page, pageSize }) => (
3346
async (dispatch) => {
3447
try {
3548
dispatch(fetchNotificationRequest({ appName }));
3649
const data = await getNotifications(appName, page, pageSize);
37-
dispatch(fetchNotificationSuccess(data));
50+
const normalisedData = normalizeNotifications((data));
51+
dispatch(fetchNotificationSuccess({ ...normalisedData, numPages: data.numPages, currentPage: data.currentPage }));
3852
} catch (error) {
3953
if (getHttpErrorStatus(error) === 403) {
4054
dispatch(fetchNotificationDenied(appName));
@@ -50,7 +64,13 @@ export const fetchAppsNotificationCount = () => (
5064
try {
5165
dispatch(fetchNotificationsCountRequest());
5266
const data = await getNotificationCounts();
53-
dispatch(fetchNotificationsCountSuccess(camelCaseObject(data)));
67+
const normalisedData = normalizeNotificationCounts((data));
68+
dispatch(fetchNotificationsCountSuccess({
69+
...normalisedData,
70+
countByAppName: data.countByAppName,
71+
count: data.count,
72+
showNotificationTray: data.showNotificationTray,
73+
}));
5474
} catch (error) {
5575
if (getHttpErrorStatus(error) === 403) {
5676
dispatch(fetchNotificationsCountDenied());

src/Notifications/index.jsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,33 +13,33 @@ import { selectNotificationTabsCount } from './data/selectors';
1313
import { resetNotificationState } from './data/thunks';
1414
import { useIsOnLargeScreen, useIsOnMediumScreen } from './data/hook';
1515
import NotificationTabs from './NotificationTabs';
16-
import { messages } from './messages';
16+
import messages from './messages';
1717

1818
const Notifications = () => {
1919
const intl = useIntl();
2020
const dispatch = useDispatch();
2121
const popoverRef = useRef(null);
2222
const buttonRef = useRef(null);
23-
const [showNotificationTray, setShowNotificationTray] = useState(false);
23+
const [enableNotificationTray, setEnableNotificationTray] = useState(false);
2424
const notificationCounts = useSelector(selectNotificationTabsCount());
2525
const isOnMediumScreen = useIsOnMediumScreen();
2626
const isOnLargeScreen = useIsOnLargeScreen();
2727

2828
const hideNotificationTray = useCallback(() => {
29-
setShowNotificationTray(prevState => !prevState);
29+
setEnableNotificationTray(prevState => !prevState);
3030
}, []);
3131

32-
const handleClickOutside = useCallback((event) => {
32+
const handleClickOutsideNotificationTray = useCallback((event) => {
3333
if (!popoverRef.current?.contains(event.target) && !buttonRef.current?.contains(event.target)) {
34-
setShowNotificationTray(false);
34+
setEnableNotificationTray(false);
3535
}
3636
}, []);
3737

3838
useEffect(() => {
39-
document.addEventListener('mousedown', handleClickOutside);
39+
document.addEventListener('mousedown', handleClickOutsideNotificationTray);
4040

4141
return () => {
42-
document.removeEventListener('mousedown', handleClickOutside);
42+
document.removeEventListener('mousedown', handleClickOutsideNotificationTray);
4343
dispatch(resetNotificationState());
4444
};
4545
}, []);
@@ -50,7 +50,7 @@ const Notifications = () => {
5050
key="bottom"
5151
placement="bottom"
5252
id="notificationTray"
53-
show={showNotificationTray}
53+
show={enableNotificationTray}
5454
overlay={(
5555
<Popover
5656
id="notificationTray"
@@ -75,7 +75,7 @@ const Notifications = () => {
7575
>
7676
<div ref={buttonRef}>
7777
<IconButton
78-
isActive={showNotificationTray}
78+
isActive={enableNotificationTray}
7979
alt="notification bell icon"
8080
onClick={hideNotificationTray}
8181
src={NotificationsNone}

src/Notifications/messages.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
import { defineMessages } from '@edx/frontend-platform/i18n';
22

3-
// eslint-disable-next-line import/prefer-default-export
4-
export const messages = defineMessages({
3+
const messages = defineMessages({
54
notificationTitle: {
65
id: 'notification.title',
76
defaultMessage: 'Notifications',
@@ -33,3 +32,5 @@ export const messages = defineMessages({
3332
description: 'Load more button to load more notifications',
3433
},
3534
});
35+
36+
export default messages;

src/Notifications/utils.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,5 +48,5 @@ export const getIconByType = (type) => {
4848
commentLiked: { icon: ThumbUpOutline, class: 'text-primary-500' },
4949
edited: { icon: EditOutline, class: 'text-primary-500' },
5050
};
51-
return iconMap[type] || null;
51+
return iconMap[type] || { icon: PostOutline, class: 'text-primary-500' };
5252
};

src/common/time-locale.js

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
1-
// eslint-disable-next-line no-unused-vars
2-
export default function timeLocale(number, index, totalSec) {
1+
export default function timeLocale(number, index) {
32
return [
43
['just now', 'right now'],
54
['%ss', 'in %s seconds'],

0 commit comments

Comments
 (0)