diff --git a/apps/app/components/notifications/index.ts b/apps/app/components/notifications/index.ts index 09255f92b..99667be22 100644 --- a/apps/app/components/notifications/index.ts +++ b/apps/app/components/notifications/index.ts @@ -1,3 +1,4 @@ export * from "./notification-card"; export * from "./notification-popover"; export * from "./select-snooze-till-modal"; +export * from "./notification-header"; diff --git a/apps/app/components/notifications/notification-card.tsx b/apps/app/components/notifications/notification-card.tsx index 321e9694c..71c2d676a 100644 --- a/apps/app/components/notifications/notification-card.tsx +++ b/apps/app/components/notifications/notification-card.tsx @@ -21,6 +21,8 @@ import { // type import type { IUserNotification } from "types"; +// constants +import { snoozeOptions } from "constants/notification"; type NotificationCardProps = { notification: IUserNotification; @@ -30,33 +32,6 @@ type NotificationCardProps = { markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise; }; -const snoozeOptions = [ - { - label: "1 day", - value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), - }, - { - label: "3 days", - value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000), - }, - { - label: "5 days", - value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000), - }, - { - label: "1 week", - value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000), - }, - { - label: "2 weeks", - value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000), - }, - { - label: "Custom", - value: null, - }, -]; - export const NotificationCard: React.FC = (props) => { const { notification, @@ -101,7 +76,11 @@ export const NotificationCard: React.FC = (props) => { ) : (
- {notification.triggered_by_details.first_name[0].toUpperCase()} + {notification.triggered_by_details.first_name?.[0] ? ( + notification.triggered_by_details.first_name?.[0]?.toUpperCase() + ) : ( + + )}
)} diff --git a/apps/app/components/notifications/notification-header.tsx b/apps/app/components/notifications/notification-header.tsx new file mode 100644 index 000000000..388668321 --- /dev/null +++ b/apps/app/components/notifications/notification-header.tsx @@ -0,0 +1,211 @@ +import React from "react"; + +import { useRouter } from "next/router"; + +// hooks +import useWorkspaceMembers from "hooks/use-workspace-members"; + +// components +import { Icon, Tooltip } from "components/ui"; +// helpers +import { getNumberCount } from "helpers/string.helper"; + +// type +import type { NotificationType, NotificationCount } from "types"; + +type NotificationHeaderProps = { + notificationCount?: NotificationCount | null; + notificationMutate: () => void; + closePopover: () => void; + isRefreshing?: boolean; + snoozed: boolean; + archived: boolean; + readNotification: boolean; + selectedTab: NotificationType; + setSnoozed: React.Dispatch>; + setArchived: React.Dispatch>; + setReadNotification: React.Dispatch>; + setSelectedTab: React.Dispatch>; +}; + +export const NotificationHeader: React.FC = (props) => { + const { + notificationCount, + notificationMutate, + closePopover, + isRefreshing, + snoozed, + archived, + readNotification, + selectedTab, + setSnoozed, + setArchived, + setReadNotification, + setSelectedTab, + } = props; + + const router = useRouter(); + const { workspaceSlug } = router.query; + + const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); + + const notificationTabs: Array<{ + label: string; + value: NotificationType; + unreadCount?: number; + }> = [ + { + label: "My Issues", + value: "assigned", + unreadCount: notificationCount?.my_issues, + }, + { + label: "Created by me", + value: "created", + unreadCount: notificationCount?.created_issues, + }, + { + label: "Subscribed", + value: "watching", + unreadCount: notificationCount?.watching_issues, + }, + ]; + + return ( + <> +
+

Notifications

