oxynote/web/app/composables/api/useNotificationAPI.ts
swithek 78c4244619
overhaul the notif inbox (#200)
* overhaul the notif inbox

* make hook summary abstract

---------

Co-authored-by: David <SE.David@protonmail.com>
Co-authored-by: david <58231312+davseby@users.noreply.github.com>
2026-10-06 19:03:49 +03:00

167 lines
4.5 KiB
TypeScript

import type { EntryKey, UseInfiniteQueryData } from "@pinia/colada"
import isDeepEqual from "fast-deep-equal"
type NotificationPages = UseInfiniteQueryData<NotificationsResponse, number>
const NOTIFICATION_QUERY_KEYS = {
list: (limit: number) => ["notifications", "list", limit] as const,
count: (read: boolean) => ["notifications", "count", read] as const,
listRoot: ["notifications", "list"] as const,
countRoot: ["notifications", "count"] as const,
}
export default function () {
const { $coreAPIClient } = useNuxtApp()
const queryCache = useQueryCache()
function useFetchManyNotifications(
paramsRef: MaybeRefOrGetter<NotificationsParams>,
) {
return useInfiniteQuery({
key: () => NOTIFICATION_QUERY_KEYS.list(toValue(paramsRef).limit),
query: async ({ pageParam }) => {
const searchParams = new URLSearchParams({
limit: String(toValue(paramsRef).limit),
page: String(pageParam),
})
return await $coreAPIClient<NotificationsResponse>(
`/api/notifications?${searchParams.toString()}`,
{ method: "GET" },
)
},
initialPageParam: 1,
getNextPageParam: (lastPage, _pages, lastPageParam) =>
lastPageParam < lastPage.pageCount ? lastPageParam + 1 : null,
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: 60 * 1000, // 1 min
autoRefetch: true,
})
}
function useFetchNotificationCount(
paramsRef: MaybeRefOrGetter<NotificationsCountParams>,
) {
return useQuery({
key: () => {
const params = toValue(paramsRef)
return NOTIFICATION_QUERY_KEYS.count(params.read)
},
query: async () => {
const params = toValue(paramsRef)
const searchParams = new URLSearchParams()
searchParams.set("read", String(params.read))
const query = searchParams.toString()
const url = query
? `/api/notifications/count?${query}`
: `/api/notifications/count`
return await $coreAPIClient<{ count: number }>(url, { method: "GET" })
},
refetchOnMount: false,
refetchOnWindowFocus: false,
refetchOnReconnect: false,
staleTime: 60 * 1000, // 1 min
autoRefetch: true,
})
}
const markNotificationsRead = useMutation({
// count is hard to optimistically update, so we skip it
onMutate: (req) => {
const entries = queryCache.getEntries({
key: NOTIFICATION_QUERY_KEYS.listRoot,
})
// the key travels next to the data rather than inside it, so the
// bookkeeping never ends up in the cached data
const oldNotifs: { key: EntryKey; data: NotificationPages }[] = []
entries.forEach((entry) => {
const oldNotifData = clone(
queryCache.getQueryData<NotificationPages>(entry.key),
)
// entries can exist without data (a query that has not
// resolved yet); including them would make the update loop
// below throw on the missing pages array and abort the whole
// mutation
if (!oldNotifData) {
return
}
oldNotifs.push({ key: clone(entry.key), data: oldNotifData })
})
const newNotifs = clone(oldNotifs)
newNotifs.forEach(({ key, data }) => {
data.pages.forEach((page) => {
page.notifications.forEach((notif) => {
if (req.ids.length === 0 || req.ids.includes(notif.id)) {
notif.read = true
}
})
})
queryCache.setQueryData(key, data)
queryCache.cancelQueries({ key })
})
return { newNotifs, oldNotifs }
},
mutation: async (req: MarkNotificationsReadRequest) => {
await $coreAPIClient(`/api/notifications/read-status`, {
method: "PUT",
body: req,
})
},
async onSuccess() {
await queryCache.invalidateQueries({
key: NOTIFICATION_QUERY_KEYS.listRoot,
})
await queryCache.invalidateQueries({
key: NOTIFICATION_QUERY_KEYS.countRoot,
})
},
onError(_err, _data, { oldNotifs, newNotifs }) {
const entries = queryCache.getEntries({
key: NOTIFICATION_QUERY_KEYS.listRoot,
})
const cachedNotifs: { key: EntryKey; data: NotificationPages }[] = []
entries.forEach((entry) => {
const cachedNotifData = clone(
queryCache.getQueryData<NotificationPages>(entry.key),
)
// mirror the onMutate filter so the rollback comparison sees
// the same set of entries the optimistic update touched
if (!cachedNotifData) {
return
}
cachedNotifs.push({ key: clone(entry.key), data: cachedNotifData })
})
if (!isDeepEqual(newNotifs, cachedNotifs)) {
return
}
// rollback
oldNotifs?.forEach(({ key, data }) => {
queryCache.setQueryData(key, data)
})
},
})
return {
useFetchManyNotifications,
useFetchNotificationCount,
markNotificationsRead,
}
}