Implemented a reason in the Notification

Used this for displaying in the mentions menu in frontend.
Rewrite backend test.
This commit is contained in:
Wolfgang Huß 2019-08-15 19:29:06 +02:00
parent af70d4f717
commit 8acccc99d0
11 changed files with 207 additions and 73 deletions

View File

@ -1,9 +1,18 @@
import {
UserInputError
} from 'apollo-server'
import extractMentionedUsers from './notifications/extractMentionedUsers' import extractMentionedUsers from './notifications/extractMentionedUsers'
import extractHashtags from './hashtags/extractHashtags' import extractHashtags from './hashtags/extractHashtags'
const notifyUsers = async (label, id, idsOfUsers, context) => { const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
if (!idsOfUsers.length) return 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 session = context.driver.session()
const createdAt = new Date().toISOString() const createdAt = new Date().toISOString()
const cypher = ` const cypher = `
@ -13,14 +22,15 @@ const notifyUsers = async (label, id, idsOfUsers, context) => {
MATCH (u: User) MATCH (u: User)
WHERE u.id in $idsOfUsers WHERE u.id in $idsOfUsers
AND NOT (u)<-[:BLOCKED]-(author) 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) MERGE (source)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u)
` `
await session.run(cypher, { await session.run(cypher, {
idsOfUsers,
label, label,
createdAt,
id, id,
idsOfUsers,
reason,
createdAt,
}) })
session.close() session.close()
} }
@ -63,7 +73,7 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
// removes classes from the content // removes classes from the content
const post = await resolve(root, args, context, resolveInfo) 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) await updateHashtagsOfPost(post.id, hashtags, context)
return post return post
@ -76,7 +86,7 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
// removes classes from the content // removes classes from the content
const comment = await resolve(root, args, context, resolveInfo) 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 return comment
} }
@ -98,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
return record.get('user') return record.get('user')
}) })
if (context.user.id !== userWrotePost.id) { 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 return comment

View File

@ -1,7 +1,14 @@
import { gql } from '../../jest/helpers' import {
gql
} from '../../jest/helpers'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing' import {
import { neode, getDriver } from '../../bootstrap/neo4j' createTestClient
} from 'apollo-server-testing'
import {
neode,
getDriver
} from '../../bootstrap/neo4j'
import createServer from '../../server' import createServer from '../../server'
const factory = Factory() const factory = Factory()
@ -49,6 +56,7 @@ describe('notifications', () => {
currentUser { currentUser {
notifications(read: $read, orderBy: createdAt_desc) { notifications(read: $read, orderBy: createdAt_desc) {
read read
reason
post { post {
content content
} }
@ -90,7 +98,11 @@ describe('notifications', () => {
authenticatedUser = await author.toJson() authenticatedUser = await author.toJson()
await mutate({ await mutate({
mutation: createPostMutation, mutation: createPostMutation,
variables: { id: 'p47', title, content }, variables: {
id: 'p47',
title,
content
},
}) })
authenticatedUser = await user.toJson() authenticatedUser = await user.toJson()
} }
@ -101,12 +113,27 @@ describe('notifications', () => {
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?' 'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { 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( await expect(
query({ query: notificationQuery, variables: { read: false } }), query({
query: notificationQuery,
variables: {
read: false
}
}),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
@ -154,15 +181,31 @@ describe('notifications', () => {
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { data: {
currentUser: { currentUser: {
notifications: [ notifications: [{
{ read: false, post: { content: expectedContent } }, read: false,
{ read: false, post: { content: expectedContent } }, reason: 'mentioned_in_post',
post: {
content: expectedContent
}
},
{
read: false,
reason: 'mentioned_in_post',
post: {
content: expectedContent
}
},
], ],
}, },
}, },
}) })
await expect( await expect(
query({ query: notificationQuery, variables: { read: false } }), query({
query: notificationQuery,
variables: {
read: false
}
}),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
}) })
@ -175,11 +218,22 @@ describe('notifications', () => {
it('sends no notification', async () => { it('sends no notification', async () => {
await createPostAction() await createPostAction()
const expected = expect.objectContaining({ const expected = expect.objectContaining({
data: { currentUser: { notifications: [] } }, data: {
currentUser: {
notifications: []
}
},
}) })
const { query } = createTestClient(server) const {
query
} = createTestClient(server)
await expect( await expect(
query({ query: notificationQuery, variables: { read: false } }), query({
query: notificationQuery,
variables: {
read: false
}
}),
).resolves.toEqual(expected) ).resolves.toEqual(expected)
}) })
}) })
@ -234,20 +288,26 @@ describe('Hashtags', () => {
}) })
it('both Hashtags are created with the "id" set to their "name"', async () => { it('both Hashtags are created with the "id" set to their "name"', async () => {
const expected = [ const expected = [{
{ id: 'Democracy', name: 'Democracy' }, id: 'Democracy',
{ id: 'Liberty', name: 'Liberty' }, name: 'Democracy'
},
{
id: 'Liberty',
name: 'Liberty'
},
] ]
await expect( await expect(
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), query({
query: postWithHastagsQuery,
variables: postWithHastagsVariables
}),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { data: {
Post: [ Post: [{
{
tags: expect.arrayContaining(expected), tags: expect.arrayContaining(expected),
}, }, ],
],
}, },
}), }),
) )
@ -277,16 +337,26 @@ describe('Hashtags', () => {
}, },
}) })
const expected = [ const expected = [{
{ id: 'Elections', name: 'Elections' }, id: 'Elections',
{ id: 'Liberty', name: 'Liberty' }, name: 'Elections'
},
{
id: 'Liberty',
name: 'Liberty'
},
] ]
await expect( await expect(
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }), query({
query: postWithHastagsQuery,
variables: postWithHastagsVariables
}),
).resolves.toEqual( ).resolves.toEqual(
expect.objectContaining({ expect.objectContaining({
data: { data: {
Post: [{ tags: expect.arrayContaining(expected) }], Post: [{
tags: expect.arrayContaining(expected)
}],
}, },
}), }),
) )

