[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 <gurusainath007@gmail.com>
This commit is contained in:
Bavisetti Narayan 2024-07-05 16:13:09 +05:30 committed by GitHub
parent 837f09ed90
commit 54a5e5e761
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
9 changed files with 141 additions and 55 deletions

View file

@ -45,6 +45,7 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
archived = request.GET.get("archived", "false") archived = request.GET.get("archived", "false")
read = request.GET.get("read", None) read = request.GET.get("read", None)
type = request.GET.get("type", "all") type = request.GET.get("type", "all")
mentioned = request.GET.get("mentioned", False)
q_filters = Q() q_filters = Q()
inbox_issue = Issue.objects.filter( inbox_issue = Issue.objects.filter(
@ -86,6 +87,13 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
if read == "true": if read == "true":
notifications = notifications.filter(read_at__isnull=False) 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(",") type = type.split(",")
# Subscribed issues # Subscribed issues
if "subscribed" in type: if "subscribed" in type:
@ -210,19 +218,35 @@ class NotificationViewSet(BaseViewSet, BasePaginator):
class UnreadNotificationEndpoint(BaseAPIView): class UnreadNotificationEndpoint(BaseAPIView):
def get(self, request, slug): def get(self, request, slug):
# Watching Issues Count # 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, workspace__slug=slug,
receiver_id=request.user.id, receiver_id=request.user.id,
read_at__isnull=True, read_at__isnull=True,
archived_at__isnull=True, archived_at__isnull=True,
snoozed_till__isnull=True, snoozed_till__isnull=True,
sender__icontains="mentioned",
).count() ).count()
return Response( return Response(
{ {
"total_unread_notifications_count": int( "total_unread_notifications_count": int(
unread_notifications_count unread_notifications_count
) ),
"mention_unread_notifications_count": int(
mention_notifications_count
),
}, },
status=status.HTTP_200_OK, status=status.HTTP_200_OK,
) )

View file

@ -64,6 +64,7 @@ export type TNotificationPaginatedInfoQueryParams = {
type?: string | undefined; type?: string | undefined;
snoozed?: boolean; snoozed?: boolean;
archived?: boolean; archived?: boolean;
mentioned?: boolean;
read?: boolean; read?: boolean;
per_page?: number; per_page?: number;
cursor?: string; cursor?: string;
@ -86,6 +87,7 @@ export type TNotificationPaginatedInfo = {
// notification count // notification count
export type TUnreadNotificationsCount = { export type TUnreadNotificationsCount = {
total_unread_notifications_count: number; total_unread_notifications_count: number;
mention_unread_notifications_count: number;
}; };
export type TCurrentSelectedNotification = { export type TCurrentSelectedNotification = {

View file

@ -23,14 +23,20 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = o
workspaceSlug ? () => getUnreadNotificationsCount(workspaceSlug) : null 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) if (isSidebarCollapsed)
return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />; return <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
return ( return (
<div className="ml-auto px-2.5 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl"> <div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
{getNumberCount(unreadNotificationsCount.total_unread_notifications_count)} {`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`}
</div> </div>
); );
}); });

View file

@ -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 <EmptyState type={currentTabEmptyState} layout="screen-simple" />;
});

View file

