From d98b45ca3df06a6d204038af1681550dd3285568 Mon Sep 17 00:00:00 2001 From: EYHN Date: Fri, 11 Jul 2025 10:50:48 +0800 Subject: [PATCH] feat(core): clear all notifications (#13144) ## Summary by CodeRabbit * **New Features** * Added a "Delete All" option in the notifications list, allowing users to mark all notifications as read at once. * Introduced a header with a menu button in the notifications list for easier access to actions. * **Style** * Updated notification list layout with improved structure, including a header and a scrollable content area. * **Localization** * Added a new English localization string for the "Delete all notifications" action. --- .../components/notification/list.style.css.ts | 25 +++++- .../core/src/components/notification/list.tsx | 85 ++++++++++++------- .../src/modules/notification/services/list.ts | 19 +++++ .../notification/stores/notification.ts | 7 ++ packages/frontend/i18n/src/i18n.gen.ts | 4 + packages/frontend/i18n/src/resources/en.json | 1 + 6 files changed, 110 insertions(+), 31 deletions(-) diff --git a/packages/frontend/core/src/components/notification/list.style.css.ts b/packages/frontend/core/src/components/notification/list.style.css.ts index b934cddf19..e46b70dee3 100644 --- a/packages/frontend/core/src/components/notification/list.style.css.ts +++ b/packages/frontend/core/src/components/notification/list.style.css.ts @@ -2,9 +2,32 @@ import { cssVar } from '@toeverything/theme'; import { cssVarV2 } from '@toeverything/theme/v2'; import { keyframes, style } from '@vanilla-extract/css'; -export const containerScrollViewport = style({ +export const container = style({ maxHeight: '448px', width: '360px', + display: 'flex', + flexDirection: 'column', +}); + +export const header = style({ + display: 'flex', + alignItems: 'center', + justifyContent: 'space-between', + fontSize: cssVar('fontSm'), + lineHeight: '22px', + padding: '4px 8px 8px', + borderBottom: `1px solid ${cssVarV2('layer/insideBorder/border')}`, +}); + +export const scrollRoot = style({ + flex: 1, + display: 'flex', + flexDirection: 'column', +}); + +export const scrollViewport = style({ + flex: 1, + padding: '8px 0px ', }); export const itemList = style({ diff --git a/packages/frontend/core/src/components/notification/list.tsx b/packages/frontend/core/src/components/notification/list.tsx index 33adee58a7..566b51dfd5 100644 --- a/packages/frontend/core/src/components/notification/list.tsx +++ b/packages/frontend/core/src/components/notification/list.tsx @@ -2,6 +2,8 @@ import { Avatar, Button, IconButton, + Menu, + MenuItem, notify, observeIntersection, Scrollable, @@ -31,6 +33,7 @@ import { CollaborationIcon, DeleteIcon, EdgelessIcon, + MoreHorizontalIcon, NotificationIcon, PageIcon, } from '@blocksuite/icons/rc'; @@ -80,37 +83,59 @@ export const NotificationList = () => { return; }, [hasMore, notificationListService]); - return ( - - - {notifications.length > 0 ? ( -
    - {notifications.map(notification => ( -
  • - -
  • - ))} - {userFriendlyError && ( -
    {userFriendlyError.message}
    - )} -
- ) : isLoading ? ( - - ) : userFriendlyError ? ( -
{userFriendlyError.message}
- ) : ( - - )} + const handleDeleteAll = useCallback(() => { + notificationListService.readAllNotifications().catch(err => { + notify.error(UserFriendlyError.fromAny(err)); + }); + }, [notificationListService]); -
- {hasMore ? t['com.affine.notification.loading-more']() : null} -
-
- -
+ return ( +
+
+ {t['com.affine.rootAppSidebar.notifications']()} + {notifications.length > 0 && ( + } onClick={handleDeleteAll}> + {t['com.affine.notification.delete-all']()} + + } + > + } /> + + )} +
+ + + {notifications.length > 0 ? ( +
    + {notifications.map(notification => ( +
  • + +
  • + ))} + {userFriendlyError && ( +
    {userFriendlyError.message}
    + )} +
+ ) : isLoading ? ( + + ) : userFriendlyError ? ( +
{userFriendlyError.message}
+ ) : ( + + )} + +
+ {hasMore ? t['com.affine.notification.loading-more']() : null} +
+
+ +
+
); }; diff --git a/packages/frontend/core/src/modules/notification/services/list.ts b/packages/frontend/core/src/modules/notification/services/list.ts index 9e860b885a..020c96a3c5 100644 --- a/packages/frontend/core/src/modules/notification/services/list.ts +++ b/packages/frontend/core/src/modules/notification/services/list.ts @@ -88,4 +88,23 @@ export class NotificationListService extends Service { Math.max(this.notificationCount.count$.value - 1, 0) ); } + + async readAllNotifications() { + // optimistic clear all notifications + this.reset(); + this.notificationCount.setCount(0); + // avoid loading more notifications after clear all notifications + this.hasMore$.setValue(false); + + try { + await this.store.readAllNotifications(); + } catch (err) { + // rollback the optimistic clear all notifications + this.reset(); + this.loadMore(); + + // rethrow the error to the caller, to notify the user + throw err; + } + } } diff --git a/packages/frontend/core/src/modules/notification/stores/notification.ts b/packages/frontend/core/src/modules/notification/stores/notification.ts index f7f663149f..945e74bfcd 100644 --- a/packages/frontend/core/src/modules/notification/stores/notification.ts +++ b/packages/frontend/core/src/modules/notification/stores/notification.ts @@ -5,6 +5,7 @@ import { mentionUserMutation, notificationCountQuery, type PaginationInput, + readAllNotificationsMutation, readNotificationMutation, type UnionNotificationBodyType, } from '@affine/graphql'; @@ -85,6 +86,12 @@ export class NotificationStore extends Store { }); } + readAllNotifications() { + return this.gqlService.gql({ + query: readAllNotificationsMutation, + }); + } + async mentionUser( userId: string, workspaceId: string, diff --git a/packages/frontend/i18n/src/i18n.gen.ts b/packages/frontend/i18n/src/i18n.gen.ts index e97a94decf..91f8040ee3 100644 --- a/packages/frontend/i18n/src/i18n.gen.ts +++ b/packages/frontend/i18n/src/i18n.gen.ts @@ -7778,6 +7778,10 @@ export function useAFFiNEI18N(): { * `Accept & Join` */ ["com.affine.notification.invitation.accept"](): string; + /** + * `Delete all notifications` + */ + ["com.affine.notification.delete-all"](): string; /** * `Tips` */ diff --git a/packages/frontend/i18n/src/resources/en.json b/packages/frontend/i18n/src/resources/en.json index 060ce21459..cd9e8f5523 100644 --- a/packages/frontend/i18n/src/resources/en.json +++ b/packages/frontend/i18n/src/resources/en.json @@ -1948,6 +1948,7 @@ "com.affine.notification.invitation-blocked": "There is an issue regarding your invitation to <1>{{workspaceName}} ", "com.affine.notification.invitation": "<1>{{username}} invited you to join <2>{{workspaceName}}", "com.affine.notification.invitation.accept": "Accept & Join", + "com.affine.notification.delete-all": "Delete all notifications", "tips": "Tips", "Template": "Template", "com.affine.template-list.delete": "Delete Template",