diff --git a/packages/react/src/context/RendererContext.tsx b/packages/react/src/context/RendererContext.tsx index 74c7c1be3f5..d5b9ce5ff96 100644 --- a/packages/react/src/context/RendererContext.tsx +++ b/packages/react/src/context/RendererContext.tsx @@ -1,5 +1,4 @@ import React from 'react'; -import type { NovuUI } from '@novu/js/ui'; import { createContextAndHook } from '../utils/createContextAndHook'; export type MountedElement = React.ReactNode; diff --git a/packages/react/src/hooks/index.ts b/packages/react/src/hooks/index.ts index 88f397d3bb3..d630ad9572b 100644 --- a/packages/react/src/hooks/index.ts +++ b/packages/react/src/hooks/index.ts @@ -1,3 +1,4 @@ export * from './useNotifications'; export * from './usePreferences'; +export * from './useCounts'; export { NovuProvider, useNovu } from './NovuProvider'; diff --git a/packages/react/src/hooks/useCounts.ts b/packages/react/src/hooks/useCounts.ts new file mode 100644 index 00000000000..be51d475a06 --- /dev/null +++ b/packages/react/src/hooks/useCounts.ts @@ -0,0 +1,106 @@ +import { useEffect, useState } from 'react'; +import { Notification, NotificationFilter, NovuError, areTagsEqual } from '@novu/js'; +import { useNovu } from './NovuProvider'; +import { useWebSocketEvent } from './internal/useWebsocketEvent'; + +type Count = { + count: number; + filter: NotificationFilter; +}; + +type UseCountsProps = { + filters: NotificationFilter[]; + onSuccess?: (data: Count[]) => void; + onError?: (error: NovuError) => void; +}; + +type UseCountsResult = { + counts?: Count[]; + error?: NovuError; + isLoading: boolean; // initial loading + isFetching: boolean; // the request is in flight + refetch: () => Promise; +}; + +export const useCounts = (props: UseCountsProps): UseCountsResult => { + const { filters, onSuccess, onError } = props; + const { notifications } = useNovu(); + const [error, setError] = useState(); + const [counts, setCounts] = useState(); + const [isLoading, setIsLoading] = useState(true); + const [isFetching, setIsFetching] = useState(false); + + const sync = async (notification?: Notification) => { + const existingCounts = counts ?? (new Array(filters.length).fill(undefined) as (Count | undefined)[]); + let countFiltersToFetch: NotificationFilter[] = []; + if (notification) { + // eslint-disable-next-line no-plusplus + for (let i = 0; i < existingCounts.length; i++) { + const filter = filters[i]; + if (areTagsEqual(filter.tags, notification.tags)) { + countFiltersToFetch.push(filter); + } + } + } else { + countFiltersToFetch = filters; + } + + if (countFiltersToFetch.length === 0) { + return; + } + + setIsFetching(true); + const countsRes = await notifications.count({ filters: countFiltersToFetch }); + setIsFetching(false); + setIsLoading(false); + if (countsRes.error) { + setError(countsRes.error); + onError?.(countsRes.error); + + return; + } + const data = countsRes.data!; + onSuccess?.(data.counts); + + setCounts((oldCounts) => { + const newCounts: Count[] = []; + const countsReceived = data.counts; + + // eslint-disable-next-line no-plusplus + for (let i = 0; i < existingCounts.length; i++) { + const countReceived = countsReceived.find((c) => areTagsEqual(c.filter.tags, existingCounts[i]?.filter.tags)); + + newCounts.push(countReceived || oldCounts![i]); + } + + return newCounts; + }); + }; + + useWebSocketEvent({ + event: 'notifications.notification_received', + eventHandler: (data) => { + sync(data.result); + }, + }); + + useWebSocketEvent({ + event: 'notifications.unread_count_changed', + eventHandler: () => { + sync(); + }, + }); + + useEffect(() => { + setError(undefined); + setIsLoading(true); + setIsFetching(false); + sync(); + }, [JSON.stringify(filters)]); + + const refetch = async () => { + await sync(); + }; + + return { counts, error, refetch, isLoading, isFetching }; +};