From 54a5e5e761df78e25817e45bba4274743ae2721a Mon Sep 17 00:00:00 2001 From: Bavisetti Narayan <72156168+NarayanBavisetti@users.noreply.github.com> Date: Fri, 5 Jul 2024 16:13:09 +0530 Subject: [PATCH] [WEB-1437] feat: notifications mention filter (#5040) * chore: implemented mentions on the notification * chore: mention notification filter * chore: handled mentions refetch and total count on header and sidebar menu option * chore: seperated notifications empty state * chore: updated sidebar menu option notification vaidation * chore: handled notificaition sidebar total notifications count --------- Co-authored-by: gurusainath --- .../plane/app/views/notification/base.py | 28 +++++++- .../types/src/workspace-notifications.d.ts | 2 + .../notification-app-sidebar-option.tsx | 12 +++- .../sidebar/empty-state.tsx | 18 +++++ .../sidebar/header/root.tsx | 26 +------- .../workspace-notifications/sidebar/index.ts | 1 + .../workspace-notifications/sidebar/root.tsx | 65 ++++++++++++++----- web/core/constants/notification.ts | 13 ++-- .../workspace-notifications.store.ts | 31 +++++++-- 9 files changed, 141 insertions(+), 55 deletions(-) create mode 100644 web/core/components/workspace-notifications/sidebar/empty-state.tsx diff --git a/apiserver/plane/app/views/notification/base.py b/apiserver/plane/app/views/notification/base.py index 12997241c..c67dec557 100644 --- a/apiserver/plane/app/views/notification/base.py +++ b/apiserver/plane/app/views/notification/base.py @@ -45,6 +45,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator): archived = request.GET.get("archived", "false") read = request.GET.get("read", None) type = request.GET.get("type", "all") + mentioned = request.GET.get("mentioned", False) q_filters = Q() inbox_issue = Issue.objects.filter( @@ -86,6 +87,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator): if read == "true": notifications = notifications.filter(read_at__isnull=False) + if mentioned: + notifications = notifications.filter(sender__icontains="mentioned") + else: + notifications = notifications.exclude( + sender__icontains="mentioned" + ) + type = type.split(",") # Subscribed issues if "subscribed" in type: @@ -210,19 +218,35 @@ class NotificationViewSet(BaseViewSet, BasePaginator): class UnreadNotificationEndpoint(BaseAPIView): def get(self, request, slug): # Watching Issues Count - unread_notifications_count = Notification.objects.filter( + unread_notifications_count = ( + Notification.objects.filter( + workspace__slug=slug, + receiver_id=request.user.id, + read_at__isnull=True, + archived_at__isnull=True, + snoozed_till__isnull=True, + ) + .exclude(sender__icontains="mentioned") + .count() + ) + + mention_notifications_count = Notification.objects.filter( workspace__slug=slug, receiver_id=request.user.id, read_at__isnull=True, archived_at__isnull=True, snoozed_till__isnull=True, + sender__icontains="mentioned", ).count() return Response( { "total_unread_notifications_count": int( unread_notifications_count - ) + ), + "mention_unread_notifications_count": int( + mention_notifications_count + ), }, status=status.HTTP_200_OK, ) diff --git a/packages/types/src/workspace-notifications.d.ts b/packages/types/src/workspace-notifications.d.ts index 0e5bb0975..485af600a 100644 --- a/packages/types/src/workspace-notifications.d.ts +++ b/packages/types/src/workspace-notifications.d.ts @@ -64,6 +64,7 @@ export type TNotificationPaginatedInfoQueryParams = { type?: string | undefined; snoozed?: boolean; archived?: boolean; + mentioned?: boolean; read?: boolean; per_page?: number; cursor?: string; @@ -86,6 +87,7 @@ export type TNotificationPaginatedInfo = { // notification count export type TUnreadNotificationsCount = { total_unread_notifications_count: number; + mention_unread_notifications_count: number; }; export type TCurrentSelectedNotification = { diff --git a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx index 0a7335df4..109cfcde5 100644 --- a/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx +++ b/web/core/components/workspace-notifications/notification-app-sidebar-option.tsx @@ -23,14 +23,20 @@ export const NotificationAppSidebarOption: FC = o workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null ); - if (unreadNotificationsCount.total_unread_notifications_count <= 0) return <>; + // derived values + const isMentionsEnabled = unreadNotificationsCount.mention_unread_notifications_count > 0 ? true : false; + const totalNotifications = isMentionsEnabled + ? unreadNotificationsCount.mention_unread_notifications_count + : unreadNotificationsCount.total_unread_notifications_count; + + if (totalNotifications <= 0) return <>; if (isSidebarCollapsed) return
; return ( -
- {getNumberCount(unreadNotificationsCount.total_unread_notifications_count)} +
+ {`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`}
); }); diff --git a/web/core/components/workspace-notifications/sidebar/empty-state.tsx b/web/core/components/workspace-notifications/sidebar/empty-state.tsx new file mode 100644 index 000000000..3b2b0c0f5 --- /dev/null +++ b/web/core/components/workspace-notifications/sidebar/empty-state.tsx @@ -0,0 +1,18 @@ +"use client"; + +import { FC } from "react"; +import { observer } from "mobx-react"; +// components +import { EmptyState } from "@/components/empty-state"; +// constants +import { EmptyStateType } from "@/constants/empty-state"; +import { ENotificationTab } from "@/constants/notification"; + +export const NotificationEmptyState: FC = observer(() => { + // derived values + const currentTabEmptyState = ENotificationTab.ALL + ? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE + : EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE; + + return ; +}); diff --git a/web/core/components/workspace-notifications/sidebar/header/root.tsx b/web/core/components/workspace-notifications/sidebar/header/root.tsx index 05bb1e44c..5804c1837 100644 --- a/web/core/components/workspace-notifications/sidebar/header/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/header/root.tsx @@ -3,25 +3,18 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { Bell } from "lucide-react"; -import { Breadcrumbs, Tooltip } from "@plane/ui"; +import { Breadcrumbs } from "@plane/ui"; // components import { BreadcrumbLink } from "@/components/common"; import { SidebarHamburgerToggle } from "@/components/core"; import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications"; -// helpers -import { getNumberCount } from "@/helpers/string.helper"; -// hooks -import { usePlatformOS } from "@/hooks/use-platform-os"; type TNotificationSidebarHeader = { workspaceSlug: string; - notificationsCount: number; }; export const NotificationSidebarHeader: FC = observer((props) => { - const { workspaceSlug, notificationsCount } = props; - // hooks - const { isMobile } = usePlatformOS(); + const { workspaceSlug } = props; if (!workspaceSlug) return <>; return ( @@ -35,20 +28,7 @@ export const NotificationSidebarHeader: FC = observe type="text" link={ -
Notifications
- 1 ? "notifications" : "notification"} in this workspace`} - position="bottom" - > -
- {getNumberCount(notificationsCount)} -
-
-
- } + label="Notifications" icon={} disableTooltip /> diff --git a/web/core/components/workspace-notifications/sidebar/index.ts b/web/core/components/workspace-notifications/sidebar/index.ts index 52dc7bde7..d8ab5c4ed 100644 --- a/web/core/components/workspace-notifications/sidebar/index.ts +++ b/web/core/components/workspace-notifications/sidebar/index.ts @@ -1,4 +1,5 @@ export * from "./loader"; +export * from "./empty-state"; export * from "./root"; diff --git a/web/core/components/workspace-notifications/sidebar/root.tsx b/web/core/components/workspace-notifications/sidebar/root.tsx index 50c52c8e0..e9fab27a8 100644 --- a/web/core/components/workspace-notifications/sidebar/root.tsx +++ b/web/core/components/workspace-notifications/sidebar/root.tsx @@ -4,16 +4,18 @@ import { FC } from "react"; import { observer } from "mobx-react"; import { useParams } from "next/navigation"; // components -import { EmptyState } from "@/components/empty-state"; import { + NotificationsLoader, + NotificationEmptyState, NotificationSidebarHeader, AppliedFilters, - NotificationsLoader, NotificationCardListRoot, } from "@/components/workspace-notifications"; // constants -import { EmptyStateType } from "@/constants/empty-state"; -import { ENotificationTab } from "@/constants/notification"; +import { NOTIFICATION_TABS } from "@/constants/notification"; +// helpers +import { cn } from "@/helpers/common.helper"; +import { getNumberCount } from "@/helpers/string.helper"; // hooks import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; @@ -21,25 +23,54 @@ export const NotificationsSidebar: FC = observer(() => { const { workspaceSlug } = useParams(); // hooks const { getWorkspaceBySlug } = useWorkspace(); - const { unreadNotificationsCount, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications(); + const { + unreadNotificationsCount, + loader, + notificationIdsByWorkspaceId, + currentNotificationTab, + setCurrentNotificationTab, + } = useWorkspaceNotifications(); // derived values const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : undefined; - // derived values - const currentTabEmptyState = ENotificationTab.ALL - ? EmptyStateType.NOTIFICATION_ALL_EMPTY_STATE - : EmptyStateType.NOTIFICATION_MENTIONS_EMPTY_STATE; - const totalNotificationCount = unreadNotificationsCount.total_unread_notifications_count; - if (!workspaceSlug || !workspace) return <>; return (
- + +
+ +
+ {NOTIFICATION_TABS.map((tab) => ( +
currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)} + > +
+
{tab.label}
+
+ {getNumberCount(tab.count(unreadNotificationsCount))} +
+
+ {currentNotificationTab === tab.value && ( +
+ )} +
+ ))}
{/* applied filters */} @@ -49,7 +80,7 @@ export const NotificationsSidebar: FC = observer(() => { {/* rendering notifications */} {loader === "init-loader" ? ( -
+
) : ( @@ -60,7 +91,7 @@ export const NotificationsSidebar: FC = observer(() => {
) : (
- +
)} diff --git a/web/core/constants/notification.ts b/web/core/constants/notification.ts index fe4f0a026..36ab3c8ee 100644 --- a/web/core/constants/notification.ts +++ b/web/core/constants/notification.ts @@ -1,3 +1,5 @@ +import { TUnreadNotificationsCount } from "@plane/types"; + export enum ENotificationTab { ALL = "all", MENTIONS = "mentions", @@ -29,11 +31,14 @@ export const NOTIFICATION_TABS = [ { label: "All", value: ENotificationTab.ALL, + count: (unReadNotification: TUnreadNotificationsCount) => unReadNotification?.total_unread_notifications_count || 0, + }, + { + label: "Mentions", + value: ENotificationTab.MENTIONS, + count: (unReadNotification: TUnreadNotificationsCount) => + unReadNotification?.mention_unread_notifications_count || 0, }, - // { - // label: "Mentions", - // value: ENotificationTab.MENTIONS, - // }, ]; export const FILTER_TYPE_OPTIONS = [ diff --git a/web/core/store/notifications/workspace-notifications.store.ts b/web/core/store/notifications/workspace-notifications.store.ts index 59e9b07a2..bbc7e563b 100644 --- a/web/core/store/notifications/workspace-notifications.store.ts +++ b/web/core/store/notifications/workspace-notifications.store.ts @@ -66,6 +66,7 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore { loader: TNotificationLoader = undefined; unreadNotificationsCount: TUnreadNotificationsCount = { total_unread_notifications_count: 0, + mention_unread_notifications_count: 0, }; notifications: Record = {}; currentNotificationTab: TNotificationTab = ENotificationTab.ALL; @@ -186,6 +187,8 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore { // 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; + if (this.currentNotificationTab === ENotificationTab.MENTIONS) queryParams.mentioned = true; + return queryParams; }; @@ -242,6 +245,12 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore { */ setCurrentNotificationTab = (tab: TNotificationTab): void => { set(this, "currentNotificationTab", tab); + + const { workspaceSlug } = this.store.router; + if (!workspaceSlug) return; + + set(this, "notifications", {}); + this.getNotifications(workspaceSlug, ENotificationLoader.INIT_LOADER, ENotificationQueryParamType.INIT); }; /** @@ -258,12 +267,22 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore { * @param { "increment" | "decrement" } type * @returns { void } */ - setUnreadNotificationsCount = (type: "increment" | "decrement"): void => - runInAction(() => { - update(this.unreadNotificationsCount, "total_unread_notifications_count", (count: 0) => - type === "increment" ? count + 1 : count - 1 - ); - }); + setUnreadNotificationsCount = (type: "increment" | "decrement"): void => { + switch (this.currentNotificationTab) { + case ENotificationTab.ALL: + update(this.unreadNotificationsCount, "total_unread_notifications_count", (count: 0) => + type === "increment" ? count + 1 : count - 1 + ); + break; + case ENotificationTab.MENTIONS: + update(this.unreadNotificationsCount, "mention_unread_notifications_count", (count: 0) => + type === "increment" ? count + 1 : count - 1 + ); + break; + default: + break; + } + }; /** * @description get unread notifications count