From 46c1734ba19caa1b9cd36ce202637802da038f42 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 28 Feb 2022 12:10:55 +0530 Subject: [PATCH 01/43] feat: Add additional attributes in `setUser` method (#3958) --- app/actions/contact_identify_action.rb | 15 +++++++-- .../api/v1/widget/contacts_controller.rb | 3 +- .../widget/store/modules/contacts.js | 33 +++++++++++++++---- spec/actions/contact_identify_action_spec.rb | 14 +++++++- 4 files changed, 55 insertions(+), 10 deletions(-) diff --git a/app/actions/contact_identify_action.rb b/app/actions/contact_identify_action.rb index 655c6bc1cad..746d1bfec4d 100644 --- a/app/actions/contact_identify_action.rb +++ b/app/actions/contact_identify_action.rb @@ -52,12 +52,11 @@ class ContactIdentifyAction end def update_contact - custom_attributes = params[:custom_attributes] ? @contact.custom_attributes.merge(params[:custom_attributes]) : @contact.custom_attributes # blank identifier or email will throw unique index error # TODO: replace reject { |_k, v| v.blank? } with compact_blank when rails is upgraded @contact.update!(params.slice(:name, :email, :identifier, :phone_number).reject do |_k, v| v.blank? - end.merge({ custom_attributes: custom_attributes })) + end.merge({ custom_attributes: custom_attributes, additional_attributes: additional_attributes })) ContactAvatarJob.perform_later(@contact, params[:avatar_url]) if params[:avatar_url].present? end @@ -68,4 +67,16 @@ class ContactIdentifyAction mergee_contact: merge_contact ).perform end + + def custom_attributes + params[:custom_attributes] ? @contact.custom_attributes.deep_merge(params[:custom_attributes].stringify_keys) : @contact.custom_attributes + end + + def additional_attributes + if params[:additional_attributes] + @contact.additional_attributes.deep_merge(params[:additional_attributes].stringify_keys) + else + @contact.additional_attributes + end + end end diff --git a/app/controllers/api/v1/widget/contacts_controller.rb b/app/controllers/api/v1/widget/contacts_controller.rb index d745c4153c2..fbc303a4f76 100644 --- a/app/controllers/api/v1/widget/contacts_controller.rb +++ b/app/controllers/api/v1/widget/contacts_controller.rb @@ -46,6 +46,7 @@ class Api::V1::Widget::ContactsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}) + params.permit(:website_token, :identifier, :identifier_hash, :email, :name, :avatar_url, :phone_number, custom_attributes: {}, + additional_attributes: {}) end end diff --git a/app/javascript/widget/store/modules/contacts.js b/app/javascript/widget/store/modules/contacts.js index 79fb36de6b1..ef146ce72cb 100644 --- a/app/javascript/widget/store/modules/contacts.js +++ b/app/javascript/widget/store/modules/contacts.js @@ -24,17 +24,38 @@ export const actions = { }, update: async ({ dispatch }, { identifier, user: userObject }) => { try { + const { + email, + name, + avatar_url, + identifier_hash, + phone_number, + company_name, + city, + country_code, + description, + custom_attributes, + social_profiles, + } = userObject; const user = { - email: userObject.email, - name: userObject.name, - avatar_url: userObject.avatar_url, - identifier_hash: userObject.identifier_hash, - phone_number: userObject.phone_number, + email, + name, + avatar_url, + identifier_hash, + phone_number, + additional_attributes: { + company_name, + city, + description, + country_code, + social_profiles, + }, + custom_attributes, }; await ContactsAPI.update(identifier, user); dispatch('get'); - if (userObject.identifier_hash) { + if (identifier_hash) { dispatch('conversation/clearConversations', {}, { root: true }); dispatch('conversation/fetchOldConversations', {}, { root: true }); } diff --git a/spec/actions/contact_identify_action_spec.rb b/spec/actions/contact_identify_action_spec.rb index 71c1b274af8..ad109a0ead9 100644 --- a/spec/actions/contact_identify_action_spec.rb +++ b/spec/actions/contact_identify_action_spec.rb @@ -6,7 +6,10 @@ describe ::ContactIdentifyAction do let!(:account) { create(:account) } let(:custom_attributes) { { test: 'test', test1: 'test1' } } let!(:contact) { create(:contact, account: account, custom_attributes: custom_attributes) } - let(:params) { { name: 'test', identifier: 'test_id', custom_attributes: { test: 'new test', test2: 'test2' } } } + let(:params) do + { name: 'test', identifier: 'test_id', additional_attributes: { location: 'Bengaulru', company_name: 'Meta' }, + custom_attributes: { test: 'new test', test2: 'test2' } } + end describe '#perform' do it 'updates the contact' do @@ -15,9 +18,18 @@ describe ::ContactIdentifyAction do expect(contact.reload.name).to eq 'test' # custom attributes are merged properly without overwriting existing ones expect(contact.custom_attributes).to eq({ 'test' => 'new test', 'test1' => 'test1', 'test2' => 'test2' }) + expect(contact.additional_attributes).to eq({ 'company_name' => 'Meta', 'location' => 'Bengaulru' }) expect(contact.reload.identifier).to eq 'test_id' end + it 'merge deeply nested additional attributes' do + create(:contact, account: account, identifier: '', email: 'test@test.com', + additional_attributes: { location: 'Bengaulru', company_name: 'Meta', social_profiles: { linkedin: 'saras' } }) + params = { email: 'test@test.com', additional_attributes: { social_profiles: { twitter: 'saras' } } } + result = described_class.new(contact: contact, params: params).perform + expect(result.additional_attributes['social_profiles']).to eq({ 'linkedin' => 'saras', 'twitter' => 'saras' }) + end + it 'enques avatar job when avatar url parameter is passed' do params = { name: 'test', avatar_url: 'https://chatwoot-assets.local/sample.png' } expect(ContactAvatarJob).to receive(:perform_later).with(contact, params[:avatar_url]).once From 87a6266ddc07df23aeb01db1f26e060c2c3f567d Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Mon, 28 Feb 2022 14:53:11 +0530 Subject: [PATCH 02/43] fix: Add resetTriggered flag to fix reset loop (#4079) --- app/javascript/packs/sdk.js | 3 +++ app/javascript/sdk/IFrameHelper.js | 4 +++- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/app/javascript/packs/sdk.js b/app/javascript/packs/sdk.js index c4bfcdb0839..66f6ba790ea 100755 --- a/app/javascript/packs/sdk.js +++ b/app/javascript/packs/sdk.js @@ -25,6 +25,7 @@ const runSDK = ({ baseUrl, websiteToken }) => { launcherTitle: chatwootSettings.launcherTitle || '', showPopoutButton: chatwootSettings.showPopoutButton || false, widgetStyle: chatwootSettings.widgetStyle || 'standard', + resetTriggered: false, toggle(state) { IFrameHelper.events.toggleBubble(state); @@ -100,6 +101,8 @@ const runSDK = ({ baseUrl, websiteToken }) => { baseUrl: window.$chatwoot.baseUrl, websiteToken: window.$chatwoot.websiteToken, }); + + window.$chatwoot.resetTriggered = true; }, }; diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 2d851ba4975..94d91f77f6d 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -136,7 +136,9 @@ export const IFrameHelper = { if (window.$chatwoot.user) { IFrameHelper.sendMessage('set-user', window.$chatwoot.user); } - dispatchWindowEvent({ eventName: CHATWOOT_READY }); + if (!window.$chatwoot.resetTriggered) { + dispatchWindowEvent({ eventName: CHATWOOT_READY }); + } }, error: ({ errorType, data }) => { dispatchWindowEvent({ eventName: CHATWOOT_ERROR, data: data }); From aff14b697f8ebf1cb09f6f70236e5db911675893 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 28 Feb 2022 15:44:02 +0530 Subject: [PATCH 03/43] chore: Add webhook URL validation (#4080) --- app/models/webhook.rb | 2 +- .../api/v1/accounts/webhook_controller_spec.rb | 9 +++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/app/models/webhook.rb b/app/models/webhook.rb index 2895978f923..a78e7c26731 100644 --- a/app/models/webhook.rb +++ b/app/models/webhook.rb @@ -20,7 +20,7 @@ class Webhook < ApplicationRecord belongs_to :inbox, optional: true validates :account_id, presence: true - validates :url, uniqueness: { scope: [:account_id] }, format: { with: URI::DEFAULT_PARSER.make_regexp } + validates :url, uniqueness: { scope: [:account_id] }, format: URI::DEFAULT_PARSER.make_regexp(%w[http https]) enum webhook_type: { account: 0, inbox: 1 } end diff --git a/spec/controllers/api/v1/accounts/webhook_controller_spec.rb b/spec/controllers/api/v1/accounts/webhook_controller_spec.rb index 5b839ebef92..ccafe2f562c 100644 --- a/spec/controllers/api/v1/accounts/webhook_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/webhook_controller_spec.rb @@ -48,6 +48,15 @@ RSpec.describe 'Webhooks API', type: :request do expect(JSON.parse(response.body)['payload']['webhook']['url']).to eql 'https://hello.com' end + + it 'throws error when invalid url provided' do + post "/api/v1/accounts/#{account.id}/webhooks", + params: { account_id: account.id, inbox_id: inbox.id, url: 'javascript:alert(1)' }, + headers: administrator.create_new_auth_token, + as: :json + expect(response).to have_http_status(:unprocessable_entity) + expect(JSON.parse(response.body)['message']).to eql 'Url is invalid' + end end end From 12c0be002e20f0ed1bb44a83280941fcd6552ad4 Mon Sep 17 00:00:00 2001 From: "Aswin Dev P.S" Date: Mon, 28 Feb 2022 16:54:43 +0530 Subject: [PATCH 04/43] feat: Format time in reports (#4072) Co-authored-by: Pranav Raj S --- .../dashboard/assets/scss/widgets/_report.scss | 3 ++- app/javascript/dashboard/store/modules/reports.js | 11 +++-------- package.json | 2 +- yarn.lock | 15 +++++++++++---- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/app/javascript/dashboard/assets/scss/widgets/_report.scss b/app/javascript/dashboard/assets/scss/widgets/_report.scss index 0ee598245db..c62eba70e36 100644 --- a/app/javascript/dashboard/assets/scss/widgets/_report.scss +++ b/app/javascript/dashboard/assets/scss/widgets/_report.scss @@ -21,8 +21,9 @@ } .metric { - font-size: $font-size-bigger; + font-size: $font-size-big; font-weight: $font-weight-feather; + margin-top: $space-smaller; } .desc { diff --git a/app/javascript/dashboard/store/modules/reports.js b/app/javascript/dashboard/store/modules/reports.js index 33f62a9c4d8..f2967945a52 100644 --- a/app/javascript/dashboard/store/modules/reports.js +++ b/app/javascript/dashboard/store/modules/reports.js @@ -5,6 +5,7 @@ import * as types from '../mutation-types'; import Report from '../../api/reports'; import { downloadCsvFile } from '../../helper/downloadCsvFile'; +import { formatTime } from '@chatwoot/utils'; const state = { fetchingStatus: false, @@ -127,18 +128,12 @@ const mutations = { // Average First Response Time let avgFirstResTimeInHr = 0; if (summaryData.avg_first_response_time) { - avgFirstResTimeInHr = ( - summaryData.avg_first_response_time / 3600 - ).toFixed(2); - avgFirstResTimeInHr = `${avgFirstResTimeInHr} Hr`; + avgFirstResTimeInHr = formatTime(summaryData.avg_first_response_time); } // Average Resolution Time let avgResolutionTimeInHr = 0; if (summaryData.avg_resolution_time) { - avgResolutionTimeInHr = (summaryData.avg_resolution_time / 3600).toFixed( - 2 - ); - avgResolutionTimeInHr = `${avgResolutionTimeInHr} Hr`; + avgResolutionTimeInHr = formatTime(summaryData.avg_resolution_time); } _state.accountSummary.avg_first_response_time = avgFirstResTimeInHr; _state.accountSummary.avg_resolution_time = avgResolutionTimeInHr; diff --git a/package.json b/package.json index 894dd7f4eb0..17733fde303 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517", - "@chatwoot/utils": "^0.0.3", + "@chatwoot/utils": "^0.0.4", "@hcaptcha/vue-hcaptcha": "^0.3.2", "@rails/actioncable": "6.1.3", "@rails/webpacker": "5.3.0", diff --git a/yarn.lock b/yarn.lock index 1fdf9fd50fc..c98eb5098b5 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,10 +1111,12 @@ prosemirror-state "^1.3.3" prosemirror-view "^1.17.2" -"@chatwoot/utils@^0.0.3": - version "0.0.3" - resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.3.tgz#40fbb6c6d1aed853b3c5189d0f4979d3ca185a2f" - integrity sha512-2KbA1r34IM2U58CUvsC9dEN3gsx5Dcc6uD1iHPFNC3zTsg8Tb+K7IQXxfZt+VAvV0fbniLzCuXt6h81Ds0aj6Q== +"@chatwoot/utils@^0.0.4": + version "0.0.4" + resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.4.tgz#519c0e0ace4d01e821bf24e36111ea7ed82da451" + integrity sha512-giL85pBJWPmUKM+AOpeWeWvsuUJrHdSAJ4RKaQh/YNQhb8AjKxdx476s7UTaKd5DV+OnCpWtmHtNV+38CAC6lw== + dependencies: + date-fns "^2.22.1" "@cnakazawa/watch@^1.0.3": version "1.0.4" @@ -5462,6 +5464,11 @@ date-fns@^1.27.2: resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-1.30.1.tgz#2e71bf0b119153dbb4cc4e88d9ea5acfb50dc05c" integrity sha512-hBSVCvSmWC+QypYObzwGOd9wqdDpOt+0wl0KbU+R+uuZBS1jN8VsD1ss3irQDknRj5NvxiTF6oj/nDRnN/UQNw== +date-fns@^2.22.1: + version "2.28.0" + resolved "https://registry.yarnpkg.com/date-fns/-/date-fns-2.28.0.tgz#9570d656f5fc13143e50c975a3b6bbeb46cd08b2" + integrity sha512-8d35hViGYx/QH0icHYCeLmsLmMUheMmTyV9Fcm6gvNwdw31yXXH+O85sOBJ+OLnLQMKZowvpKb6FgMIQjcpvQw== + date-format-parse@^0.2.6: version "0.2.6" resolved "https://registry.yarnpkg.com/date-format-parse/-/date-format-parse-0.2.6.tgz#7ed42817efb6ec39532169eada9670ae3c627aa7" From 4260441f8c95b141095e61fbe23e12852cdd8347 Mon Sep 17 00:00:00 2001 From: Sojan Jose Date: Mon, 28 Feb 2022 18:16:12 +0530 Subject: [PATCH 05/43] Chore: clean up Reporting Events (#4044) Tech debt clean up Fixes #4057 Co-authored-by: Aswin Dev P S --- app/builders/v2/report_builder.rb | 8 +-- app/dispatchers/async_dispatcher.rb | 2 +- app/helpers/api/v1/reports_helper.rb | 2 - ...istener.rb => reporting_event_listener.rb} | 10 +-- app/listeners/reporting_listener.rb | 45 ------------ app/models/concerns/reportable.rb | 2 +- app/models/label.rb | 4 +- app/models/{event.rb => reporting_event.rb} | 14 ++-- app/models/team.rb | 4 +- ...13143021_rename_events_to_report_events.rb | 5 ++ db/schema.rb | 32 ++++----- lib/reports/update_account_identity.rb | 10 --- lib/reports/update_agent_identity.rb | 11 --- lib/reports/update_identity.rb | 69 ------------------- spec/builders/v2/report_builder_spec.rb | 2 +- .../{events.rb => reporting_events.rb} | 2 +- ...ec.rb => reporting_event_listener_spec.rb} | 10 +-- spec/models/account_spec.rb | 2 +- spec/models/inbox_spec.rb | 2 +- ...{event_spec.rb => reporting_event_spec.rb} | 2 +- spec/models/user_spec.rb | 2 +- 21 files changed, 54 insertions(+), 186 deletions(-) delete mode 100644 app/helpers/api/v1/reports_helper.rb rename app/listeners/{event_listener.rb => reporting_event_listener.rb} (83%) delete mode 100644 app/listeners/reporting_listener.rb rename app/models/{event.rb => reporting_event.rb} (64%) create mode 100644 db/migrate/20210513143021_rename_events_to_report_events.rb delete mode 100644 lib/reports/update_account_identity.rb delete mode 100644 lib/reports/update_agent_identity.rb delete mode 100644 lib/reports/update_identity.rb rename spec/factories/{events.rb => reporting_events.rb} (83%) rename spec/listeners/{event_listener_spec.rb => reporting_event_listener_spec.rb} (69%) rename spec/models/{event_spec.rb => reporting_event_spec.rb} (91%) diff --git a/app/builders/v2/report_builder.rb b/app/builders/v2/report_builder.rb index 7f0dd470ca2..1f17dbb084c 100644 --- a/app/builders/v2/report_builder.rb +++ b/app/builders/v2/report_builder.rb @@ -95,15 +95,15 @@ class V2::ReportBuilder end def avg_first_response_time - (get_grouped_values scope.events.where(name: 'first_response')).average(:value) + (get_grouped_values scope.reporting_events.where(name: 'first_response')).average(:value) end def avg_resolution_time - (get_grouped_values scope.events.where(name: 'conversation_resolved')).average(:value) + (get_grouped_values scope.reporting_events.where(name: 'conversation_resolved')).average(:value) end def avg_resolution_time_summary - avg_rt = scope.events + avg_rt = scope.reporting_events .where(name: 'conversation_resolved', created_at: range) .average(:value) @@ -113,7 +113,7 @@ class V2::ReportBuilder end def avg_first_response_time_summary - avg_frt = scope.events + avg_frt = scope.reporting_events .where(name: 'first_response', created_at: range) .average(:value) diff --git a/app/dispatchers/async_dispatcher.rb b/app/dispatchers/async_dispatcher.rb index 3f95405f9ff..f2f238cd490 100644 --- a/app/dispatchers/async_dispatcher.rb +++ b/app/dispatchers/async_dispatcher.rb @@ -12,10 +12,10 @@ class AsyncDispatcher < BaseDispatcher [ CampaignListener.instance, CsatSurveyListener.instance, - EventListener.instance, HookListener.instance, InstallationWebhookListener.instance, NotificationListener.instance, + ReportingEventListener.instance, WebhookListener.instance, AutomationRuleListener.instance ] diff --git a/app/helpers/api/v1/reports_helper.rb b/app/helpers/api/v1/reports_helper.rb deleted file mode 100644 index 308bab3e640..00000000000 --- a/app/helpers/api/v1/reports_helper.rb +++ /dev/null @@ -1,2 +0,0 @@ -module Api::V1::ReportsHelper -end diff --git a/app/listeners/event_listener.rb b/app/listeners/reporting_event_listener.rb similarity index 83% rename from app/listeners/event_listener.rb rename to app/listeners/reporting_event_listener.rb index 8e7fd0daf2f..05aa1db4bb9 100644 --- a/app/listeners/event_listener.rb +++ b/app/listeners/reporting_event_listener.rb @@ -1,9 +1,9 @@ -class EventListener < BaseListener +class ReportingEventListener < BaseListener def conversation_resolved(event) conversation = extract_conversation_and_account(event)[0] time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i - event = Event.new( + reporting_event = ReportingEvent.new( name: 'conversation_resolved', value: time_to_resolve, account_id: conversation.account_id, @@ -11,7 +11,7 @@ class EventListener < BaseListener user_id: conversation.assignee_id, conversation_id: conversation.id ) - event.save + reporting_event.save end def first_reply_created(event) @@ -19,7 +19,7 @@ class EventListener < BaseListener conversation = message.conversation first_response_time = message.created_at.to_i - conversation.created_at.to_i - event = Event.new( + reporting_event = ReportingEvent.new( name: 'first_response', value: first_response_time, account_id: conversation.account_id, @@ -27,6 +27,6 @@ class EventListener < BaseListener user_id: conversation.assignee_id, conversation_id: conversation.id ) - event.save + reporting_event.save end end diff --git a/app/listeners/reporting_listener.rb b/app/listeners/reporting_listener.rb deleted file mode 100644 index 407dc47bc4e..00000000000 --- a/app/listeners/reporting_listener.rb +++ /dev/null @@ -1,45 +0,0 @@ -class ReportingListener < BaseListener - def conversation_created(event) - conversation, account = extract_conversation_and_account(event) - timestamp = event.timestamp - - ::Reports::UpdateAccountIdentity.new(account, timestamp).incr_conversations_count - end - - def conversation_resolved(event) - conversation, account = extract_conversation_and_account(event) - timestamp = event.timestamp - - time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i - - if conversation.assignee.present? - agent = conversation.assignee - ::Reports::UpdateAgentIdentity.new(account, agent, timestamp).update_avg_resolution_time(time_to_resolve) - ::Reports::UpdateAgentIdentity.new(account, agent, timestamp).incr_resolutions_count - end - - ::Reports::UpdateAccountIdentity.new(account, timestamp).update_avg_resolution_time(time_to_resolve) - ::Reports::UpdateAccountIdentity.new(account, timestamp).incr_resolutions_count - end - - def first_reply_created(event) - message, account = extract_message_and_account(event) - timestamp = event.timestamp - - conversation = message.conversation - agent = conversation.assignee - first_response_time = message.created_at.to_i - conversation.created_at.to_i - ::Reports::UpdateAgentIdentity.new(account, agent, timestamp).update_avg_first_response_time(first_response_time) if agent.present? - ::Reports::UpdateAccountIdentity.new(account, timestamp).update_avg_first_response_time(first_response_time) - end - - def message_created(event) - message, account = extract_message_and_account(event) - timestamp = event.timestamp - - return unless message.reportable? - - identity = ::Reports::UpdateAccountIdentity.new(account, timestamp) - message.outgoing? ? identity.incr_outgoing_messages_count : identity.incr_incoming_messages_count - end -end diff --git a/app/models/concerns/reportable.rb b/app/models/concerns/reportable.rb index 9da1e93dca1..23ff69fd2af 100644 --- a/app/models/concerns/reportable.rb +++ b/app/models/concerns/reportable.rb @@ -4,6 +4,6 @@ module Reportable extend ActiveSupport::Concern included do - has_many :events, dependent: :destroy_async + has_many :reporting_events, dependent: :destroy end end diff --git a/app/models/label.rb b/app/models/label.rb index 9b551141d5d..650ea6f8ea0 100644 --- a/app/models/label.rb +++ b/app/models/label.rb @@ -39,8 +39,8 @@ class Label < ApplicationRecord account.messages.where(conversation_id: conversations.pluck(:id)) end - def events - account.events.where(conversation_id: conversations.pluck(:id)) + def reporting_events + account.reporting_events.where(conversation_id: conversations.pluck(:id)) end private diff --git a/app/models/event.rb b/app/models/reporting_event.rb similarity index 64% rename from app/models/event.rb rename to app/models/reporting_event.rb index 7cb49c57649..288b15504da 100644 --- a/app/models/event.rb +++ b/app/models/reporting_event.rb @@ -1,6 +1,6 @@ # == Schema Information # -# Table name: events +# Table name: reporting_events # # id :bigint not null, primary key # name :string @@ -14,14 +14,14 @@ # # Indexes # -# index_events_on_account_id (account_id) -# index_events_on_created_at (created_at) -# index_events_on_inbox_id (inbox_id) -# index_events_on_name (name) -# index_events_on_user_id (user_id) +# index_reporting_events_on_account_id (account_id) +# index_reporting_events_on_created_at (created_at) +# index_reporting_events_on_inbox_id (inbox_id) +# index_reporting_events_on_name (name) +# index_reporting_events_on_user_id (user_id) # -class Event < ApplicationRecord +class ReportingEvent < ApplicationRecord validates :account_id, presence: true validates :name, presence: true validates :value, presence: true diff --git a/app/models/team.rb b/app/models/team.rb index 74ce2aedf2c..22e9b3fc6eb 100644 --- a/app/models/team.rb +++ b/app/models/team.rb @@ -45,7 +45,7 @@ class Team < ApplicationRecord account.messages.where(conversation_id: conversations.pluck(:id)) end - def events - account.events.where(conversation_id: conversations.pluck(:id)) + def reporting_events + account.reporting_events.where(conversation_id: conversations.pluck(:id)) end end diff --git a/db/migrate/20210513143021_rename_events_to_report_events.rb b/db/migrate/20210513143021_rename_events_to_report_events.rb new file mode 100644 index 00000000000..9241ff7b21b --- /dev/null +++ b/db/migrate/20210513143021_rename_events_to_report_events.rb @@ -0,0 +1,5 @@ +class RenameEventsToReportEvents < ActiveRecord::Migration[6.0] + def change + rename_table :events, :reporting_events + end +end diff --git a/db/schema.rb b/db/schema.rb index 8c37b7897d3..2f3334b74d4 100644 --- a/db/schema.rb +++ b/db/schema.rb @@ -434,22 +434,6 @@ ActiveRecord::Schema.define(version: 2022_02_18_120357) do t.index ["name", "account_id"], name: "index_email_templates_on_name_and_account_id", unique: true end - create_table "events", force: :cascade do |t| - t.string "name" - t.float "value" - t.integer "account_id" - t.integer "inbox_id" - t.integer "user_id" - t.integer "conversation_id" - t.datetime "created_at", precision: 6, null: false - t.datetime "updated_at", precision: 6, null: false - t.index ["account_id"], name: "index_events_on_account_id" - t.index ["created_at"], name: "index_events_on_created_at" - t.index ["inbox_id"], name: "index_events_on_inbox_id" - t.index ["name"], name: "index_events_on_name" - t.index ["user_id"], name: "index_events_on_user_id" - end - create_table "inbox_members", id: :serial, force: :cascade do |t| t.integer "user_id", null: false t.integer "inbox_id", null: false @@ -664,6 +648,22 @@ ActiveRecord::Schema.define(version: 2022_02_18_120357) do t.datetime "updated_at", precision: 6, null: false end + create_table "reporting_events", force: :cascade do |t| + t.string "name" + t.float "value" + t.integer "account_id" + t.integer "inbox_id" + t.integer "user_id" + t.integer "conversation_id" + t.datetime "created_at", precision: 6, null: false + t.datetime "updated_at", precision: 6, null: false + t.index ["account_id"], name: "index_reporting_events_on_account_id" + t.index ["created_at"], name: "index_reporting_events_on_created_at" + t.index ["inbox_id"], name: "index_reporting_events_on_inbox_id" + t.index ["name"], name: "index_reporting_events_on_name" + t.index ["user_id"], name: "index_reporting_events_on_user_id" + end + create_table "taggings", id: :serial, force: :cascade do |t| t.integer "tag_id" t.string "taggable_type" diff --git a/lib/reports/update_account_identity.rb b/lib/reports/update_account_identity.rb deleted file mode 100644 index 5a19f439fb6..00000000000 --- a/lib/reports/update_account_identity.rb +++ /dev/null @@ -1,10 +0,0 @@ -# frozen_string_literal: true - -class Reports::UpdateAccountIdentity < Reports::UpdateIdentity - attr_reader :account - - def initialize(account, timestamp = Time.now) - super(account, timestamp) - @identity = ::AccountIdentity.new(account.id) - end -end diff --git a/lib/reports/update_agent_identity.rb b/lib/reports/update_agent_identity.rb deleted file mode 100644 index e40d8353ede..00000000000 --- a/lib/reports/update_agent_identity.rb +++ /dev/null @@ -1,11 +0,0 @@ -# frozen_string_literal: true - -class Reports::UpdateAgentIdentity < Reports::UpdateIdentity - attr_reader :agent - - def initialize(account, agent, timestamp = Time.now) - super(account, timestamp) - @agent = agent - @identity = ::AgentIdentity.new(agent.id, tags: { account_id: account.id }) - end -end diff --git a/lib/reports/update_identity.rb b/lib/reports/update_identity.rb deleted file mode 100644 index 0849936f5bf..00000000000 --- a/lib/reports/update_identity.rb +++ /dev/null @@ -1,69 +0,0 @@ -# frozen_string_literal: true - -class Reports::UpdateIdentity - attr_reader :account, :identity - attr_accessor :timestamp - - def initialize(account, timestamp = Time.now) - @account = account - @timestamp = timestamp - end - - def incr_conversations_count(step = 1) - update_conversations_count(:incr, step) - end - - def decr_conversations_count(step = 1) - update_conversations_count(:decr, step) - end - - def incr_incoming_messages_count(step = 1) - update_incoming_messages_count(:incr, step) - end - - def decr_incoming_messages_count(step = 1) - update_incoming_messages_count(:decr, step) - end - - def incr_outgoing_messages_count(step = 1) - update_outgoing_messages_count(:incr, step) - end - - def decr_outgoing_messages_count(step = 1) - update_outgoing_messages_count(:decr, step) - end - - def incr_resolutions_count(step = 1) - update_resolutions_count(:incr, step) - end - - def decr_resolutions_count(step = 1) - update_resolutions_count(:decr, step) - end - - def update_avg_first_response_time(response_time) - identity.avg_first_response_time.set(response_time, timestamp) - end - - def update_avg_resolution_time(response_time) - identity.avg_resolution_time.set(response_time, timestamp) - end - - private - - def update_conversations_count(method, step) - identity.conversations_count.send(method, step, timestamp) - end - - def update_incoming_messages_count(method, step) - identity.incoming_messages_count.send(method, step, timestamp) - end - - def update_outgoing_messages_count(method, step) - identity.outgoing_messages_count.send(method, step, timestamp) - end - - def update_resolutions_count(method, step) - identity.resolutions_count.send(method, step, timestamp) - end -end diff --git a/spec/builders/v2/report_builder_spec.rb b/spec/builders/v2/report_builder_spec.rb index 16fe18f4973..c2c9977b356 100644 --- a/spec/builders/v2/report_builder_spec.rb +++ b/spec/builders/v2/report_builder_spec.rb @@ -227,7 +227,7 @@ describe ::V2::ReportBuilder do end it 'returns average first response time' do - label_2.events.update(value: 1.5) + label_2.reporting_events.update(value: 1.5) params = { metric: 'avg_first_response_time', diff --git a/spec/factories/events.rb b/spec/factories/reporting_events.rb similarity index 83% rename from spec/factories/events.rb rename to spec/factories/reporting_events.rb index 936e56ece9f..c779c01f64b 100644 --- a/spec/factories/events.rb +++ b/spec/factories/reporting_events.rb @@ -1,5 +1,5 @@ FactoryBot.define do - factory :event do + factory :reporting_event do name { 'MyString' } value { 1.5 } account_id { 1 } diff --git a/spec/listeners/event_listener_spec.rb b/spec/listeners/reporting_event_listener_spec.rb similarity index 69% rename from spec/listeners/event_listener_spec.rb rename to spec/listeners/reporting_event_listener_spec.rb index a62da768249..39981501477 100644 --- a/spec/listeners/event_listener_spec.rb +++ b/spec/listeners/reporting_event_listener_spec.rb @@ -1,5 +1,5 @@ require 'rails_helper' -describe EventListener do +describe ReportingEventListener do let(:listener) { described_class.instance } let!(:account) { create(:account) } let!(:user) { create(:user, account: account) } @@ -12,19 +12,19 @@ describe EventListener do describe '#conversation_resolved' do it 'creates conversation_resolved event' do - expect(account.events.where(name: 'conversation_resolved').count).to be 0 + expect(account.reporting_events.where(name: 'conversation_resolved').count).to be 0 event = Events::Base.new('conversation.resolved', Time.zone.now, conversation: conversation) listener.conversation_resolved(event) - expect(account.events.where(name: 'conversation_resolved').count).to be 1 + expect(account.reporting_events.where(name: 'conversation_resolved').count).to be 1 end end describe '#first_reply_created' do it 'creates first_response event' do - previous_count = account.events.where(name: 'first_response').count + previous_count = account.reporting_events.where(name: 'first_response').count event = Events::Base.new('first.reply.created', Time.zone.now, message: message) listener.first_reply_created(event) - expect(account.events.where(name: 'first_response').count).to eql previous_count + 1 + expect(account.reporting_events.where(name: 'first_response').count).to eql previous_count + 1 end end end diff --git a/spec/models/account_spec.rb b/spec/models/account_spec.rb index fc3787ab425..bd835865bf6 100644 --- a/spec/models/account_spec.rb +++ b/spec/models/account_spec.rb @@ -18,7 +18,7 @@ RSpec.describe Account do it { is_expected.to have_many(:web_widgets).class_name('::Channel::WebWidget').dependent(:destroy_async) } it { is_expected.to have_many(:webhooks).dependent(:destroy_async) } it { is_expected.to have_many(:notification_settings).dependent(:destroy_async) } - it { is_expected.to have_many(:events) } + it { is_expected.to have_many(:reporting_events) } it { is_expected.to have_many(:kbase_portals).dependent(:destroy_async) } it { is_expected.to have_many(:kbase_categories).dependent(:destroy_async) } it { is_expected.to have_many(:teams).dependent(:destroy_async) } diff --git a/spec/models/inbox_spec.rb b/spec/models/inbox_spec.rb index ba94540d83c..2da7145cd99 100644 --- a/spec/models/inbox_spec.rb +++ b/spec/models/inbox_spec.rb @@ -29,7 +29,7 @@ RSpec.describe Inbox do it { is_expected.to have_many(:webhooks).dependent(:destroy_async) } - it { is_expected.to have_many(:events) } + it { is_expected.to have_many(:reporting_events) } it { is_expected.to have_many(:hooks) } end diff --git a/spec/models/event_spec.rb b/spec/models/reporting_event_spec.rb similarity index 91% rename from spec/models/event_spec.rb rename to spec/models/reporting_event_spec.rb index 359f6c3db72..553a4e36003 100644 --- a/spec/models/event_spec.rb +++ b/spec/models/reporting_event_spec.rb @@ -1,6 +1,6 @@ require 'rails_helper' -RSpec.describe Event, type: :model do +RSpec.describe ReportingEvent, type: :model do describe 'validations' do it { is_expected.to validate_presence_of(:account_id) } it { is_expected.to validate_presence_of(:name) } diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb index e9923cd3859..92015edcc54 100644 --- a/spec/models/user_spec.rb +++ b/spec/models/user_spec.rb @@ -19,7 +19,7 @@ RSpec.describe User do it { is_expected.to have_many(:inbox_members).dependent(:destroy_async) } it { is_expected.to have_many(:notification_settings).dependent(:destroy_async) } it { is_expected.to have_many(:messages) } - it { is_expected.to have_many(:events) } + it { is_expected.to have_many(:reporting_events) } it { is_expected.to have_many(:teams) } end From bb16780278bd8ee19476b78167ebe2f2ad3173b7 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 28 Feb 2022 18:50:29 +0530 Subject: [PATCH 06/43] chore: Upgrade utils to 0.0.5 (#4082) --- package.json | 2 +- yarn.lock | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/package.json b/package.json index 17733fde303..9d08e6b6225 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "dependencies": { "@braid/vue-formulate": "^2.5.2", "@chatwoot/prosemirror-schema": "https://github.com/chatwoot/prosemirror-schema.git#7e8acadd10d7b932c0dc0bd0a18f804434f83517", - "@chatwoot/utils": "^0.0.4", + "@chatwoot/utils": "^0.0.5", "@hcaptcha/vue-hcaptcha": "^0.3.2", "@rails/actioncable": "6.1.3", "@rails/webpacker": "5.3.0", diff --git a/yarn.lock b/yarn.lock index c98eb5098b5..22337bc637c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1111,10 +1111,10 @@ prosemirror-state "^1.3.3" prosemirror-view "^1.17.2" -"@chatwoot/utils@^0.0.4": - version "0.0.4" - resolved "https://registry.yarnpkg.com/@chatwoot/utils/-/utils-0.0.4.tgz#519c0e0ace4d01e821bf24e36111ea7ed82da451" - integrity sha512-giL85pBJWPmUKM+AOpeWeWvsuUJrHdSAJ4RKaQh/YNQhb8AjKxdx476s7UTaKd5DV+OnCpWtmHtNV+38CAC6lw== +"@chatwoot/utils@^0.0.5": + version "0.0.5" + resolved "https://registry.npmjs.org/@chatwoot/utils/-/utils-0.0.5.tgz#907cdae747abc17cf2e5c31a378aba66a2f31c6f" + integrity sha512-gTQMpQuYVF5EaF4+xSmaoJKXPtbwDPjNLi5cwg44FaQSfmzFxD0fjDsDUPrxzZfURLsR8eU7Z1ulVKnZdHN4yg== dependencies: date-fns "^2.22.1" From 1de18391b4545566b03078e4f32c77ba926b6848 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 28 Feb 2022 19:48:13 +0530 Subject: [PATCH 07/43] fix: Handle invalid country code in contact details (#4081) --- .../dashboard/conversation/contact/ContactInfo.vue | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index 4a58d8a54b9..90aeb43ee70 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -210,8 +210,7 @@ export default { if (!cityAndCountry) { return ''; } - const countryFlag = countryCode ? flag(countryCode) : '🌎'; - return `${cityAndCountry} ${countryFlag}`; + return this.findCountryFlag(countryCode, cityAndCountry); }, socialProfiles() { const { @@ -261,6 +260,14 @@ export default { this.showConversationModal = false; this.showEditModal = false; }, + findCountryFlag(countryCode, cityAndCountry) { + try { + const countryFlag = countryCode ? flag(countryCode) : '🌎'; + return `${cityAndCountry} ${countryFlag}`; + } catch (error) { + return ''; + } + }, async deleteContact({ id }) { try { await this.$store.dispatch('contacts/delete', id); From eee89bf0d82822c5ee09b02734b7d029c5d07e89 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Mon, 28 Feb 2022 21:42:50 +0530 Subject: [PATCH 08/43] feat: Show cc from last email on reply editor (#3983) * Adds last emails to reply editor * Fixes bug in reply box * Adds test cases * Prevents private notes having cc bcc data * Prevents private notes having cc bcc data * Init reply head with values * fix broken tests Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Muhsin Keloth --- .../widgets/conversation/ReplyBox.vue | 20 +++++++++++-- .../widgets/conversation/ReplyEmailHead.vue | 4 +++ .../widgets/conversation/bubble/MailHead.vue | 2 +- .../store/modules/conversations/getters.js | 21 +++++++++++++ .../specs/conversations/getters.spec.js | 30 +++++++++++++++++++ 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 5f17b5114c4..7441d9a73b7 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -190,6 +190,7 @@ export default { currentChat: 'getSelectedChat', messageSignature: 'getMessageSignature', currentUser: 'getCurrentUser', + lastEmail: 'getLastEmailInSelectedChat', globalConfig: 'globalConfig/get', accountId: 'getCurrentAccountId', }), @@ -388,6 +389,8 @@ export default { } else { this.replyType = REPLY_EDITOR_MODES.NOTE; } + + this.setCCEmailFromLastChat(); }, message(updatedMessage) { this.hasSlashCommand = @@ -409,6 +412,8 @@ export default { // working even if input/textarea is focussed. document.addEventListener('keydown', this.handleKeyEvents); document.addEventListener('paste', this.onPaste); + + this.setCCEmailFromLastChat(); }, destroyed() { document.removeEventListener('keydown', this.handleKeyEvents); @@ -650,11 +655,11 @@ export default { }); } - if (this.ccEmails) { + if (this.ccEmails && !this.isOnPrivateNote) { messagePayload.ccEmails = this.ccEmails; } - if (this.bccEmails) { + if (this.bccEmails && !this.isOnPrivateNote) { messagePayload.bccEmails = this.bccEmails; } @@ -667,6 +672,17 @@ export default { this.bccEmails = value.bccEmails; this.ccEmails = value.ccEmails; }, + setCCEmailFromLastChat() { + if (this.lastEmail) { + const { + content_attributes: { email: emailAttributes = {} }, + } = this.lastEmail; + const cc = emailAttributes.cc || []; + const bcc = emailAttributes.bcc || []; + this.ccEmails = cc.join(', '); + this.bccEmails = bcc.join(', '); + } + }, }, }; diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue index e82fb2eccee..b2c7dd589ef 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyEmailHead.vue @@ -83,6 +83,10 @@ export default { } }, }, + mounted() { + this.ccEmailsVal = this.ccEmails; + this.bccEmailsVal = this.bccEmails; + }, validations: { ccEmailsVal: { hasValidEmails(value) { diff --git a/app/javascript/dashboard/components/widgets/conversation/bubble/MailHead.vue b/app/javascript/dashboard/components/widgets/conversation/bubble/MailHead.vue index 211ffeaa66c..66365e85c95 100644 --- a/app/javascript/dashboard/components/widgets/conversation/bubble/MailHead.vue +++ b/app/javascript/dashboard/components/widgets/conversation/bubble/MailHead.vue @@ -70,7 +70,7 @@ export default { return this.emailAttributes.subject || ''; }, showHead() { - return this.toMails || this.ccMails || this.bccMails; + return this.toMails || this.ccMails || this.bccMails || this.fromMail; }, }, }; diff --git a/app/javascript/dashboard/store/modules/conversations/getters.js b/app/javascript/dashboard/store/modules/conversations/getters.js index e4b0638991c..60ca0d590be 100644 --- a/app/javascript/dashboard/store/modules/conversations/getters.js +++ b/app/javascript/dashboard/store/modules/conversations/getters.js @@ -1,4 +1,5 @@ import authAPI from '../../../api/auth'; +import { MESSAGE_TYPE } from 'shared/constants/messages'; import { applyPageFilters } from './helpers'; export const getSelectedChatConversation = ({ @@ -19,6 +20,26 @@ const getters = { ); return selectedChat || {}; }, + getLastEmailInSelectedChat: (stage, _getters) => { + const selectedChat = _getters.getSelectedChat; + const { messages = [] } = selectedChat; + const lastEmail = [...messages].reverse().find(message => { + const { + content_attributes: contentAttributes = {}, + message_type: messageType, + } = message; + const { email = {} } = contentAttributes; + const isIncomingOrOutgoing = + messageType === MESSAGE_TYPE.OUTGOING || + messageType === MESSAGE_TYPE.INCOMING; + if (email.from && isIncomingOrOutgoing) { + return true; + } + return false; + }); + + return lastEmail; + }, getMineChats: _state => activeFilters => { const currentUserID = authAPI.getCurrentUser().id; diff --git a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js index bb718bb2427..7dc0dade57d 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/getters.spec.js @@ -131,4 +131,34 @@ describe('#getters', () => { expect(getters.getAppliedConversationFilters(state)).toEqual(filtersList); }); }); + + describe('#getLastEmailInSelectedChat', () => { + it('Returns cc in last email', () => { + const state = {}; + const getSelectedChat = { + messages: [ + { + message_type: 1, + content_attributes: { + email: { + from: 'why@how.my', + cc: ['nithin@me.co', 'we@who.why'], + }, + }, + }, + ], + }; + expect( + getters.getLastEmailInSelectedChat(state, { getSelectedChat }) + ).toEqual({ + message_type: 1, + content_attributes: { + email: { + from: 'why@how.my', + cc: ['nithin@me.co', 'we@who.why'], + }, + }, + }); + }); + }); }); From a3cb26a317694b5d26ff5bc90cd970920422a847 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Mon, 28 Feb 2022 21:43:24 +0530 Subject: [PATCH 09/43] fix: AudioContext warning when loading widget on Chrome (#3956) * fix: AudioContext warning when loading widget on Chrome * minor fixes * Minor fixes * adds event on document * Play audio from parent window through SDK * Adds notification to dashboard Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Vishnu Narayanan --- app/javascript/packs/application.js | 17 +++++++++-- app/javascript/packs/widget.js | 2 -- app/javascript/sdk/IFrameHelper.js | 29 +++++++++++++++++++ .../shared/helpers/AudioNotificationHelper.js | 21 +++++++++----- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/app/javascript/packs/application.js b/app/javascript/packs/application.js index 9b38758563f..79b9e927e4e 100644 --- a/app/javascript/packs/application.js +++ b/app/javascript/packs/application.js @@ -21,7 +21,10 @@ import App from '../dashboard/App'; import i18n from '../dashboard/i18n'; import createAxios from '../dashboard/helper/APIHelper'; import commonHelpers, { isJSONValid } from '../dashboard/helper/commons'; -import { getAlertAudio } from '../shared/helpers/AudioNotificationHelper'; +import { + getAlertAudio, + initOnEvents, +} from '../shared/helpers/AudioNotificationHelper'; import { initFaviconSwitcher } from '../shared/helpers/faviconHelper'; import router from '../dashboard/routes'; import store from '../dashboard/store'; @@ -102,6 +105,13 @@ window.onload = () => { vueActionCable.init(); }; +const setupAudioListeners = () => { + getAlertAudio().then(() => { + initOnEvents.forEach(event => { + document.removeEventListener(event, setupAudioListeners, false); + }); + }); +}; window.addEventListener('load', () => { verifyServiceWorkerExistence(registration => registration.pushManager.getSubscription().then(subscription => { @@ -110,6 +120,9 @@ window.addEventListener('load', () => { } }) ); - getAlertAudio(); + window.playAudioAlert = () => {}; + initOnEvents.forEach(e => { + document.addEventListener(e, setupAudioListeners, false); + }); initFaviconSwitcher(); }); diff --git a/app/javascript/packs/widget.js b/app/javascript/packs/widget.js index f9f57e2efaf..421e2f8b924 100644 --- a/app/javascript/packs/widget.js +++ b/app/javascript/packs/widget.js @@ -4,7 +4,6 @@ import VueI18n from 'vue-i18n'; import store from '../widget/store'; import App from '../widget/App.vue'; import ActionCableConnector from '../widget/helpers/actionCable'; -import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper'; import i18n from '../widget/i18n'; import router from '../widget/router'; @@ -33,5 +32,4 @@ window.onload = () => { window.WOOT_WIDGET, window.chatwootPubsubToken ); - getAlertAudio(); }; diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index 94d91f77f6d..b5cc35d7c59 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -26,6 +26,10 @@ import { dispatchWindowEvent } from 'shared/helpers/CustomEventHelper'; import { CHATWOOT_ERROR, CHATWOOT_READY } from '../widget/constants/sdkEvents'; import { SET_USER_ERROR } from '../widget/constants/errorTypes'; import { getUserCookieName } from './cookieHelpers'; +import { + getAlertAudio, + initOnEvents, +} from 'shared/helpers/AudioNotificationHelper'; import { isFlatWidgetStyle } from './settingsHelper'; export const IFrameHelper = { @@ -114,6 +118,18 @@ export const IFrameHelper = { iframe.setAttribute('style', `height: ${updatedIframeHeight} !important`); }, + setupAudioListeners: () => { + getAlertAudio().then(() => + initOnEvents.forEach(event => { + document.removeEventListener( + event, + IFrameHelper.setupAudioListeners, + false + ); + }) + ); + }, + events: { loaded: message => { Cookies.set('cw_conversation', message.config.authToken, { @@ -136,6 +152,15 @@ export const IFrameHelper = { if (window.$chatwoot.user) { IFrameHelper.sendMessage('set-user', window.$chatwoot.user); } + + dispatchWindowEvent({ eventName: CHATWOOT_READY }); + + window.playAudioAlert = () => {}; + + initOnEvents.forEach(e => { + document.addEventListener(e, IFrameHelper.setupAudioListeners, false); + }); + if (!window.$chatwoot.resetTriggered) { dispatchWindowEvent({ eventName: CHATWOOT_READY }); } @@ -214,6 +239,10 @@ export const IFrameHelper = { closeChat: () => { onBubbleClick({ toggleValue: false }); }, + + playAudio: () => { + window.playAudioAlert(); + }, }, pushEvent: eventName => { IFrameHelper.sendMessage('push-event', { eventName }); diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js index 9896d6b8e7c..3713d153164 100644 --- a/app/javascript/shared/helpers/AudioNotificationHelper.js +++ b/app/javascript/shared/helpers/AudioNotificationHelper.js @@ -1,9 +1,10 @@ import { MESSAGE_TYPE } from 'shared/constants/messages'; -import axios from 'axios'; +import { IFrameHelper } from 'widget/helpers/utils'; + import { showBadgeOnFavicon } from './faviconHelper'; +export const initOnEvents = ['click', 'touchstart', 'keypress']; export const getAlertAudio = async () => { - window.playAudioAlert = () => {}; const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const playsound = audioBuffer => { window.playAudioAlert = () => { @@ -16,11 +17,14 @@ export const getAlertAudio = async () => { }; try { - const response = await axios.get('/dashboard/audios/ding.mp3', { - responseType: 'arraybuffer', - }); + const audioRequest = new Request('/dashboard/audios/ding.mp3'); - audioCtx.decodeAudioData(response.data).then(playsound); + fetch(audioRequest) + .then(response => response.arrayBuffer()) + .then(buffer => { + audioCtx.decodeAudioData(buffer).then(playsound); + return new Promise(res => res()); + }); } catch (error) { // error } @@ -89,6 +93,7 @@ export const newMessageNotification = data => { currentUserId, assigneeId ); + if (playAudio && isNotificationEnabled) { window.playAudioAlert(); showBadgeOnFavicon(); @@ -96,5 +101,7 @@ export const newMessageNotification = data => { }; export const playNewMessageNotificationInWidget = () => { - window.playAudioAlert(); + IFrameHelper.sendMessage({ + event: 'playAudio', + }); }; From f08d1b35d0d90788b9dea2e470d03c33da59d5ba Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Tue, 1 Mar 2022 14:14:23 +0530 Subject: [PATCH 10/43] feat: Toggle automation status (#3991) --- .../accounts/automation_rules_controller.rb | 4 +- app/javascript/dashboard/components/index.js | 2 + .../sidebarComponents/SecondaryNavItem.vue | 5 +- .../widgets/modal/ConfirmationModal.vue | 75 +++++++++++++++ .../dashboard/i18n/locale/en/automation.json | 12 +++ .../dashboard/i18n/locale/en/settings.json | 3 +- .../dashboard/settings/automation/Index.vue | 93 +++++++++++++++++-- app/listeners/automation_rule_listener.rb | 3 +- 8 files changed, 185 insertions(+), 12 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue diff --git a/app/controllers/api/v1/accounts/automation_rules_controller.rb b/app/controllers/api/v1/accounts/automation_rules_controller.rb index 12b7b59574b..2dc71bcecc7 100644 --- a/app/controllers/api/v1/accounts/automation_rules_controller.rb +++ b/app/controllers/api/v1/accounts/automation_rules_controller.rb @@ -3,7 +3,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont before_action :fetch_automation_rule, only: [:show, :update, :destroy, :clone] def index - @automation_rules = Current.account.automation_rules.active + @automation_rules = Current.account.automation_rules end def create @@ -32,7 +32,7 @@ class Api::V1::Accounts::AutomationRulesController < Api::V1::Accounts::BaseCont def automation_rules_permit params.permit( - :name, :description, :event_name, :account_id, + :name, :description, :event_name, :account_id, :active, conditions: [:attribute_key, :filter_operator, :query_operator, { values: [] }], actions: [:action_name, { action_params: [] }] ) diff --git a/app/javascript/dashboard/components/index.js b/app/javascript/dashboard/components/index.js index f737bd3131c..d2614421c55 100644 --- a/app/javascript/dashboard/components/index.js +++ b/app/javascript/dashboard/components/index.js @@ -21,6 +21,7 @@ import SubmitButton from './buttons/FormSubmitButton'; import Tabs from './ui/Tabs/Tabs'; import TabsItem from './ui/Tabs/TabsItem'; import Thumbnail from './widgets/Thumbnail.vue'; +import ConfirmModal from './widgets/modal/ConfirmationModal.vue'; const WootUIKit = { AvatarUploader, @@ -45,6 +46,7 @@ const WootUIKit = { Tabs, TabsItem, Thumbnail, + ConfirmModal, install(Vue) { const keys = Object.keys(this); keys.pop(); // remove 'install' from keys diff --git a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue index 3a344d98508..ffd8018d56b 100644 --- a/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue +++ b/app/javascript/dashboard/components/layout/sidebarComponents/SecondaryNavItem.vue @@ -20,7 +20,8 @@ data-view-component="true" label="Beta" class="beta" - >Beta + > + {{ $t('SIDEBAR.BETA') }} @@ -233,7 +234,7 @@ export default { padding-left: var(--space-smaller) !important; margin-left: var(--space-half) !important; display: inline-block; - font-size: var(--font-size-mini); + font-size: var(--font-size-micro); font-weight: var(--font-weight-medium); line-height: 18px; border: 1px solid transparent; diff --git a/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue b/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue new file mode 100644 index 00000000000..0c1472a8460 --- /dev/null +++ b/app/javascript/dashboard/components/widgets/modal/ConfirmationModal.vue @@ -0,0 +1,75 @@ + + diff --git a/app/javascript/dashboard/i18n/locale/en/automation.json b/app/javascript/dashboard/i18n/locale/en/automation.json index ce95b861c09..8c92467bd96 100644 --- a/app/javascript/dashboard/i18n/locale/en/automation.json +++ b/app/javascript/dashboard/i18n/locale/en/automation.json @@ -90,6 +90,18 @@ }, "ACTION": { "DELETE_MESSAGE": "You need to have atleast one action to save" + }, + "TOGGLE": { + "ACTIVATION_TITLE": "Activate Automation Rule", + "DEACTIVATION_TITLE": "Deactivate Automation Rule", + "ACTIVATION_DESCRIPTION": "This action will activate the automation rule '{automationName}'. Are you sure you want to proceed?", + "DEACTIVATION_DESCRIPTION": "This action will deactivate the automation rule '{automationName}'. Are you sure you want to proceed?", + "ACTIVATION_SUCCESFUL": "Automation Rule Activated Successfully", + "DEACTIVATION_SUCCESFUL": "Automation Rule Deactivated Successfully", + "ACTIVATION_ERROR": "Could not Activate Automation, Please try again later", + "DEACTIVATION_ERROR": "Could not Deactivate Automation, Please try again later", + "CONFIRMATION_LABEL": "Yes", + "CANCEL_LABEL": "No" } } } diff --git a/app/javascript/dashboard/i18n/locale/en/settings.json b/app/javascript/dashboard/i18n/locale/en/settings.json index d353e6b6edb..5727e09d06c 100644 --- a/app/javascript/dashboard/i18n/locale/en/settings.json +++ b/app/javascript/dashboard/i18n/locale/en/settings.json @@ -178,7 +178,8 @@ "REPORTS_LABEL": "Labels", "REPORTS_INBOX": "Inbox", "REPORTS_TEAM": "Team", - "SET_AVAILABILITY_TITLE": "Set yourself as" + "SET_AVAILABILITY_TITLE": "Set yourself as", + "BETA": "Beta" }, "CREATE_ACCOUNT": { "NO_ACCOUNT_WARNING": "Uh oh! We could not find any Chatwoot accounts. Please create a new account to continue.", diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index be79dd4f596..811fc95c6cc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,12 +34,19 @@ {{ automation.name }} {{ automation.description }} - - + {{ readableTime(automation.created_on) }} @@ -120,6 +127,11 @@ @saveAutomation="submitAutomation" /> + diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue index 1f283904809..9d90bb9e22f 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/Index.vue @@ -10,6 +10,7 @@
+
+ +
diff --git a/app/javascript/dashboard/store/modules/csat.js b/app/javascript/dashboard/store/modules/csat.js index d38b212be6b..7bc1dad6d37 100644 --- a/app/javascript/dashboard/store/modules/csat.js +++ b/app/javascript/dashboard/store/modules/csat.js @@ -82,10 +82,13 @@ export const getters = { }; export const actions = { - get: async function getResponses({ commit }, { page = 1, from, to } = {}) { + get: async function getResponses( + { commit }, + { page = 1, from, to, user_ids } = {} + ) { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: true }); try { - const response = await CSATReports.get({ page, from, to }); + const response = await CSATReports.get({ page, from, to, user_ids }); commit(types.SET_CSAT_RESPONSE, response.data); } catch (error) { // Ignore error @@ -93,10 +96,10 @@ export const actions = { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetching: false }); } }, - getMetrics: async function getMetrics({ commit }, { from, to }) { + getMetrics: async function getMetrics({ commit }, { from, to, user_ids }) { commit(types.SET_CSAT_RESPONSE_UI_FLAG, { isFetchingMetrics: true }); try { - const response = await CSATReports.getMetrics({ from, to }); + const response = await CSATReports.getMetrics({ from, to, user_ids }); commit(types.SET_CSAT_RESPONSE_METRICS, response.data); } catch (error) { // Ignore error diff --git a/app/models/csat_survey_response.rb b/app/models/csat_survey_response.rb index 3f6a606de44..119d5c615e9 100644 --- a/app/models/csat_survey_response.rb +++ b/app/models/csat_survey_response.rb @@ -40,4 +40,7 @@ class CsatSurveyResponse < ApplicationRecord validates :account_id, presence: true validates :contact_id, presence: true validates :conversation_id, presence: true + + scope :filter_by_created_at, ->(range) { where(created_at: range) if range.present? } + scope :filter_by_assigned_agent_id, ->(user_ids) { where(assigned_agent_id: user_ids) if user_ids.present? } end diff --git a/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb b/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb index 526879099ff..abb4231d9e8 100644 --- a/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/csat_survey_responses_controller_spec.rb @@ -48,6 +48,25 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do expect(response_data.pluck('id')).not_to include(csat_10_days_ago.id) end + it 'filters csat responses based on a date range and agent ids' do + csat1_assigned_agent = create(:user, account: account, role: :agent) + csat2_assigned_agent = create(:user, account: account, role: :agent) + + create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent) + create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent) + create(:csat_survey_response, account: account, created_at: 5.days.ago) + + get "/api/v1/accounts/#{account.id}/csat_survey_responses", + params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s, + user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] }, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body) + expect(response_data.size).to eq 2 + end + it 'returns csat responses even if the agent is deleted from account' do deleted_agent_csat = create(:csat_survey_response, account: account, assigned_agent: agent) deleted_agent_csat.assigned_agent.account_users.destroy_all @@ -106,6 +125,27 @@ RSpec.describe 'CSAT Survey Responses API', type: :request do expect(response_data['total_sent_messages_count']).to eq 0 expect(response_data['ratings_count']).to eq({ '1' => 1 }) end + + it 'filters csat metrics based on a date range and agent ids' do + csat1_assigned_agent = create(:user, account: account, role: :agent) + csat2_assigned_agent = create(:user, account: account, role: :agent) + + create(:csat_survey_response, account: account, created_at: 10.days.ago, assigned_agent: csat1_assigned_agent) + create(:csat_survey_response, account: account, created_at: 3.days.ago, assigned_agent: csat2_assigned_agent) + create(:csat_survey_response, account: account, created_at: 5.days.ago) + + get "/api/v1/accounts/#{account.id}/csat_survey_responses/metrics", + params: { since: 11.days.ago.to_time.to_i.to_s, until: Time.zone.today.to_time.to_i.to_s, + user_ids: [csat1_assigned_agent.id, csat2_assigned_agent.id] }, + headers: administrator.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + response_data = JSON.parse(response.body) + expect(response_data['total_count']).to eq 2 + expect(response_data['total_sent_messages_count']).to eq 0 + expect(response_data['ratings_count']).to eq({ '1' => 2 }) + end end end end From 96b719017b7a32a81729a8f09dd79a79171bf175 Mon Sep 17 00:00:00 2001 From: giquieu Date: Fri, 4 Mar 2022 11:13:07 -0300 Subject: [PATCH 23/43] feat: Ability to send voice message to channel (#4064) Created the possibility to send audio as an attachment via the dashboard. The channels that can send audio are the same channels that can send any type of attachment. Used RecordRTC (https://github.com/muaz-khan/RecordRTC) to capture the audio and Wavesurfer (https://github.com/katspaugh/wavesurfer.js) to display the audio waves. RecordRTC can be used to record videos if necessary. Fixes #1973 --- .../components/widgets/AttachmentsPreview.vue | 5 + .../widgets/WootWriter/AudioRecorder.vue | 223 ++++++++++++++++++ .../widgets/WootWriter/ReplyBottomPanel.vue | 65 ++++- .../widgets/conversation/ReplyBox.vue | 67 +++++- .../i18n/locale/en/conversation.json | 6 + .../FluentIcon/dashboard-icons.json | 5 + app/javascript/shared/helpers/FileHelper.js | 2 +- package.json | 4 +- 8 files changed, 372 insertions(+), 5 deletions(-) create mode 100644 app/javascript/dashboard/components/widgets/WootWriter/AudioRecorder.vue diff --git a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue index 250c5c3c628..3d584621563 100644 --- a/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue +++ b/app/javascript/dashboard/components/widgets/AttachmentsPreview.vue @@ -25,6 +25,7 @@
+
+
+
+ + + + + diff --git a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue index 6df1b285850..6dcb4753256 100644 --- a/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue +++ b/app/javascript/dashboard/components/widgets/WootWriter/ReplyBottomPanel.vue @@ -11,7 +11,6 @@ size="small" @click="toggleEmojiPicker" /> - + + + {{ recordingAudioDurationText }} + ({}), @@ -134,6 +158,10 @@ export default { type: Boolean, default: false, }, + showAudioRecorder: { + type: Boolean, + default: false, + }, onFileUpload: { type: Function, default: () => {}, @@ -146,6 +174,22 @@ export default { type: Function, default: () => {}, }, + toggleAudioRecorder: { + type: Function, + default: () => {}, + }, + toggleAudioRecorderPlayPause: { + type: Function, + default: () => {}, + }, + isRecordingAudio: { + type: Boolean, + default: false, + }, + recordingAudioState: { + type: String, + default: '', + }, isSendDisabled: { type: Boolean, default: false, @@ -192,9 +236,28 @@ export default { showAttachButton() { return this.showFileUpload || this.isNote; }, + showAudioRecorderButton() { + return this.showAudioRecorder; + }, + showAudioPlayStopButton() { + return this.showAudioRecorder && this.isRecordingAudio; + }, allowedFileTypes() { return ALLOWED_FILE_TYPES; }, + audioRecorderPlayStopIcon() { + switch (this.recordingAudioState) { + // playing paused recording stopped inactive destroyed + case 'playing': + return 'microphone-pause'; + case 'paused': + return 'microphone-play'; + case 'stopped': + return 'microphone-play'; + default: + return 'microphone-stop'; + } + }, showMessageSignatureButton() { return !this.isPrivate && this.isAnEmailChannel; }, diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 37fbb4367fb..5a9253fba3d 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -33,8 +33,15 @@ :cc-emails.sync="ccEmails" :bcc-emails.sync="bccEmails" /> + this.$refs.messageInput.focus()); @@ -535,10 +568,26 @@ export default { this.attachedFiles = []; this.ccEmails = ''; this.bccEmails = ''; + this.isRecordingAudio = false; }, toggleEmojiPicker() { this.showEmojiPicker = !this.showEmojiPicker; }, + toggleAudioRecorder() { + this.isRecordingAudio = !this.isRecordingAudio; + this.isRecorderAudioStopped = !this.isRecordingAudio; + if (!this.isRecordingAudio) { + this.clearMessage(); + } + }, + toggleAudioRecorderPlayPause() { + if (this.isRecordingAudio && !this.isRecorderAudioStopped) { + this.isRecorderAudioStopped = true; + this.$refs.audioRecorderInput.stopAudioRecording(); + } else if (this.isRecordingAudio && this.isRecorderAudioStopped) { + this.$refs.audioRecorderInput.playPause(); + } + }, hideEmojiPicker() { if (this.showEmojiPicker) { this.toggleEmojiPicker(); @@ -559,6 +608,20 @@ export default { onFocus() { this.isFocused = true; }, + onStateRecorderTimerChanged(time) { + this.recordingAudioDuration = time; + }, + onStateRecorderChanged(state) { + this.recordingAudioState = state; + if (state.includes('notallowederror')) { + this.toggleAudioRecorder(); + } + }, + onRecorderBlob(file) { + if (file) { + this.onFileUpload(file); + } + }, toggleTyping(status) { const conversationId = this.currentChat.id; const isPrivate = this.isPrivate; diff --git a/app/javascript/dashboard/i18n/locale/en/conversation.json b/app/javascript/dashboard/i18n/locale/en/conversation.json index 96ba4b4295e..ac25a5aed93 100644 --- a/app/javascript/dashboard/i18n/locale/en/conversation.json +++ b/app/javascript/dashboard/i18n/locale/en/conversation.json @@ -74,8 +74,14 @@ "TIP_FORMAT_ICON": "Show rich text editor", "TIP_EMOJI_ICON": "Show emoji selector", "TIP_ATTACH_ICON": "Attach files", + "TIP_AUDIORECORDER_ICON": "Record audio", + "TIP_AUDIORECORDER_PERMISSION": "Allow access to audio", + "TIP_AUDIORECORDER_ERROR": "Could not open the audio", "ENTER_TO_SEND": "Enter to send", "DRAG_DROP": "Drag and drop here to attach", + "START_AUDIO_RECORDING": "Start audio recording", + "STOP_AUDIO_RECORDING": "Stop audio recording", + "": "", "EMAIL_HEAD": { "ADD_BCC": "Add bcc", "CC": { diff --git a/app/javascript/shared/components/FluentIcon/dashboard-icons.json b/app/javascript/shared/components/FluentIcon/dashboard-icons.json index 39064412c75..0cbc85d2225 100644 --- a/app/javascript/shared/components/FluentIcon/dashboard-icons.json +++ b/app/javascript/shared/components/FluentIcon/dashboard-icons.json @@ -85,6 +85,11 @@ "merge-outline": "M3 6.75A.75.75 0 0 1 3.75 6h4.5a.75.75 0 0 1 .53.22L13.56 11h5.878L15.72 7.28a.75.75 0 1 1 1.06-1.06l4.998 5a.75.75 0 0 1 0 1.06l-4.998 5a.75.75 0 1 1-1.06-1.06l3.718-3.72H13.56l-4.78 4.78a.75.75 0 0 1-.531.22h-4.5a.75.75 0 0 1 0-1.5h4.19l4.25-4.25L7.94 7.5H3.75A.75.75 0 0 1 3 6.75Z", "more-horizontal-outline": "M7.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM13.75 12a1.75 1.75 0 1 1-3.5 0 1.75 1.75 0 0 1 3.5 0ZM18 13.75a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5Z", "more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z", + "microphone-outline": "M12,2A3,3 0 0,1 15,5V11A3,3 0 0,1 12,14A3,3 0 0,1 9,11V5A3,3 0 0,1 12,2M19,11C19,14.53 16.39,17.44 13,17.93V21H11V17.93C7.61,17.44 5,14.53 5,11H7A5,5 0 0,0 12,16A5,5 0 0,0 17,11H19Z", + "microphone-off-outline": "M19,11C19,12.19 18.66,13.3 18.1,14.28L16.87,13.05C17.14,12.43 17.3,11.74 17.3,11H19M15,11.16L9,5.18V5A3,3 0 0,1 12,2A3,3 0 0,1 15,5V11L15,11.16M4.27,3L21,19.73L19.73,21L15.54,16.81C14.77,17.27 13.91,17.58 13,17.72V21H11V17.72C7.72,17.23 5,14.41 5,11H6.7C6.7,14 9.24,16.1 12,16.1C12.81,16.1 13.6,15.91 14.31,15.58L12.65,13.92L12,14A3,3 0 0,1 9,11V10.28L3,4.27L4.27,3Z", + "microphone-stop-outline": "M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4M9,9V15H15V9", + "microphone-pause-outline": "M13,16V8H15V16H13M9,16V8H11V16H9M12,2A10,10 0 0,1 22,12A10,10 0 0,1 12,22A10,10 0 0,1 2,12A10,10 0 0,1 12,2M12,4A8,8 0 0,0 4,12A8,8 0 0,0 12,20A8,8 0 0,0 20,12A8,8 0 0,0 12,4Z", + "microphone-play-outline": "M12,20C7.59,20 4,16.41 4,12C4,7.59 7.59,4 12,4C16.41,4 20,7.59 20,12C20,16.41 16.41,20 12,20M12,2A10,10 0 0,0 2,12A10,10 0 0,0 12,22A10,10 0 0,0 22,12A10,10 0 0,0 12,2M10,16.5L16,12L10,7.5V16.5Z", "number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z", "open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z", "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", diff --git a/app/javascript/shared/helpers/FileHelper.js b/app/javascript/shared/helpers/FileHelper.js index a784b2ba20d..d9ca9f94381 100644 --- a/app/javascript/shared/helpers/FileHelper.js +++ b/app/javascript/shared/helpers/FileHelper.js @@ -19,7 +19,7 @@ export const fileSizeInMegaBytes = bytes => { }; export const checkFileSizeLimit = (file, maximumUploadLimit) => { - const fileSize = file?.file?.size; + const fileSize = file?.file?.size || file?.size; const fileSizeInMB = fileSizeInMegaBytes(fileSize); return fileSizeInMB <= maximumUploadLimit; }; diff --git a/package.json b/package.json index 1703f63e088..4f794fa6d8a 100644 --- a/package.json +++ b/package.json @@ -50,6 +50,7 @@ "prosemirror-state": "1.3.4", "prosemirror-view": "1.18.4", "query-string": "5", + "recordrtc": "^5.6.2", "semver": "7.3.5", "spinkit": "~1.2.5", "tailwindcss": "^1.9.6", @@ -71,7 +72,8 @@ "vuedraggable": "^2.24.3", "vuelidate": "0.7.6", "vuex": "~2.1.1", - "vuex-router-sync": "~4.1.2" + "vuex-router-sync": "~4.1.2", + "wavesurfer.js": "^5.2.0" }, "devDependencies": { "@babel/core": "7.13.16", From 1aa7d6f7d8bc134f56f5fb462f8d5c778ffce54a Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Tue, 8 Mar 2022 09:22:57 +0530 Subject: [PATCH 24/43] fix: Update brand name in mailers (#4123) --- .../devise/mailer/confirmation_instructions.html.erb | 4 ++-- yarn.lock | 10 ++++++++++ 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/app/views/devise/mailer/confirmation_instructions.html.erb b/app/views/devise/mailer/confirmation_instructions.html.erb index f5297932a52..eb92e5a172c 100644 --- a/app/views/devise/mailer/confirmation_instructions.html.erb +++ b/app/views/devise/mailer/confirmation_instructions.html.erb @@ -2,7 +2,7 @@ <% account_user = @resource&.account_users&.first %> <% if account_user&.inviter.present? && @resource.unconfirmed_email.blank? %> -

<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out Chatwoot!

+

<%= account_user.inviter.name %>, with <%= account_user.account.name %>, has invited you to try out <%= global_config['BRAND_NAME'] || 'Chatwoot' %>!

<% end %>

You can confirm your account email through the link below:

@@ -11,4 +11,4 @@

<%= link_to 'Confirm my account', frontend_url('auth/password/edit', reset_password_token: @resource.send(:set_reset_password_token)) %>

<% else %>

<%= link_to 'Confirm my account', frontend_url('auth/confirmation', confirmation_token: @token) %>

-<% end %> \ No newline at end of file +<% end %> diff --git a/yarn.lock b/yarn.lock index 1ccd05b907f..931d979a676 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12747,6 +12747,11 @@ recast@^0.18.1: private "^0.1.8" source-map "~0.6.1" +recordrtc@^5.6.2: + version "5.6.2" + resolved "https://registry.yarnpkg.com/recordrtc/-/recordrtc-5.6.2.tgz#48fc214b35084973ccce82c6251198b5742bc327" + integrity sha512-1QNKKNtl7+KcwD1lyOgP3ZlbiJ1d0HtXnypUy7yq49xEERxk31PHvE9RCciDrulPCY7WJ+oz0R9hpNxgsIurGQ== + recursive-readdir@2.2.2: version "2.2.2" resolved "https://registry.yarnpkg.com/recursive-readdir/-/recursive-readdir-2.2.2.tgz#9946fb3274e1628de6e36b2f6714953b4845094f" @@ -15286,6 +15291,11 @@ watchpack@^1.7.4: chokidar "^3.4.1" watchpack-chokidar2 "^2.0.1" +wavesurfer.js@^5.2.0: + version "5.2.0" + resolved "https://registry.yarnpkg.com/wavesurfer.js/-/wavesurfer.js-5.2.0.tgz#efae107b5b561e9bfe3fffc50e6158136a17643e" + integrity sha512-SkPlTXfvKy+ZnEA7f7g7jn6iQg5/8mAvWpVV5vRbIS/FF9TB2ak9J7VayQfzfshOLW/CqccTiN6DDR/fZA902g== + wbuf@^1.1.0, wbuf@^1.7.3: version "1.7.3" resolved "https://registry.yarnpkg.com/wbuf/-/wbuf-1.7.3.tgz#c1d8d149316d3ea852848895cb6a0bfe887b87df" From fb03b6f43450bb03e38800a7e1d5a332ebd5e4ba Mon Sep 17 00:00:00 2001 From: Pranav Raj S Date: Thu, 10 Mar 2022 11:01:40 +0530 Subject: [PATCH 25/43] chore: Upgrade rails to 6.1.4.7 to fix (#4143) Fixes CVE-2022-21831 --- Gemfile.lock | 110 +++++++++++++++++++++++++-------------------------- 1 file changed, 55 insertions(+), 55 deletions(-) diff --git a/Gemfile.lock b/Gemfile.lock index a16b50a984b..2222403f1b7 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -9,63 +9,63 @@ GIT GEM remote: https://rubygems.org/ specs: - actioncable (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + actioncable (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) nio4r (~> 2.0) websocket-driver (>= 0.6.1) - actionmailbox (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailbox (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (>= 2.7.1) - actionmailer (6.1.4.6) - actionpack (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionmailer (6.1.4.7) + actionpack (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activesupport (= 6.1.4.7) mail (~> 2.5, >= 2.5.4) rails-dom-testing (~> 2.0) - actionpack (6.1.4.6) - actionview (= 6.1.4.6) - activesupport (= 6.1.4.6) + actionpack (6.1.4.7) + actionview (= 6.1.4.7) + activesupport (= 6.1.4.7) rack (~> 2.0, >= 2.0.9) rack-test (>= 0.6.3) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.0, >= 1.2.0) - actiontext (6.1.4.6) - actionpack (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + actiontext (6.1.4.7) + actionpack (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) nokogiri (>= 1.8.5) - actionview (6.1.4.6) - activesupport (= 6.1.4.6) + actionview (6.1.4.7) + activesupport (= 6.1.4.7) builder (~> 3.1) erubi (~> 1.4) rails-dom-testing (~> 2.0) rails-html-sanitizer (~> 1.1, >= 1.2.0) active_record_query_trace (1.8) - activejob (6.1.4.6) - activesupport (= 6.1.4.6) + activejob (6.1.4.7) + activesupport (= 6.1.4.7) globalid (>= 0.3.6) - activemodel (6.1.4.6) - activesupport (= 6.1.4.6) - activerecord (6.1.4.6) - activemodel (= 6.1.4.6) - activesupport (= 6.1.4.6) + activemodel (6.1.4.7) + activesupport (= 6.1.4.7) + activerecord (6.1.4.7) + activemodel (= 6.1.4.7) + activesupport (= 6.1.4.7) activerecord-import (1.3.0) activerecord (>= 4.2) - activestorage (6.1.4.6) - actionpack (= 6.1.4.6) - activejob (= 6.1.4.6) - activerecord (= 6.1.4.6) - activesupport (= 6.1.4.6) + activestorage (6.1.4.7) + actionpack (= 6.1.4.7) + activejob (= 6.1.4.7) + activerecord (= 6.1.4.7) + activesupport (= 6.1.4.7) marcel (~> 1.0.0) mini_mime (>= 1.1.0) - activesupport (6.1.4.6) + activesupport (6.1.4.7) concurrent-ruby (~> 1.0, >= 1.0.2) i18n (>= 1.6, < 2) minitest (>= 5.1) @@ -419,29 +419,29 @@ GEM rack-test (1.1.0) rack (>= 1.0, < 3) rack-timeout (0.6.0) - rails (6.1.4.6) - actioncable (= 6.1.4.6) - actionmailbox (= 6.1.4.6) - actionmailer (= 6.1.4.6) - actionpack (= 6.1.4.6) - actiontext (= 6.1.4.6) - actionview (= 6.1.4.6) - activejob (= 6.1.4.6) - activemodel (= 6.1.4.6) - activerecord (= 6.1.4.6) - activestorage (= 6.1.4.6) - activesupport (= 6.1.4.6) + rails (6.1.4.7) + actioncable (= 6.1.4.7) + actionmailbox (= 6.1.4.7) + actionmailer (= 6.1.4.7) + actionpack (= 6.1.4.7) + actiontext (= 6.1.4.7) + actionview (= 6.1.4.7) + activejob (= 6.1.4.7) + activemodel (= 6.1.4.7) + activerecord (= 6.1.4.7) + activestorage (= 6.1.4.7) + activesupport (= 6.1.4.7) bundler (>= 1.15.0) - railties (= 6.1.4.6) + railties (= 6.1.4.7) sprockets-rails (>= 2.0.0) rails-dom-testing (2.0.3) activesupport (>= 4.2.0) nokogiri (>= 1.6) rails-html-sanitizer (1.4.2) loofah (~> 2.3) - railties (6.1.4.6) - actionpack (= 6.1.4.6) - activesupport (= 6.1.4.6) + railties (6.1.4.7) + actionpack (= 6.1.4.7) + activesupport (= 6.1.4.7) method_source rake (>= 0.13) thor (~> 1.0) @@ -574,7 +574,7 @@ GEM spring-watcher-listen (2.0.1) listen (>= 2.7, < 4.0) spring (>= 1.2, < 3.0) - sprockets (4.0.2) + sprockets (4.0.3) concurrent-ruby (~> 1.0) rack (> 1, < 3) sprockets-rails (3.4.2) @@ -751,4 +751,4 @@ RUBY VERSION ruby 3.0.2p107 BUNDLED WITH - 2.2.25 + 2.3.8 From b3545f42f12ac2a158c093cc1c9951188fda54ad Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 10 Mar 2022 11:15:41 +0530 Subject: [PATCH 26/43] fixes: Contact search is skipping the '+' sign for email. (#4142) Co-authored-by: Pranav Raj S --- .../routes/dashboard/contacts/components/ContactsView.vue | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue index 7536c0d2401..dbac2f0bc1c 100644 --- a/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue +++ b/app/javascript/dashboard/routes/dashboard/contacts/components/ContactsView.vue @@ -253,7 +253,7 @@ export default { this.$store.dispatch('contacts/get', requestParams); } else { this.$store.dispatch('contacts/search', { - search: value, + search: encodeURIComponent(value), ...requestParams, }); } From 647efa12e707792ebb466cec32a113701e0a8345 Mon Sep 17 00:00:00 2001 From: Tejaswini Chile Date: Thu, 10 Mar 2022 20:27:30 +0530 Subject: [PATCH 27/43] fix: Attach instagram images with file type story_mentions (#4100) --- .../messages/messenger/message_builder.rb | 33 ++++++++- app/helpers/file_type_helper.rb | 6 +- .../widgets/conversation/Message.vue | 13 ++++ .../widgets/conversation/MessagesView.vue | 6 ++ .../widgets/conversation/bubble/Actions.vue | 32 +++++++++ .../dashboard/i18n/locale/en/chatlist.json | 1 + app/models/attachment.rb | 2 +- app/models/message.rb | 2 +- config/locales/en.yml | 3 +- .../instagram_message_create_event.rb | 68 ++++++++++++++++++ .../webhooks/instagram_events_job_spec.rb | 71 +++++++++++++++---- 11 files changed, 219 insertions(+), 18 deletions(-) diff --git a/app/builders/messages/messenger/message_builder.rb b/app/builders/messages/messenger/message_builder.rb index 08aa58be059..2e58825f4cd 100644 --- a/app/builders/messages/messenger/message_builder.rb +++ b/app/builders/messages/messenger/message_builder.rb @@ -1,10 +1,14 @@ class Messages::Messenger::MessageBuilder + include ::FileTypeHelper + def process_attachment(attachment) return if attachment['type'].to_sym == :template attachment_obj = @message.attachments.new(attachment_params(attachment).except(:remote_file_url)) attachment_obj.save! attach_file(attachment_obj, attachment_params(attachment)[:remote_file_url]) if attachment_params(attachment)[:remote_file_url] + fetch_story_link(attachment_obj) if attachment_obj.file_type == 'story_mention' + update_attachment_file_type(attachment_obj) end def attach_file(attachment, file_url) @@ -22,7 +26,7 @@ class Messages::Messenger::MessageBuilder file_type = attachment['type'].to_sym params = { file_type: file_type, account_id: @message.account_id } - if [:image, :file, :audio, :video].include? file_type + if [:image, :file, :audio, :video, :share, :story_mention].include? file_type params.merge!(file_type_params(attachment)) elsif file_type == :location params.merge!(location_params(attachment)) @@ -39,4 +43,31 @@ class Messages::Messenger::MessageBuilder remote_file_url: attachment['payload']['url'] } end + + def update_attachment_file_type(attachment) + return unless attachment.file_type == 'share' || attachment.file_type == 'story_mention' + + attachment.file_type = file_type(attachment.file&.content_type) + attachment.save! + end + + def fetch_story_link(attachment) + message = attachment.message + begin + k = Koala::Facebook::API.new(@inbox.channel.page_access_token) if @inbox.facebook? + result = k.get_object(message.source_id, fields: %w[story from]) || {} + rescue Koala::Facebook::AuthenticationError + @inbox.channel.authorization_error! + raise + rescue StandardError => e + result = {} + Sentry.capture_exception(e) + end + story_id = result['story']['mention']['id'] + story_sender = result['from']['username'] + message.content_attributes[:story_sender] = story_sender + message.content_attributes[:story_id] = story_id + message.content = I18n.t('conversations.messages.instagram_story_content', story_sender: story_sender) + message.save! + end end diff --git a/app/helpers/file_type_helper.rb b/app/helpers/file_type_helper.rb index 9e822b6c44d..db3f6249d52 100644 --- a/app/helpers/file_type_helper.rb +++ b/app/helpers/file_type_helper.rb @@ -14,7 +14,8 @@ module FileTypeHelper 'image/png', 'image/gif', 'image/bmp', - 'image/webp' + 'image/webp', + 'image' ].include?(content_type) end @@ -23,7 +24,8 @@ module FileTypeHelper 'video/ogg', 'video/mp4', 'video/webm', - 'video/quicktime' + 'video/quicktime', + 'video' ].include?(content_type) end end diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 145660c0747..8828b5bb5bb 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -50,7 +50,10 @@
  • @@ -64,6 +65,7 @@ class="message--unread" :data="message" :is-a-tweet="isATweet" + :has-instagram-story="hasInstagramStory" />
    + + + '*/*', + 'Accept-Encoding' => 'gzip;q=1.0,deflate;q=0.6,identity;q=0.3', + 'User-Agent' => 'Down/5.3.0' + } + ) + .to_return(status: 200, body: '', headers: {}) end let!(:account) { create(:account) } + let(:return_onject) do + { name: 'Jane', + id: 'Sender-id-1', + account_id: instagram_inbox.account_id, + profile_pic: 'https://chatwoot-assets.local/sample.png' } + end let!(:instagram_channel) { create(:channel_instagram_fb_page, account: account, instagram_id: 'chatwoot-app-user-id-1') } let!(:instagram_inbox) { create(:inbox, channel: instagram_channel, account: account, greeting_enabled: false) } let!(:dm_params) { build(:instagram_message_create_event).with_indifferent_access } let!(:test_params) { build(:instagram_test_text_event).with_indifferent_access } + let!(:attachment_params) { build(:instagram_message_attachment_event).with_indifferent_access } + let!(:story_mention_params) { build(:instagram_story_mention_event).with_indifferent_access } let(:fb_object) { double } describe '#perform' do @@ -20,12 +37,7 @@ describe Webhooks::InstagramEventsJob do it 'creates incoming message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - { - name: 'Jane', - id: 'Sender-id-1', - account_id: instagram_inbox.account_id, - profile_pic: 'https://chatwoot-assets.local/sample.png' - }.with_indifferent_access + return_onject.with_indifferent_access ) instagram_webhook.perform_now(dm_params[:entry]) @@ -39,12 +51,7 @@ describe Webhooks::InstagramEventsJob do it 'creates test text message in the instagram inbox' do allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) allow(fb_object).to receive(:get_object).and_return( - { - name: 'Jane', - id: 'Sender-id-1', - account_id: instagram_inbox.account_id, - profile_pic: 'https://chatwoot-assets.local/sample.png' - }.with_indifferent_access + return_onject.with_indifferent_access ) instagram_webhook.perform_now(test_params[:entry]) @@ -53,6 +60,46 @@ describe Webhooks::InstagramEventsJob do expect(instagram_inbox.messages.count).to be 1 expect(instagram_inbox.messages.last.content).to eq('This is a test message from facebook.') end + + it 'creates incoming message with attachments in the instagram inbox' do + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + allow(fb_object).to receive(:get_object).and_return( + return_onject.with_indifferent_access + ) + instagram_webhook.perform_now(attachment_params[:entry]) + + instagram_inbox.reload + + expect(instagram_inbox.contacts.count).to be 1 + expect(instagram_inbox.messages.count).to be 1 + expect(instagram_inbox.messages.last.attachments.count).to be 1 + end + + it 'creates incoming message with attachments in the instagram inbox for story mention' do + allow(Koala::Facebook::API).to receive(:new).and_return(fb_object) + allow(fb_object).to receive(:get_object).and_return( + return_onject.with_indifferent_access, + { story: + { + mention: { + link: + 'https://lookaside.fbsbx.com/ig_messaging_cdn/?asset_id=17920786367196703&signature=Aby8EXbvNu4on9efDQecXDasiJX2s0FgWhFGz3mNFB__CsHR22O_1bJiYHkbp3mC1NQeW4jHxls9WyqVgRPcyonUbSJmD44UwLfFhbCK2obesWnFi7VOnisqLu48Xd6KYuNex7uSCQKWM-nw55zQ23bBgfCYw6h5hiJjFHwJDZYm65zXpQ', + id: '17920786367196703' + } + }, + from: { + username: 'Sender-id-1', id: 'Sender-id-1' + }, + id: 'instagram-message-id-1234' }.with_indifferent_access + ) + + instagram_webhook.perform_now(story_mention_params[:entry]) + + instagram_inbox.reload + + expect(instagram_inbox.messages.count).to be 1 + expect(instagram_inbox.messages.last.attachments.count).to be 1 + end end end end From 3c3b6f90c7e4f023322b0fcec33a8de5d51a44a6 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Date: Thu, 10 Mar 2022 20:40:47 +0530 Subject: [PATCH 28/43] fix: Add base_url to the audio notification file (#4116) --- .../dashboard/settings/reports/components/ReportFilters.vue | 1 - app/javascript/sdk/IFrameHelper.js | 3 ++- app/javascript/shared/helpers/AudioNotificationHelper.js | 5 +++-- config/environments/development.rb | 1 + config/environments/production.rb | 1 + config/environments/staging.rb | 1 + config/environments/test.rb | 1 + 7 files changed, 9 insertions(+), 4 deletions(-) diff --git a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue index 2fd32ee698f..857a7436361 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/reports/components/ReportFilters.vue @@ -255,7 +255,6 @@ export default { }, methods: { onDateRangeChange() { - console.log(this.from, this.to); this.$emit('date-range-change', { from: this.from, to: this.to, diff --git a/app/javascript/sdk/IFrameHelper.js b/app/javascript/sdk/IFrameHelper.js index a7f9dc70a6f..0325199127c 100644 --- a/app/javascript/sdk/IFrameHelper.js +++ b/app/javascript/sdk/IFrameHelper.js @@ -119,7 +119,8 @@ export const IFrameHelper = { }, setupAudioListeners: () => { - getAlertAudio().then(() => + const { baseUrl = '' } = window.$chatwoot; + getAlertAudio(baseUrl).then(() => initOnEvents.forEach(event => { document.removeEventListener( event, diff --git a/app/javascript/shared/helpers/AudioNotificationHelper.js b/app/javascript/shared/helpers/AudioNotificationHelper.js index 3713d153164..672816f905a 100644 --- a/app/javascript/shared/helpers/AudioNotificationHelper.js +++ b/app/javascript/shared/helpers/AudioNotificationHelper.js @@ -4,7 +4,7 @@ import { IFrameHelper } from 'widget/helpers/utils'; import { showBadgeOnFavicon } from './faviconHelper'; export const initOnEvents = ['click', 'touchstart', 'keypress']; -export const getAlertAudio = async () => { +export const getAlertAudio = async (baseUrl = '') => { const audioCtx = new (window.AudioContext || window.webkitAudioContext)(); const playsound = audioBuffer => { window.playAudioAlert = () => { @@ -17,7 +17,8 @@ export const getAlertAudio = async () => { }; try { - const audioRequest = new Request('/dashboard/audios/ding.mp3'); + const resourceUrl = `${baseUrl}/dashboard/audios/ding.mp3`; + const audioRequest = new Request(resourceUrl); fetch(audioRequest) .then(response => response.arrayBuffer()) diff --git a/config/environments/development.rb b/config/environments/development.rb index eab9c411d43..67ffa6a52f1 100644 --- a/config/environments/development.rb +++ b/config/environments/development.rb @@ -82,6 +82,7 @@ Rails.application.configure do allow do origins '*' resource '/packs/*', headers: :any, methods: [:get, :options] + resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options] resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry'] end end diff --git a/config/environments/production.rb b/config/environments/production.rb index fb6c124d8dc..e538e28676c 100644 --- a/config/environments/production.rb +++ b/config/environments/production.rb @@ -121,6 +121,7 @@ Rails.application.configure do allow do origins '*' resource '/packs/*', headers: :any, methods: [:get, :options] + resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options] if ActiveModel::Type::Boolean.new.cast(ENV.fetch('CW_API_ONLY_SERVER', false)) resource '*', headers: :any, methods: :any, expose: ['access-token', 'client', 'uid', 'expiry'] end diff --git a/config/environments/staging.rb b/config/environments/staging.rb index 36efcc1711f..6e9b3dcf81e 100644 --- a/config/environments/staging.rb +++ b/config/environments/staging.rb @@ -87,6 +87,7 @@ Rails.application.configure do allow do origins '*' resource '/packs/*', headers: :any, methods: [:get, :options] + resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options] end end end diff --git a/config/environments/test.rb b/config/environments/test.rb index 6a25346549f..24baf5affd7 100644 --- a/config/environments/test.rb +++ b/config/environments/test.rb @@ -58,6 +58,7 @@ Rails.application.configure do allow do origins '*' resource '/packs/*', headers: :any, methods: [:get, :options] + resource '/dashboard/audios/ding.mp3', headers: :any, methods: [:get, :options] end end end From 619441ce1cb7142939d041fe6ea44520c6aee4bd Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Thu, 10 Mar 2022 22:06:59 +0530 Subject: [PATCH 29/43] fixes: Showing vue warning in the console (#4152) * fixes: Showing vue warn with PR#4100 * Minor fixes --- .../dashboard/components/widgets/conversation/Message.vue | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/app/javascript/dashboard/components/widgets/conversation/Message.vue b/app/javascript/dashboard/components/widgets/conversation/Message.vue index 8828b5bb5bb..51d82ff8c94 100644 --- a/app/javascript/dashboard/components/widgets/conversation/Message.vue +++ b/app/javascript/dashboard/components/widgets/conversation/Message.vue @@ -217,10 +217,10 @@ export default { return this.data.sender || {}; }, storySender() { - return this.contentAttributes.story_sender || {}; + return this.contentAttributes.story_sender || null; }, storyId() { - return this.contentAttributes.story_id || {}; + return this.contentAttributes.story_id || null; }, contentType() { const { From 578414d788233fa441769dcf2f3275599ea7d830 Mon Sep 17 00:00:00 2001 From: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Date: Fri, 11 Mar 2022 17:00:55 +0530 Subject: [PATCH 30/43] feat: Adds ability to delete contact without having to type name (#4155) --- .../dashboard/i18n/locale/en/contact.json | 5 ++-- .../conversation/contact/ContactInfo.vue | 23 ++++--------------- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index 3824d959315..037f6f769de 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -77,9 +77,8 @@ "CONFIRM": { "TITLE": "Confirm Deletion", "MESSAGE": "Are you sure to delete ", - "PLACE_HOLDER": "Please type {contactName} to confirm", - "YES": "Yes, Delete ", - "NO": "No, Keep " + "YES": "Yes, Delete", + "NO": "No, Keep" }, "API": { "SUCCESS_MESSAGE": "Contact deleted successfully", diff --git a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue index 90aeb43ee70..24aeeff882d 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/contact/ContactInfo.vue @@ -126,17 +126,15 @@ @close="toggleMergeModal" />
    -
  • @@ -221,22 +219,11 @@ export default { return { twitter: twitterScreenName, ...(socialProfiles || {}) }; }, // Delete Modal - deleteConfirmText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.YES')} ${this.contact.name}`; - }, - deleteRejectText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.NO')} ${this.contact.name}`; - }, confirmDeleteMessage() { return `${this.$t('DELETE_CONTACT.CONFIRM.MESSAGE')} ${ this.contact.name } ?`; }, - confirmPlaceHolderText() { - return `${this.$t('DELETE_CONTACT.CONFIRM.PLACE_HOLDER', { - contactName: this.contact.name, - })}`; - }, }, methods: { toggleMergeModal() { From e730804b4888fa0fc21fb1681f4e2a53d752e645 Mon Sep 17 00:00:00 2001 From: Muhsin Keloth Date: Mon, 14 Mar 2022 15:06:56 +0530 Subject: [PATCH 31/43] feat: Custom fields in pre-chat form (#4135) Fixes: #2744 --- .../api/v1/accounts/inboxes_controller.rb | 1 - .../api/v1/widget/base_controller.rb | 6 +- .../api/v1/widget/conversations_controller.rb | 3 +- .../components/buttons/ToggleButton.vue | 65 ++++++++ .../dashboard/i18n/locale/en/inboxMgmt.json | 10 +- .../dashboard/settings/automation/Index.vue | 54 +------ .../settings/inbox/PreChatForm/Settings.vue | 113 +++++++++---- .../FluentIcon/dashboard-icons.json | 3 +- app/javascript/widget/api/endPoints.js | 1 + .../widget/components/PreChat/Form.vue | 152 +++++++++++++----- app/javascript/widget/i18n/locale/en.json | 11 +- app/javascript/widget/mixins/configMixin.js | 3 + app/javascript/widget/views/PreChatForm.vue | 10 +- app/models/channel/web_widget.rb | 3 +- ...3742_add_custom_fields_to_pre_chat_form.rb | 28 ++++ db/schema.rb | 2 +- 16 files changed, 328 insertions(+), 137 deletions(-) create mode 100644 app/javascript/dashboard/components/buttons/ToggleButton.vue create mode 100644 db/migrate/20220309103742_add_custom_fields_to_pre_chat_form.rb diff --git a/app/controllers/api/v1/accounts/inboxes_controller.rb b/app/controllers/api/v1/accounts/inboxes_controller.rb index 2bda5c07af0..d60e4c3e872 100644 --- a/app/controllers/api/v1/accounts/inboxes_controller.rb +++ b/app/controllers/api/v1/accounts/inboxes_controller.rb @@ -49,7 +49,6 @@ class Api::V1::Accounts::InboxesController < Api::V1::Accounts::BaseController return if permitted_params(channel_attributes)[:channel].blank? validate_email_channel(channel_attributes) if @inbox.inbox_type == 'Email' - @inbox.channel.update!(permitted_params(channel_attributes)[:channel]) update_channel_feature_flags end diff --git a/app/controllers/api/v1/widget/base_controller.rb b/app/controllers/api/v1/widget/base_controller.rb index 38c880526cd..ad555fc2207 100644 --- a/app/controllers/api/v1/widget/base_controller.rb +++ b/app/controllers/api/v1/widget/base_controller.rb @@ -68,7 +68,7 @@ class Api::V1::Widget::BaseController < ApplicationController mergee_contact: @contact ).perform else - @contact.update!(email: email, name: contact_name) + @contact.update!(email: email, name: contact_name, phone_number: contact_phone_number) end end @@ -80,6 +80,10 @@ class Api::V1::Widget::BaseController < ApplicationController params[:contact][:name] || contact_email.split('@')[0] end + def contact_phone_number + params[:contact][:phone_number] + end + def browser_params { browser_name: browser.name, diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb index 8d28345d44a..8cfb65be617 100644 --- a/app/controllers/api/v1/widget/conversations_controller.rb +++ b/app/controllers/api/v1/widget/conversations_controller.rb @@ -51,6 +51,7 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController end def permitted_params - params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email], message: [:content, :referer_url, :timestamp, :echo_id]) + params.permit(:id, :typing_status, :website_token, :email, contact: [:name, :email, :phone_number], + message: [:content, :referer_url, :timestamp, :echo_id]) end end diff --git a/app/javascript/dashboard/components/buttons/ToggleButton.vue b/app/javascript/dashboard/components/buttons/ToggleButton.vue new file mode 100644 index 00000000000..37328aba5df --- /dev/null +++ b/app/javascript/dashboard/components/buttons/ToggleButton.vue @@ -0,0 +1,65 @@ + + + + diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index 538d6d5f0d6..6111b90fdf0 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -187,7 +187,7 @@ } } }, - "WHATSAPP": { + "WHATSAPP": { "TITLE": "WhatsApp Channel", "DESC": "Start supporting your customers via WhatsApp.", "PROVIDERS": { @@ -211,7 +211,6 @@ "PLACEHOLDER": "API key", "APPLY_FOR_ACCESS": "Don't have any API key? Apply for access here", "ERROR": "Please enter a valid value." - }, "SUBMIT_BUTTON": "Create WhatsApp Channel", "API": { @@ -432,6 +431,11 @@ }, "PRE_CHAT_FORM": { "DESCRIPTION": "Pre chat forms enable you to capture user information before they start conversation with you.", + "SET_FIELDS": "Pre chat form fields", + "SET_FIELDS_HEADER": { + "FIELDS": "Fields", + "REQUIRED": "Required" + }, "ENABLE": { "LABEL": "Enable pre chat form", "OPTIONS": { @@ -464,7 +468,7 @@ "VALIDATION_ERROR": "Starting time should be before closing time.", "CHOOSE": "Choose" }, - "ALL_DAY":"All-Day" + "ALL_DAY": "All-Day" }, "IMAP": { "TITLE": "IMAP", diff --git a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue index 811fc95c6cc..b3a4ce9d7b7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/automation/Index.vue @@ -34,19 +34,10 @@ {{ automation.name }} {{ automation.description }} - + /> {{ readableTime(automation.created_on) }} @@ -140,11 +131,13 @@ import AddAutomationRule from './AddAutomationRule.vue'; import EditAutomationRule from './EditAutomationRule.vue'; import alertMixin from 'shared/mixins/alertMixin'; import timeMixin from 'dashboard/mixins/time'; +import ToggleButton from 'dashboard/components/buttons/ToggleButton'; export default { components: { AddAutomationRule, EditAutomationRule, + ToggleButton, }, mixins: [alertMixin, timeMixin], data() { @@ -290,41 +283,4 @@ export default { .automation__status-checkbox { margin: 0; } -.toggle-button { - background-color: var(--s-200); - position: relative; - display: inline-flex; - height: 19px; - width: 34px; - border: 2px solid transparent; - border-radius: var(--border-radius-large); - cursor: pointer; - transition-property: background-color; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; - flex-shrink: 0; -} - -.toggle-button.active { - background-color: var(--w-500); -} - -.toggle-button span { - --space-one-point-five: 1.5rem; - height: var(--space-one-point-five); - width: var(--space-one-point-five); - display: inline-block; - background-color: var(--white); - box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, - rgba(59, 130, 246, 0.5) 0px 0px 0px 0px, rgba(0, 0, 0, 0.1) 0px 1px 3px 0px, - rgba(0, 0, 0, 0.06) 0px 1px 2px 0px; - transform: translate(0, 0); - border-radius: 100%; - transition-property: transform; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 200ms; -} -.toggle-button span.active { - transform: translate(var(--space-one-point-five), var(--space-zero)); -} diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue index 6c01bc3dd7b..bf0c22515cb 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/PreChatForm/Settings.vue @@ -15,28 +15,57 @@ - -