mirror of
https://github.com/lobehub/lobehub.git
synced 2026-03-26 13:19:34 +07:00
✨ feat: add archive all button and unread filter to inbox drawer
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}}\" 已生成完成",
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<ContentProps>(({ open, onMarkAsRead, onArchive }) => {
|
||||
const Content = memo<ContentProps>(({ open, unreadOnly, onMarkAsRead, onArchive }) => {
|
||||
const { t } = useTranslation('notification');
|
||||
const virtuaRef = useRef<VListHandle>(null);
|
||||
|
||||
@@ -31,12 +32,12 @@ const Content = memo<ContentProps>(({ 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<ContentProps>(({ 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<ContentProps>(({ open, onMarkAsRead, onArchive }) => {
|
||||
return (
|
||||
<Flexbox align="center" gap={12} justify="center" paddingBlock={48}>
|
||||
<Icon color={cssVar.colorTextQuaternary} icon={BellOffIcon} size={40} />
|
||||
<Text type="secondary">{t('inbox.empty')}</Text>
|
||||
<Text type="secondary">{t(unreadOnly ? 'inbox.emptyUnread' : 'inbox.empty')}</Text>
|
||||
</Flexbox>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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<InboxDrawerProps>(({ 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<InboxDrawerProps>(({ open, onClose }) => {
|
||||
refreshList();
|
||||
}, [refreshList]);
|
||||
|
||||
const handleArchiveAll = useCallback(async () => {
|
||||
await notificationService.archiveAll();
|
||||
refreshList();
|
||||
}, [refreshList]);
|
||||
|
||||
const handleToggleFilter = useCallback(() => {
|
||||
setUnreadOnly((prev) => !prev);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<SideBarDrawer
|
||||
open={open}
|
||||
title={t('inbox.title')}
|
||||
action={
|
||||
<ActionIcon
|
||||
icon={CheckCheckIcon}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('inbox.markAllRead')}
|
||||
onClick={handleMarkAllAsRead}
|
||||
/>
|
||||
<>
|
||||
<ActionIcon
|
||||
icon={ArchiveIcon}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('inbox.archiveAll')}
|
||||
onClick={handleArchiveAll}
|
||||
/>
|
||||
<ActionIcon
|
||||
icon={CheckCheckIcon}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('inbox.markAllRead')}
|
||||
onClick={handleMarkAllAsRead}
|
||||
/>
|
||||
<ActionIcon
|
||||
active={unreadOnly}
|
||||
icon={ListFilterIcon}
|
||||
size={DESKTOP_HEADER_ICON_SIZE}
|
||||
title={t('inbox.filterUnread')}
|
||||
onClick={handleToggleFilter}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
onClose={onClose}
|
||||
>
|
||||
<Content open={open} onArchive={handleArchive} onMarkAsRead={handleMarkAsRead} />
|
||||
<Content
|
||||
open={open}
|
||||
unreadOnly={unreadOnly}
|
||||
onArchive={handleArchive}
|
||||
onMarkAsRead={handleMarkAsRead}
|
||||
/>
|
||||
</SideBarDrawer>
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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 }) => {
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user