diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts index c636a7c87..90888cf8b 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts @@ -31,8 +31,10 @@ jest.mock('../helpers/isUserOnline', () => ({ isUserOnline: () => isUserOnlineMock(), })) +const pubsubSpy = jest.spyOn(pubsub, 'publish') + let server, query, mutate, notifiedUser, authenticatedUser -let publishSpy + const driver = getDriver() const neode = getNeode() const categoryIds = ['cat9'] @@ -65,7 +67,6 @@ const createCommentMutation = gql` beforeAll(async () => { await cleanDatabase() - publishSpy = jest.spyOn(pubsub, 'publish') const createServerResult = createServer({ context: () => { return { @@ -87,7 +88,6 @@ afterAll(async () => { }) beforeEach(async () => { - publishSpy.mockClear() notifiedUser = await Factory.build( 'user', { @@ -417,7 +417,7 @@ describe('notifications', () => { it('publishes `NOTIFICATION_ADDED` to me', async () => { await createPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -428,7 +428,7 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) describe('updates the post and mentions me again', () => { @@ -578,7 +578,7 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED`', async () => { await createPostAction() - expect(publishSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() }) }) }) @@ -722,7 +722,7 @@ describe('notifications', () => { it('does not publish `NOTIFICATION_ADDED` to authenticated user', async () => { await createCommentOnPostAction() - expect(publishSpy).toHaveBeenCalledWith( + expect(pubsubSpy).toHaveBeenCalledWith( 'NOTIFICATION_ADDED', expect.objectContaining({ notificationAdded: expect.objectContaining({ @@ -733,14 +733,14 @@ describe('notifications', () => { }), }), ) - expect(publishSpy).toHaveBeenCalledTimes(1) + expect(pubsubSpy).toHaveBeenCalledTimes(1) }) }) }) }) }) - describe('chat email notifications', () => { + describe('chat notifications', () => { let chatSender let chatReceiver let roomId @@ -779,7 +779,7 @@ describe('notifications', () => { }) describe('if the chatReceiver is online', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(true) await mutate({ @@ -790,13 +790,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver is offline', () => { - it('sends an email', async () => { + it('publishes subscriptions and sends an email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await mutate({ @@ -807,13 +826,32 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).toHaveBeenCalledTimes(1) expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1) }) }) describe('if the chatReceiver has blocked chatSender', () => { - it('sends no email', async () => { + it('publishes no subscriptions and sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.relateTo(chatSender, 'blocked') @@ -825,13 +863,37 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + + expect(sendMailMock).not.toHaveBeenCalled() + expect(chatMessageTemplateMock).not.toHaveBeenCalled() + }) + }) + + describe('if the chatReceiver has muted chatSender', () => { + it('publishes no subscriptions and sends no email', async () => { + isUserOnlineMock = jest.fn().mockReturnValue(false) + await chatReceiver.relateTo(chatSender, 'muted') + + await mutate({ + mutation: createMessageMutation(), + variables: { + roomId, + content: 'Some nice message to chatReceiver', + }, + }) + + expect(pubsubSpy).not.toHaveBeenCalled() + expect(pubsubSpy).not.toHaveBeenCalled() + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) }) describe('if the chatReceiver has disabled `emailNotificationsChatMessage`', () => { - it('sends no email', async () => { + it('publishes subscriptions but sends no email', async () => { isUserOnlineMock = jest.fn().mockReturnValue(false) await chatReceiver.update({ emailNotificationsChatMessage: false }) @@ -843,6 +905,25 @@ describe('notifications', () => { }, }) + expect(pubsubSpy).toHaveBeenCalledWith('ROOM_COUNT_UPDATED', { + roomCountUpdated: '1', + userId: 'chatReceiver', + }) + expect(pubsubSpy).toHaveBeenCalledWith('CHAT_MESSAGE_ADDED', { + chatMessageAdded: expect.objectContaining({ + id: expect.any(String), + content: 'Some nice message to chatReceiver', + senderId: 'chatSender', + username: 'chatSender', + avatar: null, + date: expect.any(String), + saved: true, + distributed: false, + seen: false, + }), + userId: 'chatReceiver', + }) + expect(sendMailMock).not.toHaveBeenCalled() expect(chatMessageTemplateMock).not.toHaveBeenCalled() }) diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts index ebbcd7886..62050b3cc 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.ts +++ b/backend/src/middleware/notifications/notificationsMiddleware.ts @@ -7,7 +7,9 @@ import { import { isUserOnline } from '@middleware/helpers/isUserOnline' import { validateNotifyUsers } from '@middleware/validation/validationMiddleware' // eslint-disable-next-line import/no-cycle -import { pubsub, NOTIFICATION_ADDED } from '@src/server' +import { getUnreadRoomsCount } from '@schema/resolvers/rooms' +// eslint-disable-next-line import/no-cycle +import { pubsub, NOTIFICATION_ADDED, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' import extractMentionedUsers from './mentions/extractMentionedUsers' @@ -436,7 +438,7 @@ const notifyUsersOfComment = async (label, commentId, reason, context) => { const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => { // Execute resolver - const result = await resolve(root, args, context, resolveInfo) + const message = await resolve(root, args, context, resolveInfo) // Query Parameters const { roomId } = args @@ -452,7 +454,7 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress) WHERE NOT recipientUser.id = $currentUserId AND NOT (recipientUser)-[:BLOCKED]-(senderUser) - AND NOT recipientUser.emailNotificationsChatMessage = false + AND NOT (recipientUser)-[:MUTED]->(senderUser) RETURN senderUser {.*}, recipientUser {.*}, emailAddress {.email} ` const txResponse = await transaction.run(messageRecipientCypher, { @@ -471,13 +473,27 @@ const handleCreateMessage = async (resolve, root, args, context, resolveInfo) => // Execute Query const { senderUser, recipientUser, email } = await messageRecipient - // Send EMail if we found a user(not blocked) and he is not considered online - if (recipientUser && !isUserOnline(recipientUser)) { - void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + if (recipientUser) { + // send subscriptions + const roomCountUpdated = await getUnreadRoomsCount(recipientUser.id, session) + + void pubsub.publish(ROOM_COUNT_UPDATED, { + roomCountUpdated, + userId: recipientUser.id, + }) + void pubsub.publish(CHAT_MESSAGE_ADDED, { + chatMessageAdded: message, + userId: recipientUser.id, + }) + + // Send EMail if we found a user(not blocked) and he is not considered online + if (recipientUser.emailNotificationsChatMessage !== false && !isUserOnline(recipientUser)) { + void sendMail(chatMessageTemplate({ email, variables: { senderUser, recipientUser } })) + } } // Return resolver result to client - return result + return message } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/schema/resolvers/messages.spec.ts b/backend/src/schema/resolvers/messages.spec.ts index 4384ddd0f..83d4134cb 100644 --- a/backend/src/schema/resolvers/messages.spec.ts +++ b/backend/src/schema/resolvers/messages.spec.ts @@ -9,13 +9,13 @@ import createServer, { pubsub } from '@src/server' const driver = getDriver() const neode = getNeode() -const pubsubSpy = jest.spyOn(pubsub, 'publish') - let query let mutate let authenticatedUser let chattingUser, otherChattingUser, notChattingUser +const pubsubSpy = jest.spyOn(pubsub, 'publish') + beforeAll(async () => { await cleanDatabase() @@ -118,7 +118,7 @@ describe('Message', () => { }) describe('user chats in room', () => { - it('returns the message and publishes subscriptions', async () => { + it('returns the message', async () => { await expect( mutate({ mutation: createMessageMutation(), @@ -143,24 +143,6 @@ describe('Message', () => { }, }, }) - expect(pubsubSpy).toBeCalledWith('ROOM_COUNT_UPDATED', { - roomCountUpdated: '1', - userId: 'other-chatting-user', - }) - expect(pubsubSpy).toBeCalledWith('CHAT_MESSAGE_ADDED', { - chatMessageAdded: expect.objectContaining({ - id: expect.any(String), - content: 'Some nice message to other chatting user', - senderId: 'chatting-user', - username: 'Chatting User', - avatar: expect.any(String), - date: expect.any(String), - saved: true, - distributed: false, - seen: false, - }), - userId: 'other-chatting-user', - }) }) describe('room is updated as well', () => { diff --git a/backend/src/schema/resolvers/messages.ts b/backend/src/schema/resolvers/messages.ts index 6879c4be9..1d59b4bbd 100644 --- a/backend/src/schema/resolvers/messages.ts +++ b/backend/src/schema/resolvers/messages.ts @@ -1,10 +1,9 @@ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' -import { pubsub, ROOM_COUNT_UPDATED, CHAT_MESSAGE_ADDED } from '@src/server' +import { pubsub, CHAT_MESSAGE_ADDED } from '@src/server' import Resolver from './helpers/Resolver' -import { getUnreadRoomsCount } from './rooms' const setMessagesAsDistributed = async (undistributedMessagesIds, session) => { return session.writeTransaction(async (transaction) => { @@ -111,22 +110,7 @@ export default { return message }) try { - const message = await writeTxResultPromise - if (message) { - const roomCountUpdated = await getUnreadRoomsCount(message.recipientId, session) - - // send subscriptions - void pubsub.publish(ROOM_COUNT_UPDATED, { - roomCountUpdated, - userId: message.recipientId, - }) - void pubsub.publish(CHAT_MESSAGE_ADDED, { - chatMessageAdded: message, - userId: message.recipientId, - }) - } - - return message + return await writeTxResultPromise } catch (error) { throw new Error(error) } finally { diff --git a/backend/src/schema/resolvers/rooms.spec.ts b/backend/src/schema/resolvers/rooms.spec.ts index 87ebb4557..2025681ad 100644 --- a/backend/src/schema/resolvers/rooms.spec.ts +++ b/backend/src/schema/resolvers/rooms.spec.ts @@ -387,6 +387,34 @@ describe('Room', () => { }, }) }) + + it('when chattingUser is blocked has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'blocked') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) + + it('when chattingUser is muted has 0 unread rooms', async () => { + authenticatedUser = await otherChattingUser.toJson() + await otherChattingUser.relateTo(chattingUser, 'muted') + await expect( + query({ + query: unreadRoomsQuery(), + }), + ).resolves.toMatchObject({ + data: { + UnreadRooms: 0, + }, + }) + }) }) describe('as not chatting user', () => { diff --git a/backend/src/schema/resolvers/rooms.ts b/backend/src/schema/resolvers/rooms.ts index 0ff37b594..1bfa354e9 100644 --- a/backend/src/schema/resolvers/rooms.ts +++ b/backend/src/schema/resolvers/rooms.ts @@ -1,6 +1,7 @@ import { withFilter } from 'graphql-subscriptions' import { neo4jgraphql } from 'neo4j-graphql-js' +// eslint-disable-next-line import/no-cycle import { pubsub, ROOM_COUNT_UPDATED } from '@src/server' import Resolver from './helpers/Resolver' @@ -8,8 +9,10 @@ import Resolver from './helpers/Resolver' export const getUnreadRoomsCount = async (userId, session) => { return session.readTransaction(async (transaction) => { const unreadRoomsCypher = ` - MATCH (:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) + MATCH (user:User { id: $userId })-[:CHATS_IN]->(room:Room)<-[:INSIDE]-(message:Message)<-[:CREATED]-(sender:User) WHERE NOT sender.id = $userId AND NOT message.seen + AND NOT (user)-[:BLOCKED]->(sender) + AND NOT (user)-[:MUTED]->(sender) RETURN toString(COUNT(DISTINCT room)) AS count ` const unreadRoomsTxResponse = await transaction.run(unreadRoomsCypher, { userId })