From b1aab228ae609485847ceaa0dd0ffacab71978d8 Mon Sep 17 00:00:00 2001 From: Abdulkadir Poyraz Date: Tue, 26 May 2020 15:13:59 +0300 Subject: [PATCH] Feature: Ability to mute contacts (#891) fixes: #867 --- .rubocop.yml | 2 ++ .../v1/accounts/conversations_controller.rb | 5 +++ .../dashboard/api/inbox/conversation.js | 4 +++ .../dashboard/i18n/locale/en/contact.json | 3 +- .../dashboard/conversation/ContactPanel.vue | 16 ++++++++++ .../store/modules/conversations/actions.js | 9 ++++++ .../store/modules/conversations/index.js | 7 +++++ .../specs/conversations/actions.spec.js | 12 +++++++ .../dashboard/store/mutation-types.js | 1 + app/models/conversation.rb | 17 ++++++++++ app/models/message.rb | 2 +- .../partials/_conversation.json.jbuilder | 1 + config/routes.rb | 1 + .../accounts/conversations_controller_spec.rb | 26 ++++++++++++++++ .../api/v1/widget/messages_controller_spec.rb | 13 ++++++++ spec/models/conversation_spec.rb | 31 +++++++++++++++++++ 16 files changed, 148 insertions(+), 2 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 9ea8ca106..fac44c67a 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -12,6 +12,8 @@ Layout/LineLength: Max: 150 Metrics/ClassLength: Max: 125 + Exclude: + - 'app/models/conversation.rb' RSpec/ExampleLength: Max: 25 Style/Documentation: diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb index 05eaa5861..8008217aa 100644 --- a/app/controllers/api/v1/accounts/conversations_controller.rb +++ b/app/controllers/api/v1/accounts/conversations_controller.rb @@ -20,6 +20,11 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController def show; end + def mute + @conversation.mute! + head :ok + end + def toggle_status @status = @conversation.toggle_status end diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 18ca9a60b..9a50ef1c8 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -39,6 +39,10 @@ class ConversationApi extends ApiClient { typing_status: status, }); } + + mute(conversationId) { + return axios.post(`${this.url}/${conversationId}/mute`); + } } export default new ConversationApi(); diff --git a/app/javascript/dashboard/i18n/locale/en/contact.json b/app/javascript/dashboard/i18n/locale/en/contact.json index dab6c730f..eb7b2d354 100644 --- a/app/javascript/dashboard/i18n/locale/en/contact.json +++ b/app/javascript/dashboard/i18n/locale/en/contact.json @@ -15,6 +15,7 @@ "UPDATE_ERROR": "Couldn't update labels, try again.", "TAG_PLACEHOLDER": "Add new label", "PLACEHOLDER": "Search or add a label" - } + }, + "MUTE_CONTACT": "Mute Contact" } } diff --git a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue index 636862232..24ce51d2c 100644 --- a/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue +++ b/app/javascript/dashboard/routes/dashboard/conversation/ContactPanel.vue @@ -90,10 +90,14 @@ icon="ion-clock" /> + + {{ $t('CONTACT_PANEL.MUTE_CONTACT') }} + @@ -248,4 +258,10 @@ export default { padding: 0.2rem; } } + +.contact--mute { + color: $alert-color; + display: block; + text-align: center; +} diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index b06312e0e..683e2a0f7 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -215,6 +215,15 @@ const actions = { // Handle error } }, + + muteConversation: async ({ commit }, conversationId) => { + try { + await ConversationApi.mute(conversationId); + commit(types.default.MUTE_CONVERSATION); + } catch (error) { + // + } + }, }; export default actions; diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index 9c9a0f22b..d016a55ef 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -10,6 +10,7 @@ const initialSelectedChat = { id: null, meta: {}, status: null, + muted: false, seen: false, agentTyping: 'off', dataFetched: false, @@ -116,6 +117,12 @@ const mutations = { _state.selectedChat.status = status; }, + [types.default.MUTE_CONVERSATION](_state) { + const [chat] = getSelectedChatConversation(_state); + chat.muted = true; + _state.selectedChat.muted = true; + }, + [types.default.SEND_MESSAGE](_state, currentMessage) { const [chat] = getSelectedChatConversation(_state); const allMessagesExceptCurrent = (chat.messages || []).filter( diff --git a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js index 306b87bbb..7898822aa 100644 --- a/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js +++ b/app/javascript/dashboard/store/modules/specs/conversations/actions.spec.js @@ -21,4 +21,16 @@ describe('#actions', () => { expect(commit.mock.calls).toEqual([]); }); }); + describe('#muteConversation', () => { + it('sends correct actions if API is success', async () => { + axios.get.mockResolvedValue(null); + await actions.muteConversation({ commit }, 1); + expect(commit.mock.calls).toEqual([[types.default.MUTE_CONVERSATION]]); + }); + it('sends correct actions if API is error', async () => { + axios.get.mockRejectedValue({ message: 'Incorrect header' }); + await actions.getConversation({ commit }); + expect(commit.mock.calls).toEqual([]); + }); + }); }); diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js index e6ee84775..d2df7aa7d 100755 --- a/app/javascript/dashboard/store/mutation-types.js +++ b/app/javascript/dashboard/store/mutation-types.js @@ -22,6 +22,7 @@ export default { RESOLVE_CONVERSATION: 'RESOLVE_CONVERSATION', ADD_CONVERSATION: 'ADD_CONVERSATION', UPDATE_CONVERSATION: 'UPDATE_CONVERSATION', + MUTE_CONVERSATION: 'MUTE_CONVERSATION', SEND_MESSAGE: 'SEND_MESSAGE', ASSIGN_AGENT: 'ASSIGN_AGENT', SET_CHAT_META: 'SET_CHAT_META', diff --git a/app/models/conversation.rb b/app/models/conversation.rb index d72202a98..59e9a853f 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -74,6 +74,15 @@ class Conversation < ApplicationRecord save end + def mute! + resolved! + Redis::Alfred.setex(mute_key, 1, mute_period) + end + + def muted? + !Redis::Alfred.get(mute_key).nil? + end + def lock! update!(locked: true) end @@ -184,4 +193,12 @@ class Conversation < ApplicationRecord messages.create(activity_message_params(content)) end + + def mute_key + format('CONVERSATION::%d::MUTED', id: id) + end + + def mute_period + 6.hours + end end diff --git a/app/models/message.rb b/app/models/message.rb index 47ea8c425..696ae5a2b 100644 --- a/app/models/message.rb +++ b/app/models/message.rb @@ -141,7 +141,7 @@ class Message < ApplicationRecord end def reopen_conversation - conversation.open! if incoming? && conversation.resolved? + conversation.open! if incoming? && conversation.resolved? && !conversation.muted? end def execute_message_template_hooks diff --git a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder index 4f511d3ba..148996898 100644 --- a/app/views/api/v1/conversations/partials/_conversation.json.jbuilder +++ b/app/views/api/v1/conversations/partials/_conversation.json.jbuilder @@ -17,6 +17,7 @@ end json.inbox_id conversation.inbox_id json.status conversation.status +json.muted conversation.muted? json.timestamp conversation.messages.last.try(:created_at).try(:to_i) json.user_last_seen_at conversation.user_last_seen_at.to_i json.agent_last_seen_at conversation.agent_last_seen_at.to_i diff --git a/config/routes.rb b/config/routes.rb index efb621ffe..d2920ed00 100644 --- a/config/routes.rb +++ b/config/routes.rb @@ -48,6 +48,7 @@ Rails.application.routes.draw do resources :labels, only: [:create, :index] end member do + post :mute post :toggle_status post :toggle_typing_status post :update_last_seen diff --git a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb index 525348d2d..49717b057 100644 --- a/spec/controllers/api/v1/accounts/conversations_controller_spec.rb +++ b/spec/controllers/api/v1/accounts/conversations_controller_spec.rb @@ -177,4 +177,30 @@ RSpec.describe 'Conversations API', type: :request do end end end + + describe 'POST /api/v1/accounts/{account.id}/conversations/:id/mute' 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}/mute" + + 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 'mutes conversation' do + post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/mute", + headers: agent.create_new_auth_token, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.resolved?).to eq(true) + expect(conversation.reload.muted?).to eq(true) + end + end + end end diff --git a/spec/controllers/api/v1/widget/messages_controller_spec.rb b/spec/controllers/api/v1/widget/messages_controller_spec.rb index 62c374295..e4983fa57 100644 --- a/spec/controllers/api/v1/widget/messages_controller_spec.rb +++ b/spec/controllers/api/v1/widget/messages_controller_spec.rb @@ -59,6 +59,19 @@ RSpec.describe '/api/v1/widget/messages', type: :request do expect(conversation.messages.last.attachments.first.file.present?).to eq(true) expect(conversation.messages.last.attachments.first.file_type).to eq('image') end + + it 'does not reopen conversation when conversation is muted' do + conversation.mute! + + message_params = { content: 'hello world', timestamp: Time.current } + post api_v1_widget_messages_url, + params: { website_token: web_widget.website_token, message: message_params }, + headers: { 'X-Auth-Token' => token }, + as: :json + + expect(response).to have_http_status(:success) + expect(conversation.reload.resolved?).to eq(true) + end end end diff --git a/spec/models/conversation_spec.rb b/spec/models/conversation_spec.rb index e69f8ea3c..73fb5800b 100644 --- a/spec/models/conversation_spec.rb +++ b/spec/models/conversation_spec.rb @@ -171,6 +171,37 @@ RSpec.describe Conversation, type: :model do end end + describe '#mute!' do + subject(:mute!) { conversation.mute! } + + let(:conversation) { create(:conversation) } + + it 'marks conversation as resolved' do + mute! + expect(conversation.reload.resolved?).to eq(true) + end + + it 'marks conversation as muted in redis' do + mute! + expect(Redis::Alfred.get(conversation.send(:mute_key))).not_to eq(nil) + end + end + + describe '#muted?' do + subject(:muted?) { conversation.muted? } + + let(:conversation) { create(:conversation) } + + it 'return true if conversation is muted' do + conversation.mute! + expect(muted?).to eq(true) + end + + it 'returns false if conversation is not muted' do + expect(muted?).to eq(false) + end + end + describe 'unread_messages' do subject(:unread_messages) { conversation.unread_messages }