mirror of
https://github.com/IT4Change/Ocelot-Social.git
synced 2026-01-14 17:04:41 +00:00
Merge pull request #4623 from Ocelot-Social-Community/4326-send-notification-email
feat: 🍰 Send Notification E-Mail
This commit is contained in:
commit
df4a5b7390
@ -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) => {
|
||||
|
||||
@ -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()
|
||||
}
|
||||
}
|
||||
39
backend/src/middleware/helpers/email/sendMail.js
Normal file
39
backend/src/middleware/helpers/email/sendMail.js
Normal 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
|
||||
@ -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 }),
|
||||
}
|
||||
}
|
||||
@ -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')
|
||||
@ -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 -->
|
||||
@ -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')
|
||||
@ -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 -->
|
||||
@ -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>
|
||||
@ -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',
|
||||
|
||||
@ -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
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -153,6 +153,10 @@ export default {
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
},
|
||||
sendNotificationEmails: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
},
|
||||
locale: {
|
||||
type: 'string',
|
||||
allow: [null],
|
||||
|
||||
@ -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 {.*}
|
||||
`
|
||||
|
||||
@ -290,6 +290,7 @@ export default {
|
||||
'termsAndConditionsAgreedAt',
|
||||
'allowEmbedIframes',
|
||||
'showShoutsPublicly',
|
||||
'sendNotificationEmails',
|
||||
'locale',
|
||||
],
|
||||
boolean: {
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
20
cypress/integration/User.SettingNotifications.feature
Normal file
20
cypress/integration/User.SettingNotifications.feature
Normal 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!"
|
||||
@ -0,0 +1,5 @@
|
||||
import { When } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
When("I click on element with ID {string}", (id) => {
|
||||
cy.get('#' + id).click()
|
||||
})
|
||||
@ -0,0 +1,5 @@
|
||||
import { Then } from "cypress-cucumber-preprocessor/steps";
|
||||
|
||||
Then("I click save", () => {
|
||||
cy.get(".save-button").click()
|
||||
})
|
||||
@ -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()
|
||||
})
|
||||
@ -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);
|
||||
});
|
||||
@ -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)
|
||||
})
|
||||
@ -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 },
|
||||
})
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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": {
|
||||
|
||||
@ -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`,
|
||||
|
||||
70
webapp/pages/settings/notifications.spec.js
Normal file
70
webapp/pages/settings/notifications.spec.js
Normal 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)
|
||||
})
|
||||
})
|
||||
})
|
||||
63
webapp/pages/settings/notifications.vue
Normal file
63
webapp/pages/settings/notifications.vue
Normal 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>
|
||||
Loading…
x
Reference in New Issue
Block a user