Feat: improve read receipts (#56)
* feat: add store in additional_attributes to conversation model * feat: set default mark_as_read to true in additional_attributes for conversation params * fix: remove mark_as_read from additional_attributes in conversation * refactor: move message read logic to mark_messages_as_read method in ConversationsController * feat: add 'Mark messages as read' option to WhatsApp channel settings * feat: add 'mark_as_read' option in factorie for WhatsApp channel * feat: integrate Checkbox component for 'mark as read' option in WhatsApp settings * fix: ensure 'mark_as_read' option is included in channel creation payload * feat: add 'Mark as read' settings for WhatsApp inbox, including UI and state management * chore: remove redundant content update test from IncomingMessageBaileysService spec * feat: update 'Mark as read' label to 'Read receipts' and replace Checkbox with Switch component in WhatsApp settings * fix: handle potential nil value for 'mark_as_read' in provider config * feat: refactor provider config to streamline channel creation with 'mark_as_read' option * feat: add test for MESSAGE_READ event dispatch in update_last_seen action * fix: update subheader for 'Mark as read' setting to read receipt behavior in WhatsApp * feat: update mark_messages_as_read behavior to handle false value in provider config * chore: update label for 'Mark as read' option * feat: update update_last_seen to send WhatsApp read receipt for WhatsApp channels * test: remove MESSAGE_READ event dispatch tests in wrong file * feat: enhance update_last_seen behavior for WhatsApp channel to conditionally dispatch messages.read event * feat: update update_last_seen to dispatch messages.read event * test: refactor update_last_seen tests * chore: refactor to ensure provider_service responds to read_messages before ensure the mark_as_read provider config * test: enhance #read_messages with provider config mark_as_read expected behaviors * chore: clarify test names and remove useless expect --------- Co-authored-by: gabrieljablonski <contact@gabrieljablonski.com>
This commit is contained in:
parent
8ec086f8d0
commit
a3effacc21
@ -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')
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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",
|
||||
|
||||
@ -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 {
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="w-[65%] flex-shrink-0 flex-grow-0 max-w-[65%]">
|
||||
<label>
|
||||
<div class="flex mb-2 items-center">
|
||||
<span class="mr-2 text-sm">
|
||||
{{ $t('INBOX_MGMT.ADD.WHATSAPP.MARK_AS_READ.LABEL') }}
|
||||
</span>
|
||||
<Switch id="markAsRead" v-model="markAsRead" />
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<div class="w-full">
|
||||
|
||||
@ -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 {
|
||||
</NextButton>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
<SettingsSection
|
||||
:title="$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MARK_AS_READ_TITLE')"
|
||||
:sub-title="
|
||||
$t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MARK_AS_READ_SUBHEADER')
|
||||
"
|
||||
>
|
||||
<div class="flex items-center gap-2">
|
||||
<Switch
|
||||
id="markAsRead"
|
||||
v-model="markAsRead"
|
||||
@change="updateWhatsAppMarkAsRead"
|
||||
/>
|
||||
<label for="markAsRead">
|
||||
{{ $t('INBOX_MGMT.SETTINGS_POPUP.WHATSAPP_MARK_AS_READ_LABEL') }}
|
||||
</label>
|
||||
</div>
|
||||
</SettingsSection>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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' })
|
||||
|
||||
@ -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
|
||||
|
||||
Loading…
Reference in New Issue
Block a user