From 7ae5593f82957f514c0d0b20671b8a10b545ee57 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wolfgang=20Hu=C3=9F?= Date: Wed, 22 Sep 2021 14:05:10 +0200 Subject: [PATCH] Implement user notifications settings --- backend/src/db/factories.js | 1 + .../src/middleware/helpers/email/sendMail.js | 9 +-- .../notifications/notificationsMiddleware.js | 22 +++--- backend/src/models/User.js | 4 ++ backend/src/schema/resolvers/registration.js | 1 + backend/src/schema/resolvers/users.js | 1 + backend/src/schema/types/type/User.gql | 2 + webapp/graphql/User.js | 5 ++ webapp/locales/de.json | 7 +- webapp/locales/en.json | 7 +- webapp/pages/settings.vue | 4 ++ webapp/pages/settings/notifications.spec.js | 70 +++++++++++++++++++ webapp/pages/settings/notifications.vue | 61 ++++++++++++++++ 13 files changed, 171 insertions(+), 23 deletions(-) create mode 100644 webapp/pages/settings/notifications.spec.js create mode 100644 webapp/pages/settings/notifications.vue diff --git a/backend/src/db/factories.js b/backend/src/db/factories.js index 64ee2009c..f3f2e5c93 100644 --- a/backend/src/db/factories.js +++ b/backend/src/db/factories.js @@ -70,6 +70,7 @@ Factory.define('basicUser') termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z', allowEmbedIframes: false, showShoutsPublicly: false, + sendNotificationEmails: true, locale: 'en', }) .attr('slug', ['slug', 'name'], (slug, name) => { diff --git a/backend/src/middleware/helpers/email/sendMail.js b/backend/src/middleware/helpers/email/sendMail.js index 3fc1e2931..2aa042bcd 100644 --- a/backend/src/middleware/helpers/email/sendMail.js +++ b/backend/src/middleware/helpers/email/sendMail.js @@ -5,12 +5,7 @@ import { htmlToText } from 'nodemailer-html-to-text' const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD -// Wolle -// let sendMailCallback = async () => {} -// Wolle -let sendMailCallback = async (templateArgs) => { - // console.log('templateArgs: ', templateArgs) -} +let sendMailCallback = async () => {} if (!hasEmailConfig) { if (!CONFIG.TEST) { // eslint-disable-next-line no-console @@ -18,8 +13,6 @@ if (!hasEmailConfig) { } } else { sendMailCallback = async (templateArgs) => { - // Wolle - // console.log('templateArgs: ', templateArgs) const transporter = nodemailer.createTransport({ host: CONFIG.SMTP_HOST, port: CONFIG.SMTP_PORT, diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js index 464bd47bb..af7cf19f9 100644 --- a/backend/src/middleware/notifications/notificationsMiddleware.js +++ b/backend/src/middleware/notifications/notificationsMiddleware.js @@ -35,26 +35,22 @@ const queryNotificationsEmails = async (context, notificationUserIds) => { const publishNotifications = async (context, promises) => { let notifications = await Promise.all(promises) notifications = notifications.flat() - // Wolle - // console.log('notifications: ', notifications) const notificationsEmailAddresses = await queryNotificationsEmails( context, notifications.map((notification) => notification.to.id), ) - // Wolle - // console.log('notificationsEmailAddresses: ', notificationsEmailAddresses) notifications.forEach((notificationAdded, index) => { pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }) - // Wolle await - sendMail( - notificationTemplate({ - email: notificationsEmailAddresses[index].email, - notification: notificationAdded, - }), - ) + if (notificationAdded.to.sendNotificationEmails) { + // Wolle await + sendMail( + notificationTemplate({ + email: notificationsEmailAddresses[index].email, + notification: notificationAdded, + }), + ) + } }) - // Wolle - // return XXX successful? } const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => { diff --git a/backend/src/models/User.js b/backend/src/models/User.js index 6cfd22268..b8d024216 100644 --- a/backend/src/models/User.js +++ b/backend/src/models/User.js @@ -153,6 +153,10 @@ export default { type: 'boolean', default: false, }, + sendNotificationEmails: { + type: 'boolean', + default: true, + }, locale: { type: 'string', allow: [null], diff --git a/backend/src/schema/resolvers/registration.js b/backend/src/schema/resolvers/registration.js index 2796028fe..fc504f7cd 100644 --- a/backend/src/schema/resolvers/registration.js +++ b/backend/src/schema/resolvers/registration.js @@ -93,6 +93,7 @@ const signupCypher = (inviteCode) => { SET user.updatedAt = toString(datetime()) SET user.allowEmbedIframes = FALSE SET user.showShoutsPublicly = FALSE + SET user.sendNotificationEmails = TRUE SET email.verifiedAt = toString(datetime()) RETURN user {.*} ` diff --git a/backend/src/schema/resolvers/users.js b/backend/src/schema/resolvers/users.js index c6bb64125..5dc78c5e1 100644 --- a/backend/src/schema/resolvers/users.js +++ b/backend/src/schema/resolvers/users.js @@ -290,6 +290,7 @@ export default { 'termsAndConditionsAgreedAt', 'allowEmbedIframes', 'showShoutsPublicly', + 'sendNotificationEmails', 'locale', ], boolean: { diff --git a/backend/src/schema/types/type/User.gql b/backend/src/schema/types/type/User.gql index b8b805a02..772dedf6b 100644 --- a/backend/src/schema/types/type/User.gql +++ b/backend/src/schema/types/type/User.gql @@ -46,6 +46,7 @@ type User { allowEmbedIframes: Boolean showShoutsPublicly: Boolean + sendNotificationEmails: Boolean locale: String friends: [User]! @relation(name: "FRIENDS", direction: "BOTH") friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)") @@ -207,6 +208,7 @@ type Mutation { termsAndConditionsAgreedAt: String allowEmbedIframes: Boolean showShoutsPublicly: Boolean + sendNotificationEmails: Boolean locale: String ): User diff --git a/webapp/graphql/User.js b/webapp/graphql/User.js index 4b3a67775..b3f131238 100644 --- a/webapp/graphql/User.js +++ b/webapp/graphql/User.js @@ -41,6 +41,7 @@ export default (i18n) => { url } showShoutsPublicly + sendNotificationEmails } } ` @@ -224,6 +225,7 @@ export const updateUserMutation = () => { $about: String $allowEmbedIframes: Boolean $showShoutsPublicly: Boolean + $sendNotificationEmails: Boolean $termsAndConditionsAgreedVersion: String $avatar: ImageInput ) { @@ -235,6 +237,7 @@ export const updateUserMutation = () => { about: $about allowEmbedIframes: $allowEmbedIframes showShoutsPublicly: $showShoutsPublicly + sendNotificationEmails: $sendNotificationEmails termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion avatar: $avatar ) { @@ -245,6 +248,7 @@ export const updateUserMutation = () => { about allowEmbedIframes showShoutsPublicly + sendNotificationEmails locale termsAndConditionsAgreedVersion avatar { @@ -275,6 +279,7 @@ export const currentUserQuery = gql` locale allowEmbedIframes showShoutsPublicly + sendNotificationEmails termsAndConditionsAgreedVersion socialMedia { id diff --git a/webapp/locales/de.json b/webapp/locales/de.json index 733a42629..e4e7332e2 100644 --- a/webapp/locales/de.json +++ b/webapp/locales/de.json @@ -739,13 +739,18 @@ "unmuted": "{name} ist nicht mehr stummgeschaltet" }, "name": "Einstellungen", + "notifications": { + "send-email-notifications": "Sende E-Mail-Benachrichtigungen", + "name": "Benachrichtigungen", + "success-update": "Benachrichtigungs-Einstellungen gespeichert!" + }, "organizations": { "name": "Meine Organisationen" }, "privacy": { "make-shouts-public": "Teile von mir empfohlene Artikel öffentlich auf meinem Profil", "name": "Privatsphäre", - "success-update": "Privatsphäre-Einstellungen gespeichert" + "success-update": "Privatsphäre-Einstellungen gespeichert!" }, "security": { "change-password": { diff --git a/webapp/locales/en.json b/webapp/locales/en.json index 3c26cd93e..d9f899664 100644 --- a/webapp/locales/en.json +++ b/webapp/locales/en.json @@ -739,13 +739,18 @@ "unmuted": "{name} is unmuted again" }, "name": "Settings", + "notifications": { + "send-email-notifications": "Send e-mail notifications", + "name": "Notifications", + "success-update": "Notifications settings saved!" + }, "organizations": { "name": "My Organizations" }, "privacy": { "make-shouts-public": "Share articles I have shouted on my public profile", "name": "Privacy", - "success-update": "Privacy settings saved" + "success-update": "Privacy settings saved!" }, "security": { "change-password": { diff --git a/webapp/pages/settings.vue b/webapp/pages/settings.vue index 6bd78b701..360f1e969 100644 --- a/webapp/pages/settings.vue +++ b/webapp/pages/settings.vue @@ -51,6 +51,10 @@ export default { name: this.$t('settings.embeds.name'), path: `/settings/embeds`, }, + { + name: this.$t('settings.notifications.name'), + path: '/settings/notifications', + }, { name: this.$t('settings.download.name'), path: `/settings/data-download`, diff --git a/webapp/pages/settings/notifications.spec.js b/webapp/pages/settings/notifications.spec.js new file mode 100644 index 000000000..7b43ef2c4 --- /dev/null +++ b/webapp/pages/settings/notifications.spec.js @@ -0,0 +1,70 @@ +import Vuex from 'vuex' +import { mount } from '@vue/test-utils' +import Notifications from './notifications.vue' + +const localVue = global.localVue + +describe('notifications.vue', () => { + let wrapper + let mocks + let store + + beforeEach(() => { + mocks = { + $t: jest.fn(), + $apollo: { + mutate: jest.fn(), + }, + $toast: { + success: jest.fn(), + error: jest.fn(), + }, + } + store = new Vuex.Store({ + getters: { + 'auth/user': () => { + return { + id: 'u343', + name: 'MyAccount', + sendNotificationEmails: true, + } + }, + }, + }) + }) + + describe('mount', () => { + const Wrapper = () => { + return mount(Notifications, { + store, + mocks, + localVue, + }) + } + + beforeEach(() => { + wrapper = Wrapper() + }) + + it('renders', () => { + expect(wrapper.is('.base-card')).toBe(true) + }) + + it('clicking on submit changes notifyByEmail to false', async () => { + wrapper.find('#send-email').trigger('click') + await wrapper.vm.$nextTick() + wrapper.find('.base-button').trigger('click') + expect(wrapper.vm.notifyByEmail).toBe(false) + }) + + it('clicking on submit with a server error shows a toast and notifyByEmail is still true', async () => { + mocks.$apollo.mutate = jest.fn().mockRejectedValue({ message: 'Ouch!' }) + wrapper.find('#send-email').trigger('click') + await wrapper.vm.$nextTick() + await wrapper.find('.base-button').trigger('click') + await wrapper.vm.$nextTick() + expect(mocks.$toast.error).toHaveBeenCalledWith('Ouch!') + expect(wrapper.vm.notifyByEmail).toBe(true) + }) + }) +}) diff --git a/webapp/pages/settings/notifications.vue b/webapp/pages/settings/notifications.vue new file mode 100644 index 000000000..3d1a7eb62 --- /dev/null +++ b/webapp/pages/settings/notifications.vue @@ -0,0 +1,61 @@ + + +