-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathnotificationsSlice.ts
136 lines (115 loc) · 4.58 KB
/
notificationsSlice.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
import {
createAction,
createEntityAdapter,
createSelector,
createSlice,
isAnyOf,
PayloadAction,
} from '@reduxjs/toolkit'
import { forceGenerateNotifications } from '../../api/server'
import { apiSlice } from '../../app/apiSlice'
import { AppRootState, AppThunk } from '../../app/store'
export interface ServerNotification {
id: string
date: string
message: string
user: string
}
export interface NotificationMetadata {
id: string
isRead: boolean
isNew: boolean
}
export function fetchNotificationsWebSocket(): AppThunk {
return (_dispatch, getState) => {
const allNotifications = selectNotificationsData(getState())
const [latestNotification] = allNotifications
const latestTimestamp = latestNotification?.date ?? ''
forceGenerateNotifications(latestTimestamp)
}
}
const metadataAdapter = createEntityAdapter<NotificationMetadata>()
const initialState = metadataAdapter.getInitialState()
const notificationsReceived = createAction<Array<ServerNotification>>('notifications/notificationsReceived')
export const notificationsApiSlice = apiSlice.injectEndpoints({
endpoints: (builder) => ({
getNotifications: builder.query<Array<ServerNotification>, void>({
query: () => '/notifications',
async onCacheEntryAdded(arg, lifecycleApi) {
// create a websocket connection when the cache subscription starts
const ws = new WebSocket('ws://localhost')
try {
// wait for the initial query to resolve before proceeding
await lifecycleApi.cacheDataLoaded
// when data is received from the socket connection to the server,
// update our query result with the received message
function listener(event: MessageEvent<string>) {
const message: PayloadAction<Array<ServerNotification>> = JSON.parse(event.data)
if (message.type === 'notifications') {
lifecycleApi.updateCachedData((draft) => {
// Insert all received notifications from the websocket
// into the existing RTKQ cache array
draft.push(...message.payload)
draft.sort((a, b) => b.date.localeCompare(a.date))
})
// Dispatch an additional action so that we can track the "isRead" state
lifecycleApi.dispatch(notificationsReceived(message.payload))
}
}
ws.addEventListener('message', listener)
} catch {
// no-op in case `cacheEntryRemoved` resolves before `cacheDataLoaded`,
// in which case `cacheDataLoaded` will throw
}
// cacheEntryRemoved will resolve when the cache subscription is no longer active
await lifecycleApi.cacheEntryRemoved
// perform cleanup steps once the `cacheEntryRemoved` promise resolves
ws.close()
},
}),
}),
})
const matchNotificationsReceived = isAnyOf(
notificationsReceived,
notificationsApiSlice.endpoints.getNotifications.matchFulfilled,
)
const notificationsSlice = createSlice({
name: 'notifications',
initialState,
reducers: {
allNotificationsRead(state) {
Object.values(state.entities).forEach((metadata) => {
metadata.isRead = true
})
},
},
extraReducers(builder) {
builder.addMatcher(matchNotificationsReceived, (state, action) => {
const notificationsWithMetadata: Array<NotificationMetadata> = action.payload.map((notification) => ({
id: notification.id,
isNew: true,
isRead: false,
}))
Object.values(state.entities).forEach((metadata) => {
// Any notification we've read is no longer new
metadata.isNew = !metadata.isRead
})
metadataAdapter.upsertMany(state, notificationsWithMetadata)
})
},
})
export const { useGetNotificationsQuery } = notificationsApiSlice
export const { allNotificationsRead } = notificationsSlice.actions
export const notificationsReducer = notificationsSlice.reducer
export const { selectAll: selectAllNotificationMetadata, selectEntities: selectMetadataEntities } =
metadataAdapter.getSelectors((state: AppRootState) => state.notifications)
export function selectUnreadNotificationsCount(state: AppRootState) {
const allNotifications = selectAllNotificationMetadata(state)
const unreadNotifications = allNotifications.filter((notification) => !notification.isRead)
return unreadNotifications.length
}
export const selectNotificationsResult = notificationsApiSlice.endpoints.getNotifications.select()
const selectNotificationsData = createSelector(
selectNotificationsResult,
(notificationsResult) => notificationsResult.data ?? [],
)