diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index dea9c0849..5b39014af 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -1,4 +1,5 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController + include Events::Types before_action :conversation, except: [:index] before_action :contact_inbox, only: [:create] @@ -23,6 +24,16 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController @status = @conversation.toggle_status end + def toggle_typing_status + user = current_user.presence || @resource + if params[:typing_status] == 'on' + Rails.configuration.dispatcher.dispatch(CONVERSATION_TYPING_ON, Time.zone.now, conversation: @conversation, user: user) + elsif params[:typing_status] == 'off' + Rails.configuration.dispatcher.dispatch(CONVERSATION_TYPING_OFF, Time.zone.now, conversation: @conversation) + end + head :ok + end + def update_last_seen @conversation.agent_last_seen_at = parsed_last_seen_at @conversation.save! diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index dddbb91a4..f157bcba8 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -48,6 +48,28 @@ class ActionCableListener < BaseListener broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) end + def conversation_typing_on(event) + conversation = event.data[:conversation] + account = conversation.account + user = event.data[:user] + tokens = user_tokens(account, conversation.inbox.members) + + [conversation.contact.pubsub_token] + + broadcast(tokens, CONVERSATION_TYPING_ON, + conversation: conversation.push_event_data, user: user.push_event_data) + end + + def conversation_typing_off(event) + conversation = event.data[:conversation] + account = conversation.account + user = event.data[:user] + tokens = user_tokens(account, conversation.inbox.members) + + [conversation.contact.pubsub_token] + + broadcast(tokens, CONVERSATION_TYPING_OFF, + conversation: conversation.push_event_data, user: user.push_event_data) + end + def assignee_changed(event) conversation, account, timestamp = extract_conversation_and_account(event) diff --git a/app/models/agent_bot.rb b/app/models/agent_bot.rb index 38b128d8d..02e4c2119 100644 --- a/app/models/agent_bot.rb +++ b/app/models/agent_bot.rb @@ -16,4 +16,20 @@ class AgentBot < ApplicationRecord has_many :agent_bot_inboxes, dependent: :destroy has_many :inboxes, through: :agent_bot_inboxes + + def push_event_data + { + name: name, + avatar_url: avatar_url, + type: 'agent_bot' + } + end + + def webhook_data + { + id: id, + name: name, + type: 'agent_bot' + } + end end diff --git a/app/models/contact.rb b/app/models/contact.rb index 087951205..31c757b68 100644 --- a/app/models/contact.rb +++ b/app/models/contact.rb @@ -50,6 +50,7 @@ class Contact < ApplicationRecord id: id, name: name, thumbnail: avatar_url, + type: 'contact', pubsub_token: pubsub_token } end @@ -58,7 +59,8 @@ class Contact < ApplicationRecord { id: id, name: name, - avatar: avatar_url + avatar: avatar_url, + type: 'contact' } end diff --git a/app/models/user.rb b/app/models/user.rb index 06c339587..2afb630ec 100644 --- a/app/models/user.rb +++ b/app/models/user.rb @@ -129,7 +129,8 @@ class User < ApplicationRecord def push_event_data { name: name, - avatar_url: avatar_url + avatar_url: avatar_url, + type: 'user' } end @@ -137,7 +138,8 @@ class User < ApplicationRecord { id: id, name: name, - email: email + email: email, + type: 'user' } end end diff --git a/config/routes.rb b/config/routes.rb index a3ea7d998..f63460e05 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -49,6 +49,7 @@ Rails.application.routes.draw do end member do post :toggle_status + post :toggle_typing_status post :update_last_seen end end diff --git a/lib/events/types.rb b/lib/events/types.rb index 422820720..beb20f9d0 100644 --- a/lib/events/types.rb +++ b/lib/events/types.rb @@ -15,6 +15,8 @@ module Events::Types CONVERSATION_RESOLVED = 'conversation.resolved' CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle' ASSIGNEE_CHANGED = 'assignee.changed' + CONVERSATION_TYPING_ON = 'conversation.typing_on' + CONVERSATION_TYPING_OFF = 'conversation.typing_off' # message events MESSAGE_CREATED = 'message.created' diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 4d3397047..525348d2d 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -122,6 +122,34 @@ RSpec.describe 'Conversations API', type: :request do end end + describe 'POST /api/v1/accounts/{account.id}/conversations/:id/toggle_typing_status' do + let(:conversation) { create(:conversation, account: account) } + + context 'when it is an unauthenticated user' do + it 'returns unauthorized' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status" + + expect(response).to have_http_status(:unauthorized) + end + end + + context 'when it is an authenticated user' do + let(:agent) { create(:user, account: account, role: :agent) } + + it 'toggles the conversation status' do + allow(Rails.configuration.dispatcher).to receive(:dispatch) + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/toggle_typing_status", + headers: agent.create_new_auth_token, + params: { typing_status: 'on' }, + as: :json + + expect(response).to have_http_status(:success) + expect(Rails.configuration.dispatcher).to have_received(:dispatch) + .with(Conversation::CONVERSATION_TYPING_ON, kind_of(Time), { conversation: conversation, user: agent }) + end + end + end + describe 'POST /api/v1/accounts/{account.id}/conversations/:id/update_last_seen' do let(:conversation) { create(:conversation, account: account) } diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index daad3cc6f..589ef280f 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -29,4 +29,36 @@ describe ActionCableListener do listener.message_created(event) end end + + describe '#typing_on' do + let(:event_name) { :'conversation.typing_on' } + let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: agent) } + + it 'sends message to account admins, inbox agents and the contact' do + # HACK: to reload conversation inbox members + expect(conversation.inbox.reload.inbox_members.count).to eq(1) + expect(ActionCableBroadcastJob).to receive(:perform_later).with( + [agent.pubsub_token, admin.pubsub_token, conversation.contact.pubsub_token], + 'conversation.typing_on', conversation: conversation.push_event_data, + user: agent.push_event_data + ) + listener.conversation_typing_on(event) + end + end + + describe '#typing_off' do + let(:event_name) { :'conversation.typing_off' } + let!(:event) { Events::Base.new(event_name, Time.zone.now, conversation: conversation, user: agent) } + + it 'sends message to account admins, inbox agents and the contact' do + # HACK: to reload conversation inbox members + expect(conversation.inbox.reload.inbox_members.count).to eq(1) + expect(ActionCableBroadcastJob).to receive(:perform_later).with( + [agent.pubsub_token, admin.pubsub_token, conversation.contact.pubsub_token], + 'conversation.typing_off', conversation: conversation.push_event_data, + user: agent.push_event_data + ) + listener.conversation_typing_off(event) + end + end end