[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")
|
||||
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,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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 = {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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 { 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
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
export * from "./loader";
|
||||
export * from "./empty-state";
|
||||
|
||||
export * from "./root";
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue