forked from github/plane
refactor: notification pagination & made context for handling functions related to notification (#1654)
* refactor: add pagination and notification context * refactor: moved header & snooze option to their own files added check before toUpperCase function
This commit is contained in:
parent
10059b2150
commit
9c28011ea5
@ -1,3 +1,4 @@
|
|||||||
export * from "./notification-card";
|
export * from "./notification-card";
|
||||||
export * from "./notification-popover";
|
export * from "./notification-popover";
|
||||||
export * from "./select-snooze-till-modal";
|
export * from "./select-snooze-till-modal";
|
||||||
|
export * from "./notification-header";
|
||||||
|
@ -21,6 +21,8 @@ import {
|
|||||||
|
|
||||||
// type
|
// type
|
||||||
import type { IUserNotification } from "types";
|
import type { IUserNotification } from "types";
|
||||||
|
// constants
|
||||||
|
import { snoozeOptions } from "constants/notification";
|
||||||
|
|
||||||
type NotificationCardProps = {
|
type NotificationCardProps = {
|
||||||
notification: IUserNotification;
|
notification: IUserNotification;
|
||||||
@ -30,33 +32,6 @@ type NotificationCardProps = {
|
|||||||
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
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<NotificationCardProps> = (props) => {
|
export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
||||||
const {
|
const {
|
||||||
notification,
|
notification,
|
||||||
@ -101,7 +76,11 @@ export const NotificationCard: React.FC<NotificationCardProps> = (props) => {
|
|||||||
) : (
|
) : (
|
||||||
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
|
<div className="w-12 h-12 bg-custom-background-80 rounded-full flex justify-center items-center">
|
||||||
<span className="text-custom-text-100 font-medium text-lg">
|
<span className="text-custom-text-100 font-medium text-lg">
|
||||||
{notification.triggered_by_details.first_name[0].toUpperCase()}
|
{notification.triggered_by_details.first_name?.[0] ? (
|
||||||
|
notification.triggered_by_details.first_name?.[0]?.toUpperCase()
|
||||||
|
) : (
|
||||||
|
<Icon iconName="person" className="h-6 w-6" />
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
211
apps/app/components/notifications/notification-header.tsx
Normal file
211
apps/app/components/notifications/notification-header.tsx
Normal file
@ -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<React.SetStateAction<boolean>>;
|
||||||
|
setArchived: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setReadNotification: React.Dispatch<React.SetStateAction<boolean>>;
|
||||||
|
setSelectedTab: React.Dispatch<React.SetStateAction<NotificationType>>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const NotificationHeader: React.FC<NotificationHeaderProps> = (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 (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center justify-between px-5 pt-5">
|
||||||
|
<h2 className="text-xl font-semibold mb-2">Notifications</h2>
|
||||||
|
<div className="flex gap-x-4 justify-center items-center text-custom-text-200">
|
||||||
|
<Tooltip tooltipContent="Refresh">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
notificationMutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="refresh" className={`${isRefreshing ? "animate-spin" : ""}`} />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Unread notifications">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSnoozed(false);
|
||||||
|
setArchived(false);
|
||||||
|
setReadNotification((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="filter_list" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Snoozed notifications">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setArchived(false);
|
||||||
|
setReadNotification(false);
|
||||||
|
setSnoozed((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="schedule" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<Tooltip tooltipContent="Archived notifications">
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSnoozed(false);
|
||||||
|
setReadNotification(false);
|
||||||
|
setArchived((prev) => !prev);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Icon iconName="archive" />
|
||||||
|
</button>
|
||||||
|
</Tooltip>
|
||||||
|
<button type="button" onClick={() => closePopover()}>
|
||||||
|
<Icon iconName="close" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="border-b border-custom-border-300 w-full px-5 mt-5">
|
||||||
|
{snoozed || archived || readNotification ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSnoozed(false);
|
||||||
|
setArchived(false);
|
||||||
|
setReadNotification(false);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<h4 className="flex items-center gap-2 pb-4">
|
||||||
|
<Icon iconName="arrow_back" />
|
||||||
|
<span className="ml-2 font-medium">
|
||||||
|
{snoozed
|
||||||
|
? "Snoozed Notifications"
|
||||||
|
: readNotification
|
||||||
|
? "Unread Notifications"
|
||||||
|
: "Archived Notifications"}
|
||||||
|
</span>
|
||||||
|
</h4>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
|
||||||
|
{notificationTabs.map((tab) =>
|
||||||
|
tab.value === "created" ? (
|
||||||
|
isMember || isOwner ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => setSelectedTab(tab.value)}
|
||||||
|
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
|
: "border-transparent text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{tab.unreadCount && tab.unreadCount > 0 ? (
|
||||||
|
<span
|
||||||
|
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "bg-custom-primary-100 text-white"
|
||||||
|
: "bg-custom-background-80 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getNumberCount(tab.unreadCount)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
) : null
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
key={tab.value}
|
||||||
|
onClick={() => setSelectedTab(tab.value)}
|
||||||
|
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "border-custom-primary-100 text-custom-primary-100"
|
||||||
|
: "border-transparent text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{tab.label}
|
||||||
|
{tab.unreadCount && tab.unreadCount > 0 ? (
|
||||||
|
<span
|
||||||
|
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
||||||
|
tab.value === selectedTab
|
||||||
|
? "bg-custom-primary-100 text-white"
|
||||||
|
: "bg-custom-background-80 text-custom-text-200"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{getNumberCount(tab.unreadCount)}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</nav>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
@ -1,19 +1,20 @@
|
|||||||
import React, { Fragment } from "react";
|
import React, { Fragment } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useTheme from "hooks/use-theme";
|
import useTheme from "hooks/use-theme";
|
||||||
|
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
|
|
||||||
// hooks
|
// hooks
|
||||||
import useWorkspaceMembers from "hooks/use-workspace-members";
|
|
||||||
import useUserNotification from "hooks/use-user-notifications";
|
import useUserNotification from "hooks/use-user-notifications";
|
||||||
|
|
||||||
// components
|
// components
|
||||||
import { Icon, Loader, EmptyState, Tooltip } from "components/ui";
|
import { Loader, EmptyState, Tooltip } from "components/ui";
|
||||||
import { SnoozeNotificationModal, NotificationCard } from "components/notifications";
|
import {
|
||||||
|
SnoozeNotificationModal,
|
||||||
|
NotificationCard,
|
||||||
|
NotificationHeader,
|
||||||
|
} from "components/notifications";
|
||||||
// icons
|
// icons
|
||||||
import { NotificationsOutlined } from "@mui/icons-material";
|
import { NotificationsOutlined } from "@mui/icons-material";
|
||||||
// images
|
// images
|
||||||
@ -21,9 +22,6 @@ import emptyNotification from "public/empty-state/notification.svg";
|
|||||||
// helpers
|
// helpers
|
||||||
import { getNumberCount } from "helpers/string.helper";
|
import { getNumberCount } from "helpers/string.helper";
|
||||||
|
|
||||||
// type
|
|
||||||
import type { NotificationType } from "types";
|
|
||||||
|
|
||||||
export const NotificationPopover = () => {
|
export const NotificationPopover = () => {
|
||||||
const {
|
const {
|
||||||
notifications,
|
notifications,
|
||||||
@ -37,44 +35,21 @@ export const NotificationPopover = () => {
|
|||||||
setSelectedTab,
|
setSelectedTab,
|
||||||
setSnoozed,
|
setSnoozed,
|
||||||
snoozed,
|
snoozed,
|
||||||
notificationsMutate,
|
notificationMutate,
|
||||||
markNotificationArchivedStatus,
|
markNotificationArchivedStatus,
|
||||||
markNotificationReadStatus,
|
markNotificationReadStatus,
|
||||||
markSnoozeNotification,
|
markSnoozeNotification,
|
||||||
notificationCount,
|
notificationCount,
|
||||||
totalNotificationCount,
|
totalNotificationCount,
|
||||||
|
setSize,
|
||||||
|
isLoadingMore,
|
||||||
|
hasMore,
|
||||||
|
isRefreshing,
|
||||||
} = useUserNotification();
|
} = useUserNotification();
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const { workspaceSlug } = router.query;
|
|
||||||
|
|
||||||
const { isOwner, isMember } = useWorkspaceMembers(workspaceSlug?.toString() ?? "");
|
|
||||||
|
|
||||||
// theme context
|
// theme context
|
||||||
const { collapsed: sidebarCollapse } = useTheme();
|
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 (
|
return (
|
||||||
<>
|
<>
|
||||||
<SnoozeNotificationModal
|
<SnoozeNotificationModal
|
||||||
@ -87,7 +62,6 @@ export const NotificationPopover = () => {
|
|||||||
) || null
|
) || null
|
||||||
}
|
}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
notificationsMutate();
|
|
||||||
setSelectedNotificationForSnooze(null);
|
setSelectedNotificationForSnooze(null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@ -126,159 +100,72 @@ export const NotificationPopover = () => {
|
|||||||
leaveTo="opacity-0 translate-y-1"
|
leaveTo="opacity-0 translate-y-1"
|
||||||
>
|
>
|
||||||
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[50vh] border border-custom-border-300 shadow-lg rounded-xl">
|
<Popover.Panel className="absolute bg-custom-background-100 flex flex-col left-0 md:left-full ml-8 z-10 top-0 md:w-[36rem] w-[20rem] h-[50vh] border border-custom-border-300 shadow-lg rounded-xl">
|
||||||
<div className="flex items-center justify-between px-5 pt-5">
|
<NotificationHeader
|
||||||
<h2 className="text-xl font-semibold mb-2">Notifications</h2>
|
notificationCount={notificationCount}
|
||||||
<div className="flex gap-x-4 justify-center items-center text-custom-text-200">
|
notificationMutate={notificationMutate}
|
||||||
<Tooltip tooltipContent="Refresh">
|
closePopover={closePopover}
|
||||||
<button
|
isRefreshing={isRefreshing}
|
||||||
type="button"
|
snoozed={snoozed}
|
||||||
onClick={(e) => {
|
archived={archived}
|
||||||
notificationsMutate();
|
readNotification={readNotification}
|
||||||
|
selectedTab={selectedTab}
|
||||||
const target = e.target as HTMLButtonElement;
|
setSnoozed={setSnoozed}
|
||||||
target?.classList.add("animate-spin");
|
setArchived={setArchived}
|
||||||
setTimeout(() => {
|
setReadNotification={setReadNotification}
|
||||||
target?.classList.remove("animate-spin");
|
setSelectedTab={setSelectedTab}
|
||||||
}, 1000);
|
/>
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="refresh" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip tooltipContent="Unread notifications">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSnoozed(false);
|
|
||||||
setArchived(false);
|
|
||||||
setReadNotification((prev) => !prev);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="filter_list" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip tooltipContent="Snoozed notifications">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setArchived(false);
|
|
||||||
setReadNotification(false);
|
|
||||||
setSnoozed((prev) => !prev);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="schedule" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<Tooltip tooltipContent="Archived notifications">
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSnoozed(false);
|
|
||||||
setReadNotification(false);
|
|
||||||
setArchived((prev) => !prev);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Icon iconName="archive" />
|
|
||||||
</button>
|
|
||||||
</Tooltip>
|
|
||||||
<button type="button" onClick={() => closePopover()}>
|
|
||||||
<Icon iconName="close" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="border-b border-custom-border-300 w-full px-5 mt-5">
|
|
||||||
{snoozed || archived || readNotification ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSnoozed(false);
|
|
||||||
setArchived(false);
|
|
||||||
setReadNotification(false);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<h4 className="flex items-center gap-2 pb-4">
|
|
||||||
<Icon iconName="arrow_back" />
|
|
||||||
<span className="ml-2 font-medium">
|
|
||||||
{snoozed
|
|
||||||
? "Snoozed Notifications"
|
|
||||||
: readNotification
|
|
||||||
? "Unread Notifications"
|
|
||||||
: "Archived Notifications"}
|
|
||||||
</span>
|
|
||||||
</h4>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<nav className="flex space-x-5 overflow-x-auto" aria-label="Tabs">
|
|
||||||
{notificationTabs.map((tab) =>
|
|
||||||
tab.value === "created" ? (
|
|
||||||
isMember || isOwner ? (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
key={tab.value}
|
|
||||||
onClick={() => setSelectedTab(tab.value)}
|
|
||||||
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium outline-none ${
|
|
||||||
tab.value === selectedTab
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
|
||||||
: "border-transparent text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
{tab.unreadCount && tab.unreadCount > 0 ? (
|
|
||||||
<span
|
|
||||||
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
|
||||||
tab.value === selectedTab
|
|
||||||
? "bg-custom-primary-100 text-white"
|
|
||||||
: "bg-custom-background-80 text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getNumberCount(tab.unreadCount)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
) : null
|
|
||||||
) : (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
key={tab.value}
|
|
||||||
onClick={() => setSelectedTab(tab.value)}
|
|
||||||
className={`whitespace-nowrap border-b-2 pb-4 px-1 text-sm font-medium ${
|
|
||||||
tab.value === selectedTab
|
|
||||||
? "border-custom-primary-100 text-custom-primary-100"
|
|
||||||
: "border-transparent text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{tab.label}
|
|
||||||
{tab.unreadCount && tab.unreadCount > 0 ? (
|
|
||||||
<span
|
|
||||||
className={`ml-2 rounded-full text-xs px-2 py-0.5 ${
|
|
||||||
tab.value === selectedTab
|
|
||||||
? "bg-custom-primary-100 text-white"
|
|
||||||
: "bg-custom-background-80 text-custom-text-200"
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{getNumberCount(tab.unreadCount)}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</nav>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{notifications ? (
|
{notifications ? (
|
||||||
notifications.length > 0 ? (
|
notifications.length > 0 ? (
|
||||||
<div className="divide-y divide-custom-border-100 overflow-y-auto h-full">
|
<div className="h-full overflow-y-auto">
|
||||||
{notifications.map((notification) => (
|
<div className="divide-y divide-custom-border-100">
|
||||||
<NotificationCard
|
{notifications.map((notification) => (
|
||||||
key={notification.id}
|
<NotificationCard
|
||||||
notification={notification}
|
key={notification.id}
|
||||||
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
notification={notification}
|
||||||
markNotificationReadStatus={markNotificationReadStatus}
|
markNotificationArchivedStatus={markNotificationArchivedStatus}
|
||||||
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
markNotificationReadStatus={markNotificationReadStatus}
|
||||||
markSnoozeNotification={markSnoozeNotification}
|
setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
|
||||||
/>
|
markSnoozeNotification={markSnoozeNotification}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
{isLoadingMore && (
|
||||||
|
<div className="mt-6 flex justify-center items-center text-sm">
|
||||||
|
<div role="status">
|
||||||
|
<svg
|
||||||
|
aria-hidden="true"
|
||||||
|
className="mr-2 h-6 w-6 animate-spin fill-blue-600 text-custom-text-200"
|
||||||
|
viewBox="0 0 100 101"
|
||||||
|
fill="none"
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M100 50.5908C100 78.2051 77.6142 100.591 50 100.591C22.3858 100.591 0 78.2051 0 50.5908C0 22.9766 22.3858 0.59082 50 0.59082C77.6142 0.59082 100 22.9766 100 50.5908ZM9.08144 50.5908C9.08144 73.1895 27.4013 91.5094 50 91.5094C72.5987 91.5094 90.9186 73.1895 90.9186 50.5908C90.9186 27.9921 72.5987 9.67226 50 9.67226C27.4013 9.67226 9.08144 27.9921 9.08144 50.5908Z"
|
||||||
|
fill="currentColor"
|
||||||
|
/>
|
||||||
|
<path
|
||||||
|
d="M93.9676 39.0409C96.393 38.4038 97.8624 35.9116 97.0079 33.5539C95.2932 28.8227 92.871 24.3692 89.8167 20.348C85.8452 15.1192 80.8826 10.7238 75.2124 7.41289C69.5422 4.10194 63.2754 1.94025 56.7698 1.05124C51.7666 0.367541 46.6976 0.446843 41.7345 1.27873C39.2613 1.69328 37.813 4.19778 38.4501 6.62326C39.0873 9.04874 41.5694 10.4717 44.0505 10.1071C47.8511 9.54855 51.7191 9.52689 55.5402 10.0491C60.8642 10.7766 65.9928 12.5457 70.6331 15.2552C75.2735 17.9648 79.3347 21.5619 82.5849 25.841C84.9175 28.9121 86.7997 32.2913 88.1811 35.8758C89.083 38.2158 91.5421 39.6781 93.9676 39.0409Z"
|
||||||
|
fill="currentFill"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
<span className="sr-only">Loading...</span>
|
||||||
|
</div>
|
||||||
|
<p>Loading notifications</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{hasMore && !isLoadingMore && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="text-custom-primary-100 mt-6 flex justify-center items-center w-full text-sm font-medium"
|
||||||
|
disabled={isLoadingMore}
|
||||||
|
onClick={() => {
|
||||||
|
setSize((prev) => prev + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Load More
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="grid h-full w-full place-items-center overflow-hidden scale-75">
|
<div className="grid h-full w-full place-items-center overflow-hidden scale-75">
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { objToQueryParams } from "helpers/string.helper";
|
||||||
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
|
import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "types";
|
||||||
|
|
||||||
const paramsToKey = (params: any) => {
|
const paramsToKey = (params: any) => {
|
||||||
@ -230,16 +231,42 @@ export const USER_WORKSPACE_NOTIFICATIONS = (
|
|||||||
) => {
|
) => {
|
||||||
const { type, snoozed, archived, read } = params;
|
const { type, snoozed, archived, read } = params;
|
||||||
|
|
||||||
return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug.toUpperCase()}_TYPE_${(
|
return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug?.toUpperCase()}_TYPE_${(
|
||||||
type ?? "assigned"
|
type ?? "assigned"
|
||||||
).toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`;
|
)?.toUpperCase()}_SNOOZED_${snoozed}_ARCHIVED_${archived}_READ_${read}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const USER_WORKSPACE_NOTIFICATIONS_DETAILS = (
|
export const USER_WORKSPACE_NOTIFICATIONS_DETAILS = (
|
||||||
workspaceSlug: string,
|
workspaceSlug: string,
|
||||||
notificationId: 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) =>
|
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,
|
||||||
|
})}`;
|
||||||
|
};
|
||||||
|
26
apps/app/constants/notification.ts
Normal file
26
apps/app/constants/notification.ts
Normal file
@ -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,
|
||||||
|
},
|
||||||
|
];
|
308
apps/app/contexts/user-notification-context.tsx
Normal file
308
apps/app/contexts/user-notification-context.tsx
Normal file
@ -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<ContextType>({} 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<ContextType>;
|
||||||
|
};
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<userNotificationContext.Provider
|
||||||
|
value={{
|
||||||
|
...state,
|
||||||
|
notifications,
|
||||||
|
notificationCount,
|
||||||
|
setSelectedTab,
|
||||||
|
setSnoozed,
|
||||||
|
setArchived,
|
||||||
|
setReadNotification,
|
||||||
|
setSelectedNotificationForSnooze,
|
||||||
|
markNotificationReadStatus,
|
||||||
|
markNotificationArchivedStatus,
|
||||||
|
markSnoozeNotification,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</userNotificationContext.Provider>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default UserNotificationContextProvider;
|
@ -112,3 +112,13 @@ export const getNumberCount = (number: number): string => {
|
|||||||
}
|
}
|
||||||
return number.toString();
|
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();
|
||||||
|
};
|
||||||
|
@ -1,18 +1,22 @@
|
|||||||
import { useState } from "react";
|
import { useMemo, useState } from "react";
|
||||||
|
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
// swr
|
// swr
|
||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import useSWRInfinite from "swr/infinite";
|
||||||
|
|
||||||
// services
|
// services
|
||||||
import userNotificationServices from "services/notifications.service";
|
import userNotificationServices from "services/notifications.service";
|
||||||
|
|
||||||
// fetch-keys
|
// fetch-keys
|
||||||
import { UNREAD_NOTIFICATIONS_COUNT, USER_WORKSPACE_NOTIFICATIONS } from "constants/fetch-keys";
|
import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "constants/fetch-keys";
|
||||||
|
|
||||||
// type
|
// type
|
||||||
import type { NotificationType } from "types";
|
import type { NotificationType, NotificationCount } from "types";
|
||||||
|
|
||||||
|
// TODO: change to 100
|
||||||
|
const PER_PAGE = 2;
|
||||||
|
|
||||||
const useUserNotification = () => {
|
const useUserNotification = () => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@ -26,20 +30,40 @@ const useUserNotification = () => {
|
|||||||
);
|
);
|
||||||
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
|
const [selectedTab, setSelectedTab] = useState<NotificationType>("assigned");
|
||||||
|
|
||||||
const params = {
|
const params = useMemo(
|
||||||
type: snoozed || archived || readNotification ? undefined : selectedTab,
|
() => ({
|
||||||
snoozed,
|
type: snoozed || archived || readNotification ? undefined : selectedTab,
|
||||||
archived,
|
snoozed,
|
||||||
read: !readNotification ? undefined : false,
|
archived,
|
||||||
};
|
read: !readNotification ? null : false,
|
||||||
|
per_page: PER_PAGE,
|
||||||
const { data: notifications, mutate: notificationsMutate } = useSWR(
|
}),
|
||||||
workspaceSlug ? USER_WORKSPACE_NOTIFICATIONS(workspaceSlug.toString(), params) : null,
|
[archived, readNotification, selectedTab, snoozed]
|
||||||
workspaceSlug
|
|
||||||
? () => userNotificationServices.getUserNotifications(workspaceSlug.toString(), params)
|
|
||||||
: null
|
|
||||||
);
|
);
|
||||||
|
|
||||||
|
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(
|
const { data: notificationCount, mutate: mutateNotificationCount } = useSWR(
|
||||||
workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null,
|
workspaceSlug ? UNREAD_NOTIFICATIONS_COUNT(workspaceSlug.toString()) : null,
|
||||||
() =>
|
() =>
|
||||||
@ -48,55 +72,114 @@ const useUserNotification = () => {
|
|||||||
: null
|
: 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) => {
|
const markNotificationReadStatus = async (notificationId: string) => {
|
||||||
if (!workspaceSlug) return;
|
if (!workspaceSlug) return;
|
||||||
|
|
||||||
const isRead =
|
const isRead =
|
||||||
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
|
notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
|
||||||
|
|
||||||
|
handleReadMutation(isRead ? "unread" : "read");
|
||||||
|
mutateNotification(notificationId, { read_at: isRead ? null : new Date() });
|
||||||
|
|
||||||
if (isRead) {
|
if (isRead) {
|
||||||
notificationsMutate(
|
|
||||||
(prev) =>
|
|
||||||
prev?.map((prevNotification) => {
|
|
||||||
if (prevNotification.id === notificationId) {
|
|
||||||
return {
|
|
||||||
...prevNotification,
|
|
||||||
read_at: null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return prevNotification;
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.markUserNotificationAsUnread(workspaceSlug.toString(), notificationId)
|
.markUserNotificationAsUnread(workspaceSlug.toString(), notificationId)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
throw new Error("Something went wrong");
|
throw new Error("Something went wrong");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
|
||||||
mutateNotificationCount();
|
mutateNotificationCount();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notificationsMutate(
|
|
||||||
(prev) =>
|
|
||||||
prev?.map((prevNotification) => {
|
|
||||||
if (prevNotification.id === notificationId) {
|
|
||||||
return {
|
|
||||||
...prevNotification,
|
|
||||||
read_at: new Date(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return prevNotification;
|
|
||||||
}),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
|
.markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
throw new Error("Something went wrong");
|
throw new Error("Something went wrong");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
|
||||||
mutateNotificationCount();
|
mutateNotificationCount();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -108,6 +191,15 @@ const useUserNotification = () => {
|
|||||||
notifications?.find((notification) => notification.id === notificationId)?.archived_at !==
|
notifications?.find((notification) => notification.id === notificationId)?.archived_at !==
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
if (!isArchived) {
|
||||||
|
handleReadMutation("read");
|
||||||
|
removeNotification(notificationId);
|
||||||
|
} else {
|
||||||
|
if (archived) {
|
||||||
|
removeNotification(notificationId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (isArchived) {
|
if (isArchived) {
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId)
|
.markUserNotificationAsUnarchived(workspaceSlug.toString(), notificationId)
|
||||||
@ -115,21 +207,17 @@ const useUserNotification = () => {
|
|||||||
throw new Error("Something went wrong");
|
throw new Error("Something went wrong");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
notificationMutate();
|
||||||
mutateNotificationCount();
|
mutateNotificationCount();
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notificationsMutate(
|
|
||||||
(prev) => prev?.filter((prevNotification) => prevNotification.id !== notificationId),
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.markUserNotificationAsArchived(workspaceSlug.toString(), notificationId)
|
.markUserNotificationAsArchived(workspaceSlug.toString(), notificationId)
|
||||||
.catch(() => {
|
.catch(() => {
|
||||||
throw new Error("Something went wrong");
|
throw new Error("Something went wrong");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
notificationMutate();
|
||||||
mutateNotificationCount();
|
mutateNotificationCount();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@ -142,20 +230,17 @@ const useUserNotification = () => {
|
|||||||
notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !==
|
notifications?.find((notification) => notification.id === notificationId)?.snoozed_till !==
|
||||||
null;
|
null;
|
||||||
|
|
||||||
|
mutateNotification(notificationId, { snoozed_till: isSnoozed ? null : dateTime });
|
||||||
|
|
||||||
if (isSnoozed) {
|
if (isSnoozed) {
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.patchUserNotification(workspaceSlug.toString(), notificationId, {
|
.patchUserNotification(workspaceSlug.toString(), notificationId, {
|
||||||
snoozed_till: null,
|
snoozed_till: null,
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
notificationMutate();
|
||||||
mutateNotificationCount();
|
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
notificationsMutate(
|
|
||||||
(prevData) => prevData?.filter((prev) => prev.id !== notificationId) || [],
|
|
||||||
false
|
|
||||||
);
|
|
||||||
await userNotificationServices
|
await userNotificationServices
|
||||||
.patchUserNotification(workspaceSlug.toString(), notificationId, {
|
.patchUserNotification(workspaceSlug.toString(), notificationId, {
|
||||||
snoozed_till: dateTime,
|
snoozed_till: dateTime,
|
||||||
@ -164,15 +249,14 @@ const useUserNotification = () => {
|
|||||||
new Error("Something went wrong");
|
new Error("Something went wrong");
|
||||||
})
|
})
|
||||||
.finally(() => {
|
.finally(() => {
|
||||||
notificationsMutate();
|
notificationMutate();
|
||||||
mutateNotificationCount();
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
notifications,
|
notifications,
|
||||||
notificationsMutate,
|
notificationMutate,
|
||||||
markNotificationReadStatus,
|
markNotificationReadStatus,
|
||||||
markNotificationArchivedStatus,
|
markNotificationArchivedStatus,
|
||||||
markSnoozeNotification,
|
markSnoozeNotification,
|
||||||
@ -193,6 +277,11 @@ const useUserNotification = () => {
|
|||||||
: null,
|
: null,
|
||||||
notificationCount,
|
notificationCount,
|
||||||
mutateNotificationCount,
|
mutateNotificationCount,
|
||||||
|
setSize,
|
||||||
|
isLoading,
|
||||||
|
isLoadingMore,
|
||||||
|
hasMore,
|
||||||
|
isRefreshing,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -4,7 +4,12 @@ import APIService from "services/api.service";
|
|||||||
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
const { NEXT_PUBLIC_API_BASE_URL } = process.env;
|
||||||
|
|
||||||
// types
|
// types
|
||||||
import { IUserNotification, INotificationParams } from "types";
|
import type {
|
||||||
|
IUserNotification,
|
||||||
|
INotificationParams,
|
||||||
|
NotificationCount,
|
||||||
|
PaginatedUserNotification,
|
||||||
|
} from "types";
|
||||||
|
|
||||||
class UserNotificationsServices extends APIService {
|
class UserNotificationsServices extends APIService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@ -152,17 +157,21 @@ class UserNotificationsServices extends APIService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async getUnreadNotificationsCount(workspaceSlug: string): Promise<{
|
async getUnreadNotificationsCount(workspaceSlug: string): Promise<NotificationCount> {
|
||||||
created_issues: number;
|
|
||||||
my_issues: number;
|
|
||||||
watching_issues: number;
|
|
||||||
}> {
|
|
||||||
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`)
|
return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`)
|
||||||
.then((response) => response?.data)
|
.then((response) => response?.data)
|
||||||
.catch((error) => {
|
.catch((error) => {
|
||||||
throw error?.response?.data;
|
throw error?.response?.data;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async getNotifications(url: string): Promise<PaginatedUserNotification> {
|
||||||
|
return this.get(url)
|
||||||
|
.then((response) => response?.data)
|
||||||
|
.catch((error) => {
|
||||||
|
throw error?.response?.data;
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const userNotificationServices = new UserNotificationsServices();
|
const userNotificationServices = new UserNotificationsServices();
|
||||||
|
17
apps/app/types/notifications.d.ts
vendored
17
apps/app/types/notifications.d.ts
vendored
@ -1,5 +1,16 @@
|
|||||||
import type { IUserLite } from "./users";
|
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 {
|
export interface IUserNotification {
|
||||||
id: string;
|
id: string;
|
||||||
created_at: Date;
|
created_at: Date;
|
||||||
@ -54,3 +65,9 @@ export interface INotificationParams {
|
|||||||
archived?: boolean;
|
archived?: boolean;
|
||||||
read?: boolean;
|
read?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NotificationCount = {
|
||||||
|
created_issues: number;
|
||||||
|
my_issues: number;
|
||||||
|
watching_issues: number;
|
||||||
|
};
|
||||||
|
Loading…
Reference in New Issue
Block a user