diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index e0926678b..ae1150aa6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -64,7 +64,7 @@ Regular pair programming sessions * we team up and work on an issue together (often using Visual Studio live sharing sessions) Open-Source Community Meeting -* every Thursday 13:00 +* bi-weekly on Mondays 13:00 (when there is no sprint retrospective) * the link will be posted in the [discord chat](https://discord.gg/6ub73U3) and on the [Agile Ventures website](https://www.agileventures.org/events?utf8=%E2%9C%93&project_id=220&commit=Filter+by+Project) * all contributors welcome! @@ -99,3 +99,34 @@ We believe in open source contributions as a learning experience – everyone is We use pair programming sessions as a tool for knowledge sharing. We can learn a lot from each other and only by sharing what we know and overcoming challenges together can we grow as a team and truly own this project collectively. As a volunteeer you have no commitment except your own self development and your awesomeness by contributing to this free and open-source software project. Cheers to you! + + +## Open-Source Bounties + +There are so many good reasons to contribute to Human Connection +* You learn state-of-the-art technologies +* You build your portfolio +* You contribute to a good cause + +Now there is one more good reason: You can receive a small fincancial +compensation for your contribution! :tada: + +### How it works + +Before you can benefit from the Open-Source bounty program you **must get one +pull request approved and merged for free**. You can choose something really +quick and easy. What's important is starting a working relationship with the +team, learning the workflow, and understanding this contribution guide. You can +filter issues by 'good first issue', to get an idea where to start. Please join +our our [community chat](https://human-connection.org/discord), too. + +You can filter Github issues with label [bounty](https://github.com/Human-Connection/Human-Connection/issues?q=is%3Aopen+is%3Aissue+label%3Abounty). These issues should have a second label `€` +which indicate their respective financial compensation in Euros. + +You can bill us after your pull request got approved and merged into `master`. +Payment methods are up to you: Bank transfer or PayPal is fine for us. Just send +us your invoice as .pdf file attached to an E-Mail once you are done. + +Our Open-Source bounty program is a work-in-progress. Based on our future +experience we will make changes and improvements. So keep an eye on this +contribution guide. diff --git a/README.md b/README.md index eaf71acb8..998f722f0 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,10 @@ Check out the [contribution guideline](./CONTRIBUTING.md), too! [![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/0)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/0)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/1)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/1)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/2)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/2)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/3)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/3)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/4)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/4)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/5)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/5)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/6)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/6)[![](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/images/7)](https://sourcerer.io/fame/roschaefer/Human-Connection/Human-Connection/links/7) +## Open-Source Bounties + +You can get a small financial compensation for your contribution :moneybag: See +details in our [Contribution Guidelines](./CONTRIBUTING.md#open-source-bounties). ## Attributions diff --git a/backend/package.json b/backend/package.json index e9e0f44a3..c8c22cc0d 100644 --- a/backend/package.json +++ b/backend/package.json @@ -38,7 +38,7 @@ }, "dependencies": { "@hapi/joi": "^17.1.0", - "@sentry/node": "^5.11.1", + "@sentry/node": "^5.11.2", "apollo-cache-inmemory": "~1.6.5", "apollo-client": "~2.6.8", "apollo-link-context": "~1.0.19", @@ -60,7 +60,7 @@ "graphql-iso-date": "~3.6.1", "graphql-middleware": "~4.0.2", "graphql-middleware-sentry": "^3.2.1", - "graphql-shield": "~7.0.8", + "graphql-shield": "~7.0.9", "graphql-tag": "~2.10.1", "helmet": "~3.21.2", "jsonwebtoken": "~8.5.1", @@ -105,7 +105,7 @@ "devDependencies": { "@babel/cli": "~7.8.3", "@babel/core": "~7.8.3", - "@babel/node": "~7.8.3", + "@babel/node": "~7.8.4", "@babel/plugin-proposal-throw-expressions": "^7.8.3", "@babel/preset-env": "~7.8.3", "@babel/register": "^7.8.3", @@ -116,7 +116,7 @@ "chai": "~4.2.0", "cucumber": "~6.0.5", "eslint": "~6.8.0", - "eslint-config-prettier": "~6.9.0", + "eslint-config-prettier": "~6.10.0", "eslint-config-standard": "~14.1.0", "eslint-plugin-import": "~2.20.0", "eslint-plugin-jest": "~23.6.0", diff --git a/backend/src/db/migrate/template.js b/backend/src/db/migrate/template.js index b8511e9bb..1d63673b4 100644 --- a/backend/src/db/migrate/template.js +++ b/backend/src/db/migrate/template.js @@ -2,30 +2,44 @@ import { getDriver } from '../../db/neo4j' export const description = '' -export function up(next) { +export async function up(next) { const driver = getDriver() const session = driver.session() + const transaction = session.beginTransaction() + try { // Implement your migration here. + await transaction.run(``) + await transaction.commit() next() - } catch (err) { - next(err) + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') } finally { session.close() - driver.close() } } -export function down(next) { +export async function down(next) { const driver = getDriver() const session = driver.session() + const transaction = session.beginTransaction() + try { - // Rollback your migration here. + // Implement your migration here. + await transaction.run(``) + await transaction.commit() next() - } catch (err) { - next(err) + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') } finally { session.close() - driver.close() } } diff --git a/backend/src/db/migrations/20200127110135-create_muted_relationship_between_existing_blocked_relationships.js b/backend/src/db/migrations/20200127110135-create_muted_relationship_between_existing_blocked_relationships.js new file mode 100644 index 000000000..ce46be9d6 --- /dev/null +++ b/backend/src/db/migrations/20200127110135-create_muted_relationship_between_existing_blocked_relationships.js @@ -0,0 +1,46 @@ +import { getDriver } from '../../db/neo4j' + +export const description = ` + This migration creates a MUTED relationship between two edges(:User) that have a pre-existing BLOCKED relationship. + It also sets the createdAt date for the BLOCKED relationship to the datetime the migration was run. This became + necessary after we redefined what it means to block someone, and what it means to mute them. Muting is about filtering + another user's content, whereas blocking means preventing that user from interacting with you/your contributions. + A blocked user will still be able to see your contributions, but will not be able to interact with them and vice versa. +` + +export async function up(next) { + const driver = getDriver() + const session = driver.session() + const transaction = session.beginTransaction() + try { + await transaction.run( + ` + MATCH (blocker:User)-[blocked:BLOCKED]->(blockee:User) + MERGE (blocker)-[muted:MUTED]->(blockee) + SET muted.createdAt = toString(datetime()), blocked.createdAt = toString(datetime()) + `, + ) + await transaction.commit() + } catch (error) { + // eslint-disable-next-line no-console + console.log(error) + await transaction.rollback() + // eslint-disable-next-line no-console + console.log('rolled back') + } finally { + session.close() + } +} + +export function down(next) { + const driver = getDriver() + const session = driver.session() + try { + // Rollback your migration here. + next() + } catch (err) { + next(err) + } finally { + session.close() + } +} diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 837193773..e0b831b59 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -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 diff --git a/backend/src/middleware/permissionsMiddleware.js b/backend/src/middleware/permissionsMiddleware.js index 50ec5aa75..755ddabf8 100644 --- a/backend/src/middleware/permissionsMiddleware.js +++ b/backend/src/middleware/permissionsMiddleware.js @@ -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, diff --git a/backend/src/middleware/xssMiddleware.js b/backend/src/middleware/xssMiddleware.js index 9b4e3e759..1292abb67 100644 --- a/backend/src/middleware/xssMiddleware.js +++ b/backend/src/middleware/xssMiddleware.js @@ -20,6 +20,7 @@ function clean(dirty) { 'hr', 'b', 'i', + 'u', 'em', 'strong', 'a', diff --git a/backend/src/models/User.js b/backend/src/models/User.js index c3ac434ec..055cbfc83 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -77,12 +77,18 @@ export default { relationship: 'BLOCKED', target: 'User', direction: 'out', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + }, }, muted: { type: 'relationship', relationship: 'MUTED', target: 'User', direction: 'out', + properties: { + createdAt: { type: 'string', isoDate: true, default: () => new Date().toISOString() }, + }, }, notifications: { type: 'relationship', diff --git a/backend/src/schema/resolvers/searches.js b/backend/src/schema/resolvers/searches.js index 5316ccd9a..994d19fa2 100644 --- a/backend/src/schema/resolvers/searches.js +++ b/backend/src/schema/resolvers/searches.js @@ -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 ` diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index 4af60f014..d1d9111b6 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -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', diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index 4eb04a638..71cc1edb0 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -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 } diff --git a/backend/yarn.lock b/backend/yarn.lock index 8f4cfc9a2..4ed151768 100644 --- a/backend/yarn.lock +++ b/backend/yarn.lock @@ -288,10 +288,10 @@ esutils "^2.0.2" js-tokens "^4.0.0" -"@babel/node@~7.8.3": - version "7.8.3" - resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.8.3.tgz#29784d445e135ca7214a9ac40535f2b8d2f980aa" - integrity sha512-GZpHg1gPnZTk1PvHRc4g/M5c50nHERkk3ojb5AuUTZFAjEKzDhBJcqvwWa7NrNT3W3Nf8t8Sj8JjA6rtXJ1z/g== +"@babel/node@~7.8.4": + version "7.8.4" + resolved "https://registry.yarnpkg.com/@babel/node/-/node-7.8.4.tgz#59b2ed7e5a9df2224592f83292d77d616fbf1ab8" + integrity sha512-MlczXI/VYRnoaWHjicqrzq2z4DhRPaWQIC+C3ISEQs5z+mEccBsn7IAI5Q97ZDTnFYw6ts5IUTzqArilC/g7nw== dependencies: "@babel/register" "^7.8.3" commander "^4.0.1" @@ -1275,65 +1275,65 @@ resolved "https://registry.yarnpkg.com/@protobufjs/utf8/-/utf8-1.1.0.tgz#a777360b5b39a1a2e5106f8e858f2fd2d060c570" integrity sha1-p3c2C1s5oaLlEG+OhY8v0tBgxXA= -"@sentry/apm@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.11.1.tgz#cc89fa4150056fbf009f92eca94fccc3980db34e" - integrity sha512-4iZH11p/7w9IMLT9hqNY1+EqLESltiIoF6/YsbpK93sXWGEs8VQ83IuvGuKWxajvHgDmj4ND0TxIliTsYqTqFw== +"@sentry/apm@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/apm/-/apm-5.11.2.tgz#35961b9d2319ad21ae91f1b697998a8c523f1919" + integrity sha512-qn4HiSZ+6b1Gg+DlXdHVpiPPEbRu4IicGSbI8HTJLzrlsjoaBQPPkDwtuQUBVq21tU3RYXnTwrl9m45KuX6alA== dependencies: - "@sentry/browser" "5.11.1" - "@sentry/hub" "5.11.1" - "@sentry/minimal" "5.11.1" + "@sentry/browser" "5.11.2" + "@sentry/hub" "5.11.2" + "@sentry/minimal" "5.11.2" "@sentry/types" "5.11.0" "@sentry/utils" "5.11.1" tslib "^1.9.3" -"@sentry/browser@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.1.tgz#337ffcb52711b23064c847a07629e966f54a5ebb" - integrity sha512-oqOX/otmuP92DEGRyZeBuQokXdeT9HQRxH73oqIURXXNLMP3PWJALSb4HtT4AftEt/2ROGobZLuA4TaID6My/Q== +"@sentry/browser@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-5.11.2.tgz#f0b19bd97e9f09a20e9f93a9835339ed9ab1f5a4" + integrity sha512-ls6ARX5m+23ld8OsuoPnR+kehjR5ketYWRcDYlmJDX2VOq5K4EzprujAo8waDB0o5a92yLXQ0ZSoK/zzAV2VoA== dependencies: - "@sentry/core" "5.11.1" + "@sentry/core" "5.11.2" "@sentry/types" "5.11.0" "@sentry/utils" "5.11.1" tslib "^1.9.3" -"@sentry/core@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.1.tgz#9e2da485e196ae32971545c1c49ee6fe719930e2" - integrity sha512-BpvPosVNT20Xso4gAV54Lu3KqDmD20vO63HYwbNdST5LUi8oYV4JhvOkoBraPEM2cbBwQvwVcFdeEYKk4tin9A== +"@sentry/core@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/core/-/core-5.11.2.tgz#f2d9d37940d291dbcb9a9e4a012f76919474bdf6" + integrity sha512-IFCXGy7ebqIq/Kb8RVryCo/SjwhPcrfBmOjkicr4+DxN1UybLre2N3p9bejQMPIteOfDVHlySLYeipjTf+mxZw== dependencies: - "@sentry/hub" "5.11.1" - "@sentry/minimal" "5.11.1" + "@sentry/hub" "5.11.2" + "@sentry/minimal" "5.11.2" "@sentry/types" "5.11.0" "@sentry/utils" "5.11.1" tslib "^1.9.3" -"@sentry/hub@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.1.tgz#ddcb865563fae53852d405885c46b4c6de68a91b" - integrity sha512-ucKprYCbGGLLjVz4hWUqHN9KH0WKUkGf5ZYfD8LUhksuobRkYVyig0ZGbshECZxW5jcDTzip4Q9Qimq/PkkXBg== +"@sentry/hub@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/hub/-/hub-5.11.2.tgz#a3b7ec27cd4cea2cddd75c372fbf1b4bc04c6aae" + integrity sha512-5BiDin6ZPsaiTm29rCC41MAjP1vOaKniqfjtXHVPm7FeOBA2bpHm95ncjLkshKGJTPfPZHXTpX/1IZsHrfGVEA== dependencies: "@sentry/types" "5.11.0" "@sentry/utils" "5.11.1" tslib "^1.9.3" -"@sentry/minimal@5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.1.tgz#0e705d01a567282d8fbbda2aed848b4974cc3cec" - integrity sha512-HK8zs7Pgdq7DsbZQTThrhQPrJsVWzz7MaluAbQA0rTIAJ3TvHKQpsVRu17xDpjZXypqWcKCRsthDrC4LxDM1Bg== +"@sentry/minimal@5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/minimal/-/minimal-5.11.2.tgz#ae417699342266ecd109a97e53cd9519c0893b21" + integrity sha512-oNuJuz3EZhVtamzABmPdr6lcYo06XHLWb2LvgnoNaYcMD1ExUSvhepOSyZ2h5STCMbmVgGVfXBNPV9RUTp8GZg== dependencies: - "@sentry/hub" "5.11.1" + "@sentry/hub" "5.11.2" "@sentry/types" "5.11.0" tslib "^1.9.3" -"@sentry/node@^5.11.1": - version "5.11.1" - resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.11.1.tgz#2a9c18cd1209cfdf7a69b9d91303413149d2c910" - integrity sha512-FbJs0blJ36gEzE0rc2yBfA/KE+kXOLl8MUfFTcyJCBdCGF8XMETDCmgINnJ4TyBUJviwKoPw2TCk9TL2pa/A1w== +"@sentry/node@^5.11.2": + version "5.11.2" + resolved "https://registry.yarnpkg.com/@sentry/node/-/node-5.11.2.tgz#575c320b624c218d2155183f6bbe82b732bfb1f2" + integrity sha512-jYq9u76BdAbOKPuYg39Xh/+797MevzjMkCIC9cw/bQxAm6nHc3FXeKqd79O33jO4Jag0JL+Bz/0JidgrKgKgXg== dependencies: - "@sentry/apm" "5.11.1" - "@sentry/core" "5.11.1" - "@sentry/hub" "5.11.1" + "@sentry/apm" "5.11.2" + "@sentry/core" "5.11.2" + "@sentry/hub" "5.11.2" "@sentry/types" "5.11.0" "@sentry/utils" "5.11.1" cookie "^0.3.1" @@ -1607,10 +1607,10 @@ dependencies: "@types/yargs-parser" "*" -"@types/yup@0.26.28": - version "0.26.28" - resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.28.tgz#6b8a98fb56ddb5c8a3a7f60996165221671c1ca1" - integrity sha512-DiT584YBKBENDzgk50LwiJLLOh+XgHpiy9p8PwSoOY696LpfrJRIeJ2AoBCG9KyjE8gWL4J64xDZgw15itecag== +"@types/yup@0.26.29": + version "0.26.29" + resolved "https://registry.yarnpkg.com/@types/yup/-/yup-0.26.29.tgz#5a533ad6f74e442436698e20b1441c68a7a1c931" + integrity sha512-M81oZOgLap0b0I/BySnpLwHjOj1BFxUKV1ytG2Kqj3jmkh8F3H11PEnk658UniftpjTXdueloOL+KZYn+SMQ9w== "@types/zen-observable@^0.8.0": version "0.8.0" @@ -3589,10 +3589,10 @@ escodegen@^1.11.1: optionalDependencies: source-map "~0.6.1" -eslint-config-prettier@~6.9.0: - version "6.9.0" - resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.9.0.tgz#430d24822e82f7deb1e22a435bfa3999fae4ad64" - integrity sha512-k4E14HBtcLv0uqThaI6I/n1LEqROp8XaPu6SO9Z32u5NlGRC07Enu1Bh2KEFw4FNHbekH8yzbIU9kUGxbiGmCA== +eslint-config-prettier@~6.10.0: + version "6.10.0" + resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-6.10.0.tgz#7b15e303bf9c956875c948f6b21500e48ded6a7f" + integrity sha512-AtndijGte1rPILInUdHjvKEGbIV06NuvPrqlIEaEaWtbtvJh464mDeyGMdZEQMsGvC0ZVkiex1fSNcC4HAbRGg== dependencies: get-stdin "^6.0.0" @@ -4476,12 +4476,12 @@ graphql-middleware@~4.0.2: dependencies: graphql-tools "^4.0.5" -graphql-shield@~7.0.8: - version "7.0.8" - resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.8.tgz#5ade058610e1b247b0762cb2424d121e9a5f5b46" - integrity sha512-KxYMhoiv5lsHcO0HZDhYjjWLbwzreDCmqmnkLRsLNY+6P0q81KSowoNVPuoAsItkjr9m5Fa6IDObOVxSTSt5Lw== +graphql-shield@~7.0.9: + version "7.0.9" + resolved "https://registry.yarnpkg.com/graphql-shield/-/graphql-shield-7.0.9.tgz#8248916e9636a7e3c05719a52fd13f2d37ccaeb2" + integrity sha512-2Dfddd2hcObCSqAj64c/Aaxvs7gaoD2QU14crj7H486QjS8jIAtEPUyLVyv8SmJ1ZD7jT6wqx6wrB15Npn5Sgw== dependencies: - "@types/yup" "0.26.28" + "@types/yup" "0.26.29" object-hash "^2.0.0" yup "^0.28.0" diff --git a/cypress/integration/common/search.js b/cypress/integration/common/search.js index f6589763b..c42ec3ff0 100644 --- a/cypress/integration/common/search.js +++ b/cypress/integration/common/search.js @@ -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 }) => { diff --git a/cypress/integration/common/steps.js b/cypress/integration/common/steps.js index 9a5c02d08..5185c09f9 100644 --- a/cypress/integration/common/steps.js +++ b/cypress/integration/common/steps.js @@ -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.") +}) \ No newline at end of file diff --git a/cypress/integration/user_profile/BlockUser.feature b/cypress/integration/user_profile/BlockUser.feature new file mode 100644 index 000000000..43efe7807 --- /dev/null +++ b/cypress/integration/user_profile/BlockUser.feature @@ -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 | diff --git a/cypress/integration/user_profile/mute-users/Mute.feature b/cypress/integration/user_profile/mute-users/Mute.feature index b52faeeaa..03ac4370b 100644 --- a/cypress/integration/user_profile/mute-users/Mute.feature +++ b/cypress/integration/user_profile/mute-users/Mute.feature @@ -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 | diff --git a/package.json b/package.json index bd8f9312f..43756b67c 100644 --- a/package.json +++ b/package.json @@ -25,8 +25,8 @@ "release": "standard-version" }, "devDependencies": { - "@babel/core": "^7.8.3", - "@babel/preset-env": "^7.8.3", + "@babel/core": "^7.8.4", + "@babel/preset-env": "^7.8.4", "@babel/register": "^7.8.3", "auto-changelog": "^1.16.2", "bcryptjs": "^2.4.3", diff --git a/webapp/assets/_new/icons/svgs/underline.svg b/webapp/assets/_new/icons/svgs/underline.svg new file mode 100755 index 000000000..f4c6e698c --- /dev/null +++ b/webapp/assets/_new/icons/svgs/underline.svg @@ -0,0 +1,5 @@ + + +underline + + diff --git a/webapp/components/AvatarMenu/AvatarMenu.vue b/webapp/components/AvatarMenu/AvatarMenu.vue index 97b937a88..f65c6f6cf 100644 --- a/webapp/components/AvatarMenu/AvatarMenu.vue +++ b/webapp/components/AvatarMenu/AvatarMenu.vue @@ -11,7 +11,7 @@ " @click.prevent="toggleMenu" > - + @@ -127,6 +127,10 @@ export default { display: flex; align-items: center; padding-left: $space-xx-small; + + > .user-avatar { + margin-right: $space-xx-small; + } } .avatar-menu-popover { padding-top: $space-x-small; diff --git a/webapp/components/ContentMenu/ContentMenu.vue b/webapp/components/ContentMenu/ContentMenu.vue index a22bc3267..5ce73c461 100644 --- a/webapp/components/ContentMenu/ContentMenu.vue +++ b/webapp/components/ContentMenu/ContentMenu.vue @@ -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', }) } diff --git a/webapp/components/Editor/MenuBar.vue b/webapp/components/Editor/MenuBar.vue index 4e43050e9..d1e084f2d 100644 --- a/webapp/components/Editor/MenuBar.vue +++ b/webapp/components/Editor/MenuBar.vue @@ -5,6 +5,12 @@ + +
- + {{ $t('profile.userAnonym') }}
.user-avatar { flex-shrink: 0; diff --git a/webapp/components/_new/generic/UserAvatar/UserAvatar.vue b/webapp/components/_new/generic/UserAvatar/UserAvatar.vue index c29f8e402..b9007b919 100644 --- a/webapp/components/_new/generic/UserAvatar/UserAvatar.vue +++ b/webapp/components/_new/generic/UserAvatar/UserAvatar.vue @@ -6,7 +6,7 @@ v-else :src="user.avatar | proxyApiUrl" class="image" - @error="event.target.style.display = 'none'" + @error="$event.target.style.display = 'none'" /> @@ -75,7 +75,6 @@ export default { > .image { position: relative; - z-index: 5; width: 100%; object-fit: cover; object-position: center; diff --git a/webapp/components/generic/SearchableInput/SearchableInput.vue b/webapp/components/generic/SearchableInput/SearchableInput.vue index 448c154e0..3260ff082 100644 --- a/webapp/components/generic/SearchableInput/SearchableInput.vue +++ b/webapp/components/generic/SearchableInput/SearchableInput.vue @@ -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) } diff --git a/webapp/graphql/PostQuery.js b/webapp/graphql/PostQuery.js index c59c894a5..5ddac9c1f 100644 --- a/webapp/graphql/PostQuery.js +++ b/webapp/graphql/PostQuery.js @@ -29,6 +29,7 @@ export default i18n => { ...user ...userCounts ...locationAndBadges + blocked } comments(orderBy: createdAt_asc) { ...comment diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 8962830dc..a73941794 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -24,6 +24,7 @@ export default i18n => { createdAt followedByCurrentUser isMuted + blocked following(first: 7) { ...user ...userCounts diff --git a/webapp/graphql/settings/BlockedUsers.js b/webapp/graphql/settings/BlockedUsers.js new file mode 100644 index 000000000..94f2121b1 --- /dev/null +++ b/webapp/graphql/settings/BlockedUsers.js @@ -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 + } + } + ` +} diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 0807488eb..0c11c8d13 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -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": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index a711c768e..7804bcbfc 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -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": { diff --git a/webapp/package.json b/webapp/package.json index 6f0bb5b15..72254960f 100644 --- a/webapp/package.json +++ b/webapp/package.json @@ -59,15 +59,15 @@ "dependencies": { "@human-connection/styleguide": "0.5.22", "@nuxtjs/apollo": "^4.0.0-rc19", - "@nuxtjs/axios": "~5.9.3", + "@nuxtjs/axios": "~5.9.4", "@nuxtjs/dotenv": "~1.4.1", "@nuxtjs/pwa": "^3.0.0-beta.19", - "@nuxtjs/sentry": "^3.0.1", + "@nuxtjs/sentry": "^3.1.0", "@nuxtjs/style-resources": "~1.0.0", "accounting": "~0.4.1", "apollo-cache-inmemory": "~1.6.5", "apollo-client": "~2.6.8", - "cookie-universal-nuxt": "~2.1.0", + "cookie-universal-nuxt": "~2.1.1", "cropperjs": "^1.5.5", "cross-env": "~7.0.0", "date-fns": "2.9.0", @@ -98,8 +98,8 @@ "devDependencies": { "@babel/core": "~7.8.3", "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/preset-env": "~7.8.3", - "@storybook/addon-a11y": "^5.3.8", + "@babel/preset-env": "~7.8.4", + "@storybook/addon-a11y": "^5.3.9", "@storybook/addon-actions": "^5.3.9", "@storybook/addon-notes": "^5.3.9", "@storybook/vue": "~5.3.9", @@ -117,7 +117,7 @@ "core-js": "~2.6.10", "css-loader": "~3.4.2", "eslint": "~6.8.0", - "eslint-config-prettier": "~6.9.0", + "eslint-config-prettier": "~6.10.0", "eslint-config-standard": "~14.1.0", "eslint-loader": "~3.0.3", "eslint-plugin-import": "~2.20.0", diff --git a/webapp/pages/post/_id/_slug/index.vue b/webapp/pages/post/_id/_slug/index.vue index 79e0af21c..1d107941a 100644 --- a/webapp/pages/post/_id/_slug/index.vue +++ b/webapp/pages/post/_id/_slug/index.vue @@ -92,11 +92,17 @@ /> + + {{ $t('settings.blocked-users.explanation.commenting-disabled') }} +
+ {{ $t('settings.blocked-users.explanation.commenting-explanation') }} + FAQ +
@@ -145,6 +151,8 @@ export default { title: 'loading', showNewCommentForm: true, blurred: false, + blocked: null, + postAuthor: null, } }, mounted() { @@ -222,6 +230,7 @@ export default { this.post = Post[0] || {} this.title = this.post.title this.blurred = this.post.imageBlurred + this.postAuthor = this.post.author }, fetchPolicy: 'cache-and-network', }, diff --git a/webapp/pages/profile/_id/_slug.vue b/webapp/pages/profile/_id/_slug.vue index ce220b66b..80471fff4 100644 --- a/webapp/pages/profile/_id/_slug.vue +++ b/webapp/pages/profile/_id/_slug.vue @@ -24,6 +24,8 @@ class="user-content-menu" @mute="muteUser" @unmute="unmuteUser" + @block="blockUser" + @unblock="unblockUser" /> @@ -64,20 +66,21 @@ - - - +
+ + {{ $t('settings.blocked-users.unblock') }} + + + {{ $t('settings.muted-users.unmute') }} + + +