mirror of
				https://github.com/lingble/chatwoot.git
				synced 2025-10-31 02:57:57 +00:00 
			
		
		
		
	Feature: Contact Panel with conversation details (#397)
* Add Contact panel changes * Fix parent iframe blocked * Add Conversation Panel, Contact messages * Update contact panel with conversation details * Update designs in sidebar * Fix specs * Specs: Add specs for conversationMetadata and contact modules * Fix currentUrl issues * Fix spelling * Set default to empty string
This commit is contained in:
		| @@ -38,7 +38,9 @@ class Api::V1::Widget::MessagesController < ActionController::Base | |||||||
|       inbox_id: inbox.id, |       inbox_id: inbox.id, | ||||||
|       contact_id: cookie_params[:contact_id], |       contact_id: cookie_params[:contact_id], | ||||||
|       additional_attributes: { |       additional_attributes: { | ||||||
|         browser: browser_params |         browser: browser_params, | ||||||
|  |         referer: permitted_params[:message][:referer_url], | ||||||
|  |         initiated_at: timestamp_params | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|   end |   end | ||||||
| @@ -53,6 +55,12 @@ class Api::V1::Widget::MessagesController < ActionController::Base | |||||||
|     } |     } | ||||||
|   end |   end | ||||||
|  |  | ||||||
|  |   def timestamp_params | ||||||
|  |     { | ||||||
|  |       timestamp: permitted_params[:message][:timestamp] | ||||||
|  |     } | ||||||
|  |   end | ||||||
|  |  | ||||||
|   def inbox |   def inbox | ||||||
|     @inbox ||= ::Inbox.find_by(id: cookie_params[:inbox_id]) |     @inbox ||= ::Inbox.find_by(id: cookie_params[:inbox_id]) | ||||||
|   end |   end | ||||||
| @@ -79,7 +87,7 @@ class Api::V1::Widget::MessagesController < ActionController::Base | |||||||
|   end |   end | ||||||
|  |  | ||||||
|   def permitted_params |   def permitted_params | ||||||
|     params.permit(:before, message: [:content]) |     params.permit(:before, message: [:content, :referer_url, :timestamp]) | ||||||
|   end |   end | ||||||
|  |  | ||||||
|   def secret_key |   def secret_key | ||||||
|   | |||||||
							
								
								
									
										9
									
								
								app/javascript/dashboard/api/contacts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								app/javascript/dashboard/api/contacts.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,9 @@ | |||||||
|  | import ApiClient from './ApiClient'; | ||||||
|  |  | ||||||
|  | class ContactAPI extends ApiClient { | ||||||
|  |   constructor() { | ||||||
|  |     super('contacts'); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export default new ContactAPI(); | ||||||
| @@ -217,3 +217,9 @@ | |||||||
|       border-left: $size solid transparent; |       border-left: $size solid transparent; | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @mixin text-ellipsis { | ||||||
|  |   overflow: hidden; | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  |   white-space: nowrap; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -8,7 +8,7 @@ | |||||||
|         :username="chat.meta.sender.name" |         :username="chat.meta.sender.name" | ||||||
|       /> |       /> | ||||||
|       <div class="user--profile__meta"> |       <div class="user--profile__meta"> | ||||||
|         <h3 class="user--name"> |         <h3 v-if="!isContactPanelOpen" class="user--name text-truncate"> | ||||||
|           {{ chat.meta.sender.name }} |           {{ chat.meta.sender.name }} | ||||||
|         </h3> |         </h3> | ||||||
|         <button |         <button | ||||||
| @@ -113,3 +113,11 @@ export default { | |||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | .text-truncate { | ||||||
|  |   white-space: nowrap; | ||||||
|  |   overflow: hidden; | ||||||
|  |   text-overflow: ellipsis; | ||||||
|  | } | ||||||
|  | </style> | ||||||
|   | |||||||
							
								
								
									
										8
									
								
								app/javascript/dashboard/i18n/locale/en/contact.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								app/javascript/dashboard/i18n/locale/en/contact.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,8 @@ | |||||||
|  | { | ||||||
|  |   "CONTACT_PANEL": { | ||||||
|  |     "BROWSER": "Browser", | ||||||
|  |     "OS": "Operating System", | ||||||
|  |     "INITIATED_FROM": "Initiated from", | ||||||
|  |     "INITIATED_AT": "Initiated at" | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -3,6 +3,7 @@ import { default as _agentMgmt } from './agentMgmt.json'; | |||||||
| import { default as _billing } from './billing.json'; | import { default as _billing } from './billing.json'; | ||||||
| import { default as _cannedMgmt } from './cannedMgmt.json'; | import { default as _cannedMgmt } from './cannedMgmt.json'; | ||||||
| import { default as _chatlist } from './chatlist.json'; | import { default as _chatlist } from './chatlist.json'; | ||||||
|  | import { default as _contact } from './contact.json'; | ||||||
| import { default as _conversation } from './conversation.json'; | import { default as _conversation } from './conversation.json'; | ||||||
| import { default as _inboxMgmt } from './inboxMgmt.json'; | import { default as _inboxMgmt } from './inboxMgmt.json'; | ||||||
| import { default as _login } from './login.json'; | import { default as _login } from './login.json'; | ||||||
| @@ -16,6 +17,7 @@ export default { | |||||||
|   ..._billing, |   ..._billing, | ||||||
|   ..._cannedMgmt, |   ..._cannedMgmt, | ||||||
|   ..._chatlist, |   ..._chatlist, | ||||||
|  |   ..._contact, | ||||||
|   ..._conversation, |   ..._conversation, | ||||||
|   ..._inboxMgmt, |   ..._inboxMgmt, | ||||||
|   ..._login, |   ..._login, | ||||||
|   | |||||||
| @@ -0,0 +1,47 @@ | |||||||
|  | <template> | ||||||
|  |   <div class="conv-details--item"> | ||||||
|  |     <div class="conv-details--item__label"> | ||||||
|  |       <i :class="icon" class="conv-details--item__icon"></i> | ||||||
|  |       {{ title }} | ||||||
|  |     </div> | ||||||
|  |     <div class="conv-details--item__value"> | ||||||
|  |       {{ value }} | ||||||
|  |     </div> | ||||||
|  |   </div> | ||||||
|  | </template> | ||||||
|  |  | ||||||
|  | <script> | ||||||
|  | export default { | ||||||
|  |   props: { | ||||||
|  |     title: { type: String, required: true }, | ||||||
|  |     icon: { type: String, required: true }, | ||||||
|  |     value: { type: [String, Number], default: '' }, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  | </script> | ||||||
|  |  | ||||||
|  | <style lang="scss" scoped> | ||||||
|  | @import '~dashboard/assets/scss/variables'; | ||||||
|  | @import '~dashboard/assets/scss/mixins'; | ||||||
|  |  | ||||||
|  | .conv-details--item { | ||||||
|  |   padding-bottom: $space-normal; | ||||||
|  |  | ||||||
|  |   &:last-child { | ||||||
|  |     padding-bottom: 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .conv-details--item__icon { | ||||||
|  |     padding-right: $space-micro; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .conv-details--item__label { | ||||||
|  |     font-weight: $font-weight-medium; | ||||||
|  |     margin-bottom: $space-micro; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .conv-details--item__value { | ||||||
|  |     word-break: break-all; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | </style> | ||||||
| @@ -1,36 +1,126 @@ | |||||||
| <template> | <template> | ||||||
|   <div class="medium-3 bg-white contact--panel"> |   <div class="medium-3 bg-white contact--panel"> | ||||||
|  |     <div class="contact--profile"> | ||||||
|  |       <div class="contact--info"> | ||||||
|         <thumbnail |         <thumbnail | ||||||
|       :src="contactImage" |           :src="contact.avatar_url" | ||||||
|       size="80px" |           size="56px" | ||||||
|           :badge="contact.channel" |           :badge="contact.channel" | ||||||
|           :username="contact.name" |           :username="contact.name" | ||||||
|         /> |         /> | ||||||
|     <h4> |         <div class="contact--details"> | ||||||
|  |           <div class="contact--name"> | ||||||
|             {{ contact.name }} |             {{ contact.name }} | ||||||
|     </h4> |           </div> | ||||||
|  |           <a | ||||||
|  |             v-if="contact.email" | ||||||
|  |             :href="`mailto:${contact.email}`" | ||||||
|  |             class="contact--email" | ||||||
|  |           > | ||||||
|  |             {{ contact.email }} | ||||||
|  |           </a> | ||||||
|  |           <div class="contact--location"> | ||||||
|  |             {{ contact.location }} | ||||||
|  |           </div> | ||||||
|  |         </div> | ||||||
|  |       </div> | ||||||
|  |       <div v-if="contact.bio" class="contact--bio"> | ||||||
|  |         {{ contact.bio }} | ||||||
|  |       </div> | ||||||
|  |     </div> | ||||||
|  |     <div v-if="browser" class="conversation--details"> | ||||||
|  |       <contact-details-item | ||||||
|  |         v-if="browser.browser_name" | ||||||
|  |         :title="$t('CONTACT_PANEL.BROWSER')" | ||||||
|  |         :value="browserName" | ||||||
|  |         icon="ion-ios-world-outline" | ||||||
|  |       /> | ||||||
|  |       <contact-details-item | ||||||
|  |         v-if="browser.platform_name" | ||||||
|  |         :title="$t('CONTACT_PANEL.OS')" | ||||||
|  |         :value="platformName" | ||||||
|  |         icon="ion-laptop" | ||||||
|  |       /> | ||||||
|  |       <contact-details-item | ||||||
|  |         v-if="referer" | ||||||
|  |         :title="$t('CONTACT_PANEL.INITIATED_FROM')" | ||||||
|  |         :value="referer" | ||||||
|  |         icon="ion-link" | ||||||
|  |       /> | ||||||
|  |       <contact-details-item | ||||||
|  |         v-if="initiatedAt" | ||||||
|  |         :title="$t('CONTACT_PANEL.INITIATED_AT')" | ||||||
|  |         :value="initiatedAt.timestamp" | ||||||
|  |         icon="ion-clock" | ||||||
|  |       /> | ||||||
|  |     </div> | ||||||
|   </div> |   </div> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
| <script> | <script> | ||||||
| import { mapGetters } from 'vuex'; |  | ||||||
| import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | import Thumbnail from 'dashboard/components/widgets/Thumbnail.vue'; | ||||||
|  | import ContactDetailsItem from './ContactDetailsItem.vue'; | ||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   components: { |   components: { | ||||||
|  |     ContactDetailsItem, | ||||||
|     Thumbnail, |     Thumbnail, | ||||||
|   }, |   }, | ||||||
|  |   props: { | ||||||
|  |     conversationId: { | ||||||
|  |       type: [Number, String], | ||||||
|  |       required: true, | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|   computed: { |   computed: { | ||||||
|     ...mapGetters({ |     currentConversationMetaData() { | ||||||
|       currentChat: 'getSelectedChat', |       return this.$store.getters[ | ||||||
|     }), |         'conversationMetadata/getConversationMetadata' | ||||||
|  |       ](this.conversationId); | ||||||
|  |     }, | ||||||
|  |     additionalAttributes() { | ||||||
|  |       return this.currentConversationMetaData.additional_attributes || {}; | ||||||
|  |     }, | ||||||
|  |     browser() { | ||||||
|  |       return this.additionalAttributes.browser || {}; | ||||||
|  |     }, | ||||||
|  |     referer() { | ||||||
|  |       return this.additionalAttributes.referer; | ||||||
|  |     }, | ||||||
|  |     initiatedAt() { | ||||||
|  |       return this.additionalAttributes.initiated_at; | ||||||
|  |     }, | ||||||
|  |     browserName() { | ||||||
|  |       return `${this.browser.browser_name || ''} ${this.browser | ||||||
|  |         .browser_version || ''}`; | ||||||
|  |     }, | ||||||
|  |     platformName() { | ||||||
|  |       const { | ||||||
|  |         platform_name: platformName, | ||||||
|  |         platform_version: platformVersion, | ||||||
|  |       } = this.browser; | ||||||
|  |       return `${platformName || ''} ${platformVersion || ''}`; | ||||||
|  |     }, | ||||||
|  |     contactId() { | ||||||
|  |       return this.currentConversationMetaData.contact_id; | ||||||
|  |     }, | ||||||
|     contact() { |     contact() { | ||||||
|       const { meta: { sender = {} } = {} } = this.currentChat || {}; |       return this.$store.getters['contacts/getContact'](this.contactId); | ||||||
|       return sender; |  | ||||||
|     }, |     }, | ||||||
|     contactImage() { |  | ||||||
|       return `/uploads/avatar/contact/${this.contact.id}/profilepic.jpeg`; |  | ||||||
|   }, |   }, | ||||||
|  |   watch: { | ||||||
|  |     contactId(newContactId, prevContactId) { | ||||||
|  |       if (newContactId && newContactId !== prevContactId) { | ||||||
|  |         this.$store.dispatch('contacts/show', { | ||||||
|  |           id: this.currentConversationMetaData.contact_id, | ||||||
|  |         }); | ||||||
|  |       } | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
|  |   mounted() { | ||||||
|  |     this.$store.dispatch('contacts/show', { | ||||||
|  |       id: this.currentConversationMetaData.contact_id, | ||||||
|  |     }); | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
| @@ -41,9 +131,70 @@ export default { | |||||||
|  |  | ||||||
| .contact--panel { | .contact--panel { | ||||||
|   @include border-normal-left; |   @include border-normal-left; | ||||||
|  |   font-size: $font-size-small; | ||||||
|  |   overflow-y: auto; | ||||||
|  |   background: $color-white; | ||||||
|  |   overflow: auto; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .contact--profile { | ||||||
|  |   width: 100%; | ||||||
|  |   padding: $space-normal $space-medium $zero; | ||||||
|  |   align-items: center; | ||||||
|  |  | ||||||
|  |   .user-thumbnail-box { | ||||||
|  |     margin-right: $space-normal; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .contact--details { | ||||||
|  |   p { | ||||||
|  |     margin-bottom: 0; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .contact--info { | ||||||
|   display: flex; |   display: flex; | ||||||
|   flex-direction: column; |  | ||||||
|   padding: $space-large $space-normal $space-normal; |  | ||||||
|   align-items: center; |   align-items: center; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | .contact--name { | ||||||
|  |   @include text-ellipsis; | ||||||
|  |  | ||||||
|  |   font-weight: $font-weight-bold; | ||||||
|  |   font-size: $font-size-default; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .contact--email { | ||||||
|  |   @include text-ellipsis; | ||||||
|  |  | ||||||
|  |   color: $color-body; | ||||||
|  |   display: block; | ||||||
|  |   line-height: $space-medium; | ||||||
|  |   text-decoration: underline; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .contact--bio { | ||||||
|  |   margin-top: $space-normal; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .conversation--details { | ||||||
|  |   padding: $space-normal $space-medium; | ||||||
|  |   width: 100%; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | .conversation--labels { | ||||||
|  |   padding: $space-medium; | ||||||
|  |  | ||||||
|  |   .icon { | ||||||
|  |     margin-right: $space-micro; | ||||||
|  |     font-size: $font-size-micro; | ||||||
|  |     color: #fff; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   .label { | ||||||
|  |     color: #fff; | ||||||
|  |     padding: 0.2rem; | ||||||
|  |   } | ||||||
|  | } | ||||||
| </style> | </style> | ||||||
|   | |||||||
| @@ -11,7 +11,10 @@ | |||||||
|       @contactPanelToggle="onToggleContactPanel" |       @contactPanelToggle="onToggleContactPanel" | ||||||
|     > |     > | ||||||
|     </conversation-box> |     </conversation-box> | ||||||
|     <contact-panel v-if="isContactPanelOpen"></contact-panel> |     <contact-panel | ||||||
|  |       v-if="isContactPanelOpen" | ||||||
|  |       :conversation-id="conversationId" | ||||||
|  |     /> | ||||||
|   </section> |   </section> | ||||||
| </template> | </template> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,6 +6,8 @@ import auth from './modules/auth'; | |||||||
| import billing from './modules/billing'; | import billing from './modules/billing'; | ||||||
| import cannedResponse from './modules/cannedResponse'; | import cannedResponse from './modules/cannedResponse'; | ||||||
| import Channel from './modules/channels'; | import Channel from './modules/channels'; | ||||||
|  | import contacts from './modules/contacts'; | ||||||
|  | import conversationMetadata from './modules/conversationMetadata'; | ||||||
| import conversations from './modules/conversations'; | import conversations from './modules/conversations'; | ||||||
| import inboxes from './modules/inboxes'; | import inboxes from './modules/inboxes'; | ||||||
| import inboxMembers from './modules/inboxMembers'; | import inboxMembers from './modules/inboxMembers'; | ||||||
| @@ -19,6 +21,8 @@ export default new Vuex.Store({ | |||||||
|     billing, |     billing, | ||||||
|     cannedResponse, |     cannedResponse, | ||||||
|     Channel, |     Channel, | ||||||
|  |     contacts, | ||||||
|  |     conversationMetadata, | ||||||
|     conversations, |     conversations, | ||||||
|     inboxes, |     inboxes, | ||||||
|     inboxMembers, |     inboxMembers, | ||||||
|   | |||||||
							
								
								
									
										85
									
								
								app/javascript/dashboard/store/modules/contacts.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										85
									
								
								app/javascript/dashboard/store/modules/contacts.js
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,85 @@ | |||||||
|  | /* eslint no-param-reassign: 0 */ | ||||||
|  | import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers'; | ||||||
|  | import * as types from '../mutation-types'; | ||||||
|  | import ContactAPI from '../../api/contacts'; | ||||||
|  |  | ||||||
|  | const state = { | ||||||
|  |   records: [], | ||||||
|  |   uiFlags: { | ||||||
|  |     isFetching: false, | ||||||
|  |     isFetchingItem: false, | ||||||
|  |     isUpdating: false, | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const getters = { | ||||||
|  |   getContacts($state) { | ||||||
|  |     return $state.records; | ||||||
|  |   }, | ||||||
|  |   getUIFlags($state) { | ||||||
|  |     return $state.uiFlags; | ||||||
|  |   }, | ||||||
|  |   getContact: $state => id => { | ||||||
|  |     const [contact = {}] = $state.records.filter( | ||||||
|  |       record => record.id === Number(id) | ||||||
|  |     ); | ||||||
|  |     return contact; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const actions = { | ||||||
|  |   get: async ({ commit }) => { | ||||||
|  |     commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: true }); | ||||||
|  |     try { | ||||||
|  |       const response = await ContactAPI.get(); | ||||||
|  |       commit(types.default.SET_CONTACTS, response.data.payload); | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false }); | ||||||
|  |     } catch (error) { | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isFetching: false }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   show: async ({ commit }, { id }) => { | ||||||
|  |     commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }); | ||||||
|  |     try { | ||||||
|  |       const response = await ContactAPI.show(id); | ||||||
|  |       commit(types.default.SET_CONTACT_ITEM, response.data.payload); | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }); | ||||||
|  |     } catch (error) { | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   update: async ({ commit }, { id, ...updateObj }) => { | ||||||
|  |     commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }); | ||||||
|  |     try { | ||||||
|  |       const response = await ContactAPI.update(id, updateObj); | ||||||
|  |       commit(types.default.EDIT_CONTACT, response.data.payload); | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }); | ||||||
|  |     } catch (error) { | ||||||
|  |       commit(types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }); | ||||||
|  |       throw new Error(error); | ||||||
|  |     } | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const mutations = { | ||||||
|  |   [types.default.SET_CONTACT_UI_FLAG]($state, data) { | ||||||
|  |     $state.uiFlags = { | ||||||
|  |       ...$state.uiFlags, | ||||||
|  |       ...data, | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|  |  | ||||||
|  |   [types.default.SET_CONTACTS]: MutationHelpers.set, | ||||||
|  |   [types.default.SET_CONTACT_ITEM]: MutationHelpers.setSingleRecord, | ||||||
|  |   [types.default.EDIT_CONTACT]: MutationHelpers.update, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   namespaced: true, | ||||||
|  |   state, | ||||||
|  |   getters, | ||||||
|  |   actions, | ||||||
|  |   mutations, | ||||||
|  | }; | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | import Vue from 'vue'; | ||||||
|  | import * as types from '../mutation-types'; | ||||||
|  |  | ||||||
|  | const state = { | ||||||
|  |   records: {}, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const getters = { | ||||||
|  |   getConversationMetadata: $state => id => { | ||||||
|  |     return $state.records[Number(id)] || {}; | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const actions = {}; | ||||||
|  |  | ||||||
|  | export const mutations = { | ||||||
|  |   [types.default.SET_CONVERSATION_METADATA]: ($state, { id, data }) => { | ||||||
|  |     Vue.set($state.records, id, data); | ||||||
|  |   }, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export default { | ||||||
|  |   namespaced: true, | ||||||
|  |   state, | ||||||
|  |   getters, | ||||||
|  |   actions, | ||||||
|  |   mutations, | ||||||
|  | }; | ||||||
| @@ -31,12 +31,21 @@ const actions = { | |||||||
|  |  | ||||||
|   fetchPreviousMessages: async ({ commit }, data) => { |   fetchPreviousMessages: async ({ commit }, data) => { | ||||||
|     try { |     try { | ||||||
|       const response = await MessageApi.getPreviousMessages(data); |       const { | ||||||
|  |         data: { meta, payload }, | ||||||
|  |       } = await MessageApi.getPreviousMessages(data); | ||||||
|  |       commit( | ||||||
|  |         `conversationMetadata/${types.default.SET_CONVERSATION_METADATA}`, | ||||||
|  |         { | ||||||
|  |           id: data.conversationId, | ||||||
|  |           data: meta, | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|       commit(types.default.SET_PREVIOUS_CONVERSATIONS, { |       commit(types.default.SET_PREVIOUS_CONVERSATIONS, { | ||||||
|         id: data.conversationId, |         id: data.conversationId, | ||||||
|         data: response.data.payload, |         data: payload, | ||||||
|       }); |       }); | ||||||
|       if (response.data.payload.length < 20) { |       if (payload.length < 20) { | ||||||
|         commit(types.default.SET_ALL_MESSAGES_LOADED); |         commit(types.default.SET_ALL_MESSAGES_LOADED); | ||||||
|       } |       } | ||||||
|     } catch (error) { |     } catch (error) { | ||||||
|   | |||||||
| @@ -60,7 +60,6 @@ const mutations = { | |||||||
|     const [chat] = getSelectedChatConversation(_state); |     const [chat] = getSelectedChatConversation(_state); | ||||||
|     Vue.set(chat, 'allMessagesLoaded', false); |     Vue.set(chat, 'allMessagesLoaded', false); | ||||||
|   }, |   }, | ||||||
|  |  | ||||||
|   [types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) { |   [types.default.CLEAR_CURRENT_CHAT_WINDOW](_state) { | ||||||
|     _state.selectedChat.id = null; |     _state.selectedChat.id = null; | ||||||
|     _state.selectedChat.agentTyping = 'off'; |     _state.selectedChat.agentTyping = 'off'; | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ describe('#mutations', () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('#EDIT_AGENT', () => { |   describe('#EDIT_AGENT', () => { | ||||||
|     it('sets allMessagesLoaded flag if payload is empty', () => { |     it('update agent record', () => { | ||||||
|       const state = { |       const state = { | ||||||
|         records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }], |         records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }], | ||||||
|       }; |       }; | ||||||
| @@ -52,7 +52,7 @@ describe('#mutations', () => { | |||||||
|   }); |   }); | ||||||
|  |  | ||||||
|   describe('#DELETE_AGENT', () => { |   describe('#DELETE_AGENT', () => { | ||||||
|     it('sets allMessagesLoaded flag if payload is empty', () => { |     it('delete agent record', () => { | ||||||
|       const state = { |       const state = { | ||||||
|         records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }], |         records: [{ id: 1, name: 'Agent1', email: 'agent1@chatwoot.com' }], | ||||||
|       }; |       }; | ||||||
|   | |||||||
| @@ -0,0 +1,72 @@ | |||||||
|  | import axios from 'axios'; | ||||||
|  | import { actions } from '../../contacts'; | ||||||
|  | import * as types from '../../../mutation-types'; | ||||||
|  | import contactList from './fixtures'; | ||||||
|  |  | ||||||
|  | const commit = jest.fn(); | ||||||
|  | global.axios = axios; | ||||||
|  | jest.mock('axios'); | ||||||
|  |  | ||||||
|  | describe('#actions', () => { | ||||||
|  |   describe('#get', () => { | ||||||
|  |     it('sends correct actions if API is success', async () => { | ||||||
|  |       axios.get.mockResolvedValue({ data: { payload: contactList } }); | ||||||
|  |       await actions.get({ commit }); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetching: true }], | ||||||
|  |         [types.default.SET_CONTACTS, contactList], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetching: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |     it('sends correct actions if API is error', async () => { | ||||||
|  |       axios.get.mockRejectedValue({ message: 'Incorrect header' }); | ||||||
|  |       await actions.get({ commit }); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetching: true }], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetching: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#show', () => { | ||||||
|  |     it('sends correct actions if API is success', async () => { | ||||||
|  |       axios.get.mockResolvedValue({ data: { payload: contactList[0] } }); | ||||||
|  |       await actions.show({ commit }, { id: contactList[0].id }); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }], | ||||||
|  |         [types.default.SET_CONTACT_ITEM, contactList[0]], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |     it('sends correct actions if API is error', async () => { | ||||||
|  |       axios.get.mockRejectedValue({ message: 'Incorrect header' }); | ||||||
|  |       await actions.show({ commit }, { id: contactList[0].id }); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: true }], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isFetchingItem: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#update', () => { | ||||||
|  |     it('sends correct actions if API is success', async () => { | ||||||
|  |       axios.patch.mockResolvedValue({ data: { payload: contactList[0] } }); | ||||||
|  |       await actions.update({ commit }, contactList[0]); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }], | ||||||
|  |         [types.default.EDIT_CONTACT, contactList[0]], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |     it('sends correct actions if API is error', async () => { | ||||||
|  |       axios.patch.mockRejectedValue({ message: 'Incorrect header' }); | ||||||
|  |       await expect(actions.update({ commit }, contactList[0])).rejects.toThrow( | ||||||
|  |         Error | ||||||
|  |       ); | ||||||
|  |       expect(commit.mock.calls).toEqual([ | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isUpdating: true }], | ||||||
|  |         [types.default.SET_CONTACT_UI_FLAG, { isUpdating: false }], | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,30 @@ | |||||||
|  | export default [ | ||||||
|  |   { | ||||||
|  |     id: 1, | ||||||
|  |     name: 'Contact 1', | ||||||
|  |     email: 'contact1@chatwoot.com', | ||||||
|  |     phone_number: '9000000001', | ||||||
|  |     thumbnail: 'contact1.png', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 2, | ||||||
|  |     name: 'Contact 2', | ||||||
|  |     email: 'contact2@chatwoot.com', | ||||||
|  |     phone_number: '9000000002', | ||||||
|  |     thumbnail: 'contact2.png', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 3, | ||||||
|  |     name: 'Contact 3', | ||||||
|  |     email: 'contact3@chatwoot.com', | ||||||
|  |     phone_number: '9000000003', | ||||||
|  |     thumbnail: 'contact3.png', | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |     id: 4, | ||||||
|  |     name: 'Contact 4', | ||||||
|  |     email: 'contact4@chatwoot.com', | ||||||
|  |     phone_number: '9000000004', | ||||||
|  |     thumbnail: 'contact4.png', | ||||||
|  |   }, | ||||||
|  | ]; | ||||||
| @@ -0,0 +1,33 @@ | |||||||
|  | import { getters } from '../../contacts'; | ||||||
|  | import contactList from './fixtures'; | ||||||
|  |  | ||||||
|  | describe('#getters', () => { | ||||||
|  |   it('getContacts', () => { | ||||||
|  |     const state = { | ||||||
|  |       records: contactList, | ||||||
|  |     }; | ||||||
|  |     expect(getters.getContacts(state)).toEqual(contactList); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('getContact', () => { | ||||||
|  |     const state = { | ||||||
|  |       records: contactList, | ||||||
|  |     }; | ||||||
|  |     expect(getters.getContact(state)(2)).toEqual(contactList[1]); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   it('getUIFlags', () => { | ||||||
|  |     const state = { | ||||||
|  |       uiFlags: { | ||||||
|  |         isFetching: true, | ||||||
|  |         isFetchingItem: true, | ||||||
|  |         isUpdating: false, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     expect(getters.getUIFlags(state)).toEqual({ | ||||||
|  |       isFetching: true, | ||||||
|  |       isFetchingItem: true, | ||||||
|  |       isUpdating: false, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,53 @@ | |||||||
|  | import * as types from '../../../mutation-types'; | ||||||
|  | import { mutations } from '../../contacts'; | ||||||
|  |  | ||||||
|  | describe('#mutations', () => { | ||||||
|  |   describe('#SET_CONTACTS', () => { | ||||||
|  |     it('set contact records', () => { | ||||||
|  |       const state = { records: [] }; | ||||||
|  |       mutations[types.default.SET_CONTACTS](state, [ | ||||||
|  |         { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }, | ||||||
|  |       ]); | ||||||
|  |       expect(state.records).toEqual([ | ||||||
|  |         { | ||||||
|  |           id: 1, | ||||||
|  |           name: 'contact1', | ||||||
|  |           email: 'contact1@chatwoot.com', | ||||||
|  |         }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#SET_CONTACT_ITEM', () => { | ||||||
|  |     it('push contact data to the store', () => { | ||||||
|  |       const state = { | ||||||
|  |         records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }], | ||||||
|  |       }; | ||||||
|  |       mutations[types.default.SET_CONTACT_ITEM](state, { | ||||||
|  |         id: 2, | ||||||
|  |         name: 'contact2', | ||||||
|  |         email: 'contact2@chatwoot.com', | ||||||
|  |       }); | ||||||
|  |       expect(state.records).toEqual([ | ||||||
|  |         { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }, | ||||||
|  |         { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   describe('#EDIT_CONTACT', () => { | ||||||
|  |     it('update contact', () => { | ||||||
|  |       const state = { | ||||||
|  |         records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }], | ||||||
|  |       }; | ||||||
|  |       mutations[types.default.EDIT_CONTACT](state, { | ||||||
|  |         id: 1, | ||||||
|  |         name: 'contact2', | ||||||
|  |         email: 'contact2@chatwoot.com', | ||||||
|  |       }); | ||||||
|  |       expect(state.records).toEqual([ | ||||||
|  |         { id: 1, name: 'contact2', email: 'contact2@chatwoot.com' }, | ||||||
|  |       ]); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,16 @@ | |||||||
|  | import { getters } from '../../conversationMetadata'; | ||||||
|  |  | ||||||
|  | describe('#getters', () => { | ||||||
|  |   it('getConversationMetadata', () => { | ||||||
|  |     const state = { | ||||||
|  |       records: { | ||||||
|  |         1: { | ||||||
|  |           browser: { name: 'Chrome' }, | ||||||
|  |         }, | ||||||
|  |       }, | ||||||
|  |     }; | ||||||
|  |     expect(getters.getConversationMetadata(state)(1)).toEqual({ | ||||||
|  |       browser: { name: 'Chrome' }, | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | import * as types from '../../../mutation-types'; | ||||||
|  | import { mutations } from '../../conversationMetadata'; | ||||||
|  |  | ||||||
|  | describe('#mutations', () => { | ||||||
|  |   describe('#SET_INBOXES', () => { | ||||||
|  |     it('set inbox records', () => { | ||||||
|  |       const state = { records: {} }; | ||||||
|  |       mutations[types.default.SET_CONVERSATION_METADATA](state, { | ||||||
|  |         id: 1, | ||||||
|  |         data: { browser: { name: 'Chrome' } }, | ||||||
|  |       }); | ||||||
|  |       expect(state.records).toEqual({ | ||||||
|  |         1: { browser: { name: 'Chrome' } }, | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   }); | ||||||
|  | }); | ||||||
| @@ -56,6 +56,12 @@ export default { | |||||||
|   EDIT_CANNED: 'EDIT_CANNED', |   EDIT_CANNED: 'EDIT_CANNED', | ||||||
|   DELETE_CANNED: 'DELETE_CANNED', |   DELETE_CANNED: 'DELETE_CANNED', | ||||||
|  |  | ||||||
|  |   // Contacts | ||||||
|  |   SET_CONTACT_UI_FLAG: 'SET_CONTACT_UI_FLAG', | ||||||
|  |   SET_CONTACT_ITEM: 'SET_CONTACT_ITEM', | ||||||
|  |   SET_CONTACTS: 'SET_CONTACTS', | ||||||
|  |   EDIT_CONTACT: 'EDIT_CONTACT', | ||||||
|  |  | ||||||
|   // Reports |   // Reports | ||||||
|   SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS', |   SET_ACCOUNT_REPORTS: 'SET_ACCOUNT_REPORTS', | ||||||
|   SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY', |   SET_ACCOUNT_SUMMARY: 'SET_ACCOUNT_SUMMARY', | ||||||
| @@ -64,4 +70,7 @@ export default { | |||||||
|   // Billings |   // Billings | ||||||
|   SET_SUBSCRIPTION: 'SET_SUBSCRIPTION', |   SET_SUBSCRIPTION: 'SET_SUBSCRIPTION', | ||||||
|   TOGGLE_SUBSCRIPTION_LOADING: 'TOGGLE_SUBSCRIPTION_LOADING', |   TOGGLE_SUBSCRIPTION_LOADING: 'TOGGLE_SUBSCRIPTION_LOADING', | ||||||
|  |  | ||||||
|  |   // Conversation Metadata | ||||||
|  |   SET_CONVERSATION_METADATA: 'SET_CONVERSATION_METADATA', | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -126,14 +126,15 @@ const IFrameHelper = { | |||||||
|     } |     } | ||||||
|     iframe.src = widgetUrl; |     iframe.src = widgetUrl; | ||||||
|  |  | ||||||
|     iframe.id = 'chatwoot_web_widget'; |     iframe.id = 'chatwoot_live_chat_widget'; | ||||||
|     iframe.style.visibility = 'hidden'; |     iframe.style.visibility = 'hidden'; | ||||||
|     holder.className = 'woot-widget-holder woot--hide'; |     holder.className = 'woot-widget-holder woot--hide'; | ||||||
|     holder.appendChild(iframe); |     holder.appendChild(iframe); | ||||||
|     body.appendChild(holder); |     body.appendChild(holder); | ||||||
|     IFrameHelper.initPostMessageCommunication(); |     IFrameHelper.initPostMessageCommunication(); | ||||||
|  |     IFrameHelper.initLocationListener(); | ||||||
|   }, |   }, | ||||||
|   getAppFrame: () => document.getElementById('chatwoot_web_widget'), |   getAppFrame: () => document.getElementById('chatwoot_live_chat_widget'), | ||||||
|   sendMessage: (key, value) => { |   sendMessage: (key, value) => { | ||||||
|     const element = IFrameHelper.getAppFrame(); |     const element = IFrameHelper.getAppFrame(); | ||||||
|     element.contentWindow.postMessage( |     element.contentWindow.postMessage( | ||||||
| @@ -154,9 +155,15 @@ const IFrameHelper = { | |||||||
|         Cookies.set('cw_conversation', message.config.authToken); |         Cookies.set('cw_conversation', message.config.authToken); | ||||||
|         IFrameHelper.sendMessage('config-set', {}); |         IFrameHelper.sendMessage('config-set', {}); | ||||||
|         IFrameHelper.onLoad(message.config.channelConfig); |         IFrameHelper.onLoad(message.config.channelConfig); | ||||||
|  |         IFrameHelper.setCurrentUrl(); | ||||||
|       } |       } | ||||||
|     }; |     }; | ||||||
|   }, |   }, | ||||||
|  |   initLocationListener: () => { | ||||||
|  |     window.onhashchange = () => { | ||||||
|  |       IFrameHelper.setCurrentUrl(); | ||||||
|  |     }; | ||||||
|  |   }, | ||||||
|   onLoad: ({ widget_color: widgetColor }) => { |   onLoad: ({ widget_color: widgetColor }) => { | ||||||
|     const iframe = IFrameHelper.getAppFrame(); |     const iframe = IFrameHelper.getAppFrame(); | ||||||
|     iframe.style.visibility = ''; |     iframe.style.visibility = ''; | ||||||
| @@ -187,6 +194,12 @@ const IFrameHelper = { | |||||||
|     bubbleHolder.appendChild(createNotificationBubble()); |     bubbleHolder.appendChild(createNotificationBubble()); | ||||||
|     onClickChatBubble(); |     onClickChatBubble(); | ||||||
|   }, |   }, | ||||||
|  |   setCurrentUrl: () => { | ||||||
|  |     console.log(IFrameHelper.getAppFrame(), document); | ||||||
|  |     IFrameHelper.sendMessage('set-current-url', { | ||||||
|  |       refererURL: window.location.href, | ||||||
|  |     }); | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| function loadIframe({ baseUrl, websiteToken }) { | function loadIframe({ baseUrl, websiteToken }) { | ||||||
|   | |||||||
| @@ -20,16 +20,6 @@ export const IFrameHelper = { | |||||||
|  |  | ||||||
| export default { | export default { | ||||||
|   name: 'App', |   name: 'App', | ||||||
|  |  | ||||||
|   methods: { |  | ||||||
|     ...mapActions('appConfig', ['setWidgetColor']), |  | ||||||
|     ...mapActions('conversation', ['fetchOldConversations']), |  | ||||||
|     scrollConversationToBottom() { |  | ||||||
|       const container = this.$el.querySelector('.conversation-wrap'); |  | ||||||
|       container.scrollTop = container.scrollHeight; |  | ||||||
|     }, |  | ||||||
|   }, |  | ||||||
|  |  | ||||||
|   mounted() { |   mounted() { | ||||||
|     if (IFrameHelper.isIFrame()) { |     if (IFrameHelper.isIFrame()) { | ||||||
|       IFrameHelper.sendMessage({ |       IFrameHelper.sendMessage({ | ||||||
| @@ -55,9 +45,19 @@ export default { | |||||||
|         this.fetchOldConversations(); |         this.fetchOldConversations(); | ||||||
|       } else if (message.event === 'widget-visible') { |       } else if (message.event === 'widget-visible') { | ||||||
|         this.scrollConversationToBottom(); |         this.scrollConversationToBottom(); | ||||||
|  |       } else if (message.event === 'set-current-url') { | ||||||
|  |         window.refererURL = message.refererURL; | ||||||
|       } |       } | ||||||
|     }); |     }); | ||||||
|   }, |   }, | ||||||
|  |   methods: { | ||||||
|  |     ...mapActions('appConfig', ['setWidgetColor']), | ||||||
|  |     ...mapActions('conversation', ['fetchOldConversations']), | ||||||
|  |     scrollConversationToBottom() { | ||||||
|  |       const container = this.$el.querySelector('.conversation-wrap'); | ||||||
|  |       container.scrollTop = container.scrollHeight; | ||||||
|  |     }, | ||||||
|  |   }, | ||||||
| }; | }; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
| @@ -3,6 +3,8 @@ const sendMessage = content => ({ | |||||||
|   params: { |   params: { | ||||||
|     message: { |     message: { | ||||||
|       content, |       content, | ||||||
|  |       timestamp: new Date().toString(), | ||||||
|  |       referer_url: window.refererURL || '', | ||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }); | }); | ||||||
|   | |||||||
| @@ -2,14 +2,20 @@ import endPoints from '../endPoints'; | |||||||
|  |  | ||||||
| describe('#sendMessage', () => { | describe('#sendMessage', () => { | ||||||
|   it('returns correct payload', () => { |   it('returns correct payload', () => { | ||||||
|  |     const spy = jest.spyOn(global, 'Date').mockImplementation(() => ({ | ||||||
|  |       toString: () => 'mock date', | ||||||
|  |     })); | ||||||
|     expect(endPoints.sendMessage('hello')).toEqual({ |     expect(endPoints.sendMessage('hello')).toEqual({ | ||||||
|       url: `/api/v1/widget/messages`, |       url: `/api/v1/widget/messages`, | ||||||
|       params: { |       params: { | ||||||
|         message: { |         message: { | ||||||
|           content: 'hello', |           content: 'hello', | ||||||
|  |           referer_url: '', | ||||||
|  |           timestamp: 'mock date', | ||||||
|         }, |         }, | ||||||
|       }, |       }, | ||||||
|     }); |     }); | ||||||
|  |     spy.mockRestore(); | ||||||
|   }); |   }); | ||||||
| }); | }); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -4,6 +4,6 @@ json.payload do | |||||||
|     json.name contact.name |     json.name contact.name | ||||||
|     json.email contact.email |     json.email contact.email | ||||||
|     json.phone_number contact.phone_number |     json.phone_number contact.phone_number | ||||||
|     json.thumbnail contact.avatar.thumb.url |     json.thumbnail contact.avatar.profile_thumb.url | ||||||
|   end |   end | ||||||
| end | end | ||||||
|   | |||||||
| @@ -1,5 +1,7 @@ | |||||||
| json.meta do | json.meta do | ||||||
|   json.labels @conversation.label_list |   json.labels @conversation.label_list | ||||||
|  |   json.additional_attributes @conversation.additional_attributes | ||||||
|  |   json.contact_id @conversation.contact_id | ||||||
| end | end | ||||||
|  |  | ||||||
| json.payload do | json.payload do | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Pranav Raj S
					Pranav Raj S