Merge pull request #2714 from Human-Connection/1724-block-users

feat: Blocked users cannot comment on posts
This commit is contained in:
mattwr18 2020-01-30 11:39:03 +01:00 committed by GitHub
commit b47f46157c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
24 changed files with 536 additions and 90 deletions

View File

@ -50,7 +50,7 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
MATCH (post: Post { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User)
WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (user)-[:BLOCKED]-(author)
MERGE (post)-[notification:NOTIFIED {reason: $reason}]->(user)
`
break
@ -60,8 +60,8 @@ const notifyUsersOfMention = async (label, id, idsOfUsers, reason, context) => {
MATCH (postAuthor: User)-[:WROTE]->(post: Post)<-[:COMMENTS]-(comment: Comment { id: $id })<-[:WROTE]-(author: User)
MATCH (user: User)
WHERE user.id in $idsOfUsers
AND NOT (user)<-[:BLOCKED]-(author)
AND NOT (user)<-[:BLOCKED]-(postAuthor)
AND NOT (user)-[:BLOCKED]-(author)
AND NOT (user)-[:BLOCKED]-(postAuthor)
MERGE (comment)-[notification:NOTIFIED {reason: $reason}]->(user)
`
break

View File

@ -102,6 +102,7 @@ export default shield(
PostsEmotionsCountByEmotion: allow,
PostsEmotionsByCurrentUser: isAuthenticated,
mutedUsers: isAuthenticated,
blockedUsers: isAuthenticated,
notifications: isAuthenticated,
Donations: isAuthenticated,
},
@ -139,6 +140,8 @@ export default shield(
RemovePostEmotions: isAuthenticated,
muteUser: isAuthenticated,
unmuteUser: isAuthenticated,
blockUser: isAuthenticated,
unblockUser: isAuthenticated,
markAsRead: isAuthenticated,
AddEmailAddress: isAuthenticated,
VerifyEmailAddress: isAuthenticated,

View File

@ -20,7 +20,7 @@ export default {
AND NOT (
author.deleted = true OR author.disabled = true
OR resource.deleted = true OR resource.disabled = true
OR (:User { id: $thisUserId })-[:BLOCKED]-(author)
OR (:User {id: $thisUserId})-[:MUTED]->(author)
)
WITH resource, author,
[(resource)<-[:COMMENTS]-(comment:Comment) | comment] as comments,
@ -40,8 +40,7 @@ export default {
YIELD node as resource, score
MATCH (resource)
WHERE score >= 0.5
AND NOT (resource.deleted = true OR resource.disabled = true
OR (:User { id: $thisUserId })-[:BLOCKED]-(resource))
AND NOT (resource.deleted = true OR resource.disabled = true)
RETURN resource {.*, __typename: labels(resource)[0]}
LIMIT $limit
`

View File

@ -23,6 +23,21 @@ export const getMutedUsers = async context => {
return mutedUsers
}
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 default {
Query: {
mutedUsers: async (object, args, context, resolveInfo) => {
@ -32,6 +47,13 @@ export default {
throw new UserInputError(e.message)
}
},
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) {
@ -86,7 +108,7 @@ export default {
const unmutedUser = await neode.find('User', params.id)
return unmutedUser.toJson()
},
block: async (object, args, context, resolveInfo) => {
blockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === args.id) return null
await neode.cypher(
@ -103,7 +125,7 @@ export default {
await user.relateTo(blockedUser, 'blocked')
return blockedUser.toJson()
},
unblock: async (object, args, context, resolveInfo) => {
unblockUser: async (object, args, context, resolveInfo) => {
const { user: currentUser } = context
if (currentUser.id === args.id) return null
await neode.cypher(
@ -229,7 +251,7 @@ export default {
boolean: {
followedByCurrentUser:
'MATCH (this)<-[:FOLLOWS]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isBlocked:
blocked:
'MATCH (this)<-[:BLOCKED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',
isMuted:
'MATCH (this)<-[:MUTED]-(u:User {id: $cypherParams.currentUserId}) RETURN COUNT(u) >= 1',

View File

@ -68,10 +68,11 @@ type User {
RETURN COUNT(u) >= 1
"""
)
isBlocked: Boolean! @cypher(
blocked: Boolean! @cypher(
statement: """
MATCH (this)<-[: BLOCKED]-(u: User { id: $cypherParams.currentUserId})
RETURN COUNT(u) >= 1
MATCH (this)-[:BLOCKED]-(user:User {id: $cypherParams.currentUserId})
RETURN COUNT(user) >= 1
"""
)
@ -207,6 +208,6 @@ type Mutation {
muteUser(id: ID!): User
unmuteUser(id: ID!): User
block(id: ID!): User
unblock(id: ID!): User
blockUser(id: ID!): User
unblockUser(id: ID!): User
}

View File

@ -11,15 +11,24 @@ Then("I should have one item in the select dropdown", () => {
});
});
Then("the search has no results", () => {
Then("the search should not contain posts by the annoying user", () => {
cy.get(".searchable-input .ds-select-dropdown").should($li => {
expect($li).to.have.length(1);
});
cy.get(".ds-select-dropdown").should("contain", 'Nothing found');
})
cy.get(".ds-select-dropdown")
.should("not.have.class", '.search-post')
.should("not.contain", 'Spam')
});
Then("the search should contain the annoying user", () => {
cy.get(".searchable-input .ds-select-dropdown").should($li => {
expect($li).to.have.length(1);
})
cy.get(".ds-select-dropdown .user-teaser .slug").should("contain", '@spammy-spammer');
cy.get(".searchable-input .ds-select-search")
.focus()
.type("{esc}");
});
})
Then("I should see the following posts in the select dropdown:", table => {
table.hashes().forEach(({ title }) => {

View File

@ -31,6 +31,7 @@ const narratorParams = {
const annoyingParams = {
email: "spammy-spammer@example.org",
slug: 'spammy-spammer',
password: "1234",
...termsAndConditionsAgreedVersion
};
@ -39,8 +40,12 @@ Given("I am logged in", () => {
cy.login(loginCredentials);
});
Given("I am logged in as the muted user", () => {
cy.login({ email: annoyingParams.email, password: '1234' });
Given("the {string} user searches for {string}", (_, postTitle) => {
cy.logout()
.login({ email: annoyingParams.email, password: '1234' })
.get(".searchable-input .ds-select-search")
.focus()
.type(postTitle);
});
Given("we have a selection of categories", () => {
@ -123,6 +128,12 @@ When("I visit the {string} page", page => {
cy.openPage(page);
});
When("a blocked user visits the post page of one of my authored posts", () => {
cy.logout()
.login({ email: annoyingParams.email, password: annoyingParams.password })
.openPage('/post/previously-created-post')
})
Given("I am on the {string} page", page => {
cy.openPage(page);
});
@ -486,7 +497,7 @@ Given("I follow the user {string}", name => {
});
});
Given('"Spammy Spammer" wrote a post {string}', title => {
Given('{string} wrote a post {string}', (_, title) => {
cy.createCategories("cat21")
.factory()
.create("Post", {
@ -501,7 +512,7 @@ Then("the list of posts of this user is empty", () => {
cy.get(".main-container").find(".ds-space.hc-empty");
});
Then("nobody is following the user profile anymore", () => {
Then("I get removed from his follower collection", () => {
cy.get(".ds-card-content").not(".post-link");
cy.get(".main-container").contains(
".ds-card-content",
@ -533,6 +544,20 @@ When("I mute the user {string}", name => {
});
});
When("I block the user {string}", name => {
cy.neode()
.first("User", {
name
})
.then(blockedUser => {
cy.neode()
.first("User", {
name: narratorParams.name
})
.relateTo(blockedUser, "blocked");
});
});
When("I log in with:", table => {
const [firstRow] = table.hashes();
const {
@ -551,3 +576,11 @@ Then("I see only one post with the title {string}", title => {
.should("have.length", 1);
cy.get(".main-container").contains(".post-link", title);
});
Then("they should not see the comment from", () => {
cy.get(".ds-card-footer").children().should('not.have.class', 'comment-form')
})
Then("they should see a text explaining commenting is not possible", () => {
cy.get('.ds-placeholder').should('contain', "Commenting is not possible at this time on this post.")
})

View File

@ -0,0 +1,46 @@
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
Background:
Given I have a user account
And there is an annoying user called "Harassing User"
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 |
| | Harassing User |
Scenario: Blocked user cannot interact with my contributions
Given I block the user "Harassing User"
And I previously created a post
And a blocked user visits the post page of one of my authored posts
Then they should not see the comment from
And they should see a text explaining commenting is not possible
Scenario: Block a previously followed user
Given I follow the user "Harassing User"
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
And I get removed from his follower collection
Scenario: Posts of blocked users are not filtered from search results
Given "Harassing User" wrote a post "You can still see my posts"
And I block the user "Harassing User"
When I search for "see"
Then I should see the following posts in the select dropdown:
| title |
| You can still see my posts |
Scenario: Blocked users can still see my posts
Given I previously created a post
And I block the user "Harassing User"
And the "blocked" user searches for "previously created"
Then I should see the following posts in the select dropdown:
| title |
| previously created post |

View File

@ -1,8 +1,7 @@
Feature: Mute a User
As a user
I'd like to have a button to mute another user
To prevent him from seeing and interacting with my contributions and also to avoid seeing his/her posts
To prevent him from seeing and interacting with my contributions
Background:
Given I have a user account
And there is an annoying user called "Spammy Spammer"
@ -22,9 +21,9 @@ Feature: Mute a User
When I visit the profile page of the annoying user
And I click on "Mute 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
And I get removed from his follower collection
Scenario: Posts of muted users are filtered from search results
Scenario: Posts of muted users are filtered from search results, users are not
Given we have the following posts in our database:
| id | title | content |
| im-not-muted | Post that should be seen | cause I'm not muted |
@ -36,18 +35,17 @@ Feature: Mute a User
When I mute the user "Spammy Spammer"
And I refresh the page
And I search for "Spam"
Then the search has no results
Then the search should not contain posts by the annoying user
But the search should contain the annoying user
But I search for "not muted"
Then I should see the following posts in the select dropdown:
| title |
| Post that should be seen |
Scenario: Muted users can still see my posts
Given I previously created a post
And I mute the user "Spammy Spammer"
Given I log out
And I am logged in as the muted user
When I search for "previously created"
And the "muted" user searches for "previously created"
Then I should see the following posts in the select dropdown:
| title |
| previously created post |

View File

@ -33,7 +33,7 @@
</client-only>
</ds-space>
<div v-if="openEditCommentMenu">
<hc-comment-form
<comment-form
:update="true"
:post="post"
:comment="comment"
@ -64,7 +64,7 @@ import { COMMENT_MAX_UNTRUNCATED_LENGTH, COMMENT_TRUNCATE_TO_LENGTH } from '~/co
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import ContentViewer from '~/components/Editor/ContentViewer'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentForm from '~/components/CommentForm/CommentForm'
import CommentMutations from '~/graphql/CommentMutations'
import scrollToAnchor from '~/mixins/scrollToAnchor.js'
@ -85,7 +85,7 @@ export default {
UserTeaser,
ContentMenu,
ContentViewer,
HcCommentForm,
CommentForm,
},
props: {
routeHash: { type: String, default: () => '' },

View File

@ -161,7 +161,7 @@ export default {
callback: () => {
this.$emit('unmute', this.resource)
},
icon: 'user-plus',
icon: 'eye',
})
} else {
routes.push({
@ -169,6 +169,23 @@ export default {
callback: () => {
this.$emit('mute', this.resource)
},
icon: 'eye-slash',
})
}
if (this.resource.blocked) {
routes.push({
label: this.$t(`settings.blocked-users.unblock`),
callback: () => {
this.$emit('unblock', this.resource)
},
icon: 'user-plus',
})
} else {
routes.push({
label: this.$t(`settings.blocked-users.block`),
callback: () => {
this.$emit('block', this.resource)
},
icon: 'user-times',
})
}

View File

@ -71,7 +71,7 @@ export default {
},
computed: {
emptyText() {
return this.isActive && !this.pending ? this.$t('search.failed') : this.$t('search.hint')
return this.isActive && !this.loading ? this.$t('search.failed') : this.$t('search.hint')
},
isActive() {
return !isEmpty(this.previousSearchTerm)
@ -104,7 +104,7 @@ export default {
*/
onEnter(event) {
clearTimeout(this.searchProcess)
if (!this.pending) {
if (!this.loading) {
this.previousSearchTerm = this.unprocessedSearchInput
this.$emit('query', this.unprocessedSearchInput)
}

View File

@ -29,6 +29,7 @@ export default i18n => {
...user
...userCounts
...locationAndBadges
blocked
}
comments(orderBy: createdAt_asc) {
...comment

View File

@ -24,6 +24,7 @@ export default i18n => {
createdAt
followedByCurrentUser
isMuted
blocked
following(first: 7) {
...user
...userCounts

View File

@ -0,0 +1,43 @@
import gql from 'graphql-tag'
export const blockedUsers = () => {
return gql`
{
blockedUsers {
id
name
slug
avatar
about
disabled
deleted
}
}
`
}
export const blockUser = () => {
return gql`
mutation($id: ID!) {
blockUser(id: $id) {
id
name
blocked
followedByCurrentUser
}
}
`
}
export const unblockUser = () => {
return gql`
mutation($id: ID!) {
unblockUser(id: $id) {
id
name
blocked
followedByCurrentUser
}
}
`
}

View File

@ -70,6 +70,11 @@
"passwordStrength4": "Sehr sicheres Passwort"
}
},
"privacy": {
"name": "Privatsphäre",
"make-shouts-public": "Teile von mir empfohlene Artikel öffentlich auf meinem Profil",
"success-update": "Privatsphäre-Einstellungen gespeichert"
},
"invites": {
"name": "Einladungen"
},
@ -143,27 +148,44 @@
"successDelete": "Social-Media gelöscht. Profil aktualisiert!"
},
"muted-users": {
"name": "Stummgeschaltete Benutzer",
"explanation": {
"intro": "Wenn ein anderer Benutzer von dir stummgeschaltet wurde, dann passiert folgendes:",
"your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der stummgeschalteten Person mehr auf.",
"search": "Die Beiträge von stummgeschalteten Personen verschwinden aus deinen Suchergebnissen."
},
"columns": {
"name": "Name",
"slug": "Alias",
"unmute": "Entsperren"
},
"empty": "Bislang hast du niemanden stummgeschaltet.",
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü stummschalten.",
"mute": "Stumm schalten",
"unmute": "Stummschaltung aufheben",
"unmuted": "{name} ist nicht mehr stummgeschaltet"
"name": "Stummgeschaltete Benutzer",
"explanation": {
"intro": "Wenn ein anderer Benutzer von dir stummgeschaltet wurde, dann passiert folgendes:",
"your-perspective": "In deiner Beitragsübersicht tauchen keine Beiträge der stummgeschalteten Person mehr auf.",
"search": "Die Beiträge von stummgeschalteten Personen verschwinden aus deinen Suchergebnissen."
},
"columns": {
"name": "Name",
"slug": "Alias",
"unmute": "Entsperren"
},
"empty": "Bislang hast du niemanden stummgeschaltet.",
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü stummschalten.",
"mute": "Stumm schalten",
"unmute": "Stummschaltung aufheben",
"unmuted": "{name} ist nicht mehr stummgeschaltet"
},
"privacy": {
"name": "Privatsphäre",
"make-shouts-public": "Teile von mir empfohlene Artikel öffentlich auf meinem Profil",
"success-update": "Privatsphäre-Einstellungen gespeichert"
"blocked-users": {
"name": "Blocked users",
"explanation": {
"intro": "Wenn ein anderer Benutzer von dir blockiert wurde, dann passiert folgendes:",
"your-perspective": "Du kannst keine Beiträge der blockierten Person mehr kommentieren.",
"their-perspective": "Die blockierte Person kann deine Beiträge nicht mehr kommentieren",
"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.",
"commenting-disabled": "Du kannst den Beitrag derzeit nicht kommentieren.",
"commenting-explanation": "Dafür kann es mehrere Gründe geben, bitte schau in unsere "
},
"columns": {
"name": "Name",
"slug": "Alias",
"unblock": "Entsperren"
},
"empty": "Bislang hast du niemanden blockiert.",
"how-to": "Du kannst andere Benutzer auf deren Profilseite über das Inhaltsmenü blockieren.",
"block": "Nutzer blockieren",
"unblock": "Nutzer entsperren",
"unblocked": "{name} ist wieder entsperrt"
}
},
"admin": {

View File

@ -329,6 +329,28 @@
"mute": "Mute user",
"unmute": "Unmute user",
"unmuted": "{name} is unmuted again"
},
"blocked-users": {
"name": "Blocked users",
"explanation": {
"intro": "If another user has been blocked by you, this is what happens:",
"your-perspective": "You will no longer be able to interact with their contributions.",
"their-perspective": "Vice versa: The blocked person will also no longer be able to interact with your contributions.",
"notifications": "Blocked users will no longer receive notifications if they mention each other.",
"closing": "This should be sufficient for now so that blocked users can no longer bother you.",
"commenting-disabled": "Commenting is not possible at this time on this post.",
"commenting-explanation": "This can happen for several reasons, please see our "
},
"columns": {
"name": "Name",
"slug": "Slug",
"unblock": "Unblock"
},
"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",
"unblocked": "{name} is unblocked again"
}
},
"admin": {

View File

@ -90,7 +90,17 @@
@toggleNewCommentForm="toggleNewCommentForm"
/>
<ds-space margin-bottom="large" />
<hc-comment-form v-if="showNewCommentForm" :post="post" @createComment="createComment" />
<comment-form
v-if="showNewCommentForm && !post.author.blocked"
:post="post"
@createComment="createComment"
/>
<ds-placeholder v-else>
{{ $t('settings.blocked-users.explanation.commenting-disabled') }}
<br />
{{ $t('settings.blocked-users.explanation.commenting-explanation') }}
<a href="https://support.human-connection.org/kb/" target="_blank">FAQ</a>
</ds-placeholder>
</ds-section>
</ds-card>
</transition>
@ -103,7 +113,7 @@ import HcHashtag from '~/components/Hashtag/Hashtag'
import ContentMenu from '~/components/ContentMenu/ContentMenu'
import UserTeaser from '~/components/UserTeaser/UserTeaser'
import HcShoutButton from '~/components/ShoutButton.vue'
import HcCommentForm from '~/components/CommentForm/CommentForm'
import CommentForm from '~/components/CommentForm/CommentForm'
import HcCommentList from '~/components/CommentList/CommentList'
import { postMenuModalsData, deletePostMutation } from '~/components/utils/PostHelpers'
import PostQuery from '~/graphql/PostQuery'
@ -122,7 +132,7 @@ export default {
UserTeaser,
HcShoutButton,
ContentMenu,
HcCommentForm,
CommentForm,
HcCommentList,
HcEmotions,
ContentViewer,
@ -139,15 +149,10 @@ export default {
title: 'loading',
showNewCommentForm: true,
blurred: false,
blocked: null,
postAuthor: null,
}
},
watch: {
Post(post) {
this.post = post[0] || {}
this.title = this.post.title
this.blurred = this.post.imageBlurred
},
},
mounted() {
setTimeout(() => {
// NOTE: quick fix for jumping flexbox implementation
@ -216,6 +221,12 @@ export default {
id: this.$route.params.id,
}
},
update({ Post }) {
this.post = Post[0] || {}
this.title = this.post.title
this.blurred = this.post.imageBlurred
this.postAuthor = this.post.author
},
fetchPolicy: 'cache-and-network',
},
},

View File

@ -24,6 +24,8 @@
class="user-content-menu"
@mute="muteUser"
@unmute="unmuteUser"
@block="blockUser"
@unblock="unblockUser"
/>
</client-only>
<ds-space margin="small">
@ -64,20 +66,21 @@
</client-only>
</ds-flex-item>
</ds-flex>
<ds-space margin="small">
<template v-if="!myProfile">
<hc-follow-button
v-if="!user.isMuted"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="optimisticFollow"
@update="updateFollow"
/>
<base-button v-else @click="unmuteUser(user)" class="unblock-user-button">
{{ $t('settings.muted-users.unmute') }}
</base-button>
</template>
</ds-space>
<div v-if="!myProfile" class="action-buttons">
<base-button v-if="user.blocked" @click="unblockUser(user)">
{{ $t('settings.blocked-users.unblock') }}
</base-button>
<base-button v-if="user.isMuted" @click="unmuteUser(user)">
{{ $t('settings.muted-users.unmute') }}
</base-button>
<hc-follow-button
v-if="!(user.blocked || user.isMuted)"
:follow-id="user.id"
:is-followed="user.followedByCurrentUser"
@optimistic="optimisticFollow"
@update="updateFollow"
/>
</div>
<template v-if="user.about">
<hr />
<ds-space margin-top="small" margin-bottom="small">
@ -285,6 +288,7 @@ import MasonryGridItem from '~/components/MasonryGrid/MasonryGridItem.vue'
import { profilePagePosts } from '~/graphql/PostQuery'
import UserQuery from '~/graphql/User'
import { muteUser, unmuteUser } from '~/graphql/settings/MutedUsers'
import { blockUser, unblockUser } from '~/graphql/settings/BlockedUsers'
import PostMutations from '~/graphql/PostMutations'
import UpdateQuery from '~/components/utils/UpdateQuery'
@ -417,6 +421,24 @@ export default {
this.$apollo.queries.profilePagePosts.refetch()
}
},
async blockUser(user) {
try {
await this.$apollo.mutate({ mutation: blockUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
}
},
async unblockUser(user) {
try {
this.$apollo.mutate({ mutation: unblockUser(), variables: { id: user.id } })
} catch (error) {
this.$toast.error(error.message)
} finally {
this.$apollo.queries.User.refetch()
}
},
pinPost(post) {
this.$apollo
.mutate({
@ -559,8 +581,13 @@ export default {
.profile-post-add-button {
box-shadow: $box-shadow-x-large;
}
.unblock-user-button {
display: block;
width: 100%;
.action-buttons {
margin: $space-small 0;
> .base-button {
display: block;
width: 100%;
margin-bottom: $space-x-small;
}
}
</style>

View File

@ -43,6 +43,10 @@ export default {
name: this.$t('settings.muted-users.name'),
path: `/settings/muted-users`,
},
{
name: this.$t('settings.blocked-users.name'),
path: `/settings/blocked-users`,
},
{
name: this.$t('settings.embeds.name'),
path: `/settings/embeds`,

View File

@ -0,0 +1,69 @@
import { config, mount, createLocalVue } from '@vue/test-utils'
import BlockedUsers from './blocked-users.vue'
import Styleguide from '@human-connection/styleguide'
import Filters from '~/plugins/vue-filters'
import { unblockUser } from '~/graphql/settings/BlockedUsers'
const localVue = createLocalVue()
localVue.use(Styleguide)
localVue.use(Filters)
config.stubs['nuxt-link'] = '<span><slot /></span>'
describe('blocked-users.vue', () => {
let wrapper
let mocks
beforeEach(() => {
mocks = {
$t: jest.fn(),
$apollo: {
mutate: jest.fn(),
queries: {
blockedUsers: {
refetch: jest.fn(),
},
},
},
$toast: {
error: jest.fn(),
success: jest.fn(),
},
}
})
describe('mount', () => {
const Wrapper = () => {
return mount(BlockedUsers, { mocks, localVue })
}
beforeEach(() => {
wrapper = Wrapper()
})
it('renders', () => {
expect(wrapper.is('div')).toBe(true)
})
describe('given a list of blocked users', () => {
beforeEach(() => {
const blockedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe', avatar: '' }]
wrapper.setData({ blockedUsers })
})
describe('click unblock', () => {
beforeEach(() => {
wrapper.find('.base-button').trigger('click')
})
it('calls unblock mutation with given user', () => {
expect(mocks.$apollo.mutate).toHaveBeenCalledWith({
mutation: unblockUser(),
variables: { id: 'u1' },
})
})
})
})
})
})

View File

@ -0,0 +1,118 @@
<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.notifications') }}
</ds-list-item>
</ds-list>
</ds-card>
</ds-space>
<ds-card v-if="blockedUsers && blockedUsers.length">
<ds-table :data="blockedUsers" :fields="fields" condensed>
<template #avatar="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
params: { id: scope.row.id, slug: scope.row.slug },
}"
>
<user-avatar :user="scope.row" size="small" />
</nuxt-link>
</template>
<template #name="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 #slug="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>
<template #unblockUser="scope">
<base-button circle size="small" @click="unblockUser(scope)" icon="user-plus" />
</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, unblockUser } from '~/graphql/settings/BlockedUsers'
import UserAvatar from '~/components/_new/generic/UserAvatar/UserAvatar'
export default {
components: {
UserAvatar,
},
data() {
return {
blockedUsers: [],
}
},
computed: {
fields() {
return {
avatar: '',
name: this.$t('settings.blocked-users.columns.name'),
slug: this.$t('settings.blocked-users.columns.slug'),
unblockUser: this.$t('settings.blocked-users.columns.unblock'),
}
},
},
apollo: {
blockedUsers: { query: blockedUsers, fetchPolicy: 'cache-and-network' },
},
methods: {
async unblockUser(user) {
await this.$apollo.mutate({
mutation: unblockUser(),
variables: { id: user.row.id },
})
this.$apollo.queries.blockedUsers.refetch()
const { name } = user.row
this.$toast.success(this.$t('settings.blocked-user.unblocked', { name }))
},
},
}
</script>
<style lang="scss">
.ds-table-col {
vertical-align: middle;
}
</style>

View File

@ -46,7 +46,7 @@ describe('muted-users.vue', () => {
expect(wrapper.is('div')).toBe(true)
})
describe('given a list of blocked users', () => {
describe('given a list of muted users', () => {
beforeEach(() => {
const mutedUsers = [{ id: 'u1', name: 'John Doe', slug: 'john-doe', avatar: '' }]
wrapper.setData({ mutedUsers })
@ -54,7 +54,7 @@ describe('muted-users.vue', () => {
describe('click unmute', () => {
beforeEach(() => {
wrapper.find('button').trigger('click')
wrapper.find('.base-button').trigger('click')
})
it('calls unmute mutation with given user', () => {

View File

@ -17,7 +17,7 @@
</ds-space>
<ds-card v-if="mutedUsers && mutedUsers.length">
<ds-table :data="mutedUsers" :fields="fields" condensed>
<template slot="avatar" slot-scope="scope">
<template #avatar="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
@ -27,7 +27,7 @@
<user-avatar :user="scope.row" size="small" />
</nuxt-link>
</template>
<template slot="name" slot-scope="scope">
<template #name="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
@ -37,7 +37,7 @@
<b>{{ scope.row.name | truncate(20) }}</b>
</nuxt-link>
</template>
<template slot="slug" slot-scope="scope">
<template #slug="scope">
<nuxt-link
:to="{
name: 'profile-id-slug',
@ -48,7 +48,7 @@
</nuxt-link>
</template>
<template slot="unmuteUser" slot-scope="scope">
<template #unmuteUser="scope">
<base-button circle size="small" @click="unmuteUser(scope)" icon="user-plus" />
</template>
</ds-table>