From 1912ef5b1c324cf386bb1e56a22b47065322b1e0 Mon Sep 17 00:00:00 2001 From: Philipinho <16838612+Philipinho@users.noreply.github.com> Date: Tue, 31 Mar 2026 15:38:06 +0100 Subject: [PATCH] feat: group notifications --- .../public/locales/en-US/translation.json | 2 ++ .../components/mention/mention-list.tsx | 1 + .../components/slash-menu/command-list.tsx | 13 ++++++----- .../components/slash-menu/render-items.ts | 2 +- .../components/notification-item.tsx | 1 + .../components/notification-list.tsx | 10 +++++++-- .../components/notification-popover.tsx | 22 ++++++++++++++++++- .../notification/notification.module.css | 1 + .../queries/notification-query.ts | 6 ++--- .../services/notification-service.ts | 1 + .../notification/types/notification.types.ts | 2 ++ .../core/notification/dto/notification.dto.ts | 10 ++++++++- .../notification/notification.constants.ts | 20 +++++++++++++++++ .../notification/notification.controller.ts | 7 +++--- .../core/notification/notification.service.ts | 10 ++++++--- .../repos/notification/notification.repo.ts | 15 +++++++++++-- 16 files changed, 101 insertions(+), 22 deletions(-) diff --git a/apps/client/public/locales/en-US/translation.json b/apps/client/public/locales/en-US/translation.json index a5286565..b1c1ed6c 100644 --- a/apps/client/public/locales/en-US/translation.json +++ b/apps/client/public/locales/en-US/translation.json @@ -690,6 +690,8 @@ "Get notified when your comment is resolved.": "Get notified when your comment is resolved.", "You are now watching this page": "You are now watching this page", "You are no longer watching this page": "You are no longer watching this page", + "Direct": "Direct", + "Updates": "Updates", "Today": "Today", "Yesterday": "Yesterday", "This week": "This week", diff --git a/apps/client/src/features/editor/components/mention/mention-list.tsx b/apps/client/src/features/editor/components/mention/mention-list.tsx index f086df49..af6f8a1d 100644 --- a/apps/client/src/features/editor/components/mention/mention-list.tsx +++ b/apps/client/src/features/editor/components/mention/mention-list.tsx @@ -294,6 +294,7 @@ const MentionList = forwardRef((props, ref) => { w={popupWidth} scrollbars={"y"} scrollbarSize={6} + overscrollBehavior={"contain"} styles={{ content: { minWidth: 0 } }} > {renderItems?.map((item, index) => { diff --git a/apps/client/src/features/editor/components/slash-menu/command-list.tsx b/apps/client/src/features/editor/components/slash-menu/command-list.tsx index ab1dcafd..54d6cd17 100644 --- a/apps/client/src/features/editor/components/slash-menu/command-list.tsx +++ b/apps/client/src/features/editor/components/slash-menu/command-list.tsx @@ -87,7 +87,13 @@ const CommandList = ({ return flatItems.length > 0 ? ( - + {Object.entries(items).map(([category, categoryItems]) => (
@@ -103,10 +109,7 @@ const CommandList = ({ })} > - + diff --git a/apps/client/src/features/editor/components/slash-menu/render-items.ts b/apps/client/src/features/editor/components/slash-menu/render-items.ts index 057e8214..041aa036 100644 --- a/apps/client/src/features/editor/components/slash-menu/render-items.ts +++ b/apps/client/src/features/editor/components/slash-menu/render-items.ts @@ -49,7 +49,7 @@ const renderItems = () => { getReferenceClientRect = props.clientRect; popup = document.createElement("div"); - popup.style.zIndex = "9999"; + popup.style.zIndex = "199"; popup.style.position = "absolute"; popup.style.top = "0"; popup.style.left = "0"; diff --git a/apps/client/src/features/notification/components/notification-item.tsx b/apps/client/src/features/notification/components/notification-item.tsx index c1118fff..0fd4f44b 100644 --- a/apps/client/src/features/notification/components/notification-item.tsx +++ b/apps/client/src/features/notification/components/notification-item.tsx @@ -77,6 +77,7 @@ export function NotificationItem({ }; const handleMarkRead = (e: React.MouseEvent) => { + e.preventDefault(); e.stopPropagation(); markReadIfNeeded(); }; diff --git a/apps/client/src/features/notification/components/notification-list.tsx b/apps/client/src/features/notification/components/notification-list.tsx index 4c992c57..4cd30677 100644 --- a/apps/client/src/features/notification/components/notification-list.tsx +++ b/apps/client/src/features/notification/components/notification-list.tsx @@ -3,17 +3,23 @@ import { IconBellOff } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { useEffect, useRef } from "react"; import { NotificationItem } from "./notification-item"; -import { INotification, NotificationFilter } from "../types/notification.types"; +import { + INotification, + NotificationFilter, + NotificationTab, +} from "../types/notification.types"; import { groupNotificationsByTime } from "../notification.utils"; import { useNotificationsQuery } from "../queries/notification-query"; import classes from "../notification.module.css"; type NotificationListProps = { + tab: NotificationTab; filter: NotificationFilter; onNavigate: () => void; }; export function NotificationList({ + tab, filter, onNavigate, }: NotificationListProps) { @@ -24,7 +30,7 @@ export function NotificationList({ hasNextPage, fetchNextPage, isFetchingNextPage, - } = useNotificationsQuery(); + } = useNotificationsQuery(tab as string); const sentinelRef = useRef(null); diff --git a/apps/client/src/features/notification/components/notification-popover.tsx b/apps/client/src/features/notification/components/notification-popover.tsx index 8ebfedad..161ac1e6 100644 --- a/apps/client/src/features/notification/components/notification-popover.tsx +++ b/apps/client/src/features/notification/components/notification-popover.tsx @@ -6,6 +6,7 @@ import { Menu, Popover, ScrollArea, + Tabs, Text, Tooltip, } from "@mantine/core"; @@ -18,15 +19,20 @@ import { } from "@tabler/icons-react"; import { useTranslation } from "react-i18next"; import { NotificationList } from "./notification-list"; -import { NotificationFilter } from "../types/notification.types"; +import { + NotificationFilter, + NotificationTab, +} from "../types/notification.types"; import { useMarkAllReadMutation, useUnreadCountQuery, } from "../queries/notification-query"; +import classes from "../notification.module.css"; export function NotificationPopover() { const { t } = useTranslation(); const [opened, setOpened] = useState(false); + const [tab, setTab] = useState("direct"); const [filter, setFilter] = useState("all"); const { data: unreadData } = useUnreadCountQuery(); @@ -125,13 +131,27 @@ export function NotificationPopover() { + setTab(value as NotificationTab)} + variant="default" + color="dark" + > + + {t("Direct")} + {t("Updates")} + + + setOpened(false)} /> diff --git a/apps/client/src/features/notification/notification.module.css b/apps/client/src/features/notification/notification.module.css index d56986ac..09802628 100644 --- a/apps/client/src/features/notification/notification.module.css +++ b/apps/client/src/features/notification/notification.module.css @@ -13,3 +13,4 @@ .divider { border-color: light-dark(var(--mantine-color-gray-2), var(--mantine-color-dark-5)); } + diff --git a/apps/client/src/features/notification/queries/notification-query.ts b/apps/client/src/features/notification/queries/notification-query.ts index 363482b1..92c46560 100644 --- a/apps/client/src/features/notification/queries/notification-query.ts +++ b/apps/client/src/features/notification/queries/notification-query.ts @@ -15,10 +15,10 @@ import { export const NOTIFICATION_KEY = ["notifications"]; export const UNREAD_COUNT_KEY = ["notifications", "unread-count"]; -export function useNotificationsQuery() { +export function useNotificationsQuery(type?: string) { return useInfiniteQuery({ - queryKey: NOTIFICATION_KEY, - queryFn: ({ pageParam }) => getNotifications({ cursor: pageParam }), + queryKey: [...NOTIFICATION_KEY, type], + queryFn: ({ pageParam }) => getNotifications({ cursor: pageParam, type }), initialPageParam: undefined as string | undefined, getNextPageParam: (lastPage) => lastPage.meta.hasNextPage ? lastPage.meta.nextCursor : undefined, diff --git a/apps/client/src/features/notification/services/notification-service.ts b/apps/client/src/features/notification/services/notification-service.ts index 8adf4909..7e4b8d2c 100644 --- a/apps/client/src/features/notification/services/notification-service.ts +++ b/apps/client/src/features/notification/services/notification-service.ts @@ -5,6 +5,7 @@ import { IPagination } from "@/lib/types"; export async function getNotifications(params: { limit?: number; cursor?: string; + type?: string; }): Promise> { const req = await api.post>( "/notifications", diff --git a/apps/client/src/features/notification/types/notification.types.ts b/apps/client/src/features/notification/types/notification.types.ts index 93550028..f64e3648 100644 --- a/apps/client/src/features/notification/types/notification.types.ts +++ b/apps/client/src/features/notification/types/notification.types.ts @@ -39,3 +39,5 @@ export type INotification = { }; export type NotificationFilter = "all" | "unread"; + +export type NotificationTab = "direct" | "updates" | "all"; diff --git a/apps/server/src/core/notification/dto/notification.dto.ts b/apps/server/src/core/notification/dto/notification.dto.ts index 0b0bde94..b583c746 100644 --- a/apps/server/src/core/notification/dto/notification.dto.ts +++ b/apps/server/src/core/notification/dto/notification.dto.ts @@ -1,4 +1,5 @@ -import { IsArray, IsOptional, IsUUID } from 'class-validator'; +import { IsArray, IsIn, IsOptional, IsString, IsUUID } from 'class-validator'; +import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; export class NotificationIdDto { @IsUUID() @@ -11,3 +12,10 @@ export class MarkNotificationsReadDto { @IsOptional() notificationIds?: string[]; } + +export class ListNotificationsDto extends PaginationOptions { + @IsOptional() + @IsString() + @IsIn(['direct', 'updates', 'all']) + type?: 'direct' | 'updates' | 'all' = 'all'; +} diff --git a/apps/server/src/core/notification/notification.constants.ts b/apps/server/src/core/notification/notification.constants.ts index b619d52f..8f7f5049 100644 --- a/apps/server/src/core/notification/notification.constants.ts +++ b/apps/server/src/core/notification/notification.constants.ts @@ -26,3 +26,23 @@ export const NotificationTypeToSettingKey: Partial< [NotificationType.COMMENT_CREATED]: 'comment.created', [NotificationType.COMMENT_RESOLVED]: 'comment.resolved', }; + +export type NotificationTab = 'direct' | 'updates' | 'all'; + +export const DIRECT_NOTIFICATION_TYPES: NotificationType[] = [ + NotificationType.COMMENT_USER_MENTION, + NotificationType.COMMENT_CREATED, + NotificationType.COMMENT_RESOLVED, + NotificationType.PAGE_USER_MENTION, + NotificationType.PAGE_PERMISSION_GRANTED, +]; + +export const UPDATES_NOTIFICATION_TYPES: NotificationType[] = [ + NotificationType.PAGE_UPDATED, +]; + +export function getTypesForTab(tab: NotificationTab): NotificationType[] | undefined { + if (tab === 'direct') return DIRECT_NOTIFICATION_TYPES; + if (tab === 'updates') return UPDATES_NOTIFICATION_TYPES; + return undefined; +} diff --git a/apps/server/src/core/notification/notification.controller.ts b/apps/server/src/core/notification/notification.controller.ts index d041414f..be5ee1d3 100644 --- a/apps/server/src/core/notification/notification.controller.ts +++ b/apps/server/src/core/notification/notification.controller.ts @@ -9,9 +9,8 @@ import { import { NotificationService } from './notification.service'; import { AuthUser } from '../../common/decorators/auth-user.decorator'; import { JwtAuthGuard } from '../../common/guards/jwt-auth.guard'; -import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { User } from '@docmost/db/types/entity.types'; -import { MarkNotificationsReadDto } from './dto/notification.dto'; +import { ListNotificationsDto, MarkNotificationsReadDto } from './dto/notification.dto'; @UseGuards(JwtAuthGuard) @Controller('notifications') @@ -21,10 +20,10 @@ export class NotificationController { @HttpCode(HttpStatus.OK) @Post('/') async getNotifications( - @Body() pagination: PaginationOptions, + @Body() dto: ListNotificationsDto, @AuthUser() user: User, ) { - return this.notificationService.findByUserId(user.id, pagination); + return this.notificationService.findByUserId(user.id, dto, dto.type); } @HttpCode(HttpStatus.OK) diff --git a/apps/server/src/core/notification/notification.service.ts b/apps/server/src/core/notification/notification.service.ts index 6ea1cbda..cbbc3e5a 100644 --- a/apps/server/src/core/notification/notification.service.ts +++ b/apps/server/src/core/notification/notification.service.ts @@ -6,7 +6,7 @@ import { InsertableNotification } from '@docmost/db/types/entity.types'; import { PaginationOptions } from '@docmost/db/pagination/pagination-options'; import { WsGateway } from '../../ws/ws.gateway'; import { MailService } from '../../integrations/mail/mail.service'; -import { NotificationType, NotificationTypeToSettingKey } from './notification.constants'; +import { NotificationTab, NotificationType, NotificationTypeToSettingKey } from './notification.constants'; @Injectable() export class NotificationService { @@ -39,8 +39,12 @@ export class NotificationService { return notification; } - async findByUserId(userId: string, pagination: PaginationOptions) { - return this.notificationRepo.findByUserId(userId, pagination); + async findByUserId( + userId: string, + pagination: PaginationOptions, + type: NotificationTab = 'all', + ) { + return this.notificationRepo.findByUserId(userId, pagination, type); } async getUnreadCount(userId: string) { diff --git a/apps/server/src/database/repos/notification/notification.repo.ts b/apps/server/src/database/repos/notification/notification.repo.ts index eac0a0d3..2914dbfc 100644 --- a/apps/server/src/database/repos/notification/notification.repo.ts +++ b/apps/server/src/database/repos/notification/notification.repo.ts @@ -11,6 +11,7 @@ import { ExpressionBuilder } from 'kysely'; import { DB } from '@docmost/db/types/db'; import { jsonObjectFrom } from 'kysely/helpers/postgres'; import { SpaceMemberRepo } from '@docmost/db/repos/space/space-member.repo'; +import { NotificationTab, NotificationType } from '../../../core/notification/notification.constants'; @Injectable() export class NotificationRepo { @@ -27,8 +28,12 @@ export class NotificationRepo { .executeTakeFirst(); } - async findByUserId(userId: string, pagination: PaginationOptions) { - const query = this.db + async findByUserId( + userId: string, + pagination: PaginationOptions, + type: NotificationTab = 'all', + ) { + let query = this.db .selectFrom('notifications') .selectAll('notifications') .select((eb) => this.withActor(eb)) @@ -42,6 +47,12 @@ export class NotificationRepo { ]), ); + if (type === 'direct') { + query = query.where('type', '!=', NotificationType.PAGE_UPDATED); + } else if (type === 'updates') { + query = query.where('type', '=', NotificationType.PAGE_UPDATED); + } + return executeWithCursorPagination(query, { perPage: pagination.limit, cursor: pagination.cursor,