diff --git a/backend/src/middleware/helpers/email/templateBuilder.spec.ts b/backend/src/middleware/helpers/email/templateBuilder.spec.ts
index cb516c0a9..437672a9a 100644
--- a/backend/src/middleware/helpers/email/templateBuilder.spec.ts
+++ b/backend/src/middleware/helpers/email/templateBuilder.spec.ts
@@ -6,6 +6,7 @@ import {
resetPasswordTemplate,
wrongAccountTemplate,
notificationTemplate,
+ chatMessageTemplate,
} from './templateBuilder'
const englishHint = 'English version below!'
@@ -34,6 +35,12 @@ const resetPasswordTemplateData = () => ({
name: 'Mr Example',
},
})
+const chatMessageTemplateData = {
+ email: 'test@example.org',
+ variables: {
+ name: 'Mr Example',
+ },
+}
const wrongAccountTemplateData = () => ({
email: 'test@example.org',
variables: {},
@@ -163,6 +170,31 @@ describe('templateBuilder', () => {
})
})
+ describe('chatMessageTemplate', () => {
+ describe('multi language', () => {
+ it('e-mail is build with all data', () => {
+ const subject = 'Neue Chatnachricht | New chat message'
+ const actionUrl = new URL('/chat', CONFIG.CLIENT_URI).toString()
+ const enContent = 'You have received a new chat message.'
+ const deContent = 'Du hast eine neue Chatnachricht erhalten.'
+ testEmailData(null, chatMessageTemplate, chatMessageTemplateData, [
+ ...textsStandard,
+ {
+ templPropName: 'subject',
+ isContaining: false,
+ text: subject,
+ },
+ englishHint,
+ actionUrl,
+ chatMessageTemplateData.variables.name,
+ enContent,
+ deContent,
+ supportUrl,
+ ])
+ })
+ })
+ })
+
describe('wrongAccountTemplate', () => {
describe('multi language', () => {
it('e-mail is build with all data', () => {
diff --git a/backend/src/middleware/helpers/email/templateBuilder.ts b/backend/src/middleware/helpers/email/templateBuilder.ts
index 78d7a9bf9..431048336 100644
--- a/backend/src/middleware/helpers/email/templateBuilder.ts
+++ b/backend/src/middleware/helpers/email/templateBuilder.ts
@@ -71,6 +71,19 @@ export const resetPasswordTemplate = ({ email, variables: { nonce, name } }) =>
}
}
+export const chatMessageTemplate = ({ email, variables: { name } }) => {
+ const subject = 'Neue Chatnachricht | New chat message'
+ const actionUrl = new URL('/chat', CONFIG.CLIENT_URI)
+ const renderParams = { ...defaultParams, englishHint, actionUrl, name, subject }
+
+ return {
+ from,
+ to: email,
+ subject,
+ html: mustache.render(templates.layout, renderParams, { content: templates.chatMessage }),
+ }
+}
+
export const wrongAccountTemplate = ({ email, _variables = {} }) => {
const subject = 'Falsche Mailadresse? | Wrong E-mail?'
const actionUrl = new URL('/password-reset/request', CONFIG.CLIENT_URI)
diff --git a/backend/src/middleware/helpers/email/templates/chatMessage.html b/backend/src/middleware/helpers/email/templates/chatMessage.html
new file mode 100644
index 000000000..0b1bacb08
--- /dev/null
+++ b/backend/src/middleware/helpers/email/templates/chatMessage.html
@@ -0,0 +1,105 @@
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Hallo {{ name }}!
+ Du hast eine neue Chatnachricht erhalten.
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+
+
+
+
+ |
+ |
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Hello {{ name }}!
+ You have received a new chat message.
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
diff --git a/backend/src/middleware/helpers/email/templates/index.ts b/backend/src/middleware/helpers/email/templates/index.ts
index b8ae01bdb..9c32c6d3e 100644
--- a/backend/src/middleware/helpers/email/templates/index.ts
+++ b/backend/src/middleware/helpers/email/templates/index.ts
@@ -7,5 +7,6 @@ export const signup = readFile('./signup.html')
export const passwordReset = readFile('./resetPassword.html')
export const wrongAccount = readFile('./wrongAccount.html')
export const emailVerification = readFile('./emailVerification.html')
+export const chatMessage = readFile('./chatMessage.html')
export const layout = readFile('./layout.html')
diff --git a/backend/src/middleware/helpers/isUserOnline.spec.ts b/backend/src/middleware/helpers/isUserOnline.spec.ts
new file mode 100644
index 000000000..bf2cb8d17
--- /dev/null
+++ b/backend/src/middleware/helpers/isUserOnline.spec.ts
@@ -0,0 +1,46 @@
+import { isUserOnline } from './isUserOnline'
+
+let user
+
+describe('isUserOnline', () => {
+ beforeEach(() => {
+ user = {
+ properties: {
+ lastActiveAt: null,
+ awaySince: null,
+ lastOnlineStatus: null,
+ },
+ }
+ })
+ describe('user has lastOnlineStatus `online`', () => {
+ it('returns true if he was active within the last 90 seconds', () => {
+ user.properties.lastOnlineStatus = 'online'
+ user.properties.lastActiveAt = new Date()
+ expect(isUserOnline(user)).toBe(true)
+ })
+ it('returns false if he was not active within the last 90 seconds', () => {
+ user.properties.lastOnlineStatus = 'online'
+ user.properties.lastActiveAt = new Date().getTime() - 90001
+ expect(isUserOnline(user)).toBe(false)
+ })
+ })
+
+ describe('user has lastOnlineStatus `away`', () => {
+ it('returns true if he went away less then 180 seconds ago', () => {
+ user.properties.lastOnlineStatus = 'away'
+ user.properties.awaySince = new Date()
+ expect(isUserOnline(user)).toBe(true)
+ })
+ it('returns false if he went away more then 180 seconds ago', () => {
+ user.properties.lastOnlineStatus = 'away'
+ user.properties.awaySince = new Date().getTime() - 180001
+ expect(isUserOnline(user)).toBe(false)
+ })
+ })
+
+ describe('user is freshly created and has never logged in', () => {
+ it('returns false', () => {
+ expect(isUserOnline(user)).toBe(false)
+ })
+ })
+})
diff --git a/backend/src/middleware/helpers/isUserOnline.ts b/backend/src/middleware/helpers/isUserOnline.ts
new file mode 100644
index 000000000..679953f81
--- /dev/null
+++ b/backend/src/middleware/helpers/isUserOnline.ts
@@ -0,0 +1,16 @@
+export const isUserOnline = (user) => {
+ // Is Recipient considered online
+ const lastActive = new Date(user.properties.lastActiveAt).getTime()
+ const awaySince = new Date(user.properties.awaySince).getTime()
+ const now = new Date().getTime()
+ const status = user.properties.lastOnlineStatus
+ if (
+ // online & last active less than 1.5min -> online
+ (status === 'online' && now - lastActive < 90000) ||
+ // away for less then 3min -> online
+ (status === 'away' && now - awaySince < 180000)
+ ) {
+ return true
+ }
+ return false
+}
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts
index 57354d13f..50d655484 100644
--- a/backend/src/middleware/notifications/notificationsMiddleware.spec.ts
+++ b/backend/src/middleware/notifications/notificationsMiddleware.spec.ts
@@ -1,5 +1,5 @@
import gql from 'graphql-tag'
-import { cleanDatabase } from '../../db/factories'
+import Factory, { cleanDatabase } from '../../db/factories'
import { createTestClient } from 'apollo-server-testing'
import { getNeode, getDriver } from '../../db/neo4j'
import createServer, { pubsub } from '../../server'
@@ -10,6 +10,23 @@ import {
changeGroupMemberRoleMutation,
removeUserFromGroupMutation,
} from '../../graphql/groups'
+import { createMessageMutation } from '../../graphql/messages'
+import { createRoomMutation } from '../../graphql/rooms'
+
+const sendMailMock = jest.fn()
+jest.mock('../helpers/email/sendMail', () => ({
+ sendMail: () => sendMailMock(),
+}))
+
+const chatMessageTemplateMock = jest.fn()
+jest.mock('../helpers/email/templateBuilder', () => ({
+ chatMessageTemplate: () => chatMessageTemplateMock(),
+}))
+
+let isUserOnlineMock = jest.fn()
+jest.mock('../helpers/isUserOnline', () => ({
+ isUserOnline: () => isUserOnlineMock(),
+}))
let server, query, mutate, notifiedUser, authenticatedUser
let publishSpy
@@ -633,6 +650,115 @@ describe('notifications', () => {
})
})
+ describe('chat email notifications', () => {
+ let chatSender
+ let chatReceiver
+ let roomId
+
+ beforeEach(async () => {
+ jest.clearAllMocks()
+
+ chatSender = await neode.create(
+ 'User',
+ {
+ id: 'chatSender',
+ name: 'chatSender',
+ slug: 'chatSender',
+ },
+ {
+ email: 'chatSender@example.org',
+ password: '1234',
+ },
+ )
+
+ chatReceiver = await Factory.build(
+ 'user',
+ { id: 'chatReceiver', name: 'chatReceiver', slug: 'chatReceiver' },
+ { email: 'user@example.org' },
+ )
+
+ authenticatedUser = await chatSender.toJson()
+
+ const room = await mutate({
+ mutation: createRoomMutation(),
+ variables: {
+ userId: 'chatReceiver',
+ },
+ })
+ roomId = room.data.CreateRoom.id
+ })
+
+ describe('chatReceiver is online', () => {
+ it('sends no email', async () => {
+ isUserOnlineMock = jest.fn().mockReturnValue(true)
+
+ await mutate({
+ mutation: createMessageMutation(),
+ variables: {
+ roomId,
+ content: 'Some nice message to chatReceiver',
+ },
+ })
+
+ expect(sendMailMock).not.toHaveBeenCalled()
+ expect(chatMessageTemplateMock).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('chatReceiver is offline', () => {
+ it('sends an email', async () => {
+ isUserOnlineMock = jest.fn().mockReturnValue(false)
+
+ await mutate({
+ mutation: createMessageMutation(),
+ variables: {
+ roomId,
+ content: 'Some nice message to chatReceiver',
+ },
+ })
+
+ expect(sendMailMock).toHaveBeenCalledTimes(1)
+ expect(chatMessageTemplateMock).toHaveBeenCalledTimes(1)
+ })
+ })
+
+ describe('chatReceiver has blocked chatSender', () => {
+ it('sends no email', async () => {
+ isUserOnlineMock = jest.fn().mockReturnValue(false)
+ await chatReceiver.relateTo(chatSender, 'blocked')
+
+ await mutate({
+ mutation: createMessageMutation(),
+ variables: {
+ roomId,
+ content: 'Some nice message to chatReceiver',
+ },
+ })
+
+ expect(sendMailMock).not.toHaveBeenCalled()
+ expect(chatMessageTemplateMock).not.toHaveBeenCalled()
+ })
+ })
+
+ describe('chatReceiver has disabled email notifications', () => {
+ it('sends no email', async () => {
+ isUserOnlineMock = jest.fn().mockReturnValue(false)
+ await chatReceiver.update({ sendNotificationEmails: false })
+
+ await mutate({
+ mutation: createMessageMutation(),
+ variables: {
+ roomId,
+ content: 'Some nice message to chatReceiver',
+ },
+ })
+
+ expect(sendMailMock).not.toHaveBeenCalled()
+ expect(chatMessageTemplateMock).not.toHaveBeenCalled()
+ })
+ })
+ })
+
describe('group notifications', () => {
let groupOwner
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.ts b/backend/src/middleware/notifications/notificationsMiddleware.ts
index aa2cee06e..7ecbf8181 100644
--- a/backend/src/middleware/notifications/notificationsMiddleware.ts
+++ b/backend/src/middleware/notifications/notificationsMiddleware.ts
@@ -2,7 +2,8 @@ import { pubsub, NOTIFICATION_ADDED } from '../../server'
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
import { sendMail } from '../helpers/email/sendMail'
-import { notificationTemplate } from '../helpers/email/templateBuilder'
+import { chatMessageTemplate, notificationTemplate } from '../helpers/email/templateBuilder'
+import { isUserOnline } from '../helpers/isUserOnline'
const queryNotificationEmails = async (context, notificationUserIds) => {
if (!(notificationUserIds && notificationUserIds.length)) return []
@@ -314,6 +315,56 @@ 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)
+
+ // Query Parameters
+ const { roomId } = args
+ const {
+ user: { id: currentUserId },
+ } = context
+
+ // Find Recipient
+ const session = context.driver.session()
+ const messageRecipient = session.readTransaction(async (transaction) => {
+ const messageRecipientCypher = `
+ MATCH (currentUser:User { id: $currentUserId })-[:CHATS_IN]->(room:Room { id: $roomId })
+ MATCH (room)<-[:CHATS_IN]-(recipientUser:User)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
+ WHERE NOT recipientUser.id = $currentUserId
+ AND NOT (recipientUser)-[:BLOCKED]-(currentUser)
+ AND recipientUser.sendNotificationEmails = true
+ RETURN recipientUser, emailAddress {.email}
+ `
+ const txResponse = await transaction.run(messageRecipientCypher, {
+ currentUserId,
+ roomId,
+ })
+
+ return {
+ user: await txResponse.records.map((record) => record.get('recipientUser'))[0],
+ email: await txResponse.records.map((record) => record.get('emailAddress'))[0]?.email,
+ }
+ })
+
+ try {
+ // Execute Query
+ const { user, email } = await messageRecipient
+
+ // Send EMail if we found a user(not blocked) and he is not considered online
+ if (user && !isUserOnline(user)) {
+ void sendMail(chatMessageTemplate({ email, variables: { name: user.properties.name } }))
+ }
+
+ // Return resolver result to client
+ return result
+ } catch (error) {
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
+
export default {
Mutation: {
CreatePost: handleContentDataOfPost,
@@ -324,5 +375,6 @@ export default {
LeaveGroup: handleLeaveGroup,
ChangeGroupMemberRole: handleChangeGroupMemberRole,
RemoveUserFromGroup: handleRemoveUserFromGroup,
+ CreateMessage: handleCreateMessage,
},
}