From 48819b5bbf5602cee1aa399815bf8b465e9cd019 Mon Sep 17 00:00:00 2001 From: YuTengjing Date: Wed, 25 Mar 2026 19:51:44 +0800 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20add=20archive=20all=20butto?= =?UTF-8?q?n=20and=20unread=20filter=20to=20inbox=20drawer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- locales/en-US/notification.json | 3 ++ locales/zh-CN/notification.json | 3 ++ packages/database/src/models/notification.ts | 10 +++- src/locales/default/notification.ts | 3 ++ .../Header/components/InboxDrawer/Content.tsx | 36 +++++++------- .../Header/components/InboxDrawer/index.tsx | 48 +++++++++++++++---- src/server/routers/lambda/notification.ts | 1 + src/services/notification.ts | 7 ++- 8 files changed, 83 insertions(+), 28 deletions(-) diff --git a/locales/en-US/notification.json b/locales/en-US/notification.json index 31839eaa35..5d74fb66ce 100644 --- a/locales/en-US/notification.json +++ b/locales/en-US/notification.json @@ -1,7 +1,10 @@ { "image_generation_completed": "Image \"{{prompt}}\" generated successfully", "image_generation_completed_title": "Image Generated", + "inbox.archiveAll": "Archive all", "inbox.empty": "No notifications yet", + "inbox.emptyUnread": "No unread notifications", + "inbox.filterUnread": "Show unread only", "inbox.markAllRead": "Mark all as read", "inbox.title": "Notifications", "video_generation_completed": "Video \"{{prompt}}\" generated successfully", diff --git a/locales/zh-CN/notification.json b/locales/zh-CN/notification.json index 17679a431d..35600dab87 100644 --- a/locales/zh-CN/notification.json +++ b/locales/zh-CN/notification.json @@ -1,7 +1,10 @@ { "image_generation_completed": "图片 \"{{prompt}}\" 已生成完成", "image_generation_completed_title": "图片已生成", + "inbox.archiveAll": "全部归档", "inbox.empty": "暂无通知", + "inbox.emptyUnread": "没有未读通知", + "inbox.filterUnread": "仅显示未读", "inbox.markAllRead": "全部标为已读", "inbox.title": "通知", "video_generation_completed": "视频 \"{{prompt}}\" 已生成完成", diff --git a/packages/database/src/models/notification.ts b/packages/database/src/models/notification.ts index 087f564683..c6b47706de 100644 --- a/packages/database/src/models/notification.ts +++ b/packages/database/src/models/notification.ts @@ -13,11 +13,17 @@ export class NotificationModel { this.userId = userId; } - async list(opts: { category?: string; cursor?: string; limit?: number } = {}) { - const { cursor, limit = 20, category } = opts; + async list( + opts: { category?: string; cursor?: string; limit?: number; unreadOnly?: boolean } = {}, + ) { + const { cursor, limit = 20, category, unreadOnly } = opts; const conditions = [eq(notifications.userId, this.userId), eq(notifications.isArchived, false)]; + if (unreadOnly) { + conditions.push(eq(notifications.isRead, false)); + } + if (category) { conditions.push(eq(notifications.category, category)); } diff --git a/src/locales/default/notification.ts b/src/locales/default/notification.ts index e4199409e1..d464ae0bf6 100644 --- a/src/locales/default/notification.ts +++ b/src/locales/default/notification.ts @@ -1,7 +1,10 @@ export default { 'image_generation_completed': 'Image "{{prompt}}" generated successfully', 'image_generation_completed_title': 'Image Generated', + 'inbox.archiveAll': 'Archive all', 'inbox.empty': 'No notifications yet', + 'inbox.emptyUnread': 'No unread notifications', + 'inbox.filterUnread': 'Show unread only', 'inbox.markAllRead': 'Mark all as read', 'inbox.title': 'Notifications', 'video_generation_completed': 'Video "{{prompt}}" generated successfully', diff --git a/src/routes/(main)/home/_layout/Header/components/InboxDrawer/Content.tsx b/src/routes/(main)/home/_layout/Header/components/InboxDrawer/Content.tsx index 78eef3d558..9d42bd78ed 100644 --- a/src/routes/(main)/home/_layout/Header/components/InboxDrawer/Content.tsx +++ b/src/routes/(main)/home/_layout/Header/components/InboxDrawer/Content.tsx @@ -3,7 +3,7 @@ import { Flexbox, Icon, Text } from '@lobehub/ui'; import { cssVar } from 'antd-style'; import { BellOffIcon } from 'lucide-react'; -import { memo, useCallback, useRef } from 'react'; +import { memo, useCallback, useEffect, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import useSWRInfinite from 'swr/infinite'; import { VList, type VListHandle } from 'virtua'; @@ -20,9 +20,10 @@ interface ContentProps { onArchive: (id: string) => void; onMarkAsRead: (id: string) => void; open: boolean; + unreadOnly?: boolean; } -const Content = memo(({ open, onMarkAsRead, onArchive }) => { +const Content = memo(({ open, unreadOnly, onMarkAsRead, onArchive }) => { const { t } = useTranslation('notification'); const virtuaRef = useRef(null); @@ -31,12 +32,12 @@ const Content = memo(({ open, onMarkAsRead, onArchive }) => { if (!open) return null; if (previousPageData && previousPageData.length < PAGE_SIZE) return null; - if (pageIndex === 0) return [FETCH_KEY] as const; + if (pageIndex === 0) return [FETCH_KEY, undefined, unreadOnly] as const; const lastItem = previousPageData?.at(-1); - return [FETCH_KEY, lastItem?.id] as const; + return [FETCH_KEY, lastItem?.id, unreadOnly] as const; }, - [open], + [open, unreadOnly], ); const { @@ -44,16 +45,19 @@ const Content = memo(({ open, onMarkAsRead, onArchive }) => { isLoading, isValidating, setSize, - } = useSWRInfinite( - getKey, - async ([, cursor]) => { - return notificationService.list({ - cursor: cursor as string | undefined, - limit: PAGE_SIZE, - }); - }, - { dedupingInterval: 0 }, - ); + } = useSWRInfinite(getKey, async ([, cursor, filterUnread]) => { + return notificationService.list({ + cursor: cursor as string | undefined, + limit: PAGE_SIZE, + unreadOnly: filterUnread, + }); + }); + + // Reset scroll position and pagination when filter changes + useEffect(() => { + setSize(1); + virtuaRef.current?.scrollTo(0); + }, [unreadOnly, setSize]); const notifications = pages?.flat() ?? []; const hasMore = pages ? pages.at(-1)?.length === PAGE_SIZE : false; @@ -80,7 +84,7 @@ const Content = memo(({ open, onMarkAsRead, onArchive }) => { return ( - {t('inbox.empty')} + {t(unreadOnly ? 'inbox.emptyUnread' : 'inbox.empty')} ); } diff --git a/src/routes/(main)/home/_layout/Header/components/InboxDrawer/index.tsx b/src/routes/(main)/home/_layout/Header/components/InboxDrawer/index.tsx index 4645e86565..052c569fc6 100644 --- a/src/routes/(main)/home/_layout/Header/components/InboxDrawer/index.tsx +++ b/src/routes/(main)/home/_layout/Header/components/InboxDrawer/index.tsx @@ -1,8 +1,8 @@ 'use client'; import { ActionIcon, Flexbox } from '@lobehub/ui'; -import { CheckCheckIcon } from 'lucide-react'; -import { memo, useCallback } from 'react'; +import { ArchiveIcon, CheckCheckIcon, ListFilterIcon } from 'lucide-react'; +import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { DESKTOP_HEADER_ICON_SIZE } from '@/const/layoutTokens'; @@ -30,6 +30,7 @@ interface InboxDrawerProps { const InboxDrawer = memo(({ open, onClose }) => { const { t } = useTranslation('notification'); + const [unreadOnly, setUnreadOnly] = useState(false); const refreshList = useCallback(() => { mutate((key: unknown) => Array.isArray(key) && key[0] === FETCH_KEY); @@ -57,21 +58,50 @@ const InboxDrawer = memo(({ open, onClose }) => { refreshList(); }, [refreshList]); + const handleArchiveAll = useCallback(async () => { + await notificationService.archiveAll(); + refreshList(); + }, [refreshList]); + + const handleToggleFilter = useCallback(() => { + setUnreadOnly((prev) => !prev); + }, []); + return ( + <> + + + + } onClose={onClose} > - + ); }); diff --git a/src/server/routers/lambda/notification.ts b/src/server/routers/lambda/notification.ts index f5b2c89132..d4f50dedb0 100644 --- a/src/server/routers/lambda/notification.ts +++ b/src/server/routers/lambda/notification.ts @@ -29,6 +29,7 @@ export const notificationRouter = router({ category: z.string().optional(), cursor: z.string().optional(), limit: z.number().min(1).max(50).default(20), + unreadOnly: z.boolean().optional(), }), ) .query(async ({ ctx, input }) => { diff --git a/src/services/notification.ts b/src/services/notification.ts index ca34c8cac4..720a57ad20 100644 --- a/src/services/notification.ts +++ b/src/services/notification.ts @@ -1,7 +1,12 @@ import { lambdaClient } from '@/libs/trpc/client'; class NotificationService { - list = (params?: { category?: string; cursor?: string; limit?: number }) => { + list = (params?: { + category?: string; + cursor?: string; + limit?: number; + unreadOnly?: boolean; + }) => { return lambdaClient.notification.list.query(params); };