Merge pull request #1209 from Human-Connection/1054-blocked-users

🍰 1054 blocked users
This commit is contained in:
mattwr18 2019-08-14 16:36:41 +02:00 committed by GitHub
commit ab6cd501fe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 1244 additions and 276 deletions

View File

@ -5,11 +5,12 @@ const notify = async (postId, idsOfMentionedUsers, context) => {
const session = context.driver.session()
const createdAt = new Date().toISOString()
const cypher = `
match(u:User) where u.id in $idsOfMentionedUsers
match(p:Post) where p.id = $postId
create(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
merge (n)-[:NOTIFIED]->(u)
merge (p)-[:NOTIFIED]->(n)
MATCH(p:Post {id: $postId})<-[:WROTE]-(author:User)
MATCH(u:User)
WHERE u.id in $idsOfMentionedUsers
AND NOT (u)<-[:BLOCKED]-(author)
CREATE(n:Notification{id: apoc.create.uuid(), read: false, createdAt: $createdAt})
MERGE (p)-[:NOTIFIED]->(n)-[:NOTIFIED]->(u)
`
await session.run(cypher, {
idsOfMentionedUsers,

View File

@ -1,12 +1,36 @@
import { GraphQLClient } from 'graphql-request'
import { host, login, gql } from '../../jest/helpers'
import { gql } from '../../jest/helpers'
import Factory from '../../seed/factories'
import { createTestClient } from 'apollo-server-testing'
import { neode, getDriver } from '../../bootstrap/neo4j'
import createServer from '../../server'
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 () => {
await factory.create('User', {
user = await instance.create('User', {
id: 'you',
name: 'Al Capone',
slug: 'al-capone',
@ -19,8 +43,8 @@ afterEach(async () => {
await factory.cleanDatabase()
})
describe('currentUser { notifications }', () => {
const query = gql`
describe('notifications', () => {
const notificationQuery = gql`
query($read: Boolean) {
currentUser {
notifications(read: $read, orderBy: createdAt_desc) {
@ -34,82 +58,60 @@ describe('currentUser { notifications }', () => {
`
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
authenticatedUser = user
})
describe('given another user', () => {
let authorClient
let authorParams
let authorHeaders
let author
beforeEach(async () => {
authorParams = {
author = await instance.create('User', {
email: 'author@example.org',
password: '1234',
id: 'author',
}
await factory.create('User', authorParams)
authorHeaders = await login(authorParams)
})
})
describe('who mentions me in a post', () => {
let post
const title = 'Mentioning Al Capone'
const content =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone">@al-capone</a> how do you do?'
beforeEach(async () => {
const createPostAction = async () => {
const createPostMutation = gql`
mutation($title: String!, $content: String!) {
CreatePost(title: $title, content: $content) {
mutation($id: ID, $title: String!, $content: String!) {
CreatePost(id: $id, title: $title, content: $content) {
id
title
content
}
}
`
authorClient = new GraphQLClient(host, {
headers: authorHeaders,
authenticatedUser = await author.toJson()
await mutate({
mutation: createPostMutation,
variables: { id: 'p47', title, content },
})
const { CreatePost } = await authorClient.request(createPostMutation, {
title,
content,
})
post = CreatePost
})
authenticatedUser = await user.toJson()
}
it('sends you a notification', async () => {
await createPostAction()
const expectedContent =
'Hey <a class="mention" data-mention-id="you" href="/profile/you/al-capone" target="_blank">@al-capone</a> how do you do?'
const expected = {
currentUser: {
notifications: [
{
read: false,
post: {
content: expectedContent,
},
},
],
const expected = expect.objectContaining({
data: {
currentUser: { notifications: [{ read: false, post: { content: expectedContent } }] },
},
}
})
const { query } = createTestClient(server)
await expect(
client.request(query, {
read: false,
}),
query({ query: notificationQuery, variables: { read: false } }),
).resolves.toEqual(expected)
})
describe('who mentions me many times', () => {
beforeEach(async () => {
const updatePostAction = async () => {
const updatedContent = `
One more mention to
<a data-mention-id="you" class="mention" href="/profile/you">
@ -132,41 +134,52 @@ describe('currentUser { notifications }', () => {
}
}
`
authorClient = new GraphQLClient(host, {
headers: authorHeaders,
authenticatedUser = await author.toJson()
await mutate({
mutation: updatePostMutation,
variables: {
id: 'p47',
title,
content: updatedContent,
},
})
await authorClient.request(updatePostMutation, {
id: post.id,
title: post.title,
content: updatedContent,
})
})
authenticatedUser = await user.toJson()
}
it('creates exactly one more notification', async () => {
await createPostAction()
await updatePostAction()
const expectedContent =
'<br>One more mention to<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again:<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>and again<br><a data-mention-id="you" class="mention" href="/profile/you" target="_blank"><br>@al-capone<br></a><br>'
const expected = {
currentUser: {
notifications: [
{
read: false,
post: {
content: expectedContent,
},
},
{
read: false,
post: {
content: expectedContent,
},
},
],
const expected = expect.objectContaining({
data: {
currentUser: {
notifications: [
{ read: false, post: { content: expectedContent } },
{ read: false, post: { content: expectedContent } },
],
},
},
}
})
await expect(
client.request(query, {
read: false,
}),
query({ query: notificationQuery, variables: { 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)
})
})
@ -204,46 +217,40 @@ describe('Hashtags', () => {
`
describe('authenticated', () => {
let headers
beforeEach(async () => {
headers = await login({
email: 'test@example.org',
password: '1234',
})
client = new GraphQLClient(host, {
headers,
})
authenticatedUser = await user.toJson()
})
describe('create a Post with Hashtags', () => {
beforeEach(async () => {
await client.request(createPostMutation, {
postId,
postTitle,
postContent,
await mutate({
mutation: createPostMutation,
variables: {
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 = [
{
id: 'Democracy',
name: 'Democracy',
},
{
id: 'Liberty',
name: 'Liberty',
},
{ id: 'Democracy', name: 'Democracy' },
{ id: 'Liberty', name: 'Liberty' },
]
await expect(
client.request(postWithHastagsQuery, postWithHastagsVariables),
).resolves.toEqual({
Post: [
{
tags: expect.arrayContaining(expected),
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }),
).resolves.toEqual(
expect.objectContaining({
data: {
Post: [
{
tags: expect.arrayContaining(expected),
},
],
},
],
})
}),
)
})
describe('afterwards update the Post by removing a Hashtag, leaving a Hashtag and add a Hashtag', () => {
@ -261,31 +268,28 @@ describe('Hashtags', () => {
`
it('only one previous Hashtag and the new Hashtag exists', async () => {
await client.request(updatePostMutation, {
postId,
postTitle,
updatedPostContent,
await mutate({
mutation: updatePostMutation,
variables: {
postId,
postTitle,
updatedPostContent,
},
})
const expected = [
{
id: 'Elections',
name: 'Elections',
},
{
id: 'Liberty',
name: 'Liberty',
},
{ id: 'Elections', name: 'Elections' },
{ id: 'Liberty', name: 'Liberty' },
]
await expect(
client.request(postWithHastagsQuery, postWithHastagsVariables),
).resolves.toEqual({
Post: [
{
tags: expect.arrayContaining(expected),
query({ query: postWithHastagsQuery, variables: postWithHastagsVariables }),
).resolves.toEqual(
expect.objectContaining({
data: {
Post: [{ tags: expect.arrayContaining(expected) }],
},
],
})
}),
)
})
})
})

View File

@ -3,11 +3,13 @@ import cheerio from 'cheerio'
export default function(content) {
if (!content) return []
const $ = cheerio.load(content)
let userIds = $('a.mention[data-mention-id]')
const userIds = $('a.mention[data-mention-id]')
.map((_, el) => {
return $(el).attr('data-mention-id')
})
.get()
userIds = userIds.map(id => id.trim()).filter(id => !!id)
return userIds
.map(id => id.trim())
.filter(id => !!id)
.filter((id, index, allIds) => allIds.indexOf(id) === index)
}

View File

@ -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>'
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>'
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('content undefined', () => {
@ -18,6 +20,10 @@ describe('extractMentionedUsers', () => {
expect(extractMentionedUsers(contentWithPlainLinks)).toEqual([])
})
it('removes duplicates', () => {
expect(extractMentionedUsers(contentWithDuplicateIds)).toEqual(['you'])
})
describe('given a link with .mention class and `data-mention-id` attribute ', () => {
it('extracts ids', () => {
expect(extractMentionedUsers(contentWithMentions)).toEqual(['u3'])

View File

@ -1,22 +1,29 @@
import { GraphQLClient } from 'graphql-request'
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 neode = getNeode()
const driver = getDriver()
const { server } = createServer({
context: () => {
return {
user: null,
neode,
driver,
}
},
})
const { query } = createTestClient(server)
beforeEach(async () => {
const userParams = { name: 'Author', email: 'author@example.org', password: '1234' }
await factory.create('User', userParams)
await factory.authenticateAs(userParams)
await factory.create('Post', { title: 'first' })
await factory.create('Post', { title: 'second' })
await factory.create('Post', { title: 'third' })
await factory.create('Post', { title: 'last' })
headers = {}
client = new GraphQLClient(host, { headers })
await neode.create('Post', { title: 'first' })
await neode.create('Post', { title: 'second' })
await neode.create('Post', { title: 'third' })
await neode.create('Post', { title: 'last' })
})
afterEach(async () => {
@ -25,10 +32,6 @@ afterEach(async () => {
describe('Query', () => {
describe('Post', () => {
beforeEach(() => {
query = '{ Post { title } }'
})
describe('orderBy', () => {
it('createdAt descending is default', async () => {
const posts = [
@ -37,15 +40,21 @@ describe('Query', () => {
{ title: 'second' },
{ title: 'first' },
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)
const expected = expect.objectContaining({ data: { Post: posts } })
await expect(
query({
query: gql`
{
Post {
title
}
}
`,
}),
).resolves.toEqual(expected)
})
describe('(orderBy: createdAt_asc)', () => {
beforeEach(() => {
query = '{ Post(orderBy: createdAt_asc) { title } }'
})
it('orders by createdAt ascending', async () => {
const posts = [
{ title: 'first' },
@ -53,8 +62,18 @@ describe('Query', () => {
{ title: 'third' },
{ title: 'last' },
]
const expected = { Post: posts }
await expect(client.request(query)).resolves.toEqual(expected)
const expected = expect.objectContaining({ data: { Post: posts } })
await expect(
query({
query: gql`
{
Post(orderBy: createdAt_asc) {
title
}
}
`,
}),
).resolves.toEqual(expected)
})
})
})

View File

@ -10,7 +10,7 @@ const instance = neode()
const isAuthenticated = rule({
cache: 'contextual',
})(async (_parent, _args, ctx, _info) => {
return ctx.user != null
return !!(ctx && ctx.user && ctx.user.id)
})
const isModerator = rule()(async (parent, args, { user }, info) => {
@ -159,6 +159,7 @@ const permissions = shield(
Badge: allow,
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: allow,
blockedUsers: isAuthenticated,
},
Mutation: {
'*': deny,
@ -195,6 +196,8 @@ const permissions = shield(
resetPassword: allow,
AddPostEmotions: isAuthenticated,
RemovePostEmotions: isAuthenticated,
block: isAuthenticated,
unblock: isAuthenticated,
},
User: {
email: isMyOwn,

View 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',
},
}

View File

@ -71,4 +71,16 @@ module.exports = {
eager: true,
cascade: true,
},
blocked: {
type: 'relationship',
relationship: 'BLOCKED',
target: 'User',
direction: 'out',
},
notifications: {
type: 'relationship',
relationship: 'NOTIFIED',
target: 'Notification',
direction: 'in',
},
}

View File

@ -7,4 +7,5 @@ export default {
EmailAddress: require('./EmailAddress.js'),
SocialMedia: require('./SocialMedia.js'),
Post: require('./Post.js'),
Notification: require('./Notification.js'),
}

View File

@ -15,10 +15,12 @@ export default function Resolver(type, options = {}) {
const {
idAttribute = 'id',
undefinedToNull = [],
boolean = {},
count = {},
hasOne = {},
hasMany = {},
} = options
const _hasResolver = (resolvers, { key, connection }, { returnType }) => {
return async (parent, params, context, resolveInfo) => {
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 resolvers = {}
for (const [key, connection] of Object.entries(obj)) {
@ -67,6 +89,7 @@ export default function Resolver(type, options = {}) {
}
const result = {
...undefinedToNullResolver(undefinedToNull),
...booleanResolver(boolean),
...countResolver(count),
...hasOneResolver(hasOne),
...hasManyResolver(hasMany),

View File

@ -1,7 +1,74 @@
import uuid from 'uuid/v4'
import { neo4jgraphql } from 'neo4j-graphql-js'
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 {
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: {
UpdatePost: async (object, params, context, resolveInfo) => {
const { categoryIds } = params
@ -112,39 +179,4 @@ export default {
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
},
},
}

View File

@ -460,6 +460,7 @@ describe('emotions', () => {
context: () => {
return {
user,
neode: instance,
driver,
}
},
@ -476,6 +477,7 @@ describe('emotions', () => {
context: () => {
return {
user,
neode: instance,
driver,
}
},

View File

@ -6,8 +6,45 @@ import Resolver from './helpers/Resolver'
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 {
Query: {
blockedUsers: async (object, args, context, resolveInfo) => {
try {
return getBlockedUsers(context)
} catch (e) {
throw new UserInputError(e.message)
}
},
User: async (object, args, context, resolveInfo) => {
const { email } = args
if (email) {
@ -20,6 +57,36 @@ export default {
},
},
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) => {
args = await fileUpload(args, { file: 'avatarUpload', url: 'avatar' })
try {
@ -73,6 +140,12 @@ export default {
'locationName',
'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: {
contributionsCount: '-[:WROTE]->(related:Post)',
friendsCount: '<-[:FRIENDS]->(related:User)',
@ -91,7 +164,6 @@ export default {
followedBy: '<-[:FOLLOWS]-(related:User)',
following: '-[:FOLLOWS]->(related:User)',
friends: '-[:FRIENDS]-(related:User)',
blacklisted: '-[:BLACKLISTED]->(related:User)',
socialMedia: '-[:OWNED_BY]->(related:SocialMedia',
contributions: '-[:WROTE]->(related:Post)',
comments: '-[:WROTE]->(related:Comment)',

View 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 } },
}),
)
})
})
})
})
})
})

View File

@ -4,15 +4,16 @@ type Query {
currentUser: User
# Get the latest Network Statistics
statistics: Statistics!
findPosts(filter: String!, limit: Int = 10): [Post]!
findPosts(query: String!, limit: Int = 10): [Post]!
@cypher(
statement: """
CALL db.index.fulltext.queryNodes('full_text_search', $filter)
CALL db.index.fulltext.queryNodes('full_text_search', $query)
YIELD node as post, score
MATCH (post)<-[:WROTE]-(user:User)
WHERE score >= 0.2
AND NOT user.deleted = true AND NOT user.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
LIMIT $limit
"""
@ -50,14 +51,6 @@ type Statistics {
countShouts: Int!
}
type Notification {
id: ID!
read: Boolean
user: User @relation(name: "NOTIFIED", direction: "OUT")
post: Post @relation(name: "NOTIFIED", direction: "IN")
createdAt: String
}
type Location {
id: ID!
name: String!

View 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
}

View File

@ -42,6 +42,12 @@ type User {
RETURN COUNT(u) >= 1
"""
)
isBlocked: Boolean! @cypher(
statement: """
MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
"""
)
#contributions: [WrittenPost]!
#contributions2(first: Int = 10, offset: Int = 0): [WrittenPost2]!
@ -67,8 +73,6 @@ type User {
organizationsCreated: [Organization] @relation(name: "CREATED_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")
badges: [Badge]! @relation(name: "REWARDED", direction: "IN")
@ -148,6 +152,8 @@ type Query {
orderBy: [_UserOrdering]
filter: _UserFilter
): [User]
blockedUsers: [User]
}
type Mutation {
@ -164,4 +170,8 @@ type Mutation {
): User
DeleteUser(id: ID!, resource: [Deletable]): User
block(id: ID!): User
unblock(id: ID!): User
}

View File

@ -120,60 +120,22 @@ import Factory from './factories'
bobDerBaumeister.relateTo(turtle, 'rewarded'),
jennyRostock.relateTo(bear, 'rewarded'),
dagobert.relateTo(rabbit, 'rewarded'),
])
await Promise.all([
f.relate('User', 'Friends', {
from: 'u1',
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',
}),
])
peterLustig.relateTo(bobDerBaumeister, 'friends'),
peterLustig.relateTo(jennyRostock, 'friends'),
bobDerBaumeister.relateTo(jennyRostock, 'friends'),
await Promise.all([
asAdmin.follow({
id: 'u3',
type: 'User',
}),
asModerator.follow({
id: 'u4',
type: 'User',
}),
asUser.follow({
id: 'u4',
type: 'User',
}),
asTick.follow({
id: 'u6',
type: 'User',
}),
asTrick.follow({
id: 'u4',
type: 'User',
}),
asTrack.follow({
id: 'u3',
type: 'User',
}),
peterLustig.relateTo(jennyRostock, 'following'),
peterLustig.relateTo(tick, 'following'),
bobDerBaumeister.relateTo(tick, 'following'),
jennyRostock.relateTo(tick, 'following'),
tick.relateTo(track, 'following'),
trick.relateTo(tick, 'following'),
track.relateTo(jennyRostock, 'following'),
dagobert.relateTo(tick, 'blocked'),
dagobert.relateTo(trick, 'blocked'),
dagobert.relateTo(track, 'blocked'),
])
await Promise.all([

View File

@ -3,7 +3,7 @@ import helmet from 'helmet'
import { ApolloServer } from 'apollo-server-express'
import CONFIG, { requiredConfigs } from './config'
import middleware from './middleware'
import { getDriver } from './bootstrap/neo4j'
import { neode as getNeode, getDriver } from './bootstrap/neo4j'
import decode from './jwt/decode'
import schema from './schema'
@ -16,6 +16,7 @@ Object.entries(requiredConfigs).map(entry => {
})
const driver = getDriver()
const neode = getNeode()
const createServer = options => {
const defaults = {
@ -23,6 +24,7 @@ const createServer = options => {
const user = await decode(driver, req.headers.authorization)
return {
driver,
neode,
user,
req,
cypherParams: {

View File

@ -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 => {
table.hashes().forEach(({ title }) => {
cy.get(".ds-select-dropdown").should("contain", title);

View File

@ -362,3 +362,96 @@ Then("the notification gets marked as read", () => {
Then("there are no notifications in the top menu", () => {
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)
})

View File

@ -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

View File

@ -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 |

View File

@ -1,5 +1,5 @@
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 neode from 'neode'
@ -16,6 +16,24 @@ beforeEach(async () => {
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', () => {
return Factory({ seedServerHost, neo4jDriver, neodeInstance: setupNeode(neo4jConfigs) })
})

View File

@ -122,13 +122,34 @@ export default {
}
}
if (this.isOwner && this.resourceType === 'user') {
routes.push({
name: this.$t(`settings.name`),
path: '/settings',
icon: 'edit',
})
if (this.resourceType === 'user') {
if (this.isOwner) {
routes.push({
name: this.$t(`settings.name`),
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
},
isModerator() {

View File

@ -46,6 +46,7 @@ export default i18n => {
}
followedByCount
followedByCurrentUser
isBlocked
followedBy(first: 7) {
id
slug

View 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
}
}`)
}

View File

@ -189,6 +189,25 @@
"submit": "Link hinzufügen",
"successAdd": "Social-Media hinzugefügt. 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": {

View File

@ -189,6 +189,25 @@
"submit": "Add link",
"successAdd": "Added 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": {

View File

@ -22,6 +22,8 @@
:resource="user"
:is-owner="myProfile"
class="user-content-menu"
@block="block"
@unblock="unblock"
/>
</no-ssr>
<ds-space margin="small">
@ -54,13 +56,18 @@
</ds-flex-item>
</ds-flex>
<ds-space margin="small">
<hc-follow-button
v-if="!myProfile"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="follow => (user.followedByCurrentUser = follow)"
@update="follow => fetchUser()"
/>
<template v-if="!myProfile">
<hc-follow-button
v-if="!user.isBlocked"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@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>
<template v-if="user.about">
<hr />
@ -242,6 +249,7 @@ import HcUpload from '~/components/Upload'
import HcAvatar from '~/components/Avatar/Avatar.vue'
import PostQuery from '~/graphql/UserProfile/Post.js'
import UserQuery from '~/graphql/UserProfile/User.js'
import { Block, Unblock } from '~/graphql/settings/BlockedUsers.js'
const tabToFilterMapping = ({ tab, id }) => {
return {
@ -372,6 +380,16 @@ export default {
}
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: {
Post: {

View File

@ -31,6 +31,10 @@ export default {
name: this.$t('settings.social-media.name'),
path: `/settings/my-social-media`,
},
{
name: this.$t('settings.blocked-users.name'),
path: `/settings/blocked-users`,
},
{
name: this.$t('settings.deleteUserAccount.name'),
path: `/settings/delete-account`,

View 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>

View File

@ -46,8 +46,8 @@ export const actions = {
await this.app.apolloProvider.defaultClient
.query({
query: gql(`
query findPosts($filter: String!) {
findPosts(filter: $filter, limit: 10) {
query findPosts($query: String!) {
findPosts(query: $query, limit: 10) {
id
slug
label: title
@ -64,7 +64,7 @@ export const actions = {
}
`),
variables: {
filter: value.replace(/\s/g, '~ ') + '~',
query: value.replace(/\s/g, '~ ') + '~',
},
})
.then(res => {