feat(core): clear all notifications (#13144)

<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## 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.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
EYHN
2025-07-11 10:50:48 +08:00
committed by GitHub
parent fc1104cd68
commit d98b45ca3d
6 changed files with 110 additions and 31 deletions
@@ -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({
@@ -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 (
<Scrollable.Root>
<Scrollable.Viewport className={styles.containerScrollViewport}>
{notifications.length > 0 ? (
<ul className={styles.itemList}>
{notifications.map(notification => (
<li key={notification.id}>
<NotificationItem notification={notification} />
</li>
))}
{userFriendlyError && (
<div className={styles.error}>{userFriendlyError.message}</div>
)}
</ul>
) : isLoading ? (
<NotificationItemSkeleton />
) : userFriendlyError ? (
<div className={styles.error}>{userFriendlyError.message}</div>
) : (
<NotificationListEmpty />
)}
const handleDeleteAll = useCallback(() => {
notificationListService.readAllNotifications().catch(err => {
notify.error(UserFriendlyError.fromAny(err));
});
}, [notificationListService]);
<div
ref={loadMoreIndicatorRef}
className={hasMore ? styles.loadMoreIndicator : ''}
>
{hasMore ? t['com.affine.notification.loading-more']() : null}
</div>
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
return (
<div className={styles.container}>
<div className={styles.header}>
<span>{t['com.affine.rootAppSidebar.notifications']()}</span>
{notifications.length > 0 && (
<Menu
items={
<MenuItem prefixIcon={<DeleteIcon />} onClick={handleDeleteAll}>
<span>{t['com.affine.notification.delete-all']()}</span>
</MenuItem>
}
>
<IconButton icon={<MoreHorizontalIcon />} />
</Menu>
)}
</div>
<Scrollable.Root className={styles.scrollRoot}>
<Scrollable.Viewport className={styles.scrollViewport}>
{notifications.length > 0 ? (
<ul className={styles.itemList}>
{notifications.map(notification => (
<li key={notification.id}>
<NotificationItem notification={notification} />
</li>
))}
{userFriendlyError && (
<div className={styles.error}>{userFriendlyError.message}</div>
)}
</ul>
) : isLoading ? (
<NotificationItemSkeleton />
) : userFriendlyError ? (
<div className={styles.error}>{userFriendlyError.message}</div>
) : (
<NotificationListEmpty />
)}
<div
ref={loadMoreIndicatorRef}
className={hasMore ? styles.loadMoreIndicator : ''}
>
{hasMore ? t['com.affine.notification.loading-more']() : null}
</div>
</Scrollable.Viewport>
<Scrollable.Scrollbar />
</Scrollable.Root>
</div>
);
};
@@ -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;
}
}
}
@@ -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,
+4
View File
@@ -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`
*/
@@ -1948,6 +1948,7 @@
"com.affine.notification.invitation-blocked": "There is an issue regarding your invitation to <1>{{workspaceName}}</1> ",
"com.affine.notification.invitation": "<1>{{username}}</1> invited you to join <2>{{workspaceName}}</2>",
"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",