From 530125d4c5e9a8eaa075bcbeaa6cad106197c6e8 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Thu, 21 Aug 2025 11:07:43 +0200 Subject: [PATCH 01/96] chore(deps): upgrade twilio-ruby to 7.6.0 for upcoming features (#12243) ### Summary - Update Twilio gem to support latest features and API changes. - No app code changes; Gemfile and Gemfile.lock only. references: #11602 , #11481 ### Testing - Existing Twilio SMS: send/receive still works; delivery status updates. - Existing Twilio WhatsApp: send/receive still works; templates (if used) unaffected. - Create new Twilio SMS/WhatsApp inboxes: can be created and can send/receive messages. Co-authored-by: Muhsin Keloth --- Gemfile | 2 +- Gemfile.lock | 18 ++++++++++-------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index a3b3c0ae8..e271e6b03 100644 --- a/Gemfile +++ b/Gemfile @@ -89,7 +89,7 @@ gem 'wisper', '2.0.0' ##--- gems for channels ---## gem 'facebook-messenger' gem 'line-bot-api' -gem 'twilio-ruby', '~> 5.66' +gem 'twilio-ruby' # twitty will handle subscription of twitter account events # gem 'twitty', git: 'https://github.com/chatwoot/twitty' gem 'twitty', '~> 0.1.5' diff --git a/Gemfile.lock b/Gemfile.lock index be258f524..531e3db2c 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -252,8 +252,10 @@ GEM railties (>= 5.0.0) faker (3.2.0) i18n (>= 1.8.11, < 2) - faraday (2.9.0) - faraday-net_http (>= 2.0, < 3.2) + faraday (2.13.1) + faraday-net_http (>= 2.0, < 3.5) + json + logger faraday-follow_redirects (0.3.0) faraday (>= 1, < 3) faraday-mashify (0.1.1) @@ -261,8 +263,8 @@ GEM hashie faraday-multipart (1.0.4) multipart-post (~> 2) - faraday-net_http (3.1.0) - net-http + faraday-net_http (3.4.0) + net-http (>= 0.5.0) faraday-net_http_persistent (2.1.0) faraday (~> 2.5) net-http-persistent (~> 4.0) @@ -421,7 +423,7 @@ GEM judoscale-sidekiq (1.8.2) judoscale-ruby (= 1.8.2) sidekiq (>= 5.0) - jwt (2.8.1) + jwt (2.10.1) base64 kaminari (1.2.2) activesupport (>= 4.1.0) @@ -503,7 +505,7 @@ GEM mutex_m (0.3.0) neighbor (0.2.3) activerecord (>= 5.2) - net-http (0.4.1) + net-http (0.6.0) uri net-http-persistent (4.0.2) connection_pool (~> 2.2) @@ -849,7 +851,7 @@ GEM i18n timeout (0.4.3) trailblazer-option (0.1.2) - twilio-ruby (5.77.0) + twilio-ruby (7.6.0) faraday (>= 0.9, < 3.0) jwt (>= 1.5, < 3.0) nokogiri (>= 1.6, < 2.0) @@ -1041,7 +1043,7 @@ DEPENDENCIES telephone_number test-prof time_diff - twilio-ruby (~> 5.66) + twilio-ruby twitty (~> 0.1.5) tzinfo-data uglifier From 47867c0b8a534f858f20544dae827ee1d727701d Mon Sep 17 00:00:00 2001 From: cakrumen <85589994+cakrumen@users.noreply.github.com> Date: Thu, 21 Aug 2025 06:26:29 -0300 Subject: [PATCH 02/96] fix: cwctl version to handle the upgrade loop (#12232) - fix: cwctl version to handle the upgrade loop Co-authored-by: Vishnu Narayanan Co-authored-by: Vishnu Narayanan --- VERSION_CWCTL | 2 +- deployment/setup_20.04.sh | 30 +++++++++++++++--------------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/VERSION_CWCTL b/VERSION_CWCTL index 4d9d11cf5..6cb9d3dd0 100644 --- a/VERSION_CWCTL +++ b/VERSION_CWCTL @@ -1 +1 @@ -3.4.2 +3.4.3 diff --git a/deployment/setup_20.04.sh b/deployment/setup_20.04.sh index 5a40ee068..37c7454f1 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.2 +# Script Version: 3.4.3 # Run this script as root set -eu -o errexit -o pipefail -o noclobber -o nounset @@ -19,7 +19,7 @@ fi # option --output/-o requires 1 argument LONGOPTS=console,debug,help,install,Install:,logs:,restart,ssl,upgrade,Upgrade:,webserver,version,web-only,worker-only,convert: OPTIONS=cdhiI:l:rsuU:wvWK -CWCTL_VERSION="3.3.0" +CWCTL_VERSION="3.4.3" pg_pass=$(head /dev/urandom | tr -dc A-Za-z0-9 | head -c 15 ; echo '') CHATWOOT_HUB_URL="https://hub.2.chatwoot.com/events" @@ -430,7 +430,7 @@ function configure_systemd_services() { if [ "$DEPLOYMENT_TYPE" == "web" ]; then echo "Setting up web-only deployment" - + # Stop and disable existing services if converting if [ "$existing_full_deployment" = true ]; then echo "Converting from full deployment to web-only" @@ -449,14 +449,14 @@ function configure_systemd_services() { cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-web.target /etc/systemd/system/chatwoot-web.target - + systemctl daemon-reload systemctl enable chatwoot-web.target systemctl start chatwoot-web.target - + elif [ "$DEPLOYMENT_TYPE" == "worker" ]; then echo "Setting up worker-only deployment" - + # Stop and disable existing services if converting if [ "$existing_full_deployment" = true ]; then echo "Converting from full deployment to worker-only" @@ -475,14 +475,14 @@ function configure_systemd_services() { cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.target /etc/systemd/system/chatwoot-worker.target - + systemctl daemon-reload systemctl enable chatwoot-worker.target systemctl start chatwoot-worker.target - + else echo "Setting up full deployment (web + worker)" - + # Stop existing specialized deployments if converting back to full if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then echo "Converting from web-only to full deployment" @@ -494,7 +494,7 @@ function configure_systemd_services() { systemctl stop chatwoot-worker.target || true systemctl disable chatwoot-worker.target || true fi - + cp /home/chatwoot/chatwoot/deployment/chatwoot-web.1.service /etc/systemd/system/chatwoot-web.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot-worker.1.service /etc/systemd/system/chatwoot-worker.1.service cp /home/chatwoot/chatwoot/deployment/chatwoot.target /etc/systemd/system/chatwoot.target @@ -538,7 +538,7 @@ function setup_ssl() { cd chatwoot sed -i "s/http:\/\/0.0.0.0:3000/https:\/\/$domain_name/g" .env EOF - + # Restart the appropriate chatwoot target if [ -f "/etc/systemd/system/chatwoot-web.target" ]; then systemctl restart chatwoot-web.target @@ -1005,7 +1005,7 @@ EOF upgrade_redis upgrade_node get_pnpm - + sudo -i -u chatwoot << EOF # Navigate to the Chatwoot directory @@ -1098,16 +1098,16 @@ function restart() { ############################################################################## function convert_deployment() { echo "Converting Chatwoot deployment to: $DEPLOYMENT_TYPE" - + # Check if Chatwoot is installed if [ ! -d "/home/chatwoot/chatwoot" ]; then echo "Chatwoot installation not found. Use --install first." exit 1 fi - + # Run the systemd service configuration which handles conversion logic configure_systemd_services - + echo "Deployment converted successfully to: $DEPLOYMENT_TYPE" } From 35d0a7f1a7fdd74988495061919e60ac609e3e1d Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Thu, 21 Aug 2025 16:44:51 +0530 Subject: [PATCH 03/96] feat: Add liquid template support for WhatsApp template parameters (#12227) Extends liquid template processing to WhatsApp `template_params`, allowing dynamic variable substitution in template parameter values. Users can now use liquid variables in WhatsApp template parameters: ``` { "template_params": { "name": "greet", "category": "MARKETING", "language": "en", "processed_params": { "body": { "customer_name": "{{contact.name}}", "customer_email": "{{contact.email | default: 'no-email@example.com'}}" } } } } ``` When the message is saved, {{contact.name}} gets replaced with the actual contact name. Supported Variables - {{contact.name}}, {{contact.email}}, {{contact.phone_number}} - {{agent.name}}, {{agent.first_name}} - {{account.name}}, {{inbox.name}} - {{conversation.display_id}} - Custom attributes: {{contact.custom_attribute.key_name}} - Liquid filters: {{ contact.email | default: "fallback@example.com" }} --- app/models/concerns/liquidable.rb | 58 ++++++++ spec/models/concerns/liquidable_shared.rb | 155 ++++++++++++++++++++++ 2 files changed, 213 insertions(+) diff --git a/app/models/concerns/liquidable.rb b/app/models/concerns/liquidable.rb index 8a30977a7..8a90f5f9f 100644 --- a/app/models/concerns/liquidable.rb +++ b/app/models/concerns/liquidable.rb @@ -3,6 +3,7 @@ module Liquidable included do before_create :process_liquid_in_content + before_create :process_liquid_in_template_params end private @@ -35,4 +36,61 @@ module Liquidable # We don't want to process liquid in code blocks content.gsub(/`(.*?)`/m, '{% raw %}`\\1`{% endraw %}') end + + def process_liquid_in_template_params + return unless template_params_present? && liquid_processable_template_params? + + processed_params = process_liquid_in_hash(template_params_data['processed_params']) + + # Update the additional_attributes with processed template_params + self.additional_attributes = additional_attributes.merge( + 'template_params' => template_params_data.merge('processed_params' => processed_params) + ) + rescue Liquid::Error + # If there is an error in the liquid syntax, we don't want to process it + end + + def template_params_present? + additional_attributes&.dig('template_params', 'processed_params').present? + end + + def liquid_processable_template_params? + message_type == 'outgoing' || message_type == 'template' + end + + def template_params_data + additional_attributes['template_params'] + end + + def process_liquid_in_hash(hash) + return hash unless hash.is_a?(Hash) + + hash.transform_values { |value| process_liquid_value(value) } + end + + def process_liquid_value(value) + case value + when String + process_liquid_string(value) + when Hash + process_liquid_in_hash(value) + when Array + process_liquid_array(value) + else + value + end + end + + def process_liquid_array(array) + array.map { |item| process_liquid_value(item) } + end + + def process_liquid_string(string) + return string if string.blank? + + template = Liquid::Template.parse(string) + template.render(message_drops) + rescue Liquid::Error + string + end end diff --git a/spec/models/concerns/liquidable_shared.rb b/spec/models/concerns/liquidable_shared.rb index 8df526a2f..7b9f856cd 100644 --- a/spec/models/concerns/liquidable_shared.rb +++ b/spec/models/concerns/liquidable_shared.rb @@ -69,4 +69,159 @@ shared_examples_for 'liqudable' do end end end + + context 'when liquid is present in template_params' do + let(:contact) do + create(:contact, name: 'john', email: 'john@example.com', phone_number: '+912883', custom_attributes: { customer_type: 'platinum' }) + end + let(:conversation) { create(:conversation, id: 1, contact: contact, custom_attributes: { priority: 'high' }) } + + context 'when message is outgoing with template_params' do + let(:message) { build(:message, conversation: conversation, message_type: 'outgoing') } + + it 'replaces liquid variables in template_params body' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'greet', + 'category' => 'MARKETING', + 'language' => 'en', + 'processed_params' => { + 'body' => { + 'customer_name' => '{{contact.name}}', + 'customer_email' => '{{contact.email}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_name']).to eq 'John' + expect(body_params['customer_email']).to eq 'john@example.com' + end + + it 'replaces liquid variables in nested template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'header' => { + 'media_url' => 'https://example.com/{{contact.name}}.jpg' + }, + 'body' => { + 'customer_name' => '{{contact.name}}', + 'priority' => '{{conversation.custom_attribute.priority}}' + }, + 'footer' => { + 'company' => '{{account.name}}' + } + } + } + } + message.save! + + processed = message.additional_attributes['template_params']['processed_params'] + expect(processed['header']['media_url']).to eq 'https://example.com/John.jpg' + expect(processed['body']['customer_name']).to eq 'John' + expect(processed['body']['priority']).to eq 'high' + expect(processed['footer']['company']).to eq conversation.account.name + end + + it 'handles arrays in template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'buttons' => [ + { 'type' => 'url', 'parameter' => 'https://example.com/{{contact.name}}' }, + { 'type' => 'text', 'parameter' => 'Hello {{contact.name}}' } + ] + } + } + } + message.save! + + buttons = message.additional_attributes['template_params']['processed_params']['buttons'] + expect(buttons[0]['parameter']).to eq 'https://example.com/John' + expect(buttons[1]['parameter']).to eq 'Hello John' + end + + it 'handles custom attributes in template_params' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_type' => '{{contact.custom_attribute.customer_type}}', + 'priority' => '{{conversation.custom_attribute.priority}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_type']).to eq 'platinum' + expect(body_params['priority']).to eq 'high' + end + + it 'handles missing email with default filter in template_params' do + contact.update!(email: nil) + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_email' => '{{ contact.email | default: "no-email@example.com" }}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_email']).to eq 'no-email@example.com' + end + + it 'handles broken liquid syntax in template_params gracefully' do + message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'broken_liquid' => '{{contact.name} {{invalid}}' + } + } + } + } + message.save! + + body_params = message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['broken_liquid']).to eq '{{contact.name} {{invalid}}' + end + + it 'does not process template_params when message is incoming' do + incoming_message = build(:message, conversation: conversation, message_type: 'incoming') + incoming_message.additional_attributes = { + 'template_params' => { + 'name' => 'test_template', + 'processed_params' => { + 'body' => { + 'customer_name' => '{{contact.name}}' + } + } + } + } + incoming_message.save! + + body_params = incoming_message.additional_attributes['template_params']['processed_params']['body'] + expect(body_params['customer_name']).to eq '{{contact.name}}' + end + + it 'does not process template_params when not present' do + message.additional_attributes = { 'other_data' => 'test' } + expect { message.save! }.not_to raise_error + end + end + end end From 1a1dfd09cb9ce68723f939289301cffb939a88fc Mon Sep 17 00:00:00 2001 From: Vishnu Narayanan Date: Thu, 21 Aug 2025 19:25:27 +0530 Subject: [PATCH 04/96] chore: add tidewave gem for development (#12236) - add tidewave gem for development ref: https://github.com/tidewave-ai/tidewave_rails --- Gemfile | 2 ++ Gemfile.lock | 59 ++++++++++++++++++++++++++++++++++++++++++++-------- 2 files changed, 52 insertions(+), 9 deletions(-) diff --git a/Gemfile b/Gemfile index e271e6b03..615267a77 100644 --- a/Gemfile +++ b/Gemfile @@ -212,6 +212,8 @@ group :development do gem 'stackprof' # Should install the associated chrome extension to view query logs gem 'meta_request', '>= 0.8.3' + + gem 'tidewave' end group :test do diff --git a/Gemfile.lock b/Gemfile.lock index 531e3db2c..8fa38a31e 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -230,6 +230,35 @@ GEM addressable (~> 2.8) drb (2.2.3) dry-cli (1.1.0) + dry-configurable (1.3.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-core (1.1.0) + concurrent-ruby (~> 1.0) + logger + zeitwerk (~> 2.6) + dry-inflector (1.2.0) + dry-initializer (3.2.0) + dry-logic (1.6.0) + bigdecimal + concurrent-ruby (~> 1.0) + dry-core (~> 1.1) + zeitwerk (~> 2.6) + dry-schema (1.14.1) + concurrent-ruby (~> 1.0) + dry-configurable (~> 1.0, >= 1.0.1) + dry-core (~> 1.1) + dry-initializer (~> 3.2) + dry-logic (~> 1.5) + dry-types (~> 1.8) + zeitwerk (~> 2.6) + dry-types (1.8.3) + bigdecimal (~> 3.0) + concurrent-ruby (~> 1.0) + dry-core (~> 1.0) + dry-inflector (~> 1.0) + dry-logic (~> 1.4) + zeitwerk (~> 2.6) ecma-re-validator (0.4.0) regexp_parser (~> 2.2) elastic-apm (4.6.2) @@ -270,6 +299,13 @@ GEM net-http-persistent (~> 4.0) faraday-retry (2.2.1) faraday (~> 2.0) + fast-mcp (1.5.0) + addressable (~> 2.8) + base64 + dry-schema (~> 1.14) + json (~> 2.0) + mime-types (~> 3.4) + rack (~> 3.1) fcm (1.0.8) faraday (>= 1.0.0, < 3.0) googleauth (~> 1) @@ -593,7 +629,7 @@ GEM activesupport (>= 3.0.0) raabro (1.4.0) racc (1.8.1) - rack (2.2.15) + rack (3.2.0) rack-attack (6.7.0) rack (>= 1.0, < 4) rack-contrib (2.5.0) @@ -602,19 +638,20 @@ GEM rack (>= 2.0.0) rack-mini-profiler (3.2.0) rack (>= 1.2.0) - rack-protection (3.2.0) + rack-protection (4.1.1) base64 (>= 0.1.0) - rack (~> 2.2, >= 2.2.4) + logger (>= 1.6.0) + rack (>= 3.0.0, < 4) rack-proxy (0.7.7) rack - rack-session (1.0.2) - rack (< 3) + rack-session (2.1.1) + base64 (>= 0.1.0) + rack (>= 3.0.0) rack-test (2.1.0) rack (>= 1.3) rack-timeout (0.6.3) - rackup (1.0.1) - rack (< 3) - webrick + rackup (2.2.1) + rack (>= 3) rails (7.1.5.2) actioncable (= 7.1.5.2) actionmailbox (= 7.1.5.2) @@ -845,6 +882,10 @@ GEM telephone_number (1.4.20) test-prof (1.2.1) thor (1.4.0) + tidewave (0.2.0) + fast-mcp (~> 1.5.0) + rack (>= 2.0) + rails (>= 7.1.0) tilt (2.3.0) time_diff (0.3.0) activesupport @@ -898,7 +939,6 @@ GEM addressable (>= 2.8.0) crack (>= 0.3.2) hashdiff (>= 0.4.0, < 2.0.0) - webrick (1.9.1) websocket-driver (0.7.7) base64 websocket-extensions (>= 0.1.0) @@ -1042,6 +1082,7 @@ DEPENDENCIES stripe telephone_number test-prof + tidewave time_diff twilio-ruby twitty (~> 0.1.5) From 6ca38e10e93f8fb1f499863c65c05d5c435d8c47 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 22 Aug 2025 00:43:34 +0530 Subject: [PATCH 05/96] feat: Migrate availability mixins to composable and helper (#11596) # Pull Request Template ## Description **This PR includes:** * Refactored two legacy mixins (`availability.js`, `nextAvailability.js`) into a Vue 3 composable (`useAvailability`), helper module and component based rendering logic. * Fixed an issue where the widget wouldn't load if business hours were enabled but all days were unchecked. * Fixed translation issue [[#11280](https://github.com/chatwoot/chatwoot/issues/11280)](https://github.com/chatwoot/chatwoot/issues/11280). * Reduced code complexity and size. * Added test coverage for both the composable and helper functions. ## 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/2bc3ed694b4349419505e275d14d0b98?sid=22d585e4-0dc7-4242-bcb6-e3edc16e3aee ### Story image ## 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 - [x] 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 Fixes https://github.com/chatwoot/chatwoot/issues/12012 --------- Co-authored-by: Muhsin Keloth Co-authored-by: Pranav Co-authored-by: Shivam Mishra --- app/javascript/histoire.setup.ts | 25 +- app/javascript/widget/App.vue | 31 +- .../Availability/AvailabilityContainer.vue | 87 +++ .../Availability/AvailabilityText.story.vue | 217 +++++++ .../Availability/AvailabilityText.vue | 178 ++++++ .../widget/components/ChatFooter.vue | 9 +- .../widget/components/ChatHeader.vue | 76 +-- .../widget/components/PreChat/Form.vue | 3 +- .../widget/components/TeamAvailability.vue | 98 +-- .../composables/specs/useAvailability.spec.js | 125 ++++ .../widget/composables/useAvailability.js | 69 +++ .../widget/helpers/availabilityHelpers.js | 289 +++++++++ .../helpers/specs/availabilityHelpers.spec.js | 580 ++++++++++++++++++ app/javascript/widget/i18n/locale/en.json | 10 +- app/javascript/widget/mixins/availability.js | 100 --- .../widget/mixins/nextAvailabilityTime.js | 250 -------- app/javascript/widget/mixins/routerMixin.js | 10 - .../mixins/specs/availabilityMixin.spec.js | 87 --- .../mixins/specs/nextAvailabilityTime.spec.js | 402 ------------ app/javascript/widget/views/Home.vue | 12 +- app/javascript/widget/views/PreChatForm.vue | 10 +- 21 files changed, 1662 insertions(+), 1006 deletions(-) create mode 100644 app/javascript/widget/components/Availability/AvailabilityContainer.vue create mode 100644 app/javascript/widget/components/Availability/AvailabilityText.story.vue create mode 100644 app/javascript/widget/components/Availability/AvailabilityText.vue create mode 100644 app/javascript/widget/composables/specs/useAvailability.spec.js create mode 100644 app/javascript/widget/composables/useAvailability.js create mode 100644 app/javascript/widget/helpers/availabilityHelpers.js create mode 100644 app/javascript/widget/helpers/specs/availabilityHelpers.spec.js delete mode 100644 app/javascript/widget/mixins/availability.js delete mode 100644 app/javascript/widget/mixins/nextAvailabilityTime.js delete mode 100644 app/javascript/widget/mixins/routerMixin.js delete mode 100644 app/javascript/widget/mixins/specs/availabilityMixin.spec.js delete mode 100644 app/javascript/widget/mixins/specs/nextAvailabilityTime.spec.js diff --git a/app/javascript/histoire.setup.ts b/app/javascript/histoire.setup.ts index 7642da78d..1c80a9f85 100644 --- a/app/javascript/histoire.setup.ts +++ b/app/javascript/histoire.setup.ts @@ -1,6 +1,7 @@ import './design-system/histoire.scss'; import { defineSetupVue3 } from '@histoire/plugin-vue'; -import i18nMessages from 'dashboard/i18n'; +import dashboardI18n from 'dashboard/i18n'; +import widgetI18n from 'widget/i18n'; import { createI18n } from 'vue-i18n'; import { vResizeObserver } from '@vueuse/components'; import store from 'dashboard/store'; @@ -9,10 +10,30 @@ import VueDOMPurifyHTML from 'vue-dompurify-html'; import { domPurifyConfig } from 'shared/helpers/HTMLSanitizer.js'; import { directive as onClickaway } from 'vue3-click-away'; +function mergeMessages(...sources) { + return sources.reduce((acc, src) => { + Object.keys(src).forEach(key => { + if ( + acc[key] && + typeof acc[key] === 'object' && + typeof src[key] === 'object' + ) { + acc[key] = mergeMessages(acc[key], src[key]); + } else { + acc[key] = src[key]; + } + }); + return acc; + }, {}); +} + const i18n = createI18n({ legacy: false, // https://github.com/intlify/vue-i18n/issues/1902 locale: 'en', - messages: i18nMessages, + messages: mergeMessages( + structuredClone(dashboardI18n), + structuredClone(widgetI18n) + ), }); export const setupVue3 = defineSetupVue3(({ app }) => { diff --git a/app/javascript/widget/App.vue b/app/javascript/widget/App.vue index 48409cef2..379b2bc53 100755 --- a/app/javascript/widget/App.vue +++ b/app/javascript/widget/App.vue @@ -4,12 +4,10 @@ import { setHeader } from 'widget/helpers/axios'; import addHours from 'date-fns/addHours'; import { IFrameHelper, RNHelper } from 'widget/helpers/utils'; import configMixin from './mixins/configMixin'; -import availabilityMixin from 'widget/mixins/availability'; import { getLocale } from './helpers/urlParamsHelper'; import { getLanguageDirection } from 'dashboard/components/widgets/conversation/advancedFilterItems/languages'; import { isEmptyObject } from 'widget/helpers/utils'; import Spinner from 'shared/components/Spinner.vue'; -import routerMixin from './mixins/routerMixin'; import { getExtraSpaceToScroll, loadedEventConfig, @@ -20,6 +18,8 @@ import { ON_UNREAD_MESSAGE_CLICK, } from './constants/widgetBusEvents'; import { useDarkMode } from 'widget/composables/useDarkMode'; +import { useRouter } from 'vue-router'; +import { useAvailability } from 'widget/composables/useAvailability'; import { SDK_SET_BUBBLE_VISIBILITY } from '../shared/constants/sharedFrameEvents'; import { emitter } from 'shared/helpers/mitt'; @@ -28,10 +28,13 @@ export default { components: { Spinner, }, - mixins: [availabilityMixin, configMixin, routerMixin], + mixins: [configMixin], setup() { const { prefersDarkMode } = useDarkMode(); - return { prefersDarkMode }; + const router = useRouter(); + const { isInWorkingHours } = useAvailability(); + + return { prefersDarkMode, router, isInWorkingHours }; }, data() { return { @@ -157,15 +160,17 @@ export default { this.setUnreadView(); }); emitter.on(ON_UNREAD_MESSAGE_CLICK, () => { - this.replaceRoute('messages').then(() => this.unsetUnreadView()); + this.router + .replace({ name: 'messages' }) + .then(() => this.unsetUnreadView()); }); }, registerCampaignEvents() { emitter.on(ON_CAMPAIGN_MESSAGE_CLICK, () => { if (this.shouldShowPreChatForm) { - this.replaceRoute('prechat-form'); + this.router.replace({ name: 'prechat-form' }); } else { - this.replaceRoute('messages'); + this.router.replace({ name: 'messages' }); emitter.emit('execute-campaign', { campaignId: this.activeCampaign.id, }); @@ -176,7 +181,7 @@ export default { const { customAttributes, campaignId } = campaignDetails; const { websiteToken } = window.chatwootWebChannel; this.executeCampaign({ campaignId, websiteToken, customAttributes }); - this.replaceRoute('messages'); + this.router.replace({ name: 'messages' }); }); emitter.on('snooze-campaigns', () => { const expireBy = addHours(new Date(), 1); @@ -192,7 +197,7 @@ export default { !messageCount && !shouldSnoozeCampaign; if (this.isIFrame && isCampaignReadyToExecute) { - this.replaceRoute('campaigns').then(() => { + this.router.replace({ name: 'campaigns' }).then(() => { this.setIframeHeight(true); IFrameHelper.sendMessage({ event: 'setUnreadMode' }); }); @@ -207,7 +212,7 @@ export default { unreadMessageCount > 0 && !this.isWidgetOpen ) { - this.replaceRoute('unread-messages').then(() => { + this.router.replace({ name: 'unread-messages' }).then(() => { this.setIframeHeight(true); IFrameHelper.sendMessage({ event: 'setUnreadMode' }); }); @@ -263,7 +268,7 @@ export default { this.initCampaigns({ currentURL: referrerURL, websiteToken, - isInBusinessHours: this.isInBusinessHours, + isInBusinessHours: this.isInWorkingHours, }); window.referrerURL = referrerURL; this.setReferrerHost(referrerHost); @@ -314,12 +319,12 @@ export default { ['unread-messages', 'campaigns'].includes(this.$route.name); if (shouldShowMessageView) { - this.replaceRoute('messages'); + this.router.replace({ name: 'messages' }); } if (shouldShowHomeView) { this.$store.dispatch('conversation/setUserLastSeen'); this.unsetUnreadView(); - this.replaceRoute('home'); + this.router.replace({ name: 'home' }); } if (!message.isOpen) { this.resetCampaign(); diff --git a/app/javascript/widget/components/Availability/AvailabilityContainer.vue b/app/javascript/widget/components/Availability/AvailabilityContainer.vue new file mode 100644 index 000000000..367564751 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityContainer.vue @@ -0,0 +1,87 @@ + + + diff --git a/app/javascript/widget/components/Availability/AvailabilityText.story.vue b/app/javascript/widget/components/Availability/AvailabilityText.story.vue new file mode 100644 index 000000000..aafcb3556 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityText.story.vue @@ -0,0 +1,217 @@ + + + diff --git a/app/javascript/widget/components/Availability/AvailabilityText.vue b/app/javascript/widget/components/Availability/AvailabilityText.vue new file mode 100644 index 000000000..649224d20 --- /dev/null +++ b/app/javascript/widget/components/Availability/AvailabilityText.vue @@ -0,0 +1,178 @@ + + + diff --git a/app/javascript/widget/components/ChatFooter.vue b/app/javascript/widget/components/ChatFooter.vue index 2bc6ba7ec..c85727a2b 100755 --- a/app/javascript/widget/components/ChatFooter.vue +++ b/app/javascript/widget/components/ChatFooter.vue @@ -6,7 +6,7 @@ import FooterReplyTo from 'widget/components/FooterReplyTo.vue'; import ChatInputWrap from 'widget/components/ChatInputWrap.vue'; import { BUS_EVENTS } from 'shared/constants/busEvents'; import { sendEmailTranscript } from 'widget/api/conversation'; -import routerMixin from 'widget/mixins/routerMixin'; +import { useRouter } from 'vue-router'; import { IFrameHelper } from '../helpers/utils'; import { CHATWOOT_ON_START_CONVERSATION } from '../constants/sdkEvents'; import { emitter } from 'shared/helpers/mitt'; @@ -17,7 +17,10 @@ export default { CustomButton, FooterReplyTo, }, - mixins: [routerMixin], + setup() { + const router = useRouter(); + return { router }; + }, data() { return { inReplyTo: null, @@ -77,7 +80,7 @@ export default { this.inReplyTo = null; }, startNewConversation() { - this.replaceRoute('prechat-form'); + this.router.replace({ name: 'prechat-form' }); IFrameHelper.sendMessage({ event: 'onEvent', eventIdentifier: CHATWOOT_ON_START_CONVERSATION, diff --git a/app/javascript/widget/components/ChatHeader.vue b/app/javascript/widget/components/ChatHeader.vue index 7c497b70f..578fb984e 100644 --- a/app/javascript/widget/components/ChatHeader.vue +++ b/app/javascript/widget/components/ChatHeader.vue @@ -1,55 +1,26 @@ - @@ -79,9 +50,12 @@ export default { ${isOnline ? 'bg-n-teal-10' : 'hidden'}`" /> -
- {{ replyWaitMessage }} -
+ diff --git a/app/javascript/widget/components/PreChat/Form.vue b/app/javascript/widget/components/PreChat/Form.vue index 6fe8e45a1..84f0036fd 100644 --- a/app/javascript/widget/components/PreChat/Form.vue +++ b/app/javascript/widget/components/PreChat/Form.vue @@ -6,7 +6,6 @@ import { getContrastingTextColor } from '@chatwoot/utils'; import { isEmptyObject } from 'widget/helpers/utils'; import { getRegexp } from 'shared/helpers/Validators'; import { useMessageFormatter } from 'shared/composables/useMessageFormatter'; -import routerMixin from 'widget/mixins/routerMixin'; import configMixin from 'widget/mixins/configMixin'; import { FormKit, createInput } from '@formkit/vue'; import PhoneInput from 'widget/components/Form/PhoneInput.vue'; @@ -17,7 +16,7 @@ export default { Spinner, FormKit, }, - mixins: [routerMixin, configMixin], + mixins: [configMixin], props: { options: { type: Object, diff --git a/app/javascript/widget/components/TeamAvailability.vue b/app/javascript/widget/components/TeamAvailability.vue index 866854c09..cc08d34c2 100644 --- a/app/javascript/widget/components/TeamAvailability.vue +++ b/app/javascript/widget/components/TeamAvailability.vue @@ -1,74 +1,27 @@ - @@ -76,17 +29,8 @@ export default {
-
-
-
- {{ headerMessage }} -
-
- {{ replyWaitMessage }} -
-
- -
+ +