diff --git a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
index 35154edf6..2af0c9365 100644
--- a/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
+++ b/app/javascript/dashboard/components/widgets/WootWriter/Editor.vue
@@ -1,55 +1,113 @@
-
+
diff --git a/app/javascript/dashboard/i18n/locale/en/generalSettings.json b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
index 684645dda..d5e8bd50b 100644
--- a/app/javascript/dashboard/i18n/locale/en/generalSettings.json
+++ b/app/javascript/dashboard/i18n/locale/en/generalSettings.json
@@ -68,7 +68,8 @@
"TYPE_LABEL": {
"conversation_creation": "New conversation",
"conversation_assignment": "Conversation Assigned",
- "assigned_conversation_new_message": "New Message"
+ "assigned_conversation_new_message": "New Message",
+ "conversation_mention": "Mention"
}
}
}
diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json
index 3c0280131..779ad71c8 100644
--- a/app/javascript/dashboard/i18n/locale/en/settings.json
+++ b/app/javascript/dashboard/i18n/locale/en/settings.json
@@ -27,6 +27,7 @@
"NOTE": "Update your email notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send email notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send email notifications when a new conversation is created",
+ "CONVERSATION_MENTION": "Send email notifications when you are mentioned in a conversation",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send email notifications when a new message is created in an assigned conversation"
},
"API": {
@@ -38,6 +39,7 @@
"NOTE": "Update your push notification preferences here",
"CONVERSATION_ASSIGNMENT": "Send push notifications when a conversation is assigned to me",
"CONVERSATION_CREATION": "Send push notifications when a new conversation is created",
+ "CONVERSATION_MENTION": "Send push notifications when you are mentioned in a conversation",
"ASSIGNED_CONVERSATION_NEW_MESSAGE": "Send push notifications when a new message is created in an assigned conversation",
"HAS_ENABLED_PUSH": "You have enabled push for this browser.",
"REQUEST_PUSH": "Enable push notifications"
diff --git a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationTable.vue b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationTable.vue
index e843ff4fb..6521b2551 100644
--- a/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationTable.vue
+++ b/app/javascript/dashboard/routes/dashboard/notifications/components/NotificationTable.vue
@@ -17,27 +17,23 @@
@click="() => onClickNotification(notificationItem)"
>
-
-
-
-
- {{ `#${notificationItem.id}` }}
-
-
- {{ notificationItem.push_message_title }}
-
-
+
+
+ {{
+ `#${
+ notificationItem.primary_actor
+ ? notificationItem.primary_actor.id
+ : 'deleted'
+ }`
+ }}
+
+
+ {{ notificationItem.push_message_title }}
+
-
-
+
+
{{
$t(
`NOTIFICATIONS_PAGE.TYPE_LABEL.${notificationItem.notification_type}`
@@ -45,8 +41,18 @@
}}
-
- {{ dynamicTime(notificationItem.created_at) }}
+
+
+
+
+
+ {{ dynamicTime(notificationItem.created_at) }}
+
@import '~dashboard/assets/scss/mixins';
-.notification--name {
- font-size: var(--font-size-small);
- margin-bottom: 0;
-}
-
.notification--title {
- font-size: var(--font-size-mini);
+ font-size: var(--font-size-small);
margin: 0;
}
@@ -132,7 +133,7 @@ export default {
@include scroll-on-hover;
flex: 1 1;
height: 100%;
- padding: var(--space-normal);
+ padding: var(--space-large) var(--space-larger);
}
.notifications-table {
@@ -153,14 +154,10 @@ export default {
padding-left: var(--space-medium);
}
}
- }
- }
- .notification--thumbnail {
- display: flex;
- align-items: center;
- .user-thumbnail-box {
- margin-right: var(--space-small);
+ &:last-child {
+ border-bottom: 0;
+ }
}
}
}
@@ -179,4 +176,25 @@ export default {
border-radius: 50%;
background: var(--color-woot);
}
+
+.notification--created-at {
+ color: var(--s-700);
+ font-size: var(--font-size-mini);
+}
+
+.notification--type {
+ font-size: var(--font-size-mini);
+}
+
+.thumbnail--column {
+ width: 5.2rem;
+}
+
+.timestamp--column {
+ width: 12rem;
+}
+
+.notification--message-title {
+ color: var(--s-700);
+}
diff --git a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
index 0caf29589..2d5e7b725 100644
--- a/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
+++ b/app/javascript/dashboard/routes/dashboard/settings/profile/NotificationSettings.vue
@@ -44,6 +44,23 @@
+
+
+
+ {{
+ $t(
+ 'PROFILE_SETTINGS.FORM.EMAIL_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
+ )
+ }}
+
+
+
+
+
+
+ {{
+ $t(
+ 'PROFILE_SETTINGS.FORM.PUSH_NOTIFICATIONS_SECTION.CONVERSATION_MENTION'
+ )
+ }}
+
+
+
{
- Vue.set($state.meta, 'unreadCount', count);
+ Vue.set($state.meta, 'unreadCount', count < 0 ? 0 : count);
},
[types.SET_NOTIFICATIONS]: ($state, data) => {
data.forEach(notification => {
diff --git a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
index 4590b7f2f..15a097df7 100644
--- a/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
+++ b/app/javascript/dashboard/store/modules/specs/notifications/mutations.spec.js
@@ -40,6 +40,12 @@ describe('#mutations', () => {
mutations[types.SET_NOTIFICATIONS_UNREAD_COUNT](state, 3);
expect(state.meta).toEqual({ unreadCount: 3 });
});
+
+ it('set notifications unread count to 0 if invalid', () => {
+ const state = { meta: { unreadCount: 4 } };
+ mutations[types.SET_NOTIFICATIONS_UNREAD_COUNT](state, -1);
+ expect(state.meta).toEqual({ unreadCount: 0 });
+ });
});
describe('#SET_NOTIFICATIONS', () => {
diff --git a/app/javascript/shared/helpers/MessageFormatter.js b/app/javascript/shared/helpers/MessageFormatter.js
index 50dd3e227..8f97e559d 100644
--- a/app/javascript/shared/helpers/MessageFormatter.js
+++ b/app/javascript/shared/helpers/MessageFormatter.js
@@ -10,9 +10,11 @@ const TWITTER_HASH_REGEX = /(^|\s)#(\w+)/g;
const TWITTER_HASH_REPLACEMENT =
'$1
#$2 ';
+const USER_MENTIONS_REGEX = /mention:\/\/(user|team)\/(\d+)\/([\w\s]+)/gm;
+
class MessageFormatter {
constructor(message, isATweet = false) {
- this.message = DOMPurify.sanitize(escapeHtml(message) || '');
+ this.message = DOMPurify.sanitize(escapeHtml(message || ''));
this.isATweet = isATweet;
this.marked = marked;
@@ -21,6 +23,10 @@ class MessageFormatter {
return `
${text} `;
},
link(url, title, text) {
+ const mentionRegex = new RegExp(USER_MENTIONS_REGEX);
+ if (url.match(mentionRegex)) {
+ return `
${text} `;
+ }
return `
${text} `;
},
diff --git a/app/listeners/notification_listener.rb b/app/listeners/notification_listener.rb
index 350c9d80a..f591dd671 100644
--- a/app/listeners/notification_listener.rb
+++ b/app/listeners/notification_listener.rb
@@ -31,6 +31,8 @@ class NotificationListener < BaseListener
message, account = extract_message_and_account(event)
conversation = message.conversation
+ generate_notifications_for_mentions(message, account)
+
# only want to notify agents about customer messages
return unless message.incoming?
return unless conversation.assignee
@@ -42,4 +44,28 @@ class NotificationListener < BaseListener
primary_actor: conversation
).perform
end
+
+ private
+
+ def get_valid_mentioned_ids(mentioned_ids, inbox)
+ valid_mentionable_ids = inbox.account.administrators.map(&:id) + inbox.members.map(&:id)
+ # Intersection of ids
+ mentioned_ids & valid_mentionable_ids.uniq.map(&:to_s)
+ end
+
+ def generate_notifications_for_mentions(message, account)
+ return unless message.private?
+
+ mentioned_ids = message.content.scan(%r{\(mention://(user|team)/(\d+)/([\w\s]+)\)}).map(&:second).uniq
+ return if mentioned_ids.blank?
+
+ get_valid_mentioned_ids(mentioned_ids, message.inbox).each do |user_id|
+ NotificationBuilder.new(
+ notification_type: 'conversation_mention',
+ user: User.find(user_id),
+ account: account,
+ primary_actor: message
+ ).perform
+ end
+ end
end
diff --git a/app/mailers/agent_notifications/conversation_notifications_mailer.rb b/app/mailers/agent_notifications/conversation_notifications_mailer.rb
index 1c9c186a6..60e5401da 100644
--- a/app/mailers/agent_notifications/conversation_notifications_mailer.rb
+++ b/app/mailers/agent_notifications/conversation_notifications_mailer.rb
@@ -19,6 +19,16 @@ class AgentNotifications::ConversationNotificationsMailer < ApplicationMailer
send_mail_with_liquid(to: @agent.email, subject: subject) and return
end
+ def conversation_mention(message, agent)
+ return unless smtp_config_set_or_development?
+
+ @agent = agent
+ @conversation = message.conversation
+ subject = "#{@agent.available_name}, You have been mentioned in conversation [ID - #{@conversation.display_id}]"
+ @action_url = app_account_conversation_url(account_id: @conversation.account_id, id: @conversation.display_id)
+ send_mail_with_liquid(to: @agent.email, subject: subject) and return
+ end
+
def assigned_conversation_new_message(conversation, agent)
return unless smtp_config_set_or_development?
# Don't spam with email notifications if agent is online
diff --git a/app/models/notification.rb b/app/models/notification.rb
index 4a8860367..91ed694a6 100644
--- a/app/models/notification.rb
+++ b/app/models/notification.rb
@@ -32,7 +32,8 @@ class Notification < ApplicationRecord
NOTIFICATION_TYPES = {
conversation_creation: 1,
conversation_assignment: 2,
- assigned_conversation_new_message: 3
+ assigned_conversation_new_message: 3,
+ conversation_mention: 4
}.freeze
enum notification_type: NOTIFICATION_TYPES
@@ -67,6 +68,10 @@ class Notification < ApplicationRecord
return "New message in your assigned conversation [ID -#{primary_actor.display_id}]." if notification_type == 'assigned_conversation_new_message'
+ if notification_type == 'conversation_mention'
+ return "You have been mentioned in conversation [ID -#{primary_actor.conversation.display_id}] by #{secondary_actor.name}"
+ end
+
''
end
diff --git a/app/views/api/v1/accounts/notifications/index.json.jbuilder b/app/views/api/v1/accounts/notifications/index.json.jbuilder
index 8897baa7f..9180fe512 100644
--- a/app/views/api/v1/accounts/notifications/index.json.jbuilder
+++ b/app/views/api/v1/accounts/notifications/index.json.jbuilder
@@ -10,9 +10,16 @@ json.data do
json.id notification.id
json.notification_type notification.notification_type
json.push_message_title notification.push_message_title
- json.primary_actor_type notification.primary_actor_type
- json.primary_actor_id notification.primary_actor_id
- json.primary_actor notification.primary_actor&.push_event_data
+ # TODO: front end assumes primary actor to be conversation. should fix in future
+ if notification.notification_type == 'conversation_mention'
+ json.primary_actor_type 'Conversation'
+ json.primary_actor_id notification.primary_actor.conversation_id
+ json.primary_actor notification.primary_actor&.conversation&.push_event_data
+ else
+ json.primary_actor_type notification.primary_actor_type
+ json.primary_actor_id notification.primary_actor_id
+ json.primary_actor notification.primary_actor&.push_event_data
+ end
json.read_at notification.read_at
json.secondary_actor notification.secondary_actor&.push_event_data
json.user notification.user&.push_event_data
diff --git a/app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_mention.liquid b/app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_mention.liquid
new file mode 100644
index 000000000..7bddf270d
--- /dev/null
+++ b/app/views/mailers/agent_notifications/conversation_notifications_mailer/conversation_mention.liquid
@@ -0,0 +1,8 @@
+
Hi {{user.available_name}}
+
+
+
Time to save the world. You have been mentioned in a conversation
+
+
+Click here to get cracking.
+
diff --git a/package.json b/package.json
index c2d140169..873bb16f7 100644
--- a/package.json
+++ b/package.json
@@ -12,7 +12,7 @@
"start:dev": "foreman start -f ./Procfile.dev"
},
"dependencies": {
- "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#main",
+ "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#45e4efcc150e6674be02bc524061373b39cfed40",
"@rails/actioncable": "^6.0.0",
"@rails/webpacker": "^5.2.0",
"axios": "^0.21.1",
diff --git a/spec/listeners/notification_listener_spec.rb b/spec/listeners/notification_listener_spec.rb
index 442d697e2..10a936d5a 100644
--- a/spec/listeners/notification_listener_spec.rb
+++ b/spec/listeners/notification_listener_spec.rb
@@ -43,4 +43,37 @@ describe NotificationListener do
end
end
end
+
+ describe 'message_created' do
+ let(:event_name) { :'message.created' }
+
+ context 'when message contains mention' do
+ it 'creates notifications for inbox member who was mentioned' do
+ notification_setting = agent_with_notification.notification_settings.find_by(account_id: account.id)
+ notification_setting.selected_email_flags = [:email_conversation_mention]
+ notification_setting.selected_push_flags = []
+ notification_setting.save!
+
+ builder = double
+ allow(NotificationBuilder).to receive(:new).and_return(builder)
+ allow(builder).to receive(:perform)
+
+ create(:inbox_member, user: agent_with_notification, inbox: inbox)
+ create(:inbox_member, user: agent_with_out_notification, inbox: inbox)
+ conversation.reload
+
+ message = build(:message, conversation: conversation, account: account,
+ content: "hi [#{agent_with_notification.name}](mention://user/#{agent_with_notification.id}/\
+ #{agent_with_notification.name})", private: true)
+
+ event = Events::Base.new(event_name, Time.zone.now, message: message)
+ listener.message_created(event)
+
+ expect(NotificationBuilder).to have_received(:new).with(notification_type: 'conversation_mention',
+ user: agent_with_notification,
+ account: account,
+ primary_actor: message)
+ end
+ end
+ end
end
diff --git a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
index 9182da9d8..d44824892 100644
--- a/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
+++ b/spec/mailers/agent_notifications/conversation_notifications_mailer_spec.rb
@@ -4,8 +4,9 @@ require 'rails_helper'
RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :mailer do
let(:class_instance) { described_class.new }
- let(:agent) { create(:user, email: 'agent1@example.com') }
- let(:conversation) { create(:conversation, assignee: agent) }
+ let!(:account) { create(:account) }
+ let(:agent) { create(:user, email: 'agent1@example.com', account: account) }
+ let(:conversation) { create(:conversation, assignee: agent, account: account) }
before do
allow(described_class).to receive(:new).and_return(class_instance)
@@ -37,6 +38,19 @@ RSpec.describe AgentNotifications::ConversationNotificationsMailer, type: :maile
end
end
+ describe 'conversation_mention' do
+ let(:message) { create(:message, conversation: conversation, account: account) }
+ let(:mail) { described_class.conversation_mention(message, agent).deliver_now }
+
+ it 'renders the subject' do
+ expect(mail.subject).to eq("#{agent.available_name}, You have been mentioned in conversation [ID - #{conversation.display_id}]")
+ end
+
+ it 'renders the receiver email' do
+ expect(mail.to).to eq([agent.email])
+ end
+ end
+
describe 'assigned_conversation_new_message' do
let(:mail) { described_class.assigned_conversation_new_message(conversation, agent).deliver_now }
diff --git a/yarn.lock b/yarn.lock
index adea29adc..a99bc167e 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -857,9 +857,9 @@
resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39"
integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==
-"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#main":
+"@chatwoot/prosemirror-schema@https://github.com/chatwoot/prosemirror-schema.git#45e4efcc150e6674be02bc524061373b39cfed40":
version "1.0.0"
- resolved "https://github.com/chatwoot/prosemirror-schema.git#0a5bb8130df9591faa9c997b1371c5b7c06af691"
+ resolved "https://github.com/chatwoot/prosemirror-schema.git#45e4efcc150e6674be02bc524061373b39cfed40"
dependencies:
prosemirror-commands "^1.1.4"
prosemirror-dropcursor "^1.3.2"