+
+ + + + + + + + + + + + + +
+
+
+ {snoozed || archived || readNotification ? ( + + ) : ( + + )} +
+ + ); +}; diff --git a/apps/app/components/notifications/notification-popover.tsx b/apps/app/components/notifications/notification-popover.tsx index 860dc4f60..ddddbfc6e 100644 --- a/apps/app/components/notifications/notification-popover.tsx +++ b/apps/app/components/notifications/notification-popover.tsx @@ -1,19 +1,20 @@ import React, { Fragment } from "react"; -import { useRouter } from "next/router"; - // hooks import useTheme from "hooks/use-theme"; import { Popover, Transition } from "@headlessui/react"; // hooks -import useWorkspaceMembers from "hooks/use-workspace-members"; import useUserNotification from "hooks/use-user-notifications"; // components -import { Icon, Loader, EmptyState, Tooltip } from "components/ui"; -import { SnoozeNotificationModal, NotificationCard } from "components/notifications"; +import { Loader, EmptyState, Tooltip } from "components/ui"; +import { + SnoozeNotificationModal, + NotificationCard, + NotificationHeader, +} from "components/notifications"; // icons import { NotificationsOutlined } from "@mui/icons-material"; // images @@ -21,9 +22,6 @@ import emptyNotification from "public/empty-state/notification.svg"; // helpers import { getNumberCount } from "helpers/string.helper"; -// type -import type { NotificationType } from "types"; - export const NotificationPopover = () => { const { notifications, @@ -37,44 +35,21 @@ export const NotificationPopover = () => { setSelectedTab, setSnoozed, snoozed, - notificationsMutate, + notificationMutate, markNotificationArchivedStatus, markNotificationReadStatus, markSnoozeNotification, notificationCount, totalNotificationCount, + setSize, + isLoadingMore, + hasMore, + isRefreshing, } = useUserNotification(); - const router = useRouter(); - const { workspaceSlug } = router.query; - - const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? ""); - // theme context const { collapsed: sidebarCollapse } = useTheme(); - const notificationTabs: Array<{ - label: string; - value: NotificationType; - unreadCount?: number; - }> = [ - { - label: "My Issues", - value: "assigned", - unreadCount: notificationCount?.my_issues, - }, - { - label: "Created by me", - value: "created", - unreadCount: notificationCount?.created_issues, - }, - { - label: "Subscribed", - value: "watching", - unreadCount: notificationCount?.watching_issues, - }, - ]; - return ( <> { ) || null } onSuccess={() => { - notificationsMutate(); setSelectedNotificationForSnooze(null); }} /> @@ -126,159 +100,72 @@ export const NotificationPopover = () => { leaveTo="opacity-0 translate-y-1" > -
-

Notifications

