diff --git a/Gemfile b/Gemfile index 4225f2904..957784f55 100644 --- a/Gemfile +++ b/Gemfile @@ -100,7 +100,7 @@ group :development, :test do gem 'factory_bot_rails' gem 'faker' gem 'listen' - gem 'mock_redis' + gem 'mock_redis', git: 'https://github.com/sds/mock_redis', ref: '16d00789f0341a3aac35126c0ffe97a596753ff9' gem 'pry-rails' gem 'rspec-rails', '~> 4.0.0.beta2' gem 'rubocop', require: false diff --git a/Gemfile.lock b/Gemfile.lock index 6c9edc7ef..bf9a33cfc 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -5,6 +5,13 @@ GIT twitty (0.1.0) oauth +GIT + remote: https://github.com/sds/mock_redis + revision: 16d00789f0341a3aac35126c0ffe97a596753ff9 + ref: 16d00789f0341a3aac35126c0ffe97a596753ff9 + specs: + mock_redis (0.22.0) + GIT remote: https://github.com/tzmfreedom/json_refs revision: e32deb073ce9aef39bdd63556bffd7fe7c2a803d @@ -270,7 +277,6 @@ GEM mini_mime (1.0.2) mini_portile2 (2.4.0) minitest (5.14.0) - mock_redis (0.22.0) msgpack (1.3.3) multi_json (1.14.1) multi_xml (0.6.0) @@ -526,7 +532,7 @@ DEPENDENCIES letter_opener listen mini_magick - mock_redis + mock_redis! nightfury pg pry-rails diff --git a/app/javascript/dashboard/helper/actionCable.js b/app/javascript/dashboard/helper/actionCable.js index 38500f16b..a1f70702e 100644 --- a/app/javascript/dashboard/helper/actionCable.js +++ b/app/javascript/dashboard/helper/actionCable.js @@ -8,7 +8,8 @@ class ActionCableConnector extends BaseActionCableConnector { 'message.created': this.onMessageCreated, 'message.updated': this.onMessageUpdated, 'conversation.created': this.onConversationCreated, - 'status_change:conversation': this.onStatusChange, + 'conversation.opened': this.onStatusChange, + 'conversation.resolved': this.onStatusChange, 'user:logout': this.onLogout, 'page:reload': this.onReload, 'assignee.changed': this.onAssigneeChanged, @@ -40,7 +41,7 @@ class ActionCableConnector extends BaseActionCableConnector { onReload = () => window.location.reload(); onStatusChange = data => { - this.app.$store.dispatch('addConversation', data); + this.app.$store.dispatch('updateConversation', data); }; } diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index c770c6a46..1ae9ba7e6 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -149,8 +149,15 @@ const actions = { commit(types.default.ADD_MESSAGE, message); }, - addConversation({ commit }, conversation) { - commit(types.default.ADD_CONVERSATION, conversation); + addConversation({ commit, state }, conversation) { + const { currentInbox } = state; + if (!currentInbox || Number(currentInbox) === conversation.inbox_id) { + commit(types.default.ADD_CONVERSATION, conversation); + } + }, + + updateConversation({ commit }, conversation) { + commit(types.default.UPDATE_CONVERSATION, conversation); }, toggleTyping: async ({ commit }, { status, inboxId, contactId }) => { diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index f61c3add4..80294456a 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -153,6 +153,24 @@ const mutations = { _state.allConversations.push(conversation); }, + [types.default.UPDATE_CONVERSATION](_state, conversation) { + const { allConversations } = _state; + const currentConversationIndex = allConversations.findIndex( + c => c.id === conversation.id + ); + if (currentConversationIndex > -1) { + const currentConversation = { + ...allConversations[currentConversationIndex], + status: conversation.status, + }; + Vue.set(allConversations, currentConversationIndex, currentConversation); + if (_state.selectedChat.id === conversation.id) { + _state.selectedChat.status = conversation.status; + window.bus.$emit('scrollToMessage'); + } + } + }, + [types.default.MARK_SEEN](_state) { _state.selectedChat.seen = true; }, diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index 93b252389..6d3e626e7 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -21,6 +21,7 @@ export default { CLEAR_ALL_MESSAGES: 'CLEAR_ALL_MESSAGES', RESOLVE_CONVERSATION: 'RESOLVE_CONVERSATION', ADD_CONVERSATION: 'ADD_CONVERSATION', + UPDATE_CONVERSATION: 'UPDATE_CONVERSATION', SEND_MESSAGE: 'SEND_MESSAGE', ASSIGN_AGENT: 'ASSIGN_AGENT', SET_CHAT_META: 'SET_CHAT_META', diff --git a/app/listeners/action_cable_listener.rb b/app/listeners/action_cable_listener.rb index 065124705..99c3bbce2 100644 --- a/app/listeners/action_cable_listener.rb +++ b/app/listeners/action_cable_listener.rb @@ -3,21 +3,21 @@ class ActionCableListener < BaseListener def conversation_created(event) conversation, account, timestamp = extract_conversation_and_account(event) - send_to_administrators(account.administrators, CONVERSATION_CREATED, conversation.push_event_data) - send_to_agents(conversation.inbox.members, CONVERSATION_CREATED, conversation.push_event_data) + + send_to_agents(account, conversation.inbox.members, CONVERSATION_CREATED, conversation.push_event_data) end def conversation_read(event) conversation, account, timestamp = extract_conversation_and_account(event) - send_to_administrators(account.administrators, CONVERSATION_READ, conversation.push_event_data) - send_to_agents(conversation.inbox.members, CONVERSATION_READ, conversation.push_event_data) + + send_to_agents(account, conversation.inbox.members, CONVERSATION_READ, conversation.push_event_data) end def message_created(event) message, account, timestamp = extract_message_and_account(event) conversation = message.conversation - send_to_administrators(account.administrators, MESSAGE_CREATED, message.push_event_data) - send_to_agents(conversation.inbox.members, MESSAGE_CREATED, message.push_event_data) + + send_to_agents(account, conversation.inbox.members, MESSAGE_CREATED, message.push_event_data) send_to_contact(conversation.contact, MESSAGE_CREATED, message) end @@ -25,55 +25,58 @@ class ActionCableListener < BaseListener message, account, timestamp = extract_message_and_account(event) conversation = message.conversation contact = conversation.contact - members = conversation.inbox.members.pluck(:pubsub_token) - send_to_members(members, MESSAGE_UPDATED, message.push_event_data) + + send_to_agents(account, conversation.inbox.members, MESSAGE_UPDATED, message.push_event_data) send_to_contact(contact, MESSAGE_UPDATED, message) end - def conversation_reopened(event) + def conversation_resolved(event) conversation, account, timestamp = extract_conversation_and_account(event) - send_to_administrators(account.administrators, CONVERSATION_REOPENED, conversation.push_event_data) - send_to_agents(conversation.inbox.members, CONVERSATION_REOPENED, conversation.push_event_data) + + send_to_agents(account, conversation.inbox.members, CONVERSATION_RESOLVED, conversation.push_event_data) + end + + def conversation_opened(event) + conversation, account, timestamp = extract_conversation_and_account(event) + + send_to_agents(account, conversation.inbox.members, CONVERSATION_OPENED, conversation.push_event_data) end def conversation_lock_toggle(event) conversation, account, timestamp = extract_conversation_and_account(event) - send_to_administrators(account.administrators, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) - send_to_agents(conversation.inbox.members, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) + + send_to_agents(account, conversation.inbox.members, CONVERSATION_LOCK_TOGGLE, conversation.lock_event_data) end def assignee_changed(event) conversation, account, timestamp = extract_conversation_and_account(event) - send_to_administrators(account.administrators, ASSIGNEE_CHANGED, conversation.push_event_data) - send_to_agents(conversation.inbox.members, ASSIGNEE_CHANGED, conversation.push_event_data) + + send_to_agents(account, conversation.inbox.members, ASSIGNEE_CHANGED, conversation.push_event_data) end def contact_created(event) contact, account, timestamp = extract_contact_and_account(event) - send_to_administrators(account.administrators, CONTACT_CREATED, contact.push_event_data) - send_to_agents(account.agents, CONTACT_CREATED, contact.push_event_data) + + send_to_agents(account, account.agents, CONTACT_CREATED, contact.push_event_data) end def contact_updated(event) contact, account, timestamp = extract_contact_and_account(event) - send_to_administrators(account.administrators, CONTACT_UPDATED, contact.push_event_data) - send_to_agents(account.agents, CONTACT_UPDATED, contact.push_event_data) + + send_to_agents(account, account.agents, CONTACT_UPDATED, contact.push_event_data) end private - def send_to_administrators(admins, event_name, data) - admin_tokens = admins.pluck(:pubsub_token) - return if admin_tokens.blank? - - ::ActionCableBroadcastJob.perform_later(admin_tokens, event_name, data) - end - - def send_to_agents(agents, event_name, data) + def send_to_agents(account, agents, event_name, data) agent_tokens = agents.pluck(:pubsub_token) - return if agent_tokens.blank? + admin_tokens = account.administrators.pluck(:pubsub_token) - ::ActionCableBroadcastJob.perform_later(agent_tokens, event_name, data) + pubsub_tokens = (agent_tokens + admin_tokens).uniq + + return if pubsub_tokens.blank? + + ::ActionCableBroadcastJob.perform_later(pubsub_tokens, event_name, data) end def send_to_contact(contact, event_name, message) diff --git a/app/listeners/agent_bot_listener.rb b/app/listeners/agent_bot_listener.rb index 7155357c8..4c7b4a610 100644 --- a/app/listeners/agent_bot_listener.rb +++ b/app/listeners/agent_bot_listener.rb @@ -1,4 +1,28 @@ class AgentBotListener < BaseListener + def conversation_resolved(event) + conversation = extract_conversation_and_account(event)[0] + inbox = conversation.inbox + return if inbox.agent_bot_inbox.blank? + return unless inbox.agent_bot_inbox.active? + + agent_bot = inbox.agent_bot_inbox.agent_bot + + payload = conversation.webhook_data.merge(event: __method__.to_s) + AgentBotJob.perform_later(agent_bot.outgoing_url, payload) + end + + def conversation_opened(event) + conversation = extract_conversation_and_account(event)[0] + inbox = conversation.inbox + return if inbox.agent_bot_inbox.blank? + return unless inbox.agent_bot_inbox.active? + + agent_bot = inbox.agent_bot_inbox.agent_bot + + payload = conversation.webhook_data.merge(event: __method__.to_s) + AgentBotJob.perform_later(agent_bot.outgoing_url, payload) + end + def message_created(event) message = extract_message_and_account(event)[0] inbox = message.inbox diff --git a/app/listeners/reporting_listener.rb b/app/listeners/reporting_listener.rb index 14027783f..2f4e069e0 100644 --- a/app/listeners/reporting_listener.rb +++ b/app/listeners/reporting_listener.rb @@ -7,9 +7,13 @@ class ReportingListener < BaseListener def conversation_resolved(event) conversation, account, timestamp = extract_conversation_and_account(event) time_to_resolve = conversation.updated_at.to_i - conversation.created_at.to_i - 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 + + 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 diff --git a/app/listeners/webhook_listener.rb b/app/listeners/webhook_listener.rb index 3597a402e..1244df148 100644 --- a/app/listeners/webhook_listener.rb +++ b/app/listeners/webhook_listener.rb @@ -1,4 +1,18 @@ class WebhookListener < BaseListener + def conversation_resolved(event) + conversation = extract_conversation_and_account(event)[0] + inbox = conversation.inbox + payload = conversation.webhook_data.merge(event: __method__.to_s) + deliver_webhook_payloads(payload, inbox) + end + + def conversation_opened(event) + conversation = extract_conversation_and_account(event)[0] + inbox = conversation.inbox + payload = conversation.webhook_data.merge(event: __method__.to_s) + deliver_webhook_payloads(payload, inbox) + end + def message_created(event) message = extract_message_and_account(event)[0] inbox = message.inbox diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 75ac69f5c..338af058a 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -54,7 +54,7 @@ class Conversation < ApplicationRecord after_update :notify_status_change, :create_activity, :send_email_notification_to_assignee - after_create :send_events, :run_round_robin + after_create :notify_conversation_creation, :run_round_robin acts_as_taggable_on :labels @@ -110,11 +110,7 @@ class Conversation < ApplicationRecord self.status = :bot if inbox.agent_bot_inbox&.active? end - def dispatch_events - dispatcher_dispatch(CONVERSATION_RESOLVED) - end - - def send_events + def notify_conversation_creation dispatcher_dispatch(CONVERSATION_CREATED) end @@ -160,7 +156,8 @@ class Conversation < ApplicationRecord def notify_status_change { - CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? && assignee.present? }, + CONVERSATION_OPENED => -> { saved_change_to_status? && open? }, + CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? }, CONVERSATION_READ => -> { saved_change_to_user_last_seen_at? }, CONVERSATION_LOCK_TOGGLE => -> { saved_change_to_locked? }, ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? } diff --git a/app/models/message.rb b/app/models/message.rb index 218059537..51210686e 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -139,10 +139,7 @@ class Message < ApplicationRecord end def reopen_conversation - if incoming? && conversation.resolved? - conversation.toggle_status - Rails.configuration.dispatcher.dispatch(CONVERSATION_REOPENED, Time.zone.now, conversation: conversation) - end + conversation.open! if incoming? && conversation.resolved? end def execute_message_template_hooks diff --git a/lib/events/types.rb b/lib/events/types.rb index 4adb3915f..422820720 100644 --- a/lib/events/types.rb +++ b/lib/events/types.rb @@ -1,27 +1,33 @@ # frozen_string_literal: true module Events::Types - CONVERSATION_CREATED = 'conversation.created' - CONVERSATION_RESOLVED = 'conversation.resolved' - CONVERSATION_READ = 'conversation.read' - WEBWIDGET_TRIGGERED = 'webwidget.triggered' - - MESSAGE_CREATED = 'message.created' - FIRST_REPLY_CREATED = 'first.reply.created' - MESSAGE_UPDATED = 'message.updated' - CONVERSATION_REOPENED = 'conversation.reopened' - CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle' - ASSIGNEE_CHANGED = 'assignee.changed' - - CONTACT_CREATED = 'contact.created' - CONTACT_UPDATED = 'contact.updated' - + # account events ACCOUNT_CREATED = 'account.created' ACCOUNT_DESTROYED = 'account.destroyed' + # channel events + WEBWIDGET_TRIGGERED = 'webwidget.triggered' + + # conversation events + CONVERSATION_CREATED = 'conversation.created' + CONVERSATION_READ = 'conversation.read' + CONVERSATION_OPENED = 'conversation.opened' + CONVERSATION_RESOLVED = 'conversation.resolved' + CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle' + ASSIGNEE_CHANGED = 'assignee.changed' + + # message events + MESSAGE_CREATED = 'message.created' + FIRST_REPLY_CREATED = 'first.reply.created' + MESSAGE_UPDATED = 'message.updated' + + # contact events + CONTACT_CREATED = 'contact.created' + CONTACT_UPDATED = 'contact.updated' + + # subscription events AGENT_ADDED = 'agent.added' AGENT_REMOVED = 'agent.removed' - SUBSCRIPTION_CREATED = 'subscription.created' SUBSCRIPTION_REACTIVATED = 'subscription.reactivated' SUBSCRIPTION_DEACTIVATED = 'subscription.deactivated' diff --git a/spec/listeners/action_cable_listener_spec.rb b/spec/listeners/action_cable_listener_spec.rb index 4b85f3084..9dffe94a1 100644 --- a/spec/listeners/action_cable_listener_spec.rb +++ b/spec/listeners/action_cable_listener_spec.rb @@ -20,9 +20,12 @@ describe ActionCableListener do let(:event_name) { :'message.created' } it 'sends message to account admins, inbox agents and the contact' do - expect(ActionCableBroadcastJob).to receive(:perform_later).with([admin.pubsub_token], 'message.created', message.push_event_data) - expect(ActionCableBroadcastJob).to receive(:perform_later).with([agent.pubsub_token], 'message.created', message.push_event_data) - expect(ActionCableBroadcastJob).to receive(:perform_later).with([conversation.contact.pubsub_token], 'message.created', message.push_event_data) + expect(ActionCableBroadcastJob).to receive(:perform_later).with( + [agent.pubsub_token, admin.pubsub_token], 'message.created', message.push_event_data + ) + expect(ActionCableBroadcastJob).to receive(:perform_later).with( + [conversation.contact.pubsub_token], 'message.created', message.push_event_data + ) listener.message_created(event) end end