feat: End conversation from widget (#3660)
Co-authored-by: Sivin Varghese <64252451+iamsivin@users.noreply.github.com> Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com> Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com> Co-authored-by: Nithin David Thomas <1277421+nithindavid@users.noreply.github.com> Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
4b748e2c8c
commit
c4837cd7ac
19 changed files with 263 additions and 18 deletions
|
@ -34,6 +34,8 @@ class Api::V1::Widget::BaseController < ApplicationController
|
|||
)
|
||||
@contact = @contact_inbox&.contact
|
||||
raise ActiveRecord::RecordNotFound unless @contact
|
||||
|
||||
Current.contact = @contact
|
||||
end
|
||||
|
||||
def create_conversation
|
||||
|
|
|
@ -44,6 +44,15 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||
head :ok
|
||||
end
|
||||
|
||||
def toggle_status
|
||||
head :not_found && return if conversation.nil?
|
||||
unless conversation.resolved?
|
||||
conversation.status = 'resolved'
|
||||
conversation.save
|
||||
end
|
||||
head :ok
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def trigger_typing_event(event)
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
|
||||
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
|
||||
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
|
||||
"checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
|
||||
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
|
||||
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
|
||||
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
|
||||
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",
|
||||
"emoji-outline": "M12 1.999c5.524 0 10.002 4.478 10.002 10.002 0 5.523-4.478 10.001-10.002 10.001-5.524 0-10.002-4.478-10.002-10.001C1.998 6.477 6.476 1.999 12 1.999Zm0 1.5a8.502 8.502 0 1 0 0 17.003A8.502 8.502 0 0 0 12 3.5ZM8.462 14.784A4.491 4.491 0 0 0 12 16.502a4.492 4.492 0 0 0 3.535-1.714.75.75 0 1 1 1.177.93A5.991 5.991 0 0 1 12 18.002a5.991 5.991 0 0 1-4.716-2.29.75.75 0 0 1 1.178-.928ZM9 8.75a1.25 1.25 0 1 1 0 2.499A1.25 1.25 0 0 1 9 8.75Zm6 0a1.25 1.25 0 1 1 0 2.499 1.25 1.25 0 0 1 0-2.499Z",
|
||||
"link-outline": "M9.25 7a.75.75 0 0 1 .11 1.492l-.11.008H7a3.5 3.5 0 0 0-.206 6.994L7 15.5h2.25a.75.75 0 0 1 .11 1.492L9.25 17H7a5 5 0 0 1-.25-9.994L7 7h2.25ZM17 7a5 5 0 0 1 .25 9.994L17 17h-2.25a.75.75 0 0 1-.11-1.492l.11-.008H17a3.5 3.5 0 0 0 .206-6.994L17 8.5h-2.25a.75.75 0 0 1-.11-1.492L14.75 7H17ZM7 11.25h10a.75.75 0 0 1 .102 1.493L17 12.75H7a.75.75 0 0 1-.102-1.493L7 11.25h10H7Z",
|
||||
"more-vertical-outline": "M12 7.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM12 13.75a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5ZM10.25 18a1.75 1.75 0 1 0 3.5 0 1.75 1.75 0 0 0-3.5 0Z",
|
||||
"open-outline": "M6.25 4.5A1.75 1.75 0 0 0 4.5 6.25v11.5c0 .966.783 1.75 1.75 1.75h11.5a1.75 1.75 0 0 0 1.75-1.75v-4a.75.75 0 0 1 1.5 0v4A3.25 3.25 0 0 1 17.75 21H6.25A3.25 3.25 0 0 1 3 17.75V6.25A3.25 3.25 0 0 1 6.25 3h4a.75.75 0 0 1 0 1.5h-4ZM13 3.75a.75.75 0 0 1 .75-.75h6.5a.75.75 0 0 1 .75.75v6.5a.75.75 0 0 1-1.5 0V5.56l-5.22 5.22a.75.75 0 0 1-1.06-1.06l5.22-5.22h-4.69a.75.75 0 0 1-.75-.75Z",
|
||||
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z"
|
||||
"send-outline": "M5.694 12 2.299 3.272c-.236-.607.356-1.188.942-.982l.093.04 18 9a.75.75 0 0 1 .097 1.283l-.097.058-18 9c-.583.291-1.217-.244-1.065-.847l.03-.096L5.694 12 2.299 3.272 5.694 12ZM4.402 4.54l2.61 6.71h6.627a.75.75 0 0 1 .743.648l.007.102a.75.75 0 0 1-.649.743l-.101.007H7.01l-2.609 6.71L19.322 12 4.401 4.54Z",
|
||||
"sign-out-outline": ["M8.502 11.5a1.002 1.002 0 1 1 0 2.004 1.002 1.002 0 0 1 0-2.004Z","M12 4.354v6.651l7.442-.001L17.72 9.28a.75.75 0 0 1-.073-.976l.073-.084a.75.75 0 0 1 .976-.073l.084.073 2.997 2.997a.75.75 0 0 1 .073.976l-.073.084-2.996 3.004a.75.75 0 0 1-1.134-.975l.072-.085 1.713-1.717-7.431.001L12 19.25a.75.75 0 0 1-.88.739l-8.5-1.502A.75.75 0 0 1 2 17.75V5.75a.75.75 0 0 1 .628-.74l8.5-1.396a.75.75 0 0 1 .872.74Zm-1.5.883-7 1.15V17.12l7 1.236V5.237Z","M13 18.501h.765l.102-.006a.75.75 0 0 0 .648-.745l-.007-4.25H13v5.001ZM13.002 10 13 8.725V5h.745a.75.75 0 0 1 .743.647l.007.102.007 4.251h-1.5Z"]
|
||||
}
|
||||
|
|
|
@ -48,6 +48,11 @@ const sendEmailTranscript = async ({ email }) => {
|
|||
{ email }
|
||||
);
|
||||
};
|
||||
const toggleStatus = async () => {
|
||||
return API.get(
|
||||
`/api/v1/widget/conversations/toggle_status${window.location.search}`
|
||||
);
|
||||
};
|
||||
|
||||
export {
|
||||
createConversationAPI,
|
||||
|
@ -58,4 +63,5 @@ export {
|
|||
toggleTyping,
|
||||
setUserLastSeenAt,
|
||||
sendEmailTranscript,
|
||||
toggleStatus,
|
||||
};
|
||||
|
|
|
@ -1,5 +1,13 @@
|
|||
<template>
|
||||
<div v-if="showHeaderActions" class="actions flex items-center">
|
||||
<button
|
||||
v-if="conversationStatus === 'open'"
|
||||
class="button transparent compact"
|
||||
:title="$t('END_CONVERSATION')"
|
||||
@click="resolveConversation"
|
||||
>
|
||||
<fluent-icon icon="sign-out" size="22" class="text-black-900" />
|
||||
</button>
|
||||
<button
|
||||
v-if="showPopoutButton"
|
||||
class="button transparent compact new-window--button "
|
||||
|
@ -19,6 +27,7 @@
|
|||
</div>
|
||||
</template>
|
||||
<script>
|
||||
import { mapGetters } from 'vuex';
|
||||
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
|
||||
import { buildPopoutURL } from '../helpers/urlParamsHelper';
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
@ -33,6 +42,9 @@ export default {
|
|||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||
}),
|
||||
isIframe() {
|
||||
return IFrameHelper.isIFrame();
|
||||
},
|
||||
|
@ -40,7 +52,13 @@ export default {
|
|||
return RNHelper.isRNWebView();
|
||||
},
|
||||
showHeaderActions() {
|
||||
return this.isIframe || this.isRNWebView;
|
||||
return this.isIframe || this.isRNWebView || this.hasWidgetOptions;
|
||||
},
|
||||
conversationStatus() {
|
||||
return this.conversationAttributes.status;
|
||||
},
|
||||
hasWidgetOptions() {
|
||||
return this.showPopoutButton || this.conversationStatus === 'open';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -72,6 +90,9 @@ export default {
|
|||
RNHelper.sendMessage({ type: 'close-widget' });
|
||||
}
|
||||
},
|
||||
resolveConversation() {
|
||||
this.$store.dispatch('conversation/resolveConversation');
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
|
|
83
app/javascript/widget/components/dropdown/DropdownMenu.vue
Normal file
83
app/javascript/widget/components/dropdown/DropdownMenu.vue
Normal file
|
@ -0,0 +1,83 @@
|
|||
<template>
|
||||
<div class="relative">
|
||||
<button class="z-10 focus:outline-none select-none" @click="toggleMenu">
|
||||
<slot name="button"></slot>
|
||||
</button>
|
||||
|
||||
<!-- to close when clicked on space around it-->
|
||||
<button
|
||||
v-if="isOpen"
|
||||
tabindex="-1"
|
||||
class="fixed inset-0 h-full w-full cursor-default focus:outline-none"
|
||||
@click="toggleMenu"
|
||||
></button>
|
||||
|
||||
<!--dropdown menu-->
|
||||
<transition
|
||||
enter-active-class="transition-all duration-200 ease-out"
|
||||
leave-active-class="transition-all duration-750 ease-in"
|
||||
enter-class="opacity-0 scale-75"
|
||||
enter-to-class="opacity-100 scale-100"
|
||||
leave-class="opacity-100 scale-100"
|
||||
leave-to-class="opacity-0 scale-75"
|
||||
>
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="menu-content absolute shadow-xl rounded-md border-solid border border-slate-100 mt-1 py-1 px-2 bg-white z-10"
|
||||
:class="menuPlacement === 'right' ? 'right-0' : 'left-0'"
|
||||
>
|
||||
<slot name="content"></slot>
|
||||
</div>
|
||||
</transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
props: {
|
||||
menuPlacement: {
|
||||
type: String,
|
||||
default: 'right',
|
||||
validator: value => ['right', 'left'].indexOf(value) !== -1,
|
||||
},
|
||||
open: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
toggleMenu: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
isOpen: false,
|
||||
};
|
||||
},
|
||||
watch: {
|
||||
open() {
|
||||
this.isOpen = !this.isOpen;
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
document.addEventListener('keydown', this.onEscape);
|
||||
},
|
||||
beforeDestroy() {
|
||||
document.removeEventListener('keydown', this.onEscape);
|
||||
},
|
||||
methods: {
|
||||
onEscape(e) {
|
||||
if (e.key === 'Esc' || e.key === 'Escape') {
|
||||
this.isOpen = false;
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.menu-content {
|
||||
width: max-content;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,67 @@
|
|||
<template>
|
||||
<button :class="['menu-item', itemClass]" @click="action">
|
||||
<fluent-icon
|
||||
v-if="icon"
|
||||
:icon="iconName"
|
||||
:size="iconSize"
|
||||
:class="iconClass"
|
||||
/>
|
||||
<span :class="[{ 'pl-3': icon }, textClass]">{{ text }}</span>
|
||||
</button>
|
||||
</template>
|
||||
<script>
|
||||
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
|
||||
|
||||
export default {
|
||||
components: { FluentIcon },
|
||||
props: {
|
||||
text: {
|
||||
type: String,
|
||||
default: 'Default',
|
||||
},
|
||||
textClass: {
|
||||
type: String,
|
||||
default: 'text-sm',
|
||||
},
|
||||
icon: {
|
||||
type: Boolean,
|
||||
default: true,
|
||||
},
|
||||
iconName: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: '15',
|
||||
},
|
||||
iconClass: {
|
||||
type: String,
|
||||
default: 'text-black-900',
|
||||
},
|
||||
itemClass: {
|
||||
type: String,
|
||||
default:
|
||||
'flex items-center p-3 cursor-pointer ml-0 border-b border-slate-100',
|
||||
},
|
||||
action: {
|
||||
type: Function,
|
||||
default: () => {},
|
||||
},
|
||||
},
|
||||
};
|
||||
</script>
|
||||
<style scoped lang="scss">
|
||||
@import '~widget/assets/scss/variables.scss';
|
||||
|
||||
.menu-item {
|
||||
margin-left: $zero !important;
|
||||
outline: none;
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
&:disabled {
|
||||
cursor: not-allowed;
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -11,12 +11,16 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
'conversation.typing_on': this.onTypingOn,
|
||||
'conversation.typing_off': this.onTypingOff,
|
||||
'conversation.status_changed': this.onStatusChange,
|
||||
'conversation.created': this.onConversationCreated,
|
||||
'presence.update': this.onPresenceUpdate,
|
||||
'contact.merged': this.onContactMerge,
|
||||
};
|
||||
}
|
||||
|
||||
onStatusChange = data => {
|
||||
if (data.status === 'resolved') {
|
||||
this.app.$store.dispatch('campaign/resetCampaign');
|
||||
}
|
||||
this.app.$store.dispatch('conversationAttributes/update', data);
|
||||
};
|
||||
|
||||
|
@ -33,6 +37,10 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||
this.app.$store.dispatch('conversation/addOrUpdateMessage', data);
|
||||
};
|
||||
|
||||
onConversationCreated = () => {
|
||||
this.app.$store.dispatch('conversationAttributes/getAttributes');
|
||||
};
|
||||
|
||||
onPresenceUpdate = data => {
|
||||
this.app.$store.dispatch('agent/updatePresence', data.users);
|
||||
};
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
"IN_A_DAY": "Typically replies in a day"
|
||||
},
|
||||
"START_CONVERSATION": "Start Conversation",
|
||||
"END_CONVERSATION": "End Conversation",
|
||||
"CONTINUE_CONVERSATION": "Continue conversation",
|
||||
"START_NEW_CONVERSATION": "Start a new conversation",
|
||||
"UNREAD_VIEW": {
|
||||
|
|
|
@ -100,6 +100,7 @@ export const actions = {
|
|||
{ root: true }
|
||||
);
|
||||
await triggerCampaign({ campaignId, websiteToken });
|
||||
commit('setCampaignExecuted', true);
|
||||
commit('setActiveCampaign', {});
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
|
@ -113,6 +114,7 @@ export const actions = {
|
|||
},
|
||||
resetCampaign: async ({ commit }) => {
|
||||
try {
|
||||
commit('setCampaignExecuted', false);
|
||||
commit('setActiveCampaign', {});
|
||||
} catch (error) {
|
||||
commit('setError', true);
|
||||
|
@ -130,6 +132,12 @@ export const mutations = {
|
|||
setError($state, value) {
|
||||
Vue.set($state.uiFlags, 'isError', value);
|
||||
},
|
||||
setHasFetched($state, value) {
|
||||
Vue.set($state.uiFlags, 'hasFetched', value);
|
||||
},
|
||||
setCampaignExecuted($state, data) {
|
||||
Vue.set($state, 'campaignHasExecuted', data);
|
||||
},
|
||||
};
|
||||
|
||||
export default {
|
||||
|
|
|
@ -5,6 +5,7 @@ import {
|
|||
sendAttachmentAPI,
|
||||
toggleTyping,
|
||||
setUserLastSeenAt,
|
||||
toggleStatus,
|
||||
} from 'widget/api/conversation';
|
||||
|
||||
import { createTemporaryMessage, getNonDeletedMessages } from './helpers';
|
||||
|
@ -130,4 +131,8 @@ export const actions = {
|
|||
// IgnoreError
|
||||
}
|
||||
},
|
||||
|
||||
resolveConversation: async () => {
|
||||
await toggleStatus();
|
||||
},
|
||||
};
|
||||
|
|
|
@ -132,6 +132,7 @@ describe('#actions', () => {
|
|||
root: true,
|
||||
},
|
||||
],
|
||||
['setCampaignExecuted', true],
|
||||
['setActiveCampaign', {}],
|
||||
[
|
||||
'conversation/setConversationUIFlag',
|
||||
|
@ -176,7 +177,10 @@ describe('#actions', () => {
|
|||
it('sends correct actions if execute campaign API is success', async () => {
|
||||
API.post.mockResolvedValue({});
|
||||
await actions.resetCampaign({ commit });
|
||||
expect(commit.mock.calls).toEqual([['setActiveCampaign', {}]]);
|
||||
expect(commit.mock.calls).toEqual([
|
||||
['setCampaignExecuted', false],
|
||||
['setActiveCampaign', {}],
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -25,4 +25,12 @@ describe('#mutations', () => {
|
|||
expect(state.activeCampaign).toEqual(campaigns[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setCampaignExecuted', () => {
|
||||
it('set campaign executed flag', () => {
|
||||
const state = { records: [], uiFlags: {}, campaignHasExecuted: false };
|
||||
mutations.setCampaignExecuted(state, true);
|
||||
expect(state.campaignHasExecuted).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,20 +1,6 @@
|
|||
class ActionCableListener < BaseListener
|
||||
include Events::Types
|
||||
|
||||
def conversation_created(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members)
|
||||
|
||||
broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_read(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members)
|
||||
|
||||
broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def message_created(event)
|
||||
message, account = extract_message_and_account(event)
|
||||
conversation = message.conversation
|
||||
|
@ -26,12 +12,25 @@ class ActionCableListener < BaseListener
|
|||
def message_updated(event)
|
||||
message, account = extract_message_and_account(event)
|
||||
conversation = message.conversation
|
||||
contact = conversation.contact
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_tokens(conversation.contact_inbox, message)
|
||||
|
||||
broadcast(account, tokens, MESSAGE_UPDATED, message.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_created(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
|
||||
|
||||
broadcast(account, tokens, CONVERSATION_CREATED, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_read(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members)
|
||||
|
||||
broadcast(account, tokens, CONVERSATION_READ, conversation.push_event_data)
|
||||
end
|
||||
|
||||
def conversation_status_changed(event)
|
||||
conversation, account = extract_conversation_and_account(event)
|
||||
tokens = user_tokens(account, conversation.inbox.members) + contact_inbox_tokens(conversation.contact_inbox)
|
||||
|
|
|
@ -20,6 +20,8 @@ module ActivityMessageHandler
|
|||
def create_status_change_message(user_name)
|
||||
content = if user_name
|
||||
I18n.t("conversations.activity.status.#{status}", user_name: user_name)
|
||||
elsif Current.contact.present?
|
||||
I18n.t('conversations.activity.status.contact_resolved', contact_name: Current.contact.name.capitalize)
|
||||
elsif resolved?
|
||||
I18n.t('conversations.activity.status.auto_resolved', duration: auto_resolve_duration)
|
||||
end
|
||||
|
|
|
@ -72,6 +72,7 @@ en:
|
|||
activity:
|
||||
status:
|
||||
resolved: "Conversation was marked resolved by %{user_name}"
|
||||
contact_resolved: "Conversation was resolved by %{contact_name}"
|
||||
open: "Conversation was reopened by %{user_name}"
|
||||
pending: "Conversation was marked as pending by %{user_name}"
|
||||
snoozed: "Conversation was snoozed by %{user_name}"
|
||||
|
|
|
@ -188,6 +188,7 @@ Rails.application.routes.draw do
|
|||
post :update_last_seen
|
||||
post :toggle_typing
|
||||
post :transcript
|
||||
get :toggle_status
|
||||
end
|
||||
end
|
||||
resource :contact, only: [:show, :update] do
|
||||
|
|
|
@ -2,10 +2,12 @@ module Current
|
|||
thread_mattr_accessor :user
|
||||
thread_mattr_accessor :account
|
||||
thread_mattr_accessor :account_user
|
||||
thread_mattr_accessor :contact
|
||||
|
||||
def self.reset
|
||||
Current.user = nil
|
||||
Current.account = nil
|
||||
Current.account_user = nil
|
||||
Current.contact = nil
|
||||
end
|
||||
end
|
||||
|
|
|
@ -118,4 +118,19 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe 'GET /api/v1/widget/conversations/toggle_status' do
|
||||
context 'when user end conversation from widget' do
|
||||
it 'resolves the conversation' do
|
||||
expect(conversation.open?).to be true
|
||||
get '/api/v1/widget/conversations/toggle_status',
|
||||
headers: { 'X-Auth-Token' => token },
|
||||
params: { website_token: web_widget.website_token },
|
||||
as: :json
|
||||
|
||||
expect(response).to have_http_status(:success)
|
||||
expect(conversation.reload.resolved?).to be true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue