[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")
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,
)

View file

@ -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 = {

View file

@ -23,14 +23,20 @@ export const NotificationAppSidebarOption: FC<TNotificationAppSidebarOption> = 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 <div className="absolute right-3.5 top-2 h-2 w-2 rounded-full bg-custom-primary-300" />;
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">
{getNumberCount(unreadNotificationsCount.total_unread_notifications_count)}
<div className="text-[8px] ml-auto bg-custom-primary-100 text-white p-1 py-0.5 rounded-full">
{`${isMentionsEnabled ? `@` : ``}${getNumberCount(totalNotifications)}`}
</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 { 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<TNotificationSidebarHeader> = 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<TNotificationSidebarHeader> = observe
type="text"
link={
<BreadcrumbLink
label={
<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>
}
label="Notifications"
icon={<Bell className="h-4 w-4 text-custom-text-300" />}
disableTooltip
/>

View file

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

View file

@ -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 (
<div className="relative w-full h-full overflow-hidden flex flex-col">
<div className="border-b border-custom-border-200">
<NotificationSidebarHeader
workspaceSlug={workspaceSlug.toString()}
notificationsCount={totalNotificationCount}
/>
<NotificationSidebarHeader workspaceSlug={workspaceSlug.toString()} />
</div>
<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>
{/* applied filters */}
@ -49,7 +80,7 @@ export const NotificationsSidebar: FC = observer(() => {
{/* rendering notifications */}
{loader === "init-loader" ? (
<div className="relative w-full h-full overflow-hidden p-5">
<div className="relative w-full h-full overflow-hidden">
<NotificationsLoader />
</div>
) : (
@ -60,7 +91,7 @@ export const NotificationsSidebar: FC = observer(() => {
</div>
) : (
<div className="relative w-full h-full flex justify-center items-center">
<EmptyState type={currentTabEmptyState} layout="screen-simple" />
<NotificationEmptyState />
</div>
)}
</>

View file

@ -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 = [

View file

@ -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<string, INotification> = {};
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