diff --git a/backend/src/middleware/handleNotifications/handleNotifications.js b/backend/src/middleware/handleNotifications/handleNotifications.js index c51bfce25..6b8368d44 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.js @@ -1,9 +1,18 @@ +import { + UserInputError +} from 'apollo-server' import extractMentionedUsers from './notifications/extractMentionedUsers' import extractHashtags from './hashtags/extractHashtags' -const notifyUsers = async (label, id, idsOfUsers, context) => { +const notifyUsers = async (label, id, idsOfUsers, reason, context) => { if (!idsOfUsers.length) return + // Done here, because Neode validation is not working. + const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'] + if (!(reasonsAllowed.includes(reason))) { + throw new UserInputError("Notification reason is not allowed!") + } + const session = context.driver.session() const createdAt = new Date().toISOString() const cypher = ` @@ -13,14 +22,15 @@ const notifyUsers = async (label, id, idsOfUsers, context) => { MATCH (u: User) WHERE u.id in $idsOfUsers AND NOT (u)<-[:BLOCKED]-(author) - CREATE (n: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt }) + CREATE (n: Notification { id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt }) MERGE (source)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u) ` await session.run(cypher, { - idsOfUsers, label, - createdAt, id, + idsOfUsers, + reason, + createdAt, }) session.close() } @@ -63,7 +73,7 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo // removes classes from the content const post = await resolve(root, args, context, resolveInfo) - await notifyUsers('Post', post.id, idsOfUsers, context) + await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context) await updateHashtagsOfPost(post.id, hashtags, context) return post @@ -76,7 +86,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI // removes classes from the content const comment = await resolve(root, args, context, resolveInfo) - await notifyUsers('Comment', comment.id, idsOfUsers, context) + await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context) return comment } @@ -98,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) => return record.get('user') }) if (context.user.id !== userWrotePost.id) { - await notifyUsers('Comment', comment.id, [userWrotePost.id], context) + await notifyUsers('Comment', comment.id, [userWrotePost.id], 'comment_on_your_post', context) } return comment diff --git a/backend/src/middleware/handleNotifications/handleNotifications.spec.js b/backend/src/middleware/handleNotifications/handleNotifications.spec.js index 40d8a2481..559c1d49a 100644 --- a/backend/src/middleware/handleNotifications/handleNotifications.spec.js +++ b/backend/src/middleware/handleNotifications/handleNotifications.spec.js @@ -1,7 +1,14 @@ -import { gql } from '../../jest/helpers' +import { + gql +} from '../../jest/helpers' import Factory from '../../seed/factories' -import { createTestClient } from 'apollo-server-testing' -import { neode, getDriver } from '../../bootstrap/neo4j' +import { + createTestClient +} from 'apollo-server-testing' +import { + neode, + getDriver +} from '../../bootstrap/neo4j' import createServer from '../../server' const factory = Factory() @@ -44,11 +51,12 @@ afterEach(async () => { }) describe('notifications', () => { - const notificationQuery = gql` + const notificationQuery = gql ` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { read + reason post { content } @@ -78,7 +86,7 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const createPostAction = async () => { - const createPostMutation = gql` + const createPostMutation = gql ` mutation($id: ID, $title: String!, $content: String!) { CreatePost(id: $id, title: $title, content: $content) { id @@ -90,7 +98,11 @@ describe('notifications', () => { authenticatedUser = await author.toJson() await mutate({ mutation: createPostMutation, - variables: { id: 'p47', title, content }, + variables: { + id: 'p47', + title, + content + }, }) authenticatedUser = await user.toJson() } @@ -101,12 +113,27 @@ describe('notifications', () => { 'Hey @al-capone how do you do?' const expected = expect.objectContaining({ data: { - currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] }, + currentUser: { + notifications: [{ + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }] + }, }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) @@ -126,7 +153,7 @@ describe('notifications', () => { @al-capone ` - const updatePostMutation = gql` + const updatePostMutation = gql ` mutation($id: ID!, $title: String!, $content: String!) { UpdatePost(id: $id, content: $content, title: $title) { title @@ -154,15 +181,31 @@ describe('notifications', () => { const expected = expect.objectContaining({ data: { currentUser: { - notifications: [ - { read: false, post: { content: expectedContent } }, - { read: false, post: { content: expectedContent } }, + notifications: [{ + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }, + { + read: false, + reason: 'mentioned_in_post', + post: { + content: expectedContent + } + }, ], }, }, }) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) }) @@ -175,11 +218,22 @@ describe('notifications', () => { it('sends no notification', async () => { await createPostAction() const expected = expect.objectContaining({ - data: { currentUser: { notifications: [] } }, + data: { + currentUser: { + notifications: [] + } + }, }) - const { query } = createTestClient(server) + const { + query + } = createTestClient(server) await expect( - query({ query: notificationQuery, variables: { read: false } }), + query({ + query: notificationQuery, + variables: { + read: false + } + }), ).resolves.toEqual(expected) }) }) @@ -193,7 +247,7 @@ describe('Hashtags', () => { const postTitle = 'Two Hashtags' const postContent = '

Hey Dude, #Democracy should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const postWithHastagsQuery = gql` + const postWithHastagsQuery = gql ` query($id: ID) { Post(id: $id) { tags { @@ -206,7 +260,7 @@ describe('Hashtags', () => { const postWithHastagsVariables = { id: postId, } - const createPostMutation = gql` + const createPostMutation = gql ` mutation($postId: ID, $postTitle: String!, $postContent: String!) { CreatePost(id: $postId, title: $postTitle, content: $postContent) { id @@ -234,20 +288,26 @@ describe('Hashtags', () => { }) it('both Hashtags are created with the "id" set to their "name"', async () => { - const expected = [ - { id: 'Democracy', name: 'Democracy' }, - { id: 'Liberty', name: 'Liberty' }, + const expected = [{ + id: 'Democracy', + name: 'Democracy' + }, + { + id: 'Liberty', + name: 'Liberty' + }, ] await expect( - query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables + }), ).resolves.toEqual( expect.objectContaining({ data: { - Post: [ - { - tags: expect.arrayContaining(expected), - }, - ], + Post: [{ + tags: expect.arrayContaining(expected), + }, ], }, }), ) @@ -257,7 +317,7 @@ describe('Hashtags', () => { // The already existing Hashtag has no class at this point. const updatedPostContent = '

Hey Dude, #Elections should work equal for everybody!? That seems to be the only way to have equal #Liberty for everyone.

' - const updatePostMutation = gql` + const updatePostMutation = gql ` mutation($postId: ID!, $postTitle: String!, $updatedPostContent: String!) { UpdatePost(id: $postId, title: $postTitle, content: $updatedPostContent) { id @@ -277,16 +337,26 @@ describe('Hashtags', () => { }, }) - const expected = [ - { id: 'Elections', name: 'Elections' }, - { id: 'Liberty', name: 'Liberty' }, + const expected = [{ + id: 'Elections', + name: 'Elections' + }, + { + id: 'Liberty', + name: 'Liberty' + }, ] await expect( - query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), + query({ + query: postWithHastagsQuery, + variables: postWithHastagsVariables + }), ).resolves.toEqual( expect.objectContaining({ data: { - Post: [{ tags: expect.arrayContaining(expected) }], + Post: [{ + tags: expect.arrayContaining(expected) + }], }, }), ) @@ -294,4 +364,4 @@ describe('Hashtags', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js index c44e19edd..f155f5648 100644 --- a/backend/src/middleware/index.js +++ b/backend/src/middleware/index.js @@ -1,6 +1,4 @@ -import { - applyMiddleware -} from 'graphql-middleware' +import { applyMiddleware } from 'graphql-middleware' import CONFIG from './../config' import activityPub from './activityPubMiddleware' @@ -32,7 +30,7 @@ export default schema => { includedFields: includedFields, orderBy: orderBy, email: email({ - isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT + isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT, }), } @@ -66,4 +64,4 @@ export default schema => { const appliedMiddlewares = order.map(key => middlewares[key]) return applyMiddleware(schema, ...appliedMiddlewares) -} \ No newline at end of file +} diff --git a/backend/src/models/Notification.js b/backend/src/models/Notification.js index b8690b8c1..89de27aec 100644 --- a/backend/src/models/Notification.js +++ b/backend/src/models/Notification.js @@ -1,9 +1,25 @@ import uuid from 'uuid/v4' module.exports = { - id: { type: 'uuid', primary: true, default: uuid }, - createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, - read: { type: 'boolean', default: false }, + id: { + type: 'uuid', + primary: true, + default: uuid, + }, + read: { + type: 'boolean', + default: false, + }, + reason: { + type: 'string', + valid: ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_your_post'], + default: 'mentioned_in_post', + }, + createdAt: { + type: 'string', + isoDate: true, + default: () => new Date().toISOString(), + }, user: { type: 'relationship', relationship: 'NOTIFIED', @@ -16,4 +32,4 @@ module.exports = { target: 'Post', direction: 'in', }, -} +} \ No newline at end of file diff --git a/backend/src/schema/resolvers/notifications.spec.js b/backend/src/schema/resolvers/notifications.spec.js index 5e7108be3..313376a25 100644 --- a/backend/src/schema/resolvers/notifications.spec.js +++ b/backend/src/schema/resolvers/notifications.spec.js @@ -1,6 +1,12 @@ -import { GraphQLClient } from 'graphql-request' +import { + GraphQLClient +} from 'graphql-request' import Factory from '../../seed/factories' -import { host, login, gql } from '../../jest/helpers' +import { + host, + login, + gql +} from '../../jest/helpers' const factory = Factory() let client @@ -19,7 +25,7 @@ afterEach(async () => { }) describe('query for notification', () => { - const notificationQuery = gql` + const notificationQuery = gql ` { Notification { id @@ -61,23 +67,29 @@ describe('currentUser notifications', () => { factory.create('User', neighborParams), factory.create('Notification', { id: 'post-mention-not-for-you', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'post-mention-already-seen', read: true, + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'post-mention-unseen', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'comment-mention-not-for-you', + reason: 'mentioned_in_comment', }), factory.create('Notification', { id: 'comment-mention-already-seen', read: true, + reason: 'mentioned_in_comment', }), factory.create('Notification', { id: 'comment-mention-unseen', + reason: 'mentioned_in_comment', }), ]) await factory.authenticateAs(neighborParams) @@ -149,7 +161,7 @@ describe('currentUser notifications', () => { }) describe('filter for read: false', () => { - const queryCurrentUserNotificationsFilterRead = gql` + const queryCurrentUserNotificationsFilterRead = gql ` query($read: Boolean) { currentUser { notifications(read: $read, orderBy: createdAt_desc) { @@ -170,8 +182,7 @@ describe('currentUser notifications', () => { it('returns only unread notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([ - { + notifications: expect.arrayContaining([{ id: 'post-mention-unseen', post: { id: 'p1', @@ -195,7 +206,7 @@ describe('currentUser notifications', () => { }) describe('no filters', () => { - const queryCurrentUserNotifications = gql` + const queryCurrentUserNotifications = gql ` { currentUser { notifications(orderBy: createdAt_desc) { @@ -213,8 +224,7 @@ describe('currentUser notifications', () => { it('returns all notifications of current user', async () => { const expected = { currentUser: { - notifications: expect.arrayContaining([ - { + notifications: expect.arrayContaining([{ id: 'post-mention-unseen', post: { id: 'p1', @@ -255,7 +265,7 @@ describe('currentUser notifications', () => { }) describe('UpdateNotification', () => { - const mutationUpdateNotification = gql` + const mutationUpdateNotification = gql ` mutation($id: ID!, $read: Boolean) { UpdateNotification(id: $id, read: $read) { id @@ -286,9 +296,11 @@ describe('UpdateNotification', () => { factory.create('User', mentionedParams), factory.create('Notification', { id: 'post-mention-to-be-updated', + reason: 'mentioned_in_post', }), factory.create('Notification', { id: 'comment-mention-to-be-updated', + reason: 'mentioned_in_comment', }), ]) await factory.authenticateAs(userParams) @@ -390,4 +402,4 @@ describe('UpdateNotification', () => { }) }) }) -}) +}) \ No newline at end of file diff --git a/backend/src/schema/types/type/Notification.gql b/backend/src/schema/types/type/Notification.gql index 0f94c2301..a03b86769 100644 --- a/backend/src/schema/types/type/Notification.gql +++ b/backend/src/schema/types/type/Notification.gql @@ -1,8 +1,9 @@ type Notification { id: ID! read: Boolean + reason: String + createdAt: String user: User @relation(name: "NOTIFIED", direction: "OUT") post: Post @relation(name: "NOTIFIED", direction: "IN") comment: Comment @relation(name: "NOTIFIED", direction: "IN") - createdAt: String } diff --git a/backend/src/seed/factories/index.js b/backend/src/seed/factories/index.js index df3886a6c..56518bd06 100644 --- a/backend/src/seed/factories/index.js +++ b/backend/src/seed/factories/index.js @@ -62,15 +62,26 @@ export default function Factory(options = {}) { lastResponse: null, neodeInstance, async authenticateAs({ email, password }) { - const headers = await authenticatedHeaders({ email, password }, seedServerHost) + const headers = await authenticatedHeaders( + { + email, + password, + }, + seedServerHost, + ) this.lastResponse = headers - this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) + this.graphQLClient = new GraphQLClient(seedServerHost, { + headers, + }) return this }, async create(node, args = {}) { const { factory, mutation, variables } = this.factories[node](args) if (factory) { - this.lastResponse = await factory({ args, neodeInstance }) + this.lastResponse = await factory({ + args, + neodeInstance, + }) return this.lastResponse } else { this.lastResponse = await this.graphQLClient.request(mutation, variables) @@ -121,11 +132,15 @@ export default function Factory(options = {}) { }, async invite({ email }) { const mutation = ` mutation($email: String!) { invite( email: $email) } ` - this.lastResponse = await this.graphQLClient.request(mutation, { email }) + this.lastResponse = await this.graphQLClient.request(mutation, { + email, + }) return this }, async cleanDatabase() { - this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) + this.lastResponse = await cleanDatabase({ + driver: this.neo4jDriver, + }) return this }, async emote({ to, data }) { diff --git a/webapp/components/notifications/Notification/Notification.vue b/webapp/components/notifications/Notification/Notification.vue index ae1eeddc9..a982e33a9 100644 --- a/webapp/components/notifications/Notification/Notification.vue +++ b/webapp/components/notifications/Notification/Notification.vue @@ -8,9 +8,7 @@ :trunc="35" /> - - {{ $t('notifications.menu.mentioned', { resource: post.id ? 'post' : 'comment' }) }} - + {{ $t(notificationTextIdents[notification.reason]) }}