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 655c6bc1c..746d1bfec 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 d745c4153..fbc303a4f 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 79fb36de6..ef146ce72 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 71c1b274a..ad109a0ea 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 c4bfcdb08..66f6ba790 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 2d851ba49..94d91f77f 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 2895978f9..a78e7c267 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 5b839ebef..ccafe2f56 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 0ee598245..c62eba70e 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 33f62a9c4..f2967945a 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 894dd7f4e..17733fde3 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 1fdf9fd50..c98eb5098 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 7f0dd470c..1f17dbb08 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 3f95405f9..f2f238cd4 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 308bab3e6..000000000 --- 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 8e7fd0daf..05aa1db4b 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 407dc47bc..000000000 --- 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 9da1e93dc..23ff69fd2 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 9b551141d..650ea6f8e 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 7cb49c576..288b15504 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 74ce2aedf..22e9b3fc6 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 000000000..9241ff7b2 --- /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 8c37b7897..2f3334b74 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 5a19f439f..000000000 --- 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 e40d8353e..000000000 --- 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 0849936f5..000000000 --- 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 16fe18f49..c2c9977b3 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 936e56ece..c779c01f6 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 a62da7682..399815014 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 fc3787ab4..bd835865b 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 ba94540d8..2da7145cd 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 359f6c3db..553a4e360 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 e9923cd38..92015edcc 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 17733fde3..9d08e6b62 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 c98eb5098..22337bc63 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 4a58d8a54..90aeb43ee 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 5f17b5114..7441d9a73 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 e82fb2ecc..b2c7dd589 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 211ffeaa6..66365e85c 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 e4b063899..60ca0d590 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 bb718bb24..7dc0dade5 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 9b3875856..79b9e927e 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 f9f57e2ef..421e2f8b9 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 94d91f77f..b5cc35d7c 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 9896d6b8e..3713d1531 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 12b7b5957..2dc71bcec 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 f737bd313..d2614421c 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 3a344d985..ffd8018d5 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 000000000..0c1472a84 --- /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 ce95b861c..8c92467bd 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 d353e6b6e..5727e09d0 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 be79dd4f5..811fc95c6 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 1f2839048..9d90bb9e2 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 d38b212be..7bc1dad6d 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 3f6a606de..119d5c615 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 526879099..abb4231d9 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 250c5c3c6..3d5846215 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 6df1b2858..6dcb47532 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 37fbb4367..5a9253fba 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 96ba4b429..ac25a5aed 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 39064412c..0cbc85d22 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 a784b2ba2..d9ca9f943 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 1703f63e0..4f794fa6d 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 f5297932a..eb92e5a17 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 1ccd05b90..931d979a6 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 a16b50a98..2222403f1 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 7536c0d24..dbac2f0bc 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 08aa58be0..2e58825f4 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 9e822b6c4..db3f6249d 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 145660c07..8828b5bb5 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 2fd32ee69..857a74363 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 a7f9dc70a..032519912 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 3713d1531..672816f90 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 eab9c411d..67ffa6a52 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 fb6c124d8..e538e2867 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 36efcc171..6e9b3dcf8 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 6a2534654..24baf5aff 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 8828b5bb5..51d82ff8c 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 3824d9593..037f6f769 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 90aeb43ee..24aeeff88 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 2bda5c07a..d60e4c3e8 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 38c880526..ad555fc22 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 8d28345d4..8cfb65be6 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 000000000..37328aba5 --- /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 538d6d5f0..6111b90fd 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 811fc95c6..b3a4ce9d7 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 6c01bc3dd..bf0c22515 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 @@ - -