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:
@@ -2,9 +2,32 @@ import { cssVar } from '@toeverything/theme';
|
|||||||
import { cssVarV2 } from '@toeverything/theme/v2';
|
import { cssVarV2 } from '@toeverything/theme/v2';
|
||||||
import { keyframes, style } from '@vanilla-extract/css';
|
import { keyframes, style } from '@vanilla-extract/css';
|
||||||
|
|
||||||
export const containerScrollViewport = style({
|
export const container = style({
|
||||||
maxHeight: '448px',
|
maxHeight: '448px',
|
||||||
width: '360px',
|
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({
|
export const itemList = style({
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import {
|
|||||||
Avatar,
|
Avatar,
|
||||||
Button,
|
Button,
|
||||||
IconButton,
|
IconButton,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
notify,
|
notify,
|
||||||
observeIntersection,
|
observeIntersection,
|
||||||
Scrollable,
|
Scrollable,
|
||||||
@@ -31,6 +33,7 @@ import {
|
|||||||
CollaborationIcon,
|
CollaborationIcon,
|
||||||
DeleteIcon,
|
DeleteIcon,
|
||||||
EdgelessIcon,
|
EdgelessIcon,
|
||||||
|
MoreHorizontalIcon,
|
||||||
NotificationIcon,
|
NotificationIcon,
|
||||||
PageIcon,
|
PageIcon,
|
||||||
} from '@blocksuite/icons/rc';
|
} from '@blocksuite/icons/rc';
|
||||||
@@ -80,37 +83,59 @@ export const NotificationList = () => {
|
|||||||
return;
|
return;
|
||||||
}, [hasMore, notificationListService]);
|
}, [hasMore, notificationListService]);
|
||||||
|
|
||||||
return (
|
const handleDeleteAll = useCallback(() => {
|
||||||
<Scrollable.Root>
|
notificationListService.readAllNotifications().catch(err => {
|
||||||
<Scrollable.Viewport className={styles.containerScrollViewport}>
|
notify.error(UserFriendlyError.fromAny(err));
|
||||||
{notifications.length > 0 ? (
|
});
|
||||||
<ul className={styles.itemList}>
|
}, [notificationListService]);
|
||||||
{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
|
return (
|
||||||
ref={loadMoreIndicatorRef}
|
<div className={styles.container}>
|
||||||
className={hasMore ? styles.loadMoreIndicator : ''}
|
<div className={styles.header}>
|
||||||
>
|
<span>{t['com.affine.rootAppSidebar.notifications']()}</span>
|
||||||
{hasMore ? t['com.affine.notification.loading-more']() : null}
|
{notifications.length > 0 && (
|
||||||
</div>
|
<Menu
|
||||||
</Scrollable.Viewport>
|
items={
|
||||||
<Scrollable.Scrollbar />
|
<MenuItem prefixIcon={<DeleteIcon />} onClick={handleDeleteAll}>
|
||||||
</Scrollable.Root>
|
<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)
|
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,
|
mentionUserMutation,
|
||||||
notificationCountQuery,
|
notificationCountQuery,
|
||||||
type PaginationInput,
|
type PaginationInput,
|
||||||
|
readAllNotificationsMutation,
|
||||||
readNotificationMutation,
|
readNotificationMutation,
|
||||||
type UnionNotificationBodyType,
|
type UnionNotificationBodyType,
|
||||||
} from '@affine/graphql';
|
} from '@affine/graphql';
|
||||||
@@ -85,6 +86,12 @@ export class NotificationStore extends Store {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
readAllNotifications() {
|
||||||
|
return this.gqlService.gql({
|
||||||
|
query: readAllNotificationsMutation,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async mentionUser(
|
async mentionUser(
|
||||||
userId: string,
|
userId: string,
|
||||||
workspaceId: string,
|
workspaceId: string,
|
||||||
|
|||||||
@@ -7778,6 +7778,10 @@ export function useAFFiNEI18N(): {
|
|||||||
* `Accept & Join`
|
* `Accept & Join`
|
||||||
*/
|
*/
|
||||||
["com.affine.notification.invitation.accept"](): string;
|
["com.affine.notification.invitation.accept"](): string;
|
||||||
|
/**
|
||||||
|
* `Delete all notifications`
|
||||||
|
*/
|
||||||
|
["com.affine.notification.delete-all"](): string;
|
||||||
/**
|
/**
|
||||||
* `Tips`
|
* `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-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": "<1>{{username}}</1> invited you to join <2>{{workspaceName}}</2>",
|
||||||
"com.affine.notification.invitation.accept": "Accept & Join",
|
"com.affine.notification.invitation.accept": "Accept & Join",
|
||||||
|
"com.affine.notification.delete-all": "Delete all notifications",
|
||||||
"tips": "Tips",
|
"tips": "Tips",
|
||||||
"Template": "Template",
|
"Template": "Template",
|
||||||
"com.affine.template-list.delete": "Delete Template",
|
"com.affine.template-list.delete": "Delete Template",
|
||||||
|
|||||||
Reference in New Issue
Block a user