diff --git a/client/components/user-settings/Info.vue b/client/components/user-settings/Info.vue
index e2ac03840..68a8f0ca1 100644
--- a/client/components/user-settings/Info.vue
+++ b/client/components/user-settings/Info.vue
@@ -41,6 +41,21 @@
outlined
class="required" />
+
+
+
+
+
+
Cancel
@@ -58,14 +73,17 @@
+
+
diff --git a/server/activity/status.hooks.js b/server/activity/status.hooks.js
index ed6d6aa6b..06d23591f 100644
--- a/server/activity/status.hooks.js
+++ b/server/activity/status.hooks.js
@@ -37,7 +37,8 @@ exports.add = (ActivityStatus, Hooks, { Activity }) => {
});
const isUnchanged = previousStatus.assigneeId === status.assigneeId;
const isSelfAssign = status.assigneeId === userId;
- if (isUnchanged || isSelfAssign) return;
+ const isDisabled = !status.assignee.notifications.assignment;
+ if (isUnchanged || isSelfAssign || isDisabled) return;
sendEmailNotification(activity);
}
diff --git a/server/comment/hooks.js b/server/comment/hooks.js
index fb6145301..e38386da6 100644
--- a/server/comment/hooks.js
+++ b/server/comment/hooks.js
@@ -1,7 +1,6 @@
'use strict';
const mail = require('../shared/mail');
-const map = require('lodash/map');
const pick = require('lodash/pick');
const { schema } = require('@tailor-cms/config');
const sse = require('../shared/sse');
@@ -80,7 +79,10 @@ exports.add = (Comment, Hooks, db) => {
action: isCreate ? 'left' : 'updated',
...pick(comment, ['id', 'content', 'createdAt'])
};
- const collaborators = map(repository.repositoryUsers, 'user.email');
+ const collaborators = repository.repositoryUsers.reduce((acc, { user }) => {
+ if (user.notifications.comment) acc.push(user.email);
+ return acc;
+ }, []);
const recipients = without(collaborators, author.email);
if (recipients.length) mail.sendCommentNotification(recipients, data);
}
diff --git a/server/shared/database/migrations/20210614092852-alter-user-add-notifications.js b/server/shared/database/migrations/20210614092852-alter-user-add-notifications.js
new file mode 100644
index 000000000..61d689acf
--- /dev/null
+++ b/server/shared/database/migrations/20210614092852-alter-user-add-notifications.js
@@ -0,0 +1,16 @@
+'use strict';
+
+const TABLE_NAME = 'user';
+const COLUMN_NAME = 'notifications';
+
+module.exports = {
+ up: (queryInterface, { JSONB }) => {
+ return queryInterface.addColumn(TABLE_NAME, COLUMN_NAME, {
+ type: JSONB,
+ defaultValue: { comment: true, assignment: true }
+ });
+ },
+ down: queryInterface => {
+ return queryInterface.removeColumn(TABLE_NAME, COLUMN_NAME);
+ }
+};
diff --git a/server/shared/mail/index.js b/server/shared/mail/index.js
index db1a2b749..2f08bbeb1 100644
--- a/server/shared/mail/index.js
+++ b/server/shared/mail/index.js
@@ -30,6 +30,7 @@ const elementUrl = ({ repositoryId, activityId, elementUid }) => {
const query = `${activityId}?elementId=${elementUid}`;
return urlJoin(origin, '/#/repository', `${repositoryId}/editor`, query);
};
+const settingsUrl = () => urlJoin(origin, '/#/settings');
module.exports = {
send,
@@ -81,6 +82,7 @@ function sendCommentNotification(users, comment) {
const data = {
href,
origin,
+ unsubscribeLink: settingsUrl(),
getInitials: () => (text, render) => render(text).substr(0, 2).toUpperCase(),
...comment
};
@@ -101,6 +103,7 @@ function sendAssigneeNotification(assignee, activity) {
const data = {
...activity,
origin,
+ unsubscribeLink: settingsUrl(),
href: activityStatusUrl(activity.repositoryId, activity.id)
};
const html = renderHtml(path.join(templatesDir, 'assignee.mjml'), data);
diff --git a/server/shared/mail/templates/assignee.mjml b/server/shared/mail/templates/assignee.mjml
index 343f9e40e..b9a24d5cd 100644
--- a/server/shared/mail/templates/assignee.mjml
+++ b/server/shared/mail/templates/assignee.mjml
@@ -27,5 +27,16 @@
+
+
+
+ You can
+
+ Unsubscribe
+
+ from assignment notifications in the profile page.
+
+
+
diff --git a/server/shared/mail/templates/assignee.txt b/server/shared/mail/templates/assignee.txt
index 1b62daf45..61101ff31 100644
--- a/server/shared/mail/templates/assignee.txt
+++ b/server/shared/mail/templates/assignee.txt
@@ -9,5 +9,9 @@ View the {{label}} status by visiting the link:
Or copy and paste this URL into your browser.
+You can unsubscribe from assignment notifications in the profile page by visiting the link:
+
+{{unsubscribeLink}}
+
-------------------------------------------------
diff --git a/server/shared/mail/templates/comment.mjml b/server/shared/mail/templates/comment.mjml
index dcb88c2ad..cd25a1a11 100644
--- a/server/shared/mail/templates/comment.mjml
+++ b/server/shared/mail/templates/comment.mjml
@@ -62,5 +62,16 @@
+
+
+
+ You can
+
+ Unsubscribe
+
+ from comment notifications in the profile page.
+
+
+
diff --git a/server/shared/mail/templates/comment.txt b/server/shared/mail/templates/comment.txt
index cd20817c2..73807bd00 100644
--- a/server/shared/mail/templates/comment.txt
+++ b/server/shared/mail/templates/comment.txt
@@ -11,4 +11,8 @@ View {{activityLabel}} by clicking the URL below:
Or copy and paste this URL into your browser.
+You can unsubscribe from comment notifications in the profile page by visiting the link:
+
+{{unsubscribeLink}}
+
-------------------------------------------------
diff --git a/server/user/user.controller.js b/server/user/user.controller.js
index c42a9c3a6..aa83292b3 100644
--- a/server/user/user.controller.js
+++ b/server/user/user.controller.js
@@ -48,8 +48,8 @@ function getProfile({ user, authData }, res) {
}
function updateProfile({ user, body }, res) {
- const { email, firstName, lastName, imgUrl } = body;
- return user.update({ email, firstName, lastName, imgUrl })
+ const { email, firstName, lastName, imgUrl, notifications } = body;
+ return user.update({ email, firstName, lastName, imgUrl, notifications })
.then(({ profile }) => res.json({ user: profile }))
.catch(() => validationError(CONFLICT));
}
diff --git a/server/user/user.model.js b/server/user/user.model.js
index 3e36ce8fa..9bc608cd6 100644
--- a/server/user/user.model.js
+++ b/server/user/user.model.js
@@ -18,7 +18,7 @@ const { user: { ADMIN, USER, INTEGRATION } } = roles;
const gravatarConfig = { size: 130, default: 'identicon' };
class User extends Model {
- static fields({ DATE, ENUM, STRING, TEXT, UUID, UUIDV4, VIRTUAL }) {
+ static fields({ DATE, ENUM, JSONB, STRING, TEXT, UUID, UUIDV4, VIRTUAL }) {
return {
uid: {
type: UUID,
@@ -73,12 +73,16 @@ class User extends Model {
return imgUrl || gravatar.url(this.email, gravatarConfig, true /* https */);
}
},
+ notifications: {
+ type: JSONB,
+ defaultValue: { assignment: true, comment: true }
+ },
profile: {
type: VIRTUAL,
get() {
return pick(this, [
'id', 'email', 'role', 'firstName', 'lastName', 'fullName', 'label',
- 'imgUrl', 'createdAt', 'updatedAt', 'deletedAt'
+ 'imgUrl', 'notifications', 'createdAt', 'updatedAt', 'deletedAt'
]);
}
},