complex email notification settings

This commit is contained in:
Ulf Gebhardt 2025-04-05 03:37:59 +02:00
parent 16ecfab216
commit 14fefd2cfc
Signed by: ulfgebhardt
GPG Key ID: DA6B843E748679C9
9 changed files with 237 additions and 32 deletions

View File

@ -70,7 +70,13 @@ Factory.define('basicUser')
termsAndConditionsAgreedAt: '2019-08-01T10:47:19.212Z',
allowEmbedIframes: false,
showShoutsPublicly: false,
sendNotificationEmails: true,
emailNotificationsCommentOnObservedPost: true,
emailNotificationsPostByFollowedUser: true,
emailNotificationsPostInGroup: true,
emailNotificationsGroupMemberJoined: true,
emailNotificationsGroupMemberLeft: true,
emailNotificationsGroupMemberRemoved: true,
emailNotificationsGroupMemberRoleChanged: true,
locale: 'en',
})
.attr('slug', ['slug', 'name'], (slug, name) => {

View File

@ -53,19 +53,25 @@ const createDefaultAdminUser = async (session) => {
`MERGE (e:EmailAddress {
email: "${defaultAdmin.email}",
createdAt: toString(datetime())
})-[:BELONGS_TO]->(u:User {
name: "${defaultAdmin.name}",
encryptedPassword: "${defaultAdmin.password}",
role: "admin",
id: "${defaultAdmin.id}",
slug: "${defaultAdmin.slug}",
createdAt: toString(datetime()),
allowEmbedIframes: false,
showShoutsPublicly: false,
sendNotificationEmails: true,
deleted: false,
disabled: false
})-[:PRIMARY_EMAIL]->(e)`,
})-[:BELONGS_TO]->(u:User {
name: "${defaultAdmin.name}",
encryptedPassword: "${defaultAdmin.password}",
role: "admin",
id: "${defaultAdmin.id}",
slug: "${defaultAdmin.slug}",
createdAt: toString(datetime()),
allowEmbedIframes: false,
showShoutsPublicly: false,
emailNotificationsCommentOnObservedPost: true
emailNotificationsPostByFollowedUser: true
emailNotificationsPostInGroup: true
emailNotificationsGroupMemberJoined: true
emailNotificationsGroupMemberLeft: true
emailNotificationsGroupMemberRemoved: true
emailNotificationsGroupMemberRoleChanged: true
deleted: false,
disabled: false
})-[:PRIMARY_EMAIL]->(e)`,
)
})
try {

View File

@ -0,0 +1,67 @@
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.emailNotificationsCommentOnObservedPost = user.sendNotificationEmails
SET user.emailNotificationsPostByFollowedUser = user.sendNotificationEmails
SET user.emailNotificationsPostInGroup = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberJoined = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberLeft = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberRemoved = user.sendNotificationEmails
SET user.emailNotificationsGroupMemberRoleChanged = user.sendNotificationEmails
REMOVE user.sendNotificationEmails
`)
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)
SET user.sendNotificationEmails = true
REMOVE user.emailNotificationsCommentOnObservedPost
REMOVE user.emailNotificationsPostByFollowedUser
REMOVE user.emailNotificationsPostInGroup
REMOVE user.emailNotificationsGroupMemberJoined
REMOVE user.emailNotificationsGroupMemberLeft
REMOVE user.emailNotificationsGroupMemberRemoved
REMOVE user.emailNotificationsGroupMemberRoleChanged
`)
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

@ -31,7 +31,7 @@ const queryNotificationEmails = async (context, notificationUserIds) => {
}
}
const publishNotifications = async (context, promises) => {
const publishNotifications = async (context, promises, emailNotificationSetting: string) => {
let notifications = await Promise.all(promises)
notifications = notifications.flat()
const notificationsEmailAddresses = await queryNotificationEmails(
@ -40,7 +40,7 @@ const publishNotifications = async (context, promises) => {
)
notifications.forEach((notificationAdded, index) => {
pubsub.publish(NOTIFICATION_ADDED, { notificationAdded })
if (notificationAdded.to.sendNotificationEmails) {
if (notificationAdded.to[emailNotificationSetting] ?? true) { // Default to true
sendMail(
notificationTemplate({
email: notificationsEmailAddresses[index].email,
@ -57,7 +57,7 @@ const handleJoinGroup = async (resolve, root, args, context, resolveInfo) => {
if (user) {
await publishNotifications(context, [
notifyOwnersOfGroup(groupId, userId, 'user_joined_group', context),
])
], 'emailNotificationsGroupMemberJoined')
}
return user
}
@ -68,7 +68,7 @@ const handleLeaveGroup = async (resolve, root, args, context, resolveInfo) => {
if (user) {
await publishNotifications(context, [
notifyOwnersOfGroup(groupId, userId, 'user_left_group', context),
])
], 'emailNotificationsGroupMemberLeft')
}
return user
}
@ -79,7 +79,7 @@ const handleChangeGroupMemberRole = async (resolve, root, args, context, resolve
if (user) {
await publishNotifications(context, [
notifyMemberOfGroup(groupId, userId, 'changed_group_member_role', context),
])
], 'emailNotificationsGroupMemberRoleChanged')
}
return user
}
@ -90,7 +90,7 @@ const handleRemoveUserFromGroup = async (resolve, root, args, context, resolveIn
if (user) {
await publishNotifications(context, [
notifyMemberOfGroup(groupId, userId, 'removed_user_from_group', context),
])
], 'emailNotificationsGroupMemberRemoved')
}
return user
}
@ -101,7 +101,7 @@ const handleContentDataOfPost = async (resolve, root, args, context, resolveInfo
if (post) {
await publishNotifications(context, [
notifyUsersOfMention('Post', post.id, idsOfUsers, 'mentioned_in_post', context),
])
], 'TODO')
}
return post
}
@ -120,11 +120,20 @@ const handleContentDataOfComment = async (resolve, root, args, context, resolveI
'mentioned_in_comment',
context,
),
], 'TODO')
await publishNotifications(context, [
notifyUsersOfComment('Comment', comment.id, 'commented_on_post', context),
])
], 'emailNotificationsCommentOnObservedPost')
return comment
}
/* TODO unused
emailNotificationsPostByFollowedUser: true
emailNotificationsPostInGroup: true
*/
const postAuthorOfComment = async (commentId, { context }) => {
const session = context.driver.session()
let postAuthorId

View File

@ -155,10 +155,37 @@ export default {
type: 'boolean',
default: false,
},
sendNotificationEmails: {
// emailNotifications
emailNotificationsCommentOnObservedPost: {
type: 'boolean',
default: true,
},
emailNotificationsPostByFollowedUser: {
type: 'boolean',
default: true,
},
emailNotificationsPostInGroup: {
type: 'boolean',
default: true,
},
emailNotificationsGroupMemberJoined: {
type: 'boolean',
default: true,
},
emailNotificationsGroupMemberLeft: {
type: 'boolean',
default: true,
},
emailNotificationsGroupMemberRemoved: {
type: 'boolean',
default: true,
},
emailNotificationsGroupMemberRoleChanged: {
type: 'boolean',
default: true,
},
locale: {
type: 'string',
allow: [null],

View File

@ -98,7 +98,13 @@ const signupCypher = (inviteCode) => {
SET user.updatedAt = toString(datetime())
SET user.allowEmbedIframes = false
SET user.showShoutsPublicly = false
SET user.sendNotificationEmails = true
SET user.emailNotificationsCommentOnObservedPost = true
SET user.emailNotificationsPostByFollowedUser = true
SET user.emailNotificationsPostInGroup = true
SET user.emailNotificationsGroupMemberJoined = true
SET user.emailNotificationsGroupMemberLeft = true
SET user.emailNotificationsGroupMemberRemoved = true
SET user.emailNotificationsGroupMemberRoleChanged = true
SET email.verifiedAt = toString(datetime())
WITH user
OPTIONAL MATCH (post:Post)-[:IN]->(group:Group)

View File

@ -150,6 +150,30 @@ export default {
}
params.termsAndConditionsAgreedAt = new Date().toISOString()
}
const {
emailNotificationSettings,
}: { emailNotificationSettings: { name: string; value: boolean }[] | undefined } = params
if (emailNotificationSettings) {
emailNotificationSettings.forEach((setting) => {
const allowedSettingNames = [
'commentOnObservedPost',
'postByFollowedUser',
'postInGroup',
'groupMemberJoined',
'groupMemberLeft',
'groupMemberRemoved',
'groupMemberRoleChanged',
]
if (!allowedSettingNames.includes(setting.name)) {
return
}
params[
'emailNotifications' + setting.name.charAt(0).toUpperCase() + setting.name.slice(1)
] = setting.value
})
}
const session = context.driver.session()
const writeTxResultPromise = session.writeTransaction(async (transaction) => {
@ -355,6 +379,44 @@ export default {
const [{ email }] = result.records.map((r) => r.get('e').properties)
return email
},
emailNotificationSettings: async (parent, params, context, resolveInfo) => {
const { user } = context
const { id } = parent
// Its not the own user
if (user.id !== id) {
return []
}
return [
{
name: 'commentOnObservedPost',
type: 'post',
value: user.emailNotificationsCommentOnObservedPost ?? true,
},
{
name: 'postByFollowedUser',
type: 'post',
value: user.emailNotificationsPostByFollowedUser ?? true,
},
{ name: 'postInGroup', type: 'post', value: user.emailNotificationsPostInGroup ?? true},
{
name: 'groupMemberJoined',
type: 'group',
value: user.emailNotificationsGroupMemberJoined ?? true,
},
{ name: 'groupMemberLeft', type: 'group', value: user.emailNotificationsGroupMemberLeft ?? true},
{
name: 'groupMemberRemoved',
type: 'group',
value: user.emailNotificationsGroupMemberRemoved ?? true,
},
{
name: 'groupMemberRoleChanged',
type: 'group',
value: user.emailNotificationsGroupMemberRoleChanged ?? true,
},
]
},
...Resolver('User', {
undefinedToNull: [
'actorId',
@ -366,7 +428,6 @@ export default {
'termsAndConditionsAgreedAt',
'allowEmbedIframes',
'showShoutsPublicly',
'sendNotificationEmails',
'locale',
],
boolean: {

View File

@ -19,6 +19,17 @@ enum _UserOrdering {
locale_desc
}
input EmailNotificationSettingsInput {
name: String
value: Boolean
}
type EmailNotificationSettings {
name: String
type: String
value: Boolean
}
type User {
id: ID!
actorId: String
@ -46,7 +57,7 @@ type User {
allowEmbedIframes: Boolean
showShoutsPublicly: Boolean
sendNotificationEmails: Boolean
emailNotificationSettings: [EmailNotificationSettings]!
locale: String
friends: [User]! @relation(name: "FRIENDS", direction: "BOTH")
friendsCount: Int! @cypher(statement: "MATCH (this)<-[:FRIENDS]->(r:User) RETURN COUNT(DISTINCT r)")
@ -206,7 +217,7 @@ type Mutation {
termsAndConditionsAgreedAt: String
allowEmbedIframes: Boolean
showShoutsPublicly: Boolean
sendNotificationEmails: Boolean
emailNotificationSettings: [EmailNotificationSettingsInput]
locale: String
): User

View File

@ -46,7 +46,11 @@ export const profileUserQuery = (i18n) => {
url
}
showShoutsPublicly
sendNotificationEmails
emailNotificationSettings {
name
type
value
}
}
}
`
@ -335,7 +339,7 @@ export const updateUserMutation = () => {
$about: String
$allowEmbedIframes: Boolean
$showShoutsPublicly: Boolean
$sendNotificationEmails: Boolean
$emailNotificationSettings: [EmailNotificationSettingsInput]
$termsAndConditionsAgreedVersion: String
$avatar: ImageInput
$locationName: String # empty string '' sets it to null
@ -347,7 +351,7 @@ export const updateUserMutation = () => {
about: $about
allowEmbedIframes: $allowEmbedIframes
showShoutsPublicly: $showShoutsPublicly
sendNotificationEmails: $sendNotificationEmails
emailNotificationSettings: $emailNotificationSettings
termsAndConditionsAgreedVersion: $termsAndConditionsAgreedVersion
avatar: $avatar
locationName: $locationName
@ -359,7 +363,11 @@ export const updateUserMutation = () => {
about
allowEmbedIframes
showShoutsPublicly
sendNotificationEmails
emailNotificationSettings {
name
type
value
}
locale
termsAndConditionsAgreedVersion
avatar {
@ -390,7 +398,11 @@ export const currentUserQuery = gql`
locale
allowEmbedIframes
showShoutsPublicly
sendNotificationEmails
emailNotificationSettings {
name
type
value
}
termsAndConditionsAgreedVersion
socialMedia {
id