fix(notifications): keep notifications visible when marking all as read

Emit a notification:all-read event instead of notification:cleared when marking all notifications as read, and handle this event on the client by updating the read status rather than emptying the notification list.

Closes #587
This commit is contained in:
neon
2026-07-08 16:23:38 +00:00
parent e0d4bae9cf
commit 512f16e288
5 changed files with 40 additions and 3 deletions
@@ -363,6 +363,31 @@ describe('useNotifications', () => {
expect(notifications.value.every((n) => n.read)).toBe(true)
expect(unreadCount.value).toBe(0)
})
it('keeps notifications visible when all-read socket event arrives', async () => {
const useNotifications = await loadModule()
const { fetchNotifications, notifications, subscribe, total, unreadCount } = useNotifications()
mockApi.mockReturnValueOnce(
mockOk({
items: [makeNotification({ id: 1, read: false }), makeNotification({ id: 2, read: false })],
total: 2,
}),
)
await fetchNotifications(true)
unreadCount.value = 2
subscribe()
const allReadHandler = mockSocket.on.mock.calls.find(([event]: unknown[]) => event === 'notification:all-read')?.[1] as (() => void) | undefined
expect(allReadHandler).toBeDefined()
allReadHandler!()
expect(notifications.value).toHaveLength(2)
expect(notifications.value.every((n) => n.read)).toBe(true)
expect(total.value).toBe(2)
expect(unreadCount.value).toBe(0)
})
})
describe('dismiss', () => {
@@ -114,6 +114,11 @@ function getSocket(): Socket {
total.value = Math.max(0, total.value - 1)
})
socket.on('notification:all-read', () => {
notifications.value.forEach((n) => (n.read = true))
unreadCount.value = 0
})
socket.on('notification:cleared', () => {
notifications.value = []
total.value = 0
@@ -81,6 +81,10 @@ export class NotificationGateway implements OnGatewayInit, OnGatewayConnection,
this.server?.to(`user:${userId}`).emit('notification:dismissed', { id: notificationId });
}
emitAllRead(userId: number): void {
this.server?.to(`user:${userId}`).emit('notification:all-read', {});
}
emitCleared(userId: number): void {
this.server?.to(`user:${userId}`).emit('notification:cleared', {});
}
@@ -24,6 +24,7 @@ describe('NotificationService', () => {
emitCountUpdate: ReturnType<typeof vi.fn>;
emitRead: ReturnType<typeof vi.fn>;
emitDismissed: ReturnType<typeof vi.fn>;
emitAllRead: ReturnType<typeof vi.fn>;
emitCleared: ReturnType<typeof vi.fn>;
};
@@ -47,6 +48,7 @@ describe('NotificationService', () => {
emitCountUpdate: vi.fn(),
emitRead: vi.fn(),
emitDismissed: vi.fn(),
emitAllRead: vi.fn(),
emitCleared: vi.fn(),
};
service = new NotificationService(repo as never, gateway as never);
@@ -272,7 +274,7 @@ describe('NotificationService', () => {
// ---------- markAllAsRead() ----------
describe('markAllAsRead()', () => {
it('marks all as read and emits count update + cleared', async () => {
it('marks all as read and emits count update + all-read', async () => {
repo.setAllRead.mockResolvedValue(undefined);
repo.countUnread.mockResolvedValue(0);
@@ -281,7 +283,8 @@ describe('NotificationService', () => {
expect(repo.setAllRead).toHaveBeenCalledWith(42);
expect(repo.countUnread).toHaveBeenCalledWith(42);
expect(gateway.emitCountUpdate).toHaveBeenCalledWith(42, 0);
expect(gateway.emitCleared).toHaveBeenCalledWith(42);
expect(gateway.emitAllRead).toHaveBeenCalledWith(42);
expect(gateway.emitCleared).not.toHaveBeenCalled();
});
});
@@ -104,7 +104,7 @@ export class NotificationService {
await this.repo.setAllRead(userId);
const count = await this.repo.countUnread(userId);
this.gateway.emitCountUpdate(userId, count);
this.gateway.emitCleared(userId);
this.gateway.emitAllRead(userId);
}
async dismiss(userId: number, id: number): Promise<boolean> {