mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1270 from Human-Connection/1062-notification-about-comment-on-post
🍰 1062 notification about comment on post
This commit is contained in:
commit
8a729122c3
@ -1,96 +0,0 @@
|
||||
import extractMentionedUsers from './notifications/extractMentionedUsers'
|
||||
import extractHashtags from './hashtags/extractHashtags'
|
||||
|
||||
const notifyMentions = async (label, id, idsOfMentionedUsers, context) => {
|
||||
if (!idsOfMentionedUsers.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
const createdAt = new Date().toISOString()
|
||||
let cypher
|
||||
if (label === 'Post') {
|
||||
cypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfMentionedUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt })
|
||||
MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
} else {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfMentionedUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, createdAt: $createdAt })
|
||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
}
|
||||
await session.run(cypher, {
|
||||
idsOfMentionedUsers,
|
||||
label,
|
||||
createdAt,
|
||||
id,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p: Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfMentionedUsers = extractMentionedUsers(args.content)
|
||||
const hashtags = extractHashtags(args.content)
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
await notifyMentions('Post', post.id, idsOfMentionedUsers, context)
|
||||
await updateHashtagsOfPost(post.id, hashtags, context)
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfMentionedUsers = extractMentionedUsers(args.content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
await notifyMentions('Comment', comment.id, idsOfMentionedUsers, context)
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleContentDataOfComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
@ -1,392 +0,0 @@
|
||||
import { gql } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let user
|
||||
let authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const instance = neode()
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $content: String!, $categoryIds: [ID]!) {
|
||||
CreatePost(id: $id, title: $title, content: $content, categoryIds: $categoryIds) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $content: String!, $categoryIds: [ID]!) {
|
||||
UpdatePost(id: $id, content: $content, title: $title, categoryIds: $categoryIds) {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: instance,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
server = createServerResult.server
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
user = await instance.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('notifications', () => {
|
||||
const notificationQuery = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
post {
|
||||
content
|
||||
}
|
||||
comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = user
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
let postAuthor
|
||||
beforeEach(async () => {
|
||||
postAuthor = await instance.create('User', {
|
||||
email: 'post-author@example.org',
|
||||
password: '1234',
|
||||
id: 'postAuthor',
|
||||
})
|
||||
})
|
||||
|
||||
describe('who mentions me in a post', () => {
|
||||
const title = 'Mentioning Al Capone'
|
||||
const content =
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
|
||||
const createPostAction = async () => {
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: { id: 'p47', title, content, categoryIds },
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
}
|
||||
|
||||
it('sends you a notification', async () => {
|
||||
await createPostAction()
|
||||
const expectedContent =
|
||||
'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({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('who mentions me many times', () => {
|
||||
const updatePostAction = async () => {
|
||||
const updatedContent = `
|
||||
One more mention to
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again:
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
`
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
id: 'p47',
|
||||
title,
|
||||
content: updatedContent,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
}
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
await createPostAction()
|
||||
await updatePostAction()
|
||||
const expectedContent =
|
||||
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me', () => {
|
||||
beforeEach(async () => {
|
||||
await postAuthor.relateTo(user, 'blocked')
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me and a mentioner mentions me in a comment', () => {
|
||||
const createCommentOnPostAction = async () => {
|
||||
await createPostAction()
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $commentContent: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $commentContent) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
authenticatedUser = await commentMentioner.toJson()
|
||||
await mutate({
|
||||
mutation: createCommentMutation,
|
||||
variables: {
|
||||
id: 'c47',
|
||||
postId: 'p47',
|
||||
commentContent:
|
||||
'One mention of me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">.',
|
||||
},
|
||||
})
|
||||
authenticatedUser = await user.toJson()
|
||||
}
|
||||
let commentMentioner
|
||||
|
||||
beforeEach(async () => {
|
||||
await postAuthor.relateTo(user, 'blocked')
|
||||
commentMentioner = await instance.create('User', {
|
||||
id: 'mentioner',
|
||||
name: 'Mr Mentioner',
|
||||
slug: 'mr-mentioner',
|
||||
email: 'mentioner@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('Hashtags', () => {
|
||||
const id = 'p135'
|
||||
const title = 'Two Hashtags'
|
||||
const content =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postWithHastagsQuery = gql`
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
tags {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const postWithHastagsVariables = {
|
||||
id,
|
||||
}
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('create a Post with Hashtags', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('both Hashtags are created with the "id" set to their "name"', async () => {
|
||||
const expected = [{ id: 'Democracy' }, { id: 'Liberty' }]
|
||||
await expect(
|
||||
query({
|
||||
query: postWithHastagsQuery,
|
||||
variables: postWithHastagsVariables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||
// The already existing Hashtag has no class at this point.
|
||||
const content =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
|
||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||
await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
id,
|
||||
title,
|
||||
content,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
|
||||
const expected = [{ id: 'Elections' }, { id: 'Liberty' }]
|
||||
await expect(
|
||||
query({
|
||||
query: postWithHastagsQuery,
|
||||
variables: postWithHastagsVariables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
49
backend/src/middleware/hashtags/hashtagsMiddleware.js
Normal file
49
backend/src/middleware/hashtags/hashtagsMiddleware.js
Normal file
@ -0,0 +1,49 @@
|
||||
import extractHashtags from '../hashtags/extractHashtags'
|
||||
|
||||
const updateHashtagsOfPost = async (postId, hashtags, context) => {
|
||||
if (!hashtags.length) return
|
||||
|
||||
const session = context.driver.session()
|
||||
// We need two Cypher statements, because the 'MATCH' in the 'cypherDeletePreviousRelations' statement
|
||||
// functions as an 'if'. In case there is no previous relation, the rest of the commands are omitted
|
||||
// and no new Hashtags and relations will be created.
|
||||
const cypherDeletePreviousRelations = `
|
||||
MATCH (p: Post { id: $postId })-[previousRelations: TAGGED]->(t: Tag)
|
||||
DELETE previousRelations
|
||||
RETURN p, t
|
||||
`
|
||||
const cypherCreateNewTagsAndRelations = `
|
||||
MATCH (p: Post { id: $postId})
|
||||
UNWIND $hashtags AS tagName
|
||||
MERGE (t: Tag { id: tagName, disabled: false, deleted: false })
|
||||
MERGE (p)-[:TAGGED]->(t)
|
||||
RETURN p, t
|
||||
`
|
||||
await session.run(cypherDeletePreviousRelations, {
|
||||
postId,
|
||||
})
|
||||
await session.run(cypherCreateNewTagsAndRelations, {
|
||||
postId,
|
||||
hashtags,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const hashtags = extractHashtags(args.content)
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (post) {
|
||||
await updateHashtagsOfPost(post.id, hashtags, context)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
},
|
||||
}
|
||||
176
backend/src/middleware/hashtags/hashtagsMiddleware.spec.js
Normal file
176
backend/src/middleware/hashtags/hashtagsMiddleware.spec.js
Normal file
@ -0,0 +1,176 @@
|
||||
import { gql } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let hashtagingUser
|
||||
let authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const instance = neode()
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: instance,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
server = createServerResult.server
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
hashtagingUser = await instance.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('hashtags', () => {
|
||||
const id = 'p135'
|
||||
const title = 'Two Hashtags'
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Democracy">#Democracy</a> should work equal for everybody!? That seems to be the only way to have equal <a class="hashtag" href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
const postWithHastagsQuery = gql`
|
||||
query($id: ID) {
|
||||
Post(id: $id) {
|
||||
tags {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const postWithHastagsVariables = {
|
||||
id,
|
||||
}
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await hashtagingUser.toJson()
|
||||
})
|
||||
|
||||
describe('create a Post with Hashtags', () => {
|
||||
beforeEach(async () => {
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id,
|
||||
title,
|
||||
postContent,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('both hashtags are created with the "id" set to their "name"', async () => {
|
||||
const expected = [
|
||||
{
|
||||
id: 'Democracy',
|
||||
},
|
||||
{
|
||||
id: 'Liberty',
|
||||
},
|
||||
]
|
||||
await expect(
|
||||
query({
|
||||
query: postWithHastagsQuery,
|
||||
variables: postWithHastagsVariables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
||||
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||
// The already existing Hashtag has no class at this point.
|
||||
const postContent =
|
||||
'<p>Hey Dude, <a class="hashtag" href="/search/hashtag/Elections">#Elections</a> should work equal for everybody!? That seems to be the only way to have equal <a href="/search/hashtag/Liberty">#Liberty</a> for everyone.</p>'
|
||||
|
||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||
await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
id,
|
||||
title,
|
||||
postContent,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
|
||||
const expected = [
|
||||
{
|
||||
id: 'Elections',
|
||||
},
|
||||
{
|
||||
id: 'Liberty',
|
||||
},
|
||||
]
|
||||
await expect(
|
||||
query({
|
||||
query: postWithHastagsQuery,
|
||||
variables: postWithHastagsVariables,
|
||||
}),
|
||||
).resolves.toEqual(
|
||||
expect.objectContaining({
|
||||
data: {
|
||||
Post: [
|
||||
{
|
||||
tags: expect.arrayContaining(expected),
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -12,7 +12,8 @@ import user from './userMiddleware'
|
||||
import includedFields from './includedFieldsMiddleware'
|
||||
import orderBy from './orderByMiddleware'
|
||||
import validation from './validation/validationMiddleware'
|
||||
import handleContentData from './handleHtmlContent/handleContentData'
|
||||
import notifications from './notifications/notificationsMiddleware'
|
||||
import hashtags from './hashtags/hashtagsMiddleware'
|
||||
import email from './email/emailMiddleware'
|
||||
import sentry from './sentryMiddleware'
|
||||
|
||||
@ -25,13 +26,16 @@ export default schema => {
|
||||
validation,
|
||||
sluggify,
|
||||
excerpt,
|
||||
handleContentData,
|
||||
notifications,
|
||||
hashtags,
|
||||
xss,
|
||||
softDelete,
|
||||
user,
|
||||
includedFields,
|
||||
orderBy,
|
||||
email: email({ isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT }),
|
||||
email: email({
|
||||
isEnabled: CONFIG.SMTP_HOST && CONFIG.SMTP_PORT,
|
||||
}),
|
||||
}
|
||||
|
||||
let order = [
|
||||
@ -43,7 +47,8 @@ export default schema => {
|
||||
'sluggify',
|
||||
'excerpt',
|
||||
'email',
|
||||
'handleContentData',
|
||||
'notifications',
|
||||
'hashtags',
|
||||
'xss',
|
||||
'softDelete',
|
||||
'user',
|
||||
|
||||
122
backend/src/middleware/notifications/notificationsMiddleware.js
Normal file
122
backend/src/middleware/notifications/notificationsMiddleware.js
Normal file
@ -0,0 +1,122 @@
|
||||
import extractMentionedUsers from './mentions/extractMentionedUsers'
|
||||
|
||||
const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
if (!idsOfUsers.length) return
|
||||
|
||||
// Checked here, because it does not go through GraphQL checks at all in this file.
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'comment_on_post']
|
||||
if (!reasonsAllowed.includes(reason)) {
|
||||
throw new Error('Notification reason is not allowed!')
|
||||
}
|
||||
if (
|
||||
(label === 'Post' && reason !== 'mentioned_in_post') ||
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'comment_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
|
||||
const session = context.driver.session()
|
||||
const createdAt = new Date().toISOString()
|
||||
let cypher
|
||||
switch (reason) {
|
||||
case 'mentioned_in_post': {
|
||||
cypher = `
|
||||
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
||||
MERGE (post)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'mentioned_in_comment': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (user)<-[:BLOCKED]-(postAuthor)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'comment_on_post': {
|
||||
cypher = `
|
||||
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
|
||||
MATCH (user: User)
|
||||
WHERE user.id in $idsOfUsers
|
||||
AND NOT (user)<-[:BLOCKED]-(author)
|
||||
AND NOT (author)<-[:BLOCKED]-(user)
|
||||
CREATE (notification: Notification {id: apoc.create.uuid(), read: false, reason: $reason, createdAt: $createdAt })
|
||||
MERGE (comment)-[:NOTIFIED]->(notification)-[:NOTIFIED]->(user)
|
||||
`
|
||||
break
|
||||
}
|
||||
}
|
||||
await session.run(cypher, {
|
||||
label,
|
||||
id,
|
||||
idsOfUsers,
|
||||
reason,
|
||||
createdAt,
|
||||
})
|
||||
session.close()
|
||||
}
|
||||
|
||||
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
|
||||
const post = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (post) {
|
||||
await notifyUsers('Post', post.id, idsOfUsers, 'mentioned_in_post', context)
|
||||
}
|
||||
|
||||
return post
|
||||
}
|
||||
|
||||
const handleContentDataOfComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const idsOfUsers = extractMentionedUsers(args.content)
|
||||
const comment = await resolve(root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
await notifyUsers('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context)
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
const handleCreateComment = async (resolve, root, args, context, resolveInfo) => {
|
||||
const comment = await handleContentDataOfComment(resolve, root, args, context, resolveInfo)
|
||||
|
||||
if (comment) {
|
||||
const session = context.driver.session()
|
||||
const cypherFindUser = `
|
||||
MATCH (user: User)-[:WROTE]->(:Post)<-[:COMMENTS]-(:Comment { id: $commentId })
|
||||
RETURN user { .id }
|
||||
`
|
||||
const result = await session.run(cypherFindUser, {
|
||||
commentId: comment.id,
|
||||
})
|
||||
session.close()
|
||||
const [postAuthor] = await result.records.map(record => {
|
||||
return record.get('user')
|
||||
})
|
||||
if (context.user.id !== postAuthor.id) {
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context)
|
||||
}
|
||||
}
|
||||
|
||||
return comment
|
||||
}
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
CreatePost: handleContentDataOfPost,
|
||||
UpdatePost: handleContentDataOfPost,
|
||||
CreateComment: handleCreateComment,
|
||||
UpdateComment: handleContentDataOfComment,
|
||||
},
|
||||
}
|
||||
@ -0,0 +1,463 @@
|
||||
import { gql } from '../../jest/helpers'
|
||||
import Factory from '../../seed/factories'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||
import createServer from '../../server'
|
||||
|
||||
let server
|
||||
let query
|
||||
let mutate
|
||||
let notifiedUser
|
||||
let authenticatedUser
|
||||
const factory = Factory()
|
||||
const driver = getDriver()
|
||||
const instance = neode()
|
||||
const categoryIds = ['cat9']
|
||||
const createPostMutation = gql`
|
||||
mutation($id: ID, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
CreatePost(id: $id, title: $title, content: $postContent, categoryIds: $categoryIds) {
|
||||
id
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
const updatePostMutation = gql`
|
||||
mutation($id: ID!, $title: String!, $postContent: String!, $categoryIds: [ID]!) {
|
||||
UpdatePost(id: $id, content: $postContent, title: $title, categoryIds: $categoryIds) {
|
||||
title
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
const createCommentMutation = gql`
|
||||
mutation($id: ID, $postId: ID!, $commentContent: String!) {
|
||||
CreateComment(id: $id, postId: $postId, content: $commentContent) {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
beforeAll(() => {
|
||||
const createServerResult = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
user: authenticatedUser,
|
||||
neode: instance,
|
||||
driver,
|
||||
}
|
||||
},
|
||||
})
|
||||
server = createServerResult.server
|
||||
const createTestClientResult = createTestClient(server)
|
||||
query = createTestClientResult.query
|
||||
mutate = createTestClientResult.mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
notifiedUser = await instance.create('User', {
|
||||
id: 'you',
|
||||
name: 'Al Capone',
|
||||
slug: 'al-capone',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('notifications', () => {
|
||||
const notificationQuery = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
reason
|
||||
post {
|
||||
content
|
||||
}
|
||||
comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await notifiedUser.toJson()
|
||||
})
|
||||
|
||||
describe('given another user', () => {
|
||||
let title
|
||||
let postContent
|
||||
let postAuthor
|
||||
const createPostAction = async () => {
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createPostMutation,
|
||||
variables: {
|
||||
id: 'p47',
|
||||
title,
|
||||
postContent,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await notifiedUser.toJson()
|
||||
}
|
||||
|
||||
let commentContent
|
||||
let commentAuthor
|
||||
const createCommentOnPostAction = async () => {
|
||||
await createPostAction()
|
||||
authenticatedUser = await commentAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: createCommentMutation,
|
||||
variables: {
|
||||
id: 'c47',
|
||||
postId: 'p47',
|
||||
commentContent,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await notifiedUser.toJson()
|
||||
}
|
||||
|
||||
describe('comments on my post', () => {
|
||||
beforeEach(async () => {
|
||||
title = 'My post'
|
||||
postContent = 'My post content.'
|
||||
postAuthor = notifiedUser
|
||||
})
|
||||
|
||||
describe('commenter is not me', () => {
|
||||
beforeEach(async () => {
|
||||
commentContent = 'Commenters comment.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
email: 'commentauthor@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends me a notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'comment_on_post',
|
||||
post: null,
|
||||
comment: {
|
||||
content: commentContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('sends me no notification if I have blocked the comment author', async () => {
|
||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('commenter is me', () => {
|
||||
beforeEach(async () => {
|
||||
commentContent = 'My comment.'
|
||||
commentAuthor = notifiedUser
|
||||
})
|
||||
|
||||
it('sends me no notification', async () => {
|
||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
postAuthor = await instance.create('User', {
|
||||
id: 'postAuthor',
|
||||
name: 'Mrs Post',
|
||||
slug: 'mrs-post',
|
||||
email: 'post-author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
describe('mentions me in a post', () => {
|
||||
beforeEach(async () => {
|
||||
title = 'Mentioning Al Capone'
|
||||
postContent =
|
||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||
})
|
||||
|
||||
it('sends me a notification', async () => {
|
||||
await createPostAction()
|
||||
const expectedContent =
|
||||
'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({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('many times', () => {
|
||||
const updatePostAction = async () => {
|
||||
const updatedContent = `
|
||||
One more mention to
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again:
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
and again
|
||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||
@al-capone
|
||||
</a>
|
||||
`
|
||||
authenticatedUser = await postAuthor.toJson()
|
||||
await mutate({
|
||||
mutation: updatePostMutation,
|
||||
variables: {
|
||||
id: 'p47',
|
||||
title,
|
||||
postContent: updatedContent,
|
||||
categoryIds,
|
||||
},
|
||||
})
|
||||
authenticatedUser = await notifiedUser.toJson()
|
||||
}
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
await createPostAction()
|
||||
await updatePostAction()
|
||||
const expectedContent =
|
||||
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me', () => {
|
||||
beforeEach(async () => {
|
||||
await postAuthor.relateTo(notifiedUser, 'blocked')
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('mentions me in a comment', () => {
|
||||
beforeEach(async () => {
|
||||
title = 'Post where I get mentioned in a comment'
|
||||
postContent = 'Content of post where I get mentioned in a comment.'
|
||||
})
|
||||
|
||||
describe('I am not blocked at all', () => {
|
||||
beforeEach(async () => {
|
||||
commentContent =
|
||||
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
email: 'comment-author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends a notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_comment',
|
||||
post: null,
|
||||
comment: {
|
||||
content: commentContent,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me', () => {
|
||||
beforeEach(async () => {
|
||||
await postAuthor.relateTo(notifiedUser, 'blocked')
|
||||
commentContent =
|
||||
'One mention about me with <a data-mention-id="you" class="mention" href="/profile/you" target="_blank">@al-capone</a>.'
|
||||
commentAuthor = await instance.create('User', {
|
||||
id: 'commentAuthor',
|
||||
name: 'Mrs Comment',
|
||||
slug: 'mrs-comment',
|
||||
email: 'comment-author@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
})
|
||||
|
||||
it('sends no notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
query({
|
||||
query: notificationQuery,
|
||||
variables: {
|
||||
read: false,
|
||||
},
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
@ -1,9 +1,26 @@
|
||||
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_post'],
|
||||
invalid: [null],
|
||||
default: 'mentioned_in_post',
|
||||
},
|
||||
createdAt: {
|
||||
type: 'string',
|
||||
isoDate: true,
|
||||
default: () => new Date().toISOString(),
|
||||
},
|
||||
user: {
|
||||
type: 'relationship',
|
||||
relationship: 'NOTIFIED',
|
||||
|
||||
@ -69,23 +69,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)
|
||||
@ -287,9 +293,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)
|
||||
|
||||
5
backend/src/schema/types/enum/ReasonNotification.gql
Normal file
5
backend/src/schema/types/enum/ReasonNotification.gql
Normal file
@ -0,0 +1,5 @@
|
||||
enum ReasonNotification {
|
||||
mentioned_in_post
|
||||
mentioned_in_comment
|
||||
comment_on_post
|
||||
}
|
||||
@ -1,8 +1,9 @@
|
||||
type Notification {
|
||||
id: ID!
|
||||
read: Boolean
|
||||
reason: ReasonNotification
|
||||
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
|
||||
}
|
||||
|
||||
@ -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 }) {
|
||||
|
||||
@ -14,10 +14,11 @@ describe('Notification', () => {
|
||||
let stubs
|
||||
let mocks
|
||||
let propsData
|
||||
let wrapper
|
||||
beforeEach(() => {
|
||||
propsData = {}
|
||||
mocks = {
|
||||
$t: jest.fn(),
|
||||
$t: key => key,
|
||||
}
|
||||
stubs = {
|
||||
NuxtLink: RouterLinkStub,
|
||||
@ -33,37 +34,159 @@ describe('Notification', () => {
|
||||
})
|
||||
}
|
||||
|
||||
describe('given a notification', () => {
|
||||
describe('given a notification about a comment on a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
post: {
|
||||
title: "It's a title",
|
||||
id: 'post-1',
|
||||
slug: 'its-a-title',
|
||||
contentExcerpt: '<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best',
|
||||
reason: 'comment_on_post',
|
||||
post: null,
|
||||
comment: {
|
||||
id: 'comment-1',
|
||||
contentExcerpt:
|
||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||
post: {
|
||||
title: "It's a post title",
|
||||
id: 'post-1',
|
||||
slug: 'its-a-title',
|
||||
contentExcerpt: 'Post content.',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
'notifications.menu.comment_on_post',
|
||||
)
|
||||
})
|
||||
it('renders title', () => {
|
||||
expect(Wrapper().text()).toContain("It's a title")
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain("It's a post title")
|
||||
})
|
||||
it('renders the "Comment:"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('Comment:')
|
||||
})
|
||||
|
||||
it('renders the contentExcerpt', () => {
|
||||
expect(Wrapper().text()).toContain('@jenny-rostock is the best')
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
|
||||
it('has no class "read"', () => {
|
||||
expect(Wrapper().classes()).not.toContain('read')
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification.read = true
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(Wrapper().classes()).toContain('read')
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a notification about a mention in a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
title: "It's a post title",
|
||||
id: 'post-1',
|
||||
slug: 'its-a-title',
|
||||
contentExcerpt:
|
||||
'<a href="/profile/u3" target="_blank">@jenny-rostock</a> is the best on this post.',
|
||||
},
|
||||
comment: null,
|
||||
}
|
||||
})
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
'notifications.menu.mentioned_in_post',
|
||||
)
|
||||
})
|
||||
it('renders title', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain("It's a post title")
|
||||
})
|
||||
it('renders the contentExcerpt', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@jenny-rostock is the best on this post.')
|
||||
})
|
||||
it('has no class "read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification.read = true
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('given a notification about a mention in a comment', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
reason: 'mentioned_in_comment',
|
||||
post: null,
|
||||
comment: {
|
||||
id: 'comment-1',
|
||||
contentExcerpt:
|
||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||
post: {
|
||||
title: "It's a post title",
|
||||
id: 'post-1',
|
||||
slug: 'its-a-title',
|
||||
contentExcerpt: 'Post content.',
|
||||
},
|
||||
},
|
||||
}
|
||||
})
|
||||
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
'notifications.menu.mentioned_in_comment',
|
||||
)
|
||||
})
|
||||
it('renders title', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain("It's a post title")
|
||||
})
|
||||
|
||||
it('renders the "Comment:"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('Comment:')
|
||||
})
|
||||
|
||||
it('renders the contentExcerpt', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.text()).toContain('@dagobert-duck is the best on this comment.')
|
||||
})
|
||||
|
||||
it('has no class "read"', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.classes()).not.toContain('read')
|
||||
})
|
||||
|
||||
describe('that is read', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification.read = true
|
||||
wrapper = Wrapper()
|
||||
})
|
||||
|
||||
it('has class "read"', () => {
|
||||
expect(wrapper.classes()).toContain('read')
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -10,8 +10,8 @@
|
||||
/>
|
||||
<hc-user v-else :user="comment.author" :date-time="comment.createdAt" :trunc="35" />
|
||||
</ds-space>
|
||||
<ds-text color="soft">
|
||||
{{ $t('notifications.menu.mentioned', { resource: resourceType }) }}
|
||||
<ds-text class="reason-text-for-test" color="soft">
|
||||
{{ $t(`notifications.menu.${notification.reason}`) }}
|
||||
</ds-text>
|
||||
</no-ssr>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
|
||||
@ -78,12 +78,13 @@ export default i18n => {
|
||||
|
||||
export const currentUserNotificationsQuery = () => {
|
||||
return gql`
|
||||
{
|
||||
query {
|
||||
currentUser {
|
||||
id
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
post {
|
||||
id
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"all": "Alle"
|
||||
},
|
||||
"general": {
|
||||
"header": "Filtern nach..."
|
||||
"header": "Filtern nach …"
|
||||
},
|
||||
"followers": {
|
||||
"label": "Benutzern, denen ich folge"
|
||||
@ -96,7 +96,7 @@
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"placeholder": "Schreib etwas Inspirierendes...",
|
||||
"placeholder": "Schreib etwas Inspirierendes …",
|
||||
"mention": {
|
||||
"noUsersFound": "Keine Benutzer gefunden"
|
||||
},
|
||||
@ -132,7 +132,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"menu": {
|
||||
"mentioned": "hat dich in einem {resource} erwähnt"
|
||||
"mentioned_in_post": "Hat dich in einem Beitrag erwähnt …",
|
||||
"mentioned_in_comment": "Hat dich in einem Kommentar erwähnt …",
|
||||
"comment_on_post": "Hat deinen Beitrag kommentiert …"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
@ -304,7 +306,7 @@
|
||||
},
|
||||
"comment": {
|
||||
"content": {
|
||||
"unavailable-placeholder": "...dieser Kommentar ist nicht mehr verfügbar"
|
||||
"unavailable-placeholder": "… dieser Kommentar ist nicht mehr verfügbar"
|
||||
},
|
||||
"menu": {
|
||||
"edit": "Kommentar bearbeiten",
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
"all": "All"
|
||||
},
|
||||
"general": {
|
||||
"header": "Filter by..."
|
||||
"header": "Filter by …"
|
||||
},
|
||||
"followers": {
|
||||
"label": "Users I follow"
|
||||
@ -96,7 +96,7 @@
|
||||
}
|
||||
},
|
||||
"editor": {
|
||||
"placeholder": "Leave your inspirational thoughts...",
|
||||
"placeholder": "Leave your inspirational thoughts …",
|
||||
"mention": {
|
||||
"noUsersFound": "No users found"
|
||||
},
|
||||
@ -132,7 +132,9 @@
|
||||
},
|
||||
"notifications": {
|
||||
"menu": {
|
||||
"mentioned": "mentioned you in a {resource}"
|
||||
"mentioned_in_post": "Mentioned you in a post …",
|
||||
"mentioned_in_comment": "Mentioned you in a comment …",
|
||||
"comment_on_post": "Commented on your post …"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
@ -304,7 +306,7 @@
|
||||
},
|
||||
"comment": {
|
||||
"content": {
|
||||
"unavailable-placeholder": "...this comment is not available anymore"
|
||||
"unavailable-placeholder": "… this comment is not available anymore"
|
||||
},
|
||||
"menu": {
|
||||
"edit": "Edit Comment",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user