Skip to content

Commit 939361d

Browse files
committed
Show notifications from cache and track client-side read status
1 parent 6103d6a commit 939361d

File tree

3 files changed

+37
-31
lines changed

3 files changed

+37
-31
lines changed

src/app/Navbar.js

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,23 @@ import { useDispatch, useSelector } from 'react-redux'
33
import { Link } from 'react-router-dom'
44

55
import {
6-
fetchNotifications,
7-
selectAllNotifications,
6+
fetchNotificationsWebsocket,
7+
selectNotificationsMetadata,
8+
useGetNotificationsQuery,
89
} from '../features/notifications/notificationsSlice'
910

1011
export const Navbar = () => {
1112
const dispatch = useDispatch()
12-
const notifications = useSelector(selectAllNotifications)
13-
const numUnreadNotifications = notifications.filter((n) => !n.read).length
13+
14+
// Trigger initial fetch of notifications and keep the websocket open to receive updates
15+
useGetNotificationsQuery()
16+
17+
const notificationsMetadata = useSelector(selectNotificationsMetadata)
18+
const numUnreadNotifications = notificationsMetadata.filter((n) => !n.read)
19+
.length
1420

1521
const fetchNewNotifications = () => {
16-
dispatch(fetchNotifications())
22+
dispatch(fetchNotificationsWebsocket())
1723
}
1824

1925
let unreadNotificationsBadge

src/features/notifications/NotificationsList.js

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,15 @@ import classnames from 'classnames'
66
import { selectAllUsers } from '../users/usersSlice'
77

88
import {
9-
selectAllNotifications,
9+
useGetNotificationsQuery,
1010
allNotificationsRead,
11+
selectMetadataEntities,
1112
} from './notificationsSlice'
1213

1314
export const NotificationsList = () => {
1415
const dispatch = useDispatch()
15-
const notifications = useSelector(selectAllNotifications)
16+
const { data: notifications = [] } = useGetNotificationsQuery()
17+
const notificationsMetadata = useSelector(selectMetadataEntities)
1618
const users = useSelector(selectAllUsers)
1719

1820
useLayoutEffect(() => {
@@ -26,8 +28,10 @@ export const NotificationsList = () => {
2628
name: 'Unknown User',
2729
}
2830

31+
const metadata = notificationsMetadata[notification.id]
32+
2933
const notificationClassname = classnames('notification', {
30-
new: notification.isNew,
34+
new: metadata.isNew,
3135
})
3236

3337
return (

src/features/notifications/notificationsSlice.js

Lines changed: 19 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,25 @@
11
import {
2+
createAction,
23
createSlice,
3-
createAsyncThunk,
44
createEntityAdapter,
55
createSelector,
6+
isAnyOf,
67
} from '@reduxjs/toolkit'
78

8-
import { client } from '../../api/client'
99
import { forceGenerateNotifications } from '../../api/server'
1010
import { apiSlice } from '../api/apiSlice'
1111

12+
const notificationsReceived = createAction(
13+
'notifications/notificationsReceived'
14+
)
15+
1216
export const extendedApi = apiSlice.injectEndpoints({
1317
endpoints: (builder) => ({
1418
getNotifications: builder.query({
1519
query: () => '/notifications',
16-
transformResponse: (res) => res.notifications,
1720
async onCacheEntryAdded(
1821
arg,
19-
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved }
22+
{ updateCachedData, cacheDataLoaded, cacheEntryRemoved, dispatch }
2023
) {
2124
// create a websocket connection when the cache subscription starts
2225
const ws = new WebSocket('ws://localhost')
@@ -36,6 +39,8 @@ export const extendedApi = apiSlice.injectEndpoints({
3639
draft.push(...message.payload)
3740
draft.sort((a, b) => b.date.localeCompare(a.date))
3841
})
42+
// Dispatch an additional action so we can track "read" state
43+
dispatch(notificationsReceived(message.payload))
3944
break
4045
}
4146
default:
@@ -76,21 +81,11 @@ export const fetchNotificationsWebsocket = () => (dispatch, getState) => {
7681
forceGenerateNotifications(latestTimestamp)
7782
}
7883

79-
const notificationsAdapter = createEntityAdapter({
80-
sortComparer: (a, b) => b.date.localeCompare(a.date),
81-
})
84+
const notificationsAdapter = createEntityAdapter()
8285

83-
export const fetchNotifications = createAsyncThunk(
84-
'notifications/fetchNotifications',
85-
async (_, { getState }) => {
86-
const allNotifications = selectAllNotifications(getState())
87-
const [latestNotification] = allNotifications
88-
const latestTimestamp = latestNotification ? latestNotification.date : ''
89-
const response = await client.get(
90-
`/fakeApi/notifications?since=${latestTimestamp}`
91-
)
92-
return response.data
93-
}
86+
const matchNotificationsReceived = isAnyOf(
87+
notificationsReceived,
88+
extendedApi.endpoints.getNotifications.matchFulfilled
9489
)
9590

9691
const notificationsSlice = createSlice({
@@ -104,10 +99,10 @@ const notificationsSlice = createSlice({
10499
},
105100
},
106101
extraReducers(builder) {
107-
builder.addCase(fetchNotifications.fulfilled, (state, action) => {
102+
builder.addMatcher(matchNotificationsReceived, (state, action) => {
108103
// Add client-side metadata for tracking new notifications
109-
const notificationsWithMetadata = action.payload.map((notification) => ({
110-
...notification,
104+
const notificationsMetadata = action.payload.map((notification) => ({
105+
id: notification.id,
111106
read: false,
112107
isNew: true,
113108
}))
@@ -117,7 +112,7 @@ const notificationsSlice = createSlice({
117112
notification.isNew = !notification.read
118113
})
119114

120-
notificationsAdapter.upsertMany(state, notificationsWithMetadata)
115+
notificationsAdapter.upsertMany(state, notificationsMetadata)
121116
})
122117
},
123118
})
@@ -127,5 +122,6 @@ export const { allNotificationsRead } = notificationsSlice.actions
127122
export default notificationsSlice.reducer
128123

129124
export const {
130-
selectAll: selectAllNotifications,
125+
selectAll: selectNotificationsMetadata,
126+
selectEntities: selectMetadataEntities,
131127
} = notificationsAdapter.getSelectors((state) => state.notifications)

0 commit comments

Comments
 (0)