feat: Ability to unmute muted conversations (#1319)

This commit is contained in:
Dmitriy Shcherbakan 2020-10-08 09:32:08 +03:00 committed by GitHub
parent 2aad33a5be
commit ecebe163e1
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 242 additions and 0 deletions

View file

@ -32,6 +32,11 @@ class Api::V1::Accounts::ConversationsController < Api::V1::Accounts::BaseContro
head :ok
end
def unmute
@conversation.unmute!
head :ok
end
def transcript
ConversationReplyMailer.conversation_transcript(@conversation, params[:email])&.deliver_later if params[:email].present?
head :ok

View file

@ -43,6 +43,10 @@ class ConversationApi extends ApiClient {
return axios.post(`${this.url}/${conversationId}/mute`);
}
unmute(conversationId) {
return axios.post(`${this.url}/${conversationId}/unmute`);
}
meta({ inboxId, status, assigneeType, labels }) {
return axios.get(`${this.url}/meta`, {
params: {

View file

@ -14,7 +14,32 @@ describe('#ConversationAPI', () => {
expect(conversationAPI).toHaveProperty('markMessageRead');
expect(conversationAPI).toHaveProperty('toggleTyping');
expect(conversationAPI).toHaveProperty('mute');
expect(conversationAPI).toHaveProperty('unmute');
expect(conversationAPI).toHaveProperty('meta');
expect(conversationAPI).toHaveProperty('sendEmailTranscript');
});
describe('API calls', () => {
let originalAxios = null;
let axiosMock = null;
beforeEach(() => {
originalAxios = window.axios;
axiosMock = { post: jest.fn(() => Promise.resolve()) };
window.axios = axiosMock;
});
afterEach(() => {
window.axios = originalAxios;
});
it('#unmute', () => {
conversationAPI.unmute(45);
expect(axiosMock.post).toHaveBeenCalledWith(
'/api/v1/conversations/45/unmute'
);
});
});
});

View file

@ -22,6 +22,15 @@
>
<span>{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}</span>
</button>
<button
v-else
class="button small clear row alert small-6 action--button"
@click="unmute"
>
<span>{{ $t('CONTACT_PANEL.UNMUTE_CONTACT') }}</span>
</button>
<button
class="button small clear row small-6 action--button"
@click="toggleEmailActionsModal"
@ -67,6 +76,11 @@ export default {
this.showAlert(this.$t('CONTACT_PANEL.MUTED_SUCCESS'));
this.toggleConversationActions();
},
unmute() {
this.$store.dispatch('unmuteConversation', this.currentChat.id);
this.showAlert(this.$t('CONTACT_PANEL.UNMUTED_SUCCESS'));
this.toggleConversationActions();
},
toggleEmailActionsModal() {
this.showEmailActionsModal = !this.showEmailActionsModal;
this.hideConversationActions();
@ -129,6 +143,7 @@ export default {
display: flex;
align-items: center;
width: 100%;
white-space: nowrap;
padding: var(--space-small) var(--space-smaller);
font-size: var(--font-size-small);

View file

@ -0,0 +1,130 @@
import { createLocalVue, mount } from '@vue/test-utils';
import Vuex from 'vuex';
import VueI18n from 'vue-i18n';
import Button from 'dashboard/components/buttons/Button';
import i18n from 'dashboard/i18n';
import MoreActions from '../MoreActions';
const localVue = createLocalVue();
localVue.use(Vuex);
localVue.use(VueI18n);
localVue.locale('en', i18n.en);
localVue.component('woot-button', Button);
describe('MoveActions', () => {
let currentChat = { id: 8, muted: false };
let state = null;
let muteConversation = null;
let unmuteConversation = null;
let modules = null;
let getters = null;
let store = null;
let moreActions = null;
beforeEach(() => {
window.bus = {
$emit: jest.fn(),
};
state = {
authenticated: true,
currentChat,
};
muteConversation = jest.fn(() => Promise.resolve());
unmuteConversation = jest.fn(() => Promise.resolve());
modules = {
conversations: {
actions: {
muteConversation,
unmuteConversation,
},
},
};
getters = {
getSelectedChat: () => currentChat,
};
store = new Vuex.Store({
state,
modules,
getters,
});
moreActions = mount(MoreActions, { store, localVue });
});
it('opens the menu when user clicks "more"', async () => {
expect(moreActions.find('.dropdown-pane').exists()).toBe(false);
await moreActions.find('.more--button').trigger('click');
expect(moreActions.find('.dropdown-pane').exists()).toBe(true);
});
describe('muting discussion', () => {
it('triggers "muteConversation"', async () => {
await moreActions.find('.more--button').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(muteConversation).toBeCalledWith(
expect.any(Object),
currentChat.id,
undefined
);
});
it('shows alert', async () => {
await moreActions.find('.more--button').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(window.bus.$emit).toBeCalledWith(
'newToastMessage',
'This conversation is muted for 6 hours'
);
});
});
describe('unmuting discussion', () => {
beforeEach(() => {
currentChat.muted = true;
});
it('triggers "unmuteConversation"', async () => {
await moreActions.find('.more--button').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(unmuteConversation).toBeCalledWith(
expect.any(Object),
currentChat.id,
undefined
);
});
it('shows alert', async () => {
await moreActions.find('.more--button').trigger('click');
await moreActions
.find('.dropdown-pane button:first-child')
.trigger('click');
expect(window.bus.$emit).toBeCalledWith(
'newToastMessage',
'This conversation is unmuted'
);
});
});
});

View file

@ -32,7 +32,9 @@
"NO_AVAILABLE_LABELS": "There are no labels added to this conversation."
},
"MUTE_CONTACT": "Mute Conversation",
"UNMUTE_CONTACT": "Unmute Conversation",
"MUTED_SUCCESS": "This conversation is muted for 6 hours",
"UNMUTED_SUCCESS": "This conversation is unmuted",
"SEND_TRANSCRIPT": "Send Transcript",
"EDIT_LABEL": "Edit"
},

View file

@ -224,6 +224,15 @@ const actions = {
}
},
unmuteConversation: async ({ commit }, conversationId) => {
try {
await ConversationApi.unmute(conversationId);
commit(types.default.UNMUTE_CONVERSATION);
} catch (error) {
//
}
},
sendEmailTranscript: async (_, { conversationId, email }) => {
try {
await ConversationApi.sendEmailTranscript({ conversationId, email });

View file

@ -73,6 +73,11 @@ export const mutations = {
chat.muted = true;
},
[types.default.UNMUTE_CONVERSATION](_state) {
const [chat] = getSelectedChatConversation(_state);
chat.muted = false;
},
[types.default.SEND_MESSAGE](_state, currentMessage) {
const [chat] = getSelectedChatConversation(_state);
const allMessagesExceptCurrent = (chat.messages || []).filter(

View file

@ -25,6 +25,7 @@ export default {
ADD_CONVERSATION: 'ADD_CONVERSATION',
UPDATE_CONVERSATION: 'UPDATE_CONVERSATION',
MUTE_CONVERSATION: 'MUTE_CONVERSATION',
UNMUTE_CONVERSATION: 'UNMUTE_CONVERSATION',
SEND_MESSAGE: 'SEND_MESSAGE',
ASSIGN_AGENT: 'ASSIGN_AGENT',
SET_CHAT_META: 'SET_CHAT_META',

View file

@ -89,6 +89,10 @@ class Conversation < ApplicationRecord
Redis::Alfred.setex(mute_key, 1, mute_period)
end
def unmute!
Redis::Alfred.delete(mute_key)
end
def muted?
!Redis::Alfred.get(mute_key).nil?
end

View file

@ -55,6 +55,7 @@ Rails.application.routes.draw do
end
member do
post :mute
post :unmute
post :transcript
post :toggle_status
post :toggle_typing_status

View file

@ -246,6 +246,31 @@ RSpec.describe 'Conversations API', type: :request do
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/unmute' do
let(:conversation) { create(:conversation, account: account).tap(&:mute!) }
context 'when it is an unauthenticated user' do
it 'returns unauthorized' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unmute"
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 'unmutes conversation' do
post "/api/v1/accounts/#{account.id}/conversations/#{conversation.display_id}/unmute",
headers: agent.create_new_auth_token,
as: :json
expect(response).to have_http_status(:success)
expect(conversation.reload.muted?).to eq(false)
end
end
end
describe 'POST /api/v1/accounts/{account.id}/conversations/:id/transcript' do
let(:conversation) { create(:conversation, account: account) }

View file

@ -289,6 +289,22 @@ RSpec.describe Conversation, type: :model do
end
end
describe '#unmute!' do
subject(:unmute!) { conversation.unmute! }
let(:conversation) { create(:conversation).tap(&:mute!) }
it 'does not change conversation status' do
expect { unmute! }.not_to(change { conversation.reload.status })
end
it 'marks conversation as muted in redis' do
expect { unmute! }
.to change { Redis::Alfred.get(conversation.send(:mute_key)) }
.to nil
end
end
describe '#muted?' do
subject(:muted?) { conversation.muted? }