Merge pull request #4623 from Ocelot-Social-Community/4326-send-notification-email

feat: 🍰 Send Notification E-Mail
This commit is contained in:
Wolfgang Huß 2021-09-29 08:31:39 +02:00 committed by GitHub
commit df4a5b7390
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
35 changed files with 588 additions and 69 deletions

View File

@ -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) => {

View File

@ -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()
}
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,84 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Benachrichtigung" -->
<!-- Email Body German : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hallo {{ name }},</h1>
<p style="margin: 0;">Du hast mindestens eine Benachrichtigung erhalten. Klick auf diesen Button,
um sie anzusehen:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">Benachrichtigungen
ansehen</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">Bis bald bei <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> Dein {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: Möchtest du keine E-Mails mehr erhalten, dann ändere deine <a href="{{{ ORGANIZATION_URL }}}/settings/notifications"
style="color: #17b53e;">Benachrichtigungseinstellungen</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body German : END -->

View File

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

View File

@ -0,0 +1,83 @@
<!-- emailSubject: "{{APPLICATION_NAME}} Notification" -->
<!-- Email Body English : BEGIN -->
<table class="email-german" align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%"
style="margin: auto;">
<!-- Hero Image, Flush : BEGIN -->
<tr>
<td style="background-color: #ffffff;">
<img
src="{{{ welcomeImageUrl }}}"
width="300" height="" alt="Welcome image" border="0"
style="width: 100%; max-width: 300px; height: auto; background: #ffffff; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; color: #555555; margin: auto; display: block; padding: 20px;"
class="g-img">
</td>
</tr>
<!-- Hero Image, Flush : END -->
<!-- 1 Column Text + Button : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td
style="padding: 20px; padding-top: 0; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<h1
style="margin: 0 0 10px 0; font-family: Lato, sans-serif; font-size: 25px; line-height: 30px; color: #333333; font-weight: normal;">
Hello {{ name }},</h1>
<p style="margin: 0;">You received at least one notification. Click on this button to view them:</p>
</td>
</tr>
<tr>
<td style="padding: 0 20px;">
<!-- Button : BEGIN -->
<table align="center" role="presentation" cellspacing="0" cellpadding="0" border="0" style="margin: auto;">
<tr>
<td class="button-td button-td-primary" style="border-radius: 4px; background: #17b53e;">
<a class="button-a button-a-primary" href="{{{ actionUrl }}}"
style="background: #17b53e; font-family: Lato, sans-serif; font-size: 16px; line-height: 15px; text-decoration: none; padding: 13px 17px; color: #ffffff; display: block; border-radius: 4px;">View
notifications</a>
</td>
</tr>
</table>
<!-- Button : END -->
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text + Button : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">See you soon on <a href="{{{ ORGANIZATION_URL }}}"
style="color: #17b53e;">{{APPLICATION_NAME}}</a>!</p>
<p style="margin: 0; margin-bottom: 10px;"> The {{APPLICATION_NAME}} Team</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
<!-- 1 Column Text : BEGIN -->
<tr>
<td style="background-color: #ffffff; padding: 0 20px;">
<table role="presentation" cellspacing="0" cellpadding="0" border="0" width="100%">
<tr>
<td style="padding: 20px; font-family: Lato, sans-serif; font-size: 16px; line-height: 22px; color: #555555;">
<p style="margin: 0; margin-top: 10px;">PS: If you don't want to receive e-mails anymore, change your <a href="{{{ ORGANIZATION_URL }}}/settings/notifications"
style="color: #17b53e;">notification settings</a>.</p>
</td>
</tr>
</table>
</td>
</tr>
<!-- 1 Column Text : END -->
</table>
<!-- Email Body English : END -->

View File

@ -159,7 +159,7 @@
<td>
<![endif]-->
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">English version below!</p>
<p style="color:#19c243; font-style: italic; font-family: Lato, sans-serif; font-size: 16px; padding-top: 20px;">{{englishHint}}</p>
{{> content}}
@ -169,10 +169,11 @@
<tr>
<td
style="padding: 20px; font-family: Lato, sans-serif; font-size: 12px; line-height: 15px; text-align: center; color: #888888;">
<br><br>
{{ORGANIZATION_NAME}}
<br>{{ORGANIZATION_URL}}<br>
<br><br>
<br>
<a href="{{{ ORGANIZATION_URL }}}" target="_blank" style="color: #17b53e;">{{ORGANIZATION_NAME}}</a>
<br>
<br>
<br>
</td>
</tr>
</table>

View File

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

View File

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

View File

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

View File

@ -153,6 +153,10 @@ export default {
type: 'boolean',
default: false,
},
sendNotificationEmails: {
type: 'boolean',
default: true,
},
locale: {
type: 'string',
allow: [null],

View File

@ -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 {.*}
`

View File

@ -290,6 +290,7 @@ export default {
'termsAndConditionsAgreedAt',
'allowEmbedIframes',
'showShoutsPublicly',
'sendNotificationEmails',
'locale',
],
boolean: {

View File

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

View File

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

View File

@ -0,0 +1,5 @@
import { When } from "cypress-cucumber-preprocessor/steps";
When("I click on element with ID {string}", (id) => {
cy.get('#' + id).click()
})

View File

@ -0,0 +1,5 @@
import { Then } from "cypress-cucumber-preprocessor/steps";
Then("I click save", () => {
cy.get(".save-button").click()
})

View File

@ -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()
})

View File

@ -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);
});

View File

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

View File

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

View File

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

View File

@ -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": {

View File

@ -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": {

View File

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

View File

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

View File

@ -0,0 +1,63 @@
<template>
<base-card>
<h2 class="title">{{ $t('settings.notifications.name') }}</h2>
<ds-space margin-bottom="small">
<input id="send-email" type="checkbox" v-model="notifyByEmail" />
<label for="send-email">{{ $t('settings.notifications.send-email-notifications') }}</label>
</ds-space>
<base-button class="save-button" filled @click="submit" :disabled="disabled">
{{ $t('actions.save') }}
</base-button>
</base-card>
</template>
<script>
import { mapGetters, mapMutations } from 'vuex'
import { updateUserMutation } from '~/graphql/User'
export default {
data() {
return {
notifyByEmail: false,
}
},
computed: {
...mapGetters({
currentUser: 'auth/user',
}),
disabled() {
return this.notifyByEmail === this.currentUser.sendNotificationEmails
},
},
created() {
this.notifyByEmail = this.currentUser.sendNotificationEmails || false
},
methods: {
...mapMutations({
setCurrentUser: 'auth/SET_USER',
}),
async submit() {
try {
await this.$apollo.mutate({
mutation: updateUserMutation(),
variables: {
id: this.currentUser.id,
sendNotificationEmails: this.notifyByEmail,
},
update: (_, { data: { UpdateUser } }) => {
const { sendNotificationEmails } = UpdateUser
this.setCurrentUser({
...this.currentUser,
sendNotificationEmails,
})
this.$toast.success(this.$t('settings.notifications.success-update'))
},
})
} catch (error) {
this.notifyByEmail = !this.notifyByEmail
this.$toast.error(error.message)
}
},
},
}
</script>