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