-
- - - - - - - - - - - - - -
-
-
- {snoozed || archived || readNotification ? ( - - ) : ( - - )} -
+ {notifications ? ( notifications.length > 0 ? ( -
- {notifications.map((notification) => ( - - ))} +
+
+ {notifications.map((notification) => ( + + ))} +
+ {isLoadingMore && ( +
+
+ + Loading... +
+

Loading notifications

+
+ )} + {hasMore && !isLoadingMore && ( + + )}
) : (
diff --git a/apps/app/constants/fetch-keys.ts b/apps/app/constants/fetch-keys.ts index 8928c2236..f52ecc2e0 100644 --- a/apps/app/constants/fetch-keys.ts +++ b/apps/app/constants/fetch-keys.ts @@ -1,3 +1,4 @@ +import { objToQueryParams } from "helpers/string.helper"; import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types"; const paramsToKey = (params: any) => { @@ -230,16 +231,42 @@ export const USER_WORKSPACE_NOTIFICATIONS = ( ) => { const { type, snoozed, archived, read } = params; - return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug.toUpperCase()}_TYPE_${( + return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug?.toUpperCase()}_TYPE_${( type ?? "assigned" - ).toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`; + )?.toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`; }; export const USER_WORKSPACE_NOTIFICATIONS_DETAILS = ( workspaceSlug: string, notificationId: string ) => - `USER_WORKSPACE_NOTIFICATIONS_DETAILS_${workspaceSlug.toUpperCase()}_${notificationId.toUpperCase()}`; + `USER_WORKSPACE_NOTIFICATIONS_DETAILS_${workspaceSlug?.toUpperCase()}_${notificationId?.toUpperCase()}`; export const UNREAD_NOTIFICATIONS_COUNT = (workspaceSlug: string) => - `UNREAD_NOTIFICATIONS_COUNT_${workspaceSlug.toUpperCase()}`; + `UNREAD_NOTIFICATIONS_COUNT_${workspaceSlug?.toUpperCase()}`; + +export const getPaginatedNotificationKey = ( + index: number, + prevData: any, + workspaceSlug: string, + params: any +) => { + if (prevData && !prevData?.results?.length) return null; + + if (index === 0) + return `/api/workspaces/${workspaceSlug}/users/notifications?${objToQueryParams({ + ...params, + // TODO: change to '100:0:0' + cursor: "2:0:0", + })}`; + + const cursor = prevData?.next_cursor; + const nextPageResults = prevData?.next_page_results; + + if (!nextPageResults) return null; + + return `/api/workspaces/${workspaceSlug}/users/notifications?${objToQueryParams({ + ...params, + cursor, + })}`; +}; diff --git a/apps/app/constants/notification.ts b/apps/app/constants/notification.ts new file mode 100644 index 000000000..b36df35dd --- /dev/null +++ b/apps/app/constants/notification.ts @@ -0,0 +1,26 @@ +export const snoozeOptions = [ + { + label: "1 day", + value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000), + }, + { + label: "3 days", + value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000), + }, + { + label: "5 days", + value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000), + }, + { + label: "1 week", + value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000), + }, + { + label: "2 weeks", + value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000), + }, + { + label: "Custom", + value: null, + }, +]; diff --git a/apps/app/contexts/user-notification-context.tsx b/apps/app/contexts/user-notification-context.tsx new file mode 100644 index 000000000..456773f8c --- /dev/null +++ b/apps/app/contexts/user-notification-context.tsx @@ -0,0 +1,308 @@ +import { createContext, useCallback, useEffect, useReducer } from "react"; + +import { useRouter } from "next/router"; + +import useSWR from "swr"; + +// services +import userNotificationServices from "services/notifications.service"; + +// fetch-keys +import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; + +// type +import type { NotificationType, NotificationCount, IUserNotification } from "types"; + +export const userNotificationContext = createContext({} as ContextType); + +type UserNotificationProps = { + selectedTab: NotificationType; + snoozed: boolean; + archived: boolean; + readNotification: boolean; + selectedNotificationForSnooze: string | null; +}; + +type ReducerActionType = { + type: + | "READ_NOTIFICATION_COUNT" + | "SET_SELECTED_TAB" + | "SET_SNOOZED" + | "SET_ARCHIVED" + | "SET_READ_NOTIFICATION" + | "SET_SELECTED_NOTIFICATION_FOR_SNOOZE" + | "SET_NOTIFICATIONS"; + payload?: Partial; +}; + +type ContextType = UserNotificationProps & { + notifications?: IUserNotification[]; + notificationCount?: NotificationCount | null; + setSelectedTab: (tab: NotificationType) => void; + setSnoozed: (snoozed: boolean) => void; + setArchived: (archived: boolean) => void; + setReadNotification: (readNotification: boolean) => void; + setSelectedNotificationForSnooze: (notificationId: string | null) => void; + markNotificationReadStatus: (notificationId: string) => void; + markNotificationArchivedStatus: (notificationId: string) => void; + markSnoozeNotification: (notificationId: string, dateTime?: Date) => void; +}; + +type StateType = { + selectedTab: NotificationType; + snoozed: boolean; + archived: boolean; + readNotification: boolean; + selectedNotificationForSnooze: string | null; +}; + +type ReducerFunctionType = (state: StateType, action: ReducerActionType) => StateType; + +export const initialState: StateType = { + selectedTab: "assigned", + snoozed: false, + archived: false, + readNotification: false, + selectedNotificationForSnooze: null, +}; + +export const reducer: ReducerFunctionType = (state, action) => { + const { type, payload } = action; + + switch (type) { + case "READ_NOTIFICATION_COUNT": + case "SET_SELECTED_TAB": + case "SET_SNOOZED": + case "SET_ARCHIVED": + case "SET_READ_NOTIFICATION": + case "SET_SELECTED_NOTIFICATION_FOR_SNOOZE": + case "SET_NOTIFICATIONS": { + return { ...state, ...payload }; + } + + default: + return state; + } +}; + +const UserNotificationContextProvider: React.FC<{ + children: React.ReactNode; +}> = ({ children }) => { + const router = useRouter(); + const { workspaceSlug } = router.query; + + const [state, dispatch] = useReducer(reducer, initialState); + + const { selectedTab, snoozed, archived, readNotification, selectedNotificationForSnooze } = state; + + const params = { + type: snoozed || archived || readNotification ? undefined : selectedTab, + snoozed, + archived, + read: !readNotification ? undefined : false, + }; + + const { data: notifications, mutate: notificationsMutate } = useSWR( + workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null, + workspaceSlug + ? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params) + : null + ); + + const { data: notificationCount, mutate: mutateNotificationCount } = useSWR( + workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null, + () => + workspaceSlug + ? userNotificationServices.getUnreadNotificationsCount(workspaceSlug.toString()) + : null + ); + + const handleReadMutation = (action: "read" | "unread") => { + const notificationCountNumber = action === "read" ? -1 : 1; + + mutateNotificationCount((prev) => { + if (!prev) return prev; + + const notificationType: keyof NotificationCount = + selectedTab === "assigned" + ? "my_issues" + : selectedTab === "created" + ? "created_issues" + : "watching_issues"; + + return { + ...prev, + [notificationType]: prev[notificationType] + notificationCountNumber, + }; + }, false); + }; + + const markNotificationReadStatus = async (notificationId: string) => { + if (!workspaceSlug) return; + const isRead = + notifications?.find((notification) => notification.id === notificationId)?.read_at !== null; + + notificationsMutate( + (previousNotifications) => + previousNotifications?.map((notification) => + notification.id === notificationId + ? { ...notification, read_at: isRead ? null : new Date() } + : notification + ), + false + ); + + handleReadMutation(isRead ? "unread" : "read"); + + if (isRead) { + await userNotificationServices + .markUserNotificationAsUnread(workspaceSlug.toString(), notificationId) + .catch(() => { + throw new Error("Something went wrong"); + }) + .finally(() => { + notificationsMutate(); + mutateNotificationCount(); + }); + } else { + await userNotificationServices + .markUserNotificationAsRead(workspaceSlug.toString(), notificationId) + .catch(() => { + throw new Error("Something went wrong"); + }) + .finally(() => { + notificationsMutate(); + mutateNotificationCount(); + }); + } + }; + + const markNotificationArchivedStatus = async (notificationId: string) => { + if (!workspaceSlug) return; + const isArchived = + notifications?.find((notification) => notification.id === notificationId)?.archived_at !== + null; + + if (!isArchived) { + handleReadMutation("read"); + } + + if (isArchived) { + await userNotificationServices + .markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId) + .catch(() => { + throw new Error("Something went wrong"); + }) + .finally(() => { + notificationsMutate(); + mutateNotificationCount(); + }); + } else { + notificationsMutate( + (prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId), + false + ); + await userNotificationServices + .markUserNotificationAsArchived(workspaceSlug.toString(), notificationId) + .catch(() => { + throw new Error("Something went wrong"); + }) + .finally(() => { + notificationsMutate(); + mutateNotificationCount(); + }); + } + }; + + const markSnoozeNotification = async (notificationId: string, dateTime?: Date) => { + if (!workspaceSlug) return; + + const isSnoozed = + notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !== + null; + + notificationsMutate( + (previousNotifications) => + previousNotifications?.map((notification) => + notification.id === notificationId + ? { ...notification, snoozed_till: isSnoozed ? null : new Date(dateTime!) } + : notification + ) || [], + false + ); + + if (isSnoozed) { + await userNotificationServices + .patchUserNotification(workspaceSlug.toString(), notificationId, { + snoozed_till: null, + }) + .finally(() => { + notificationsMutate(); + }); + } else { + await userNotificationServices + .patchUserNotification(workspaceSlug.toString(), notificationId, { + snoozed_till: dateTime, + }) + .catch(() => { + new Error("Something went wrong"); + }) + .finally(() => { + notificationsMutate(); + }); + } + }; + + const setSelectedTab = useCallback((tab: NotificationType) => { + dispatch({ type: "SET_SELECTED_TAB", payload: { selectedTab: tab } }); + }, []); + + const setSnoozed = useCallback((snoozed: boolean) => { + dispatch({ type: "SET_SNOOZED", payload: { snoozed } }); + }, []); + + const setArchived = useCallback((archived: boolean) => { + dispatch({ type: "SET_ARCHIVED", payload: { archived } }); + }, []); + + const setReadNotification = useCallback((readNotification: boolean) => { + dispatch({ type: "SET_READ_NOTIFICATION", payload: { readNotification } }); + }, []); + + const setSelectedNotificationForSnooze = useCallback((notificationId: string | null) => { + dispatch({ + type: "SET_SELECTED_NOTIFICATION_FOR_SNOOZE", + payload: { selectedNotificationForSnooze: notificationId }, + }); + }, []); + + useEffect(() => { + dispatch({ type: "SET_NOTIFICATIONS", payload: { notifications } }); + }, [notifications]); + + useEffect(() => { + dispatch({ type: "READ_NOTIFICATION_COUNT", payload: { notificationCount } }); + }, [notificationCount]); + + return ( + + {children} + + ); +}; + +export default UserNotificationContextProvider; diff --git a/apps/app/helpers/string.helper.ts b/apps/app/helpers/string.helper.ts index 82fb363cf..0fc84fda1 100644 --- a/apps/app/helpers/string.helper.ts +++ b/apps/app/helpers/string.helper.ts @@ -112,3 +112,13 @@ export const getNumberCount = (number: number): string => { } return number.toString(); }; + +export const objToQueryParams = (obj: any) => { + const params = new URLSearchParams(); + + for (const [key, value] of Object.entries(obj)) { + if (value !== undefined && value !== null) params.append(key, value as string); + } + + return params.toString(); +}; diff --git a/apps/app/hooks/use-user-notifications.tsx b/apps/app/hooks/use-user-notifications.tsx index 1a4d6d8a8..e7880dadf 100644 --- a/apps/app/hooks/use-user-notifications.tsx +++ b/apps/app/hooks/use-user-notifications.tsx @@ -1,18 +1,22 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useRouter } from "next/router"; // swr import useSWR from "swr"; +import useSWRInfinite from "swr/infinite"; // services import userNotificationServices from "services/notifications.service"; // fetch-keys -import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys"; +import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys"; // type -import type { NotificationType } from "types"; +import type { NotificationType, NotificationCount } from "types"; + +// TODO: change to 100 +const PER_PAGE = 2; const useUserNotification = () => { const router = useRouter(); @@ -26,20 +30,40 @@ const useUserNotification = () => { ); const [selectedTab, setSelectedTab] = useState("assigned"); - const params = { - type: snoozed || archived || readNotification ? undefined : selectedTab, - snoozed, - archived, - read: !readNotification ? undefined : false, - }; - - const { data: notifications, mutate: notificationsMutate } = useSWR( - workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null, - workspaceSlug - ? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params) - : null + const params = useMemo( + () => ({ + type: snoozed || archived || readNotification ? undefined : selectedTab, + snoozed, + archived, + read: !readNotification ? null : false, + per_page: PER_PAGE, + }), + [archived, readNotification, selectedTab, snoozed] ); + const { + data: paginatedData, + size, + setSize, + isLoading, + isValidating, + mutate: notificationMutate, + } = useSWRInfinite( + workspaceSlug + ? (index, prevData) => + getPaginatedNotificationKey(index, prevData, workspaceSlug.toString(), params) + : () => null, + async (url: string) => await userNotificationServices.getNotifications(url) + ); + + const isLoadingMore = + isLoading || (size > 0 && paginatedData && typeof paginatedData[size - 1] === "undefined"); + const isEmpty = paginatedData?.[0]?.results?.length === 0; + const notifications = paginatedData ? paginatedData.map((d) => d.results).flat() : undefined; + const hasMore = + isEmpty || (paginatedData && paginatedData[paginatedData.length - 1].next_page_results); + const isRefreshing = isValidating && paginatedData && paginatedData.length === size; + const { data: notificationCount, mutate: mutateNotificationCount } = useSWR( workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null, () => @@ -48,55 +72,114 @@ const useUserNotification = () => { : null ); + const handleReadMutation = (action: "read" | "unread") => { + const notificationCountNumber = action === "read" ? -1 : 1; + + mutateNotificationCount((prev) => { + if (!prev) return prev; + + const notificationType: keyof NotificationCount = + selectedTab === "assigned" + ? "my_issues" + : selectedTab === "created" + ? "created_issues" + : "watching_issues"; + + return { + ...prev, + [notificationType]: prev[notificationType] + notificationCountNumber, + }; + }, false); + }; + + const mutateNotification = (notificationId: string, value: Object) => { + notificationMutate((previousNotifications) => { + if (!previousNotifications) return previousNotifications; + + const notificationIndex = Math.floor( + previousNotifications + .map((d) => d.results) + .flat() + .findIndex((notification) => notification.id === notificationId) / PER_PAGE + ); + + let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex( + (notification) => notification.id === notificationId + ); + + if (notificationIndexInPage === -1) return previousNotifications; + + notificationIndexInPage = + notificationIndexInPage === -1 ? 0 : notificationIndexInPage % PER_PAGE; + + if (notificationIndex === -1) return previousNotifications; + + if (notificationIndexInPage === -1) return previousNotifications; + + const key = Object.keys(value)[0]; + (previousNotifications[notificationIndex].results[notificationIndexInPage] as any)[key] = ( + value as any + )[key]; + + return previousNotifications; + }, false); + }; + + const removeNotification = (notificationId: string) => { + notificationMutate((previousNotifications) => { + if (!previousNotifications) return previousNotifications; + + const notificationIndex = Math.floor( + previousNotifications + .map((d) => d.results) + .flat() + .findIndex((notification) => notification.id === notificationId) / PER_PAGE + ); + + let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex( + (notification) => notification.id === notificationId + ); + + if (notificationIndexInPage === -1) return previousNotifications; + + notificationIndexInPage = + notificationIndexInPage === -1 ? 0 : notificationIndexInPage % PER_PAGE; + + if (notificationIndex === -1) return previousNotifications; + + if (notificationIndexInPage === -1) return previousNotifications; + + previousNotifications[notificationIndex].results.splice(notificationIndexInPage, 1); + + return previousNotifications; + }, false); + }; + const markNotificationReadStatus = async (notificationId: string) => { if (!workspaceSlug) return; + const isRead = notifications?.find((notification) => notification.id === notificationId)?.read_at !== null; + handleReadMutation(isRead ? "unread" : "read"); + mutateNotification(notificationId, { read_at: isRead ? null : new Date() }); + if (isRead) { - notificationsMutate( - (prev) => - prev?.map((prevNotification) => { - if (prevNotification.id === notificationId) { - return { - ...prevNotification, - read_at: null, - }; - } - return prevNotification; - }), - false - ); await userNotificationServices .markUserNotificationAsUnread(workspaceSlug.toString(), notificationId) .catch(() => { throw new Error("Something went wrong"); }) .finally(() => { - notificationsMutate(); mutateNotificationCount(); }); } else { - notificationsMutate( - (prev) => - prev?.map((prevNotification) => { - if (prevNotification.id === notificationId) { - return { - ...prevNotification, - read_at: new Date(), - }; - } - return prevNotification; - }), - false - ); await userNotificationServices .markUserNotificationAsRead(workspaceSlug.toString(), notificationId) .catch(() => { throw new Error("Something went wrong"); }) .finally(() => { - notificationsMutate(); mutateNotificationCount(); }); } @@ -108,6 +191,15 @@ const useUserNotification = () => { notifications?.find((notification) => notification.id === notificationId)?.archived_at !== null; + if (!isArchived) { + handleReadMutation("read"); + removeNotification(notificationId); + } else { + if (archived) { + removeNotification(notificationId); + } + } + if (isArchived) { await userNotificationServices .markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId) @@ -115,21 +207,17 @@ const useUserNotification = () => { throw new Error("Something went wrong"); }) .finally(() => { - notificationsMutate(); + notificationMutate(); mutateNotificationCount(); }); } else { - notificationsMutate( - (prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId), - false - ); await userNotificationServices .markUserNotificationAsArchived(workspaceSlug.toString(), notificationId) .catch(() => { throw new Error("Something went wrong"); }) .finally(() => { - notificationsMutate(); + notificationMutate(); mutateNotificationCount(); }); } @@ -142,20 +230,17 @@ const useUserNotification = () => { notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !== null; + mutateNotification(notificationId, { snoozed_till: isSnoozed ? null : dateTime }); + if (isSnoozed) { await userNotificationServices .patchUserNotification(workspaceSlug.toString(), notificationId, { snoozed_till: null, }) .finally(() => { - notificationsMutate(); - mutateNotificationCount(); + notificationMutate(); }); } else { - notificationsMutate( - (prevData) => prevData?.filter((prev) => prev.id !== notificationId) || [], - false - ); await userNotificationServices .patchUserNotification(workspaceSlug.toString(), notificationId, { snoozed_till: dateTime, @@ -164,15 +249,14 @@ const useUserNotification = () => { new Error("Something went wrong"); }) .finally(() => { - notificationsMutate(); - mutateNotificationCount(); + notificationMutate(); }); } }; return { notifications, - notificationsMutate, + notificationMutate, markNotificationReadStatus, markNotificationArchivedStatus, markSnoozeNotification, @@ -193,6 +277,11 @@ const useUserNotification = () => { : null, notificationCount, mutateNotificationCount, + setSize, + isLoading, + isLoadingMore, + hasMore, + isRefreshing, }; }; diff --git a/apps/app/services/notifications.service.ts b/apps/app/services/notifications.service.ts index 6c6bec62d..a8652b777 100644 --- a/apps/app/services/notifications.service.ts +++ b/apps/app/services/notifications.service.ts @@ -4,7 +4,12 @@ import APIService from "services/api.service"; const { NEXT_PUBLIC_API_BASE_URL } = process.env; // types -import { IUserNotification, INotificationParams } from "types"; +import type { + IUserNotification, + INotificationParams, + NotificationCount, + PaginatedUserNotification, +} from "types"; class UserNotificationsServices extends APIService { constructor() { @@ -152,17 +157,21 @@ class UserNotificationsServices extends APIService { }); } - async getUnreadNotificationsCount(workspaceSlug: string): Promise<{ - created_issues: number; - my_issues: number; - watching_issues: number; - }> { + async getUnreadNotificationsCount(workspaceSlug: string): Promise { return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`) .then((response) => response?.data) .catch((error) => { throw error?.response?.data; }); } + + async getNotifications(url: string): Promise { + return this.get(url) + .then((response) => response?.data) + .catch((error) => { + throw error?.response?.data; + }); + } } const userNotificationServices = new UserNotificationsServices(); diff --git a/apps/app/types/notifications.d.ts b/apps/app/types/notifications.d.ts index b49c1c9be..adb4e469a 100644 --- a/apps/app/types/notifications.d.ts +++ b/apps/app/types/notifications.d.ts @@ -1,5 +1,16 @@ import type { IUserLite } from "./users"; +export interface PaginatedUserNotification { + next_cursor: string; + prev_cursor: string; + next_page_results: boolean; + prev_page_results: boolean; + count: number; + total_pages: number; + extra_stats: null; + results: IUserNotification[]; +} + export interface IUserNotification { id: string; created_at: Date; @@ -54,3 +65,9 @@ export interface INotificationParams { archived?: boolean; read?: boolean; } + +export type NotificationCount = { + created_issues: number; + my_issues: number; + watching_issues: number; +};