[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:
parent
837f09ed90
commit
54a5e5e761
9 changed files with 141 additions and 55 deletions
|
|
@ -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,
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -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" />;
|
||||||
|
});
|
||||||
|
|
@ -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
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,5 @@
|
||||||
export * from "./loader";
|
export * from "./loader";
|
||||||
|
export * from "./empty-state";
|
||||||
|
|
||||||
export * from "./root";
|
export * from "./root";
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -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 = [
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue