feat: toggle typing status on channel provider (#39)
* feat: toggle typing status on channel provider * refactor: general refactorings * fix: check if channel implements method * test: channel listener specs * test: channel spec * test: baileys service spec
This commit is contained in:
parent
81d62d94f1
commit
62ef2113d5
@ -48,6 +48,8 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
case permitted_params[:typing_status]
|
case permitted_params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
|
when 'recording'
|
||||||
|
trigger_typing_event(CONVERSATION_RECORDING)
|
||||||
when 'off'
|
when 'off'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -30,6 +30,8 @@ class Public::Api::V1::Inboxes::ConversationsController < Public::Api::V1::Inbox
|
|||||||
case params[:typing_status]
|
case params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON)
|
trigger_typing_event(CONVERSATION_TYPING_ON)
|
||||||
|
when 'recording'
|
||||||
|
trigger_typing_event(CONVERSATION_RECORDING)
|
||||||
when 'off'
|
when 'off'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
trigger_typing_event(CONVERSATION_TYPING_OFF)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -18,7 +18,8 @@ class AsyncDispatcher < BaseDispatcher
|
|||||||
NotificationListener.instance,
|
NotificationListener.instance,
|
||||||
ParticipationListener.instance,
|
ParticipationListener.instance,
|
||||||
ReportingEventListener.instance,
|
ReportingEventListener.instance,
|
||||||
WebhookListener.instance
|
WebhookListener.instance,
|
||||||
|
ChannelListener.instance
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@ -576,7 +576,11 @@ function createEditorView() {
|
|||||||
handleDOMEvents: {
|
handleDOMEvents: {
|
||||||
keyup: () => {
|
keyup: () => {
|
||||||
if (!props.disabled) {
|
if (!props.disabled) {
|
||||||
typingIndicator.start();
|
if (props.modelValue.length) {
|
||||||
|
typingIndicator.start();
|
||||||
|
} else {
|
||||||
|
typingIndicator.stop();
|
||||||
|
}
|
||||||
updateImgToolbarOnDelete();
|
updateImgToolbarOnDelete();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -863,6 +863,9 @@ export default {
|
|||||||
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
this.isRecorderAudioStopped = !this.isRecordingAudio;
|
||||||
if (!this.isRecordingAudio) {
|
if (!this.isRecordingAudio) {
|
||||||
this.resetAudioRecorderInput();
|
this.resetAudioRecorderInput();
|
||||||
|
this.onTypingOff();
|
||||||
|
} else {
|
||||||
|
this.onRecording();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
toggleAudioRecorderPlayPause() {
|
toggleAudioRecorderPlayPause() {
|
||||||
@ -872,6 +875,7 @@ export default {
|
|||||||
if (!this.isRecorderAudioStopped) {
|
if (!this.isRecorderAudioStopped) {
|
||||||
this.isRecorderAudioStopped = true;
|
this.isRecorderAudioStopped = true;
|
||||||
this.$refs.audioRecorderInput.stopRecording();
|
this.$refs.audioRecorderInput.stopRecording();
|
||||||
|
this.onTypingOff();
|
||||||
} else if (this.isRecorderAudioStopped) {
|
} else if (this.isRecorderAudioStopped) {
|
||||||
this.$refs.audioRecorderInput.playPause();
|
this.$refs.audioRecorderInput.playPause();
|
||||||
}
|
}
|
||||||
@ -887,6 +891,9 @@ export default {
|
|||||||
onTypingOn() {
|
onTypingOn() {
|
||||||
this.toggleTyping('on');
|
this.toggleTyping('on');
|
||||||
},
|
},
|
||||||
|
onRecording() {
|
||||||
|
this.toggleTyping('recording');
|
||||||
|
},
|
||||||
onTypingOff() {
|
onTypingOff() {
|
||||||
this.toggleTyping('off');
|
this.toggleTyping('off');
|
||||||
},
|
},
|
||||||
|
|||||||
@ -144,7 +144,11 @@ export default {
|
|||||||
this.resizeTextarea();
|
this.resizeTextarea();
|
||||||
},
|
},
|
||||||
onKeyup() {
|
onKeyup() {
|
||||||
this.typingIndicator.start();
|
if (this.modelValue.length) {
|
||||||
|
this.typingIndicator.start();
|
||||||
|
} else {
|
||||||
|
this.typingIndicator.stop();
|
||||||
|
}
|
||||||
},
|
},
|
||||||
onBlur() {
|
onBlur() {
|
||||||
this.typingIndicator.stop();
|
this.typingIndicator.stop();
|
||||||
|
|||||||
25
app/listeners/channel_listener.rb
Normal file
25
app/listeners/channel_listener.rb
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
class ChannelListener < BaseListener
|
||||||
|
def conversation_typing_on(event)
|
||||||
|
handle_typing_event(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_recording(event)
|
||||||
|
handle_typing_event(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
def conversation_typing_off(event)
|
||||||
|
handle_typing_event(event)
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def handle_typing_event(event)
|
||||||
|
is_private, conversation = event.data.values_at(:is_private, :conversation)
|
||||||
|
return if is_private
|
||||||
|
|
||||||
|
channel = conversation.inbox.channel
|
||||||
|
return unless channel.respond_to?(:toggle_typing_status)
|
||||||
|
|
||||||
|
channel.toggle_typing_status(event.name, conversation: conversation)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -79,6 +79,12 @@ class Channel::Whatsapp < ApplicationRecord
|
|||||||
data
|
data
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def toggle_typing_status(typing_status, conversation:)
|
||||||
|
return unless provider_service.respond_to?(:toggle_typing_status)
|
||||||
|
|
||||||
|
provider_service.toggle_typing_status(conversation.contact.phone_number, typing_status)
|
||||||
|
end
|
||||||
|
|
||||||
delegate :setup_channel_provider, to: :provider_service
|
delegate :setup_channel_provider, to: :provider_service
|
||||||
delegate :disconnect_channel_provider, to: :provider_service
|
delegate :disconnect_channel_provider, to: :provider_service
|
||||||
delegate :send_message, to: :provider_service
|
delegate :send_message, to: :provider_service
|
||||||
|
|||||||
@ -18,6 +18,8 @@ class Conversations::TypingStatusManager
|
|||||||
case params[:typing_status]
|
case params[:typing_status]
|
||||||
when 'on'
|
when 'on'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
trigger_typing_event(CONVERSATION_TYPING_ON, params[:is_private])
|
||||||
|
when 'recording'
|
||||||
|
trigger_typing_event(CONVERSATION_RECORDING, params[:is_private])
|
||||||
when 'off'
|
when 'off'
|
||||||
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
trigger_typing_event(CONVERSATION_TYPING_OFF, params[:is_private])
|
||||||
end
|
end
|
||||||
|
|||||||
@ -66,6 +66,26 @@ class Whatsapp::Providers::WhatsappBaileysService < Whatsapp::Providers::BaseSer
|
|||||||
process_response(response)
|
process_response(response)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def toggle_typing_status(phone_number, typing_status)
|
||||||
|
@phone_number = phone_number
|
||||||
|
status_map = {
|
||||||
|
Events::Types::CONVERSATION_TYPING_ON => 'composing',
|
||||||
|
Events::Types::CONVERSATION_RECORDING => 'recording',
|
||||||
|
Events::Types::CONVERSATION_TYPING_OFF => 'paused'
|
||||||
|
}
|
||||||
|
|
||||||
|
response = HTTParty.patch(
|
||||||
|
"#{provider_url}/connections/#{whatsapp_channel.phone_number}/presence",
|
||||||
|
headers: api_headers,
|
||||||
|
body: {
|
||||||
|
toJid: remote_jid,
|
||||||
|
type: status_map[typing_status]
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
|
||||||
|
process_response(response)
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def provider_url
|
def provider_url
|
||||||
|
|||||||
@ -27,6 +27,7 @@ module Events::Types
|
|||||||
ASSIGNEE_CHANGED = 'assignee.changed'
|
ASSIGNEE_CHANGED = 'assignee.changed'
|
||||||
TEAM_CHANGED = 'team.changed'
|
TEAM_CHANGED = 'team.changed'
|
||||||
CONVERSATION_TYPING_ON = 'conversation.typing_on'
|
CONVERSATION_TYPING_ON = 'conversation.typing_on'
|
||||||
|
CONVERSATION_RECORDING = 'conversation.recording'
|
||||||
CONVERSATION_TYPING_OFF = 'conversation.typing_off'
|
CONVERSATION_TYPING_OFF = 'conversation.typing_off'
|
||||||
CONVERSATION_MENTIONED = 'conversation.mentioned'
|
CONVERSATION_MENTIONED = 'conversation.mentioned'
|
||||||
|
|
||||||
|
|||||||
66
spec/listeners/channel_listener_spec.rb
Normal file
66
spec/listeners/channel_listener_spec.rb
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
require 'rails_helper'
|
||||||
|
describe ChannelListener do
|
||||||
|
let(:listener) { described_class.instance }
|
||||||
|
|
||||||
|
context 'when handling typing events' do
|
||||||
|
let(:channel) { create(:channel_whatsapp, sync_templates: false, validate_provider_config: false) }
|
||||||
|
let(:conversation) { create(:conversation, inbox: create(:inbox, channel: channel)) }
|
||||||
|
|
||||||
|
it 'skips the event if is_private is true' do
|
||||||
|
allow(channel).to receive(:toggle_typing_status)
|
||||||
|
|
||||||
|
listener.conversation_typing_on(build_typing_event(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation, is_private: true))
|
||||||
|
|
||||||
|
expect(channel).not_to have_received(:toggle_typing_status)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'skips the event if channel does not respond to toggle_typing_status' do
|
||||||
|
channel = create(:channel_api)
|
||||||
|
conversation = create(:conversation, inbox: create(:inbox, channel: channel))
|
||||||
|
|
||||||
|
expect do
|
||||||
|
listener.conversation_typing_on(build_typing_event(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation))
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_typing_on' do
|
||||||
|
let(:event_name) { Events::Types::CONVERSATION_TYPING_ON }
|
||||||
|
|
||||||
|
it 'calls toggle_typing_status on the channel' do
|
||||||
|
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||||
|
|
||||||
|
listener.conversation_typing_on(build_typing_event(event_name, conversation: conversation))
|
||||||
|
|
||||||
|
expect(channel).to have_received(:toggle_typing_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_recording' do
|
||||||
|
let(:event_name) { Events::Types::CONVERSATION_RECORDING }
|
||||||
|
|
||||||
|
it 'calls toggle_typing_status on the channel' do
|
||||||
|
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||||
|
|
||||||
|
listener.conversation_recording(build_typing_event(event_name, conversation: conversation))
|
||||||
|
|
||||||
|
expect(channel).to have_received(:toggle_typing_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#conversation_typing_off' do
|
||||||
|
let(:event_name) { Events::Types::CONVERSATION_TYPING_OFF }
|
||||||
|
|
||||||
|
it 'calls toggle_typing_status on the channel' do
|
||||||
|
allow(channel).to receive(:toggle_typing_status).with(event_name, conversation: conversation)
|
||||||
|
|
||||||
|
listener.conversation_typing_off(build_typing_event(event_name, conversation: conversation))
|
||||||
|
|
||||||
|
expect(channel).to have_received(:toggle_typing_status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def build_typing_event(event_name, conversation:, is_private: false)
|
||||||
|
Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: create(:user), is_private: is_private)
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -62,6 +62,35 @@ RSpec.describe Channel::Whatsapp do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#toggle_typing_status' do
|
||||||
|
let(:channel) { create(:channel_whatsapp, provider: 'baileys', validate_provider_config: false, sync_templates: false) }
|
||||||
|
let(:conversation) { create(:conversation) }
|
||||||
|
|
||||||
|
it 'calls provider service method' do
|
||||||
|
provider_double = instance_double(Whatsapp::Providers::WhatsappBaileysService, toggle_typing_status: nil)
|
||||||
|
allow(Whatsapp::Providers::WhatsappBaileysService).to receive(:new)
|
||||||
|
.with(whatsapp_channel: channel)
|
||||||
|
.and_return(provider_double)
|
||||||
|
|
||||||
|
channel.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation)
|
||||||
|
|
||||||
|
expect(provider_double).to have_received(:toggle_typing_status)
|
||||||
|
.with(conversation.contact.phone_number, Events::Types::CONVERSATION_TYPING_ON)
|
||||||
|
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)
|
||||||
|
.with(whatsapp_channel: channel)
|
||||||
|
.and_return(provider_double)
|
||||||
|
|
||||||
|
expect do
|
||||||
|
channel.toggle_typing_status(Events::Types::CONVERSATION_TYPING_ON, conversation: conversation)
|
||||||
|
end.not_to raise_error
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'callbacks' do
|
describe 'callbacks' do
|
||||||
describe '#disconnect_channel_provider' do
|
describe '#disconnect_channel_provider' do
|
||||||
context 'when provider is baileys' do
|
context 'when provider is baileys' do
|
||||||
|
|||||||
@ -363,6 +363,80 @@ describe Whatsapp::Providers::WhatsappBaileysService do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe '#toggle_typing_status' do
|
||||||
|
let(:request_path) { "#{whatsapp_channel.provider_config['provider_url']}/connections/#{whatsapp_channel.phone_number}/presence" }
|
||||||
|
|
||||||
|
it 'calls presence endpoint for typing on' do
|
||||||
|
request = stub_request(:patch, request_path)
|
||||||
|
.with(
|
||||||
|
headers: stub_headers(whatsapp_channel),
|
||||||
|
body: {
|
||||||
|
toJid: test_send_jid,
|
||||||
|
type: 'composing'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(status: 200)
|
||||||
|
|
||||||
|
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
|
||||||
|
|
||||||
|
expect(request).to have_been_requested
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls presence endpoint for recording' do
|
||||||
|
request = stub_request(:patch, request_path)
|
||||||
|
.with(
|
||||||
|
headers: stub_headers(whatsapp_channel),
|
||||||
|
body: {
|
||||||
|
toJid: test_send_jid,
|
||||||
|
type: 'recording'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(status: 200)
|
||||||
|
|
||||||
|
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_RECORDING)
|
||||||
|
|
||||||
|
expect(request).to have_been_requested
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'calls presence endpoint for typing off' do
|
||||||
|
request = stub_request(:patch, request_path)
|
||||||
|
.with(
|
||||||
|
headers: stub_headers(whatsapp_channel),
|
||||||
|
body: {
|
||||||
|
toJid: test_send_jid,
|
||||||
|
type: 'paused'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(status: 200)
|
||||||
|
|
||||||
|
service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_OFF)
|
||||||
|
|
||||||
|
expect(request).to have_been_requested
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'logs the error and returns false' do
|
||||||
|
stub_request(:patch, request_path)
|
||||||
|
.with(
|
||||||
|
headers: stub_headers(whatsapp_channel),
|
||||||
|
body: {
|
||||||
|
toJid: test_send_jid,
|
||||||
|
type: 'composing'
|
||||||
|
}.to_json
|
||||||
|
)
|
||||||
|
.to_return(
|
||||||
|
status: 400,
|
||||||
|
body: 'error message',
|
||||||
|
headers: {}
|
||||||
|
)
|
||||||
|
allow(Rails.logger).to receive(:error).with('error message')
|
||||||
|
|
||||||
|
response = service.toggle_typing_status(test_send_phone_number, Events::Types::CONVERSATION_TYPING_ON)
|
||||||
|
|
||||||
|
expect(response).to be(false)
|
||||||
|
expect(Rails.logger).to have_received(:error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
|
context 'when environment variable BAILEYS_PROVIDER_DEFAULT_URL is set' do
|
||||||
it 'uses the base url from the environment variable' do
|
it 'uses the base url from the environment variable' do
|
||||||
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
|
stub_const('Whatsapp::Providers::WhatsappBaileysService::DEFAULT_URL', 'http://test.com')
|
||||||
|
|||||||
@ -8,7 +8,11 @@ parameters:
|
|||||||
in: query
|
in: query
|
||||||
required: true
|
required: true
|
||||||
type: string
|
type: string
|
||||||
description: Typing status, either 'on' or 'off'
|
enum:
|
||||||
|
- 'on'
|
||||||
|
- 'recording'
|
||||||
|
- 'off'
|
||||||
|
description: Typing status.
|
||||||
responses:
|
responses:
|
||||||
200:
|
200:
|
||||||
description: Typing status toggled successfully
|
description: Typing status toggled successfully
|
||||||
|
|||||||
@ -989,7 +989,12 @@
|
|||||||
"in": "query",
|
"in": "query",
|
||||||
"required": true,
|
"required": true,
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "Typing status, either 'on' or 'off'"
|
"enum": [
|
||||||
|
"on",
|
||||||
|
"recording",
|
||||||
|
"off"
|
||||||
|
],
|
||||||
|
"description": "Typing status."
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"responses": {
|
"responses": {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user