From db79c00ea7dd8e63ef924312099cea166d0330cf Mon Sep 17 00:00:00 2001 From: fengmk2 Date: Tue, 8 Jul 2025 15:19:45 +0800 Subject: [PATCH] feat(server): support read all notifications (#13083) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit close AF-2719 #### PR Dependency Tree * **PR #13083** 👈 This tree was auto-generated by [Charcoal](https://github.com/danerwilliams/charcoal) ## Summary by CodeRabbit * **New Features** * Added the ability to mark all notifications as read with a single action. * **Bug Fixes** * Ensured notifications marked as read are no longer shown as unread. * **Tests** * Introduced new tests to verify the functionality of marking all notifications as read. --- .../e2e/notification/resolver.spec.ts | 39 +++++++++++++++++++ .../server/src/core/notification/resolver.ts | 8 ++++ .../server/src/core/notification/service.ts | 4 ++ .../src/models/__tests__/notification.spec.ts | 29 ++++++++++++++ .../backend/server/src/models/notification.ts | 12 ++++++ packages/backend/server/src/schema.gql | 3 ++ packages/common/graphql/src/graphql/index.ts | 8 ++++ .../src/graphql/read-all-notifications.gql | 3 ++ packages/common/graphql/src/schema.ts | 16 ++++++++ 9 files changed, 122 insertions(+) create mode 100644 packages/common/graphql/src/graphql/read-all-notifications.gql diff --git a/packages/backend/server/src/__tests__/e2e/notification/resolver.spec.ts b/packages/backend/server/src/__tests__/e2e/notification/resolver.spec.ts index 57a562100d..1dd0748f3a 100644 --- a/packages/backend/server/src/__tests__/e2e/notification/resolver.spec.ts +++ b/packages/backend/server/src/__tests__/e2e/notification/resolver.spec.ts @@ -8,6 +8,7 @@ import { notificationCountQuery, NotificationObjectType, NotificationType, + readAllNotificationsMutation, readNotificationMutation, } from '@affine/graphql'; @@ -677,3 +678,41 @@ e2e('should list and count notifications', async t => { t.is(result3.currentUser!.notifications.edges.length, 0); } }); + +e2e('should mark all notifications as read', async t => { + const { member, owner, workspace } = await init(); + await app.login(owner); + + await app.gql({ + query: mentionUserMutation, + variables: { + input: { + userId: member.id, + workspaceId: workspace.id, + doc: { + id: 'doc-id-1', + title: 'doc-title-1', + blockId: 'block-id-1', + mode: DocMode.page, + }, + }, + }, + }); + + await app.login(member); + + await app.gql({ + query: readAllNotificationsMutation, + }); + + const result = await app.gql({ + query: listNotificationsQuery, + variables: { + pagination: { + first: 10, + offset: 0, + }, + }, + }); + t.is(result.currentUser!.notifications.totalCount, 0); +}); diff --git a/packages/backend/server/src/core/notification/resolver.ts b/packages/backend/server/src/core/notification/resolver.ts index 91461c43c4..54829378c0 100644 --- a/packages/backend/server/src/core/notification/resolver.ts +++ b/packages/backend/server/src/core/notification/resolver.ts @@ -100,6 +100,14 @@ export class UserNotificationResolver { await this.service.markAsRead(me.id, notificationId); return true; } + + @Mutation(() => Boolean, { + description: 'mark all notifications as read', + }) + async readAllNotifications(@CurrentUser() me: UserType) { + await this.service.markAllAsRead(me.id); + return true; + } } @Resolver(() => NotificationObjectType) diff --git a/packages/backend/server/src/core/notification/service.ts b/packages/backend/server/src/core/notification/service.ts index 9a9759d4ed..529d8397b1 100644 --- a/packages/backend/server/src/core/notification/service.ts +++ b/packages/backend/server/src/core/notification/service.ts @@ -399,6 +399,10 @@ export class NotificationService { } } + async markAllAsRead(userId: string) { + await this.models.notification.markAllAsRead(userId); + } + /** * Find notifications by user id, order by createdAt desc */ diff --git a/packages/backend/server/src/models/__tests__/notification.spec.ts b/packages/backend/server/src/models/__tests__/notification.spec.ts index cc5e101537..1358f338fb 100644 --- a/packages/backend/server/src/models/__tests__/notification.spec.ts +++ b/packages/backend/server/src/models/__tests__/notification.spec.ts @@ -430,3 +430,32 @@ test('should create a comment mention notification', async t => { t.is(notification.body.commentId, commentId); t.is(notification.body.replyId, replyId); }); + +test('should mark all notifications as read', async t => { + await models.notification.createMention({ + userId: user.id, + body: { + workspaceId: workspace.id, + doc: { + id: docId, + title: 'doc-title', + blockId: 'blockId', + mode: DocMode.page, + }, + createdByUserId: createdBy.id, + }, + }); + await models.notification.createInvitation({ + userId: user.id, + body: { + workspaceId: workspace.id, + createdByUserId: createdBy.id, + inviteId: randomUUID(), + }, + }); + + await models.notification.markAllAsRead(user.id); + + const notifications = await models.notification.findManyByUserId(user.id); + t.is(notifications.length, 0); +}); diff --git a/packages/backend/server/src/models/notification.ts b/packages/backend/server/src/models/notification.ts index 279b9a6bd8..6d1888bd87 100644 --- a/packages/backend/server/src/models/notification.ts +++ b/packages/backend/server/src/models/notification.ts @@ -261,6 +261,18 @@ export class NotificationModel extends BaseModel { }); } + async markAllAsRead(userId: string) { + const { count } = await this.db.notification.updateMany({ + where: { userId }, + data: { + read: true, + }, + }); + this.logger.log( + `Marked all notifications as read for user ${userId}, count: ${count}` + ); + } + /** * Find many notifications by user id, exclude read notifications by default */ diff --git a/packages/backend/server/src/schema.gql b/packages/backend/server/src/schema.gql index 6d8081598f..7c8858389a 100644 --- a/packages/backend/server/src/schema.gql +++ b/packages/backend/server/src/schema.gql @@ -1236,6 +1236,9 @@ type Mutation { """queue workspace doc embedding""" queueWorkspaceEmbedding(docId: [String!]!, workspaceId: String!): Boolean! + """mark all notifications as read""" + readAllNotifications: Boolean! + """mark notification as read""" readNotification(id: String!): Boolean! recoverDoc(guid: String!, timestamp: DateTime!, workspaceId: String!): DateTime! diff --git a/packages/common/graphql/src/graphql/index.ts b/packages/common/graphql/src/graphql/index.ts index 7fae7a5fe8..557a280566 100644 --- a/packages/common/graphql/src/graphql/index.ts +++ b/packages/common/graphql/src/graphql/index.ts @@ -1989,6 +1989,14 @@ export const quotaQuery = { deprecations: ["'storageQuota' is deprecated: use `UserQuotaType['usedStorageQuota']` instead"], }; +export const readAllNotificationsMutation = { + id: 'readAllNotificationsMutation' as const, + op: 'readAllNotifications', + query: `mutation readAllNotifications { + readAllNotifications +}`, +}; + export const readNotificationMutation = { id: 'readNotificationMutation' as const, op: 'readNotification', diff --git a/packages/common/graphql/src/graphql/read-all-notifications.gql b/packages/common/graphql/src/graphql/read-all-notifications.gql new file mode 100644 index 0000000000..d38617aa89 --- /dev/null +++ b/packages/common/graphql/src/graphql/read-all-notifications.gql @@ -0,0 +1,3 @@ +mutation readAllNotifications { + readAllNotifications +} diff --git a/packages/common/graphql/src/schema.ts b/packages/common/graphql/src/schema.ts index a1965c8fbb..1714e37f38 100644 --- a/packages/common/graphql/src/schema.ts +++ b/packages/common/graphql/src/schema.ts @@ -1371,6 +1371,8 @@ export interface Mutation { publishPage: DocType; /** queue workspace doc embedding */ queueWorkspaceEmbedding: Scalars['Boolean']['output']; + /** mark all notifications as read */ + readAllNotifications: Scalars['Boolean']['output']; /** mark notification as read */ readNotification: Scalars['Boolean']['output']; recoverDoc: Scalars['DateTime']['output']; @@ -5218,6 +5220,15 @@ export type QuotaQuery = { } | null; }; +export type ReadAllNotificationsMutationVariables = Exact<{ + [key: string]: never; +}>; + +export type ReadAllNotificationsMutation = { + __typename?: 'Mutation'; + readAllNotifications: boolean; +}; + export type ReadNotificationMutationVariables = Exact<{ id: Scalars['String']['input']; }>; @@ -6352,6 +6363,11 @@ export type Mutations = variables: PublishPageMutationVariables; response: PublishPageMutation; } + | { + name: 'readAllNotificationsMutation'; + variables: ReadAllNotificationsMutationVariables; + response: ReadAllNotificationsMutation; + } | { name: 'readNotificationMutation'; variables: ReadNotificationMutationVariables;