mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2025-12-13 07:45:56 +00:00
Merge pull request #1426 from Human-Connection/1414-refactor_notifications
1414 Bugfix: Delete notifications if connected post, comment or user is deleted
This commit is contained in:
commit
63520a43cd
@ -12,11 +12,9 @@ export default {
|
||||
CreatePost: setCreatedAt,
|
||||
CreateComment: setCreatedAt,
|
||||
CreateOrganization: setCreatedAt,
|
||||
CreateNotification: setCreatedAt,
|
||||
UpdateUser: setUpdatedAt,
|
||||
UpdatePost: setUpdatedAt,
|
||||
UpdateComment: setUpdatedAt,
|
||||
UpdateOrganization: setUpdatedAt,
|
||||
UpdateNotification: setUpdatedAt,
|
||||
},
|
||||
}
|
||||
|
||||
@ -4,13 +4,13 @@ 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']
|
||||
const reasonsAllowed = ['mentioned_in_post', 'mentioned_in_comment', 'commented_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))
|
||||
(label === 'Comment' && !['mentioned_in_comment', 'commented_on_post'].includes(reason))
|
||||
) {
|
||||
throw new Error('Notification does not fit the reason!')
|
||||
}
|
||||
@ -25,8 +25,9 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
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)
|
||||
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = $createdAt
|
||||
`
|
||||
break
|
||||
}
|
||||
@ -37,20 +38,22 @@ const notifyUsers = async (label, id, idsOfUsers, reason, context) => {
|
||||
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)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = $createdAt
|
||||
`
|
||||
break
|
||||
}
|
||||
case 'comment_on_post': {
|
||||
case 'commented_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)
|
||||
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
|
||||
SET notification.read = FALSE
|
||||
SET notification.createdAt = $createdAt
|
||||
`
|
||||
break
|
||||
}
|
||||
@ -105,7 +108,7 @@ const handleCreateComment = async (resolve, root, args, context, resolveInfo) =>
|
||||
return record.get('user')
|
||||
})
|
||||
if (context.user.id !== postAuthor.id) {
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'comment_on_post', context)
|
||||
await notifyUsers('Comment', comment.id, [postAuthor.id], 'commented_on_post', context)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -77,14 +77,18 @@ afterEach(async () => {
|
||||
describe('notifications', () => {
|
||||
const notificationQuery = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
reason
|
||||
post {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
id
|
||||
content
|
||||
}
|
||||
comment {
|
||||
... on Comment {
|
||||
id
|
||||
content
|
||||
}
|
||||
}
|
||||
@ -154,18 +158,18 @@ describe('notifications', () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'comment_on_post',
|
||||
post: null,
|
||||
comment: {
|
||||
content: commentContent,
|
||||
},
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
createdAt: expect.any(String),
|
||||
reason: 'commented_on_post',
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
id: 'c47',
|
||||
content: commentContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
@ -183,11 +187,7 @@ describe('notifications', () => {
|
||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
@ -211,11 +211,7 @@ describe('notifications', () => {
|
||||
await notifiedUser.relateTo(commentAuthor, 'blocked')
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
@ -253,18 +249,18 @@ describe('notifications', () => {
|
||||
'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,
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
createdAt: expect.any(String),
|
||||
reason: 'mentioned_in_post',
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'p47',
|
||||
content: expectedContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
@ -278,7 +274,7 @@ describe('notifications', () => {
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('many times', () => {
|
||||
describe('updates the post and mentions me again', () => {
|
||||
const updatePostAction = async () => {
|
||||
const updatedContent = `
|
||||
One more mention to
|
||||
@ -307,33 +303,25 @@ describe('notifications', () => {
|
||||
authenticatedUser = await notifiedUser.toJson()
|
||||
}
|
||||
|
||||
it('creates exactly one more notification', async () => {
|
||||
it('creates no duplicate notification for the same resource', async () => {
|
||||
const expectedUpdatedContent =
|
||||
'<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>'
|
||||
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,
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
createdAt: expect.any(String),
|
||||
reason: 'mentioned_in_post',
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'p47',
|
||||
content: expectedUpdatedContent,
|
||||
},
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
content: expectedContent,
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await expect(
|
||||
@ -345,6 +333,68 @@ describe('notifications', () => {
|
||||
}),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
describe('if the notification was marked as read earlier', () => {
|
||||
const markAsReadAction = async () => {
|
||||
const mutation = gql`
|
||||
mutation($id: ID!) {
|
||||
markAsRead(id: $id) {
|
||||
read
|
||||
}
|
||||
}
|
||||
`
|
||||
await mutate({ mutation, variables: { id: 'p47' } })
|
||||
}
|
||||
|
||||
describe('but the next mention happens after the notification was marked as read', () => {
|
||||
it('sets the `read` attribute to false again', async () => {
|
||||
await createPostAction()
|
||||
await markAsReadAction()
|
||||
const {
|
||||
data: {
|
||||
notifications: [{ read: readBefore }],
|
||||
},
|
||||
} = await query({
|
||||
query: notificationQuery,
|
||||
})
|
||||
await updatePostAction()
|
||||
const {
|
||||
data: {
|
||||
notifications: [{ read: readAfter }],
|
||||
},
|
||||
} = await query({
|
||||
query: notificationQuery,
|
||||
})
|
||||
expect(readBefore).toEqual(true)
|
||||
expect(readAfter).toEqual(false)
|
||||
})
|
||||
|
||||
it('updates the `createdAt` attribute', async () => {
|
||||
await createPostAction()
|
||||
await markAsReadAction()
|
||||
const {
|
||||
data: {
|
||||
notifications: [{ createdAt: createdAtBefore }],
|
||||
},
|
||||
} = await query({
|
||||
query: notificationQuery,
|
||||
})
|
||||
await updatePostAction()
|
||||
const {
|
||||
data: {
|
||||
notifications: [{ createdAt: createdAtAfter }],
|
||||
},
|
||||
} = await query({
|
||||
query: notificationQuery,
|
||||
})
|
||||
expect(createdAtBefore).toBeTruthy()
|
||||
expect(Date.parse(createdAtBefore)).toEqual(expect.any(Number))
|
||||
expect(createdAtAfter).toBeTruthy()
|
||||
expect(Date.parse(createdAtAfter)).toEqual(expect.any(Number))
|
||||
expect(createdAtBefore).not.toEqual(createdAtAfter)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('but the author of the post blocked me', () => {
|
||||
@ -355,11 +405,7 @@ describe('notifications', () => {
|
||||
it('sends no notification', async () => {
|
||||
await createPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
@ -397,18 +443,18 @@ describe('notifications', () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
reason: 'mentioned_in_comment',
|
||||
post: null,
|
||||
comment: {
|
||||
content: commentContent,
|
||||
},
|
||||
notifications: [
|
||||
{
|
||||
read: false,
|
||||
createdAt: expect.any(String),
|
||||
reason: 'mentioned_in_comment',
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
id: 'c47',
|
||||
content: commentContent,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
@ -440,11 +486,7 @@ describe('notifications', () => {
|
||||
it('sends no notification', async () => {
|
||||
await createCommentOnPostAction()
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
currentUser: {
|
||||
notifications: [],
|
||||
},
|
||||
},
|
||||
data: { notifications: [] },
|
||||
})
|
||||
const { query } = createTestClient(server)
|
||||
await expect(
|
||||
|
||||
@ -41,32 +41,6 @@ const isMySocialMedia = rule({
|
||||
return socialMedia.ownedBy.node.id === user.id
|
||||
})
|
||||
|
||||
const belongsToMe = rule({
|
||||
cache: 'no_cache',
|
||||
})(async (_, args, context) => {
|
||||
const {
|
||||
driver,
|
||||
user: { id: userId },
|
||||
} = context
|
||||
const { id: notificationId } = args
|
||||
const session = driver.session()
|
||||
const result = await session.run(
|
||||
`
|
||||
MATCH (u:User {id: $userId})<-[:NOTIFIED]-(n:Notification {id: $notificationId})
|
||||
RETURN n
|
||||
`,
|
||||
{
|
||||
userId,
|
||||
notificationId,
|
||||
},
|
||||
)
|
||||
const [notification] = result.records.map(record => {
|
||||
return record.get('n')
|
||||
})
|
||||
session.close()
|
||||
return Boolean(notification)
|
||||
})
|
||||
|
||||
/* TODO: decide if we want to remove this check: the check
|
||||
* `onlyEnabledContent` throws authorization errors only if you have
|
||||
* arguments for `disabled` or `deleted` assuming these are filter
|
||||
@ -149,7 +123,6 @@ const permissions = shield(
|
||||
Category: allow,
|
||||
Tag: allow,
|
||||
Report: isModerator,
|
||||
Notification: isAdmin,
|
||||
statistics: allow,
|
||||
currentUser: allow,
|
||||
Post: or(onlyEnabledContent, isModerator),
|
||||
@ -160,6 +133,7 @@ const permissions = shield(
|
||||
PostsEmotionsCountByEmotion: allow,
|
||||
PostsEmotionsByCurrentUser: allow,
|
||||
blockedUsers: isAuthenticated,
|
||||
notifications: isAuthenticated,
|
||||
},
|
||||
Mutation: {
|
||||
'*': deny,
|
||||
@ -168,7 +142,6 @@ const permissions = shield(
|
||||
Signup: isAdmin,
|
||||
SignupVerification: allow,
|
||||
CreateInvitationCode: and(isAuthenticated, or(not(invitationLimitReached), isAdmin)),
|
||||
UpdateNotification: belongsToMe,
|
||||
UpdateUser: onlyYourself,
|
||||
CreatePost: isAuthenticated,
|
||||
UpdatePost: isAuthor,
|
||||
@ -198,6 +171,7 @@ const permissions = shield(
|
||||
RemovePostEmotions: isAuthenticated,
|
||||
block: isAuthenticated,
|
||||
unblock: isAuthenticated,
|
||||
markAsRead: isAuthenticated,
|
||||
},
|
||||
User: {
|
||||
email: isMyOwn,
|
||||
|
||||
@ -1,36 +0,0 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
module.exports = {
|
||||
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',
|
||||
target: 'User',
|
||||
direction: 'out',
|
||||
},
|
||||
post: {
|
||||
type: 'relationship',
|
||||
relationship: 'NOTIFIED',
|
||||
target: 'Post',
|
||||
direction: 'in',
|
||||
},
|
||||
}
|
||||
@ -7,6 +7,5 @@ export default {
|
||||
EmailAddress: require('./EmailAddress.js'),
|
||||
SocialMedia: require('./SocialMedia.js'),
|
||||
Post: require('./Post.js'),
|
||||
Notification: require('./Notification.js'),
|
||||
Category: require('./Category.js'),
|
||||
}
|
||||
|
||||
@ -20,6 +20,7 @@ export default applyScalars(
|
||||
'Statistics',
|
||||
'LoggedInUser',
|
||||
'SocialMedia',
|
||||
'NOTIFIED',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
@ -32,6 +33,7 @@ export default applyScalars(
|
||||
'Statistics',
|
||||
'LoggedInUser',
|
||||
'SocialMedia',
|
||||
'NOTIFIED',
|
||||
],
|
||||
// add 'User' here as soon as possible
|
||||
},
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
export default {
|
||||
Mutation: {
|
||||
@ -52,4 +53,13 @@ export default {
|
||||
return comment
|
||||
},
|
||||
},
|
||||
Comment: {
|
||||
...Resolver('Comment', {
|
||||
hasOne: {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
post: '-[:COMMENTS]->(related:Post)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
},
|
||||
}),
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,14 +1,80 @@
|
||||
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
const resourceTypes = ['Post', 'Comment']
|
||||
|
||||
const transformReturnType = record => {
|
||||
return {
|
||||
...record.get('notification').properties,
|
||||
from: {
|
||||
__typename: record.get('resource').labels.find(l => resourceTypes.includes(l)),
|
||||
...record.get('resource').properties,
|
||||
},
|
||||
to: {
|
||||
...record.get('user').properties,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export default {
|
||||
Query: {
|
||||
Notification: (object, params, context, resolveInfo) => {
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
notifications: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notifications
|
||||
let whereClause
|
||||
let orderByClause
|
||||
switch (args.read) {
|
||||
case true:
|
||||
whereClause = 'WHERE notification.read = TRUE'
|
||||
break
|
||||
case false:
|
||||
whereClause = 'WHERE notification.read = FALSE'
|
||||
break
|
||||
default:
|
||||
whereClause = ''
|
||||
}
|
||||
switch (args.orderBy) {
|
||||
case 'createdAt_asc':
|
||||
orderByClause = 'ORDER BY notification.createdAt ASC'
|
||||
break
|
||||
case 'createdAt_desc':
|
||||
orderByClause = 'ORDER BY notification.createdAt DESC'
|
||||
break
|
||||
default:
|
||||
orderByClause = ''
|
||||
}
|
||||
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource)-[notification:NOTIFIED]->(user:User {id:$id})
|
||||
${whereClause}
|
||||
RETURN resource, notification, user
|
||||
${orderByClause}
|
||||
`
|
||||
const result = await session.run(cypher, { id: currentUser.id })
|
||||
notifications = await result.records.map(transformReturnType)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notifications
|
||||
},
|
||||
},
|
||||
Mutation: {
|
||||
UpdateNotification: (object, params, context, resolveInfo) => {
|
||||
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||
markAsRead: async (parent, args, context, resolveInfo) => {
|
||||
const { user: currentUser } = context
|
||||
const session = context.driver.session()
|
||||
let notification
|
||||
try {
|
||||
const cypher = `
|
||||
MATCH (resource {id: $resourceId})-[notification:NOTIFIED {read: FALSE}]->(user:User {id:$id})
|
||||
SET notification.read = TRUE
|
||||
RETURN resource, notification, user
|
||||
`
|
||||
const result = await session.run(cypher, { resourceId: args.id, id: currentUser.id })
|
||||
const notifications = await result.records.map(transformReturnType)
|
||||
notification = notifications[0]
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return notification
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,397 +1,309 @@
|
||||
import { GraphQLClient } from 'graphql-request'
|
||||
import Factory from '../../seed/factories'
|
||||
import { host, login, gql } from '../../jest/helpers'
|
||||
import { neode } from '../../bootstrap/neo4j'
|
||||
import { gql } from '../../jest/helpers'
|
||||
import { neode as getNeode, getDriver } from '../../bootstrap/neo4j'
|
||||
import { createTestClient } from 'apollo-server-testing'
|
||||
import createServer from '../.././server'
|
||||
|
||||
let client
|
||||
const factory = Factory()
|
||||
const instance = neode()
|
||||
const neode = getNeode()
|
||||
const driver = getDriver()
|
||||
const userParams = {
|
||||
id: 'you',
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
}
|
||||
const categoryIds = ['cat9']
|
||||
|
||||
let authenticatedUser
|
||||
let user
|
||||
let variables
|
||||
let query
|
||||
let mutate
|
||||
|
||||
beforeAll(() => {
|
||||
const { server } = createServer({
|
||||
context: () => {
|
||||
return {
|
||||
driver,
|
||||
user: authenticatedUser,
|
||||
}
|
||||
},
|
||||
})
|
||||
query = createTestClient(server).query
|
||||
mutate = createTestClient(server).mutate
|
||||
})
|
||||
|
||||
beforeEach(async () => {
|
||||
await factory.create('User', userParams)
|
||||
await instance.create('Category', {
|
||||
id: 'cat9',
|
||||
name: 'Democracy & Politics',
|
||||
icon: 'university',
|
||||
})
|
||||
authenticatedUser = null
|
||||
variables = { orderBy: 'createdAt_asc' }
|
||||
})
|
||||
|
||||
afterEach(async () => {
|
||||
await factory.cleanDatabase()
|
||||
})
|
||||
|
||||
describe('Notification', () => {
|
||||
const notificationQuery = gql`
|
||||
query {
|
||||
Notification {
|
||||
id
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(client.request(notificationQuery)).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
describe('given some notifications', () => {
|
||||
beforeEach(async () => {
|
||||
user = await factory.create('User', userParams)
|
||||
await factory.create('User', { id: 'neighbor' })
|
||||
await Promise.all(setupNotifications.map(s => neode.cypher(s)))
|
||||
})
|
||||
})
|
||||
const setupNotifications = [
|
||||
`MATCH(user:User {id: 'neighbor'})
|
||||
MERGE (:Post {id: 'p1', content: 'Not for you'})
|
||||
-[:NOTIFIED {createdAt: "2019-08-29T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
|
||||
->(user);
|
||||
`,
|
||||
`MATCH(user:User {id: 'you'})
|
||||
MERGE (:Post {id: 'p2', content: 'Already seen post mentioning'})
|
||||
-[:NOTIFIED {createdAt: "2019-08-30T17:33:48.651Z", read: true, reason: "mentioned_in_post"}]
|
||||
->(user);
|
||||
`,
|
||||
`MATCH(user:User {id: 'you'})
|
||||
MERGE (:Post {id: 'p3', content: 'You have been mentioned in a post'})
|
||||
-[:NOTIFIED {createdAt: "2019-08-31T17:33:48.651Z", read: false, reason: "mentioned_in_post"}]
|
||||
->(user);
|
||||
`,
|
||||
`MATCH(user:User {id: 'you'})
|
||||
MATCH(post:Post {id: 'p3'})
|
||||
CREATE (comment:Comment {id: 'c1', content: 'You have seen this comment mentioning already'})
|
||||
MERGE (comment)-[:COMMENTS]->(post)
|
||||
MERGE (comment)
|
||||
-[:NOTIFIED {createdAt: "2019-08-30T15:33:48.651Z", read: true, reason: "mentioned_in_comment"}]
|
||||
->(user);
|
||||
`,
|
||||
`MATCH(user:User {id: 'you'})
|
||||
MATCH(post:Post {id: 'p3'})
|
||||
CREATE (comment:Comment {id: 'c2', content: 'You have been mentioned in a comment'})
|
||||
MERGE (comment)-[:COMMENTS]->(post)
|
||||
MERGE (comment)
|
||||
-[:NOTIFIED {createdAt: "2019-08-30T19:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
|
||||
->(user);
|
||||
`,
|
||||
`MATCH(user:User {id: 'neighbor'})
|
||||
MATCH(post:Post {id: 'p3'})
|
||||
CREATE (comment:Comment {id: 'c3', content: 'Somebody else was mentioned in a comment'})
|
||||
MERGE (comment)-[:COMMENTS]->(post)
|
||||
MERGE (comment)
|
||||
-[:NOTIFIED {createdAt: "2019-09-01T17:33:48.651Z", read: false, reason: "mentioned_in_comment"}]
|
||||
->(user);
|
||||
`,
|
||||
]
|
||||
|
||||
describe('currentUser notifications', () => {
|
||||
const variables = {}
|
||||
|
||||
describe('authenticated', () => {
|
||||
let headers
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
describe('given some notifications', () => {
|
||||
beforeEach(async () => {
|
||||
const neighborParams = {
|
||||
email: 'neighbor@example.org',
|
||||
password: '1234',
|
||||
id: 'neighbor',
|
||||
describe('notifications', () => {
|
||||
const notificationQuery = gql`
|
||||
query($read: Boolean, $orderBy: NotificationOrdering) {
|
||||
notifications(read: $read, orderBy: $orderBy) {
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
content
|
||||
}
|
||||
... on Comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
read
|
||||
createdAt
|
||||
}
|
||||
await Promise.all([
|
||||
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)
|
||||
await factory.create('Post', { id: 'p1', categoryIds })
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-not-for-you',
|
||||
to: 'neighbor',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-not-for-you',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-unseen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-unseen',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-already-seen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-already-seen',
|
||||
}),
|
||||
])
|
||||
// Comment and its notifications
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-not-for-you',
|
||||
to: 'neighbor',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-not-for-you',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-unseen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-unseen',
|
||||
}),
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-already-seen',
|
||||
to: 'you',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'c1',
|
||||
to: 'comment-mention-already-seen',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
describe('filter for read: false', () => {
|
||||
const queryCurrentUserNotificationsFilterRead = gql`
|
||||
query($read: Boolean) {
|
||||
currentUser {
|
||||
notifications(read: $read, orderBy: createdAt_desc) {
|
||||
id
|
||||
post {
|
||||
id
|
||||
}
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = { read: false }
|
||||
it('returns only unread notifications of current user', async () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: expect.arrayContaining([
|
||||
{
|
||||
id: 'post-mention-unseen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-unseen',
|
||||
post: null,
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(queryCurrentUserNotificationsFilterRead, variables),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
describe('no filters', () => {
|
||||
const queryCurrentUserNotifications = gql`
|
||||
query {
|
||||
currentUser {
|
||||
notifications(orderBy: createdAt_desc) {
|
||||
id
|
||||
post {
|
||||
id
|
||||
}
|
||||
comment {
|
||||
id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
it('returns all notifications of current user', async () => {
|
||||
const expected = {
|
||||
currentUser: {
|
||||
notifications: expect.arrayContaining([
|
||||
{
|
||||
id: 'post-mention-unseen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'post-mention-already-seen',
|
||||
post: {
|
||||
id: 'p1',
|
||||
},
|
||||
comment: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-unseen',
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
post: null,
|
||||
},
|
||||
{
|
||||
id: 'comment-mention-already-seen',
|
||||
comment: {
|
||||
id: 'c1',
|
||||
},
|
||||
post: null,
|
||||
},
|
||||
]),
|
||||
},
|
||||
}
|
||||
await expect(client.request(queryCurrentUserNotifications, variables)).resolves.toEqual(
|
||||
expected,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('UpdateNotification', () => {
|
||||
const mutationUpdateNotification = gql`
|
||||
mutation($id: ID!, $read: Boolean) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`
|
||||
const variablesPostUpdateNotification = {
|
||||
id: 'post-mention-to-be-updated',
|
||||
read: true,
|
||||
}
|
||||
const variablesCommentUpdateNotification = {
|
||||
id: 'comment-mention-to-be-updated',
|
||||
read: true,
|
||||
}
|
||||
|
||||
describe('given some notifications', () => {
|
||||
let headers
|
||||
|
||||
beforeEach(async () => {
|
||||
const mentionedParams = {
|
||||
id: 'mentioned-1',
|
||||
email: 'mentioned@example.org',
|
||||
password: '1234',
|
||||
slug: 'mentioned',
|
||||
}
|
||||
await Promise.all([
|
||||
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)
|
||||
await factory.create('Post', { id: 'p1', categoryIds })
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'post-mention-to-be-updated',
|
||||
to: 'mentioned-1',
|
||||
}),
|
||||
factory.relate('Notification', 'Post', {
|
||||
from: 'p1',
|
||||
to: 'post-mention-to-be-updated',
|
||||
}),
|
||||
])
|
||||
// Comment and its notifications
|
||||
await Promise.all([
|
||||
factory.create('Comment', {
|
||||
id: 'c1',
|
||||
postId: 'p1',
|
||||
}),
|
||||
])
|
||||
await Promise.all([
|
||||
factory.relate('Notification', 'User', {
|
||||
from: 'comment-mention-to-be-updated',
|
||||
to: 'mentioned-1',
|
||||
}),
|
||||
factory.relate('Notification', 'Comment', {
|
||||
from: 'p1',
|
||||
to: 'comment-mention-to-be-updated',
|
||||
}),
|
||||
])
|
||||
})
|
||||
|
||||
`
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
client = new GraphQLClient(host)
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
const result = await query({ query: notificationQuery })
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'test@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('no filters', () => {
|
||||
it('returns all notifications of current user', async () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have seen this comment mentioning already',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T15:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'Already seen post mentioning',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T17:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
})
|
||||
await expect(query({ query: notificationQuery, variables })).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
|
||||
it('throws authorization error', async () => {
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).rejects.toThrow('Not Authorised')
|
||||
})
|
||||
|
||||
describe('and owner', () => {
|
||||
beforeEach(async () => {
|
||||
headers = await login({
|
||||
email: 'mentioned@example.org',
|
||||
password: '1234',
|
||||
})
|
||||
client = new GraphQLClient(host, {
|
||||
headers,
|
||||
})
|
||||
})
|
||||
|
||||
it('updates post notification', async () => {
|
||||
const expected = {
|
||||
UpdateNotification: {
|
||||
id: 'post-mention-to-be-updated',
|
||||
read: true,
|
||||
describe('filter for read: false', () => {
|
||||
it('returns only unread notifications of current user', async () => {
|
||||
const expected = expect.objectContaining({
|
||||
data: {
|
||||
notifications: [
|
||||
{
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
},
|
||||
{
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
},
|
||||
read: false,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
},
|
||||
],
|
||||
},
|
||||
}
|
||||
})
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesPostUpdateNotification),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
|
||||
it('updates comment notification', async () => {
|
||||
const expected = {
|
||||
UpdateNotification: {
|
||||
id: 'comment-mention-to-be-updated',
|
||||
read: true,
|
||||
},
|
||||
}
|
||||
await expect(
|
||||
client.request(mutationUpdateNotification, variablesCommentUpdateNotification),
|
||||
query({ query: notificationQuery, variables: { ...variables, read: false } }),
|
||||
).resolves.toEqual(expected)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('markAsRead', () => {
|
||||
const markAsReadMutation = gql`
|
||||
mutation($id: ID!) {
|
||||
markAsRead(id: $id) {
|
||||
from {
|
||||
__typename
|
||||
... on Post {
|
||||
content
|
||||
}
|
||||
... on Comment {
|
||||
content
|
||||
}
|
||||
}
|
||||
read
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
`
|
||||
describe('unauthenticated', () => {
|
||||
it('throws authorization error', async () => {
|
||||
const result = await mutate({
|
||||
mutation: markAsReadMutation,
|
||||
variables: { ...variables, id: 'p1' },
|
||||
})
|
||||
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||
})
|
||||
})
|
||||
|
||||
describe('authenticated', () => {
|
||||
beforeEach(async () => {
|
||||
authenticatedUser = await user.toJson()
|
||||
})
|
||||
|
||||
describe('not being notified at all', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'p1',
|
||||
}
|
||||
})
|
||||
|
||||
it('returns null', async () => {
|
||||
const response = await mutate({ mutation: markAsReadMutation, variables })
|
||||
expect(response.data.markAsRead).toEqual(null)
|
||||
expect(response.errors).toBeUndefined()
|
||||
})
|
||||
})
|
||||
|
||||
describe('being notified', () => {
|
||||
describe('on a post', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'p3',
|
||||
}
|
||||
})
|
||||
|
||||
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
||||
expect(data).toEqual({
|
||||
markAsRead: {
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
content: 'You have been mentioned in a post',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-31T17:33:48.651Z',
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
describe('but notification was already marked as read', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'p2',
|
||||
}
|
||||
})
|
||||
it('returns null', async () => {
|
||||
const response = await mutate({ mutation: markAsReadMutation, variables })
|
||||
expect(response.data.markAsRead).toEqual(null)
|
||||
expect(response.errors).toBeUndefined()
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('on a comment', () => {
|
||||
beforeEach(async () => {
|
||||
variables = {
|
||||
...variables,
|
||||
id: 'c2',
|
||||
}
|
||||
})
|
||||
|
||||
it('updates `read` attribute and returns NOTIFIED relationship', async () => {
|
||||
const { data } = await mutate({ mutation: markAsReadMutation, variables })
|
||||
expect(data).toEqual({
|
||||
markAsRead: {
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
content: 'You have been mentioned in a comment',
|
||||
},
|
||||
read: true,
|
||||
createdAt: '2019-08-30T19:33:48.651Z',
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -3,6 +3,7 @@ import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||
import fileUpload from './fileUpload'
|
||||
import { getBlockedUsers, getBlockedByUsers } from './users.js'
|
||||
import { mergeWith, isArray } from 'lodash'
|
||||
import Resolver from './helpers/Resolver'
|
||||
|
||||
const filterForBlockedUsers = async (params, context) => {
|
||||
if (!context.user) return params
|
||||
@ -181,4 +182,46 @@ export default {
|
||||
return emoted
|
||||
},
|
||||
},
|
||||
Post: {
|
||||
...Resolver('Post', {
|
||||
hasMany: {
|
||||
tags: '-[:TAGGED]->(related:Tag)',
|
||||
categories: '-[:CATEGORIZED]->(related:Category)',
|
||||
comments: '<-[:COMMENTS]-(related:Comment)',
|
||||
shoutedBy: '<-[:SHOUTED]-(related:User)',
|
||||
emotions: '<-[related:EMOTED]',
|
||||
},
|
||||
hasOne: {
|
||||
author: '<-[:WROTE]-(related:User)',
|
||||
disabledBy: '<-[:DISABLED]-(related:User)',
|
||||
},
|
||||
count: {
|
||||
shoutedCount:
|
||||
'<-[:SHOUTED]-(related:User) WHERE NOT related.deleted = true AND NOT related.disabled = true',
|
||||
emotionsCount: '<-[related:EMOTED]-(:User)',
|
||||
},
|
||||
boolean: {
|
||||
shoutedByCurrentUser:
|
||||
'<-[:SHOUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||
},
|
||||
}),
|
||||
relatedContributions: async (parent, params, context, resolveInfo) => {
|
||||
if (typeof parent.relatedContributions !== 'undefined') return parent.relatedContributions
|
||||
const { id } = parent
|
||||
const statement = `
|
||||
MATCH (p:Post {id: $id})-[:TAGGED|CATEGORIZED]->(categoryOrTag)<-[:TAGGED|CATEGORIZED]-(post:Post)
|
||||
RETURN DISTINCT post
|
||||
LIMIT 10
|
||||
`
|
||||
let relatedContributions
|
||||
const session = context.driver.session()
|
||||
try {
|
||||
const result = await session.run(statement, { id })
|
||||
relatedContributions = result.records.map(r => r.get('post').properties)
|
||||
} finally {
|
||||
session.close()
|
||||
}
|
||||
return relatedContributions
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
enum ReasonNotification {
|
||||
mentioned_in_post
|
||||
mentioned_in_comment
|
||||
comment_on_post
|
||||
}
|
||||
commented_on_post
|
||||
}
|
||||
|
||||
28
backend/src/schema/types/type/NOTIFIED.gql
Normal file
28
backend/src/schema/types/type/NOTIFIED.gql
Normal file
@ -0,0 +1,28 @@
|
||||
type NOTIFIED {
|
||||
from: NotificationSource
|
||||
to: User
|
||||
createdAt: String
|
||||
read: Boolean
|
||||
reason: NotificationReason
|
||||
}
|
||||
|
||||
union NotificationSource = Post | Comment
|
||||
|
||||
enum NotificationOrdering {
|
||||
createdAt_asc
|
||||
createdAt_desc
|
||||
}
|
||||
|
||||
enum NotificationReason {
|
||||
mentioned_in_post
|
||||
mentioned_in_comment
|
||||
commented_on_post
|
||||
}
|
||||
|
||||
type Query {
|
||||
notifications(read: Boolean, orderBy: NotificationOrdering): [NOTIFIED]
|
||||
}
|
||||
|
||||
type Mutation {
|
||||
markAsRead(id: ID!): NOTIFIED
|
||||
}
|
||||
@ -1,9 +0,0 @@
|
||||
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")
|
||||
}
|
||||
@ -24,8 +24,6 @@ type User {
|
||||
createdAt: String
|
||||
updatedAt: String
|
||||
|
||||
notifications(read: Boolean): [Notification]! @relation(name: "NOTIFIED", direction: "IN")
|
||||
|
||||
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
|
||||
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
|
||||
|
||||
|
||||
@ -8,7 +8,6 @@ import createComment from './comments.js'
|
||||
import createCategory from './categories.js'
|
||||
import createTag from './tags.js'
|
||||
import createReport from './reports.js'
|
||||
import createNotification from './notifications.js'
|
||||
|
||||
export const seedServerHost = 'http://127.0.0.1:4001'
|
||||
|
||||
@ -31,7 +30,6 @@ const factories = {
|
||||
Category: createCategory,
|
||||
Tag: createTag,
|
||||
Report: createReport,
|
||||
Notification: createNotification,
|
||||
}
|
||||
|
||||
export const cleanDatabase = async (options = {}) => {
|
||||
|
||||
@ -1,17 +0,0 @@
|
||||
import uuid from 'uuid/v4'
|
||||
|
||||
export default function(params) {
|
||||
const { id = uuid(), read = false } = params
|
||||
|
||||
return {
|
||||
mutation: `
|
||||
mutation($id: ID, $read: Boolean) {
|
||||
CreateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`,
|
||||
variables: { id, read },
|
||||
}
|
||||
}
|
||||
@ -270,7 +270,7 @@ import Factory from './factories'
|
||||
const hashtag1 =
|
||||
'See <a class="hashtag" href="/search/hashtag/NaturphilosophieYoga">#NaturphilosophieYoga</a> can really help you!'
|
||||
const hashtagAndMention1 =
|
||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u3" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
'The new physics of <a class="hashtag" href="/search/hashtag/QuantenFlussTheorie">#QuantenFlussTheorie</a> can explain <a class="hashtag" href="/search/hashtag/QuantumGravity">#QuantumGravity</a>! <a class="mention" data-mention-id="u1" href="/profile/u1">@peter-lustig</a> got that already. ;-)'
|
||||
|
||||
await Promise.all([
|
||||
asAdmin.create('Post', {
|
||||
|
||||
@ -357,7 +357,7 @@ When("mention {string} in the text", mention => {
|
||||
});
|
||||
|
||||
Then("the notification gets marked as read", () => {
|
||||
cy.get(".post.createdAt")
|
||||
cy.get(".notifications-menu-popover .notification")
|
||||
.first()
|
||||
.should("have.class", "read");
|
||||
});
|
||||
|
||||
@ -37,9 +37,9 @@ describe('Notification', () => {
|
||||
describe('given a notification about a comment on a post', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
reason: 'comment_on_post',
|
||||
post: null,
|
||||
comment: {
|
||||
reason: 'commented_on_post',
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
id: 'comment-1',
|
||||
contentExcerpt:
|
||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||
@ -56,7 +56,7 @@ describe('Notification', () => {
|
||||
it('renders reason', () => {
|
||||
wrapper = Wrapper()
|
||||
expect(wrapper.find('.reason-text-for-test').text()).toEqual(
|
||||
'notifications.menu.comment_on_post',
|
||||
'notifications.menu.commented_on_post',
|
||||
)
|
||||
})
|
||||
it('renders title', () => {
|
||||
@ -92,14 +92,14 @@ describe('Notification', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
reason: 'mentioned_in_post',
|
||||
post: {
|
||||
from: {
|
||||
__typename: '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,
|
||||
}
|
||||
})
|
||||
|
||||
@ -138,8 +138,8 @@ describe('Notification', () => {
|
||||
beforeEach(() => {
|
||||
propsData.notification = {
|
||||
reason: 'mentioned_in_comment',
|
||||
post: null,
|
||||
comment: {
|
||||
from: {
|
||||
__typename: 'Comment',
|
||||
id: 'comment-1',
|
||||
contentExcerpt:
|
||||
'<a href="/profile/u123" target="_blank">@dagobert-duck</a> is the best on this comment.',
|
||||
|
||||
@ -1,14 +1,8 @@
|
||||
<template>
|
||||
<ds-space :class="[{ read: notification.read }, notification]" margin-bottom="x-small">
|
||||
<ds-space :class="{ read: notification.read, notification: true }" margin-bottom="x-small">
|
||||
<client-only>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<hc-user
|
||||
v-if="resourceType == 'Post'"
|
||||
:user="post.author"
|
||||
:date-time="post.createdAt"
|
||||
:trunc="35"
|
||||
/>
|
||||
<hc-user v-else :user="comment.author" :date-time="comment.createdAt" :trunc="35" />
|
||||
<hc-user :user="from.author" :date-time="from.createdAt" :trunc="35" />
|
||||
</ds-space>
|
||||
<ds-text class="reason-text-for-test" color="soft">
|
||||
{{ $t(`notifications.menu.${notification.reason}`) }}
|
||||
@ -22,16 +16,15 @@
|
||||
>
|
||||
<ds-space margin-bottom="x-small">
|
||||
<ds-card
|
||||
:header="post.title || comment.post.title"
|
||||
:header="from.title || from.post.title"
|
||||
hover
|
||||
space="x-small"
|
||||
class="notifications-card"
|
||||
>
|
||||
<ds-space margin-bottom="x-small" />
|
||||
<div v-if="resourceType == 'Post'">{{ post.contentExcerpt | removeHtml }}</div>
|
||||
<div v-else>
|
||||
<span class="comment-notification-header">Comment:</span>
|
||||
{{ comment.contentExcerpt | removeHtml }}
|
||||
<div>
|
||||
<span v-if="isComment" class="comment-notification-header">Comment:</span>
|
||||
{{ from.contentExcerpt | removeHtml }}
|
||||
</div>
|
||||
</ds-card>
|
||||
</ds-space>
|
||||
@ -54,23 +47,21 @@ export default {
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
resourceType() {
|
||||
return this.post.id ? 'Post' : 'Comment'
|
||||
from() {
|
||||
return this.notification.from
|
||||
},
|
||||
post() {
|
||||
return this.notification.post || {}
|
||||
},
|
||||
comment() {
|
||||
return this.notification.comment || {}
|
||||
isComment() {
|
||||
return this.from.__typename === 'Comment'
|
||||
},
|
||||
params() {
|
||||
const post = this.isComment ? this.from.post : this.from
|
||||
return {
|
||||
id: this.post.id || this.comment.post.id,
|
||||
slug: this.post.slug || this.comment.post.slug,
|
||||
id: post.id,
|
||||
slug: post.slug,
|
||||
}
|
||||
},
|
||||
hashParam() {
|
||||
return this.post.id ? {} : { hash: `#commentId-${this.comment.id}` }
|
||||
return this.isComment ? { hash: `#commentId-${this.from.id}` } : {}
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -40,9 +40,9 @@ describe('NotificationList.vue', () => {
|
||||
propsData = {
|
||||
notifications: [
|
||||
{
|
||||
id: 'notification-41',
|
||||
read: false,
|
||||
post: {
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'post-1',
|
||||
title: 'some post title',
|
||||
slug: 'some-post-title',
|
||||
@ -55,9 +55,9 @@ describe('NotificationList.vue', () => {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'notification-42',
|
||||
read: false,
|
||||
post: {
|
||||
from: {
|
||||
__typename: 'Post',
|
||||
id: 'post-2',
|
||||
title: 'another post title',
|
||||
slug: 'another-post-title',
|
||||
@ -115,9 +115,9 @@ describe('NotificationList.vue', () => {
|
||||
.trigger('click')
|
||||
})
|
||||
|
||||
it("emits 'markAsRead' with the notificationId", () => {
|
||||
it("emits 'markAsRead' with the id of the notification source", () => {
|
||||
expect(wrapper.emitted('markAsRead')).toBeTruthy()
|
||||
expect(wrapper.emitted('markAsRead')[0]).toEqual(['notification-42'])
|
||||
expect(wrapper.emitted('markAsRead')[0]).toEqual(['post-2'])
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@ -4,7 +4,7 @@
|
||||
v-for="notification in notifications"
|
||||
:key="notification.id"
|
||||
:notification="notification"
|
||||
@read="markAsRead(notification.id)"
|
||||
@read="markAsRead(notification.from.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
@ -24,8 +24,8 @@ export default {
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
markAsRead(notificationId) {
|
||||
this.$emit('markAsRead', notificationId)
|
||||
markAsRead(notificationSourceId) {
|
||||
this.$emit('markAsRead', notificationSourceId)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -18,7 +18,7 @@
|
||||
|
||||
<script>
|
||||
import Dropdown from '~/components/Dropdown'
|
||||
import { currentUserNotificationsQuery, updateNotificationMutation } from '~/graphql/User'
|
||||
import { notificationQuery, markAsReadMutation } from '~/graphql/User'
|
||||
import NotificationList from '../NotificationList/NotificationList'
|
||||
|
||||
export default {
|
||||
@ -27,36 +27,41 @@ export default {
|
||||
NotificationList,
|
||||
Dropdown,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
notifications: [],
|
||||
}
|
||||
},
|
||||
props: {
|
||||
placement: { type: String },
|
||||
},
|
||||
computed: {
|
||||
totalNotifications() {
|
||||
return (this.notifications || []).length
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
async markAsRead(notificationId) {
|
||||
const variables = { id: notificationId, read: true }
|
||||
async markAsRead(notificationSourceId) {
|
||||
const variables = { id: notificationSourceId }
|
||||
try {
|
||||
await this.$apollo.mutate({
|
||||
mutation: updateNotificationMutation(),
|
||||
const {
|
||||
data: { markAsRead },
|
||||
} = await this.$apollo.mutate({
|
||||
mutation: markAsReadMutation(),
|
||||
variables,
|
||||
})
|
||||
if (!(markAsRead && markAsRead.read === true)) return
|
||||
this.notifications = this.notifications.map(n => {
|
||||
return n.from.id === markAsRead.from.id ? markAsRead : n
|
||||
})
|
||||
} catch (err) {
|
||||
throw new Error(err)
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
totalNotifications() {
|
||||
return this.notifications.length
|
||||
},
|
||||
},
|
||||
apollo: {
|
||||
notifications: {
|
||||
query: currentUserNotificationsQuery(),
|
||||
update: data => {
|
||||
const {
|
||||
currentUser: { notifications },
|
||||
} = data
|
||||
return notifications
|
||||
},
|
||||
query: notificationQuery(),
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -1,5 +1,58 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
const fragments = gql`
|
||||
fragment post on Post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
|
||||
fragment comment on Comment {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
contentExcerpt
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
export default i18n => {
|
||||
const lang = i18n.locale().toUpperCase()
|
||||
return gql`
|
||||
@ -76,77 +129,37 @@ export default i18n => {
|
||||
`
|
||||
}
|
||||
|
||||
export const currentUserNotificationsQuery = () => {
|
||||
export const notificationQuery = () => {
|
||||
return gql`
|
||||
${fragments}
|
||||
query {
|
||||
currentUser {
|
||||
id
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
comment {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
contentExcerpt
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
post {
|
||||
id
|
||||
createdAt
|
||||
disabled
|
||||
deleted
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
avatar
|
||||
}
|
||||
}
|
||||
}
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
from {
|
||||
__typename
|
||||
...post
|
||||
...comment
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
}
|
||||
|
||||
export const updateNotificationMutation = () => {
|
||||
export const markAsReadMutation = () => {
|
||||
return gql`
|
||||
mutation($id: ID!, $read: Boolean!) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
${fragments}
|
||||
mutation($id: ID!) {
|
||||
markAsRead(id: $id) {
|
||||
read
|
||||
reason
|
||||
createdAt
|
||||
from {
|
||||
__typename
|
||||
...post
|
||||
...comment
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
@ -134,7 +134,7 @@
|
||||
"menu": {
|
||||
"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 …"
|
||||
"commented_on_post": "Hat deinen Beitrag kommentiert …"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -134,7 +134,7 @@
|
||||
"menu": {
|
||||
"mentioned_in_post": "Mentioned you in a post …",
|
||||
"mentioned_in_comment": "Mentioned you in a comment …",
|
||||
"comment_on_post": "Commented on your post …"
|
||||
"commented_on_post": "Commented on your post …"
|
||||
}
|
||||
},
|
||||
"search": {
|
||||
|
||||
@ -1,3 +1,10 @@
|
||||
import { InMemoryCache, IntrospectionFragmentMatcher } from 'apollo-cache-inmemory'
|
||||
import introspectionQueryResultData from './apollo-config/fragmentTypes.json'
|
||||
|
||||
const fragmentMatcher = new IntrospectionFragmentMatcher({
|
||||
introspectionQueryResultData,
|
||||
})
|
||||
|
||||
export default ({ app }) => {
|
||||
const backendUrl = process.env.GRAPHQL_URI || 'http://localhost:4000'
|
||||
|
||||
@ -10,5 +17,6 @@ export default ({ app }) => {
|
||||
tokenName: 'human-connection-token',
|
||||
persisting: false,
|
||||
websocketsOnly: false,
|
||||
cache: new InMemoryCache({ fragmentMatcher }),
|
||||
}
|
||||
}
|
||||
|
||||
18
webapp/plugins/apollo-config/fragmentTypes.json
Normal file
18
webapp/plugins/apollo-config/fragmentTypes.json
Normal file
@ -0,0 +1,18 @@
|
||||
{
|
||||
"__schema": {
|
||||
"types": [
|
||||
{
|
||||
"kind": "UNION",
|
||||
"name": "NotificationSource",
|
||||
"possibleTypes": [
|
||||
{
|
||||
"name": "Post"
|
||||
},
|
||||
{
|
||||
"name": "Comment"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
@ -86,23 +86,6 @@ export const actions = {
|
||||
id
|
||||
url
|
||||
}
|
||||
notifications(read: false, orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@ -1,99 +0,0 @@
|
||||
import gql from 'graphql-tag'
|
||||
|
||||
export const state = () => {
|
||||
return {
|
||||
notifications: null,
|
||||
pending: false,
|
||||
}
|
||||
}
|
||||
|
||||
export const mutations = {
|
||||
SET_NOTIFICATIONS(state, notifications) {
|
||||
state.notifications = notifications
|
||||
},
|
||||
SET_PENDING(state, pending) {
|
||||
state.pending = pending
|
||||
},
|
||||
UPDATE_NOTIFICATIONS(state, notification) {
|
||||
const notifications = state.notifications
|
||||
const toBeUpdated = notifications.find(n => {
|
||||
return n.id === notification.id
|
||||
})
|
||||
state.notifications = {
|
||||
...toBeUpdated,
|
||||
...notification,
|
||||
}
|
||||
},
|
||||
}
|
||||
export const getters = {
|
||||
notifications(state) {
|
||||
return !!state.notifications
|
||||
},
|
||||
}
|
||||
|
||||
export const actions = {
|
||||
async init({ getters, commit }) {
|
||||
if (getters.notifications) return
|
||||
commit('SET_PENDING', true)
|
||||
const client = this.app.apolloProvider.defaultClient
|
||||
let notifications
|
||||
try {
|
||||
const {
|
||||
data: { currentUser },
|
||||
} = await client.query({
|
||||
query: gql`
|
||||
{
|
||||
currentUser {
|
||||
id
|
||||
notifications(orderBy: createdAt_desc) {
|
||||
id
|
||||
read
|
||||
createdAt
|
||||
post {
|
||||
author {
|
||||
id
|
||||
slug
|
||||
name
|
||||
disabled
|
||||
deleted
|
||||
}
|
||||
title
|
||||
contentExcerpt
|
||||
slug
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`,
|
||||
})
|
||||
notifications = currentUser.notifications
|
||||
commit('SET_NOTIFICATIONS', notifications)
|
||||
} finally {
|
||||
commit('SET_PENDING', false)
|
||||
}
|
||||
return notifications
|
||||
},
|
||||
|
||||
async markAsRead({ commit, rootGetters }, notificationId) {
|
||||
const client = this.app.apolloProvider.defaultClient
|
||||
const mutation = gql`
|
||||
mutation($id: ID!, $read: Boolean!) {
|
||||
UpdateNotification(id: $id, read: $read) {
|
||||
id
|
||||
read
|
||||
}
|
||||
}
|
||||
`
|
||||
const variables = {
|
||||
id: notificationId,
|
||||
read: true,
|
||||
}
|
||||
const {
|
||||
data: { UpdateNotification },
|
||||
} = await client.mutate({
|
||||
mutation,
|
||||
variables,
|
||||
})
|
||||
commit('UPDATE_NOTIFICATIONS', UpdateNotification)
|
||||
},
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user