mirror of
https://github.com/Ocelot-Social-Community/Ocelot-Social.git
synced 2025-12-13 07:46:06 +00:00
Merge branch 'master' of github.com:Human-Connection/Human-Connection into 1017-send-out-notifications-on-create-omment
This commit is contained in:
commit
569ace137b
@ -89,7 +89,7 @@
|
|||||||
"metascraper-youtube": "^5.6.3",
|
"metascraper-youtube": "^5.6.3",
|
||||||
"minimatch": "^3.0.4",
|
"minimatch": "^3.0.4",
|
||||||
"neo4j-driver": "~1.7.5",
|
"neo4j-driver": "~1.7.5",
|
||||||
"neo4j-graphql-js": "^2.6.3",
|
"neo4j-graphql-js": "^2.7.0",
|
||||||
"neode": "^0.3.1",
|
"neode": "^0.3.1",
|
||||||
"node-fetch": "~2.6.0",
|
"node-fetch": "~2.6.0",
|
||||||
"nodemailer": "^6.3.0",
|
"nodemailer": "^6.3.0",
|
||||||
|
|||||||
@ -7,11 +7,14 @@ const notifyMentions = async (label, id, idsOfMentionedUsers, context) => {
|
|||||||
const session = context.driver.session()
|
const session = context.driver.session()
|
||||||
const createdAt = new Date().toISOString()
|
const createdAt = new Date().toISOString()
|
||||||
const cypher = `
|
const cypher = `
|
||||||
MATCH (u: User) WHERE u.id in $idsOfMentionedUsers
|
MATCH (source)
|
||||||
MATCH (source) WHERE source.id = $id AND $label IN LABELS(source)
|
WHERE source.id = $id AND $label IN LABELS(source)
|
||||||
CREATE (n: Notification { id: apoc.create.uuid(), read: false, createdAt: $createdAt })
|
MATCH(source)<-[:WROTE]-(author:User)
|
||||||
MERGE (n)-[:NOTIFIED]->(u)
|
MATCH(u:User)
|
||||||
MERGE (source)-[:NOTIFIED]->(n)
|
WHERE u.id in $idsOfMentionedUsers
|
||||||
|
AND NOT (u)<-[:BLOCKED]-(author)
|
||||||
|
CREATE(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
|
||||||
|
MERGE (source)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u)
|
||||||
`
|
`
|
||||||
await session.run(cypher, {
|
await session.run(cypher, {
|
||||||
idsOfMentionedUsers,
|
idsOfMentionedUsers,
|
||||||
|
|||||||
@ -1,12 +1,36 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
import { gql } from '../../jest/helpers'
|
||||||
import { host, login, gql } from '../../jest/helpers'
|
|
||||||
import Factory from '../../seed/factories'
|
import Factory from '../../seed/factories'
|
||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import { neode, getDriver } from '../../bootstrap/neo4j'
|
||||||
|
import createServer from '../../server'
|
||||||
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
let client
|
const driver = getDriver()
|
||||||
|
const instance = neode()
|
||||||
|
let server
|
||||||
|
let query
|
||||||
|
let mutate
|
||||||
|
let user
|
||||||
|
let authenticatedUser
|
||||||
|
|
||||||
|
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 () => {
|
beforeEach(async () => {
|
||||||
await factory.create('User', {
|
user = await instance.create('User', {
|
||||||
id: 'you',
|
id: 'you',
|
||||||
name: 'Al Capone',
|
name: 'Al Capone',
|
||||||
slug: 'al-capone',
|
slug: 'al-capone',
|
||||||
@ -19,8 +43,8 @@ afterEach(async () => {
|
|||||||
await factory.cleanDatabase()
|
await factory.cleanDatabase()
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('currentUser { notifications }', () => {
|
describe('notifications', () => {
|
||||||
const query = gql`
|
const notificationQuery = gql`
|
||||||
query($read: Boolean) {
|
query($read: Boolean) {
|
||||||
currentUser {
|
currentUser {
|
||||||
notifications(read: $read, orderBy: createdAt_desc) {
|
notifications(read: $read, orderBy: createdAt_desc) {
|
||||||
@ -34,82 +58,60 @@ describe('currentUser { notifications }', () => {
|
|||||||
`
|
`
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
let headers
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
authenticatedUser = user
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('given another user', () => {
|
describe('given another user', () => {
|
||||||
let authorClient
|
let author
|
||||||
let authorParams
|
|
||||||
let authorHeaders
|
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
authorParams = {
|
author = await instance.create('User', {
|
||||||
email: 'author@example.org',
|
email: 'author@example.org',
|
||||||
password: '1234',
|
password: '1234',
|
||||||
id: 'author',
|
id: 'author',
|
||||||
}
|
})
|
||||||
await factory.create('User', authorParams)
|
|
||||||
authorHeaders = await login(authorParams)
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('who mentions me in a post', () => {
|
describe('who mentions me in a post', () => {
|
||||||
let post
|
|
||||||
const title = 'Mentioning Al Capone'
|
const title = 'Mentioning Al Capone'
|
||||||
const content =
|
const content =
|
||||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
|
||||||
|
|
||||||
beforeEach(async () => {
|
const createPostAction = async () => {
|
||||||
const createPostMutation = gql`
|
const createPostMutation = gql`
|
||||||
mutation($title: String!, $content: String!) {
|
mutation($id: ID, $title: String!, $content: String!) {
|
||||||
CreatePost(title: $title, content: $content) {
|
CreatePost(id: $id, title: $title, content: $content) {
|
||||||
id
|
id
|
||||||
title
|
title
|
||||||
content
|
content
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
authorClient = new GraphQLClient(host, {
|
authenticatedUser = await author.toJson()
|
||||||
headers: authorHeaders,
|
await mutate({
|
||||||
|
mutation: createPostMutation,
|
||||||
|
variables: { id: 'p47', title, content },
|
||||||
})
|
})
|
||||||
const { CreatePost } = await authorClient.request(createPostMutation, {
|
authenticatedUser = await user.toJson()
|
||||||
title,
|
}
|
||||||
content,
|
|
||||||
})
|
|
||||||
post = CreatePost
|
|
||||||
})
|
|
||||||
|
|
||||||
it('sends you a notification', async () => {
|
it('sends you a notification', async () => {
|
||||||
|
await createPostAction()
|
||||||
const expectedContent =
|
const expectedContent =
|
||||||
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
|
||||||
const expected = {
|
const expected = expect.objectContaining({
|
||||||
currentUser: {
|
data: {
|
||||||
notifications: [
|
currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] },
|
||||||
{
|
|
||||||
read: false,
|
|
||||||
post: {
|
|
||||||
content: expectedContent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
|
const { query } = createTestClient(server)
|
||||||
await expect(
|
await expect(
|
||||||
client.request(query, {
|
query({ query: notificationQuery, variables: { read: false } }),
|
||||||
read: false,
|
|
||||||
}),
|
|
||||||
).resolves.toEqual(expected)
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('who mentions me many times', () => {
|
describe('who mentions me many times', () => {
|
||||||
beforeEach(async () => {
|
const updatePostAction = async () => {
|
||||||
const updatedContent = `
|
const updatedContent = `
|
||||||
One more mention to
|
One more mention to
|
||||||
<a data-mention-id="you" class="mention" href="/profile/you">
|
<a data-mention-id="you" class="mention" href="/profile/you">
|
||||||
@ -132,41 +134,52 @@ describe('currentUser { notifications }', () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
authorClient = new GraphQLClient(host, {
|
authenticatedUser = await author.toJson()
|
||||||
headers: authorHeaders,
|
await mutate({
|
||||||
|
mutation: updatePostMutation,
|
||||||
|
variables: {
|
||||||
|
id: 'p47',
|
||||||
|
title,
|
||||||
|
content: updatedContent,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
await authorClient.request(updatePostMutation, {
|
authenticatedUser = await user.toJson()
|
||||||
id: post.id,
|
}
|
||||||
title: post.title,
|
|
||||||
content: updatedContent,
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
it('creates exactly one more notification', async () => {
|
it('creates exactly one more notification', async () => {
|
||||||
|
await createPostAction()
|
||||||
|
await updatePostAction()
|
||||||
const expectedContent =
|
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>'
|
'<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 = {
|
const expected = expect.objectContaining({
|
||||||
currentUser: {
|
data: {
|
||||||
notifications: [
|
currentUser: {
|
||||||
{
|
notifications: [
|
||||||
read: false,
|
{ read: false, post: { content: expectedContent } },
|
||||||
post: {
|
{ read: false, post: { content: expectedContent } },
|
||||||
content: expectedContent,
|
],
|
||||||
},
|
},
|
||||||
},
|
|
||||||
{
|
|
||||||
read: false,
|
|
||||||
post: {
|
|
||||||
content: expectedContent,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
}
|
})
|
||||||
await expect(
|
await expect(
|
||||||
client.request(query, {
|
query({ query: notificationQuery, variables: { read: false } }),
|
||||||
read: false,
|
).resolves.toEqual(expected)
|
||||||
}),
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('but the author of the post blocked me', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await author.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)
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
@ -204,46 +217,40 @@ describe('Hashtags', () => {
|
|||||||
`
|
`
|
||||||
|
|
||||||
describe('authenticated', () => {
|
describe('authenticated', () => {
|
||||||
let headers
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
headers = await login({
|
authenticatedUser = await user.toJson()
|
||||||
email: 'test@example.org',
|
|
||||||
password: '1234',
|
|
||||||
})
|
|
||||||
client = new GraphQLClient(host, {
|
|
||||||
headers,
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('create a Post with Hashtags', () => {
|
describe('create a Post with Hashtags', () => {
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
await client.request(createPostMutation, {
|
await mutate({
|
||||||
postId,
|
mutation: createPostMutation,
|
||||||
postTitle,
|
variables: {
|
||||||
postContent,
|
postId,
|
||||||
|
postTitle,
|
||||||
|
postContent,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
it('both Hashtags are created with the "id" set to thier "name"', async () => {
|
it('both Hashtags are created with the "id" set to their "name"', async () => {
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{ id: 'Democracy', name: 'Democracy' },
|
||||||
id: 'Democracy',
|
{ id: 'Liberty', name: 'Liberty' },
|
||||||
name: 'Democracy',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Liberty',
|
|
||||||
name: 'Liberty',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
await expect(
|
await expect(
|
||||||
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual(
|
||||||
Post: [
|
expect.objectContaining({
|
||||||
{
|
data: {
|
||||||
tags: expect.arrayContaining(expected),
|
Post: [
|
||||||
|
{
|
||||||
|
tags: expect.arrayContaining(expected),
|
||||||
|
},
|
||||||
|
],
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
|
||||||
@ -261,31 +268,28 @@ describe('Hashtags', () => {
|
|||||||
`
|
`
|
||||||
|
|
||||||
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
it('only one previous Hashtag and the new Hashtag exists', async () => {
|
||||||
await client.request(updatePostMutation, {
|
await mutate({
|
||||||
postId,
|
mutation: updatePostMutation,
|
||||||
postTitle,
|
variables: {
|
||||||
updatedPostContent,
|
postId,
|
||||||
|
postTitle,
|
||||||
|
updatedPostContent,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
const expected = [
|
const expected = [
|
||||||
{
|
{ id: 'Elections', name: 'Elections' },
|
||||||
id: 'Elections',
|
{ id: 'Liberty', name: 'Liberty' },
|
||||||
name: 'Elections',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: 'Liberty',
|
|
||||||
name: 'Liberty',
|
|
||||||
},
|
|
||||||
]
|
]
|
||||||
await expect(
|
await expect(
|
||||||
client.request(postWithHastagsQuery, postWithHastagsVariables),
|
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }),
|
||||||
).resolves.toEqual({
|
).resolves.toEqual(
|
||||||
Post: [
|
expect.objectContaining({
|
||||||
{
|
data: {
|
||||||
tags: expect.arrayContaining(expected),
|
Post: [{ tags: expect.arrayContaining(expected) }],
|
||||||
},
|
},
|
||||||
],
|
}),
|
||||||
})
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -3,11 +3,13 @@ import cheerio from 'cheerio'
|
|||||||
export default function(content) {
|
export default function(content) {
|
||||||
if (!content) return []
|
if (!content) return []
|
||||||
const $ = cheerio.load(content)
|
const $ = cheerio.load(content)
|
||||||
let userIds = $('a.mention[data-mention-id]')
|
const userIds = $('a.mention[data-mention-id]')
|
||||||
.map((_, el) => {
|
.map((_, el) => {
|
||||||
return $(el).attr('data-mention-id')
|
return $(el).attr('data-mention-id')
|
||||||
})
|
})
|
||||||
.get()
|
.get()
|
||||||
userIds = userIds.map(id => id.trim()).filter(id => !!id)
|
|
||||||
return userIds
|
return userIds
|
||||||
|
.map(id => id.trim())
|
||||||
|
.filter(id => !!id)
|
||||||
|
.filter((id, index, allIds) => allIds.indexOf(id) === index)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,6 +6,8 @@ const contentEmptyMentions =
|
|||||||
'<p>Something inspirational about <a href="/profile/u2" data-mention-id="" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" data-mention-id target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a href="/profile/u2" data-mention-id="" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3/jenny-rostock" class="mention" data-mention-id target="_blank">@jenny-rostock</a>.</p>'
|
||||||
const contentWithPlainLinks =
|
const contentWithPlainLinks =
|
||||||
'<p>Something inspirational about <a class="mention" href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
'<p>Something inspirational about <a class="mention" href="/profile/u2" target="_blank">@bob-der-baumeister</a> and <a href="/profile/u3" target="_blank">@jenny-rostock</a>.</p>'
|
||||||
|
const contentWithDuplicateIds =
|
||||||
|
'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>'
|
||||||
|
|
||||||
describe('extractMentionedUsers', () => {
|
describe('extractMentionedUsers', () => {
|
||||||
describe('content undefined', () => {
|
describe('content undefined', () => {
|
||||||
@ -18,6 +20,10 @@ describe('extractMentionedUsers', () => {
|
|||||||
expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([])
|
expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('removes duplicates', () => {
|
||||||
|
expect(extractMentionedUsers(contentWithDuplicateIds)).toEqual(['you'])
|
||||||
|
})
|
||||||
|
|
||||||
describe('given a link with .mention class and `data-mention-id` attribute ', () => {
|
describe('given a link with .mention class and `data-mention-id` attribute ', () => {
|
||||||
it('extracts ids', () => {
|
it('extracts ids', () => {
|
||||||
expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3'])
|
expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3'])
|
||||||
|
|||||||
@ -1,22 +1,29 @@
|
|||||||
import { GraphQLClient } from 'graphql-request'
|
|
||||||
import Factory from '../seed/factories'
|
import Factory from '../seed/factories'
|
||||||
import { host } from '../jest/helpers'
|
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
|
|
||||||
let headers
|
|
||||||
let query
|
|
||||||
const factory = Factory()
|
const factory = Factory()
|
||||||
|
const neode = getNeode()
|
||||||
|
const driver = getDriver()
|
||||||
|
|
||||||
|
const { server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user: null,
|
||||||
|
neode,
|
||||||
|
driver,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
const userParams = { name: 'Author', email: 'author@example.org', password: '1234' }
|
await neode.create('Post', { title: 'first' })
|
||||||
await factory.create('User', userParams)
|
await neode.create('Post', { title: 'second' })
|
||||||
await factory.authenticateAs(userParams)
|
await neode.create('Post', { title: 'third' })
|
||||||
await factory.create('Post', { title: 'first' })
|
await neode.create('Post', { title: 'last' })
|
||||||
await factory.create('Post', { title: 'second' })
|
|
||||||
await factory.create('Post', { title: 'third' })
|
|
||||||
await factory.create('Post', { title: 'last' })
|
|
||||||
headers = {}
|
|
||||||
client = new GraphQLClient(host, { headers })
|
|
||||||
})
|
})
|
||||||
|
|
||||||
afterEach(async () => {
|
afterEach(async () => {
|
||||||
@ -25,10 +32,6 @@ afterEach(async () => {
|
|||||||
|
|
||||||
describe('Query', () => {
|
describe('Query', () => {
|
||||||
describe('Post', () => {
|
describe('Post', () => {
|
||||||
beforeEach(() => {
|
|
||||||
query = '{ Post { title } }'
|
|
||||||
})
|
|
||||||
|
|
||||||
describe('orderBy', () => {
|
describe('orderBy', () => {
|
||||||
it('createdAt descending is default', async () => {
|
it('createdAt descending is default', async () => {
|
||||||
const posts = [
|
const posts = [
|
||||||
@ -37,15 +40,21 @@ describe('Query', () => {
|
|||||||
{ title: 'second' },
|
{ title: 'second' },
|
||||||
{ title: 'first' },
|
{ title: 'first' },
|
||||||
]
|
]
|
||||||
const expected = { Post: posts }
|
const expected = expect.objectContaining({ data: { Post: posts } })
|
||||||
await expect(client.request(query)).resolves.toEqual(expected)
|
await expect(
|
||||||
|
query({
|
||||||
|
query: gql`
|
||||||
|
{
|
||||||
|
Post {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
|
|
||||||
describe('(orderBy: createdAt_asc)', () => {
|
describe('(orderBy: createdAt_asc)', () => {
|
||||||
beforeEach(() => {
|
|
||||||
query = '{ Post(orderBy: createdAt_asc) { title } }'
|
|
||||||
})
|
|
||||||
|
|
||||||
it('orders by createdAt ascending', async () => {
|
it('orders by createdAt ascending', async () => {
|
||||||
const posts = [
|
const posts = [
|
||||||
{ title: 'first' },
|
{ title: 'first' },
|
||||||
@ -53,8 +62,18 @@ describe('Query', () => {
|
|||||||
{ title: 'third' },
|
{ title: 'third' },
|
||||||
{ title: 'last' },
|
{ title: 'last' },
|
||||||
]
|
]
|
||||||
const expected = { Post: posts }
|
const expected = expect.objectContaining({ data: { Post: posts } })
|
||||||
await expect(client.request(query)).resolves.toEqual(expected)
|
await expect(
|
||||||
|
query({
|
||||||
|
query: gql`
|
||||||
|
{
|
||||||
|
Post(orderBy: createdAt_asc) {
|
||||||
|
title
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
}),
|
||||||
|
).resolves.toEqual(expected)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -10,7 +10,7 @@ const instance = neode()
|
|||||||
const isAuthenticated = rule({
|
const isAuthenticated = rule({
|
||||||
cache: 'contextual',
|
cache: 'contextual',
|
||||||
})(async (_parent, _args, ctx, _info) => {
|
})(async (_parent, _args, ctx, _info) => {
|
||||||
return ctx.user != null
|
return !!(ctx && ctx.user && ctx.user.id)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isModerator = rule()(async (parent, args, { user }, info) => {
|
const isModerator = rule()(async (parent, args, { user }, info) => {
|
||||||
@ -159,6 +159,7 @@ const permissions = shield(
|
|||||||
Badge: allow,
|
Badge: allow,
|
||||||
PostsEmotionsCountByEmotion: allow,
|
PostsEmotionsCountByEmotion: allow,
|
||||||
PostsEmotionsByCurrentUser: allow,
|
PostsEmotionsByCurrentUser: allow,
|
||||||
|
blockedUsers: isAuthenticated,
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
'*': deny,
|
'*': deny,
|
||||||
@ -195,6 +196,8 @@ const permissions = shield(
|
|||||||
resetPassword: allow,
|
resetPassword: allow,
|
||||||
AddPostEmotions: isAuthenticated,
|
AddPostEmotions: isAuthenticated,
|
||||||
RemovePostEmotions: isAuthenticated,
|
RemovePostEmotions: isAuthenticated,
|
||||||
|
block: isAuthenticated,
|
||||||
|
unblock: isAuthenticated,
|
||||||
},
|
},
|
||||||
User: {
|
User: {
|
||||||
email: isMyOwn,
|
email: isMyOwn,
|
||||||
|
|||||||
19
backend/src/models/Notification.js
Normal file
19
backend/src/models/Notification.js
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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 },
|
||||||
|
user: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'NOTIFIED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
post: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'NOTIFIED',
|
||||||
|
target: 'Post',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
|
}
|
||||||
@ -71,4 +71,16 @@ module.exports = {
|
|||||||
eager: true,
|
eager: true,
|
||||||
cascade: true,
|
cascade: true,
|
||||||
},
|
},
|
||||||
|
blocked: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'BLOCKED',
|
||||||
|
target: 'User',
|
||||||
|
direction: 'out',
|
||||||
|
},
|
||||||
|
notifications: {
|
||||||
|
type: 'relationship',
|
||||||
|
relationship: 'NOTIFIED',
|
||||||
|
target: 'Notification',
|
||||||
|
direction: 'in',
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,4 +7,5 @@ export default {
|
|||||||
EmailAddress: require('./EmailAddress.js'),
|
EmailAddress: require('./EmailAddress.js'),
|
||||||
SocialMedia: require('./SocialMedia.js'),
|
SocialMedia: require('./SocialMedia.js'),
|
||||||
Post: require('./Post.js'),
|
Post: require('./Post.js'),
|
||||||
|
Notification: require('./Notification.js'),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,10 +15,12 @@ export default function Resolver(type, options = {}) {
|
|||||||
const {
|
const {
|
||||||
idAttribute = 'id',
|
idAttribute = 'id',
|
||||||
undefinedToNull = [],
|
undefinedToNull = [],
|
||||||
|
boolean = {},
|
||||||
count = {},
|
count = {},
|
||||||
hasOne = {},
|
hasOne = {},
|
||||||
hasMany = {},
|
hasMany = {},
|
||||||
} = options
|
} = options
|
||||||
|
|
||||||
const _hasResolver = (resolvers, { key, connection }, { returnType }) => {
|
const _hasResolver = (resolvers, { key, connection }, { returnType }) => {
|
||||||
return async (parent, params, context, resolveInfo) => {
|
return async (parent, params, context, resolveInfo) => {
|
||||||
if (typeof parent[key] !== 'undefined') return parent[key]
|
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||||
@ -31,6 +33,26 @@ export default function Resolver(type, options = {}) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const booleanResolver = obj => {
|
||||||
|
const resolvers = {}
|
||||||
|
for (const [key, condition] of Object.entries(obj)) {
|
||||||
|
resolvers[key] = async (parent, params, { cypherParams }, resolveInfo) => {
|
||||||
|
if (typeof parent[key] !== 'undefined') return parent[key]
|
||||||
|
const result = await instance.cypher(
|
||||||
|
`
|
||||||
|
${condition.replace('this', 'this {id: $parent.id}')} as ${key}`,
|
||||||
|
{
|
||||||
|
parent,
|
||||||
|
cypherParams,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
const [record] = result.records
|
||||||
|
return record.get(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return resolvers
|
||||||
|
}
|
||||||
|
|
||||||
const countResolver = obj => {
|
const countResolver = obj => {
|
||||||
const resolvers = {}
|
const resolvers = {}
|
||||||
for (const [key, connection] of Object.entries(obj)) {
|
for (const [key, connection] of Object.entries(obj)) {
|
||||||
@ -67,6 +89,7 @@ export default function Resolver(type, options = {}) {
|
|||||||
}
|
}
|
||||||
const result = {
|
const result = {
|
||||||
...undefinedToNullResolver(undefinedToNull),
|
...undefinedToNullResolver(undefinedToNull),
|
||||||
|
...booleanResolver(boolean),
|
||||||
...countResolver(count),
|
...countResolver(count),
|
||||||
...hasOneResolver(hasOne),
|
...hasOneResolver(hasOne),
|
||||||
...hasManyResolver(hasMany),
|
...hasManyResolver(hasMany),
|
||||||
|
|||||||
@ -1,7 +1,74 @@
|
|||||||
import uuid from 'uuid/v4'
|
import uuid from 'uuid/v4'
|
||||||
|
import { neo4jgraphql } from 'neo4j-graphql-js'
|
||||||
import fileUpload from './fileUpload'
|
import fileUpload from './fileUpload'
|
||||||
|
import { getBlockedUsers, getBlockedByUsers } from './users.js'
|
||||||
|
import { mergeWith, isArray } from 'lodash'
|
||||||
|
|
||||||
|
const filterForBlockedUsers = async (params, context) => {
|
||||||
|
if (!context.user) return params
|
||||||
|
const [blockedUsers, blockedByUsers] = await Promise.all([
|
||||||
|
getBlockedUsers(context),
|
||||||
|
getBlockedByUsers(context),
|
||||||
|
])
|
||||||
|
const badIds = [...blockedByUsers.map(b => b.id), ...blockedUsers.map(b => b.id)]
|
||||||
|
params.filter = mergeWith(
|
||||||
|
params.filter,
|
||||||
|
{
|
||||||
|
author_not: { id_in: badIds },
|
||||||
|
},
|
||||||
|
(objValue, srcValue) => {
|
||||||
|
if (isArray(objValue)) {
|
||||||
|
return objValue.concat(srcValue)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
Query: {
|
||||||
|
Post: async (object, params, context, resolveInfo) => {
|
||||||
|
params = await filterForBlockedUsers(params, context)
|
||||||
|
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||||
|
},
|
||||||
|
findPosts: async (object, params, context, resolveInfo) => {
|
||||||
|
params = await filterForBlockedUsers(params, context)
|
||||||
|
return neo4jgraphql(object, params, context, resolveInfo, false)
|
||||||
|
},
|
||||||
|
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { postId, data } = params
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
||||||
|
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
||||||
|
`,
|
||||||
|
{ postId, data },
|
||||||
|
)
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
const [emotionsCount] = transactionRes.records.map(record => {
|
||||||
|
return record.get('emotionsCount').low
|
||||||
|
})
|
||||||
|
|
||||||
|
return emotionsCount
|
||||||
|
},
|
||||||
|
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
||||||
|
const session = context.driver.session()
|
||||||
|
const { postId } = params
|
||||||
|
const transactionRes = await session.run(
|
||||||
|
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
||||||
|
RETURN collect(emoted.emotion) as emotion`,
|
||||||
|
{ userId: context.user.id, postId },
|
||||||
|
)
|
||||||
|
|
||||||
|
session.close()
|
||||||
|
|
||||||
|
const [emotions] = transactionRes.records.map(record => {
|
||||||
|
return record.get('emotion')
|
||||||
|
})
|
||||||
|
return emotions
|
||||||
|
},
|
||||||
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
UpdatePost: async (object, params, context, resolveInfo) => {
|
UpdatePost: async (object, params, context, resolveInfo) => {
|
||||||
const { categoryIds } = params
|
const { categoryIds } = params
|
||||||
@ -112,39 +179,4 @@ export default {
|
|||||||
return emoted
|
return emoted
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Query: {
|
|
||||||
PostsEmotionsCountByEmotion: async (object, params, context, resolveInfo) => {
|
|
||||||
const session = context.driver.session()
|
|
||||||
const { postId, data } = params
|
|
||||||
const transactionRes = await session.run(
|
|
||||||
`MATCH (post:Post {id: $postId})<-[emoted:EMOTED {emotion: $data.emotion}]-()
|
|
||||||
RETURN COUNT(DISTINCT emoted) as emotionsCount
|
|
||||||
`,
|
|
||||||
{ postId, data },
|
|
||||||
)
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
const [emotionsCount] = transactionRes.records.map(record => {
|
|
||||||
return record.get('emotionsCount').low
|
|
||||||
})
|
|
||||||
|
|
||||||
return emotionsCount
|
|
||||||
},
|
|
||||||
PostsEmotionsByCurrentUser: async (object, params, context, resolveInfo) => {
|
|
||||||
const session = context.driver.session()
|
|
||||||
const { postId } = params
|
|
||||||
const transactionRes = await session.run(
|
|
||||||
`MATCH (user:User {id: $userId})-[emoted:EMOTED]->(post:Post {id: $postId})
|
|
||||||
RETURN collect(emoted.emotion) as emotion`,
|
|
||||||
{ userId: context.user.id, postId },
|
|
||||||
)
|
|
||||||
|
|
||||||
session.close()
|
|
||||||
|
|
||||||
const [emotions] = transactionRes.records.map(record => {
|
|
||||||
return record.get('emotion')
|
|
||||||
})
|
|
||||||
return emotions
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -460,6 +460,7 @@ describe('emotions', () => {
|
|||||||
context: () => {
|
context: () => {
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
neode: instance,
|
||||||
driver,
|
driver,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -476,6 +477,7 @@ describe('emotions', () => {
|
|||||||
context: () => {
|
context: () => {
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
|
neode: instance,
|
||||||
driver,
|
driver,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -6,8 +6,45 @@ import Resolver from './helpers/Resolver'
|
|||||||
|
|
||||||
const instance = neode()
|
const instance = neode()
|
||||||
|
|
||||||
|
export const getBlockedUsers = async context => {
|
||||||
|
const { neode } = context
|
||||||
|
const userModel = neode.model('User')
|
||||||
|
let blockedUsers = neode
|
||||||
|
.query()
|
||||||
|
.match('user', userModel)
|
||||||
|
.where('user.id', context.user.id)
|
||||||
|
.relationship(userModel.relationships().get('blocked'))
|
||||||
|
.to('blocked', userModel)
|
||||||
|
.return('blocked')
|
||||||
|
blockedUsers = await blockedUsers.execute()
|
||||||
|
blockedUsers = blockedUsers.records.map(r => r.get('blocked').properties)
|
||||||
|
return blockedUsers
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getBlockedByUsers = async context => {
|
||||||
|
const { neode } = context
|
||||||
|
const userModel = neode.model('User')
|
||||||
|
let blockedByUsers = neode
|
||||||
|
.query()
|
||||||
|
.match('user', userModel)
|
||||||
|
.relationship(userModel.relationships().get('blocked'))
|
||||||
|
.to('blocked', userModel)
|
||||||
|
.where('blocked.id', context.user.id)
|
||||||
|
.return('user')
|
||||||
|
blockedByUsers = await blockedByUsers.execute()
|
||||||
|
blockedByUsers = blockedByUsers.records.map(r => r.get('user').properties)
|
||||||
|
return blockedByUsers
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
Query: {
|
Query: {
|
||||||
|
blockedUsers: async (object, args, context, resolveInfo) => {
|
||||||
|
try {
|
||||||
|
return getBlockedUsers(context)
|
||||||
|
} catch (e) {
|
||||||
|
throw new UserInputError(e.message)
|
||||||
|
}
|
||||||
|
},
|
||||||
User: async (object, args, context, resolveInfo) => {
|
User: async (object, args, context, resolveInfo) => {
|
||||||
const { email } = args
|
const { email } = args
|
||||||
if (email) {
|
if (email) {
|
||||||
@ -20,6 +57,36 @@ export default {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
Mutation: {
|
Mutation: {
|
||||||
|
block: async (object, args, context, resolveInfo) => {
|
||||||
|
const { user: currentUser } = context
|
||||||
|
if (currentUser.id === args.id) return null
|
||||||
|
await instance.cypher(
|
||||||
|
`
|
||||||
|
MATCH(u:User {id: $currentUser.id})-[r:FOLLOWS]->(b:User {id: $args.id})
|
||||||
|
DELETE r
|
||||||
|
`,
|
||||||
|
{ currentUser, args },
|
||||||
|
)
|
||||||
|
const [user, blockedUser] = await Promise.all([
|
||||||
|
instance.find('User', currentUser.id),
|
||||||
|
instance.find('User', args.id),
|
||||||
|
])
|
||||||
|
await user.relateTo(blockedUser, 'blocked')
|
||||||
|
return blockedUser.toJson()
|
||||||
|
},
|
||||||
|
unblock: async (object, args, context, resolveInfo) => {
|
||||||
|
const { user: currentUser } = context
|
||||||
|
if (currentUser.id === args.id) return null
|
||||||
|
await instance.cypher(
|
||||||
|
`
|
||||||
|
MATCH(u:User {id: $currentUser.id})-[r:BLOCKED]->(b:User {id: $args.id})
|
||||||
|
DELETE r
|
||||||
|
`,
|
||||||
|
{ currentUser, args },
|
||||||
|
)
|
||||||
|
const blockedUser = await instance.find('User', args.id)
|
||||||
|
return blockedUser.toJson()
|
||||||
|
},
|
||||||
UpdateUser: async (object, args, context, resolveInfo) => {
|
UpdateUser: async (object, args, context, resolveInfo) => {
|
||||||
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
|
||||||
try {
|
try {
|
||||||
@ -73,6 +140,12 @@ export default {
|
|||||||
'locationName',
|
'locationName',
|
||||||
'about',
|
'about',
|
||||||
],
|
],
|
||||||
|
boolean: {
|
||||||
|
followedByCurrentUser:
|
||||||
|
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
|
isBlocked:
|
||||||
|
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
|
||||||
|
},
|
||||||
count: {
|
count: {
|
||||||
contributionsCount: '-[:WROTE]->(related:Post)',
|
contributionsCount: '-[:WROTE]->(related:Post)',
|
||||||
friendsCount: '<-[:FRIENDS]->(related:User)',
|
friendsCount: '<-[:FRIENDS]->(related:User)',
|
||||||
@ -91,7 +164,6 @@ export default {
|
|||||||
followedBy: '<-[:FOLLOWS]-(related:User)',
|
followedBy: '<-[:FOLLOWS]-(related:User)',
|
||||||
following: '-[:FOLLOWS]->(related:User)',
|
following: '-[:FOLLOWS]->(related:User)',
|
||||||
friends: '-[:FRIENDS]-(related:User)',
|
friends: '-[:FRIENDS]-(related:User)',
|
||||||
blacklisted: '-[:BLACKLISTED]->(related:User)',
|
|
||||||
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
|
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
|
||||||
contributions: '-[:WROTE]->(related:Post)',
|
contributions: '-[:WROTE]->(related:Post)',
|
||||||
comments: '-[:WROTE]->(related:Comment)',
|
comments: '-[:WROTE]->(related:Comment)',
|
||||||
|
|||||||
393
backend/src/schema/resolvers/users/blockedUsers.spec.js
Normal file
393
backend/src/schema/resolvers/users/blockedUsers.spec.js
Normal file
@ -0,0 +1,393 @@
|
|||||||
|
import { createTestClient } from 'apollo-server-testing'
|
||||||
|
import createServer from '../../../server'
|
||||||
|
import Factory from '../../../seed/factories'
|
||||||
|
import { gql } from '../../../jest/helpers'
|
||||||
|
import { neode, getDriver } from '../../../bootstrap/neo4j'
|
||||||
|
|
||||||
|
const driver = getDriver()
|
||||||
|
const factory = Factory()
|
||||||
|
const instance = neode()
|
||||||
|
|
||||||
|
let currentUser
|
||||||
|
let blockedUser
|
||||||
|
let authenticatedUser
|
||||||
|
let server
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
authenticatedUser = undefined
|
||||||
|
;({ server } = createServer({
|
||||||
|
context: () => {
|
||||||
|
return {
|
||||||
|
user: authenticatedUser,
|
||||||
|
driver,
|
||||||
|
neode: instance,
|
||||||
|
cypherParams: {
|
||||||
|
currentUserId: authenticatedUser ? authenticatedUser.id : null,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}))
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await factory.cleanDatabase()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('blockedUsers', () => {
|
||||||
|
let blockedUserQuery
|
||||||
|
beforeEach(() => {
|
||||||
|
blockedUserQuery = gql`
|
||||||
|
query {
|
||||||
|
blockedUsers {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws permission error', async () => {
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
const result = await query({ query: blockedUserQuery })
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated and given a blocked user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await instance.create('User', {
|
||||||
|
name: 'Current User',
|
||||||
|
id: 'u1',
|
||||||
|
})
|
||||||
|
blockedUser = await instance.create('User', {
|
||||||
|
name: 'Blocked User',
|
||||||
|
id: 'u2',
|
||||||
|
})
|
||||||
|
await currentUser.relateTo(blockedUser, 'blocked')
|
||||||
|
authenticatedUser = await currentUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('returns a list of blocked users', async () => {
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
await expect(query({ query: blockedUserQuery })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
blockedUsers: [
|
||||||
|
{
|
||||||
|
name: 'Blocked User',
|
||||||
|
id: 'u2',
|
||||||
|
isBlocked: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('block', () => {
|
||||||
|
let blockAction
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentUser = undefined
|
||||||
|
blockAction = variables => {
|
||||||
|
const { mutate } = createTestClient(server)
|
||||||
|
const blockMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
block(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return mutate({ mutation: blockMutation, variables })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws permission error', async () => {
|
||||||
|
const result = await blockAction({ id: 'u2' })
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await instance.create('User', {
|
||||||
|
name: 'Current User',
|
||||||
|
id: 'u1',
|
||||||
|
})
|
||||||
|
authenticatedUser = await currentUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('block yourself', () => {
|
||||||
|
it('returns null', async () => {
|
||||||
|
await expect(blockAction({ id: 'u1' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({ data: { block: null } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('block not existing user', () => {
|
||||||
|
it('returns null', async () => {
|
||||||
|
await expect(blockAction({ id: 'u2' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({ data: { block: null } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a to-be-blocked user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
blockedUser = await instance.create('User', {
|
||||||
|
name: 'Blocked User',
|
||||||
|
id: 'u2',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
it('blocks a user', async () => {
|
||||||
|
await expect(blockAction({ id: 'u2' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { block: { id: 'u2', name: 'Blocked User', isBlocked: true } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unfollows the user', async () => {
|
||||||
|
await currentUser.relateTo(blockedUser, 'following')
|
||||||
|
const queryUser = gql`
|
||||||
|
query {
|
||||||
|
User(id: "u2") {
|
||||||
|
id
|
||||||
|
isBlocked
|
||||||
|
followedByCurrentUser
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
await expect(query({ query: queryUser })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { User: [{ id: 'u2', isBlocked: false, followedByCurrentUser: true }] },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
await blockAction({ id: 'u2' })
|
||||||
|
await expect(query({ query: queryUser })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { User: [{ id: 'u2', isBlocked: true, followedByCurrentUser: false }] },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given both the current user and the to-be-blocked user write a post', () => {
|
||||||
|
let postQuery
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const post1 = await instance.create('Post', {
|
||||||
|
id: 'p12',
|
||||||
|
title: 'A post written by the current user',
|
||||||
|
})
|
||||||
|
const post2 = await instance.create('Post', {
|
||||||
|
id: 'p23',
|
||||||
|
title: 'A post written by the blocked user',
|
||||||
|
})
|
||||||
|
await Promise.all([
|
||||||
|
post1.relateTo(currentUser, 'author'),
|
||||||
|
post2.relateTo(blockedUser, 'author'),
|
||||||
|
])
|
||||||
|
postQuery = gql`
|
||||||
|
query {
|
||||||
|
Post(orderBy: createdAt_asc) {
|
||||||
|
id
|
||||||
|
title
|
||||||
|
author {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
})
|
||||||
|
|
||||||
|
const bothPostsAreInTheNewsfeed = async () => {
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
id: 'p12',
|
||||||
|
title: 'A post written by the current user',
|
||||||
|
author: {
|
||||||
|
name: 'Current User',
|
||||||
|
id: 'u1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'p23',
|
||||||
|
title: 'A post written by the blocked user',
|
||||||
|
author: {
|
||||||
|
name: 'Blocked User',
|
||||||
|
id: 'u2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('from the perspective of the current user', () => {
|
||||||
|
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
|
||||||
|
|
||||||
|
describe('but if the current user blocks the other user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await currentUser.relateTo(blockedUser, 'blocked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the blocked user's post won't show up in the newsfeed of the current user", async () => {
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
id: 'p12',
|
||||||
|
title: 'A post written by the current user',
|
||||||
|
author: { name: 'Current User', id: 'u1' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('from the perspective of the blocked user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
authenticatedUser = await blockedUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
it('both posts are in the newsfeed', bothPostsAreInTheNewsfeed)
|
||||||
|
describe('but if the current user blocks the other user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await currentUser.relateTo(blockedUser, 'blocked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it("the current user's post won't show up in the newsfeed of the blocked user", async () => {
|
||||||
|
const { query } = createTestClient(server)
|
||||||
|
await expect(query({ query: postQuery })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: {
|
||||||
|
Post: [
|
||||||
|
{
|
||||||
|
id: 'p23',
|
||||||
|
title: 'A post written by the blocked user',
|
||||||
|
author: { name: 'Blocked User', id: 'u2' },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unblock', () => {
|
||||||
|
let unblockAction
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentUser = undefined
|
||||||
|
unblockAction = variables => {
|
||||||
|
const { mutate } = createTestClient(server)
|
||||||
|
const unblockMutation = gql`
|
||||||
|
mutation($id: ID!) {
|
||||||
|
unblock(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isBlocked
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
return mutate({ mutation: unblockMutation, variables })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
it('throws permission error', async () => {
|
||||||
|
const result = await unblockAction({ id: 'u2' })
|
||||||
|
expect(result.errors[0]).toHaveProperty('message', 'Not Authorised!')
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('authenticated', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
currentUser = await instance.create('User', {
|
||||||
|
name: 'Current User',
|
||||||
|
id: 'u1',
|
||||||
|
})
|
||||||
|
authenticatedUser = await currentUser.toJson()
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unblock yourself', () => {
|
||||||
|
it('returns null', async () => {
|
||||||
|
await expect(unblockAction({ id: 'u1' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({ data: { unblock: null } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unblock not-existing user', () => {
|
||||||
|
it('returns null', async () => {
|
||||||
|
await expect(unblockAction({ id: 'lksjdflksfdj' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({ data: { unblock: null } }),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given another user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
blockedUser = await instance.create('User', {
|
||||||
|
name: 'Blocked User',
|
||||||
|
id: 'u2',
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unblocking a not yet blocked user', () => {
|
||||||
|
it('does not hurt', async () => {
|
||||||
|
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('given a blocked user', () => {
|
||||||
|
beforeEach(async () => {
|
||||||
|
await currentUser.relateTo(blockedUser, 'blocked')
|
||||||
|
})
|
||||||
|
|
||||||
|
it('unblocks a user', async () => {
|
||||||
|
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
describe('unblocking twice', () => {
|
||||||
|
it('has no effect', async () => {
|
||||||
|
await unblockAction({ id: 'u2' })
|
||||||
|
await expect(unblockAction({ id: 'u2' })).resolves.toEqual(
|
||||||
|
expect.objectContaining({
|
||||||
|
data: { unblock: { id: 'u2', name: 'Blocked User', isBlocked: false } },
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
@ -4,15 +4,16 @@ type Query {
|
|||||||
currentUser: User
|
currentUser: User
|
||||||
# Get the latest Network Statistics
|
# Get the latest Network Statistics
|
||||||
statistics: Statistics!
|
statistics: Statistics!
|
||||||
findPosts(filter: String!, limit: Int = 10): [Post]!
|
findPosts(query: String!, limit: Int = 10): [Post]!
|
||||||
@cypher(
|
@cypher(
|
||||||
statement: """
|
statement: """
|
||||||
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
|
CALL db.index.fulltext.queryNodes('full_text_search', $query)
|
||||||
YIELD node as post, score
|
YIELD node as post, score
|
||||||
MATCH (post)<-[:WROTE]-(user:User)
|
MATCH (post)<-[:WROTE]-(user:User)
|
||||||
WHERE score >= 0.2
|
WHERE score >= 0.2
|
||||||
AND NOT user.deleted = true AND NOT user.disabled = true
|
AND NOT user.deleted = true AND NOT user.disabled = true
|
||||||
AND NOT post.deleted = true AND NOT post.disabled = true
|
AND NOT post.deleted = true AND NOT post.disabled = true
|
||||||
|
AND NOT user.id in COALESCE($filter.author_not.id_in, [])
|
||||||
RETURN post
|
RETURN post
|
||||||
LIMIT $limit
|
LIMIT $limit
|
||||||
"""
|
"""
|
||||||
@ -50,15 +51,6 @@ type Statistics {
|
|||||||
countShouts: Int!
|
countShouts: Int!
|
||||||
}
|
}
|
||||||
|
|
||||||
type Notification {
|
|
||||||
id: ID!
|
|
||||||
read: Boolean
|
|
||||||
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
|
||||||
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
comment: Comment @relation(name: "NOTIFIED", direction: "IN")
|
|
||||||
createdAt: String
|
|
||||||
}
|
|
||||||
|
|
||||||
type Location {
|
type Location {
|
||||||
id: ID!
|
id: ID!
|
||||||
name: String!
|
name: String!
|
||||||
@ -131,4 +123,3 @@ type SharedInboxEndpoint {
|
|||||||
id: ID!
|
id: ID!
|
||||||
uri: String
|
uri: String
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -10,4 +10,4 @@ type Category {
|
|||||||
|
|
||||||
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
|
posts: [Post]! @relation(name: "CATEGORIZED", direction: "IN")
|
||||||
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
|
postCount: Int! @cypher(statement: "MATCH (this)<-[:CATEGORIZED]-(r:Post) RETURN COUNT(r)")
|
||||||
}
|
}
|
||||||
|
|||||||
7
backend/src/schema/types/type/Notification.gql
Normal file
7
backend/src/schema/types/type/Notification.gql
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
type Notification {
|
||||||
|
id: ID!
|
||||||
|
read: Boolean
|
||||||
|
user: User @relation(name: "NOTIFIED", direction: "OUT")
|
||||||
|
post: Post @relation(name: "NOTIFIED", direction: "IN")
|
||||||
|
createdAt: String
|
||||||
|
}
|
||||||
@ -42,6 +42,12 @@ type User {
|
|||||||
RETURN COUNT(u) >= 1
|
RETURN COUNT(u) >= 1
|
||||||
"""
|
"""
|
||||||
)
|
)
|
||||||
|
isBlocked: Boolean! @cypher(
|
||||||
|
statement: """
|
||||||
|
MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId})
|
||||||
|
RETURN COUNT(u) >= 1
|
||||||
|
"""
|
||||||
|
)
|
||||||
|
|
||||||
#contributions: [WrittenPost]!
|
#contributions: [WrittenPost]!
|
||||||
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
|
||||||
@ -67,8 +73,6 @@ type User {
|
|||||||
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
|
organizationsCreated: [Organization] @relation(name: "CREATED_ORGA", direction: "OUT")
|
||||||
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
|
organizationsOwned: [Organization] @relation(name: "OWNING_ORGA", direction: "OUT")
|
||||||
|
|
||||||
blacklisted: [User]! @relation(name: "BLACKLISTED", direction: "OUT")
|
|
||||||
|
|
||||||
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
categories: [Category]! @relation(name: "CATEGORIZED", direction: "OUT")
|
||||||
|
|
||||||
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
|
||||||
@ -148,6 +152,8 @@ type Query {
|
|||||||
orderBy: [_UserOrdering]
|
orderBy: [_UserOrdering]
|
||||||
filter: _UserFilter
|
filter: _UserFilter
|
||||||
): [User]
|
): [User]
|
||||||
|
|
||||||
|
blockedUsers: [User]
|
||||||
}
|
}
|
||||||
|
|
||||||
type Mutation {
|
type Mutation {
|
||||||
@ -164,4 +170,8 @@ type Mutation {
|
|||||||
): User
|
): User
|
||||||
|
|
||||||
DeleteUser(id: ID!, resource: [Deletable]): User
|
DeleteUser(id: ID!, resource: [Deletable]): User
|
||||||
|
|
||||||
|
|
||||||
|
block(id: ID!): User
|
||||||
|
unblock(id: ID!): User
|
||||||
}
|
}
|
||||||
|
|||||||
@ -127,73 +127,35 @@ import Factory from './factories'
|
|||||||
bobDerBaumeister.relateTo(turtle, 'rewarded'),
|
bobDerBaumeister.relateTo(turtle, 'rewarded'),
|
||||||
jennyRostock.relateTo(bear, 'rewarded'),
|
jennyRostock.relateTo(bear, 'rewarded'),
|
||||||
dagobert.relateTo(rabbit, 'rewarded'),
|
dagobert.relateTo(rabbit, 'rewarded'),
|
||||||
])
|
|
||||||
|
|
||||||
await Promise.all([
|
peterLustig.relateTo(bobDerBaumeister, 'friends'),
|
||||||
f.relate('User', 'Friends', {
|
peterLustig.relateTo(jennyRostock, 'friends'),
|
||||||
from: 'u1',
|
bobDerBaumeister.relateTo(jennyRostock, 'friends'),
|
||||||
to: 'u2',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Friends', {
|
|
||||||
from: 'u1',
|
|
||||||
to: 'u3',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Friends', {
|
|
||||||
from: 'u2',
|
|
||||||
to: 'u3',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Blacklisted', {
|
|
||||||
from: 'u7',
|
|
||||||
to: 'u4',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Blacklisted', {
|
|
||||||
from: 'u7',
|
|
||||||
to: 'u5',
|
|
||||||
}),
|
|
||||||
f.relate('User', 'Blacklisted', {
|
|
||||||
from: 'u7',
|
|
||||||
to: 'u6',
|
|
||||||
}),
|
|
||||||
])
|
|
||||||
|
|
||||||
await Promise.all([
|
peterLustig.relateTo(jennyRostock, 'following'),
|
||||||
asAdmin.follow({
|
peterLustig.relateTo(tick, 'following'),
|
||||||
id: 'u3',
|
bobDerBaumeister.relateTo(tick, 'following'),
|
||||||
type: 'User',
|
jennyRostock.relateTo(tick, 'following'),
|
||||||
}),
|
tick.relateTo(track, 'following'),
|
||||||
asModerator.follow({
|
trick.relateTo(tick, 'following'),
|
||||||
id: 'u4',
|
track.relateTo(jennyRostock, 'following'),
|
||||||
type: 'User',
|
|
||||||
}),
|
dagobert.relateTo(tick, 'blocked'),
|
||||||
asUser.follow({
|
dagobert.relateTo(trick, 'blocked'),
|
||||||
id: 'u4',
|
dagobert.relateTo(track, 'blocked'),
|
||||||
type: 'User',
|
|
||||||
}),
|
|
||||||
asTick.follow({
|
|
||||||
id: 'u6',
|
|
||||||
type: 'User',
|
|
||||||
}),
|
|
||||||
asTrick.follow({
|
|
||||||
id: 'u4',
|
|
||||||
type: 'User',
|
|
||||||
}),
|
|
||||||
asTrack.follow({
|
|
||||||
id: 'u3',
|
|
||||||
type: 'User',
|
|
||||||
}),
|
|
||||||
])
|
])
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat1',
|
id: 'cat1',
|
||||||
name: 'Just For Fun',
|
name: 'Just For Fun',
|
||||||
slug: 'justforfun',
|
slug: 'just-for-fun',
|
||||||
icon: 'smile',
|
icon: 'smile',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat2',
|
id: 'cat2',
|
||||||
name: 'Happyness & Values',
|
name: 'Happiness & Values',
|
||||||
slug: 'happyness-values',
|
slug: 'happiness-values',
|
||||||
icon: 'heart-o',
|
icon: 'heart-o',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
@ -211,13 +173,13 @@ import Factory from './factories'
|
|||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat5',
|
id: 'cat5',
|
||||||
name: 'Animal Protection',
|
name: 'Animal Protection',
|
||||||
slug: 'animalprotection',
|
slug: 'animal-protection',
|
||||||
icon: 'paw',
|
icon: 'paw',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat6',
|
id: 'cat6',
|
||||||
name: 'Humanrights Justice',
|
name: 'Human Rights & Justice',
|
||||||
slug: 'humanrights-justice',
|
slug: 'human-rights-justice',
|
||||||
icon: 'balance-scale',
|
icon: 'balance-scale',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
@ -253,19 +215,19 @@ import Factory from './factories'
|
|||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat12',
|
id: 'cat12',
|
||||||
name: 'IT, Internet & Data Privacy',
|
name: 'IT, Internet & Data Privacy',
|
||||||
slug: 'it-internet-dataprivacy',
|
slug: 'it-internet-data-privacy',
|
||||||
icon: 'mouse-pointer',
|
icon: 'mouse-pointer',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat13',
|
id: 'cat13',
|
||||||
name: 'Art, Curlure & Sport',
|
name: 'Art, Culture & Sport',
|
||||||
slug: 'art-culture-sport',
|
slug: 'art-culture-sport',
|
||||||
icon: 'paint-brush',
|
icon: 'paint-brush',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat14',
|
id: 'cat14',
|
||||||
name: 'Freedom of Speech',
|
name: 'Freedom of Speech',
|
||||||
slug: 'freedomofspeech',
|
slug: 'freedom-of-speech',
|
||||||
icon: 'bullhorn',
|
icon: 'bullhorn',
|
||||||
}),
|
}),
|
||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
@ -277,7 +239,7 @@ import Factory from './factories'
|
|||||||
f.create('Category', {
|
f.create('Category', {
|
||||||
id: 'cat16',
|
id: 'cat16',
|
||||||
name: 'Global Peace & Nonviolence',
|
name: 'Global Peace & Nonviolence',
|
||||||
slug: 'globalpeace-nonviolence',
|
slug: 'global-peace-nonviolence',
|
||||||
icon: 'angellist',
|
icon: 'angellist',
|
||||||
}),
|
}),
|
||||||
])
|
])
|
||||||
|
|||||||
@ -3,7 +3,7 @@ import helmet from 'helmet'
|
|||||||
import { ApolloServer } from 'apollo-server-express'
|
import { ApolloServer } from 'apollo-server-express'
|
||||||
import CONFIG, { requiredConfigs } from './config'
|
import CONFIG, { requiredConfigs } from './config'
|
||||||
import middleware from './middleware'
|
import middleware from './middleware'
|
||||||
import { getDriver } from './bootstrap/neo4j'
|
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
|
||||||
import decode from './jwt/decode'
|
import decode from './jwt/decode'
|
||||||
import schema from './schema'
|
import schema from './schema'
|
||||||
|
|
||||||
@ -16,6 +16,7 @@ Object.entries(requiredConfigs).map(entry => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
const driver = getDriver()
|
const driver = getDriver()
|
||||||
|
const neode = getNeode()
|
||||||
|
|
||||||
const createServer = options => {
|
const createServer = options => {
|
||||||
const defaults = {
|
const defaults = {
|
||||||
@ -23,6 +24,7 @@ const createServer = options => {
|
|||||||
const user = await decode(driver, req.headers.authorization)
|
const user = await decode(driver, req.headers.authorization)
|
||||||
return {
|
return {
|
||||||
driver,
|
driver,
|
||||||
|
neode,
|
||||||
user,
|
user,
|
||||||
req,
|
req,
|
||||||
cypherParams: {
|
cypherParams: {
|
||||||
|
|||||||
@ -707,10 +707,18 @@
|
|||||||
pirates "^4.0.0"
|
pirates "^4.0.0"
|
||||||
source-map-support "^0.5.9"
|
source-map-support "^0.5.9"
|
||||||
|
|
||||||
"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4":
|
"@babel/runtime-corejs2@^7.5.5":
|
||||||
version "7.4.5"
|
version "7.5.5"
|
||||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.4.5.tgz#582bb531f5f9dc67d2fcb682979894f75e253f12"
|
resolved "https://registry.yarnpkg.com/@babel/runtime-corejs2/-/runtime-corejs2-7.5.5.tgz#c3214c08ef20341af4187f1c9fbdc357fbec96b2"
|
||||||
integrity sha512-TuI4qpWZP6lGOGIuGWtp9sPluqYICmbk8T/1vpSysqJxRPkudh/ofFWyqdcMsDf2s7KvDL4/YHgKyvcS3g9CJQ==
|
integrity sha512-FYATQVR00NSNi7mUfpPDp7E8RYMXDuO8gaix7u/w3GekfUinKgX1AcTxs7SoiEmoEW9mbpjrwqWSW6zCmw5h8A==
|
||||||
|
dependencies:
|
||||||
|
core-js "^2.6.5"
|
||||||
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
|
"@babel/runtime@^7.0.0", "@babel/runtime@^7.4.4", "@babel/runtime@^7.5.5":
|
||||||
|
version "7.5.5"
|
||||||
|
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.5.5.tgz#74fba56d35efbeca444091c7850ccd494fd2f132"
|
||||||
|
integrity sha512-28QvEGyQyNkB0/m2B4FU7IEZGK2NUrcMtT6BZEFALTguLk+AUT6ofsHtPk5QyjAdUkpMJ+/Em+quwz4HOt30AQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
regenerator-runtime "^0.13.2"
|
regenerator-runtime "^0.13.2"
|
||||||
|
|
||||||
@ -5638,7 +5646,7 @@ lodash.unescape@4.0.1:
|
|||||||
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
|
resolved "https://registry.yarnpkg.com/lodash.unescape/-/lodash.unescape-4.0.1.tgz#bf2249886ce514cda112fae9218cdc065211fc9c"
|
||||||
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
|
integrity sha1-vyJJiGzlFM2hEvrpIYzcBlIR/Jw=
|
||||||
|
|
||||||
lodash@4.17.15, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11, lodash@~4.17.14, lodash@~4.17.15:
|
lodash@4.17.15, lodash@^4.13.1, lodash@^4.15.0, lodash@^4.17.10, lodash@^4.17.11, lodash@^4.17.12, lodash@^4.17.13, lodash@^4.17.14, lodash@^4.17.15, lodash@^4.17.4, lodash@^4.17.5, lodash@~4.17.11, lodash@~4.17.14, lodash@~4.17.15:
|
||||||
version "4.17.15"
|
version "4.17.15"
|
||||||
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.15.tgz#b447f6670a0455bbfeedd11392eff330ea097548"
|
||||||
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
integrity sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A==
|
||||||
@ -6133,14 +6141,16 @@ neo4j-driver@^1.6.3, neo4j-driver@^1.7.3, neo4j-driver@~1.7.5:
|
|||||||
text-encoding-utf-8 "^1.0.2"
|
text-encoding-utf-8 "^1.0.2"
|
||||||
uri-js "^4.2.2"
|
uri-js "^4.2.2"
|
||||||
|
|
||||||
neo4j-graphql-js@^2.6.3:
|
neo4j-graphql-js@^2.7.0:
|
||||||
version "2.6.3"
|
version "2.7.0"
|
||||||
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.6.3.tgz#8f28c2479adda08c90abcc32a784587ef49b8b95"
|
resolved "https://registry.yarnpkg.com/neo4j-graphql-js/-/neo4j-graphql-js-2.7.0.tgz#4f3f94436c494b1488d914022e00155fb7a58112"
|
||||||
integrity sha512-WZdEqQ8EL9GOIB1ZccbLk1BZz5Dqdbk9i8BDXqxhp1SOI07P9y2cZ244f2Uz4zyES9AVXGmv+861N5xLhrSL2A==
|
integrity sha512-518T6KTDFs9qXa3i+uou7pUi0pBW+Yra2CR7cVcMeUpqJqh8CjxpGz8CJDX7K34MmlSZm3MlA4A1v4tAU1JePw==
|
||||||
dependencies:
|
dependencies:
|
||||||
|
"@babel/runtime" "^7.5.5"
|
||||||
|
"@babel/runtime-corejs2" "^7.5.5"
|
||||||
graphql "^14.2.1"
|
graphql "^14.2.1"
|
||||||
graphql-auth-directives "^2.1.0"
|
graphql-auth-directives "^2.1.0"
|
||||||
lodash "^4.17.11"
|
lodash "^4.17.15"
|
||||||
neo4j-driver "^1.7.3"
|
neo4j-driver "^1.7.3"
|
||||||
|
|
||||||
neode@^0.3.1:
|
neode@^0.3.1:
|
||||||
|
|||||||
@ -11,6 +11,13 @@ Then("I should have one post in the select dropdown", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Then("the search has no results", () => {
|
||||||
|
cy.get(".input .ds-select-dropdown").should($li => {
|
||||||
|
expect($li).to.have.length(1);
|
||||||
|
});
|
||||||
|
cy.get(".ds-select-dropdown").should("contain", 'Nothing found');
|
||||||
|
});
|
||||||
|
|
||||||
Then("I should see the following posts in the select dropdown:", table => {
|
Then("I should see the following posts in the select dropdown:", table => {
|
||||||
table.hashes().forEach(({ title }) => {
|
table.hashes().forEach(({ title }) => {
|
||||||
cy.get(".ds-select-dropdown").should("contain", title);
|
cy.get(".ds-select-dropdown").should("contain", title);
|
||||||
|
|||||||
@ -362,3 +362,96 @@ Then("the notification gets marked as read", () => {
|
|||||||
Then("there are no notifications in the top menu", () => {
|
Then("there are no notifications in the top menu", () => {
|
||||||
cy.get(".notifications-menu").should("contain", "0");
|
cy.get(".notifications-menu").should("contain", "0");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Given("there is an annoying user called {string}", (name) => {
|
||||||
|
const annoyingParams = {
|
||||||
|
email: 'spammy-spammer@example.org',
|
||||||
|
password: '1234',
|
||||||
|
}
|
||||||
|
cy.factory().create('User', {
|
||||||
|
...annoyingParams,
|
||||||
|
id: 'annoying-user',
|
||||||
|
name
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Given("I am on the profile page of the annoying user", (name) => {
|
||||||
|
cy.openPage('/profile/annoying-user/spammy-spammer');
|
||||||
|
})
|
||||||
|
|
||||||
|
When("I visit the profile page of the annoying user", (name) => {
|
||||||
|
cy.openPage('/profile/annoying-user');
|
||||||
|
})
|
||||||
|
|
||||||
|
When("I ", (name) => {
|
||||||
|
cy.openPage('/profile/annoying-user');
|
||||||
|
})
|
||||||
|
|
||||||
|
When("I click on {string} from the content menu in the user info box", (button) => {
|
||||||
|
cy.get('.user-content-menu .content-menu-trigger')
|
||||||
|
.click()
|
||||||
|
cy.get('.popover .ds-menu-item-link')
|
||||||
|
.contains(button)
|
||||||
|
.click()
|
||||||
|
})
|
||||||
|
|
||||||
|
When ("I navigate to my {string} settings page", (settingsPage) => {
|
||||||
|
cy.get(".avatar-menu").click();
|
||||||
|
cy.get(".avatar-menu-popover")
|
||||||
|
.find('a[href]').contains("Settings").click()
|
||||||
|
cy.contains('.ds-menu-item-link', settingsPage).click()
|
||||||
|
})
|
||||||
|
|
||||||
|
Given("I follow the user {string}", (name) => {
|
||||||
|
cy.neode()
|
||||||
|
.first('User', { name }).then((followed) => {
|
||||||
|
cy.neode()
|
||||||
|
.first('User', {name: narratorParams.name})
|
||||||
|
.relateTo(followed, 'following')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Given("\"Spammy Spammer\" wrote a post {string}", (title) => {
|
||||||
|
cy.factory()
|
||||||
|
.authenticateAs({
|
||||||
|
email: 'spammy-spammer@example.org',
|
||||||
|
password: '1234',
|
||||||
|
})
|
||||||
|
.create("Post", { title })
|
||||||
|
})
|
||||||
|
|
||||||
|
Then("the list of posts of this user is empty", () => {
|
||||||
|
cy.get('.ds-card-content').not('.post-link')
|
||||||
|
cy.get('.main-container').find('.ds-space.hc-empty')
|
||||||
|
})
|
||||||
|
|
||||||
|
Then("nobody is following the user profile anymore", () => {
|
||||||
|
cy.get('.ds-card-content').not('.post-link')
|
||||||
|
cy.get('.main-container').contains('.ds-card-content', 'is not followed by anyone')
|
||||||
|
})
|
||||||
|
|
||||||
|
Given("I wrote a post {string}", (title) => {
|
||||||
|
cy.factory()
|
||||||
|
.authenticateAs(loginCredentials)
|
||||||
|
.create("Post", { title })
|
||||||
|
})
|
||||||
|
|
||||||
|
When("I block the user {string}", (name) => {
|
||||||
|
cy.neode()
|
||||||
|
.first('User', { name }).then((blocked) => {
|
||||||
|
cy.neode()
|
||||||
|
.first('User', {name: narratorParams.name})
|
||||||
|
.relateTo(blocked, 'blocked')
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
When("I log in with:", (table) => {
|
||||||
|
const [firstRow] = table.hashes()
|
||||||
|
const { Email, Password } = firstRow
|
||||||
|
cy.login({email: Email, password: Password})
|
||||||
|
})
|
||||||
|
|
||||||
|
Then("I see only one post with the title {string}", (title) => {
|
||||||
|
cy.get('.main-container').find('.post-link').should('have.length', 1)
|
||||||
|
cy.get('.main-container').contains('.post-link', title)
|
||||||
|
})
|
||||||
|
|||||||
@ -0,0 +1,36 @@
|
|||||||
|
Feature: Block a User
|
||||||
|
As a user
|
||||||
|
I'd like to have a button to block another user
|
||||||
|
To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I have a user account
|
||||||
|
And there is an annoying user called "Spammy Spammer"
|
||||||
|
And I am logged in
|
||||||
|
|
||||||
|
Scenario: Block a user
|
||||||
|
Given I am on the profile page of the annoying user
|
||||||
|
When I click on "Block user" from the content menu in the user info box
|
||||||
|
And I navigate to my "Blocked users" settings page
|
||||||
|
Then I can see the following table:
|
||||||
|
| Avatar | Name |
|
||||||
|
| | Spammy Spammer |
|
||||||
|
|
||||||
|
Scenario: Block a previously followed user
|
||||||
|
Given I follow the user "Spammy Spammer"
|
||||||
|
And "Spammy Spammer" wrote a post "Spam Spam Spam"
|
||||||
|
When I visit the profile page of the annoying user
|
||||||
|
And I click on "Block user" from the content menu in the user info box
|
||||||
|
Then the list of posts of this user is empty
|
||||||
|
And nobody is following the user profile anymore
|
||||||
|
|
||||||
|
Scenario: Posts of blocked users are filtered from search results
|
||||||
|
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
|
||||||
|
When I search for "Spam"
|
||||||
|
Then I should see the following posts in the select dropdown:
|
||||||
|
| title |
|
||||||
|
| Spam Spam Spam |
|
||||||
|
When I block the user "Spammy Spammer"
|
||||||
|
And I refresh the page
|
||||||
|
And I search for "Spam"
|
||||||
|
Then the search has no results
|
||||||
@ -0,0 +1,22 @@
|
|||||||
|
Feature: Block a User
|
||||||
|
As a user
|
||||||
|
I'd like to have a button to block another user
|
||||||
|
To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
|
||||||
|
|
||||||
|
Background:
|
||||||
|
Given I have a user account
|
||||||
|
And there is an annoying user called "Spammy Spammer"
|
||||||
|
|
||||||
|
Scenario Outline: Blocked users cannot see each others posts
|
||||||
|
Given "Spammy Spammer" wrote a post "Spam Spam Spam"
|
||||||
|
And I wrote a post "I hate spammers"
|
||||||
|
And I block the user "Spammy Spammer"
|
||||||
|
When I log in with:
|
||||||
|
| Email | Password |
|
||||||
|
| <email> | <password> |
|
||||||
|
Then I see only one post with the title "<expected_title>"
|
||||||
|
Examples:
|
||||||
|
| email | password | expected_title |
|
||||||
|
| peterpan@example.org | 1234 | I hate spammers |
|
||||||
|
| spammy-spammer@example.org | 1234 | Spam Spam Spam |
|
||||||
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import Factory from '../../backend/src/seed/factories'
|
import Factory from '../../backend/src/seed/factories'
|
||||||
import { getDriver } from '../../backend/src/bootstrap/neo4j'
|
import { getDriver, neode as getNeode } from '../../backend/src/bootstrap/neo4j'
|
||||||
import setupNeode from '../../backend/src/bootstrap/neode'
|
import setupNeode from '../../backend/src/bootstrap/neode'
|
||||||
import neode from 'neode'
|
import neode from 'neode'
|
||||||
|
|
||||||
@ -16,6 +16,24 @@ beforeEach(async () => {
|
|||||||
await factory.cleanDatabase({ seedServerHost, neo4jDriver })
|
await factory.cleanDatabase({ seedServerHost, neo4jDriver })
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Cypress.Commands.add('neode', () => {
|
||||||
|
return setupNeode(neo4jConfigs)
|
||||||
|
})
|
||||||
|
Cypress.Commands.add(
|
||||||
|
'first',
|
||||||
|
{ prevSubject: true },
|
||||||
|
async (neode, model, properties) => {
|
||||||
|
return neode.first(model, properties)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
Cypress.Commands.add(
|
||||||
|
'relateTo',
|
||||||
|
{ prevSubject: true },
|
||||||
|
async (node, otherNode, relationship) => {
|
||||||
|
return node.relateTo(otherNode, relationship)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
Cypress.Commands.add('factory', () => {
|
Cypress.Commands.add('factory', () => {
|
||||||
return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) })
|
return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) })
|
||||||
})
|
})
|
||||||
|
|||||||
@ -43,11 +43,13 @@ c.updatedAt = category.updatedAt.`$date`
|
|||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-justforfun")
|
WHERE (c.icon = "categories-justforfun")
|
||||||
SET c.icon = 'smile'
|
SET c.icon = 'smile'
|
||||||
|
SET c.slug = 'just-for-fun'
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-luck")
|
WHERE (c.icon = "categories-luck")
|
||||||
SET c.icon = 'heart-o'
|
SET c.icon = 'heart-o'
|
||||||
|
SET c.slug = 'happiness-values'
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
@ -63,11 +65,14 @@ SET c.icon = 'tree'
|
|||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-animal-justice")
|
WHERE (c.icon = "categories-animal-justice")
|
||||||
SET c.icon = 'paw'
|
SET c.icon = 'paw'
|
||||||
|
SET c.slug = 'animal-protection'
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-human-rights")
|
WHERE (c.icon = "categories-human-rights")
|
||||||
SET c.icon = 'balance-scale'
|
SET c.icon = 'balance-scale'
|
||||||
|
SET c.slug = 'human-rights-justice'
|
||||||
|
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
@ -98,6 +103,7 @@ SET c.icon = 'flash'
|
|||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-internet")
|
WHERE (c.icon = "categories-internet")
|
||||||
SET c.icon = 'mouse-pointer'
|
SET c.icon = 'mouse-pointer'
|
||||||
|
SET c.slug = 'it-internet-data-privacy'
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
@ -108,6 +114,7 @@ SET c.icon = 'paint-brush'
|
|||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-freedom-of-speech")
|
WHERE (c.icon = "categories-freedom-of-speech")
|
||||||
SET c.icon = 'bullhorn'
|
SET c.icon = 'bullhorn'
|
||||||
|
SET c.slug = 'freedom-of-speech'
|
||||||
;
|
;
|
||||||
|
|
||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
@ -118,4 +125,5 @@ SET c.icon = 'shopping-cart'
|
|||||||
MATCH (c:Category)
|
MATCH (c:Category)
|
||||||
WHERE (c.icon = "categories-peace")
|
WHERE (c.icon = "categories-peace")
|
||||||
SET c.icon = 'angellist'
|
SET c.icon = 'angellist'
|
||||||
|
SET c.slug = 'global-peace-nonviolence'
|
||||||
;
|
;
|
||||||
|
|||||||
@ -117,7 +117,7 @@ MERGE (e:EmailAddress {
|
|||||||
verifiedAt: toString(datetime())
|
verifiedAt: toString(datetime())
|
||||||
})
|
})
|
||||||
MERGE (e)-[:BELONGS_TO]->(u)
|
MERGE (e)-[:BELONGS_TO]->(u)
|
||||||
MERGE (u)<-[:PRIMARY_EMAIL]-(e)
|
MERGE (u)-[:PRIMARY_EMAIL]->(e)
|
||||||
WITH u, user, user.badgeIds AS badgeIds
|
WITH u, user, user.badgeIds AS badgeIds
|
||||||
UNWIND badgeIds AS badgeId
|
UNWIND badgeIds AS badgeId
|
||||||
MATCH (b:Badge {id: badgeId})
|
MATCH (b:Badge {id: badgeId})
|
||||||
|
|||||||
@ -10,7 +10,7 @@
|
|||||||
:disabled="isDisabled(category.id)"
|
:disabled="isDisabled(category.id)"
|
||||||
>
|
>
|
||||||
<ds-icon :name="category.icon" />
|
<ds-icon :name="category.icon" />
|
||||||
{{ category.name }}
|
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||||
</ds-button>
|
</ds-button>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -122,13 +122,34 @@ export default {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.isOwner && this.resourceType === 'user') {
|
if (this.resourceType === 'user') {
|
||||||
routes.push({
|
if (this.isOwner) {
|
||||||
name: this.$t(`settings.name`),
|
routes.push({
|
||||||
path: '/settings',
|
name: this.$t(`settings.name`),
|
||||||
icon: 'edit',
|
path: '/settings',
|
||||||
})
|
icon: 'edit',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
if (this.resource.isBlocked) {
|
||||||
|
routes.push({
|
||||||
|
name: this.$t(`settings.blocked-users.unblock`),
|
||||||
|
callback: () => {
|
||||||
|
this.$emit('unblock', this.resource)
|
||||||
|
},
|
||||||
|
icon: 'user-plus',
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
routes.push({
|
||||||
|
name: this.$t(`settings.blocked-users.block`),
|
||||||
|
callback: () => {
|
||||||
|
this.$emit('block', this.resource)
|
||||||
|
},
|
||||||
|
icon: 'user-times',
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return routes
|
return routes
|
||||||
},
|
},
|
||||||
isModerator() {
|
isModerator() {
|
||||||
|
|||||||
@ -5,7 +5,7 @@
|
|||||||
<ds-heading tag="h4">{{ $t('filter-posts.categories.header') }}</ds-heading>
|
<ds-heading tag="h4">{{ $t('filter-posts.categories.header') }}</ds-heading>
|
||||||
<ds-space margin-bottom="large" />
|
<ds-space margin-bottom="large" />
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<ds-flex>
|
<ds-flex :gutter="{ lg: 'small' }">
|
||||||
<ds-flex-item
|
<ds-flex-item
|
||||||
:width="{ base: '100%', sm: '100%', md: '100%', lg: '5%' }"
|
:width="{ base: '100%', sm: '100%', md: '100%', lg: '5%' }"
|
||||||
class="categories-menu-item"
|
class="categories-menu-item"
|
||||||
@ -47,7 +47,9 @@
|
|||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-flex>
|
<ds-flex>
|
||||||
<ds-flex-item class="categories-menu-item">
|
<ds-flex-item class="categories-menu-item">
|
||||||
<label class="category-labels">{{ category.name }}</label>
|
<label class="category-labels">
|
||||||
|
{{ $t(`contribution.category.name.${category.slug}`) }}
|
||||||
|
</label>
|
||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
<ds-space margin-bottom="xx-large" />
|
<ds-space margin-bottom="xx-large" />
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
|
|||||||
@ -56,6 +56,7 @@ describe('CreateUserAccount', () => {
|
|||||||
wrapper.find('input#name').setValue('John Doe')
|
wrapper.find('input#name').setValue('John Doe')
|
||||||
wrapper.find('input#password').setValue('hellopassword')
|
wrapper.find('input#password').setValue('hellopassword')
|
||||||
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
|
wrapper.find('input#passwordConfirmation').setValue('hellopassword')
|
||||||
|
wrapper.find('input#checkbox').setChecked()
|
||||||
await wrapper.find('form').trigger('submit')
|
await wrapper.find('form').trigger('submit')
|
||||||
await wrapper.html()
|
await wrapper.html()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,71 +1,82 @@
|
|||||||
<template>
|
<template>
|
||||||
<ds-card v-if="success" class="success">
|
<ds-container width="small">
|
||||||
<ds-space>
|
<ds-card v-if="success" class="success">
|
||||||
<sweetalert-icon icon="success" />
|
<ds-space>
|
||||||
<ds-text align="center" bold color="success">
|
<sweetalert-icon icon="success" />
|
||||||
{{ $t('registration.create-user-account.success') }}
|
<ds-text align="center" bold color="success">
|
||||||
</ds-text>
|
{{ $t('registration.create-user-account.success') }}
|
||||||
</ds-space>
|
</ds-text>
|
||||||
</ds-card>
|
</ds-space>
|
||||||
<ds-form
|
</ds-card>
|
||||||
v-else
|
<ds-form
|
||||||
class="create-user-account"
|
v-else
|
||||||
v-model="formData"
|
class="create-user-account"
|
||||||
:schema="formSchema"
|
v-model="formData"
|
||||||
@submit="submit"
|
:schema="formSchema"
|
||||||
>
|
@submit="submit"
|
||||||
<template slot-scope="{ errors }">
|
>
|
||||||
<ds-card :header="$t('registration.create-user-account.title')">
|
<template slot-scope="{ errors }">
|
||||||
<ds-input
|
<ds-card :header="$t('registration.create-user-account.title')">
|
||||||
id="name"
|
<ds-input
|
||||||
model="name"
|
id="name"
|
||||||
icon="user"
|
model="name"
|
||||||
:label="$t('settings.data.labelName')"
|
icon="user"
|
||||||
:placeholder="$t('settings.data.namePlaceholder')"
|
:label="$t('settings.data.labelName')"
|
||||||
/>
|
:placeholder="$t('settings.data.namePlaceholder')"
|
||||||
<ds-input
|
/>
|
||||||
id="bio"
|
<ds-input
|
||||||
model="about"
|
id="bio"
|
||||||
type="textarea"
|
model="about"
|
||||||
rows="3"
|
type="textarea"
|
||||||
:label="$t('settings.data.labelBio')"
|
rows="3"
|
||||||
:placeholder="$t('settings.data.labelBio')"
|
:label="$t('settings.data.labelBio')"
|
||||||
/>
|
:placeholder="$t('settings.data.labelBio')"
|
||||||
<ds-input
|
/>
|
||||||
id="password"
|
<ds-input
|
||||||
model="password"
|
id="password"
|
||||||
type="password"
|
model="password"
|
||||||
autocomplete="off"
|
type="password"
|
||||||
:label="$t('settings.security.change-password.label-new-password')"
|
autocomplete="off"
|
||||||
/>
|
:label="$t('settings.security.change-password.label-new-password')"
|
||||||
<ds-input
|
/>
|
||||||
id="passwordConfirmation"
|
<ds-input
|
||||||
model="passwordConfirmation"
|
id="passwordConfirmation"
|
||||||
type="password"
|
model="passwordConfirmation"
|
||||||
autocomplete="off"
|
type="password"
|
||||||
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
autocomplete="off"
|
||||||
/>
|
:label="$t('settings.security.change-password.label-new-password-confirm')"
|
||||||
<password-strength :password="formData.password" />
|
/>
|
||||||
<template slot="footer">
|
<password-strength :password="formData.password" />
|
||||||
<ds-space class="backendErrors" v-if="backendErrors">
|
|
||||||
<ds-text align="center" bold color="danger">
|
<ds-text>
|
||||||
{{ backendErrors.message }}
|
<input
|
||||||
</ds-text>
|
id="checkbox"
|
||||||
</ds-space>
|
type="checkbox"
|
||||||
<ds-button
|
v-model="termsAndConditionsConfirmed"
|
||||||
style="float: right;"
|
:checked="termsAndConditionsConfirmed"
|
||||||
icon="check"
|
/>
|
||||||
type="submit"
|
<label for="checkbox" v-html="$t('site.termsAndConditionsConfirmed')"></label>
|
||||||
:loading="$apollo.loading"
|
</ds-text>
|
||||||
:disabled="errors"
|
|
||||||
primary
|
<template slot="footer">
|
||||||
>
|
<ds-space class="backendErrors" v-if="backendErrors">
|
||||||
{{ $t('actions.save') }}
|
<ds-text align="center" bold color="danger">{{ backendErrors.message }}</ds-text>
|
||||||
</ds-button>
|
</ds-space>
|
||||||
</template>
|
<ds-button
|
||||||
</ds-card>
|
style="float: right;"
|
||||||
</template>
|
icon="check"
|
||||||
</ds-form>
|
type="submit"
|
||||||
|
:loading="$apollo.loading"
|
||||||
|
:disabled="errors || !termsAndConditionsConfirmed"
|
||||||
|
primary
|
||||||
|
>
|
||||||
|
{{ $t('actions.save') }}
|
||||||
|
</ds-button>
|
||||||
|
</template>
|
||||||
|
</ds-card>
|
||||||
|
</template>
|
||||||
|
</ds-form>
|
||||||
|
</ds-container>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
@ -73,7 +84,6 @@ import gql from 'graphql-tag'
|
|||||||
import PasswordStrength from '../Password/Strength'
|
import PasswordStrength from '../Password/Strength'
|
||||||
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
import { SweetalertIcon } from 'vue-sweetalert-icons'
|
||||||
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
import PasswordForm from '~/components/utils/PasswordFormHelper'
|
||||||
|
|
||||||
export const SignupVerificationMutation = gql`
|
export const SignupVerificationMutation = gql`
|
||||||
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
|
mutation($nonce: String!, $name: String!, $email: String!, $password: String!) {
|
||||||
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
|
SignupVerification(nonce: $nonce, email: $email, name: $name, password: $password) {
|
||||||
@ -111,6 +121,10 @@ export default {
|
|||||||
disabled: true,
|
disabled: true,
|
||||||
success: null,
|
success: null,
|
||||||
backendErrors: null,
|
backendErrors: null,
|
||||||
|
// TODO: Our styleguide does not support checkmarks.
|
||||||
|
// Integrate termsAndConditionsConfirmed into `this.formData` once we
|
||||||
|
// have checkmarks available.
|
||||||
|
termsAndConditionsConfirmed: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@ -4,7 +4,7 @@ export default () => {
|
|||||||
return gql(`{
|
return gql(`{
|
||||||
Category {
|
Category {
|
||||||
id
|
id
|
||||||
name
|
slug
|
||||||
icon
|
icon
|
||||||
}
|
}
|
||||||
}`)
|
}`)
|
||||||
|
|||||||
@ -46,6 +46,7 @@ export default i18n => {
|
|||||||
}
|
}
|
||||||
followedByCount
|
followedByCount
|
||||||
followedByCurrentUser
|
followedByCurrentUser
|
||||||
|
isBlocked
|
||||||
followedBy(first: 7) {
|
followedBy(first: 7) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
|
|||||||
39
webapp/graphql/settings/BlockedUsers.js
Normal file
39
webapp/graphql/settings/BlockedUsers.js
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
import gql from 'graphql-tag'
|
||||||
|
|
||||||
|
export const BlockedUsers = () => {
|
||||||
|
return gql(`
|
||||||
|
{
|
||||||
|
blockedUsers {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
slug
|
||||||
|
avatar
|
||||||
|
about
|
||||||
|
disabled
|
||||||
|
deleted
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Block = () => {
|
||||||
|
return gql(`mutation($id:ID!) {
|
||||||
|
block(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isBlocked
|
||||||
|
followedByCurrentUser
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const Unblock = () => {
|
||||||
|
return gql(`mutation($id:ID!) {
|
||||||
|
unblock(id: $id) {
|
||||||
|
id
|
||||||
|
name
|
||||||
|
isBlocked
|
||||||
|
followedByCurrentUser
|
||||||
|
}
|
||||||
|
}`)
|
||||||
|
}
|
||||||
@ -30,7 +30,8 @@
|
|||||||
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
|
"responsible": "Verantwortlicher gemäß § 55 Abs. 2 RStV ",
|
||||||
"bank": "Bankverbindung",
|
"bank": "Bankverbindung",
|
||||||
"germany": "Deutschland",
|
"germany": "Deutschland",
|
||||||
"code-of-conduct": "Verhaltenscodex"
|
"code-of-conduct": "Verhaltenscodex",
|
||||||
|
"termsAndConditionsConfirmed": "Ich habe die <a href=\"/terms-and-conditions\" target=\"_blank\">Nutzungsbedingungen</a> durchgelesen und stimme ihnen zu."
|
||||||
},
|
},
|
||||||
"sorting": {
|
"sorting": {
|
||||||
"newest": "Neuste",
|
"newest": "Neuste",
|
||||||
@ -189,6 +190,25 @@
|
|||||||
"submit": "Link hinzufügen",
|
"submit": "Link hinzufügen",
|
||||||
"successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
|
"successAdd": "Social-Media hinzugefügt. Profil aktualisiert!",
|
||||||
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
|
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
|
||||||
|
},
|
||||||
|
"blocked-users": {
|
||||||
|
"name": "Blockierte Benutzer",
|
||||||
|
"explanation": {
|
||||||
|
"intro": "Wenn ein anderer Benutzer von dir blockiert wurde, dann passiert folgendes:",
|
||||||
|
"your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der blockierten Person mehr auf.",
|
||||||
|
"their-perspective": "Umgekehrt das gleiche: Die blockierte Person sieht deine Beiträge auch nicht mehr in ihrer Übersicht.",
|
||||||
|
"search": "Die Beiträge von blockierten Personen verschwinden aus deinen Suchergebnissen.",
|
||||||
|
"notifications": "Von dir blockierte Personen erhalten keine Benachrichtigungen mehr, wenn sie in deinen Beiträgen erwähnt werden.",
|
||||||
|
"closing": "Das sollte fürs Erste genügen, damit blockierte Benutzer dich nicht mehr länger belästigen können."
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"name": "Name",
|
||||||
|
"slug": "Alias"
|
||||||
|
},
|
||||||
|
"empty": "Bislang hast du niemanden blockiert.",
|
||||||
|
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
|
||||||
|
"block": "Nutzer blockieren",
|
||||||
|
"unblock": "Nutzer entblocken"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
@ -444,6 +464,26 @@
|
|||||||
"cry": "Zum Weinen",
|
"cry": "Zum Weinen",
|
||||||
"angry": "Verärgert",
|
"angry": "Verärgert",
|
||||||
"emoted": "angegeben"
|
"emoted": "angegeben"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": {
|
||||||
|
"freedom-of-speech": "Redefreiheit",
|
||||||
|
"consumption-sustainability": "Verbrauch & Nachhaltigkeit",
|
||||||
|
"global-peace-nonviolence": "Globaler Frieden & Gewaltlosigkeit",
|
||||||
|
"just-for-fun": "Nur zum Spaß",
|
||||||
|
"happiness-values": "Glück & Werte",
|
||||||
|
"health-wellbeing": "Gesundheit & Wohlbefinden",
|
||||||
|
"environment-nature": "Umwelt & Natur",
|
||||||
|
"animal-protection": "Schutz der Tiere",
|
||||||
|
"human-rights-justice": "Menschenrechte & Gerechtigkeit",
|
||||||
|
"education-sciences": "Bildung & Wissenschaft",
|
||||||
|
"cooperation-development": "Zusammenarbeit & Entwicklung",
|
||||||
|
"democracy-politics": "Demokratie & Politik",
|
||||||
|
"economy-finances": "Wirtschaft & Finanzen",
|
||||||
|
"energy-technology": "Energie & Technologie",
|
||||||
|
"it-internet-data-privacy": "IT, Internet & Datenschutz",
|
||||||
|
"art-culture-sport": "Kunst, Kultur & Sport"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changelog": {
|
"changelog": {
|
||||||
@ -483,7 +523,6 @@
|
|||||||
"title": "Zweck",
|
"title": "Zweck",
|
||||||
"description": "Mit diesen Verhaltensregeln regeln wir die wesentlichen Grundsätze für das Verhalten in unserem Sozialen Netzwerk. Dabei ist die Menschenrechtscharta der Vereinten Nationen unsere Orientierung und bildet das Herz unseres Werteverständnisses. Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an."
|
"description": "Mit diesen Verhaltensregeln regeln wir die wesentlichen Grundsätze für das Verhalten in unserem Sozialen Netzwerk. Dabei ist die Menschenrechtscharta der Vereinten Nationen unsere Orientierung und bildet das Herz unseres Werteverständnisses. Die Verhaltensregeln dienen als Leitsätze für den persönlichen Auftritt und den Umgang untereinander. Wer als Nutzer im Human Connection Netzwerk aktiv ist, Beiträge verfasst, kommentiert oder mit anderen Nutzern, auch außerhalb des Netzwerkes, Kontakt aufnimmt, erkennt diese Verhaltensregeln als verbindlich an."
|
||||||
},
|
},
|
||||||
|
|
||||||
"expected-behaviour": {
|
"expected-behaviour": {
|
||||||
"title": "Erwartetes Verhalten",
|
"title": "Erwartetes Verhalten",
|
||||||
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
"description": "Die folgenden Verhaltensweisen werden von allen Community-Mitgliedern erwartet und gefordert:",
|
||||||
|
|||||||
@ -30,7 +30,8 @@
|
|||||||
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
|
"responsible": "responsible for contents of this page (§ 55 Abs. 2 RStV)",
|
||||||
"bank": "bank account",
|
"bank": "bank account",
|
||||||
"germany": "Germany",
|
"germany": "Germany",
|
||||||
"code-of-conduct": "Code of Conduct"
|
"code-of-conduct": "Code of Conduct",
|
||||||
|
"termsAndConditionsConfirmed": "I have read and confirmed the <a href=\"/terms-and-conditions\" target=\"_blank\">terms and conditions</a>."
|
||||||
},
|
},
|
||||||
"sorting": {
|
"sorting": {
|
||||||
"newest": "Newest",
|
"newest": "Newest",
|
||||||
@ -189,6 +190,25 @@
|
|||||||
"submit": "Add link",
|
"submit": "Add link",
|
||||||
"successAdd": "Added social media. Updated user profile!",
|
"successAdd": "Added social media. Updated user profile!",
|
||||||
"successDelete": "Deleted social media. Updated user profile!"
|
"successDelete": "Deleted social media. Updated user profile!"
|
||||||
|
},
|
||||||
|
"blocked-users": {
|
||||||
|
"name": "Blocked users",
|
||||||
|
"explanation": {
|
||||||
|
"intro": "If another user has been blocked by you, this is what happens:",
|
||||||
|
"your-perspective": "The blocked person's posts will no longer appear in your news feed.",
|
||||||
|
"their-perspective": "Vice versa: The blocked person will also no longer see your posts in their news feed.",
|
||||||
|
"search": "Posts of blocked people disappear from your search results.",
|
||||||
|
"notifications": "Blocked users will no longer receive notifications if they are mentioned in your posts.",
|
||||||
|
"closing": "This should be sufficient for now so that blocked users can no longer bother you."
|
||||||
|
},
|
||||||
|
"columns": {
|
||||||
|
"name": "Name",
|
||||||
|
"slug": "Slug"
|
||||||
|
},
|
||||||
|
"empty": "So far, you have not blocked anybody.",
|
||||||
|
"how-to": "You can block other users on their profile page via the content menu.",
|
||||||
|
"block": "Block user",
|
||||||
|
"unblock": "Unblock user"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"admin": {
|
"admin": {
|
||||||
@ -444,6 +464,26 @@
|
|||||||
"cry": "Cry",
|
"cry": "Cry",
|
||||||
"angry": "Angry",
|
"angry": "Angry",
|
||||||
"emoted": "emoted"
|
"emoted": "emoted"
|
||||||
|
},
|
||||||
|
"category": {
|
||||||
|
"name": {
|
||||||
|
"freedom-of-speech": "Freedom of Speech",
|
||||||
|
"consumption-sustainability": "Consumption & Sustainability",
|
||||||
|
"global-peace-nonviolence": "Global Peace & Nonviolence",
|
||||||
|
"just-for-fun": "Just for Fun",
|
||||||
|
"happiness-values": "Happiness & Values",
|
||||||
|
"health-wellbeing": "Health & Wellbeing",
|
||||||
|
"environment-nature": "Environment & Nature",
|
||||||
|
"animal-protection": "Animal Protection",
|
||||||
|
"human-rights-justice": "Human Rights & Justice",
|
||||||
|
"education-sciences": "Education & Sciences",
|
||||||
|
"cooperation-development": "Cooperation & Development",
|
||||||
|
"democracy-politics": "Democracy & Politics",
|
||||||
|
"economy-finances": "Economy & Finances",
|
||||||
|
"energy-technology": "Energy & Technology",
|
||||||
|
"it-internet-data-privacy": "IT, Internet & Data Privacy",
|
||||||
|
"art-culture-sport": "Art, Culture, & Sport"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"changelog": {
|
"changelog": {
|
||||||
|
|||||||
@ -72,8 +72,8 @@
|
|||||||
"nuxt-env": "~0.1.0",
|
"nuxt-env": "~0.1.0",
|
||||||
"stack-utils": "^1.0.2",
|
"stack-utils": "^1.0.2",
|
||||||
"string-hash": "^1.1.3",
|
"string-hash": "^1.1.3",
|
||||||
"tiptap": "~1.24.0",
|
"tiptap": "~1.24.2",
|
||||||
"tiptap-extensions": "~1.26.1",
|
"tiptap-extensions": "~1.26.2",
|
||||||
"v-tooltip": "~2.0.2",
|
"v-tooltip": "~2.0.2",
|
||||||
"vue-count-to": "~1.0.13",
|
"vue-count-to": "~1.0.13",
|
||||||
"vue-izitoast": "roschaefer/vue-izitoast#patch-1",
|
"vue-izitoast": "roschaefer/vue-izitoast#patch-1",
|
||||||
|
|||||||
@ -22,6 +22,8 @@
|
|||||||
:resource="user"
|
:resource="user"
|
||||||
:is-owner="myProfile"
|
:is-owner="myProfile"
|
||||||
class="user-content-menu"
|
class="user-content-menu"
|
||||||
|
@block="block"
|
||||||
|
@unblock="unblock"
|
||||||
/>
|
/>
|
||||||
</no-ssr>
|
</no-ssr>
|
||||||
<ds-space margin="small">
|
<ds-space margin="small">
|
||||||
@ -54,13 +56,18 @@
|
|||||||
</ds-flex-item>
|
</ds-flex-item>
|
||||||
</ds-flex>
|
</ds-flex>
|
||||||
<ds-space margin="small">
|
<ds-space margin="small">
|
||||||
<hc-follow-button
|
<template v-if="!myProfile">
|
||||||
v-if="!myProfile"
|
<hc-follow-button
|
||||||
:follow-id="user.id"
|
v-if="!user.isBlocked"
|
||||||
:is-followed="user.followedByCurrentUser"
|
:follow-id="user.id"
|
||||||
@optimistic="follow => (user.followedByCurrentUser = follow)"
|
:is-followed="user.followedByCurrentUser"
|
||||||
@update="follow => fetchUser()"
|
@optimistic="follow => (user.followedByCurrentUser = follow)"
|
||||||
/>
|
@update="follow => fetchUser()"
|
||||||
|
/>
|
||||||
|
<ds-button v-else fullwidth @click="unblock(user)">
|
||||||
|
{{ $t('settings.blocked-users.unblock') }}
|
||||||
|
</ds-button>
|
||||||
|
</template>
|
||||||
</ds-space>
|
</ds-space>
|
||||||
<template v-if="user.about">
|
<template v-if="user.about">
|
||||||
<hr />
|
<hr />
|
||||||
@ -242,6 +249,7 @@ import HcUpload from '~/components/Upload'
|
|||||||
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||||
import PostQuery from '~/graphql/UserProfile/Post.js'
|
import PostQuery from '~/graphql/UserProfile/Post.js'
|
||||||
import UserQuery from '~/graphql/UserProfile/User.js'
|
import UserQuery from '~/graphql/UserProfile/User.js'
|
||||||
|
import { Block, Unblock } from '~/graphql/settings/BlockedUsers.js'
|
||||||
|
|
||||||
const tabToFilterMapping = ({ tab, id }) => {
|
const tabToFilterMapping = ({ tab, id }) => {
|
||||||
return {
|
return {
|
||||||
@ -372,6 +380,16 @@ export default {
|
|||||||
}
|
}
|
||||||
return this.uniq(this.Post.filter(post => !post.deleted))
|
return this.uniq(this.Post.filter(post => !post.deleted))
|
||||||
},
|
},
|
||||||
|
async block(user) {
|
||||||
|
await this.$apollo.mutate({ mutation: Block(), variables: { id: user.id } })
|
||||||
|
this.$apollo.queries.User.refetch()
|
||||||
|
this.$apollo.queries.Post.refetch()
|
||||||
|
},
|
||||||
|
async unblock(user) {
|
||||||
|
await this.$apollo.mutate({ mutation: Unblock(), variables: { id: user.id } })
|
||||||
|
this.$apollo.queries.User.refetch()
|
||||||
|
this.$apollo.queries.Post.refetch()
|
||||||
|
},
|
||||||
},
|
},
|
||||||
apollo: {
|
apollo: {
|
||||||
Post: {
|
Post: {
|
||||||
|
|||||||
@ -31,6 +31,10 @@ export default {
|
|||||||
name: this.$t('settings.social-media.name'),
|
name: this.$t('settings.social-media.name'),
|
||||||
path: `/settings/my-social-media`,
|
path: `/settings/my-social-media`,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
name: this.$t('settings.blocked-users.name'),
|
||||||
|
path: `/settings/blocked-users`,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
name: this.$t('settings.deleteUserAccount.name'),
|
name: this.$t('settings.deleteUserAccount.name'),
|
||||||
path: `/settings/delete-account`,
|
path: `/settings/delete-account`,
|
||||||
|
|||||||
108
webapp/pages/settings/blocked-users.vue
Normal file
108
webapp/pages/settings/blocked-users.vue
Normal file
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<ds-space>
|
||||||
|
<ds-card :header="$t('settings.blocked-users.name')">
|
||||||
|
<ds-text>
|
||||||
|
{{ $t('settings.blocked-users.explanation.intro') }}
|
||||||
|
</ds-text>
|
||||||
|
<ds-list>
|
||||||
|
<ds-list-item>
|
||||||
|
{{ $t('settings.blocked-users.explanation.your-perspective') }}
|
||||||
|
</ds-list-item>
|
||||||
|
<ds-list-item>
|
||||||
|
{{ $t('settings.blocked-users.explanation.their-perspective') }}
|
||||||
|
</ds-list-item>
|
||||||
|
<ds-list-item>
|
||||||
|
{{ $t('settings.blocked-users.explanation.search') }}
|
||||||
|
</ds-list-item>
|
||||||
|
<ds-list-item>
|
||||||
|
{{ $t('settings.blocked-users.explanation.notifications') }}
|
||||||
|
</ds-list-item>
|
||||||
|
</ds-list>
|
||||||
|
<ds-text>
|
||||||
|
{{ $t('settings.blocked-users.explanation.closing') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-card>
|
||||||
|
</ds-space>
|
||||||
|
<ds-card v-if="blockedUsers && blockedUsers.length">
|
||||||
|
<ds-table :data="blockedUsers" :fields="fields" condensed>
|
||||||
|
<template slot="avatar" slot-scope="scope">
|
||||||
|
<nuxt-link
|
||||||
|
:to="{
|
||||||
|
name: 'profile-id-slug',
|
||||||
|
params: { id: scope.row.id, slug: scope.row.slug },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<hc-avatar :user="scope.row" size="small" />
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template slot="name" slot-scope="scope">
|
||||||
|
<nuxt-link
|
||||||
|
:to="{
|
||||||
|
name: 'profile-id-slug',
|
||||||
|
params: { id: scope.row.id, slug: scope.row.slug },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<b>{{ scope.row.name | truncate(20) }}</b>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
<template slot="slug" slot-scope="scope">
|
||||||
|
<nuxt-link
|
||||||
|
:to="{
|
||||||
|
name: 'profile-id-slug',
|
||||||
|
params: { id: scope.row.id, slug: scope.row.slug },
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<b>{{ scope.row.slug | truncate(20) }}</b>
|
||||||
|
</nuxt-link>
|
||||||
|
</template>
|
||||||
|
</ds-table>
|
||||||
|
</ds-card>
|
||||||
|
<ds-card v-else>
|
||||||
|
<ds-space>
|
||||||
|
<ds-placeholder>
|
||||||
|
{{ $t('settings.blocked-users.empty') }}
|
||||||
|
</ds-placeholder>
|
||||||
|
</ds-space>
|
||||||
|
<ds-space>
|
||||||
|
<ds-text align="center">
|
||||||
|
{{ $t('settings.blocked-users.how-to') }}
|
||||||
|
</ds-text>
|
||||||
|
</ds-space>
|
||||||
|
</ds-card>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { BlockedUsers } from '~/graphql/settings/BlockedUsers'
|
||||||
|
import HcAvatar from '~/components/Avatar/Avatar.vue'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
components: {
|
||||||
|
HcAvatar,
|
||||||
|
},
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
blockedUsers: [],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fields() {
|
||||||
|
return {
|
||||||
|
avatar: '',
|
||||||
|
name: this.$t('settings.blocked-users.columns.name'),
|
||||||
|
slug: this.$t('settings.blocked-users.columns.slug'),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
apollo: {
|
||||||
|
blockedUsers: { query: BlockedUsers, fetchPolicy: 'cache-and-network' },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
.ds-table-col {
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -46,8 +46,8 @@ export const actions = {
|
|||||||
await this.app.apolloProvider.defaultClient
|
await this.app.apolloProvider.defaultClient
|
||||||
.query({
|
.query({
|
||||||
query: gql(`
|
query: gql(`
|
||||||
query findPosts($filter: String!) {
|
query findPosts($query: String!) {
|
||||||
findPosts(filter: $filter, limit: 10) {
|
findPosts(query: $query, limit: 10) {
|
||||||
id
|
id
|
||||||
slug
|
slug
|
||||||
label: title
|
label: title
|
||||||
@ -64,7 +64,7 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
`),
|
`),
|
||||||
variables: {
|
variables: {
|
||||||
filter: value.replace(/\s/g, '~ ') + '~',
|
query: value.replace(/\s/g, '~ ') + '~',
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
.then(res => {
|
.then(res => {
|
||||||
|
|||||||
@ -14184,10 +14184,10 @@ tiptap-commands@^1.10.12:
|
|||||||
prosemirror-utils "^0.9.6"
|
prosemirror-utils "^0.9.6"
|
||||||
tiptap-utils "^1.6.1"
|
tiptap-utils "^1.6.1"
|
||||||
|
|
||||||
tiptap-extensions@~1.26.1:
|
tiptap-extensions@~1.26.2:
|
||||||
version "1.26.1"
|
version "1.26.2"
|
||||||
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.26.1.tgz#f0fa492f268f73e4a4313de5de8d22799988ae39"
|
resolved "https://registry.yarnpkg.com/tiptap-extensions/-/tiptap-extensions-1.26.2.tgz#bf920a8c3586788f4c90690e4e6ee7e26bbc0390"
|
||||||
integrity sha512-Qzr63TOhZLLKw4A/F/EnxwWtdDELA0w6ogk/cKI+xmDUM14RN6VsVoz3QVZyuyzC/ceiQ6dV/iP67PJJ6kvKTw==
|
integrity sha512-UrpDkfZZXJkFd2VRUQAXhYb3M5c0c6JM4wT9yCXfFWLuzz7ifFNPuWnWK7FZOZ6QQpS27ZnYrpQQyQ63xeDSQg==
|
||||||
dependencies:
|
dependencies:
|
||||||
lowlight "^1.12.1"
|
lowlight "^1.12.1"
|
||||||
prosemirror-collab "^1.1.2"
|
prosemirror-collab "^1.1.2"
|
||||||
@ -14198,7 +14198,7 @@ tiptap-extensions@~1.26.1:
|
|||||||
prosemirror-transform "^1.1.3"
|
prosemirror-transform "^1.1.3"
|
||||||
prosemirror-utils "^0.9.6"
|
prosemirror-utils "^0.9.6"
|
||||||
prosemirror-view "^1.9.13"
|
prosemirror-view "^1.9.13"
|
||||||
tiptap "^1.24.1"
|
tiptap "^1.24.2"
|
||||||
tiptap-commands "^1.10.12"
|
tiptap-commands "^1.10.12"
|
||||||
|
|
||||||
tiptap-utils@^1.6.1:
|
tiptap-utils@^1.6.1:
|
||||||
@ -14211,10 +14211,10 @@ tiptap-utils@^1.6.1:
|
|||||||
prosemirror-tables "^0.9.1"
|
prosemirror-tables "^0.9.1"
|
||||||
prosemirror-utils "^0.9.6"
|
prosemirror-utils "^0.9.6"
|
||||||
|
|
||||||
tiptap@^1.24.1, tiptap@~1.24.0:
|
tiptap@^1.24.2, tiptap@~1.24.2:
|
||||||
version "1.24.1"
|
version "1.24.2"
|
||||||
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.24.1.tgz#ecbc4c42be37c26dd73bf3969af1fe20befad8b7"
|
resolved "https://registry.yarnpkg.com/tiptap/-/tiptap-1.24.2.tgz#bdaef96b4c3b6b5091da66ad8ebcd1f4515d1197"
|
||||||
integrity sha512-/4ABBDoEoDbFCHQQ65clEAjTxqR3DX1PQDsjp7LnYjPiVMGLL11NKnyT1kAn8QNaNGzV+uSSgkROBArcsiF5eA==
|
integrity sha512-KRfGmMdHdLmuBg1+nmlFtaqV27rjYrILAgIBcs+paopw2JDP7Hy71HdCJO80Jmr2O5q8dqE4AiMovb9aNrMZtg==
|
||||||
dependencies:
|
dependencies:
|
||||||
prosemirror-commands "^1.0.8"
|
prosemirror-commands "^1.0.8"
|
||||||
prosemirror-dropcursor "^1.1.1"
|
prosemirror-dropcursor "^1.1.1"
|
||||||
@ -14889,7 +14889,8 @@ vue-hot-reload-api@^2.3.0:
|
|||||||
|
|
||||||
vue-izitoast@roschaefer/vue-izitoast#patch-1:
|
vue-izitoast@roschaefer/vue-izitoast#patch-1:
|
||||||
version "1.1.2"
|
version "1.1.2"
|
||||||
resolved "https://codeload.github.com/roschaefer/vue-izitoast/tar.gz/c246fd78b1964c71b1889683379902d8d6284280"
|
uid ba6b03eb24c7c04c299e64a9703e101bf158ae50
|
||||||
|
resolved "https://codeload.github.com/roschaefer/vue-izitoast/tar.gz/ba6b03eb24c7c04c299e64a9703e101bf158ae50"
|
||||||
dependencies:
|
dependencies:
|
||||||
izitoast "^1.3.0"
|
izitoast "^1.3.0"
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user