diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 77f82e507..de19128cf 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -110,9 +110,7 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro end def update_last_seen - # NOTE: Use old `agent_last_seen_at`, so we reference messages received after that - Rails.configuration.dispatcher.dispatch(Events::Types::MESSAGES_READ, Time.zone.now, conversation: @conversation, - last_seen_at: @conversation.agent_last_seen_at) + dispatch_messages_read_event if assignee? update_last_seen_on_conversation(DateTime.now.utc, assignee?) end @@ -206,6 +204,12 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro def assignee? @conversation.assignee_id? && Current.user == @conversation.assignee end + + def dispatch_messages_read_event + # NOTE: Use old `agent_last_seen_at`, so we reference messages received after that + Rails.configuration.dispatcher.dispatch(Events::Types::MESSAGES_READ, Time.zone.now, conversation: @conversation, + last_seen_at: @conversation.agent_last_seen_at) + end end Api::V1::Accounts::ConversationsController.prepend_mod_with('Api::V1::Accounts::ConversationsController') diff --git a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json index d88e34562..14ebc31d4 100644 --- a/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/en/inboxMgmt.json @@ -267,7 +267,10 @@ "PROVIDER_URL": { "LABEL": "Provider URL", "PLACEHOLDER": "If provider is not running locally, please provide the URL", - "ERROR":"Please enter a valid URL" + "ERROR": "Please enter a valid URL" + }, + "MARK_AS_READ": { + "LABEL": "Send read receipts" }, "ADVANCED_OPTIONS": "Advanced options", "BAILEYS": { @@ -558,7 +561,10 @@ "WHATSAPP_PROVIDER_URL_SUBHEADER": "If the provider is not running locally, please provide the URL.", "WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Enter the provider URL", "WHATSAPP_PROVIDER_URL_ERROR": "Please enter a valid URL", - "UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings" + "UPDATE_PRE_CHAT_FORM_SETTINGS": "Update Pre Chat Form Settings", + "WHATSAPP_MARK_AS_READ_TITLE": "Read receipts", + "WHATSAPP_MARK_AS_READ_SUBHEADER": "If turned off, when a message is viewed in Chatwoot, a read receipt will not be sent to the sender. Your messages will still be able to receive read receipts from the sender.", + "WHATSAPP_MARK_AS_READ_LABEL": "Send read receipts" }, "HELP_CENTER": { "LABEL": "Help Center", diff --git a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json index b1713e19e..5217cb6ec 100644 --- a/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json +++ b/app/javascript/dashboard/i18n/locale/pt_BR/inboxMgmt.json @@ -267,7 +267,10 @@ "PROVIDER_URL": { "LABEL": "URL do provedor", "PLACEHOLDER": "Se o provedor não está rodando localmente, por favor, insira a URL do provedor", - "ERROR":"Por favor, insira uma URL válida" + "ERROR": "Por favor, insira uma URL válida" + }, + "MARK_AS_READ": { + "LABEL": "Enviar confirmações de leitura" }, "ADVANCED_OPTIONS": "Opções avançadas", "BAILEYS": { @@ -558,7 +561,10 @@ "WHATSAPP_PROVIDER_URL_SUBHEADER": "Se o provedor não estiver rodando localmente, por favor, forneça a URL.", "WHATSAPP_PROVIDER_URL_PLACEHOLDER": "Digite a URL do provedor", "WHATSAPP_PROVIDER_URL_ERROR": "Por favor, insira uma URL válida", - "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat" + "UPDATE_PRE_CHAT_FORM_SETTINGS": "Atualizar configurações do Formulário Pre Chat", + "WHATSAPP_MARK_AS_READ_TITLE": "Confirmações de leitura", + "WHATSAPP_MARK_AS_READ_SUBHEADER": "Se essa opção estiver desativada, ao visualizar uma mensagem pelo Chatwoot, não será enviada uma confirmação de leitura para o remetente. As suas mensagens ainda poderão receber confirmações de leitura.", + "WHATSAPP_MARK_AS_READ_LABEL": "Enviar confirmações de leitura" }, "HELP_CENTER": { "LABEL": "Centro de Ajuda", diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue index 72680cb1d..b5565c2bc 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/channels/BaileysWhatsapp.vue @@ -8,10 +8,13 @@ import { isPhoneE164OrEmpty } from 'shared/helpers/Validators'; import { isValidURL } from '../../../../../helper/URLHelper'; import NextButton from 'dashboard/components-next/button/Button.vue'; +import Switch from 'dashboard/components-next/switch/Switch.vue'; export default { components: { NextButton, + // eslint-disable-next-line vue/no-reserved-component-names + Switch, }, setup() { return { v$: useVuelidate() }; @@ -23,6 +26,7 @@ export default { apiKey: '', providerUrl: '', showAdvancedOptions: false, + markAsRead: true, }; }, computed: { @@ -47,6 +51,15 @@ export default { } try { + const providerConfig = { + mark_as_read: this.markAsRead, + }; + + if (this.apiKey || this.providerUrl) { + providerConfig.api_key = this.apiKey; + providerConfig.url = this.providerUrl; + } + const whatsappChannel = await this.$store.dispatch( 'inboxes/createChannel', { @@ -55,13 +68,7 @@ export default { type: 'whatsapp', phone_number: this.phoneNumber, provider: 'baileys', - provider_config: - this.apiKey || this.providerUrl - ? { - api_key: this.apiKey, - url: this.providerUrl, - } - : {}, + provider_config: providerConfig, }, } ); @@ -159,6 +166,17 @@ export default { + +
+ +
diff --git a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue index 805b8e458..96987b2a7 100644 --- a/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue +++ b/app/javascript/dashboard/routes/dashboard/settings/inbox/settingsPage/ConfigurationPage.vue @@ -10,6 +10,7 @@ import { requiredIf } from '@vuelidate/validators'; import { isValidURL } from '../../../../../helper/URLHelper'; import WhatsappBaileysLinkDeviceModal from '../components/WhatsappBaileysLinkDeviceModal.vue'; import InboxName from '../../../../../components/widgets/InboxName.vue'; +import Switch from 'dashboard/components-next/switch/Switch.vue'; export default { components: { @@ -19,6 +20,8 @@ export default { NextButton, WhatsappBaileysLinkDeviceModal, InboxName, + // eslint-disable-next-line vue/no-reserved-component-names + Switch, }, mixins: [inboxMixin], props: { @@ -36,6 +39,7 @@ export default { whatsAppInboxAPIKey: '', whatsAppProviderUrl: '', showBaileysLinkDeviceModal: false, + markAsRead: true, }; }, validations() { @@ -57,6 +61,7 @@ export default { methods: { setDefaults() { this.hmacMandatory = this.inbox.hmac_mandatory || false; + this.markAsRead = this.inbox.provider_config.mark_as_read ?? true; }, handleHmacFlag() { this.updateInbox(); @@ -114,6 +119,24 @@ export default { useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); } }, + async updateWhatsAppMarkAsRead() { + try { + const payload = { + id: this.inbox.id, + formData: false, + channel: { + provider_config: { + ...this.inbox.provider_config, + mark_as_read: this.markAsRead, + }, + }, + }; + await this.$store.dispatch('inboxes/updateInbox', payload); + useAlert(this.$t('INBOX_MGMT.EDIT.API.SUCCESS_MESSAGE')); + } catch (error) { + useAlert(this.$t('INBOX_MGMT.EDIT.API.ERROR_MESSAGE')); + } + }, onOpenBaileysLinkDeviceModal() { this.showBaileysLinkDeviceModal = true; }, @@ -383,6 +406,23 @@ export default {
+ +
+ + +
+
diff --git a/app/models/channel/whatsapp.rb b/app/models/channel/whatsapp.rb index 66e3a3883..53bdccdd1 100644 --- a/app/models/channel/whatsapp.rb +++ b/app/models/channel/whatsapp.rb @@ -93,6 +93,8 @@ class Channel::Whatsapp < ApplicationRecord def read_messages(messages, conversation:) return unless provider_service.respond_to?(:read_messages) + # NOTE: This is the default behavior, so `mark_as_read` being `nil` is the same as `true`. + return if provider_config&.dig('mark_as_read') == false provider_service.read_messages(conversation.contact.phone_number, messages) end diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 43ec3bb55..de9cf7f3c 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -688,18 +688,33 @@ RSpec.describe 'Conversations API', type: :request do expect(conversation.reload.assignee_last_seen_at).not_to be_nil end - it 'dispatches messages.read event' do + it 'dispatches messages.read event when user is assignee' do freeze_time - conversation.update!(agent_last_seen_at: 1.hour.ago) + + previous_agent_last_seen_at = 1.hour.ago + conversation.update!(agent_last_seen_at: previous_agent_last_seen_at, assignee: agent) + allow(Rails.configuration.dispatcher).to receive(:dispatch) - .with(Events::Types::MESSAGES_READ, Time.zone.now, conversation: conversation, last_seen_at: conversation.agent_last_seen_at) post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen", headers: agent.create_new_auth_token, as: :json expect(response).to have_http_status(:success) - expect(Rails.configuration.dispatcher).to have_received(:dispatch) + expect(Rails.configuration.dispatcher) + .to have_received(:dispatch) + .with(Events::Types::MESSAGES_READ, Time.zone.now, conversation: conversation, last_seen_at: previous_agent_last_seen_at) + end + + it 'does not dispatch messages.read event when user is not assignee' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/update_last_seen", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(Rails.configuration.dispatcher).not_to have_received(:dispatch) end end end diff --git a/spec/factories/channel/channel_whatsapp.rb b/spec/factories/channel/channel_whatsapp.rb index 36e696e81..fd0786a00 100644 --- a/spec/factories/channel/channel_whatsapp.rb +++ b/spec/factories/channel/channel_whatsapp.rb @@ -77,7 +77,7 @@ FactoryBot.define do channel_whatsapp.define_singleton_method(:validate_provider_config) { nil } unless options.validate_provider_config if channel_whatsapp.provider == 'baileys' channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'provider_url' => 'https://baileys.api', - 'phone_number_id' => '123456789' }) + 'phone_number_id' => '123456789', 'mark_as_read' => true }) elsif channel_whatsapp.provider == 'whatsapp_cloud' channel_whatsapp.provider_config = channel_whatsapp.provider_config.merge({ 'api_key' => 'test_key', 'phone_number_id' => '123456789', 'business_account_id' => '123456789' }) diff --git a/spec/models/channel/whatsapp_spec.rb b/spec/models/channel/whatsapp_spec.rb index bc597e6dc..9150ba01e 100644 --- a/spec/models/channel/whatsapp_spec.rb +++ b/spec/models/channel/whatsapp_spec.rb @@ -121,7 +121,9 @@ RSpec.describe Channel::Whatsapp do end describe '#read_messages' do - let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) } + let(:channel) do + create(:channel_whatsapp, provider: 'baileys', provider_config: { mark_as_read: true }, validate_provider_config: false, sync_templates: false) + end let(:conversation) { create(:conversation) } let(:message) { create(:message) } @@ -137,13 +139,30 @@ RSpec.describe Channel::Whatsapp do expect(provider_double).to have_received(:read_messages) end - it 'does not call method if provider service does not implement it' do - channel = create(:channel_whatsapp, provider: 'whatsapp_cloud', validate_provider_config: false, sync_templates: false) - provider_double = instance_double(Whatsapp::Providers::WhatsappCloudService) - allow(Whatsapp::Providers::WhatsappCloudService).to receive(:new) + it 'call method when the provider config mark_as_read is nil' do + channel.update!(provider_config: {}) + provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, read_messages: nil) + allow(provider_double).to receive(:read_messages).with([message], conversation.contact.phone_number) + allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new) .with(whatsapp_channel: channel) .and_return(provider_double) + channel.read_messages([message], conversation: conversation) + + expect(provider_double).to have_received(:read_messages) + end + + it 'does not call method if provider service does not implement it' do + channel.update!(provider: 'whatsapp_cloud') + + expect do + channel.read_messages([message], conversation: conversation) + end.not_to raise_error + end + + it 'does not call method if provider config mark_as_read is false' do + channel.update!(provider_config: { mark_as_read: false }) + expect do channel.read_messages([message], conversation: conversation) end.not_to raise_error