@ -3,25 +3,18 @@
import { FC } from "react"; import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { Bell } from "lucide-react"; import { Bell } from "lucide-react";
import { Breadcrumbs, Tooltip } from "@plane/ui"; import { Breadcrumbs } from "@plane/ui";
// components // components
import { BreadcrumbLink } from "@/components/common"; import { BreadcrumbLink } from "@/components/common";
import { SidebarHamburgerToggle } from "@/components/core"; import { SidebarHamburgerToggle } from "@/components/core";
import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications"; import { NotificationSidebarHeaderOptions } from "@/components/workspace-notifications";
// helpers
import { getNumberCount } from "@/helpers/string.helper";
// hooks
import { usePlatformOS } from "@/hooks/use-platform-os";
type TNotificationSidebarHeader = { type TNotificationSidebarHeader = {
workspaceSlug: string; workspaceSlug: string;
notificationsCount: number;
}; };
export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => { export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observer((props) => {
const { workspaceSlug, notificationsCount } = props; const { workspaceSlug } = props;
// hooks
const { isMobile } = usePlatformOS();
if (!workspaceSlug) return <></>; if (!workspaceSlug) return <></>;
return ( return (
@ -35,20 +28,7 @@ export const NotificationSidebarHeader: FC<TNotificationSidebarHeader> = observe
type="text" type="text"
link={ link={
<BreadcrumbLink <BreadcrumbLink
label={ label="Notifications"
<div className="flex items-center gap-2">
<div className="font-medium">Notifications</div>
<Tooltip
isMobile={isMobile}
tooltipContent={`There are ${notificationsCount} ${notificationsCount > 1 ? "notifications" : "notification"} in this workspace`}
position="bottom"
>
<div className="px-2.5 py-0.5 bg-custom-primary-100/20 text-custom-primary-100 text-xs font-semibold rounded-xl">
{getNumberCount(notificationsCount)}
</div>
</Tooltip>
</div>
}
icon={<Bell className="h-4 w-4 text-custom-text-300" />} icon={<Bell className="h-4 w-4 text-custom-text-300" />}
disableTooltip disableTooltip
/> />

View file

@ -1,4 +1,5 @@
export * from "./loader"; export * from "./loader";
export * from "./empty-state";
export * from "./root"; export * from "./root";

View file

@ -4,16 +4,18 @@ import { FC } from "react";
import { observer } from "mobx-react"; import { observer } from "mobx-react";
import { useParams } from "next/navigation"; import { useParams } from "next/navigation";
// components // components
import { EmptyState } from "@/components/empty-state";
import { import {
NotificationsLoader,
NotificationEmptyState,
NotificationSidebarHeader, NotificationSidebarHeader,
AppliedFilters, AppliedFilters,
NotificationsLoader,
NotificationCardListRoot, NotificationCardListRoot,
} from "@/components/workspace-notifications"; } from "@/components/workspace-notifications";
// constants // constants
import { EmptyStateType } from "@/constants/empty-state"; import { NOTIFICATION_TABS } from "@/constants/notification";
import { ENotificationTab } from "@/constants/notification"; // helpers
import { cn } from "@/helpers/common.helper";
import { getNumberCount } from "@/helpers/string.helper";
// hooks // hooks
import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store"; import { useWorkspace, useWorkspaceNotifications } from "@/hooks/store";
@ -21,25 +23,54 @@ export const NotificationsSidebar: FC = observer(() => {
const { workspaceSlug } = useParams(); const { workspaceSlug } = useParams();
// hooks // hooks
const { getWorkspaceBySlug } = useWorkspace(); const { getWorkspaceBySlug } = useWorkspace();
const { unreadNotificationsCount, loader, notificationIdsByWorkspaceId } = useWorkspaceNotifications(); const {
unreadNotificationsCount,
loader,
notificationIdsByWorkspaceId,
currentNotificationTab,
setCurrentNotificationTab,
} = useWorkspaceNotifications();
// derived values // derived values
const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined; const workspace = workspaceSlug ? getWorkspaceBySlug(workspaceSlug.toString()) : undefined;
const notificationIds = workspace ? notificationIdsByWorkspaceId(workspace.id) : 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 <></>; if (!workspaceSlug || !workspace) return <></>;
return ( return (
<div className="relative w-full h-full overflow-hidden flex flex-col"> <div className="relative w-full h-full overflow-hidden flex flex-col">
<div className="border-b border-custom-border-200"> <div className="border-b border-custom-border-200">
<NotificationSidebarHeader <NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
workspaceSlug={workspaceSlug.toString()} </div>
notificationsCount={totalNotificationCount}
/> <div className="flex-shrink-0 w-full h-[46px] border-b border-custom-border-200 px-5 relative flex items-center gap-2">
{NOTIFICATION_TABS.map((tab) => (
<div
key={tab.value}
className="h-full px-3 relative flex flex-col cursor-pointer"
onClick={() => currentNotificationTab != tab.value && setCurrentNotificationTab(tab.value)}
>
<div
className={cn(
`relative h-full flex justify-center items-center gap-1 text-sm transition-all`,
currentNotificationTab === tab.value
? "text-custom-primary-100"
: "text-custom-text-100 hover:text-custom-text-200"
)}
>
<div className="font-medium">{tab.label}</div>
<div
className={cn(
`rounded-full text-xs px-1.5 py-0.5`,
currentNotificationTab === tab.value ? `bg-custom-primary-100/20` : `bg-custom-background-80/50`
)}
>
{getNumberCount(tab.count(unreadNotificationsCount))}
</div>
</div>
{currentNotificationTab === tab.value && (
<div className="border absolute bottom-0 right-0 left-0 rounded-t-md border-custom-primary-100" />
)}
</div>
))}
</div> </div>
{/* applied filters */} {/* applied filters */}
@ -49,7 +80,7 @@ export const NotificationsSidebar: FC = observer(() => {
{/* rendering notifications */} {/* rendering notifications */}
{loader === "init-loader" ? ( {loader === "init-loader" ? (
<div className="relative w-full h-full overflow-hidden p-5"> <div className="relative w-full h-full overflow-hidden">
<NotificationsLoader /> <NotificationsLoader />
</div> </div>
) : ( ) : (
@ -60,7 +91,7 @@ export const NotificationsSidebar: FC = observer(() => {
</div> </div>
) : ( ) : (
<div className="relative w-full h-full flex justify-center items-center"> <div className="relative w-full h-full flex justify-center items-center">
<EmptyState type={currentTabEmptyState} layout="screen-simple" /> <NotificationEmptyState />
</div> </div>
)} )}
</> </>

View file

@ -1,3 +1,5 @@
import { TUnreadNotificationsCount } from "@plane/types";
export enum ENotificationTab { export enum ENotificationTab {
ALL = "all", ALL = "all",
MENTIONS = "mentions", MENTIONS = "mentions",
@ -29,11 +31,14 @@ export const NOTIFICATION_TABS = [
{ {
label: "All", label: "All",
value: ENotificationTab.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 = [ export const FILTER_TYPE_OPTIONS = [

View file

@ -66,6 +66,7 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
loader: TNotificationLoader = undefined; loader: TNotificationLoader = undefined;
unreadNotificationsCount: TUnreadNotificationsCount = { unreadNotificationsCount: TUnreadNotificationsCount = {
total_unread_notifications_count: 0, total_unread_notifications_count: 0,
mention_unread_notifications_count: 0,
}; };
notifications: Record<string, INotification> = {}; notifications: Record<string, INotification> = {};
currentNotificationTab: TNotificationTab = ENotificationTab.ALL; 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. // 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; queryParams.read = this.filters.read === true ? false : undefined;
if (this.currentNotificationTab === ENotificationTab.MENTIONS) queryParams.mentioned = true;
return queryParams; return queryParams;
}; };
@ -242,6 +245,12 @@ export class WorkspaceNotificationStore implements IWorkspaceNotificationStore {
*/ */
setCurrentNotificationTab = (tab: TNotificationTab): void => { setCurrentNotificationTab = (tab: TNotificationTab): void => {
set(this, "currentNotificationTab", tab); 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 * @param { "increment" | "decrement" } type
* @returns { void } * @returns { void }
*/ */
setUnreadNotificationsCount = (type: "increment" | "decrement"): void => setUnreadNotificationsCount = (type: "increment" | "decrement"): void => {
runInAction(() => { switch (this.currentNotificationTab) {
update(this.unreadNotificationsCount, "total_unread_notifications_count", (count: 0) => case ENotificationTab.ALL:
type === "increment" ? count + 1 : count - 1 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 * @description get unread notifications count