mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 19:17:48 +00:00 
			
		
		
		
	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:
		| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Aswin Dev P.S
					Aswin Dev P.S