2023-10-14 11:39:24 +00:00
|
|
|
import type { Accessor, JSX } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
import { createContext, createEffect, createMemo, createSignal, useContext } from 'solid-js'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { createStore } from 'solid-js/store'
|
2023-10-14 11:39:24 +00:00
|
|
|
import { Portal } from 'solid-js/web'
|
2023-11-14 15:10:00 +00:00
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
|
|
|
import { NotificationsPanel } from '../components/NotificationsPanel'
|
|
|
|
import { Notification } from '../graphql/types.gen'
|
2023-11-14 15:10:00 +00:00
|
|
|
import { apiClient } from '../utils/apiClient'
|
|
|
|
import { apiBaseUrl } from '../utils/config'
|
|
|
|
import SSEService, { EventData } from '../utils/sseService'
|
|
|
|
|
|
|
|
import { useSession } from './session'
|
2023-11-17 15:08:52 +00:00
|
|
|
import { Portal } from 'solid-js/web'
|
|
|
|
import { ShowIfAuthenticated } from '../components/_shared/ShowIfAuthenticated'
|
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-14 11:39:24 +00:00
|
|
|
|
2023-10-20 18:07:33 +00:00
|
|
|
export type MessageHandler = (m: SSEMessage) => void
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
type NotificationsContextType = {
|
2023-10-19 14:44:26 +00:00
|
|
|
notificationEntities: Record<number, SSEMessage>
|
2023-10-14 11:39:24 +00:00
|
|
|
unreadNotificationsCount: Accessor<number>
|
|
|
|
sortedNotifications: Accessor<Notification[]>
|
2023-11-01 15:13:54 +00:00
|
|
|
loadedNotificationsCount: Accessor<number>
|
|
|
|
totalNotificationsCount: Accessor<number>
|
2023-10-14 11:39:24 +00:00
|
|
|
actions: {
|
|
|
|
showNotificationsPanel: () => void
|
2023-10-16 17:24:33 +00:00
|
|
|
hideNotificationsPanel: () => void
|
2023-10-14 11:39:24 +00:00
|
|
|
markNotificationAsRead: (notification: Notification) => Promise<void>
|
2023-11-01 15:13:54 +00:00
|
|
|
markAllNotificationsAsRead: () => Promise<void>
|
|
|
|
loadNotifications: (options: { limit: number; offset: number }) => Promise<Notification[]>
|
2023-10-16 21:45:22 +00:00
|
|
|
setMessageHandler: (h: MessageHandler) => void
|
2023-10-14 11:39:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2023-11-01 15:13:54 +00:00
|
|
|
export const PAGE_SIZE = 20
|
2023-10-14 11:39:24 +00:00
|
|
|
const NotificationsContext = createContext<NotificationsContextType>()
|
|
|
|
|
|
|
|
export function useNotifications() {
|
|
|
|
return useContext(NotificationsContext)
|
|
|
|
}
|
|
|
|
|
|
|
|
const sseService = new SSEService()
|
|
|
|
|
|
|
|
export const NotificationsProvider = (props: { children: JSX.Element }) => {
|
|
|
|
const [isNotificationsPanelOpen, setIsNotificationsPanelOpen] = createSignal(false)
|
|
|
|
const [unreadNotificationsCount, setUnreadNotificationsCount] = createSignal(0)
|
2023-11-01 15:13:54 +00:00
|
|
|
const [totalNotificationsCount, setTotalNotificationsCount] = createSignal(0)
|
2023-10-14 11:39:24 +00:00
|
|
|
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>>>()
|
2023-11-17 15:08:52 +00:00
|
|
|
|
2023-10-14 23:27:33 +00:00
|
|
|
onMount(() => {
|
|
|
|
const dbx = openDB('notifications-db', 1, {
|
2023-11-13 13:44:04 +00:00
|
|
|
upgrade(indexedDb) {
|
|
|
|
indexedDb.createObjectStore('notifications')
|
2023-11-17 15:08:52 +00:00
|
|
|
},
|
2023-10-14 23:27:33 +00:00
|
|
|
})
|
|
|
|
setDb(dbx)
|
2023-10-14 23:05:07 +00:00
|
|
|
})
|
2023-10-14 11:39:24 +00:00
|
|
|
|
2023-11-01 15:13:54 +00:00
|
|
|
const loadNotifications = async (options: { limit: number; offset?: number }) => {
|
|
|
|
const { notifications, totalUnreadCount, totalCount } = await apiClient.getNotifications(options)
|
2023-10-14 11:39:24 +00:00
|
|
|
const newNotificationEntities = notifications.reduce((acc, notification) => {
|
|
|
|
acc[notification.id] = notification
|
|
|
|
return acc
|
|
|
|
}, {})
|
|
|
|
|
2023-11-01 15:13:54 +00:00
|
|
|
setTotalNotificationsCount(totalCount)
|
2023-10-14 11:39:24 +00:00
|
|
|
setUnreadNotificationsCount(totalUnreadCount)
|
2023-10-14 23:05:07 +00:00
|
|
|
setNotificationEntities(
|
|
|
|
notifications.reduce((acc, notification) => {
|
|
|
|
acc[notification.id] = notification
|
|
|
|
return acc
|
2023-11-17 15:08:52 +00:00
|
|
|
}, {}),
|
2023-10-14 23:05:07 +00:00
|
|
|
)
|
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
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-14 11:39:24 +00:00
|
|
|
})
|
|
|
|
|
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-11-01 15:13:54 +00:00
|
|
|
const loadedNotificationsCount = createMemo(() => Object.keys(notificationEntities).length)
|
2023-11-17 15:08:52 +00:00
|
|
|
|
2023-10-14 11:39:24 +00:00
|
|
|
createEffect(() => {
|
|
|
|
if (isAuthenticated()) {
|
|
|
|
sseService.connect(`${apiBaseUrl}/subscribe/${user().id}`)
|
|
|
|
sseService.subscribeToEvent('message', (data: EventData) => {
|
|
|
|
if (data.type === 'newNotifications') {
|
2023-11-14 10:45:44 +00:00
|
|
|
loadNotifications({ limit: Math.max(PAGE_SIZE, loadedNotificationsCount()) })
|
2023-10-14 11:39:24 +00:00
|
|
|
} else {
|
|
|
|
console.error(`[NotificationsProvider] unknown message type: ${JSON.stringify(data)}`)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
} else {
|
|
|
|
sseService.disconnect()
|
|
|
|
}
|
|
|
|
})
|
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 () => {
|
2023-10-14 11:39:24 +00:00
|
|
|
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',
|
2023-11-17 15:08:52 +00:00
|
|
|
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)
|
2023-11-17 09:00:00 +00:00
|
|
|
console.log('[context.notifications] Received message:', m)
|
2023-11-16 15:34:37 +00:00
|
|
|
if (m.entity === 'chat' || m.entity == 'message') {
|
2023-10-19 14:44:26 +00:00
|
|
|
messageHandler()(m)
|
2023-10-19 10:19:52 +00:00
|
|
|
} else {
|
|
|
|
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(),
|
2023-11-17 15:08:52 +00:00
|
|
|
seen: false,
|
2023-10-19 10:19:52 +00:00
|
|
|
})
|
|
|
|
}
|
|
|
|
},
|
|
|
|
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-11-17 15:08:52 +00:00
|
|
|
},
|
2023-10-19 10:19:52 +00:00
|
|
|
})
|
2023-10-14 11:39:24 +00:00
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
const markNotificationAsRead = async (notification: Notification) => {
|
|
|
|
await apiClient.markNotificationAsRead(notification.id)
|
2023-11-15 17:52:05 +00:00
|
|
|
setNotificationEntities(notification.id, 'seen', true)
|
|
|
|
setUnreadNotificationsCount((oldCount) => oldCount - 1)
|
2023-11-01 15:13:54 +00:00
|
|
|
}
|
|
|
|
const markAllNotificationsAsRead = async () => {
|
|
|
|
await apiClient.markAllNotificationsAsRead()
|
|
|
|
loadNotifications({ limit: loadedNotificationsCount() })
|
2023-10-14 11:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const showNotificationsPanel = () => {
|
|
|
|
setIsNotificationsPanelOpen(true)
|
|
|
|
}
|
|
|
|
|
2023-10-16 17:24:33 +00:00
|
|
|
const hideNotificationsPanel = () => {
|
|
|
|
setIsNotificationsPanelOpen(false)
|
|
|
|
}
|
|
|
|
|
2023-10-16 18:00:22 +00:00
|
|
|
const actions = {
|
|
|
|
setMessageHandler,
|
|
|
|
showNotificationsPanel,
|
|
|
|
hideNotificationsPanel,
|
2023-11-01 15:13:54 +00:00
|
|
|
markNotificationAsRead,
|
|
|
|
markAllNotificationsAsRead,
|
2023-11-14 15:10:00 +00:00
|
|
|
loadNotifications,
|
2023-10-16 18:00:22 +00:00
|
|
|
}
|
2023-10-14 11:39:24 +00:00
|
|
|
|
|
|
|
const value: NotificationsContextType = {
|
|
|
|
notificationEntities,
|
|
|
|
sortedNotifications,
|
|
|
|
unreadNotificationsCount,
|
2023-11-01 15:13:54 +00:00
|
|
|
loadedNotificationsCount,
|
|
|
|
totalNotificationsCount,
|
2023-11-14 15:10:00 +00:00
|
|
|
actions,
|
2023-10-14 11:39:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
const handleNotificationPanelClose = () => {
|
|
|
|
setIsNotificationsPanelOpen(false)
|
|
|
|
}
|
|
|
|
|
|
|
|
return (
|
|
|
|
<NotificationsContext.Provider value={value}>
|
|
|
|
{props.children}
|
|
|
|
<ShowIfAuthenticated>
|
|
|
|
<Portal>
|
|
|
|
<NotificationsPanel isOpen={isNotificationsPanelOpen()} onClose={handleNotificationPanelClose} />
|
|
|
|
</Portal>
|
|
|
|
</ShowIfAuthenticated>
|
|
|
|
</NotificationsContext.Provider>
|
|
|
|
)
|
|
|
|
}
|