From a2e348df06baae9afe600a4e764b629f43ffc4ec Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 12 Jan 2026 11:22:44 +0530 Subject: [PATCH 1/2] fix: Backslash issue with `--` and improve autolink handling (#13208) --- app/javascript/dashboard/constants/editor.js | 5 +-- .../helper/specs/editorHelper.spec.js | 31 +++++++++++++++++++ package.json | 2 +- pnpm-lock.yaml | 10 +++--- 4 files changed, 40 insertions(+), 8 deletions(-) diff --git a/app/javascript/dashboard/constants/editor.js b/app/javascript/dashboard/constants/editor.js index 6b1a99f23..2aa98477e 100644 --- a/app/javascript/dashboard/constants/editor.js +++ b/app/javascript/dashboard/constants/editor.js @@ -237,10 +237,11 @@ export const MARKDOWN_PATTERNS = [ patterns: [{ pattern: /`([^`]+)`/g, replacement: '$1' }], }, { - type: 'link', // PM: link, eg: [text](url) or + type: 'link', // PM: link patterns: [ { pattern: /\[([^\]]+)\]\([^)]+\)/g, replacement: '$1' }, // [text](url) -> text - { pattern: /<(https?:\/\/[^>]+)>/g, replacement: '$1' }, // -> url (autolinks) + { pattern: /<([a-zA-Z][a-zA-Z0-9+.-]*:[^\s>]+)>/g, replacement: '$1' }, // , , , , etc + { pattern: /<([^\s@]+@[^\s@>]+)>/g, replacement: '$1' }, // -> user@example.com ], }, ]; diff --git a/app/javascript/dashboard/helper/specs/editorHelper.spec.js b/app/javascript/dashboard/helper/specs/editorHelper.spec.js index f928b1d28..abbef420f 100644 --- a/app/javascript/dashboard/helper/specs/editorHelper.spec.js +++ b/app/javascript/dashboard/helper/specs/editorHelper.spec.js @@ -901,6 +901,17 @@ describe('stripUnsupportedFormatting', () => { expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content); }); + it('preserves various URI scheme autolinks', () => { + const content = + 'Email or call '; + expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content); + }); + + it('preserves email autolinks', () => { + const content = 'Contact us at '; + expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content); + }); + it('preserves lists when schema supports them', () => { const content = '- item 1\n- item 2\n1. first\n2. second'; expect(stripUnsupportedFormatting(content, fullSchema)).toBe(content); @@ -984,6 +995,26 @@ describe('stripUnsupportedFormatting', () => { expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected); }); + it('converts URI scheme autolinks to plain text', () => { + const content = + 'Email or call '; + const expected = + 'Email mailto:support@example.com or call tel:+1234567890'; + expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected); + }); + + it('converts email autolinks to plain text', () => { + const content = 'Reach us at for help'; + const expected = 'Reach us at admin@chatwoot.com for help'; + expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected); + }); + + it('handles mixed autolink types', () => { + const content = 'Visit or email '; + const expected = 'Visit https://example.com or email info@example.com'; + expect(stripUnsupportedFormatting(content, emptySchema)).toBe(expected); + }); + it('strips bullet list markers', () => { expect( stripUnsupportedFormatting('- item 1\n- item 2', emptySchema) diff --git a/package.json b/package.json index 6d5a37b5a..1d9b72be2 100644 --- a/package.json +++ b/package.json @@ -33,7 +33,7 @@ "dependencies": { "@breezystack/lamejs": "^1.2.7", "@chatwoot/ninja-keys": "1.2.3", - "@chatwoot/prosemirror-schema": "1.3.4", + "@chatwoot/prosemirror-schema": "1.3.5", "@chatwoot/utils": "^0.0.51", "@formkit/core": "^1.6.7", "@formkit/vue": "^1.6.7", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 26bd3141e..f062848c5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -23,8 +23,8 @@ importers: specifier: 1.2.3 version: 1.2.3 '@chatwoot/prosemirror-schema': - specifier: 1.3.4 - version: 1.3.4 + specifier: 1.3.5 + version: 1.3.5 '@chatwoot/utils': specifier: ^0.0.51 version: 0.0.51 @@ -454,8 +454,8 @@ packages: '@chatwoot/ninja-keys@1.2.3': resolution: {integrity: sha512-xM8d9P5ikDMZm2WbaCTk/TW5HFauylrU3cJ75fq5je6ixKwyhl/0kZbVN/vbbZN4+AUX/OaSIn6IJbtCgIF67g==} - '@chatwoot/prosemirror-schema@1.3.4': - resolution: {integrity: sha512-XY1UyG9topVy7fNCt45YgiU/QzlJJRhrcrSWcvAIIcDYKomnEcGOyeAfM3WeFD6GgOuLzRUyIHrW+WQGpHaZvA==} + '@chatwoot/prosemirror-schema@1.3.5': + resolution: {integrity: sha512-3Koj3jwO1qOxJG84D4FqPOJ6o8k6ehZi1zedO3vKRERATm2Cy1p+ET6FEvVYWUpoBvDwR6hNVScXrcNNVobhsA==} '@chatwoot/utils@0.0.51': resolution: {integrity: sha512-WlEmWfOTzR7YZRUWzn5Wpm15/BRudpwqoNckph8TohyDbiim1CP4UZGa+qjajxTbNGLLhtKlm0Xl+X16+5Wceg==} @@ -4970,7 +4970,7 @@ snapshots: hotkeys-js: 3.8.7 lit: 2.2.6 - '@chatwoot/prosemirror-schema@1.3.4': + '@chatwoot/prosemirror-schema@1.3.5': dependencies: markdown-it-sup: 2.0.0 prosemirror-commands: 1.6.0 From d526cf283d610274fe321ee87076f3b5b936f67f Mon Sep 17 00:00:00 2001 From: Tanmay Deep Sharma <32020192+tds-1@users.noreply.github.com> Date: Mon, 12 Jan 2026 13:15:40 +0530 Subject: [PATCH 2/2] fix: pass serialized data in notification.deleted event to avoid Deserialisation (#13061) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit https://one.newrelic.com/alerts/issue?account=3437125&duration=259200000&state=d088e9b7-d0ce-3fcf-fda5-145df8b9cb2a ## Description Pass serialized data instead of ActiveRecord object in dispatch_destroy_event to prevent ActiveJob::DeserializationError when the notification is already deleted. This error occurs frequently because RemoveDuplicateNotificationJob deletes notifications, and by the time the async EventDispatcherJob runs, the record no longer exists. ## Type of change - [ ] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ## Checklist: - [ ] My code follows the style guidelines of this project - [ ] I have performed a self-review of my code - [ ] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [ ] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [ ] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --- > [!NOTE] > Avoids ActiveJob deserialization failures by sending serialized data for notification deletion and updating the listener accordingly. > > - `Notification#dispatch_destroy_event` now dispatches `NOTIFICATION_DELETED` with serialized `notification_data` (`id`, `user_id`, `account_id`) instead of the AR object > - `ActionCableListener#notification_deleted` reads `notification_data`, finds `User`/`Account`, computes `unread_count`/`count` via `NotificationFinder`, and broadcasts using the user’s pubsub token > - Specs updated to pass `notification_data` and assert payload (including `unread_count`/`count`) > > Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit e2ffbe765b148fdfd2cd2e031c657c36e423c1f5. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot). --------- Co-authored-by: Vishnu Narayanan --- app/listeners/action_cable_listener.rb | 16 ++++++++++++---- app/models/notification.rb | 12 +++++++++++- spec/listeners/action_cable_listener_spec.rb | 9 ++++++++- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index 109f6d344..48b7a3aa9 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -14,11 +14,19 @@ class ActionCableListener < BaseListener end def notification_deleted(event) - return if event.data[:notification].user.blank? + notification_data = event.data[:notification_data] - notification, account, unread_count, count = extract_notification_and_account(event) - tokens = [event.data[:notification].user.pubsub_token] - broadcast(account, tokens, NOTIFICATION_DELETED, { notification: { id: notification.id }, unread_count: unread_count, count: count }) + user = User.find_by(id: notification_data[:user_id]) + account = Account.find_by(id: notification_data[:account_id]) + return if user.blank? || account.blank? + + notification_finder = NotificationFinder.new(user, account) + tokens = [user.pubsub_token] + broadcast(account, tokens, NOTIFICATION_DELETED, { + notification: { id: notification_data[:id] }, + unread_count: notification_finder.unread_count, + count: notification_finder.count + }) end def account_cache_invalidated(event) diff --git a/app/models/notification.rb b/app/models/notification.rb index db07e3679..806eabdf1 100644 --- a/app/models/notification.rb +++ b/app/models/notification.rb @@ -180,7 +180,17 @@ class Notification < ApplicationRecord end def dispatch_destroy_event - Rails.configuration.dispatcher.dispatch(NOTIFICATION_DELETED, Time.zone.now, notification: self) + # Pass serialized data instead of ActiveRecord object to avoid DeserializationError + # when the async EventDispatcherJob runs after the notification has been deleted + Rails.configuration.dispatcher.dispatch( + NOTIFICATION_DELETED, + Time.zone.now, + notification_data: { + id: id, + user_id: user_id, + account_id: account_id + } + ) end def set_last_activity_at diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index 55b74116c..61f818da1 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -132,7 +132,14 @@ describe ActionCableListener do describe '#notification_deleted' do let(:event_name) { :'notification.deleted' } let!(:notification) { create(:notification, account: account, user: agent) } - let!(:event) { Events::Base.new(event_name, Time.zone.now, notification: notification) } + let(:notification_data) do + { + id: notification.id, + user_id: agent.id, + account_id: account.id + } + end + let!(:event) { Events::Base.new(event_name, Time.zone.now, notification_data: notification_data) } it 'sends message to account admins, inbox agents' do expect(ActionCableBroadcastJob).to receive(:perform_later).with(