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 }