From b220663785e7f8a2e93b0670ba9885285e14c567 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 23 Feb 2026 15:40:54 +0400 Subject: [PATCH 1/3] fix: Skip notifications for private notes (#13617) When agents or integrations create private notes on a conversation, every note was sending a notification to the assigned agent and all conversation participants. The fix ensures that private notes no longer trigger new message notifications. If someone explicitly mentions a teammate in a private note, that person will still get notified as expected. --- app/models/concerns/message_filter_helpers.rb | 2 +- spec/listeners/notification_listener_spec.rb | 22 ++++++++++++++++--- .../new_message_notification_service_spec.rb | 8 ++++++- 3 files changed, 27 insertions(+), 5 deletions(-) diff --git a/app/models/concerns/message_filter_helpers.rb b/app/models/concerns/message_filter_helpers.rb index 38124f347..39ee71089 100644 --- a/app/models/concerns/message_filter_helpers.rb +++ b/app/models/concerns/message_filter_helpers.rb @@ -14,7 +14,7 @@ module MessageFilterHelpers end def notifiable? - incoming? || outgoing? + (incoming? || outgoing?) && !private? end def conversation_transcriptable? diff --git a/spec/listeners/notification_listener_spec.rb b/spec/listeners/notification_listener_spec.rb index 76e458b8e..048339ca5 100644 --- a/spec/listeners/notification_listener_spec.rb +++ b/spec/listeners/notification_listener_spec.rb @@ -142,7 +142,24 @@ describe NotificationListener do expect(first_agent.notifications.first.notification_type).to eq('conversation_mention') end - it 'will not create duplicate new message notifications for assignment & participation' do + it 'will create a mention notification when a user is mentioned in a private note' do + create(:inbox_member, user: first_agent, inbox: inbox) + + message = build( + :message, + conversation: conversation, + account: account, + content: "hey [#{first_agent.name}](mention://user/#{first_agent.id}/#{first_agent.name})", + private: true + ) + event = Events::Base.new(event_name, Time.zone.now, message: message) + listener.message_created(event) + + expect(first_agent.notifications.count).to eq(1) + expect(first_agent.notifications.first.notification_type).to eq('conversation_mention') + end + + it 'will not create new message notifications for private messages without mentions' do create(:inbox_member, user: first_agent, inbox: inbox) conversation.update(assignee: first_agent) # participants is created by async job. so creating it directly for testcase @@ -160,8 +177,7 @@ describe NotificationListener do listener.message_created(event) expect(conversation.conversation_participants.map(&:user)).to include(first_agent) - expect(first_agent.notifications.count).to eq(1) - expect(first_agent.notifications.first.notification_type).to eq('assigned_conversation_new_message') + expect(first_agent.notifications.count).to eq(0) end end diff --git a/spec/services/messages/new_message_notification_service_spec.rb b/spec/services/messages/new_message_notification_service_spec.rb index 51da1ad5a..c8e2b7b98 100644 --- a/spec/services/messages/new_message_notification_service_spec.rb +++ b/spec/services/messages/new_message_notification_service_spec.rb @@ -2,11 +2,17 @@ require 'rails_helper' describe Messages::NewMessageNotificationService do context 'when message is not notifiable' do - it 'will not create any notifications' do + it 'will not create any notifications for activity messages' do message = build(:message, message_type: :activity) expect(NotificationBuilder).not_to receive(:new) described_class.new(message: message).perform end + + it 'will not create any notifications for private messages' do + message = build(:message, message_type: :outgoing, private: true) + expect(NotificationBuilder).not_to receive(:new) + described_class.new(message: message).perform + end end context 'when message is notifiable' do From 5b167b5b5b9de36ee1afaec3748a43e5ba08537d Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 23 Feb 2026 19:26:45 +0400 Subject: [PATCH 2/3] fix(contacts): Show telegram id in contact details form (#13611) ## Summary This change fixes a mismatch in contact details where Telegram data could be shown in the contact profile/social icon area but was not available in the editable contact form. ### What changed - Added Telegram to the social links section of the next-gen contact form so agents can view and edit it alongside Facebook, Instagram, TikTok, Twitter, GitHub, and LinkedIn. - Added Telegram support to the legacy conversation contact edit form for parity between both contact editing experiences. - Mapped social_telegram_user_name into the editable socialProfiles payload when preparing contact form state, so Telegram usernames sourced from channel attributes are visible in the form. - Updated the conversation contact social profile merge logic so Telegram display prefers an explicitly saved social profile value and falls back to social_telegram_user_name when needed. - Added the missing English i18n placeholder: Add Telegram. ### Why Without this, users could see Telegram info in some contact views but could not reliably edit it in contact details, creating inconsistent behavior between display and edit states. --------- Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> --- .../Contacts/ContactsForm/ContactsForm.vue | 11 ++++++++++- app/javascript/dashboard/i18n/locale/en/contact.json | 3 +++ .../dashboard/conversation/contact/ContactForm.vue | 4 ++++ .../dashboard/conversation/contact/ContactInfo.vue | 8 ++++++-- 4 files changed, 23 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue index 3dc29738e..66296257f 100644 --- a/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue +++ b/app/javascript/dashboard/components-next/Contacts/ContactsForm/ContactsForm.vue @@ -44,6 +44,7 @@ const SOCIAL_CONFIG = { LINKEDIN: 'i-ri-linkedin-box-fill', FACEBOOK: 'i-ri-facebook-circle-fill', INSTAGRAM: 'i-ri-instagram-line', + TELEGRAM: 'i-ri-telegram-fill', TIKTOK: 'i-ri-tiktok-fill', TWITTER: 'i-ri-twitter-x-fill', GITHUB: 'i-ri-github-fill', @@ -66,6 +67,7 @@ const defaultState = { facebook: '', github: '', instagram: '', + telegram: '', tiktok: '', linkedin: '', twitter: '', @@ -103,9 +105,13 @@ const prepareStateBasedOnProps = () => { countryCode = '', country = '', city = '', + socialTelegramUserName = '', socialProfiles = {}, } = additionalAttributes || {}; + const telegramUsername = + socialProfiles?.telegram || socialTelegramUserName || ''; + Object.assign(state, { id, name, @@ -119,7 +125,10 @@ const prepareStateBasedOnProps = () => { countryCode, country, city, - socialProfiles, + socialProfiles: { + ...socialProfiles, + telegram: telegramUsername, + }, }, }); }; diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index fe04e5848..0d9f79984 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -458,6 +458,9 @@ "INSTAGRAM": { "PLACEHOLDER": "Add Instagram" }, + "TELEGRAM": { + "PLACEHOLDER": "Add Telegram" + }, "TIKTOK": { "PLACEHOLDER": "Add TikTok" }, diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue index 77d4b9773..d8479c4f2 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactForm.vue @@ -58,12 +58,14 @@ export default { twitter: '', linkedin: '', github: '', + telegram: '', }, socialProfileKeys: [ { key: 'facebook', prefixURL: 'https://facebook.com/' }, { key: 'twitter', prefixURL: 'https://twitter.com/' }, { key: 'linkedin', prefixURL: 'https://linkedin.com/' }, { key: 'github', prefixURL: 'https://github.com/' }, + { key: 'telegram', prefixURL: 'https://t.me/' }, { key: 'tiktok', prefixURL: 'https://tiktok.com/@' }, ], }; @@ -175,12 +177,14 @@ export default { const { social_profiles: socialProfiles = {}, screen_name: twitterScreenName, + social_telegram_user_name: telegramUserName, } = additionalAttributes; this.socialProfileUserNames = { twitter: socialProfiles.twitter || twitterScreenName || '', facebook: socialProfiles.facebook || '', linkedin: socialProfiles.linkedin || '', github: socialProfiles.github || '', + telegram: socialProfiles.telegram || telegramUserName || '', instagram: socialProfiles.instagram || '', tiktok: socialProfiles.tiktok || '', }; diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index 7392bea78..3899d1ddf 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -81,10 +81,14 @@ export default { screen_name: twitterScreenName, social_telegram_user_name: telegramUsername, } = this.additionalAttributes; + + const telegram = socialProfiles?.telegram || telegramUsername || ''; + const twitter = socialProfiles?.twitter || twitterScreenName || ''; + return { - twitter: twitterScreenName, - telegram: telegramUsername, ...(socialProfiles || {}), + twitter, + telegram, }; }, // Delete Modal From 2b85275e2693f7ec3b5f03cc7e2e3e5c1b75ba1b Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Tue, 24 Feb 2026 13:32:54 +0530 Subject: [PATCH 3/3] feat: show assignment policy name in auto-assignment activity messages (#13598) --- .../concerns/activity_message_handler.rb | 22 +----------- .../assignee_activity_message_handler.rb | 35 +++++++++++++++++++ .../auto_assignment/assignment_service.rb | 4 +++ config/locales/en.yml | 3 ++ 4 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 app/models/concerns/assignee_activity_message_handler.rb diff --git a/app/models/concerns/activity_message_handler.rb b/app/models/concerns/activity_message_handler.rb index c25aba47f..0300bd2d1 100644 --- a/app/models/concerns/activity_message_handler.rb +++ b/app/models/concerns/activity_message_handler.rb @@ -1,6 +1,7 @@ module ActivityMessageHandler extend ActiveSupport::Concern + include AssigneeActivityMessageHandler include PriorityActivityMessageHandler include LabelActivityMessageHandler include SlaActivityMessageHandler @@ -104,27 +105,6 @@ module ActivityMessageHandler content = I18n.t("conversations.activity.#{change_type}", user_name: Current.user.name) ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content end - - def generate_assignee_change_activity_content(user_name) - params = { assignee_name: assignee&.name || '', user_name: user_name } - key = assignee_id ? 'assigned' : 'removed' - key = 'self_assigned' if self_assign? assignee_id - I18n.t("conversations.activity.assignee.#{key}", **params) - end - - def create_assignee_change_activity(user_name) - user_name = activity_message_owner(user_name) - - return unless user_name - - content = generate_assignee_change_activity_content(user_name) - ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content - end - - def activity_message_owner(user_name) - user_name = I18n.t('automation.system_name') if !user_name && Current.executed_by.present? - user_name - end end ActivityMessageHandler.prepend_mod_with('ActivityMessageHandler') diff --git a/app/models/concerns/assignee_activity_message_handler.rb b/app/models/concerns/assignee_activity_message_handler.rb new file mode 100644 index 000000000..ff5516ec9 --- /dev/null +++ b/app/models/concerns/assignee_activity_message_handler.rb @@ -0,0 +1,35 @@ +module AssigneeActivityMessageHandler + extend ActiveSupport::Concern + + private + + def create_assignee_change_activity(user_name) + user_name = activity_message_owner(user_name) + + return unless user_name + + content = generate_assignee_change_activity_content(user_name) + ::Conversations::ActivityMessageJob.perform_later(self, activity_message_params(content)) if content + end + + def generate_assignee_change_activity_content(user_name) + params = { assignee_name: assignee&.name || '', user_name: user_name } + key = assignee_id ? 'assigned' : 'removed' + key = 'self_assigned' if self_assign? assignee_id + I18n.t("conversations.activity.assignee.#{key}", **params) + end + + def activity_message_owner(user_name) + if !user_name && Current.executed_by.present? + user_name = case Current.executed_by + when AssignmentPolicy + I18n.t('auto_assignment.policy_actor', policy_name: Current.executed_by.name) + when Inbox + I18n.t('auto_assignment.default_policy_name') + else + I18n.t('automation.system_name') + end + end + user_name + end +end diff --git a/app/services/auto_assignment/assignment_service.rb b/app/services/auto_assignment/assignment_service.rb index 89eff9d1c..e27f1e829 100644 --- a/app/services/auto_assignment/assignment_service.rb +++ b/app/services/auto_assignment/assignment_service.rb @@ -72,13 +72,17 @@ class AutoAssignment::AssignmentService end def assign_conversation(conversation, agent) + Current.executed_by = inbox.assignment_policy || inbox conversation.update!(assignee: agent) + Current.executed_by = nil rate_limiter = build_rate_limiter(agent) rate_limiter.track_assignment(conversation) dispatch_assignment_event(conversation, agent) true + ensure + Current.executed_by = nil end def dispatch_assignment_event(conversation, agent) diff --git a/config/locales/en.yml b/config/locales/en.yml index 05276009f..9cab941c2 100644 --- a/config/locales/en.yml +++ b/config/locales/en.yml @@ -438,6 +438,9 @@ en: seconds: one: '%{count} second' other: '%{count} seconds' + auto_assignment: + default_policy_name: 'Default Policy' + policy_actor: 'Automation System via %{policy_name}' automation: system_name: 'Automation System' crm: