From 5ab913f7b5f7e5133f6a7740dcdc3c09e2a57dac Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 1 Aug 2025 02:13:46 -0700 Subject: [PATCH 1/8] chore: Add a condition to handle bounced email (#11873) Add bounced emails to the conversation thread. Fix Gmail bounce detection by checking the X-Failed-Recipients header. Currently, bounced emails are rejected as auto-replies, which causes support agents to miss important delivery failure context. This PR ensures bounced messages are correctly added to the thread, preserving visibility for the support team. --- .../incoming_email_validity_helper.rb | 13 +- app/presenters/mail_presenter.rb | 4 + spec/fixtures/files/bounced_gmail.eml | 120 ++++++++++++++++++ spec/mailboxes/imap/imap_mailbox_spec.rb | 8 ++ 4 files changed, 139 insertions(+), 6 deletions(-) create mode 100644 spec/fixtures/files/bounced_gmail.eml diff --git a/app/mailboxes/incoming_email_validity_helper.rb b/app/mailboxes/incoming_email_validity_helper.rb index 252ef5257..9483c9768 100644 --- a/app/mailboxes/incoming_email_validity_helper.rb +++ b/app/mailboxes/incoming_email_validity_helper.rb @@ -4,16 +4,17 @@ module IncomingEmailValidityHelper def incoming_email_from_valid_email? return false unless valid_external_email_for_active_account? + # Return if email doesn't have a valid sender + # This can happen in cases like bounce emails for invalid contact email address + return false unless Devise.email_regexp.match?(@processed_mail.original_sender) + + # Process bounced emails, as regular emails + return true if @processed_mail.bounced? + # we skip processing auto reply emails like delivery status notifications # out of office replies, etc. return false if auto_reply_email? - # return if email doesn't have a valid sender - # This can happen in cases like bounce emails for invalid contact email address - # TODO: Handle the bounce separately and mark the contact as invalid in case of reply bounces - # The returned value could be "\"\"" for some email clients - return false unless Devise.email_regexp.match?(@processed_mail.original_sender) - true end diff --git a/app/presenters/mail_presenter.rb b/app/presenters/mail_presenter.rb index 890e97a78..e57831c96 100644 --- a/app/presenters/mail_presenter.rb +++ b/app/presenters/mail_presenter.rb @@ -157,6 +157,10 @@ class MailPresenter < SimpleDelegator auto_submitted? || x_auto_reply? end + def bounced? + @mail.bounced? || @mail['X-Failed-Recipients'].try(:value).present? + end + def notification_email_from_chatwoot? # notification emails are send via mailer sender email address. so it should match original_sender == Mail::Address.new(ENV.fetch('MAILER_SENDER_EMAIL', 'Chatwoot ')).address diff --git a/spec/fixtures/files/bounced_gmail.eml b/spec/fixtures/files/bounced_gmail.eml new file mode 100644 index 000000000..2c45bcd4d --- /dev/null +++ b/spec/fixtures/files/bounced_gmail.eml @@ -0,0 +1,120 @@ +Delivered-To: robert.smith@gmail.com +Return-Path: <> +Subject: Delivery Status Notification (Failure) +From: Mail Delivery Subsystem +To: robert.smith@gmail.com +Content-Type: multipart/report; boundary="00000000000093475906390e1e9b"; report-type=delivery-status +Auto-Submitted: auto-replied +Message-ID: <686707c9.050a0220.302e7d.0cb2.GMR@mx.google.com> +Date: Thu, 03 Jul 2025 15:44:25 -0700 (PDT) +X-Failed-Recipients: alex.jones@fictionalcorp.com + +--00000000000093475906390e1e9b +Content-Type: multipart/related; boundary="000000000000936d8406390e1ec7" + +--000000000000936d8406390e1ec7 +Content-Type: multipart/alternative; boundary="000000000000936d9006390e1ec8" + +--000000000000936d9006390e1ec8 +Content-Type: text/plain; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + +** Address not found ** + +Your message wasn't delivered to alex.jones@fictionalcorp.com because the address co= +uldn't be found or is unable to receive email. + +Learn more here: https://support.google.com/mail/?p=3DNoSuchUser + +The response was: + +550 5.1.1 The email account that you tried to reach does not exist. Please = +try double-checking the recipient's email address for typos or unnecessary = +spaces. For more information, go to https://support.google.com/mail/?p=3DNo= +SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp + +--000000000000936d9006390e1ec8 +Content-Type: text/html; charset="UTF-8" +Content-Transfer-Encoding: quoted-printable + + + + + + + + + + + +
+ + +
++=3D"Error + + + + +

+Address not found +

+Your message wasn't delivered to alex.jones@fictionalcorp.com because the address couldn't be found = +or is unable to receive email. +
+LEARN MORE +
+
+
+The response was:
+

+550 5.1.1 The email account that you tried to reach does not exist. Please = +try double-checking the recipient's email address for typos or unnecessary = +spaces. For more information, go to https://support.google.com/mail/?p=3DNo= +SuchUser d2e1a72fcca58-74ce2b0525csor332154b3a.0 - gsmtp +

+
+ + + +--000000000000936d9006390e1ec8-- +--000000000000936d8406390e1ec7 +Content-Type: image/png; name="icon.png" +Content-Disposition: attachment; filename="icon.png" +Content-Transfer-Encoding: base64 +Content-ID: + +--000000000000936d8406390e1ec7-- +--00000000000093475906390e1e9b +Content-Type: message/delivery-status + +--00000000000093475906390e1e9b +Content-Type: message/rfc822 + +Date: Thu, 03 Jul 2025 15:44:23 -0700 +From: Robert Smith +Reply-To: robert.smith@gmail.com +To: alex.jones@fictionalcorp.com +Message-ID: +In-Reply-To: +Subject: Just checking in +Mime-Version: 1.0 +Content-Type: text/html; charset=UTF-8 +Content-Transfer-Encoding: 7bit + +

Hey, just checking in. Let me know if you got my earlier message.

+ +--00000000000093475906390e1e9b-- diff --git a/spec/mailboxes/imap/imap_mailbox_spec.rb b/spec/mailboxes/imap/imap_mailbox_spec.rb index cc72be18b..fc94c98be 100644 --- a/spec/mailboxes/imap/imap_mailbox_spec.rb +++ b/spec/mailboxes/imap/imap_mailbox_spec.rb @@ -115,6 +115,14 @@ RSpec.describe Imap::ImapMailbox do end end + context 'when the email is bounced' do + let!(:bounced_mail) { create_inbound_email_from_fixture('bounced_gmail.eml') } + + it 'processes the bounced email' do + expect { class_instance.process(bounced_mail.mail, channel) }.to change(Message, :count) + end + end + context 'when a reply for existing email conversation' do let(:prev_conversation) { create(:conversation, account: account, inbox: channel.inbox, assignee: agent) } let(:reply_mail) do From 4dc7a653eb83c552d6338bac85909c6d6981e1e2 Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Fri, 1 Aug 2025 16:38:06 +0530 Subject: [PATCH 2/8] fix: outer heredocs variable expansion during cwctl upgrade (#12086) - fix: outer heredocs variable expansion during cwctl upgrade --- VERSION_CWCTL | 2 +- deployment/setup_20.04.sh | 11 ++++++----- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 47b322c97..4d9d11cf5 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -3.4.1 +3.4.2 diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index aaa928f4d..5a40ee068 100644 --- a/deployment/setup_20.04.sh +++ b/deployment/setup_20.04.sh @@ -2,7 +2,7 @@ # Description: Install and manage a Chatwoot installation. # OS: Ubuntu 20.04 LTS, 22.04 LTS, 24.04 LTS -# Script Version: 3.4.1 +# Script Version: 3.4.2 # Run this script as root set -eu -o errexit -o pipefail -o noclobber -o nounset @@ -990,7 +990,7 @@ EOF # Check if CW_VERSION is 4.0 or above if [[ "$(printf '%s\n' "$CW_VERSION" "4.0" | sort -V | head -n 1)" == "4.0" ]]; then echo "Chatwoot v4.0 and above requires pgvector support in PostgreSQL." - read -p "Does your postgres support pgvector and want to proceed with the upgrade? [Y/n]: " user_input + read -p "Does your postgres support pgvector and want to proceed with the upgrade? [y/N]: " user_input user_input=${user_input:-Y} if [[ "$user_input" =~ ^([yY][eE][sS]|[yY])$ ]]; then echo "Proceeding with the upgrade..." @@ -1005,6 +1005,7 @@ EOF upgrade_redis upgrade_node get_pnpm + sudo -i -u chatwoot << EOF # Navigate to the Chatwoot directory @@ -1016,9 +1017,9 @@ EOF # Ensure the ruby version is upto date # Parse the latest ruby version - latest_ruby_version="$(cat '.ruby-version')" - rvm install "ruby-$latest_ruby_version" - rvm use "$latest_ruby_version" --default + latest_ruby_version="\$(cat '.ruby-version')" + rvm install "ruby-\$latest_ruby_version" + rvm use "\$latest_ruby_version" --default # Update dependencies bundle From 51b9fd8eca76930ffb4f26adfe202ea1d326e534 Mon Sep 17 00:00:00 2001 From: Pranav Date: Fri, 1 Aug 2025 16:32:29 -0700 Subject: [PATCH 3/8] fix: Disable IMAP inboxes that requires authorization (#12092) This PR disables queueing IMAP sync jobs for emails channels that - are in free plan if on Chatwoot cloud. - requires authorization --- .../inboxes/fetch_imap_email_inboxes_job.rb | 10 ++++++- .../fetch_imap_email_inboxes_job_spec.rb | 26 +++++++++++++++++++ .../fetch_imap_email_inboxes_job_spec.rb | 20 ++++++++++++++ 3 files changed, 55 insertions(+), 1 deletion(-) create mode 100644 spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb diff --git a/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb index 56e8c2235..ea2705955 100644 --- a/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb +++ b/app/jobs/inboxes/fetch_imap_email_inboxes_job.rb @@ -1,5 +1,6 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob queue_as :scheduled_jobs + include BillingHelper def perform email_inboxes = Inbox.where(channel_type: 'Channel::Email') @@ -11,6 +12,13 @@ class Inboxes::FetchImapEmailInboxesJob < ApplicationJob private def should_fetch_emails?(inbox) - inbox.channel.imap_enabled && !inbox.account.suspended? + return false if inbox.account.suspended? + return false unless inbox.channel.imap_enabled + return false if inbox.channel.reauthorization_required? + + return true unless ChatwootApp.chatwoot_cloud? + return false if default_plan?(inbox.account) + + true end end diff --git a/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb new file mode 100644 index 000000000..ef5b9b0c7 --- /dev/null +++ b/spec/enterprise/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb @@ -0,0 +1,26 @@ +require 'rails_helper' + +RSpec.describe Inboxes::FetchImapEmailInboxesJob do + context 'when chatwoot_cloud is enabled' do + let(:account) { create(:account) } + let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) } + let(:imap_email_channel) { create(:channel_email, imap_enabled: true, account: account) } + let(:premium_imap_channel) { create(:channel_email, imap_enabled: true, account: premium_account) } + + before do + premium_account.custom_attributes['plan_name'] = 'Startups' + InstallationConfig.where(name: 'DEPLOYMENT_ENV').first_or_create!(value: 'cloud') + InstallationConfig.where(name: 'CHATWOOT_CLOUD_PLANS').first_or_create!(value: [{ 'name' => 'Hacker' }]) + end + + it 'skips inboxes with default plan' do + expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(imap_email_channel) + described_class.perform_now + end + + it 'processes inboxes with premium plan' do + expect(Inboxes::FetchImapEmailsJob).to receive(:perform_later).with(premium_imap_channel) + described_class.perform_now + end + end +end diff --git a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb index 18685a649..abcab1e8f 100644 --- a/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb +++ b/spec/jobs/inboxes/fetch_imap_email_inboxes_job_spec.rb @@ -3,6 +3,7 @@ require 'rails_helper' RSpec.describe Inboxes::FetchImapEmailInboxesJob do let(:account) { create(:account) } let(:suspended_account) { create(:account, status: 'suspended') } + let(:premium_account) { create(:account, custom_attributes: { plan_name: 'Startups' }) } let(:imap_email_channel) do create(:channel_email, imap_enabled: true, account: account) @@ -16,6 +17,19 @@ RSpec.describe Inboxes::FetchImapEmailInboxesJob do create(:channel_email, imap_enabled: false, account: account) end + let(:reauth_required_channel) do + create(:channel_email, imap_enabled: true, account: account) + end + + let(:premium_imap_channel) do + create(:channel_email, imap_enabled: true, account: premium_account) + end + + before do + reauth_required_channel.prompt_reauthorization! + premium_account.custom_attributes['plan_name'] = 'Startups' + end + it 'enqueues the job' do expect { described_class.perform_later }.to have_enqueued_job(described_class) .on_queue('scheduled_jobs') @@ -44,5 +58,11 @@ RSpec.describe Inboxes::FetchImapEmailInboxesJob do described_class.perform_now end + + it 'skips channels requiring reauthorization' do + expect(Inboxes::FetchImapEmailsJob).not_to receive(:perform_later).with(reauth_required_channel) + + described_class.perform_now + end end end From 65312744c70cc1a2c15abad6705fe7b30167d66c Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:15:36 +0530 Subject: [PATCH 4/8] chore: Update inbox view context menu (#12090) # Pull Request Template ## Description This PR updates the inbox view context menu to use the existing conversation card context menu for consistency. ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Screenshots **Before** image image **After** image image ## Checklist: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../components-next/Inbox/InboxCard.vue | 3 +- .../routes/dashboard/inbox/InboxList.vue | 2 +- .../inbox/components/InboxContextMenu.vue | 51 +++++++++---------- 3 files changed, 27 insertions(+), 29 deletions(-) diff --git a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue index 79a451404..90cc1ff53 100644 --- a/app/javascript/dashboard/components-next/Inbox/InboxCard.vue +++ b/app/javascript/dashboard/components-next/Inbox/InboxCard.vue @@ -63,11 +63,12 @@ const lastActivityAt = computed(() => { }); const menuItems = computed(() => [ - { key: 'delete', label: t('INBOX.MENU_ITEM.DELETE') }, { key: isUnread.value ? 'mark_as_read' : 'mark_as_unread', + icon: isUnread.value ? 'mail' : 'mail-unread', label: t(`INBOX.MENU_ITEM.MARK_AS_${isUnread.value ? 'READ' : 'UNREAD'}`), }, + { key: 'delete', icon: 'delete', label: t('INBOX.MENU_ITEM.DELETE') }, ]); const messageClasses = computed(() => ({ diff --git a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue index ce6bd8be0..6f6f6cad3 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/InboxList.vue @@ -246,7 +246,7 @@ onMounted(() => { :key="notificationItem.id" :inbox-item="notificationItem" :state-inbox="stateInbox(notificationItem.primaryActor?.inboxId)" - class="inbox-card rounded-lg hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3" + class="inbox-card rounded-none hover:rounded-lg hover:bg-n-alpha-1 dark:hover:bg-n-alpha-3" :class=" currentConversationId === notificationItem.primaryActor?.id ? 'bg-n-alpha-1 dark:bg-n-alpha-3 rounded-lg active' diff --git a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue index 8156bebbe..4671df3bb 100644 --- a/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue +++ b/app/javascript/dashboard/routes/dashboard/inbox/components/InboxContextMenu.vue @@ -1,32 +1,27 @@ - @@ -37,12 +32,14 @@ export default { @close="handleClose" >
From 60a1e9b15dc912db86e03a2e7a791e0f9d11281b Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 5 Aug 2025 01:50:45 +0400 Subject: [PATCH 5/8] fix: Populate meta field for whatsApp shared contacts (#12097) Fixes https://github.com/chatwoot/chatwoot/issues/11999 --- app/services/whatsapp/incoming_message_base_service.rb | 9 ++++++++- spec/services/whatsapp/incoming_message_service_spec.rb | 9 +++------ 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/app/services/whatsapp/incoming_message_base_service.rb b/app/services/whatsapp/incoming_message_base_service.rb index f057fadbd..94ad5c7d1 100644 --- a/app/services/whatsapp/incoming_message_base_service.rb +++ b/app/services/whatsapp/incoming_message_base_service.rb @@ -156,11 +156,18 @@ class Whatsapp::IncomingMessageBaseService phones = contact[:phones] phones = [{ phone: 'Phone number is not available' }] if phones.blank? + name_info = contact['name'] || {} + contact_meta = { + firstName: name_info['first_name'], + lastName: name_info['last_name'] + }.compact + phones.each do |phone| @message.attachments.new( account_id: @message.account_id, file_type: file_content_type(message_type), - fallback_title: phone[:phone].to_s + fallback_title: phone[:phone].to_s, + meta: contact_meta ) end end diff --git a/spec/services/whatsapp/incoming_message_service_spec.rb b/spec/services/whatsapp/incoming_message_service_spec.rb index 0bcbf2a3e..4035a47df 100644 --- a/spec/services/whatsapp/incoming_message_service_spec.rb +++ b/spec/services/whatsapp/incoming_message_service_spec.rb @@ -267,19 +267,16 @@ describe Whatsapp::IncomingMessageService do ] }] }.with_indifferent_access described_class.new(inbox: whatsapp_channel.inbox, params: params).perform expect(Contact.all.first.name).to eq('Kedar') - expect(whatsapp_channel.inbox.conversations.count).not_to eq(0) - # Two messages are tested deliberately to ensure multiple contact attachments work. m1 = whatsapp_channel.inbox.messages.first - contact_attachments = m1.attachments.first expect(m1.content).to eq('Apple Inc.') - expect(contact_attachments.fallback_title).to eq('+911800') + expect(m1.attachments.first.fallback_title).to eq('+911800') + expect(m1.attachments.first.meta).to eq({}) m2 = whatsapp_channel.inbox.messages.last - contact_attachments = m2.attachments.first expect(m2.content).to eq('Chatwoot') - expect(contact_attachments.fallback_title).to eq('+1 (415) 341-8386') + expect(m2.attachments.first.meta).to eq({ 'firstName' => 'Chatwoot' }) end end From 53fce7be03f2f22c5109671adc11b4cdac9676f0 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:36:58 +0530 Subject: [PATCH 6/8] fix: Conditionally fetch limits and assistants for enterprise/cloud (#12099) # Pull Request Template ## Description ### Issue The Community Edition (CE) dashboard was making API requests to enterprise-only endpoints, causing 404 errors: * `/enterprise/api/v1/accounts/1/limits` * `/api/v1/accounts/1/captain/assistants?page=1` ### Solution 1. Added conditional checks to prevent these calls. 2. Remove unused component `app/javascript/dashboard/components/app/UpgradeBanner.vue` Fixes [CW-4695](https://linear.app/chatwoot/issue/CW-4695/440-ce-dashboard-calls-enterprise-urls), https://github.com/chatwoot/chatwoot/issues/12023 ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## Checklist: - [x] My code follows the style guidelines of this project - [x] 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 - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../components/app/UpgradeBanner.vue | 94 ------------------- .../components/copilot/CopilotContainer.vue | 9 +- .../dashboard/composables/useCaptain.js | 6 +- .../routes/dashboard/upgrade/UpgradePage.vue | 8 +- 4 files changed, 20 insertions(+), 97 deletions(-) delete mode 100644 app/javascript/dashboard/components/app/UpgradeBanner.vue diff --git a/app/javascript/dashboard/components/app/UpgradeBanner.vue b/app/javascript/dashboard/components/app/UpgradeBanner.vue deleted file mode 100644 index d41ce3438..000000000 --- a/app/javascript/dashboard/components/app/UpgradeBanner.vue +++ /dev/null @@ -1,94 +0,0 @@ - - - - diff --git a/app/javascript/dashboard/components/copilot/CopilotContainer.vue b/app/javascript/dashboard/components/copilot/CopilotContainer.vue index 41ffa6b61..da0e69492 100644 --- a/app/javascript/dashboard/components/copilot/CopilotContainer.vue +++ b/app/javascript/dashboard/components/copilot/CopilotContainer.vue @@ -4,6 +4,7 @@ import { useStore } from 'dashboard/composables/store'; import Copilot from 'dashboard/components-next/copilot/Copilot.vue'; import { useMapGetter } from 'dashboard/composables/store'; import { useUISettings } from 'dashboard/composables/useUISettings'; +import { useConfig } from 'dashboard/composables/useConfig'; import { useWindowSize } from '@vueuse/core'; import { vOnClickOutside } from '@vueuse/components'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; @@ -18,6 +19,7 @@ defineProps({ const store = useStore(); const { uiSettings, updateUISettings } = useUISettings(); +const { isEnterprise } = useConfig(); const { width: windowWidth } = useWindowSize(); const currentUser = useMapGetter('getCurrentUser'); @@ -82,6 +84,9 @@ const setAssistant = async assistant => { }; const shouldShowCopilotPanel = computed(() => { + if (!isEnterprise) { + return false; + } const isCaptainEnabled = isFeatureEnabledonAccount.value( currentAccountId.value, FEATURE_FLAGS.CAPTAIN @@ -113,7 +118,9 @@ const sendMessage = async message => { }; onMounted(() => { - store.dispatch('captainAssistants/get'); + if (isEnterprise) { + store.dispatch('captainAssistants/get'); + } }); diff --git a/app/javascript/dashboard/composables/useCaptain.js b/app/javascript/dashboard/composables/useCaptain.js index d28560944..3f93cfc58 100644 --- a/app/javascript/dashboard/composables/useCaptain.js +++ b/app/javascript/dashboard/composables/useCaptain.js @@ -1,12 +1,14 @@ import { computed } from 'vue'; import { useStore } from 'dashboard/composables/store.js'; import { useAccount } from 'dashboard/composables/useAccount'; +import { useConfig } from 'dashboard/composables/useConfig'; import { useCamelCase } from 'dashboard/composables/useTransformKeys'; import { FEATURE_FLAGS } from 'dashboard/featureFlags'; export function useCaptain() { const store = useStore(); const { isCloudFeatureEnabled, currentAccount } = useAccount(); + const { isEnterprise } = useConfig(); const captainEnabled = computed(() => { return isCloudFeatureEnabled(FEATURE_FLAGS.CAPTAIN); @@ -33,7 +35,9 @@ export function useCaptain() { }); const fetchLimits = () => { - store.dispatch('accounts/limits'); + if (isEnterprise) { + store.dispatch('accounts/limits'); + } }; return { diff --git a/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue b/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue index bd028779d..a650aac37 100644 --- a/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue +++ b/app/javascript/dashboard/routes/dashboard/upgrade/UpgradePage.vue @@ -4,6 +4,7 @@ import { useStore } from 'dashboard/composables/store'; import { useMapGetter } from 'dashboard/composables/store.js'; import { useRouter } from 'vue-router'; import { useAccount } from 'dashboard/composables/useAccount'; +import { useConfig } from 'dashboard/composables/useConfig'; import { differenceInDays } from 'date-fns'; import { useAdmin } from 'dashboard/composables/useAdmin'; import { useI18n } from 'vue-i18n'; @@ -22,6 +23,7 @@ const router = useRouter(); const store = useStore(); const { t } = useI18n(); const { accountId, currentAccount } = useAccount(); +const { isEnterprise } = useConfig(); const { isAdmin } = useAdmin(); const isOnChatwootCloud = useMapGetter('globalConfig/isOnChatwootCloud'); @@ -100,7 +102,11 @@ const routeToBilling = () => { }); }; -onMounted(() => fetchLimits()); +onMounted(() => { + if (isEnterprise) { + fetchLimits(); + } +}); defineExpose({ shouldShowUpgradePage }); From 270f26e47194f6a74bae59b3514d1c4f7b52293b Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Tue, 5 Aug 2025 03:52:20 +0530 Subject: [PATCH 7/8] chore: Add new tab and copy link to conversation context menu (#12089) # Pull Request Template ## Description This PR includes the following enhancements to the conversation card context menu: 1. **Added "Open in New Tab" and "Copy Conversation Link" options.** * "Open in New Tab" allows users to quickly open a conversation in a separate browser tab. * "Copy Conversation Link" copies the conversation URL to the clipboard for easy sharing. 2. **Enabled the context menu in Previous Conversations card** with support for these two options. Fixes https://linear.app/chatwoot/issue/CW-4722/cannot-open-previous-conversations-in-a-new-tab ## Type of change - [x] Bug fix (non-breaking change which fixes an issue) ## How Has This Been Tested? ### Loom video https://www.loom.com/share/37b45d23c6804db292568d093b645ac0?sid=c3105971-f938-41bd-9f52-0f00d419d1b3 ## Checklist: - [x] My code follows the style guidelines of this project - [x] I have performed a self-review of my code - [x] I have commented on my code, particularly in hard-to-understand areas - [ ] I have made corresponding changes to the documentation - [x] My changes generate no new warnings - [ ] I have added tests that prove my fix is effective or that my feature works - [x] New and existing unit tests pass locally with my changes - [ ] Any dependent changes have been merged and published in downstream modules --------- Co-authored-by: Pranav --- .../widgets/conversation/ConversationCard.vue | 31 ++- .../conversation/contextMenu/Index.vue | 239 ++++++++++++------ .../conversation/contextMenu/menuItem.vue | 34 ++- .../i18n/locale/en/conversation.json | 3 + .../conversation/ContactConversations.vue | 2 + 5 files changed, 206 insertions(+), 103 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue index 30e99ca9d..e87bea268 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationCard.vue @@ -68,6 +68,10 @@ export default { type: Boolean, default: false, }, + allowedContextMenuOptions: { + type: Array, + default: () => [], + }, }, emits: [ 'contextMenuToggle', @@ -151,11 +155,9 @@ export default { hasSlaPolicyId() { return this.chat?.sla_policy_id; }, - }, - methods: { - onCardClick(e) { + conversationPath() { const { activeInbox, chat } = this; - const path = frontendURL( + return frontendURL( conversationUrl({ accountId: this.accountId, activeInbox, @@ -166,18 +168,26 @@ export default { conversationType: this.conversationType, }) ); + }, + }, + methods: { + onCardClick(e) { + const path = this.conversationPath; + if (!path) return; + // Handle Ctrl/Cmd + Click for new tab if (e.metaKey || e.ctrlKey) { + e.preventDefault(); window.open( - window.chatwootConfig.hostURL + path, + `${window.chatwootConfig.hostURL}${path}`, '_blank', - 'noopener noreferrer nofollow' + 'noopener,noreferrer' ); return; } - if (this.isActiveChat) { - return; - } + + // Skip if already active + if (this.isActiveChat) return; router.push({ path }); }, @@ -359,6 +369,8 @@ export default { :priority="chat.priority" :chat-id="chat.id" :has-unread-messages="hasUnread" + :conversation-url="conversationPath" + :allowed-options="allowedContextMenuOptions" @update-conversation="onUpdateConversation" @assign-agent="onAssignAgent" @assign-label="onAssignLabel" @@ -367,6 +379,7 @@ export default { @mark-as-read="markAsRead" @assign-priority="assignPriority" @delete-conversation="deleteConversation" + @close="closeContextMenu" /> diff --git a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue index 6c788bbd2..a6f79500a 100644 --- a/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue +++ b/app/javascript/dashboard/components/widgets/conversation/contextMenu/Index.vue @@ -1,5 +1,8 @@