View File

@ -1,6 +1,4 @@
import { import { applyMiddleware } from 'graphql-middleware'
applyMiddleware
} from 'graphql-middleware'
import CONFIG from './../config' import CONFIG from './../config'
import activityPub from './activityPubMiddleware' import activityPub from './activityPubMiddleware'
@ -32,7 +30,7 @@ export default schema => {
includedFields: includedFields, includedFields: includedFields,
orderBy: orderBy, orderBy: orderBy,
email: email({ email: email({
isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT,
}), }),
} }

View File

@ -1,9 +1,25 @@
import uuid from 'uuid/v4' import uuid from 'uuid/v4'
module.exports = { module.exports = {
id: { type: 'uuid', primary: true, default: uuid }, id: {
createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, type: 'uuid',
read: { type: 'boolean', default: false }, 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: { user: {
type: 'relationship', type: 'relationship',
relationship: 'NOTIFIED', relationship: 'NOTIFIED',

View File

@ -1,6 +1,12 @@
import { GraphQLClient } from 'graphql-request' import {
GraphQLClient
} from 'graphql-request'
import Factory from '../../seed/factories' import Factory from '../../seed/factories'
import { host, login, gql } from '../../jest/helpers' import {
host,
login,
gql
} from '../../jest/helpers'
const factory = Factory() const factory = Factory()
let client let client
@ -61,23 +67,29 @@ describe('currentUser notifications', () => {
factory.create('User', neighborParams), factory.create('User', neighborParams),
factory.create('Notification', { factory.create('Notification', {
id: 'post-mention-not-for-you', id: 'post-mention-not-for-you',
reason: 'mentioned_in_post',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'post-mention-already-seen', id: 'post-mention-already-seen',
read: true, read: true,
reason: 'mentioned_in_post',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'post-mention-unseen', id: 'post-mention-unseen',
reason: 'mentioned_in_post',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'comment-mention-not-for-you', id: 'comment-mention-not-for-you',
reason: 'mentioned_in_comment',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'comment-mention-already-seen', id: 'comment-mention-already-seen',
read: true, read: true,
reason: 'mentioned_in_comment',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'comment-mention-unseen', id: 'comment-mention-unseen',
reason: 'mentioned_in_comment',
}), }),
]) ])
await factory.authenticateAs(neighborParams) await factory.authenticateAs(neighborParams)
@ -170,8 +182,7 @@ describe('currentUser notifications', () => {
it('returns only unread notifications of current user', async () => { it('returns only unread notifications of current user', async () => {
const expected = { const expected = {
currentUser: { currentUser: {
notifications: expect.arrayContaining([ notifications: expect.arrayContaining([{
{
id: 'post-mention-unseen', id: 'post-mention-unseen',
post: { post: {
id: 'p1', id: 'p1',
@ -213,8 +224,7 @@ describe('currentUser notifications', () => {
it('returns all notifications of current user', async () => { it('returns all notifications of current user', async () => {
const expected = { const expected = {
currentUser: { currentUser: {
notifications: expect.arrayContaining([ notifications: expect.arrayContaining([{
{
id: 'post-mention-unseen', id: 'post-mention-unseen',
post: { post: {
id: 'p1', id: 'p1',
@ -286,9 +296,11 @@ describe('UpdateNotification', () => {
factory.create('User', mentionedParams), factory.create('User', mentionedParams),
factory.create('Notification', { factory.create('Notification', {
id: 'post-mention-to-be-updated', id: 'post-mention-to-be-updated',
reason: 'mentioned_in_post',
}), }),
factory.create('Notification', { factory.create('Notification', {
id: 'comment-mention-to-be-updated', id: 'comment-mention-to-be-updated',
reason: 'mentioned_in_comment',
}), }),
]) ])
await factory.authenticateAs(userParams) await factory.authenticateAs(userParams)

View File

@ -1,8 +1,9 @@
type Notification { type Notification {
id: ID! id: ID!
read: Boolean read: Boolean
reason: String
createdAt: String
user: User @relation(name: "NOTIFIED", direction: "OUT") user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN") post: Post @relation(name: "NOTIFIED", direction: "IN")
comment: Comment @relation(name: "NOTIFIED", direction: "IN") comment: Comment @relation(name: "NOTIFIED", direction: "IN")
createdAt: String
} }

View File

@ -62,15 +62,26 @@ export default function Factory(options = {}) {
lastResponse: null, lastResponse: null,
neodeInstance, neodeInstance,
async authenticateAs({ email, password }) { async authenticateAs({ email, password }) {
const headers = await authenticatedHeaders({ email, password }, seedServerHost) const headers = await authenticatedHeaders(
{
email,
password,
},
seedServerHost,
)
this.lastResponse = headers this.lastResponse = headers
this.graphQLClient = new GraphQLClient(seedServerHost, { headers }) this.graphQLClient = new GraphQLClient(seedServerHost, {
headers,
})
return this return this
}, },
async create(node, args = {}) { async create(node, args = {}) {
const { factory, mutation, variables } = this.factories[node](args) const { factory, mutation, variables } = this.factories[node](args)
if (factory) { if (factory) {
this.lastResponse = await factory({ args, neodeInstance }) this.lastResponse = await factory({
args,
neodeInstance,
})
return this.lastResponse return this.lastResponse
} else { } else {
this.lastResponse = await this.graphQLClient.request(mutation, variables) this.lastResponse = await this.graphQLClient.request(mutation, variables)
@ -121,11 +132,15 @@ export default function Factory(options = {}) {
}, },
async invite({ email }) { async invite({ email }) {
const mutation = ` mutation($email: String!) { invite( email: $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 return this
}, },
async cleanDatabase() { async cleanDatabase() {
this.lastResponse = await cleanDatabase({ driver: this.neo4jDriver }) this.lastResponse = await cleanDatabase({
driver: this.neo4jDriver,
})
return this return this
}, },
async emote({ to, data }) { async emote({ to, data }) {

View File

@ -8,9 +8,7 @@
:trunc="35" :trunc="35"
/> />
</ds-space> </ds-space>
<ds-text color="soft"> <ds-text color="soft">{{ $t(notificationTextIdents[notification.reason]) }}</ds-text>
{{ $t('notifications.menu.mentioned', { resource: post.id ? 'post' : 'comment' }) }}
</ds-text>
</no-ssr> </no-ssr>
<ds-space margin-bottom="x-small" /> <ds-space margin-bottom="x-small" />
<nuxt-link <nuxt-link
@ -49,6 +47,15 @@ export default {
required: true, required: true,
}, },
}, },
data() {
return {
notificationTextIdents: {
mentioned_in_post: 'notifications.menu.mentionedInPost',
mentioned_in_comment: 'notifications.menu.mentionedInComment',
comment_on_your_post: 'notifications.menu.commentedOnPost',
},
}
},
computed: { computed: {
excerpt() { excerpt() {
const excerpt = this.post.id ? this.post.contentExcerpt : this.comment.contentExcerpt const excerpt = this.post.id ? this.post.contentExcerpt : this.comment.contentExcerpt

View File

@ -37,6 +37,7 @@ const NOTIFICATIONS = gql`
notifications(read: false, orderBy: createdAt_desc) { notifications(read: false, orderBy: createdAt_desc) {
id id
read read
reason
createdAt createdAt
post { post {
id id

View File

@ -124,7 +124,9 @@
}, },
"notifications": { "notifications": {
"menu": { "menu": {
"mentioned": "hat dich in einem {resource} erwähnt" "mentionedInPost": "Hat dich in einem Beitrag erwähnt …",
"mentionedInComment": "Hat dich in einem Kommentar erwähnt …",
"commentedOnPost": "Hat deinen Beitrag kommentiert …"
} }
}, },
"search": { "search": {

View File

@ -124,7 +124,9 @@
}, },
"notifications": { "notifications": {
"menu": { "menu": {
"mentioned": "mentioned you in a {resource}" "mentionedInPost": "Mentioned you in a post …",
"mentionedInComment": "Mentioned you in a comment …",
"commentedOnPost": "Commented on your post …"
} }
}, },
"search": { "search": {