From f5957e7970bf860a88a7d2a487430f4ded246d9a Mon Sep 17 00:00:00 2001 From: Pranav Date: Sun, 11 Jan 2026 00:31:17 -0800 Subject: [PATCH 1/3] fix: Reset sidebar to show expanded list when refreshing the page (#13229) Previously, the sidebar remembered which section was expanded using session storage. This caused a confusing experience where the sidebar would collapse on page refresh. With this update, the session storage dependency is removed, and the sidebar would expand based on the current active page, which gives a cleaner UX. --- .../dashboard/components-next/sidebar/Sidebar.vue | 12 ++---------- .../components-next/sidebar/SidebarGroup.vue | 12 +++++++++++- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue index a37cde89f..95baafcfd 100644 --- a/app/javascript/dashboard/components-next/sidebar/Sidebar.vue +++ b/app/javascript/dashboard/components-next/sidebar/Sidebar.vue @@ -1,12 +1,11 @@ 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 2/3] 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 3/3] 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(