diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py
index 5af5d0a9a..2ef8ab511 100644
--- a/apiserver/plane/app/views/notification/base.py
+++ b/apiserver/plane/app/views/notification/base.py
@@ -43,8 +43,9 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
# Get query parameters
snoozed = request.GET.get("snoozed", "false")
archived = request.GET.get("archived", "false")
- read = request.GET.get("read", "true")
+ read = request.GET.get("read", None)
type = request.GET.get("type", "all")
+ q_filters = Q()
notifications = (
Notification.objects.filter(
@@ -74,8 +75,12 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
if read == "false":
notifications = notifications.filter(read_at__isnull=True)
+ if read == "true":
+ notifications = notifications.filter(read_at__isnull=False)
+
+ type = type.split(",")
# Subscribed issues
- if type == "watching":
+ if "subscribed" in type:
issue_ids = (
IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
@@ -97,35 +102,32 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
.filter(created=False, assigned=False)
.values_list("issue_id", flat=True)
)
- notifications = notifications.filter(
- entity_identifier__in=issue_ids,
- )
+ q_filters |= Q(entity_identifier__in=issue_ids)
# Assigned Issues
- if type == "assigned":
+ if "assigned" in type:
issue_ids = IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True)
- notifications = notifications.filter(
- entity_identifier__in=issue_ids
- )
+ q_filters |= Q(entity_identifier__in=issue_ids)
# Created issues
- if type == "created":
+ if "created" in type:
if WorkspaceMember.objects.filter(
workspace__slug=slug,
member=request.user,
role__lt=15,
is_active=True,
).exists():
- notifications = Notification.objects.none()
+ notifications = notifications.none()
else:
issue_ids = Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True)
- notifications = notifications.filter(
- entity_identifier__in=issue_ids
- )
+ q_filters |= Q(entity_identifier__in=issue_ids)
+
+ # Apply the combined Q object filters
+ notifications = notifications.filter(q_filters)
# Pagination
if request.GET.get("per_page", False) and request.GET.get(
@@ -200,11 +202,12 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug):
# Watching Issues Count
- watching_issues_count = Notification.objects.filter(
+ subscribed_issues_count = Notification.objects.filter(
workspace__slug=slug,
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
+ snoozed_till__isnull=True,
entity_identifier__in=IssueSubscriber.objects.filter(
workspace__slug=slug, subscriber_id=request.user.id
).values_list("issue_id", flat=True),
@@ -216,6 +219,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
+ snoozed_till__isnull=True,
entity_identifier__in=IssueAssignee.objects.filter(
workspace__slug=slug, assignee_id=request.user.id
).values_list("issue_id", flat=True),
@@ -227,6 +231,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
receiver_id=request.user.id,
read_at__isnull=True,
archived_at__isnull=True,
+ snoozed_till__isnull=True,
entity_identifier__in=Issue.objects.filter(
workspace__slug=slug, created_by=request.user
).values_list("pk", flat=True),
@@ -234,7 +239,7 @@ class UnreadNotificationEndpoint(BaseAPIView):
return Response(
{
- "watching_issues": watching_issues_count,
+ "subscribed_issues": subscribed_issues_count,
"my_issues": my_issues_count,
"created_issues": created_issues_count,
},
diff --git a/packages/types/src/enums.ts b/packages/types/src/enums.ts
index cc8575374..08949bd17 100644
--- a/packages/types/src/enums.ts
+++ b/packages/types/src/enums.ts
@@ -37,3 +37,10 @@ export enum EEstimateUpdateStages {
EDIT = "edit",
SWITCH = "switch",
}
+
+// workspace notifications
+export enum ENotificationFilterType {
+ CREATED = "created",
+ ASSIGNED = "assigned",
+ SUBSCRIBED = "subscribed",
+}
diff --git a/packages/types/src/index.d.ts b/packages/types/src/index.d.ts
index 25c2b255b..353aeaf08 100644
--- a/packages/types/src/index.d.ts
+++ b/packages/types/src/index.d.ts
@@ -19,7 +19,6 @@ export * from "./auth";
export * from "./calendar";
export * from "./instance";
export * from "./issues/base"; // TODO: Remove this after development and the refactor/mobx-store-issue branch is stable
-export * from "./notifications";
export * from "./reaction";
export * from "./view-props";
export * from "./waitlist";
@@ -28,3 +27,4 @@ export * from "./workspace-views";
export * from "./common";
export * from "./pragmatic";
export * from "./publish";
+export * from "./workspace-notifications";
diff --git a/packages/types/src/notifications.d.ts b/packages/types/src/notifications.d.ts
deleted file mode 100644
index d739b2309..000000000
--- a/packages/types/src/notifications.d.ts
+++ /dev/null
@@ -1,79 +0,0 @@
-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 {
- archived_at: string | null;
- created_at: string;
- created_by: null;
- data: Data;
- entity_identifier: string;
- entity_name: string;
- id: string;
- message: null;
- message_html: string;
- message_stripped: null;
- project: string;
- read_at: Date | null;
- receiver: string;
- sender: string;
- snoozed_till: Date | null;
- title: string;
- triggered_by: string;
- triggered_by_details: IUserLite;
- updated_at: Date;
- updated_by: null;
- workspace: string;
-}
-
-export interface Data {
- issue: INotificationIssueLite;
- issue_activity: {
- actor: string;
- field: string;
- id: string;
- issue_comment: string | null;
- new_value: string;
- old_value: string;
- verb: "created" | "updated";
- };
-}
-
-export interface INotificationIssueLite {
- id: string;
- name: string;
- identifier: string;
- state_name: string;
- sequence_id: number;
- state_group: string;
-}
-
-export type NotificationType = "created" | "assigned" | "watching" | "all";
-
-export interface INotificationParams {
- snoozed?: boolean;
- type?: NotificationType;
- archived?: boolean;
- read?: boolean;
-}
-
-export type NotificationCount = {
- created_issues: number;
- my_issues: number;
- watching_issues: number;
-};
-
-export interface IMarkAllAsReadPayload {
- archived?: boolean;
- snoozed?: boolean;
- type?: NotificationType;
-}
diff --git a/packages/types/src/workspace-notifications.d.ts b/packages/types/src/workspace-notifications.d.ts
new file mode 100644
index 000000000..95d93b890
--- /dev/null
+++ b/packages/types/src/workspace-notifications.d.ts
@@ -0,0 +1,90 @@
+import type { IUserLite } from "./users";
+import { ENotificationFilterType } from "./enums";
+
+// filters
+export type TNotificationFilter = {
+ type: {
+ [key in ENotificationFilterType]: boolean;
+ };
+ snoozed: boolean;
+ archived: boolean;
+ read: boolean;
+};
+
+// notification payload
+export type TNotificationIssueLite = {
+ id: string | undefined;
+ sequence_id: number | undefined;
+ identifier: string | undefined;
+ name: string | undefined;
+ state_name: string | undefined;
+ state_group: string | undefined;
+};
+
+export type TNotificationData = {
+ issue: TNotificationIssueLite | undefined;
+ issue_activity: {
+ id: string | undefined;
+ actor: string | undefined;
+ field: string | undefined;
+ issue_comment: string | undefined;
+ verb: "created" | "updated";
+ new_value: string | undefined;
+ old_value: string | undefined;
+ };
+};
+
+export type TNotification = {
+ id: string | undefined;
+ title: string | undefined;
+ data: TNotificationData | undefined;
+ entity_identifier: string | undefined;
+ entity_name: string | undefined;
+ message_html: string | undefined;
+ message: undefined;
+ message_stripped: undefined;
+ sender: string | undefined;
+ receiver: string | undefined;
+ triggered_by: string | undefined;
+ triggered_by_details: IUserLite | undefined;
+ read_at: string | undefined;
+ archived_at: string | undefined;
+ snoozed_till: string | undefined;
+ workspace: string | undefined;
+ project: string | undefined;
+ created_at: string | undefined;
+ updated_at: string | undefined;
+ created_by: string | undefined;
+ updated_by: string | undefined;
+};
+
+// notification paginated information
+export type TNotificationPaginatedInfoQueryParams = {
+ type?: string | undefined;
+ snoozed?: boolean;
+ archived?: boolean;
+ read?: boolean;
+ per_page?: number;
+ cursor?: string;
+};
+
+export type TNotificationPaginatedInfo = {
+ next_cursor: string | undefined;
+ prev_cursor: string | undefined;
+ next_page_results: boolean | undefined;
+ prev_page_results: boolean | undefined;
+ total_pages: number | undefined;
+ extra_stats: string | undefined;
+ count: number | undefined; // current paginated results count
+ total_count: number | undefined; // total available results count
+ results: TNotification[] | undefined;
+ grouped_by: string | undefined;
+ sub_grouped_by: string | undefined;
+};
+
+// notification count
+export type TUnreadNotificationsCount = {
+ created_issues: number | undefined;
+ my_issues: number | undefined;
+ subscribed_issues: number | undefined;
+};
diff --git a/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx
new file mode 100644
index 000000000..49303d361
--- /dev/null
+++ b/web/app/[workspaceSlug]/(projects)/notifications/layout.tsx
@@ -0,0 +1,15 @@
+"use client";
+
+// components
+import { NotificationsSidebar } from "@/components/workspace-notifications";
+
+export default function ProjectInboxIssuesLayout({ children }: { children: React.ReactNode }) {
+ return (
+
+ );
+}
diff --git a/web/app/[workspaceSlug]/(projects)/notifications/page.tsx b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx
new file mode 100644
index 000000000..6b8a90f16
--- /dev/null
+++ b/web/app/[workspaceSlug]/(projects)/notifications/page.tsx
@@ -0,0 +1,46 @@
+"use client";
+
+import { observer } from "mobx-react";
+import useSWR from "swr";
+// components
+import { PageHead } from "@/components/core";
+import { IssuePeekOverview } from "@/components/issues";
+// constants
+import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
+// hooks
+import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
+
+const WorkspaceDashboardPage = observer(() => {
+ // hooks
+ const { currentWorkspace } = useWorkspace();
+ const { notificationIdsByWorkspaceId, getNotifications } = useWorkspaceNotifications();
+ // derived values
+ const pageTitle = currentWorkspace?.name ? `${currentWorkspace?.name} - Notifications` : undefined;
+
+ // fetch workspace notifications
+ const notificationMutation =
+ currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
+ ? ENotificationLoader.MUTATION_LOADER
+ : ENotificationLoader.INIT_LOADER;
+ const notificationLoader =
+ currentWorkspace && notificationIdsByWorkspaceId(currentWorkspace.id)
+ ? ENotificationQueryParamType.CURRENT
+ : ENotificationQueryParamType.INIT;
+ useSWR(
+ currentWorkspace?.slug ? `WORKSPACE_NOTIFICATION` : null,
+ currentWorkspace?.slug
+ ? async () => getNotifications(currentWorkspace?.slug, notificationMutation, notificationLoader)
+ : null
+ );
+
+ return (
+ <>
+
+
+
+
+ >
+ );
+});
+
+export default WorkspaceDashboardPage;
diff --git a/web/core/components/common/breadcrumb-link.tsx b/web/core/components/common/breadcrumb-link.tsx
index 2768a5d71..c04888790 100644
--- a/web/core/components/common/breadcrumb-link.tsx
+++ b/web/core/components/common/breadcrumb-link.tsx
@@ -1,20 +1,22 @@
"use client";
+import { ReactNode } from "react";
import Link from "next/link";
import { Tooltip } from "@plane/ui";
import { usePlatformOS } from "@/hooks/use-platform-os";
type Props = {
- label?: string;
+ label?: string | ReactNode;
href?: string;
icon?: React.ReactNode | undefined;
+ disableTooltip?: boolean;
};
export const BreadcrumbLink: React.FC = (props) => {
- const { href, label, icon } = props;
+ const { href, label, icon, disableTooltip = false } = props;
const { isMobile } = usePlatformOS();
return (
-
+
{href ? (
@@ -25,7 +27,9 @@ export const BreadcrumbLink: React.FC
= (props) => {
{icon && (
{icon}
)}
- {label}
+ {label && (
+ {label}
+ )}
) : (
diff --git a/web/core/components/core/app-header.tsx b/web/core/components/core/app-header.tsx
index 83e148d4c..05f7ab4c1 100644
--- a/web/core/components/core/app-header.tsx
+++ b/web/core/components/core/app-header.tsx
@@ -16,7 +16,7 @@ export const AppHeader = (props: AppHeaderProps) => {
<>
-
+
{header}
diff --git a/web/core/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx b/web/core/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx
index 63ce3fe66..322c00054 100644
--- a/web/core/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx
+++ b/web/core/components/issues/issue-layouts/roots/archived-issue-layout-root.tsx
@@ -4,11 +4,7 @@ import { useParams } from "next/navigation";
import useSWR from "swr";
// mobx store
// components
-import {
- ArchivedIssueListLayout,
- ArchivedIssueAppliedFiltersRoot,
- IssuePeekOverview,
-} from "@/components/issues";
+import { ArchivedIssueListLayout, ArchivedIssueAppliedFiltersRoot, IssuePeekOverview } from "@/components/issues";
import { EIssuesStoreType } from "@/constants/issue";
// ui
import { useIssues } from "@/hooks/store";
diff --git a/web/core/components/issues/peek-overview/header.tsx b/web/core/components/issues/peek-overview/header.tsx
index 374a280e2..9967ad067 100644
--- a/web/core/components/issues/peek-overview/header.tsx
+++ b/web/core/components/issues/peek-overview/header.tsx
@@ -54,6 +54,7 @@ export type PeekOverviewHeaderProps = {
issueId: string;
isArchived: boolean;
disabled: boolean;
+ embedIssue: boolean;
toggleDeleteIssueModal: (issueId: string | null) => void;
toggleArchiveIssueModal: (issueId: string | null) => void;
handleRestoreIssue: () => void;
@@ -69,6 +70,7 @@ export const IssuePeekOverviewHeader: FC
= observer((pr
issueId,
isArchived,
disabled,
+ embedIssue = false,
removeRoutePeekId,
toggleDeleteIssueModal,
toggleArchiveIssueModal,
@@ -123,7 +125,7 @@ export const IssuePeekOverviewHeader: FC = observer((pr
- {currentMode && (
+ {currentMode && embedIssue === false && (
= observer((props) => {
- const { is_archived = false, is_draft = false } = props;
+ const { embedIssue = false, is_archived = false, is_draft = false } = props;
// router
const pathname = usePathname();
const {
@@ -406,6 +407,7 @@ export const IssuePeekOverview: FC = observer((props) => {
isLoading={isLoading}
is_archived={is_archived}
disabled={!isEditable}
+ embedIssue={embedIssue}
issueOperations={issueOperations}
/>
);
diff --git a/web/core/components/issues/peek-overview/view.tsx b/web/core/components/issues/peek-overview/view.tsx
index 803f02fb7..dde36db17 100644
--- a/web/core/components/issues/peek-overview/view.tsx
+++ b/web/core/components/issues/peek-overview/view.tsx
@@ -29,11 +29,21 @@ interface IIssueView {
isLoading?: boolean;
is_archived: boolean;
disabled?: boolean;
+ embedIssue?: boolean;
issueOperations: TIssueOperations;
}
export const IssueView: FC = observer((props) => {
- const { workspaceSlug, projectId, issueId, isLoading, is_archived, disabled = false, issueOperations } = props;
+ const {
+ workspaceSlug,
+ projectId,
+ issueId,
+ isLoading,
+ is_archived,
+ disabled = false,
+ embedIssue = false,
+ issueOperations,
+ } = props;
// states
const [peekMode, setPeekMode] = useState("side-peek");
const [isSubmitting, setIsSubmitting] = useState<"submitting" | "submitted" | "saved">("saved");
@@ -84,6 +94,16 @@ export const IssueView: FC = observer((props) => {
removeRoutePeekId();
};
+ const peekOverviewIssueClassName = cn(
+ !embedIssue &&
+ "fixed z-20 flex flex-col overflow-hidden rounded border border-custom-border-200 bg-custom-background-100 transition-all duration-300",
+ !embedIssue && {
+ "bottom-0 right-0 top-0 w-full md:w-[50%]": peekMode === "side-peek",
+ "size-5/6 top-[8.33%] left-[8.33%]": peekMode === "modal",
+ "inset-0 m-4": peekMode === "full-screen",
+ }
+ );
+
return (
<>
{issue && !is_archived && (
@@ -113,14 +133,7 @@ export const IssueView: FC = observer((props) => {
{issueId && (
= observer((props) => {
projectId={projectId}
isSubmitting={isSubmitting}
disabled={disabled}
+ embedIssue={embedIssue}
/>
{/* content */}
diff --git a/web/core/components/notifications/index.ts b/web/core/components/notifications/index.ts
deleted file mode 100644
index 99667be22..000000000
--- a/web/core/components/notifications/index.ts
+++ /dev/null
@@ -1,4 +0,0 @@
-export * from "./notification-card";
-export * from "./notification-popover";
-export * from "./select-snooze-till-modal";
-export * from "./notification-header";
diff --git a/web/core/components/notifications/notification-card.tsx b/web/core/components/notifications/notification-card.tsx
deleted file mode 100644
index 9b540d118..000000000
--- a/web/core/components/notifications/notification-card.tsx
+++ /dev/null
@@ -1,423 +0,0 @@
-"use client";
-
-import React, { useEffect, useRef } from "react";
-import Image from "next/image";
-import Link from "next/link";
-import { useParams } from "next/navigation";
-import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react";
-import { Menu } from "@headlessui/react";
-// type
-import type { IUserNotification, NotificationType } from "@plane/types";
-import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui";
-// constants
-import {
- ISSUE_OPENED,
- NOTIFICATIONS_READ,
- NOTIFICATION_ARCHIVED,
- NOTIFICATION_SNOOZED,
-} from "@/constants/event-tracker";
-import { snoozeOptions } from "@/constants/notification";
-// helper
-import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "@/helpers/date-time.helper";
-import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
-import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "@/helpers/string.helper";
-// hooks
-import { useEventTracker } from "@/hooks/store";
-import { usePlatformOS } from "@/hooks/use-platform-os";
-
-type NotificationCardProps = {
- selectedTab: NotificationType;
- notification: IUserNotification;
- isSnoozedTabOpen: boolean;
- closePopover: () => void;
- markNotificationReadStatus: (notificationId: string) => Promise
;
- markNotificationReadStatusToggle: (notificationId: string) => Promise;
- markNotificationArchivedStatus: (notificationId: string) => Promise;
- setSelectedNotificationForSnooze: (notificationId: string) => void;
- markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise;
-};
-
-export const NotificationCard: React.FC = (props) => {
- const {
- selectedTab,
- notification,
- isSnoozedTabOpen,
- closePopover,
- markNotificationReadStatus,
- markNotificationReadStatusToggle,
- markNotificationArchivedStatus,
- setSelectedNotificationForSnooze,
- markSnoozeNotification,
- } = props;
- // store hooks
- const { captureEvent } = useEventTracker();
- const { isMobile } = usePlatformOS();
- const { workspaceSlug } = useParams();
- // states
- const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false);
- // refs
- const snoozeRef = useRef(null);
-
- const moreOptions = [
- {
- id: 1,
- name: notification.read_at ? "Mark as unread" : "Mark as read",
- icon: ,
- onClick: () => {
- markNotificationReadStatusToggle(notification.id).then(() => {
- setToast({
- title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
- type: TOAST_TYPE.SUCCESS,
- });
- });
- },
- },
- {
- id: 2,
- name: notification.archived_at ? "Unarchive" : "Archive",
- icon: notification.archived_at ? (
-
- ) : (
-
- ),
- onClick: () => {
- markNotificationArchivedStatus(notification.id).then(() => {
- setToast({
- title: notification.archived_at ? "Notification un-archived" : "Notification archived",
- type: TOAST_TYPE.SUCCESS,
- });
- });
- },
- },
- ];
-
- const snoozeOptionOnClick = (date: Date | null) => {
- if (!date) {
- setSelectedNotificationForSnooze(notification.id);
- return;
- }
- markSnoozeNotification(notification.id, date).then(() => {
- setToast({
- title: `Notification snoozed till ${renderFormattedDate(date)}`,
- type: TOAST_TYPE.SUCCESS,
- });
- });
- };
-
- // close snooze options on outside click
- useEffect(() => {
- const handleClickOutside = (event: any) => {
- if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) {
- setShowSnoozeOptions(false);
- }
- };
- document.addEventListener("mousedown", handleClickOutside, true);
- document.addEventListener("touchend", handleClickOutside, true);
- return () => {
- document.removeEventListener("mousedown", handleClickOutside, true);
- document.removeEventListener("touchend", handleClickOutside, true);
- };
- }, []);
-
- const notificationField = notification.data.issue_activity.field;
- const notificationTriggeredBy = notification.triggered_by_details;
-
- const snoozedTillDate = getDate(notification?.snoozed_till);
-
- if (snoozedTillDate && isSnoozedTabOpen && snoozedTillDate < new Date()) return null;
-
- return (
- {
- markNotificationReadStatus(notification.id);
- captureEvent(ISSUE_OPENED, {
- issue_id: notification.data.issue.id,
- element: "notification",
- });
- closePopover();
- }}
- href={`/${workspaceSlug}/projects/${notification.project}/${
- notificationField === "archived_at" ? "archives/" : ""
- }issues/${notification.data.issue.id}`}
- className={`group relative flex w-full cursor-pointer items-center gap-4 p-3 pl-6 ${
- notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200"
- }`}
- >
- {notification.read_at === null && (
-
- )}
-
- {notificationTriggeredBy.avatar && notificationTriggeredBy.avatar !== "" ? (
-
-
-
- ) : (
-
-
- {notificationTriggeredBy.is_bot ? (
- notificationTriggeredBy.first_name?.[0]?.toUpperCase()
- ) : notificationTriggeredBy.display_name?.[0] ? (
- notificationTriggeredBy.display_name?.[0]?.toUpperCase()
- ) : (
-
- )}
-
-
- )}
-
-
-
- {!notification.message ? (
-
-
- {notificationTriggeredBy.is_bot
- ? notificationTriggeredBy.first_name
- : notificationTriggeredBy.display_name}{" "}
-
- {!["comment", "archived_at"].includes(notificationField) && notification.data.issue_activity.verb}{" "}
- {notificationField === "comment"
- ? "commented"
- : notificationField === "archived_at"
- ? notification.data.issue_activity.new_value === "restore"
- ? "restored the issue"
- : "archived the issue"
- : notificationField === "None"
- ? null
- : replaceUnderscoreIfSnakeCase(notificationField)}{" "}
- {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
-
- {" "}
- {notificationField !== "None" ? (
- notificationField !== "comment" ? (
- notificationField === "target_date" ? (
- renderFormattedDate(notification.data.issue_activity.new_value)
- ) : notificationField === "attachment" ? (
- "the issue"
- ) : notificationField === "description" ? (
- stripAndTruncateHTML(notification.data.issue_activity.new_value, 55)
- ) : notificationField === "archived_at" ? null : (
- notification.data.issue_activity.new_value
- )
- ) : (
-
- {sanitizeCommentForNotification(notification.data.issue_activity.new_value ?? undefined)}
-
- )
- ) : (
- "the issue and assigned it to you."
- )}
-
-
- ) : (
-
- {notification.message}
-
- )}
-
-
- {showSnoozeOptions && (
-
- {snoozeOptions.map((item) => (
-
{
- e.stopPropagation();
- e.preventDefault();
- setShowSnoozeOptions(false);
- snoozeOptionOnClick(item.value);
- }}
- >
- {item.label}
-
- ))}
-
- )}
-
-
-
-
-
- {truncateText(
- `${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`,
- 50
- )}
-
- {notification.snoozed_till ? (
-
-
-
- Till {renderFormattedDate(notification.snoozed_till)},{" "}
- {renderFormattedTime(notification.snoozed_till, "12-hour")}
-
-
- ) : (
-
{calculateTimeAgo(notification.created_at)}
- )}
-
-
-
- {[
- {
- id: 1,
- name: notification.read_at ? "Mark as unread" : "Mark as read",
- icon:
,
- onClick: () => {
- markNotificationReadStatusToggle(notification.id).then(() => {
- captureEvent(NOTIFICATIONS_READ, {
- issue_id: notification.data.issue.id,
- tab: selectedTab,
- state: "SUCCESS",
- });
- setToast({
- title: notification.read_at ? "Notification marked as read" : "Notification marked as unread",
- type: TOAST_TYPE.SUCCESS,
- });
- });
- },
- },
- {
- id: 2,
- name: notification.archived_at ? "Unarchive" : "Archive",
- icon: notification.archived_at ? (
-
- ) : (
-
- ),
- onClick: () => {
- markNotificationArchivedStatus(notification.id).then(() => {
- captureEvent(NOTIFICATION_ARCHIVED, {
- issue_id: notification.data.issue.id,
- tab: selectedTab,
- state: "SUCCESS",
- });
- setToast({
- title: notification.archived_at ? "Notification un-archived" : "Notification archived",
- type: TOAST_TYPE.SUCCESS,
- });
- });
- },
- },
- ].map((item) => (
-
-
-
- ))}
-
-
-
-
-
- }
- optionsClassName="!z-20"
- >
- {snoozeOptions.map((item) => (
- {
- e.stopPropagation();
- e.preventDefault();
-
- if (!item.value) {
- setSelectedNotificationForSnooze(notification.id);
- return;
- }
-
- markSnoozeNotification(notification.id, item.value).then(() => {
- captureEvent(NOTIFICATION_SNOOZED, {
- issue_id: notification.data.issue.id,
- tab: selectedTab,
- state: "SUCCESS",
- });
- setToast({
- title: `Notification snoozed till ${renderFormattedDate(item.value)}`,
- type: TOAST_TYPE.SUCCESS,
- });
- });
- }}
- >
- {item.label}
-
- ))}
-
-
-
- );
-};
diff --git a/web/core/components/notifications/notification-header.tsx b/web/core/components/notifications/notification-header.tsx
deleted file mode 100644
index e72e02ef0..000000000
--- a/web/core/components/notifications/notification-header.tsx
+++ /dev/null
@@ -1,223 +0,0 @@
-"use client";
-
-import React from "react";
-import { ArrowLeft, CheckCheck, Clock, ListFilter, MoreVertical, RefreshCw, X } from "lucide-react";
-import type { NotificationType, NotificationCount } from "@plane/types";
-// components
-import { ArchiveIcon, CustomMenu, Tooltip } from "@plane/ui";
-import { SidebarHamburgerToggle } from "@/components/core/sidebar";
-// ui
-// hooks
-import {
- ARCHIVED_NOTIFICATIONS,
- NOTIFICATIONS_READ,
- SNOOZED_NOTIFICATIONS,
- UNREAD_NOTIFICATIONS,
-} from "@/constants/event-tracker";
-import { getNumberCount } from "@/helpers/string.helper";
-import { useEventTracker } from "@/hooks/store";
-import { usePlatformOS } from "@/hooks/use-platform-os";
-// helpers
-// type
-// constants
-
-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>;
- markAllNotificationsAsRead: () => Promise;
-};
-
-export const NotificationHeader: React.FC = (props) => {
- const {
- notificationCount,
- notificationMutate,
- closePopover,
- isRefreshing,
- snoozed,
- archived,
- readNotification,
- selectedTab,
- setSnoozed,
- setArchived,
- setReadNotification,
- setSelectedTab,
- markAllNotificationsAsRead,
- } = props;
- // store hooks
- const { captureEvent } = useEventTracker();
- // hooks
- const { isMobile } = usePlatformOS();
-
- 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
-
-
-
-
-
-
-
-
-
-
-
-
- }
- closeOnSelect
- >
-
{
- markAllNotificationsAsRead();
- captureEvent(NOTIFICATIONS_READ);
- }}
- >
-
-
- Mark all as read
-
-
-
{
- setArchived(false);
- setReadNotification(false);
- setSnoozed((prev) => !prev);
- captureEvent(SNOOZED_NOTIFICATIONS);
- }}
- >
-
-
- Show snoozed
-
-
-
{
- setSnoozed(false);
- setReadNotification(false);
- setArchived((prev) => !prev);
- captureEvent(ARCHIVED_NOTIFICATIONS);
- }}
- >
-
-
-
-
-
-
-
-
-
-
-
- {snoozed || archived || readNotification ? (
-
- ) : (
-
- )}
-
- >
- );
-};
diff --git a/web/core/components/notifications/notification-popover.tsx b/web/core/components/notifications/notification-popover.tsx
deleted file mode 100644
index ce4c6efd8..000000000
--- a/web/core/components/notifications/notification-popover.tsx
+++ /dev/null
@@ -1,226 +0,0 @@
-"use client";
-
-import React, { Fragment } from "react";
-import { observer } from "mobx-react";
-import { Bell } from "lucide-react";
-import { Popover, Transition } from "@headlessui/react";
-// ui
-import { Tooltip } from "@plane/ui";
-// components
-import { EmptyState } from "@/components/empty-state";
-import { SnoozeNotificationModal, NotificationCard, NotificationHeader } from "@/components/notifications";
-import { NotificationsLoader } from "@/components/ui";
-// constants
-import { EmptyStateType } from "@/constants/empty-state";
-// helpers
-import { cn } from "@/helpers/common.helper";
-import { getNumberCount } from "@/helpers/string.helper";
-// hooks
-import { useAppTheme } from "@/hooks/store";
-import useOutsideClickDetector from "@/hooks/use-outside-click-detector";
-import { usePlatformOS } from "@/hooks/use-platform-os";
-import useUserNotification from "@/hooks/use-user-notifications";
-
-export const NotificationPopover = observer(() => {
- // states
- const [isActive, setIsActive] = React.useState(false);
- // store hooks
- const { sidebarCollapsed, toggleSidebar } = useAppTheme();
- // refs
- const notificationPopoverRef = React.useRef
(null);
- // hooks
- const { isMobile } = usePlatformOS();
-
- const {
- notifications,
- archived,
- readNotification,
- selectedNotificationForSnooze,
- selectedTab,
- setArchived,
- setReadNotification,
- setSelectedNotificationForSnooze,
- setSelectedTab,
- setSnoozed,
- snoozed,
- notificationMutate,
- markNotificationArchivedStatus,
- markNotificationReadStatus,
- markNotificationAsRead,
- markSnoozeNotification,
- notificationCount,
- totalNotificationCount,
- setSize,
- isLoadingMore,
- hasMore,
- isRefreshing,
- setFetchNotifications,
- markAllNotificationsAsRead,
- } = useUserNotification();
- const isSidebarCollapsed = sidebarCollapsed;
- useOutsideClickDetector(notificationPopoverRef, () => {
- // if snooze modal is open, then don't close the popover
- if (selectedNotificationForSnooze === null) setIsActive(false);
- });
-
- const currentTabEmptyState = snoozed
- ? EmptyStateType.NOTIFICATION_SNOOZED_EMPTY_STATE
- : archived
- ? EmptyStateType.NOTIFICATION_ARCHIVED_EMPTY_STATE
- : selectedTab === "created"
- ? EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE
- : selectedTab === "watching"
- ? EmptyStateType.NOTIFICATION_SUBSCRIBED_EMPTY_STATE
- : EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE;
-
- return (
- <>
- setSelectedNotificationForSnooze(null)}
- onSubmit={markSnoozeNotification}
- notification={
- notifications?.find((notification: any) => notification.id === selectedNotificationForSnooze) || null
- }
- onSuccess={() => setSelectedNotificationForSnooze(null)}
- />
-
- <>
-
-
-
-
-
- setIsActive(false)}
- isRefreshing={isRefreshing}
- snoozed={snoozed}
- archived={archived}
- readNotification={readNotification}
- selectedTab={selectedTab}
- setSnoozed={setSnoozed}
- setArchived={setArchived}
- setReadNotification={setReadNotification}
- setSelectedTab={setSelectedTab}
- markAllNotificationsAsRead={markAllNotificationsAsRead}
- />
-
- {notifications ? (
- notifications.length > 0 ? (
-
-
- {notifications.map((notification: any) => (
- setIsActive(false)}
- notification={notification}
- markNotificationArchivedStatus={markNotificationArchivedStatus}
- markNotificationReadStatus={markNotificationAsRead}
- markNotificationReadStatusToggle={markNotificationReadStatus}
- setSelectedNotificationForSnooze={setSelectedNotificationForSnooze}
- markSnoozeNotification={markSnoozeNotification}
- />
- ))}
-
- {isLoadingMore && (
-
-
-
Loading notifications
-
- )}
- {hasMore && !isLoadingMore && (
-
- )}
-
- ) : (
-
-
-
- )
- ) : (
-
- )}
-
-
- >
-
- >
- );
-});
diff --git a/web/core/components/workspace-notifications/index.ts b/web/core/components/workspace-notifications/index.ts
new file mode 100644
index 000000000..2682c9114
--- /dev/null
+++ b/web/core/components/workspace-notifications/index.ts
@@ -0,0 +1,2 @@
+export * from "./notification-app-sidebar-option";
+export * from "./sidebar";
diff --git a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx
new file mode 100644
index 000000000..acde9b839
--- /dev/null
+++ b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx
@@ -0,0 +1,36 @@
+"use client";
+
+import { FC } from "react";
+import { observer } from "mobx-react";
+import useSWR from "swr";
+// helpers
+import { getNumberCount } from "@/helpers/string.helper";
+// hooks
+import { useWorkspaceNotifications } from "@/hooks/store";
+
+type TNotificationAppSidebarOption = {
+ workspaceSlug: string;
+ isSidebarCollapsed: boolean | undefined;
+};
+
+export const NotificationAppSidebarOption: FC = observer((props) => {
+ const { workspaceSlug, isSidebarCollapsed } = props;
+ // hooks
+ const { totalUnreadNotificationsCount, getUnreadNotificationsCount } = useWorkspaceNotifications();
+
+ useSWR(
+ workspaceSlug ? "WORKSPACE_UNREAD_NOTIFICATION_COUNT" : null,
+ workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null
+ );
+
+ if (totalUnreadNotificationsCount <= 0) return <>>;
+
+ if (isSidebarCollapsed)
+ return ;
+
+ return (
+
+ {getNumberCount(totalUnreadNotificationsCount)}
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/filters/applied-filter.tsx b/web/core/components/workspace-notifications/sidebar/filters/applied-filter.tsx
new file mode 100644
index 000000000..babf99ba7
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/filters/applied-filter.tsx
@@ -0,0 +1,64 @@
+"use client";
+
+import { FC } from "react";
+import { observer } from "mobx-react";
+import { X } from "lucide-react";
+// constants
+import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
+// hooks
+import { useWorkspaceNotifications } from "@/hooks/store";
+
+type TAppliedFilters = {
+ workspaceSlug: string;
+};
+
+export const AppliedFilters: FC = observer((props) => {
+ const { workspaceSlug } = props;
+ // hooks
+ const { filters, updateFilters } = useWorkspaceNotifications();
+ // derived values
+ const isFiltersEnabled = Object.entries(filters.type || {}).some(([, value]) => value);
+
+ const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
+ updateFilters("type", {
+ ...filters.type,
+ [filterType]: filterValue,
+ });
+
+ const handleClearFilters = () => {
+ updateFilters("type", {
+ [ENotificationFilterType.ASSIGNED]: false,
+ [ENotificationFilterType.CREATED]: false,
+ [ENotificationFilterType.SUBSCRIBED]: false,
+ });
+ };
+
+ if (!isFiltersEnabled || !workspaceSlug) return <>>;
+ return (
+
+ {FILTER_TYPE_OPTIONS.map((filter) => {
+ const isSelected = filters?.type?.[filter?.value] || false;
+ if (!isSelected) return <>>;
+ return (
+
handleFilterTypeChange(filter?.value, !isSelected)}
+ >
+
{filter.label}
+
+
+
+
+ );
+ })}
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/filters/index.ts b/web/core/components/workspace-notifications/sidebar/filters/index.ts
new file mode 100644
index 000000000..4f7dbf49f
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/filters/index.ts
@@ -0,0 +1,2 @@
+export * from "./root";
+export * from "./applied-filter";
diff --git a/web/core/components/workspace-notifications/sidebar/filters/root.tsx b/web/core/components/workspace-notifications/sidebar/filters/root.tsx
new file mode 100644
index 000000000..86994c819
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/filters/root.tsx
@@ -0,0 +1,80 @@
+"use client";
+
+import { FC, Fragment } from "react";
+import { observer } from "mobx-react";
+import { Check, ListFilter } from "lucide-react";
+import { Popover, Transition } from "@headlessui/react";
+import { Tooltip } from "@plane/ui";
+// constants
+import { ENotificationFilterType, FILTER_TYPE_OPTIONS } from "@/constants/notification";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useWorkspaceNotifications } from "@/hooks/store";
+import { usePlatformOS } from "@/hooks/use-platform-os";
+
+export const NotificationFilter: FC = observer(() => {
+ // hooks
+ const { isMobile } = usePlatformOS();
+ const { filters, updateFilters } = useWorkspaceNotifications();
+
+ const handleFilterTypeChange = (filterType: ENotificationFilterType, filterValue: boolean) =>
+ updateFilters("type", {
+ ...filters.type,
+ [filterType]: filterValue,
+ });
+
+ return (
+
+
+ (open ? "bg-custom-background-80" : "")
+ )}
+ >
+
+
+
+
+
+
+
+ {FILTER_TYPE_OPTIONS.map((filter) => {
+ const isSelected = filters?.type?.[filter?.value] || false;
+ return (
+
handleFilterTypeChange(filter?.value, !isSelected)}
+ >
+
+ {isSelected && }
+
+
+ {filter.label}
+
+
+ );
+ })}
+
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/header/index.ts b/web/core/components/workspace-notifications/sidebar/header/index.ts
new file mode 100644
index 000000000..eb738f5f3
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/header/index.ts
@@ -0,0 +1,2 @@
+export * from "./root";
+export * from "./options";
diff --git a/web/core/components/workspace-notifications/sidebar/header/options/index.ts b/web/core/components/workspace-notifications/sidebar/header/options/index.ts
new file mode 100644
index 000000000..e4d478279
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/header/options/index.ts
@@ -0,0 +1,2 @@
+export * from "./root";
+export * from "./menu-option";
diff --git a/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx b/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx
new file mode 100644
index 000000000..509dd9de3
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/header/options/menu-option.tsx
@@ -0,0 +1,160 @@
+"use client";
+
+import { FC, Fragment } from "react";
+import { observer } from "mobx-react";
+import { Check, CheckCheck, CheckCircle, Clock, MoreVertical } from "lucide-react";
+import { Popover, Transition } from "@headlessui/react";
+import { TNotificationFilter } from "@plane/types";
+import { ArchiveIcon, Spinner, Tooltip } from "@plane/ui";
+// constants
+import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
+import { ENotificationLoader } from "@/constants/notification";
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
+import { usePlatformOS } from "@/hooks/use-platform-os";
+
+type TNotificationHeaderMenuOption = {
+ workspaceSlug: string;
+};
+
+export const NotificationHeaderMenuOption: FC = observer((props) => {
+ const { workspaceSlug } = props;
+ // hooks
+ const { captureEvent } = useEventTracker();
+ const { isMobile } = usePlatformOS();
+ const { loader, filters, updateFilters, updateBulkFilters, markAllNotificationsAsRead } = useWorkspaceNotifications();
+
+ const handleFilterChange = (filterType: keyof TNotificationFilter, filterValue: boolean) =>
+ updateFilters(filterType, filterValue);
+
+ const handleBulkFilterChange = (filter: Partial) => updateBulkFilters(filter);
+
+ const handleMarkAllNotificationsAsRead = async () => {
+ // NOTE: We are using loader to prevent continues request when we are making all the notification to read
+ if (loader) return;
+ try {
+ await markAllNotificationsAsRead(workspaceSlug);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+ (open ? "bg-custom-background-80" : "")
+ )}
+ >
+
+
+
+
+
+
+
+
+
{
+ handleMarkAllNotificationsAsRead();
+ captureEvent(NOTIFICATIONS_READ);
+ }}
+ >
+
+
Mark all as read
+ {loader === ENotificationLoader.MARK_ALL_AS_READY && (
+
+
+
+ )}
+
+
+
+
+
+
+
handleFilterChange("read", !filters?.read)}
+ >
+
+
+ Show unread
+
+ {filters?.read && (
+
+
+
+ )}
+
+
+
+ handleBulkFilterChange({
+ archived: !filters?.archived,
+ snoozed: false,
+ })
+ }
+ >
+
+
+ Show Archived
+
+ {filters?.archived && (
+
+
+
+ )}
+
+
+
+ handleBulkFilterChange({
+ snoozed: !filters?.snoozed,
+ archived: false,
+ })
+ }
+ >
+
+
+ Show Snoozed
+
+ {filters?.snoozed && (
+
+
+
+ )}
+
+
+
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/header/options/root.tsx b/web/core/components/workspace-notifications/sidebar/header/options/root.tsx
new file mode 100644
index 000000000..2fc8c4bde
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/header/options/root.tsx
@@ -0,0 +1,51 @@
+import { FC } from "react";
+import { observer } from "mobx-react";
+import { RefreshCw } from "lucide-react";
+import { Tooltip } from "@plane/ui";
+// components
+import { NotificationFilter, NotificationHeaderMenuOption } from "@/components/workspace-notifications";
+// constants
+import { ENotificationLoader, ENotificationQueryParamType } from "@/constants/notification";
+// hooks
+import { useWorkspaceNotifications } from "@/hooks/store";
+import { usePlatformOS } from "@/hooks/use-platform-os";
+
+type TNotificationSidebarHeaderOptions = {
+ workspaceSlug: string;
+};
+
+export const NotificationSidebarHeaderOptions: FC = observer((props) => {
+ const { workspaceSlug } = props;
+ // hooks
+ const { isMobile } = usePlatformOS();
+ const { loader, getNotifications } = useWorkspaceNotifications();
+
+ const refreshNotifications = async () => {
+ if (loader) return;
+ try {
+ await getNotifications(workspaceSlug, ENotificationLoader.MUTATION_LOADER, ENotificationQueryParamType.CURRENT);
+ } catch (error) {
+ console.error(error);
+ }
+ };
+
+ return (
+
+ {/* refetch current notifications */}
+
+
+
+
+
+
+ {/* notification filters */}
+
+
+ {/* notification menu options */}
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx
new file mode 100644
index 000000000..d84e61a99
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx
@@ -0,0 +1,51 @@
+"use client";
+
+import { FC } from "react";
+import { observer } from "mobx-react";
+import { Bell } from "lucide-react";
+import { Breadcrumbs } from "@plane/ui";
+// components
+import { BreadcrumbLink } from "@/components/common";
+import { SidebarHamburgerToggle } from "@/components/core";
+import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
+
+type TNotificationSidebarHeader = {
+ workspaceSlug: string;
+ notificationsCount: number;
+};
+
+export const NotificationSidebarHeader: FC = observer((props) => {
+ const { workspaceSlug, notificationsCount } = props;
+
+ if (!workspaceSlug) return <>>;
+ return (
+
+
+
+
+
+
+
+ Notifications
+
+ {notificationsCount}
+
+
+ }
+ icon={
}
+ disableTooltip
+ />
+ }
+ />
+
+
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/index.ts b/web/core/components/workspace-notifications/sidebar/index.ts
new file mode 100644
index 000000000..52dc7bde7
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/index.ts
@@ -0,0 +1,9 @@
+export * from "./loader";
+
+export * from "./root";
+
+export * from "./header";
+
+export * from "./filters";
+
+export * from "./notification-card";
diff --git a/web/core/components/workspace-notifications/sidebar/loader.tsx b/web/core/components/workspace-notifications/sidebar/loader.tsx
new file mode 100644
index 000000000..7485c2c4c
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/loader.tsx
@@ -0,0 +1,16 @@
+export const NotificationsLoader = () => (
+
+ {[...Array(3)].map((i) => (
+
+ ))}
+
+);
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/index.ts b/web/core/components/workspace-notifications/sidebar/notification-card/index.ts
new file mode 100644
index 000000000..d4000aa9e
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/index.ts
@@ -0,0 +1,3 @@
+export * from "./root";
+export * from "./item";
+export * from "./options";
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx
new file mode 100644
index 000000000..506665e13
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/item.tsx
@@ -0,0 +1,169 @@
+"use client";
+
+import { FC, useState } from "react";
+import { observer } from "mobx-react";
+import { Clock } from "lucide-react";
+import { Avatar } from "@plane/ui";
+// components
+import { NotificationOption } from "@/components/workspace-notifications";
+// helpers
+import { cn } from "@/helpers/common.helper";
+import { calculateTimeAgo, renderFormattedDate, renderFormattedTime } from "@/helpers/date-time.helper";
+import { sanitizeCommentForNotification } from "@/helpers/notification.helper";
+import { replaceUnderscoreIfSnakeCase, stripAndTruncateHTML } from "@/helpers/string.helper";
+// hooks
+import { useIssueDetail, useNotification } from "@/hooks/store";
+
+type TNotificationItem = {
+ workspaceSlug: string;
+ notificationId: string;
+};
+
+export const NotificationItem: FC = observer((props) => {
+ const { workspaceSlug, notificationId } = props;
+ // hooks
+ const { asJson: notification, markNotificationAsRead } = useNotification(notificationId);
+ const { getIsIssuePeeked, setPeekIssue } = useIssueDetail();
+ // states
+ const [isSnoozeStateModalOpen, setIsSnoozeStateModalOpen] = useState(false);
+ const [customSnoozeModal, setCustomSnoozeModal] = useState(false);
+
+ // derived values
+ const projectId = notification?.project || undefined;
+ const issueId = notification?.data?.issue?.id || undefined;
+
+ const notificationField = notification?.data?.issue_activity.field || undefined;
+ const notificationTriggeredBy = notification.triggered_by_details || undefined;
+
+ const handleNotificationIssuePeekOverview = async () => {
+ if (
+ workspaceSlug &&
+ projectId &&
+ issueId &&
+ !getIsIssuePeeked(issueId) &&
+ !isSnoozeStateModalOpen &&
+ !customSnoozeModal
+ ) {
+ setPeekIssue({ workspaceSlug, projectId, issueId });
+ // make the notification as read
+ if (notification.read_at === null)
+ try {
+ await markNotificationAsRead(workspaceSlug);
+ } catch (error) {
+ console.error(error);
+ }
+ }
+ };
+
+ if (!workspaceSlug || !notificationId || !notification?.id || !notificationField) return <>>;
+
+ return (
+
+ {notification.read_at === null && (
+
+ )}
+
+
+
+ {notificationTriggeredBy && (
+
+ )}
+
+
+
+
+
+ {!notification.message ? (
+ <>
+
+ {notificationTriggeredBy?.is_bot
+ ? notificationTriggeredBy?.first_name
+ : notificationTriggeredBy?.display_name}{" "}
+
+ {!["comment", "archived_at"].includes(notificationField) && notification?.data?.issue_activity.verb}{" "}
+ {notificationField === "comment"
+ ? "commented"
+ : notificationField === "archived_at"
+ ? notification?.data?.issue_activity.new_value === "restore"
+ ? "restored the issue"
+ : "archived the issue"
+ : notificationField === "None"
+ ? null
+ : replaceUnderscoreIfSnakeCase(notificationField)}{" "}
+ {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""}
+
+ {" "}
+ {notificationField !== "None" ? (
+ notificationField !== "comment" ? (
+ notificationField === "target_date" ? (
+ renderFormattedDate(notification?.data?.issue_activity.new_value)
+ ) : notificationField === "attachment" ? (
+ "the issue"
+ ) : notificationField === "description" ? (
+ stripAndTruncateHTML(notification?.data?.issue_activity.new_value || "", 55)
+ ) : notificationField === "archived_at" ? null : (
+ notification?.data?.issue_activity.new_value
+ )
+ ) : (
+
+ {sanitizeCommentForNotification(notification?.data?.issue_activity.new_value ?? undefined)}
+
+ )
+ ) : (
+ "the issue and assigned it to you."
+ )}
+
+ >
+ ) : (
+ {notification.message}
+ )}
+
+
+
+
+
+
+ {notification?.data?.issue?.identifier}-{notification?.data?.issue?.sequence_id}
+ {notification?.data?.issue?.name}
+
+
+ {notification?.snoozed_till ? (
+
+
+
+ Till {renderFormattedDate(notification.snoozed_till)},
+ {renderFormattedTime(notification.snoozed_till, "12-hour")}
+
+
+ ) : (
+
+ {notification.created_at && calculateTimeAgo(notification.created_at)}
+
+ )}
+
+
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/archive.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/archive.tsx
new file mode 100644
index 000000000..395f0a75f
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/archive.tsx
@@ -0,0 +1,58 @@
+"use client";
+
+import { FC } from "react";
+import { observer } from "mobx-react";
+import { ArchiveRestore } from "lucide-react";
+import { ArchiveIcon, TOAST_TYPE, setToast } from "@plane/ui";
+// components
+import { NotificationItemOptionButton } from "@/components/workspace-notifications";
+// constants
+import { NOTIFICATION_ARCHIVED } from "@/constants/event-tracker";
+// hooks
+import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
+// store
+import { INotification } from "@/store/notifications/notification";
+
+type TNotificationItemArchiveOption = {
+ workspaceSlug: string;
+ notification: INotification;
+};
+
+export const NotificationItemArchiveOption: FC = observer((props) => {
+ const { workspaceSlug, notification } = props;
+ // hooks
+ const { captureEvent } = useEventTracker();
+ const { currentNotificationTab } = useWorkspaceNotifications();
+ const { asJson: data, archiveNotification, unArchiveNotification } = notification;
+
+ const handleNotificationUpdate = async () => {
+ try {
+ const request = data.archived_at ? unArchiveNotification : archiveNotification;
+ await request(workspaceSlug);
+ captureEvent(NOTIFICATION_ARCHIVED, {
+ issue_id: data?.data?.issue?.id,
+ tab: currentNotificationTab,
+ state: "SUCCESS",
+ });
+ setToast({
+ title: data.archived_at ? "Notification un-archived" : "Notification archived",
+ type: TOAST_TYPE.SUCCESS,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ return (
+
+ {data.archived_at ? (
+
+ ) : (
+
+ )}
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx
new file mode 100644
index 000000000..cfaf26cf5
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/button.tsx
@@ -0,0 +1,40 @@
+"use client";
+
+import { FC, ReactNode } from "react";
+import { Tooltip } from "@plane/ui";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { usePlatformOS } from "@/hooks/use-platform-os";
+
+type TNotificationItemOptionButton = {
+ tooltipContent?: string;
+ buttonClassName?: string;
+ callBack: () => void;
+ children: ReactNode;
+};
+
+export const NotificationItemOptionButton: FC = (props) => {
+ const { tooltipContent = "", buttonClassName = "", children, callBack } = props;
+ // hooks
+ const { isMobile } = usePlatformOS();
+
+ return (
+
+
+
+ );
+};
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/index.ts b/web/core/components/workspace-notifications/sidebar/notification-card/options/index.ts
new file mode 100644
index 000000000..47dff3192
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/index.ts
@@ -0,0 +1,7 @@
+export * from "./root";
+
+export * from "./read";
+export * from "./archive";
+export * from "./snooze";
+
+export * from "./button";
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/read.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/read.tsx
new file mode 100644
index 000000000..42b4e29f0
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/read.tsx
@@ -0,0 +1,54 @@
+"use client";
+
+import { FC } from "react";
+import { observer } from "mobx-react";
+import { MessageSquare } from "lucide-react";
+import { TOAST_TYPE, setToast } from "@plane/ui";
+// components
+import { NotificationItemOptionButton } from "@/components/workspace-notifications";
+// constants
+import { NOTIFICATIONS_READ } from "@/constants/event-tracker";
+// hooks
+import { useEventTracker, useWorkspaceNotifications } from "@/hooks/store";
+// store
+import { INotification } from "@/store/notifications/notification";
+
+type TNotificationItemReadOption = {
+ workspaceSlug: string;
+ notification: INotification;
+};
+
+export const NotificationItemReadOption: FC = observer((props) => {
+ const { workspaceSlug, notification } = props;
+ // hooks
+ const { captureEvent } = useEventTracker();
+ const { currentNotificationTab } = useWorkspaceNotifications();
+ const { asJson: data, markNotificationAsRead, markNotificationAsUnRead } = notification;
+
+ const handleNotificationUpdate = async () => {
+ try {
+ const request = data.read_at ? markNotificationAsUnRead : markNotificationAsRead;
+ await request(workspaceSlug);
+ captureEvent(NOTIFICATIONS_READ, {
+ issue_id: data?.data?.issue?.id,
+ tab: currentNotificationTab,
+ state: "SUCCESS",
+ });
+ setToast({
+ title: data.read_at ? "Notification marked as unread" : "Notification marked as read",
+ type: TOAST_TYPE.SUCCESS,
+ });
+ } catch (e) {
+ console.error(e);
+ }
+ };
+
+ return (
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx
new file mode 100644
index 000000000..2017c0db8
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/root.tsx
@@ -0,0 +1,57 @@
+"use client";
+
+import { FC, Dispatch, SetStateAction } from "react";
+import { observer } from "mobx-react";
+// components
+import {
+ NotificationItemReadOption,
+ NotificationItemArchiveOption,
+ NotificationItemSnoozeOption,
+} from "@/components/workspace-notifications";
+// helpers
+import { cn } from "@/helpers/common.helper";
+// hooks
+import { useNotification } from "@/hooks/store";
+
+type TNotificationOption = {
+ workspaceSlug: string;
+ notificationId: string;
+ isSnoozeStateModalOpen: boolean;
+ setIsSnoozeStateModalOpen: Dispatch>;
+ customSnoozeModal: boolean;
+ setCustomSnoozeModal: Dispatch>;
+};
+
+export const NotificationOption: FC = observer((props) => {
+ const {
+ workspaceSlug,
+ notificationId,
+ isSnoozeStateModalOpen,
+ setIsSnoozeStateModalOpen,
+ customSnoozeModal,
+ setCustomSnoozeModal,
+ } = props;
+ // hooks
+ const notification = useNotification(notificationId);
+
+ return (
+
+
+ {/* read */}
+
+
+ {/* archive */}
+
+
+ {/* snooze notification */}
+
+
+
+ );
+});
diff --git a/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/index.ts b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/index.ts
new file mode 100644
index 000000000..5238728d7
--- /dev/null
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/index.ts
@@ -0,0 +1,2 @@
+export * from "./root";
+export * from "./modal";
diff --git a/web/core/components/notifications/select-snooze-till-modal.tsx b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx
similarity index 90%
rename from web/core/components/notifications/select-snooze-till-modal.tsx
rename to web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx
index 3512aa418..1ca19a5ef 100644
--- a/web/core/components/notifications/select-snooze-till-modal.tsx
+++ b/web/core/components/workspace-notifications/sidebar/notification-card/options/snooze/modal.tsx
@@ -5,40 +5,37 @@ import { useParams } from "next/navigation";
import { useForm, Controller } from "react-hook-form";
import { X } from "lucide-react";
import { Transition, Dialog } from "@headlessui/react";
-import type { IUserNotification } from "@plane/types";
-import { Button, CustomSelect, TOAST_TYPE, setToast } from "@plane/ui";
+import { TNotification } from "@plane/types";
+import { Button, CustomSelect } from "@plane/ui";
+// components
import { DateDropdown } from "@/components/dropdowns";
// constants
import { allTimeIn30MinutesInterval12HoursFormat } from "@/constants/notification";
-// ui
-// types
// helpers
-import { getDate } from "helpers/date-time.helper";
+import { getDate } from "@/helpers/date-time.helper";
-type SnoozeModalProps = {
+type TNotificationSnoozeModal = {
isOpen: boolean;
onClose: () => void;
- onSuccess: () => void;
- notification: IUserNotification | null;
- onSubmit: (notificationId: string, dateTime?: Date | undefined) => Promise;
+ onSubmit: (dateTime?: Date | undefined) => Promise;
};
type FormValues = {
- time: string | null;
- date: Date | null;
+ time: string | undefined;
+ date: Date | undefined;
period: "AM" | "PM";
};
const defaultValues: FormValues = {
- time: null,
- date: null,
+ time: undefined,
+ date: undefined,
period: "AM",
};
const timeStamps = allTimeIn30MinutesInterval12HoursFormat;
-export const SnoozeNotificationModal: FC = (props) => {
- const { isOpen, onClose, notification, onSuccess, onSubmit: handleSubmitSnooze } = props;
+export const NotificationSnoozeModal: FC = (props) => {
+ const { isOpen, onClose, onSubmit: handleSubmitSnooze } = props;
const { workspaceSlug } = useParams();
@@ -53,6 +50,19 @@ export const SnoozeNotificationModal: FC = (props) => {
defaultValues,
});
+ const handleClose = () => {
+ // This is a workaround to fix the issue of the Notification popover modal close on closing this modal
+ const closeTimeout = setTimeout(() => {
+ onClose();
+ clearTimeout(closeTimeout);
+ }, 50);
+
+ const timeout = setTimeout(() => {
+ reset({ ...defaultValues });
+ clearTimeout(timeout);
+ }, 500);
+ };
+
const getTimeStamp = () => {
const today = new Date();
const formDataDate = watch("date");
@@ -82,7 +92,7 @@ export const SnoozeNotificationModal: FC = (props) => {
};
const onSubmit = async (formData: FormValues) => {
- if (!workspaceSlug || !notification || !formData.date || !formData.time) return;
+ if (!workspaceSlug || !formData.date || !formData.time) return;
const period = formData.period;
@@ -96,30 +106,11 @@ export const SnoozeNotificationModal: FC = (props) => {
dateTime?.setHours(hours);
dateTime?.setMinutes(minutes);
- await handleSubmitSnooze(notification.id, dateTime).then(() => {
+ await handleSubmitSnooze(dateTime).then(() => {
handleClose();
- onSuccess();
- setToast({
- title: "Success!",
- message: "Notification snoozed successfully",
- type: TOAST_TYPE.SUCCESS,
- });
});
};
- const handleClose = () => {
- // This is a workaround to fix the issue of the Notification popover modal close on closing this modal
- const closeTimeout = setTimeout(() => {
- onClose();
- clearTimeout(closeTimeout);
- }, 50);
-
- const timeout = setTimeout(() => {
- reset({ ...defaultValues });
- clearTimeout(timeout);
- }, 500);
- };
-
return (
);
});
diff --git a/web/core/constants/dashboard.ts b/web/core/constants/dashboard.ts
index 25306911f..056941ef9 100644
--- a/web/core/constants/dashboard.ts
+++ b/web/core/constants/dashboard.ts
@@ -2,7 +2,7 @@
import { linearGradientDef } from "@nivo/core";
// icons
-import { BarChart2, Briefcase, CheckCircle, Home, Settings } from "lucide-react";
+import { BarChart2, Bell, Briefcase, CheckCircle, Home, Settings } from "lucide-react";
// types
import { TIssuesListTypes, TStateGroups } from "@plane/types";
// ui
@@ -317,4 +317,12 @@ export const SIDEBAR_USER_MENU_ITEMS: {
highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/`,
Icon: Home,
},
+ {
+ key: "notifications",
+ label: "Notifications",
+ href: `/notifications`,
+ access: EUserWorkspaceRoles.GUEST,
+ highlight: (pathname: string, baseUrl: string) => pathname === `${baseUrl}/notifications`,
+ Icon: Bell,
+ },
];
diff --git a/web/core/constants/empty-state.ts b/web/core/constants/empty-state.ts
index 0a71ccc62..cad133dd0 100644
--- a/web/core/constants/empty-state.ts
+++ b/web/core/constants/empty-state.ts
@@ -80,6 +80,8 @@ export enum EmptyStateType {
ISSUE_RELATION_EMPTY_STATE = "issue-relation-empty-state",
ISSUE_COMMENT_EMPTY_STATE = "issue-comment-empty-state",
+ NOTIFICATION_ALL_EMPTY_STATE = "notification-all-empty-state",
+ NOTIFICATION_MENTIONS_EMPTY_STATE = "notification-mentions-empty-state",
NOTIFICATION_MY_ISSUE_EMPTY_STATE = "notification-my-issues-empty-state",
NOTIFICATION_CREATED_EMPTY_STATE = "notification-created-empty-state",
NOTIFICATION_SUBSCRIBED_EMPTY_STATE = "notification-subscribed-empty-state",
@@ -593,13 +595,24 @@ const emptyStateDetails = {
path: "/empty-state/search/comments",
},
+ [EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE]: {
+ key: EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE,
+ title: "No issues assigned",
+ description: "Updates for issues assigned to you can be \n seen here",
+ path: "/empty-state/search/notification",
+ },
+ [EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE]: {
+ key: EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE,
+ title: "No issues assigned",
+ description: "Updates for issues assigned to you can be \n seen here",
+ path: "/empty-state/search/notification",
+ },
[EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE]: {
key: EmptyStateType.NOTIFICATION_MY_ISSUE_EMPTY_STATE,
title: "No issues assigned",
description: "Updates for issues assigned to you can be \n seen here",
path: "/empty-state/search/notification",
},
-
[EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE]: {
key: EmptyStateType.NOTIFICATION_CREATED_EMPTY_STATE,
title: "No updates to issues",
@@ -630,6 +643,7 @@ const emptyStateDetails = {
description: "Any notification you archive will be \n available here to help you focus",
path: "/empty-state/search/archive",
},
+
[EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE]: {
key: EmptyStateType.ACTIVE_CYCLE_PROGRESS_EMPTY_STATE,
title: "Add issues to the cycle to view it's \n progress",
diff --git a/web/core/constants/fetch-keys.ts b/web/core/constants/fetch-keys.ts
index 5d0fb7717..ec5de760f 100644
--- a/web/core/constants/fetch-keys.ts
+++ b/web/core/constants/fetch-keys.ts
@@ -1,5 +1,4 @@
-import { IAnalyticsParams, IJiraMetadata, INotificationParams } from "@plane/types";
-import { objToQueryParams } from "@/helpers/string.helper";
+import { IAnalyticsParams, IJiraMetadata } from "@plane/types";
const paramsToKey = (params: any) => {
const {
@@ -246,41 +245,6 @@ export const ANALYTICS = (workspaceSlug: string, params: IAnalyticsParams) =>
export const DEFAULT_ANALYTICS = (workspaceSlug: string, params?: Partial) =>
`DEFAULT_ANALYTICS_${workspaceSlug.toUpperCase()}_${params?.project?.toString()}_${params?.cycle}_${params?.module}`;
-// notifications
-export const USER_WORKSPACE_NOTIFICATIONS = (workspaceSlug: string, params: INotificationParams) => {
- const { type, snoozed, archived, read } = params;
-
- return `USER_WORKSPACE_NOTIFICATIONS_${workspaceSlug?.toUpperCase()}_TYPE_${(
- type ?? "assigned"
- )?.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()}`;
-
-export const UNREAD_NOTIFICATIONS_COUNT = (workspaceSlug: string) =>
- `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,
- cursor: "30: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,
- })}`;
-};
-
// profile
export const USER_PROFILE_DATA = (workspaceSlug: string, userId: string) =>
`USER_PROFILE_ACTIVITY_${workspaceSlug.toUpperCase()}_${userId.toUpperCase()}`;
diff --git a/web/core/constants/notification.ts b/web/core/constants/notification.ts
index 04f803151..fe4f0a026 100644
--- a/web/core/constants/notification.ts
+++ b/web/core/constants/notification.ts
@@ -1,27 +1,101 @@
-export const snoozeOptions = [
+export enum ENotificationTab {
+ ALL = "all",
+ MENTIONS = "mentions",
+}
+
+export enum ENotificationFilterType {
+ CREATED = "created",
+ ASSIGNED = "assigned",
+ SUBSCRIBED = "subscribed",
+}
+
+export enum ENotificationLoader {
+ INIT_LOADER = "init-loader",
+ MUTATION_LOADER = "mutation-loader",
+ PAGINATION_LOADER = "pagination-loader",
+ REFRESH = "refresh",
+ MARK_ALL_AS_READY = "mark-all-as-read",
+}
+
+export enum ENotificationQueryParamType {
+ INIT = "init",
+ CURRENT = "current",
+ NEXT = "next",
+}
+
+export type TNotificationTab = ENotificationTab.ALL | ENotificationTab.MENTIONS;
+
+export const NOTIFICATION_TABS = [
{
+ label: "All",
+ value: ENotificationTab.ALL,
+ },
+ // {
+ // label: "Mentions",
+ // value: ENotificationTab.MENTIONS,
+ // },
+];
+
+export const FILTER_TYPE_OPTIONS = [
+ {
+ label: "Assigned to me",
+ value: ENotificationFilterType.ASSIGNED,
+ },
+ {
+ label: "Created by me",
+ value: ENotificationFilterType.CREATED,
+ },
+ {
+ label: "Subscribed by me",
+ value: ENotificationFilterType.SUBSCRIBED,
+ },
+];
+
+export const NOTIFICATION_SNOOZE_OPTIONS = [
+ {
+ key: "1_day",
label: "1 day",
- value: new Date(new Date().getTime() + 24 * 60 * 60 * 1000),
+ value: () => {
+ const date = new Date();
+ return new Date(date.getTime() + 24 * 60 * 60 * 1000);
+ },
},
{
+ key: "3_days",
label: "3 days",
- value: new Date(new Date().getTime() + 3 * 24 * 60 * 60 * 1000),
+ value: () => {
+ const date = new Date();
+ return new Date(date.getTime() + 3 * 24 * 60 * 60 * 1000);
+ },
},
{
+ key: "5_days",
label: "5 days",
- value: new Date(new Date().getTime() + 5 * 24 * 60 * 60 * 1000),
+ value: () => {
+ const date = new Date();
+ return new Date(date.getTime() + 5 * 24 * 60 * 60 * 1000);
+ },
},
{
+ key: "1_week",
label: "1 week",
- value: new Date(new Date().getTime() + 7 * 24 * 60 * 60 * 1000),
+ value: () => {
+ const date = new Date();
+ return new Date(date.getTime() + 7 * 24 * 60 * 60 * 1000);
+ },
},
{
+ key: "2_weeks",
label: "2 weeks",
- value: new Date(new Date().getTime() + 14 * 24 * 60 * 60 * 1000),
+ value: () => {
+ const date = new Date();
+ return new Date(date.getTime() + 14 * 24 * 60 * 60 * 1000);
+ },
},
{
+ key: "custom",
label: "Custom",
- value: null,
+ value: undefined,
},
];
diff --git a/web/core/hooks/store/index.ts b/web/core/hooks/store/index.ts
index 0b02ae670..c20f42087 100644
--- a/web/core/hooks/store/index.ts
+++ b/web/core/hooks/store/index.ts
@@ -33,3 +33,4 @@ export * from "./use-app-theme";
export * from "./use-command-palette";
export * from "./use-router-params";
export * from "./estimates";
+export * from "./notifications";
diff --git a/web/core/hooks/store/notifications/index.ts b/web/core/hooks/store/notifications/index.ts
new file mode 100644
index 000000000..07bcca1cf
--- /dev/null
+++ b/web/core/hooks/store/notifications/index.ts
@@ -0,0 +1,2 @@
+export * from "./use-workspace-notifications";
+export * from "./use-notification";
diff --git a/web/core/hooks/store/notifications/use-notification.ts b/web/core/hooks/store/notifications/use-notification.ts
new file mode 100644
index 000000000..9df87dd5c
--- /dev/null
+++ b/web/core/hooks/store/notifications/use-notification.ts
@@ -0,0 +1,13 @@
+import { useContext } from "react";
+// mobx store
+import { StoreContext } from "@/lib/store-context";
+// mobx store
+import { INotification } from "@/store/notifications/notification";
+
+export const useNotification = (notificationId: string | undefined): INotification => {
+ const context = useContext(StoreContext);
+ if (context === undefined) throw new Error("useNotification must be used within StoreProvider");
+ if (!notificationId) return {} as INotification;
+
+ return context.workspaceNotification.notifications?.[notificationId] ?? {};
+};
diff --git a/web/core/hooks/store/notifications/use-workspace-notifications.ts b/web/core/hooks/store/notifications/use-workspace-notifications.ts
new file mode 100644
index 000000000..f882d6716
--- /dev/null
+++ b/web/core/hooks/store/notifications/use-workspace-notifications.ts
@@ -0,0 +1,12 @@
+import { useContext } from "react";
+// context
+import { StoreContext } from "@/lib/store-context";
+// mobx store
+import { IWorkspaceNotificationStore } from "@/store/notifications/workspace-notifications.store";
+
+export const useWorkspaceNotifications = (): IWorkspaceNotificationStore => {
+ const context = useContext(StoreContext);
+ if (context === undefined) throw new Error("useWorkspaceNotifications must be used within StoreProvider");
+
+ return context.workspaceNotification;
+};
diff --git a/web/core/hooks/use-issue-notification-subscription.tsx b/web/core/hooks/use-issue-notification-subscription.tsx
deleted file mode 100644
index 2770f00f5..000000000
--- a/web/core/hooks/use-issue-notification-subscription.tsx
+++ /dev/null
@@ -1,75 +0,0 @@
-import { useCallback } from "react";
-import useSWR from "swr";
-import { IUser, TUserProfile } from "@plane/types";
-// services
-import { NotificationService } from "@/services/notification.service";
-// types
-
-const userNotificationServices = new NotificationService();
-
-const useUserIssueNotificationSubscription = (
- user: IUser | null,
- profile: TUserProfile | undefined,
- workspaceSlug?: string | string[] | null,
- projectId?: string | string[] | null,
- issueId?: string | string[] | null
-) => {
- const { data, error, mutate } = useSWR(
- workspaceSlug && projectId && issueId ? `SUBSCRIPTION_STATUE_${workspaceSlug}_${projectId}_${issueId}` : null,
- workspaceSlug && projectId && issueId
- ? () =>
- userNotificationServices.getIssueNotificationSubscriptionStatus(
- workspaceSlug.toString(),
- projectId.toString(),
- issueId.toString()
- )
- : null
- );
-
- const handleUnsubscribe = useCallback(() => {
- if (!workspaceSlug || !projectId || !issueId) return;
-
- mutate(
- {
- subscribed: false,
- },
- false
- );
-
- userNotificationServices
- .unsubscribeFromIssueNotifications(workspaceSlug.toString(), projectId.toString(), issueId.toString())
- .then(() => {
- mutate({
- subscribed: false,
- });
- });
- }, [workspaceSlug, projectId, issueId, mutate]);
-
- const handleSubscribe = useCallback(() => {
- if (!workspaceSlug || !projectId || !issueId || !user) return;
-
- mutate(
- {
- subscribed: true,
- },
- false
- );
-
- userNotificationServices
- .subscribeToIssueNotifications(workspaceSlug.toString(), projectId.toString(), issueId.toString())
- .then(() => {
- mutate({
- subscribed: true,
- });
- });
- }, [workspaceSlug, projectId, issueId, mutate, user]);
-
- return {
- loading: !data && !error,
- subscribed: data?.subscribed,
- handleSubscribe,
- handleUnsubscribe,
- } as const;
-};
-
-export default useUserIssueNotificationSubscription;
diff --git a/web/core/hooks/use-user-notifications.tsx b/web/core/hooks/use-user-notifications.tsx
deleted file mode 100644
index 03e1eb355..000000000
--- a/web/core/hooks/use-user-notifications.tsx
+++ /dev/null
@@ -1,317 +0,0 @@
-"use client";
-
-import { useMemo, useState } from "react";
-import { useParams } from "next/navigation";
-// swr
-import useSWR from "swr";
-import useSWRInfinite from "swr/infinite";
-import type { NotificationType, NotificationCount, IMarkAllAsReadPayload } from "@plane/types";
-// ui
-import { TOAST_TYPE, setToast } from "@plane/ui";
-// constant
-import { UNREAD_NOTIFICATIONS_COUNT, getPaginatedNotificationKey } from "@/constants/fetch-keys";
-// services
-import { NotificationService } from "@/services/notification.service";
-
-const PER_PAGE = 30;
-
-const userNotificationServices = new NotificationService();
-
-const useUserNotification = (): any => {
- const { workspaceSlug } = useParams();
-
- const [snoozed, setSnoozed] = useState(false);
- const [archived, setArchived] = useState(false);
- const [readNotification, setReadNotification] = useState(false);
- const [fetchNotifications, setFetchNotifications] = useState(false);
- const [selectedNotificationForSnooze, setSelectedNotificationForSnooze] = useState(null);
- const [selectedTab, setSelectedTab] = useState("assigned");
-
- 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(
- fetchNotifications && 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,
- () => (workspaceSlug ? userNotificationServices.getUnreadNotificationsCount(workspaceSlug.toString()) : null)
- );
-
- const handleReadMutation = (action: "read" | "unread") => {
- const notificationCountNumber = action === "read" ? -1 : 1;
-
- mutateNotificationCount((prev: any) => {
- 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: any) => {
- if (!previousNotifications) return previousNotifications;
-
- const notificationIndex = Math.floor(
- previousNotifications
- .map((d: any) => d.results)
- .flat()
- .findIndex((notification: any) => notification.id === notificationId) / PER_PAGE
- );
-
- let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex(
- (notification: any) => 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: any) => {
- if (!previousNotifications) return previousNotifications;
-
- const notificationIndex = Math.floor(
- previousNotifications
- .map((d: any) => d.results)
- .flat()
- .findIndex((notification: any) => notification.id === notificationId) / PER_PAGE
- );
-
- let notificationIndexInPage = previousNotifications[notificationIndex].results.findIndex(
- (notification: any) => 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 (readNotification) removeNotification(notificationId);
-
- if (isRead) {
- await userNotificationServices
- .markUserNotificationAsUnread(workspaceSlug.toString(), notificationId)
- .catch(() => {
- throw new Error("Something went wrong");
- })
- .finally(() => {
- mutateNotificationCount();
- });
- } else {
- await userNotificationServices
- .markUserNotificationAsRead(workspaceSlug.toString(), notificationId)
- .catch(() => {
- throw new Error("Something went wrong");
- })
- .finally(() => {
- mutateNotificationCount();
- });
- }
- };
-
- const markNotificationAsRead = async (notificationId: string) => {
- if (!workspaceSlug) return;
-
- const isRead = notifications?.find((notification) => notification.id === notificationId)?.read_at !== null;
-
- if (isRead) return;
-
- mutateNotification(notificationId, { read_at: new Date() });
- handleReadMutation("read");
-
- await userNotificationServices.markUserNotificationAsRead(workspaceSlug.toString(), notificationId).catch(() => {
- throw new Error("Something went wrong");
- });
-
- mutateNotificationCount();
- };
-
- const markNotificationArchivedStatus = async (notificationId: string) => {
- if (!workspaceSlug) return;
- const isArchived = 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)
- .catch(() => {
- throw new Error("Something went wrong");
- })
- .finally(() => {
- notificationMutate();
- mutateNotificationCount();
- });
- } else {
- await userNotificationServices
- .markUserNotificationAsArchived(workspaceSlug.toString(), notificationId)
- .catch(() => {
- throw new Error("Something went wrong");
- })
- .finally(() => {
- notificationMutate();
- mutateNotificationCount();
- });
- }
- };
-
- const markSnoozeNotification = async (notificationId: string, dateTime?: Date) => {
- if (!workspaceSlug) return;
-
- const isSnoozed = 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(() => {
- notificationMutate();
- });
- } else {
- await userNotificationServices
- .patchUserNotification(workspaceSlug.toString(), notificationId, {
- snoozed_till: dateTime,
- })
- .catch(() => {
- new Error("Something went wrong");
- })
- .finally(() => {
- notificationMutate();
- });
- }
- };
-
- const markAllNotificationsAsRead = async () => {
- if (!workspaceSlug) return;
-
- let markAsReadParams: IMarkAllAsReadPayload;
-
- if (snoozed) markAsReadParams = { archived: false, snoozed: true };
- else if (archived) markAsReadParams = { archived: true, snoozed: false };
- else markAsReadParams = { archived: false, snoozed: false, type: readNotification ? "all" : selectedTab };
-
- await userNotificationServices
- .markAllNotificationsAsRead(workspaceSlug.toString(), markAsReadParams)
- .then(() => {
- setToast({
- type: TOAST_TYPE.SUCCESS,
- title: "Success!",
- message: "All Notifications marked as read.",
- });
- })
- .catch(() => {
- setToast({
- type: TOAST_TYPE.ERROR,
- title: "Error!",
- message: "Something went wrong. Please try again.",
- });
- })
- .finally(() => {
- notificationMutate();
- mutateNotificationCount();
- });
- };
-
- return {
- notifications,
- notificationMutate,
- markNotificationReadStatus,
- markNotificationArchivedStatus,
- markSnoozeNotification,
- snoozed,
- setSnoozed,
- archived,
- setArchived,
- readNotification,
- setReadNotification,
- selectedNotificationForSnooze,
- setSelectedNotificationForSnooze,
- selectedTab,
- setSelectedTab,
- totalNotificationCount: notificationCount
- ? notificationCount.created_issues + notificationCount.watching_issues + notificationCount.my_issues
- : null,
- notificationCount,
- mutateNotificationCount,
- setSize,
- isLoading,
- isLoadingMore,
- hasMore,
- isRefreshing,
- setFetchNotifications,
- markNotificationAsRead,
- markAllNotificationsAsRead,
- };
-};
-
-export default useUserNotification;
diff --git a/web/core/services/issue/issue.service.ts b/web/core/services/issue/issue.service.ts
index 7cded4633..69ef1ed5c 100644
--- a/web/core/services/issue/issue.service.ts
+++ b/web/core/services/issue/issue.service.ts
@@ -1,5 +1,13 @@
// types
-import type { TIssue, IIssueDisplayProperties, TIssueLink, TIssueSubIssues, TIssueActivity, TIssuesResponse, TBulkOperationsPayload } from "@plane/types";
+import type {
+ TIssue,
+ IIssueDisplayProperties,
+ TIssueLink,
+ TIssueSubIssues,
+ TIssueActivity,
+ TIssuesResponse,
+ TBulkOperationsPayload,
+} from "@plane/types";
// helpers
import { API_BASE_URL } from "@/helpers/common.helper";
// services
@@ -272,4 +280,35 @@ export class IssueService extends APIService {
throw error?.response?.data;
});
}
+
+ // issue subscriptions
+ async getIssueNotificationSubscriptionStatus(
+ workspaceSlug: string,
+ projectId: string,
+ issueId: string
+ ): Promise<{
+ subscribed: boolean;
+ }> {
+ return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async unsubscribeFromIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
+
+ async subscribeToIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise {
+ return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
+ .then((response) => response?.data)
+ .catch((error) => {
+ throw error?.response?.data;
+ });
+ }
}
diff --git a/web/core/services/notification.service.ts b/web/core/services/notification.service.ts
deleted file mode 100644
index ff1cc8177..000000000
--- a/web/core/services/notification.service.ts
+++ /dev/null
@@ -1,144 +0,0 @@
-// services
-import type {
- IUserNotification,
- INotificationParams,
- NotificationCount,
- PaginatedUserNotification,
- IMarkAllAsReadPayload,
-} from "@plane/types";
-import { API_BASE_URL } from "@/helpers/common.helper";
-import { APIService } from "@/services/api.service";
-// types
-// helpers
-
-export class NotificationService extends APIService {
- constructor() {
- super(API_BASE_URL);
- }
-
- async getUserNotifications(workspaceSlug: string, params: INotificationParams): Promise {
- return this.get(`/api/workspaces/${workspaceSlug}/users/notifications`, {
- params,
- })
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async getUserNotificationDetailById(workspaceSlug: string, notificationId: string): Promise {
- return this.get(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async markUserNotificationAsRead(workspaceSlug: string, notificationId: string): Promise {
- return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async markUserNotificationAsUnread(workspaceSlug: string, notificationId: string): Promise {
- return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async markUserNotificationAsArchived(workspaceSlug: string, notificationId: string): Promise {
- return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async markUserNotificationAsUnarchived(workspaceSlug: string, notificationId: string): Promise {
- return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async patchUserNotification(
- workspaceSlug: string,
- notificationId: string,
- data: Partial
- ): Promise {
- return this.patch(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`, data)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async deleteUserNotification(workspaceSlug: string, notificationId: string): Promise {
- return this.delete(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async subscribeToIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise {
- return this.post(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async getIssueNotificationSubscriptionStatus(
- workspaceSlug: string,
- projectId: string,
- issueId: string
- ): Promise<{
- subscribed: boolean;
- }> {
- return this.get(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- async unsubscribeFromIssueNotifications(workspaceSlug: string, projectId: string, issueId: string): Promise {
- return this.delete(`/api/workspaces/${workspaceSlug}/projects/${projectId}/issues/${issueId}/subscribe/`)
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-
- 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;
- });
- }
-
- async markAllNotificationsAsRead(workspaceSlug: string, payload: IMarkAllAsReadPayload): Promise {
- return this.post(`/api/workspaces/${workspaceSlug}/users/notifications/mark-all-read/`, {
- ...payload,
- })
- .then((response) => response?.data)
- .catch((error) => {
- throw error?.response?.data;
- });
- }
-}
diff --git a/web/core/services/workspace-notification.service.ts b/web/core/services/workspace-notification.service.ts
new file mode 100644
index 000000000..933b82063
--- /dev/null
+++ b/web/core/services/workspace-notification.service.ts
@@ -0,0 +1,118 @@
+/* eslint-disable no-useless-catch */
+
+import type {
+ TNotificationPaginatedInfo,
+ TNotificationPaginatedInfoQueryParams,
+ TNotification,
+ TUnreadNotificationsCount,
+} from "@plane/types";
+// helpers
+import { API_BASE_URL } from "@/helpers/common.helper";
+// services
+import { APIService } from "@/services/api.service";
+
+export class WorkspaceNotificationService extends APIService {
+ constructor() {
+ super(API_BASE_URL);
+ }
+
+ async fetchUnreadNotificationsCount(workspaceSlug: string): Promise {
+ try {
+ const { data } = await this.get(`/api/workspaces/${workspaceSlug}/users/notifications/unread/`);
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async fetchNotifications(
+ workspaceSlug: string,
+ params: TNotificationPaginatedInfoQueryParams
+ ): Promise {
+ try {
+ const { data } = await this.get(`/api/workspaces/${workspaceSlug}/users/notifications`, {
+ params,
+ });
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async updateNotificationById(
+ workspaceSlug: string,
+ notificationId: string,
+ payload: Partial
+ ): Promise {
+ try {
+ const { data } = await this.patch(
+ `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/`,
+ payload
+ );
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async markNotificationAsRead(workspaceSlug: string, notificationId: string): Promise {
+ try {
+ const { data } = await this.post(`/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`);
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async markNotificationAsUnread(workspaceSlug: string, notificationId: string): Promise {
+ try {
+ const { data } = await this.delete(
+ `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/read/`
+ );
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async markNotificationAsArchived(workspaceSlug: string, notificationId: string): Promise {
+ try {
+ const { data } = await this.post(
+ `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`
+ );
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async markNotificationAsUnArchived(
+ workspaceSlug: string,
+ notificationId: string
+ ): Promise {
+ try {
+ const { data } = await this.delete(
+ `/api/workspaces/${workspaceSlug}/users/notifications/${notificationId}/archive/`
+ );
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+
+ async markAllNotificationsAsRead(
+ workspaceSlug: string,
+ payload: TNotificationPaginatedInfoQueryParams
+ ): Promise {
+ try {
+ const { data } = await this.post(`/api/workspaces/${workspaceSlug}/users/notifications/mark-all-read/`, payload);
+ return data || undefined;
+ } catch (error) {
+ throw error;
+ }
+ }
+}
+
+const workspaceNotificationService = new WorkspaceNotificationService();
+
+export default workspaceNotificationService;
diff --git a/web/core/store/issue/issue-details/subscription.store.ts b/web/core/store/issue/issue-details/subscription.store.ts
index 1326d705e..76757f644 100644
--- a/web/core/store/issue/issue-details/subscription.store.ts
+++ b/web/core/store/issue/issue-details/subscription.store.ts
@@ -1,7 +1,7 @@
import set from "lodash/set";
import { action, makeObservable, observable, runInAction } from "mobx";
// services
-import { NotificationService } from "@/services/notification.service";
+import { IssueService } from "@/services/issue/issue.service";
// types
import { IIssueDetail } from "./root.store";
@@ -25,7 +25,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// root store
rootIssueDetail: IIssueDetail;
// services
- notificationService;
+ issueService;
constructor(rootStore: IIssueDetail) {
makeObservable(this, {
@@ -40,7 +40,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
// root store
this.rootIssueDetail = rootStore;
// services
- this.notificationService = new NotificationService();
+ this.issueService = new IssueService();
}
// helper methods
@@ -62,7 +62,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
fetchSubscriptions = async (workspaceSlug: string, projectId: string, issueId: string) => {
try {
- const subscription = await this.notificationService.getIssueNotificationSubscriptionStatus(
+ const subscription = await this.issueService.getIssueNotificationSubscriptionStatus(
workspaceSlug,
projectId,
issueId
@@ -85,7 +85,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], true);
});
- await this.notificationService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
+ await this.issueService.subscribeToIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error;
@@ -101,7 +101,7 @@ export class IssueSubscriptionStore implements IIssueSubscriptionStore {
set(this.subscriptionMap, [issueId, currentUserId], false);
});
- await this.notificationService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
+ await this.issueService.unsubscribeFromIssueNotifications(workspaceSlug, projectId, issueId);
} catch (error) {
this.fetchSubscriptions(workspaceSlug, projectId, issueId);
throw error;
diff --git a/web/core/store/notifications/notification.ts b/web/core/store/notifications/notification.ts
new file mode 100644
index 000000000..dabb25d97
--- /dev/null
+++ b/web/core/store/notifications/notification.ts
@@ -0,0 +1,321 @@
+/* eslint-disable no-useless-catch */
+import set from "lodash/set";
+import { action, computed, makeObservable, observable, runInAction } from "mobx";
+import { IUserLite, TNotification, TNotificationData } from "@plane/types";
+// services
+import workspaceNotificationService from "@/services/workspace-notification.service";
+// store
+import { CoreRootStore } from "../root.store";
+
+export interface INotification extends TNotification {
+ // observables
+ // computed
+ asJson: TNotification;
+ // computed functions
+ // helper functions
+ mutateNotification: (notification: Partial) => void;
+ // actions
+ updateNotification: (workspaceSlug: string, payload: Partial) => Promise;
+ markNotificationAsRead: (workspaceSlug: string) => Promise;
+ markNotificationAsUnRead: (workspaceSlug: string) => Promise;
+ archiveNotification: (workspaceSlug: string) => Promise;
+ unArchiveNotification: (workspaceSlug: string) => Promise;
+ snoozeNotification: (workspaceSlug: string, snoozeTill: Date) => Promise;
+ unSnoozeNotification: (workspaceSlug: string) => Promise;
+}
+
+export class Notification implements INotification {
+ // observables
+ id: string | undefined = undefined;
+ title: string | undefined = undefined;
+ data: TNotificationData | undefined = undefined;
+ entity_identifier: string | undefined = undefined;
+ entity_name: string | undefined = undefined;
+ message_html: string | undefined = undefined;
+ message: undefined = undefined;
+ message_stripped: undefined = undefined;
+ sender: string | undefined = undefined;
+ receiver: string | undefined = undefined;
+ triggered_by: string | undefined = undefined;
+ triggered_by_details: IUserLite | undefined = undefined;
+ read_at: string | undefined = undefined;
+ archived_at: string | undefined = undefined;
+ snoozed_till: string | undefined = undefined;
+ workspace: string | undefined = undefined;
+ project: string | undefined = undefined;
+ created_at: string | undefined = undefined;
+ updated_at: string | undefined = undefined;
+ created_by: string | undefined = undefined;
+ updated_by: string | undefined = undefined;
+
+ constructor(
+ private store: CoreRootStore,
+ private notification: TNotification
+ ) {
+ makeObservable(this, {
+ // observables
+ id: observable.ref,
+ title: observable.ref,
+ data: observable,
+ entity_identifier: observable.ref,
+ entity_name: observable.ref,
+ message_html: observable.ref,
+ message: observable.ref,
+ message_stripped: observable.ref,
+ sender: observable.ref,
+ receiver: observable.ref,
+ triggered_by: observable.ref,
+ triggered_by_details: observable,
+ read_at: observable.ref,
+ archived_at: observable.ref,
+ snoozed_till: observable.ref,
+ workspace: observable.ref,
+ project: observable.ref,
+ created_at: observable.ref,
+ updated_at: observable.ref,
+ created_by: observable.ref,
+ updated_by: observable.ref,
+ // computed
+ asJson: computed,
+ // actions
+ updateNotification: action,
+ markNotificationAsRead: action,
+ markNotificationAsUnRead: action,
+ archiveNotification: action,
+ unArchiveNotification: action,
+ snoozeNotification: action,
+ unSnoozeNotification: action,
+ });
+ this.id = this.notification.id;
+ this.title = this.notification.title;
+ this.data = this.notification.data;
+ this.entity_identifier = this.notification.entity_identifier;
+ this.entity_name = this.notification.entity_name;
+ this.message_html = this.notification.message_html;
+ this.message = this.notification.message;
+ this.message_stripped = this.notification.message_stripped;
+ this.sender = this.notification.sender;
+ this.receiver = this.notification.receiver;
+ this.triggered_by = this.notification.triggered_by;
+ this.triggered_by_details = this.notification.triggered_by_details;
+ this.read_at = this.notification.read_at;
+ this.archived_at = this.notification.archived_at;
+ this.snoozed_till = this.notification.snoozed_till;
+ this.workspace = this.notification.workspace;
+ this.project = this.notification.project;
+ this.created_at = this.notification.created_at;
+ this.updated_at = this.notification.updated_at;
+ this.created_by = this.notification.created_by;
+ this.updated_by = this.notification.updated_by;
+ }
+
+ // computed
+ /**
+ * @description get notification as json
+ */
+ get asJson() {
+ return {
+ id: this.id,
+ title: this.title,
+ data: this.data,
+ entity_identifier: this.entity_identifier,
+ entity_name: this.entity_name,
+ message_html: this.message_html,
+ message: this.message,
+ message_stripped: this.message_stripped,
+ sender: this.sender,
+ receiver: this.receiver,
+ triggered_by: this.triggered_by,
+ triggered_by_details: this.triggered_by_details,
+ read_at: this.read_at,
+ archived_at: this.archived_at,
+ snoozed_till: this.snoozed_till,
+ workspace: this.workspace,
+ project: this.project,
+ created_at: this.created_at,
+ updated_at: this.updated_at,
+ created_by: this.created_by,
+ updated_by: this.updated_by,
+ };
+ }
+
+ // computed functions
+
+ // helper functions
+ mutateNotification = (notification: Partial) => {
+ Object.entries(notification).forEach(([key, value]) => {
+ if (key in this) {
+ set(this, key, value);
+ }
+ });
+ };
+
+ // actions
+ /**
+ * @description update notification
+ * @param { string } workspaceSlug
+ * @param { Partial } payload
+ * @returns { TNotification | undefined }
+ */
+ updateNotification = async (
+ workspaceSlug: string,
+ payload: Partial
+ ): Promise => {
+ if (!this.id) return undefined;
+
+ try {
+ const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
+ if (notification) {
+ runInAction(() => this.mutateNotification(notification));
+ }
+ return notification;
+ } catch (error) {
+ throw error;
+ }
+ };
+
+ /**
+ * @description mark notification as read
+ * @param { string } workspaceSlug
+ * @returns { TNotification | undefined }
+ */
+ markNotificationAsRead = async (workspaceSlug: string): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationReadAt = this.read_at;
+ try {
+ const payload: Partial = {
+ read_at: new Date().toISOString(),
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.markNotificationAsRead(workspaceSlug, this.id);
+ if (notification) {
+ runInAction(() => this.mutateNotification(notification));
+ }
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
+ throw error;
+ }
+ };
+
+ /**
+ * @description mark notification as unread
+ * @param { string } workspaceSlug
+ * @returns { TNotification | undefined }
+ */
+ markNotificationAsUnRead = async (workspaceSlug: string): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationReadAt = this.read_at;
+ try {
+ const payload: Partial = {
+ read_at: undefined,
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.markNotificationAsUnread(workspaceSlug, this.id);
+ if (notification) {
+ runInAction(() => this.mutateNotification(notification));
+ }
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ read_at: currentNotificationReadAt }));
+ throw error;
+ }
+ };
+
+ /**
+ * @description archive notification
+ * @param { string } workspaceSlug
+ * @returns { TNotification | undefined }
+ */
+ archiveNotification = async (workspaceSlug: string): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationArchivedAt = this.archived_at;
+ try {
+ const payload: Partial = {
+ archived_at: new Date().toISOString(),
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.markNotificationAsArchived(workspaceSlug, this.id);
+ if (notification) {
+ runInAction(() => this.mutateNotification(notification));
+ }
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ archived_at: currentNotificationArchivedAt }));
+ throw error;
+ }
+ };
+
+ /**
+ * @description unarchive notification
+ * @param { string } workspaceSlug
+ * @returns { TNotification | undefined }
+ */
+ unArchiveNotification = async (workspaceSlug: string): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationArchivedAt = this.archived_at;
+ try {
+ const payload: Partial = {
+ archived_at: undefined,
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.markNotificationAsUnArchived(workspaceSlug, this.id);
+ if (notification) {
+ runInAction(() => this.mutateNotification(notification));
+ }
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ archived_at: currentNotificationArchivedAt }));
+ throw error;
+ }
+ };
+
+ /**
+ * @description snooze notification
+ * @param { string } workspaceSlug
+ * @param { Date } snoozeTill
+ * @returns { TNotification | undefined }
+ */
+ snoozeNotification = async (workspaceSlug: string, snoozeTill: Date): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationSnoozeTill = this.snoozed_till;
+ try {
+ const payload: Partial = {
+ snoozed_till: snoozeTill.toISOString(),
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ snoozed_till: currentNotificationSnoozeTill }));
+ throw error;
+ }
+ };
+
+ /**
+ * @description un snooze notification
+ * @param { string } workspaceSlug
+ * @returns { TNotification | undefined }
+ */
+ unSnoozeNotification = async (workspaceSlug: string): Promise => {
+ if (!this.id) return undefined;
+
+ const currentNotificationSnoozeTill = this.snoozed_till;
+ try {
+ const payload: Partial = {
+ snoozed_till: undefined,
+ };
+ runInAction(() => this.mutateNotification(payload));
+ const notification = await workspaceNotificationService.updateNotificationById(workspaceSlug, this.id, payload);
+ return notification;
+ } catch (error) {
+ runInAction(() => this.mutateNotification({ snoozed_till: currentNotificationSnoozeTill }));
+ throw error;
+ }
+ };
+}
diff --git a/web/core/store/notifications/workspace-notifications.store.ts b/web/core/store/notifications/workspace-notifications.store.ts
new file mode 100644
index 000000000..1bdc30451
--- /dev/null
+++ b/web/core/store/notifications/workspace-notifications.store.ts
@@ -0,0 +1,319 @@
+import isEmpty from "lodash/isEmpty";
+import set from "lodash/set";
+import { action, computed, makeObservable, observable, runInAction } from "mobx";
+import { computedFn } from "mobx-utils";
+import {
+ TNotification,
+ TNotificationFilter,
+ TNotificationPaginatedInfo,
+ TNotificationPaginatedInfoQueryParams,
+ TUnreadNotificationsCount,
+} from "@plane/types";
+// constants
+import {
+ ENotificationLoader,
+ ENotificationQueryParamType,
+ ENotificationTab,
+ TNotificationTab,
+} from "@/constants/notification";
+// services
+import workspaceNotificationService from "@/services/workspace-notification.service";
+// store
+import { Notification, INotification } from "@/store/notifications/notification";
+import { CoreRootStore } from "@/store/root.store";
+
+type TNotificationLoader = ENotificationLoader | undefined;
+type TNotificationQueryParamType = ENotificationQueryParamType;
+
+export interface IWorkspaceNotificationStore {
+ // observables
+ loader: TNotificationLoader;
+ unreadNotificationsCount: TUnreadNotificationsCount | undefined;
+ notifications: Record; // notification_id -> notification
+ currentNotificationTab: TNotificationTab;
+ paginationInfo: Omit | undefined;
+ filters: TNotificationFilter;
+ // computed
+ totalUnreadNotificationsCount: number;
+ // computed functions
+ notificationIdsByWorkspaceId: (workspaceId: string) => string[] | undefined;
+ // helper actions
+ mutateNotifications: (notifications: TNotification[]) => void;
+ updateFilters: (key: T, value: TNotificationFilter[T]) => void;
+ updateBulkFilters: (filters: Partial) => void;
+ // actions
+ setCurrentNotificationTab: (tab: TNotificationTab) => void;
+ getUnreadNotificationsCount: (workspaceSlug: string) => Promise;
+ getNotifications: (
+ workspaceSlug: string,
+ loader?: TNotificationLoader,
+ queryCursorType?: TNotificationQueryParamType
+ ) => Promise;
+ markAllNotificationsAsRead: (workspaceId: string) => Promise;
+}
+
+export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
+ // constants
+ paginatedCount = 30;
+ // observables
+ loader: TNotificationLoader = undefined;
+ unreadNotificationsCount: TUnreadNotificationsCount | undefined = undefined;
+ notifications: Record = {};
+ currentNotificationTab: TNotificationTab = ENotificationTab.ALL;
+ paginationInfo: Omit | undefined = undefined;
+ filters: TNotificationFilter = {
+ type: {
+ assigned: false,
+ created: false,
+ subscribed: false,
+ },
+ snoozed: false,
+ archived: false,
+ read: false,
+ };
+
+ constructor(private store: CoreRootStore) {
+ makeObservable(this, {
+ // observables
+ loader: observable.ref,
+ unreadNotificationsCount: observable.ref,
+ notifications: observable,
+ currentNotificationTab: observable.ref,
+ paginationInfo: observable,
+ filters: observable,
+ // computed
+ totalUnreadNotificationsCount: computed,
+ // helper actions
+ setCurrentNotificationTab: action,
+ mutateNotifications: action,
+ updateFilters: action,
+ updateBulkFilters: action,
+ // actions
+ getUnreadNotificationsCount: action,
+ getNotifications: action,
+ markAllNotificationsAsRead: action,
+ });
+ }
+
+ // computed
+ get totalUnreadNotificationsCount() {
+ let count: number = 0;
+ if (!this.unreadNotificationsCount) return count;
+
+ Object.values(this.unreadNotificationsCount).forEach((value) => {
+ count += value || 0;
+ });
+ return count;
+ }
+
+ // computed functions
+ /**
+ * @description get notification ids by workspace id
+ * @param { string } workspaceId
+ */
+ notificationIdsByWorkspaceId = computedFn((workspaceId: string) => {
+ if (!workspaceId || isEmpty(this.notifications)) return undefined;
+ const workspaceNotificationIds = Object.values(this.notifications || {})
+ .filter((n) => n.workspace === workspaceId)
+ .filter((n) => {
+ if (!this.filters.archived && !this.filters.snoozed) {
+ if (n.archived_at) {
+ return false;
+ } else if (n.snoozed_till) {
+ return false;
+ } else {
+ return true;
+ }
+ } else {
+ if (this.filters.snoozed) {
+ return n.snoozed_till ? true : false;
+ } else if (this.filters.archived) {
+ return n.archived_at ? true : false;
+ } else {
+ return true;
+ }
+ }
+ })
+ // .filter((n) => (this.filters.read ? (n.read_at ? true : false) : n.read_at ? false : true))
+ .map((n) => n.id) as string[];
+ return workspaceNotificationIds;
+ });
+
+ // helper functions
+ /**
+ * @description generate notification query params
+ * @returns { object }
+ */
+ generateNotificationQueryParams = (paramType: TNotificationQueryParamType): TNotificationPaginatedInfoQueryParams => {
+ const queryParamsType =
+ Object.entries(this.filters.type)
+ .filter(([, value]) => value)
+ .map(([key]) => key)
+ .join(",") || undefined;
+
+ const currentPage = this.paginationInfo ? Number(this.paginationInfo?.prev_cursor?.split(":")[1] || 0) + 1 : 0;
+
+ const queryCursorNext =
+ paramType === ENotificationQueryParamType.INIT
+ ? `${this.paginatedCount}:0:0`
+ : paramType === ENotificationQueryParamType.CURRENT
+ ? `${this.paginatedCount}:${currentPage}:0`
+ : paramType === ENotificationQueryParamType.NEXT && this.paginationInfo
+ ? this.paginationInfo?.next_cursor
+ : `${this.paginatedCount}:${currentPage}:0`;
+
+ const queryParams: TNotificationPaginatedInfoQueryParams = {
+ type: queryParamsType,
+ snoozed: this.filters.snoozed || false,
+ archived: this.filters.archived || false,
+ read: undefined,
+ per_page: this.paginatedCount,
+ cursor: queryCursorNext,
+ };
+
+ // NOTE: This validation is required to show all the read and unread notifications in a single place it may change in future.
+ queryParams.read = this.filters.read === true ? false : undefined;
+
+ return queryParams;
+ };
+
+ // helper actions
+ /**
+ * @description mutate and validate current existing and new notifications
+ * @param { TNotification[] } notifications
+ */
+ mutateNotifications = (notifications: TNotification[]) => {
+ (notifications || []).forEach((notification) => {
+ if (!notification.id) return;
+ if (this.notifications[notification.id]) {
+ this.notifications[notification.id].mutateNotification(notification);
+ } else {
+ set(this.notifications, notification.id, new Notification(this.store, notification));
+ }
+ });
+ };
+
+ /**
+ * @description update filters
+ * @param { T extends keyof TNotificationFilter } key
+ * @param { TNotificationFilter[T] } value
+ */
+ updateFilters = (key: T, value: TNotificationFilter[T]) => {
+ set(this.filters, key, value);
+ const { workspaceSlug } = this.store.router;
+ if (!workspaceSlug) return;
+
+ set(this, "notifications", {});
+ this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT);
+ };
+
+ /**
+ * @description update bulk filters
+ * @param { Partial } filters
+ */
+ updateBulkFilters = (filters: Partial) => {
+ Object.entries(filters).forEach(([key, value]) => {
+ set(this.filters, key, value);
+ });
+
+ const { workspaceSlug } = this.store.router;
+ if (!workspaceSlug) return;
+
+ set(this, "notifications", {});
+ this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT);
+ };
+
+ // actions
+ /**
+ * @description set notification tab
+ * @returns { void }
+ */
+ setCurrentNotificationTab = (tab: TNotificationTab): void => {
+ set(this, "currentNotificationTab", tab);
+ };
+
+ /**
+ * @description get unread notifications count
+ * @param { string } workspaceSlug,
+ * @param { TNotificationQueryParamType } queryCursorType,
+ * @returns { number | undefined }
+ */
+ getUnreadNotificationsCount = async (workspaceSlug: string): Promise => {
+ try {
+ const unreadNotificationCount = await workspaceNotificationService.fetchUnreadNotificationsCount(workspaceSlug);
+ if (unreadNotificationCount)
+ runInAction(() => {
+ set(this, "unreadNotificationsCount", unreadNotificationCount);
+ });
+ return unreadNotificationCount || undefined;
+ } catch (error) {
+ console.error("WorkspaceNotificationStore -> getUnreadNotificationsCount -> error", error);
+ throw error;
+ }
+ };
+
+ /**
+ * @description get all workspace notification
+ * @param { string } workspaceSlug,
+ * @param { TNotificationLoader } loader,
+ * @returns { TNotification | undefined }
+ */
+ getNotifications = async (
+ workspaceSlug: string,
+ loader: TNotificationLoader = ENotificationLoader.INIT_LOADER,
+ queryParamType: TNotificationQueryParamType = ENotificationQueryParamType.INIT
+ ): Promise => {
+ this.loader = loader;
+ try {
+ const queryParams = this.generateNotificationQueryParams(queryParamType);
+ await this.getUnreadNotificationsCount(workspaceSlug);
+ const notificationResponse = await workspaceNotificationService.fetchNotifications(workspaceSlug, queryParams);
+ if (notificationResponse) {
+ const { results, ...paginationInfo } = notificationResponse;
+ runInAction(() => {
+ if (results) {
+ this.mutateNotifications(results);
+ }
+ set(this, "paginationInfo", paginationInfo);
+ });
+ }
+ return notificationResponse;
+ } catch (error) {
+ console.error("WorkspaceNotificationStore -> getNotifications -> error", error);
+ throw error;
+ } finally {
+ runInAction(() => (this.loader = undefined));
+ }
+ };
+
+ /**
+ * @description mark all notifications as read
+ * @param { string } workspaceSlug,
+ * @returns { void }
+ */
+ markAllNotificationsAsRead = async (workspaceSlug: string): Promise => {
+ try {
+ this.loader = ENotificationLoader.MARK_ALL_AS_READY;
+ const queryParams = this.generateNotificationQueryParams(ENotificationQueryParamType.INIT);
+ const params = {
+ type: queryParams.type,
+ snoozed: queryParams.snoozed,
+ archived: queryParams.archived,
+ read: queryParams.read,
+ };
+ await workspaceNotificationService.markAllNotificationsAsRead(workspaceSlug, params);
+ runInAction(() => {
+ Object.values(this.notifications).forEach((notification) =>
+ notification.mutateNotification({
+ read_at: new Date().toUTCString(),
+ })
+ );
+ });
+ } catch (error) {
+ console.error("WorkspaceNotificationStore -> markAllNotificationsAsRead -> error", error);
+ throw error;
+ } finally {
+ runInAction(() => (this.loader = undefined));
+ }
+ };
+}
diff --git a/web/core/store/root.store.ts b/web/core/store/root.store.ts
index 9bdf6d148..8cf126ead 100644
--- a/web/core/store/root.store.ts
+++ b/web/core/store/root.store.ts
@@ -15,6 +15,7 @@ import { IMemberRootStore, MemberRootStore } from "./member";
import { IModuleStore, ModulesStore } from "./module.store";
import { IModuleFilterStore, ModuleFilterStore } from "./module_filter.store";
import { IMultipleSelectStore, MultipleSelectStore } from "./multiple_select.store";
+import { IWorkspaceNotificationStore, WorkspaceNotificationStore } from "./notifications/workspace-notifications.store";
import { IProjectPageStore, ProjectPageStore } from "./pages/project-page.store";
import { IProjectRootStore, ProjectRootStore } from "./project";
import { IProjectViewStore, ProjectViewStore } from "./project-view.store";
@@ -50,6 +51,7 @@ export class CoreRootStore {
projectInbox: IProjectInboxStore;
projectEstimate: IProjectEstimateStore;
multipleSelect: IMultipleSelectStore;
+ workspaceNotification: IWorkspaceNotificationStore;
constructor() {
this.router = new RouterStore();
@@ -75,6 +77,7 @@ export class CoreRootStore {
this.projectInbox = new ProjectInboxStore(this);
this.projectPages = new ProjectPageStore(this);
this.projectEstimate = new ProjectEstimateStore(this);
+ this.workspaceNotification = new WorkspaceNotificationStore(this);
}
resetOnSignOut() {
@@ -103,5 +106,6 @@ export class CoreRootStore {
this.projectPages = new ProjectPageStore(this);
this.multipleSelect = new MultipleSelectStore();
this.projectEstimate = new ProjectEstimateStore(this);
+ this.workspaceNotification = new WorkspaceNotificationStore(this);
}
}