webapp/src/context/notifications.tsx

176 lines
5.5 KiB
TypeScript
Raw Normal View History

import type { Accessor, JSX } from 'solid-js'
2023-10-14 23:27:33 +00:00
import { createContext, createEffect, createMemo, createSignal, onMount, useContext } from 'solid-js'
import { useSession } from './session'
import { Portal } from 'solid-js/web'
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
import { NotificationsPanel } from '../components/NotificationsPanel'
import { createStore } from 'solid-js/store'
2023-10-14 23:27:33 +00:00
import { IDBPDatabase, openDB } from 'idb'
2023-10-19 10:19:52 +00:00
import { fetchEventSource } from '@microsoft/fetch-event-source'
2023-10-14 23:05:07 +00:00
import { getToken } from '../graphql/privateGraphQLClient'
import { Author, Message, Reaction, Shout } from '../graphql/types.gen'
2023-11-13 14:43:08 +00:00
export const PAGE_SIZE = 20
2023-10-19 14:44:26 +00:00
export interface SSEMessage {
2023-10-20 18:07:33 +00:00
id: string
2023-10-19 14:44:26 +00:00
entity: string
action: string
2023-10-14 23:05:07 +00:00
payload: any // Author | Shout | Reaction | Message
2023-10-19 14:44:26 +00:00
timestamp?: number
seen?: boolean
2023-10-14 23:05:07 +00:00
}
2023-10-20 18:07:33 +00:00
export type MessageHandler = (m: SSEMessage) => void
type NotificationsContextType = {
2023-10-19 14:44:26 +00:00
notificationEntities: Record<number, SSEMessage>
unreadNotificationsCount: Accessor<number>
2023-10-19 14:44:26 +00:00
sortedNotifications: Accessor<SSEMessage[]>
actions: {
showNotificationsPanel: () => void
hideNotificationsPanel: () => void
2023-10-19 14:44:26 +00:00
markNotificationAsRead: (notification: SSEMessage) => Promise<void>
2023-10-16 21:45:22 +00:00
setMessageHandler: (h: MessageHandler) => void
}
}
const NotificationsContext = createContext<NotificationsContextType>()
export function useNotifications() {
return useContext(NotificationsContext)
}
export const NotificationsProvider = (props: { children: JSX.Element }) => {
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
const { isAuthenticated, user } = useSession()
2023-10-19 14:44:26 +00:00
const [notificationEntities, setNotificationEntities] = createStore<Record<number, SSEMessage>>({})
2023-10-14 23:27:33 +00:00
const [db, setDb] = createSignal<Promise<IDBPDatabase<unknown>>>()
onMount(() => {
const dbx = openDB('notifications-db', 1, {
2023-11-13 13:44:04 +00:00
upgrade(indexedDb) {
indexedDb.createObjectStore('notifications')
2023-10-14 23:27:33 +00:00
}
})
setDb(dbx)
2023-10-14 23:05:07 +00:00
})
const loadNotifications = async () => {
2023-10-14 23:27:33 +00:00
const storage = await db()
const notifications = await storage.getAll('notifications')
2023-10-18 09:46:35 +00:00
console.log('[context.notifications] Loaded notifications:', notifications)
2023-10-20 18:07:33 +00:00
const totalUnreadCount = notifications.filter((notification) => !notification.seen).length
2023-10-18 09:46:35 +00:00
console.log('[context.notifications] Total unread count:', totalUnreadCount)
setUnreadNotificationsCount(totalUnreadCount)
2023-10-14 23:05:07 +00:00
setNotificationEntities(
notifications.reduce((acc, notification) => {
acc[notification.id] = notification
return acc
}, {})
)
return notifications
}
const sortedNotifications = createMemo(() => {
2023-10-14 23:05:07 +00:00
return Object.values(notificationEntities).sort((a, b) => b.timestamp - a.timestamp)
})
2023-10-19 14:44:26 +00:00
const storeNotification = async (notification: SSEMessage) => {
2023-10-18 09:46:35 +00:00
console.log('[context.notifications] Storing notification:', notification)
2023-10-14 23:27:33 +00:00
const storage = await db()
const tx = storage.transaction('notifications', 'readwrite')
2023-10-14 23:05:07 +00:00
const store = tx.objectStore('notifications')
2023-10-14 23:27:33 +00:00
2023-10-20 18:07:33 +00:00
await store.put(notification, 'id')
2023-10-14 23:05:07 +00:00
await tx.done
loadNotifications()
}
2023-10-20 18:07:33 +00:00
const [messageHandler, setMessageHandler] = createSignal<MessageHandler>(console.warn)
2023-10-14 23:05:07 +00:00
2023-10-19 10:19:52 +00:00
createEffect(async () => {
if (isAuthenticated()) {
loadNotifications()
2023-11-15 09:51:13 +00:00
await fetchEventSource('https://chat.discours.io/connect', {
2023-10-19 10:19:52 +00:00
method: 'GET',
headers: {
'Content-Type': 'application/json',
Authorization: getToken()
2023-10-19 10:19:52 +00:00
},
onmessage(event) {
2023-10-19 14:44:26 +00:00
const m: SSEMessage = JSON.parse(event.data)
if (m.entity === 'chat') {
console.log('[context.notifications] Received message:', m)
messageHandler()(m)
2023-10-19 10:19:52 +00:00
} else {
2023-10-19 14:44:26 +00:00
console.log('[context.notifications] Received notification:', m)
2023-10-19 10:19:52 +00:00
storeNotification({
2023-10-19 14:44:26 +00:00
...m,
2023-10-20 18:07:33 +00:00
id: event.id,
2023-10-19 10:19:52 +00:00
timestamp: Date.now(),
seen: false
})
}
},
onclose() {
console.log('[context.notifications] sse connection closed by server')
},
onerror(err) {
console.error('[context.notifications] sse connection closed by error', err)
2023-11-13 13:44:04 +00:00
throw new Error(err) // NOTE: simple hack to close the connection
}
2023-10-19 10:19:52 +00:00
})
}
})
2023-10-19 14:44:26 +00:00
const markNotificationAsRead = async (notification: SSEMessage) => {
2023-10-18 09:46:35 +00:00
console.log('[context.notifications] Marking notification as read:', notification)
2023-10-14 23:27:33 +00:00
const storage = await db()
await storage.put('notifications', { ...notification, seen: true })
loadNotifications()
}
const showNotificationsPanel = () => {
setIsNotificationsPanelOpen(true)
}
const hideNotificationsPanel = () => {
setIsNotificationsPanelOpen(false)
}
2023-10-16 18:00:22 +00:00
const actions = {
setMessageHandler,
showNotificationsPanel,
hideNotificationsPanel,
markNotificationAsRead
}
const value: NotificationsContextType = {
notificationEntities,
sortedNotifications,
unreadNotificationsCount,
actions
}
const handleNotificationPanelClose = () => {
setIsNotificationsPanelOpen(false)
}
return (
<NotificationsContext.Provider value={value}>
{props.children}
<ShowIfAuthenticated>
<Portal>
<NotificationsPanel isOpen={isNotificationsPanelOpen()} onClose={handleNotificationPanelClose} />
</Portal>
</ShowIfAuthenticated>
</NotificationsContext.Provider>
)
}