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/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.js b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.js
new file mode 100644
index 000000000..0d1f4fb91
--- /dev/null
+++ b/backend/src/db/migrations/20210923140939-add-sendNotificationEmails-property-to-all-users.js
@@ -0,0 +1,59 @@
+import { getDriver } from '../../db/neo4j'
+
+export const description = ''
+
+export async function up(next) {
+ const driver = getDriver()
+ const session = driver.session()
+ const transaction = session.beginTransaction()
+
+ try {
+ // Implement your migration here.
+ await transaction.run(
+ `
+ MATCH (user:User)
+ SET user.sendNotificationEmails = true
+ RETURN user {.*}
+ `,
+ )
+ await transaction.commit()
+ next()
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ await transaction.rollback()
+ // eslint-disable-next-line no-console
+ console.log('rolled back')
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
+
+export async function down(next) {
+ const driver = getDriver()
+ const session = driver.session()
+ const transaction = session.beginTransaction()
+
+ try {
+ // Implement your migration here.
+ await transaction.run(
+ `
+ MATCH (user:User)
+ REMOVE user.sendNotificationEmails
+ RETURN user {.*}
+ `,
+ )
+ await transaction.commit()
+ next()
+ } catch (error) {
+ // eslint-disable-next-line no-console
+ console.log(error)
+ await transaction.rollback()
+ // eslint-disable-next-line no-console
+ console.log('rolled back')
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
diff --git a/backend/src/middleware/helpers/email/sendMail.js b/backend/src/middleware/helpers/email/sendMail.js
new file mode 100644
index 000000000..a25938a14
--- /dev/null
+++ b/backend/src/middleware/helpers/email/sendMail.js
@@ -0,0 +1,39 @@
+import CONFIG from '../../../config'
+import nodemailer from 'nodemailer'
+import { htmlToText } from 'nodemailer-html-to-text'
+
+const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
+const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
+
+let sendMailCallback = async () => {}
+if (!hasEmailConfig) {
+ if (!CONFIG.TEST) {
+ // eslint-disable-next-line no-console
+ console.log('Warning: Middlewares will not try to send mails.')
+ }
+} else {
+ sendMailCallback = async (templateArgs) => {
+ const transporter = nodemailer.createTransport({
+ host: CONFIG.SMTP_HOST,
+ port: CONFIG.SMTP_PORT,
+ ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
+ secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
+ auth: hasAuthData && {
+ user: CONFIG.SMTP_USERNAME,
+ pass: CONFIG.SMTP_PASSWORD,
+ },
+ })
+
+ transporter.use(
+ 'compile',
+ htmlToText({
+ ignoreImage: true,
+ wordwrap: false,
+ }),
+ )
+
+ await transporter.sendMail(templateArgs)
+ }
+}
+
+export const sendMail = sendMailCallback
diff --git a/backend/src/middleware/email/templateBuilder.js b/backend/src/middleware/helpers/email/templateBuilder.js
similarity index 64%
rename from backend/src/middleware/email/templateBuilder.js
rename to backend/src/middleware/helpers/email/templateBuilder.js
index 872b86b29..8098b38fe 100644
--- a/backend/src/middleware/email/templateBuilder.js
+++ b/backend/src/middleware/helpers/email/templateBuilder.js
@@ -1,18 +1,23 @@
import mustache from 'mustache'
-import CONFIG from '../../config'
-import logosWebapp from '../../config/logos.js'
+import CONFIG from '../../../config'
+import metadata from '../../../config/metadata.js'
+import logosWebapp from '../../../config/logos.js'
import * as templates from './templates'
+import * as templatesEN from './templates/en'
+import * as templatesDE from './templates/de'
const from = CONFIG.EMAIL_DEFAULT_SENDER
const welcomeImageUrl = new URL(logosWebapp.LOGO_WELCOME_PATH, CONFIG.CLIENT_URI)
const defaultParams = {
- supportUrl: CONFIG.SUPPORT_URL,
- APPLICATION_NAME: CONFIG.APPLICATION_NAME,
- ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
welcomeImageUrl,
+ APPLICATION_NAME: CONFIG.APPLICATION_NAME,
+ ORGANIZATION_NAME: metadata.ORGANIZATION_NAME,
+ ORGANIZATION_URL: CONFIG.ORGANIZATION_URL,
+ supportUrl: CONFIG.SUPPORT_URL,
}
+const englishHint = 'English version below!'
export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
const subject = `Willkommen, Bienvenue, Welcome to ${CONFIG.APPLICATION_NAME}!`
@@ -33,7 +38,7 @@ export const signupTemplate = ({ email, nonce, inviteCode = null }) => {
subject,
html: mustache.render(
templates.layout,
- { ...defaultParams, actionUrl, nonce, subject },
+ { ...defaultParams, englishHint, actionUrl, nonce, subject },
{ content: templates.signup },
),
}
@@ -51,7 +56,7 @@ export const emailVerificationTemplate = ({ email, nonce, name }) => {
subject,
html: mustache.render(
templates.layout,
- { ...defaultParams, actionUrl, name, nonce, subject },
+ { ...defaultParams, englishHint, actionUrl, name, nonce, subject },
{ content: templates.emailVerification },
),
}
@@ -69,7 +74,7 @@ export const resetPasswordTemplate = ({ email, nonce, name }) => {
subject,
html: mustache.render(
templates.layout,
- { ...defaultParams, actionUrl, name, nonce, subject },
+ { ...defaultParams, englishHint, actionUrl, name, nonce, subject },
{ content: templates.passwordReset },
),
}
@@ -85,8 +90,35 @@ export const wrongAccountTemplate = ({ email }) => {
subject,
html: mustache.render(
templates.layout,
- { ...defaultParams, actionUrl, supportUrl: CONFIG.SUPPORT_URL, welcomeImageUrl },
+ { ...defaultParams, englishHint, actionUrl },
{ content: templates.wrongAccount },
),
}
}
+
+export const notificationTemplate = ({ email, notification }) => {
+ const actionUrl = new URL('/notifications', CONFIG.CLIENT_URI)
+ const renderParams = { ...defaultParams, name: notification.to.name, actionUrl }
+ let content
+ switch (notification.to.locale) {
+ case 'de':
+ content = templatesDE.notification
+ break
+ case 'en':
+ content = templatesEN.notification
+ break
+
+ default:
+ content = templatesEN.notification
+ break
+ }
+ const subjectUnrendered = content.split('\n')[0].split('"')[1]
+ const subject = mustache.render(subjectUnrendered, renderParams, {})
+
+ return {
+ from,
+ to: email,
+ subject,
+ html: mustache.render(templates.layout, renderParams, { content }),
+ }
+}
diff --git a/backend/src/middleware/helpers/email/templates/de/index.js b/backend/src/middleware/helpers/email/templates/de/index.js
new file mode 100644
index 000000000..0f9d13c36
--- /dev/null
+++ b/backend/src/middleware/helpers/email/templates/de/index.js
@@ -0,0 +1,6 @@
+import fs from 'fs'
+import path from 'path'
+
+const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
+
+export const notification = readFile('./notification.html')
diff --git a/backend/src/middleware/helpers/email/templates/de/notification.html b/backend/src/middleware/helpers/email/templates/de/notification.html
new file mode 100644
index 000000000..b3c60b26e
--- /dev/null
+++ b/backend/src/middleware/helpers/email/templates/de/notification.html
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Hallo {{ name }},
+ Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button,
+ um sie anzusehen:
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
diff --git a/backend/src/middleware/email/templates/emailVerification.html b/backend/src/middleware/helpers/email/templates/emailVerification.html
similarity index 100%
rename from backend/src/middleware/email/templates/emailVerification.html
rename to backend/src/middleware/helpers/email/templates/emailVerification.html
diff --git a/backend/src/middleware/helpers/email/templates/en/index.js b/backend/src/middleware/helpers/email/templates/en/index.js
new file mode 100644
index 000000000..0f9d13c36
--- /dev/null
+++ b/backend/src/middleware/helpers/email/templates/en/index.js
@@ -0,0 +1,6 @@
+import fs from 'fs'
+import path from 'path'
+
+const readFile = (fileName) => fs.readFileSync(path.join(__dirname, fileName), 'utf-8')
+
+export const notification = readFile('./notification.html')
diff --git a/backend/src/middleware/helpers/email/templates/en/notification.html b/backend/src/middleware/helpers/email/templates/en/notification.html
new file mode 100644
index 000000000..58cbffd6f
--- /dev/null
+++ b/backend/src/middleware/helpers/email/templates/en/notification.html
@@ -0,0 +1,83 @@
+
+
+
+
+
+
+
+
+ |
+
+
+
+
+
+
+
+
+
+
+ Hello {{ name }},
+ You received at least one notification. Click on this button to view them:
+ |
+
+
+ |
+
+
+
+ |
+
+
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
+ |
+
+ |
+
+
+
+
+
diff --git a/backend/src/middleware/email/templates/index.js b/backend/src/middleware/helpers/email/templates/index.js
similarity index 100%
rename from backend/src/middleware/email/templates/index.js
rename to backend/src/middleware/helpers/email/templates/index.js
diff --git a/backend/src/middleware/email/templates/layout.html b/backend/src/middleware/helpers/email/templates/layout.html
similarity index 94%
rename from backend/src/middleware/email/templates/layout.html
rename to backend/src/middleware/helpers/email/templates/layout.html
index da2053a93..0c68d6309 100644
--- a/backend/src/middleware/email/templates/layout.html
+++ b/backend/src/middleware/helpers/email/templates/layout.html
@@ -159,7 +159,7 @@
- English version below!
+ {{englishHint}}
{{> content}}
@@ -169,10 +169,11 @@
|
-
- {{ORGANIZATION_NAME}}
- {{ORGANIZATION_URL}}
-
+
+ {{ORGANIZATION_NAME}}
+
+
+
|
diff --git a/backend/src/middleware/email/templates/resetPassword.html b/backend/src/middleware/helpers/email/templates/resetPassword.html
similarity index 100%
rename from backend/src/middleware/email/templates/resetPassword.html
rename to backend/src/middleware/helpers/email/templates/resetPassword.html
diff --git a/backend/src/middleware/email/templates/signup.html b/backend/src/middleware/helpers/email/templates/signup.html
similarity index 100%
rename from backend/src/middleware/email/templates/signup.html
rename to backend/src/middleware/helpers/email/templates/signup.html
diff --git a/backend/src/middleware/email/templates/wrongAccount.html b/backend/src/middleware/helpers/email/templates/wrongAccount.html
similarity index 100%
rename from backend/src/middleware/email/templates/wrongAccount.html
rename to backend/src/middleware/helpers/email/templates/wrongAccount.html
diff --git a/backend/src/middleware/index.js b/backend/src/middleware/index.js
index 592e25a60..22e92e1a3 100644
--- a/backend/src/middleware/index.js
+++ b/backend/src/middleware/index.js
@@ -12,7 +12,7 @@ import orderBy from './orderByMiddleware'
import validation from './validation/validationMiddleware'
import notifications from './notifications/notificationsMiddleware'
import hashtags from './hashtags/hashtagsMiddleware'
-import email from './email/emailMiddleware'
+import login from './login/loginMiddleware'
import sentry from './sentryMiddleware'
import languages from './languages/languages'
import userInteractions from './userInteractions'
@@ -26,7 +26,7 @@ export default (schema) => {
validation,
sluggify,
excerpt,
- email,
+ login,
notifications,
hashtags,
softDelete,
@@ -46,7 +46,7 @@ export default (schema) => {
'sluggify',
'languages',
'excerpt',
- 'email',
+ 'login',
'notifications',
'hashtags',
'softDelete',
diff --git a/backend/src/middleware/email/emailMiddleware.js b/backend/src/middleware/login/loginMiddleware.js
similarity index 56%
rename from backend/src/middleware/email/emailMiddleware.js
rename to backend/src/middleware/login/loginMiddleware.js
index 571b733d5..b7fe0239a 100644
--- a/backend/src/middleware/email/emailMiddleware.js
+++ b/backend/src/middleware/login/loginMiddleware.js
@@ -1,46 +1,10 @@
-import CONFIG from '../../config'
-import nodemailer from 'nodemailer'
-import { htmlToText } from 'nodemailer-html-to-text'
+import { sendMail } from '../helpers/email/sendMail'
import {
signupTemplate,
resetPasswordTemplate,
wrongAccountTemplate,
emailVerificationTemplate,
-} from './templateBuilder'
-
-const hasEmailConfig = CONFIG.SMTP_HOST && CONFIG.SMTP_PORT
-const hasAuthData = CONFIG.SMTP_USERNAME && CONFIG.SMTP_PASSWORD
-
-let sendMail = () => {}
-if (!hasEmailConfig) {
- if (!CONFIG.TEST) {
- // eslint-disable-next-line no-console
- console.log('Warning: Email middleware will not try to send mails.')
- }
-} else {
- sendMail = async (templateArgs) => {
- const transporter = nodemailer.createTransport({
- host: CONFIG.SMTP_HOST,
- port: CONFIG.SMTP_PORT,
- ignoreTLS: CONFIG.SMTP_IGNORE_TLS,
- secure: CONFIG.SMTP_SECURE, // true for 465, false for other ports
- auth: hasAuthData && {
- user: CONFIG.SMTP_USERNAME,
- pass: CONFIG.SMTP_PASSWORD,
- },
- })
-
- transporter.use(
- 'compile',
- htmlToText({
- ignoreImage: true,
- wordwrap: false,
- }),
- )
-
- await transporter.sendMail(templateArgs)
- }
-}
+} from '../helpers/email/templateBuilder'
const sendSignupMail = async (resolve, root, args, context, resolveInfo) => {
const { inviteCode } = args
diff --git a/backend/src/middleware/notifications/notificationsMiddleware.js b/backend/src/middleware/notifications/notificationsMiddleware.js
index c76b9ca0e..2bc53ab7c 100644
--- a/backend/src/middleware/notifications/notificationsMiddleware.js
+++ b/backend/src/middleware/notifications/notificationsMiddleware.js
@@ -1,21 +1,64 @@
+import { pubsub, NOTIFICATION_ADDED } from '../../server'
import extractMentionedUsers from './mentions/extractMentionedUsers'
import { validateNotifyUsers } from '../validation/validationMiddleware'
-import { pubsub, NOTIFICATION_ADDED } from '../../server'
+import { sendMail } from '../helpers/email/sendMail'
+import { notificationTemplate } from '../helpers/email/templateBuilder'
-const publishNotifications = async (...promises) => {
- const notifications = await Promise.all(promises)
- notifications
- .flat()
- .forEach((notificationAdded) => pubsub.publish(NOTIFICATION_ADDED, { notificationAdded }))
+const queryNotificationEmails = async (context, notificationUserIds) => {
+ if (!(notificationUserIds && notificationUserIds.length)) return []
+ const userEmailCypher = `
+ MATCH (user: User)
+ // blocked users are filtered out from notifications already
+ WHERE user.id in $notificationUserIds
+ WITH user
+ MATCH (user)-[:PRIMARY_EMAIL]->(emailAddress:EmailAddress)
+ RETURN emailAddress {.email}
+ `
+ const session = context.driver.session()
+ const writeTxResultPromise = session.writeTransaction(async (transaction) => {
+ const emailAddressTransactionResponse = await transaction.run(userEmailCypher, {
+ notificationUserIds,
+ })
+ return emailAddressTransactionResponse.records.map((record) => record.get('emailAddress'))
+ })
+ try {
+ const emailAddresses = await writeTxResultPromise
+ return emailAddresses
+ } catch (error) {
+ throw new Error(error)
+ } finally {
+ session.close()
+ }
+}
+
+const publishNotifications = async (context, promises) => {
+ let notifications = await Promise.all(promises)
+ notifications = notifications.flat()
+ const notificationsEmailAddresses = await queryNotificationEmails(
+ context,
+ notifications.map((notification) => notification.to.id),
+ )
+ notifications.forEach((notificationAdded, index) => {
+ pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
+ if (notificationAdded.to.sendNotificationEmails) {
+ // Wolle await
+ sendMail(
+ notificationTemplate({
+ email: notificationsEmailAddresses[index].email,
+ notification: notificationAdded,
+ }),
+ )
+ }
+ })
}
const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo) => {
const idsOfUsers = extractMentionedUsers(args.content)
const post = await resolve(root, args, context, resolveInfo)
if (post) {
- await publishNotifications(
+ await publishNotifications(context, [
notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
- )
+ ])
}
return post
}
@@ -26,10 +69,10 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
const comment = await resolve(root, args, context, resolveInfo)
const [postAuthor] = await postAuthorOfComment(comment.id, { context })
idsOfUsers = idsOfUsers.filter((id) => id !== postAuthor.id)
- await publishNotifications(
+ await publishNotifications(context, [
notifyUsersOfMention('Comment', comment.id, idsOfUsers, 'mentioned_in_comment', context),
notifyUsersOfComment('Comment', comment.id, postAuthor.id, 'commented_on_post', context),
- )
+ ])
return comment
}
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/cypress/integration/User.SettingNotifications.feature b/cypress/integration/User.SettingNotifications.feature
new file mode 100644
index 000000000..7e4301b81
--- /dev/null
+++ b/cypress/integration/User.SettingNotifications.feature
@@ -0,0 +1,20 @@
+Feature: User sets donations info settings
+ As a user
+ I want to change my notifications settings
+ In order to manage the notifications
+
+ Background:
+ Given the following "users" are in the database:
+ | email | password | id | name | slug | termsAndConditionsAgreedVersion |
+ | peterpan@example.org | 123 | id-of-peter-pan | Peter Pan | peter-pan | 0.0.4 |
+ | user@example.org | 123 | user | User | user | 0.0.4 |
+ And I am logged in as "peter-pan"
+
+ Scenario: The notifications setting "Send e-mail notifications" is set to true by default and can be set to false
+ # When I navigate to my "Notifications" settings page
+ When I navigate to page "/settings/notifications"
+ Then the checkbox with ID "send-email" should "be.checked"
+ And I click on element with ID "send-email"
+ And the checkbox with ID "send-email" should "not.be.checked"
+ Then I click save
+ And I see a toaster with "Notifications settings saved!"
diff --git a/cypress/integration/User.SettingNotifications/I_click_on_element_with_ID_{string}.js b/cypress/integration/User.SettingNotifications/I_click_on_element_with_ID_{string}.js
new file mode 100644
index 000000000..251c38941
--- /dev/null
+++ b/cypress/integration/User.SettingNotifications/I_click_on_element_with_ID_{string}.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("I click on element with ID {string}", (id) => {
+ cy.get('#' + id).click()
+})
diff --git a/cypress/integration/User.SettingNotifications/I_click_save.js b/cypress/integration/User.SettingNotifications/I_click_save.js
new file mode 100644
index 000000000..32d702f1e
--- /dev/null
+++ b/cypress/integration/User.SettingNotifications/I_click_save.js
@@ -0,0 +1,5 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I click save", () => {
+ cy.get(".save-button").click()
+})
\ No newline at end of file
diff --git a/cypress/integration/User.SettingNotifications/I_click_the_checkbox_show_donations_progress_bar_and_save.js b/cypress/integration/User.SettingNotifications/I_click_the_checkbox_show_donations_progress_bar_and_save.js
new file mode 100644
index 000000000..b4289dd5e
--- /dev/null
+++ b/cypress/integration/User.SettingNotifications/I_click_the_checkbox_show_donations_progress_bar_and_save.js
@@ -0,0 +1,6 @@
+import { Then } from "cypress-cucumber-preprocessor/steps";
+
+Then("I click the checkbox show donations progress bar and save", () => {
+ cy.get("#showDonations").click()
+ cy.get(".donations-info-button").click()
+})
\ No newline at end of file
diff --git a/cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js b/cypress/integration/UserProfile.ChangePassword/I_submit_the_form.js
similarity index 100%
rename from cypress/integration/UserProfile.ChangePassword/I_submit_the_form copy.js
rename to cypress/integration/UserProfile.ChangePassword/I_submit_the_form.js
diff --git a/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js b/cypress/integration/common/I_see_a_{string}_message.js
similarity index 61%
rename from cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js
rename to cypress/integration/common/I_see_a_{string}_message.js
index 90ddf0bd3..6cc2cbf6b 100644
--- a/cypress/integration/UserProfile.ChangePassword/I_see_a_{string}_message.js
+++ b/cypress/integration/common/I_see_a_{string}_message.js
@@ -1,5 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps";
-Then("I see a {string} message:", (type, message) => {
+Then("I see a {string} message:", (_type, message) => {
cy.contains(message);
});
\ No newline at end of file
diff --git a/cypress/integration/common/the_checkbox_with_ID_{string}_should_{string}.js b/cypress/integration/common/the_checkbox_with_ID_{string}_should_{string}.js
new file mode 100644
index 000000000..ad3f7f3cc
--- /dev/null
+++ b/cypress/integration/common/the_checkbox_with_ID_{string}_should_{string}.js
@@ -0,0 +1,5 @@
+import { When } from "cypress-cucumber-preprocessor/steps";
+
+When("the checkbox with ID {string} should {string}", (id, value) => {
+ cy.get('#' + id).should(value)
+})
diff --git a/webapp/components/Registration/RegistrationSlideEmail.vue b/webapp/components/Registration/RegistrationSlideEmail.vue
index 5289248cc..045269f00 100644
--- a/webapp/components/Registration/RegistrationSlideEmail.vue
+++ b/webapp/components/Registration/RegistrationSlideEmail.vue
@@ -154,7 +154,7 @@ export default {
this.sliderData.setSliderValuesCallback(null, {
sliderSettings: { buttonLoading: true },
})
- const response = await this.$apollo.mutate({ mutation: SignupMutation, variables }) // e-mail is send in emailMiddleware of backend
+ const response = await this.$apollo.mutate({ mutation: SignupMutation, variables }) // e-mail is send in loginMiddleware of backend
this.sliderData.setSliderValuesCallback(null, {
sliderData: { request: { variables }, response: response.data },
})
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..9816e167a 100644
--- a/webapp/locales/de.json
+++ b/webapp/locales/de.json
@@ -739,13 +739,18 @@
"unmuted": "{name} ist nicht mehr stummgeschaltet"
},
"name": "Einstellungen",
+ "notifications": {
+ "name": "Benachrichtigungen",
+ "send-email-notifications": "Sende E-Mail-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..11b920056 100644
--- a/webapp/locales/en.json
+++ b/webapp/locales/en.json
@@ -739,13 +739,18 @@
"unmuted": "{name} is unmuted again"
},
"name": "Settings",
+ "notifications": {
+ "name": "Notifications",
+ "send-email-notifications": "Send e-mail 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..a2828a1a9
--- /dev/null
+++ b/webapp/pages/settings/notifications.vue
@@ -0,0 +1,63 @@
+
+
+ {{ $t('settings.notifications.name') }}
+
+
+
+
+
+ {{ $t('actions.save') }}
+
+
+
+
+