Chore: Contact Sidebar, conversation cleanup (#908)
- Update sidebar design - Move every contact data to contacts module - Revert go to next conversation feature - Fix issues with new conversation in action cable - Escape HTML content - Broadcast event when conversation.contact changes. Co-authored-by: Sojan <sojan@pepalo.com>
This commit is contained in:
parent
8c52a3a953
commit
f78df91dd2
22 changed files with 252 additions and 125 deletions
|
@ -91,7 +91,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-labels-wrap {
|
.sidebar-labels-wrap {
|
||||||
|
|
||||||
&.has-edited,
|
&.has-edited,
|
||||||
&:hover {
|
&:hover {
|
||||||
.multiselect {
|
.multiselect {
|
||||||
|
@ -108,8 +107,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.multiselect {
|
.multiselect {
|
||||||
margin-top: $space-small;
|
|
||||||
|
|
||||||
>.multiselect__select {
|
>.multiselect__select {
|
||||||
visibility: hidden;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,15 +6,15 @@
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
v-if="!hideThumbnail"
|
v-if="!hideThumbnail"
|
||||||
:src="chat.meta.sender.thumbnail"
|
:src="currentContact.thumbnail"
|
||||||
:badge="chat.meta.sender.channel"
|
:badge="currentContact.channel"
|
||||||
class="columns"
|
class="columns"
|
||||||
:username="chat.meta.sender.name"
|
:username="currentContact.name"
|
||||||
size="40px"
|
size="40px"
|
||||||
/>
|
/>
|
||||||
<div class="conversation--details columns">
|
<div class="conversation--details columns">
|
||||||
<h4 class="conversation--user">
|
<h4 class="conversation--user">
|
||||||
{{ chat.meta.sender.name }}
|
{{ currentContact.name }}
|
||||||
<span
|
<span
|
||||||
v-if="!hideInboxName && isInboxNameVisible"
|
v-if="!hideInboxName && isInboxNameVisible"
|
||||||
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
v-tooltip.bottom="inboxName(chat.inbox_id)"
|
||||||
|
@ -25,12 +25,13 @@
|
||||||
</h4>
|
</h4>
|
||||||
<p
|
<p
|
||||||
class="conversation--message"
|
class="conversation--message"
|
||||||
v-html="extractMessageText(lastMessage(chat))"
|
v-html="extractMessageText(lastMessageInChat)"
|
||||||
></p>
|
/>
|
||||||
|
|
||||||
<div class="conversation--meta">
|
<div class="conversation--meta">
|
||||||
<span class="timestamp">
|
<span class="timestamp">
|
||||||
{{ dynamicTime(lastMessage(chat).created_at) }}
|
{{
|
||||||
|
lastMessageInChat ? dynamicTime(lastMessageInChat.created_at) : ''
|
||||||
|
}}
|
||||||
</span>
|
</span>
|
||||||
<span class="unread">{{ getUnreadCount }}</span>
|
<span class="unread">{{ getUnreadCount }}</span>
|
||||||
</div>
|
</div>
|
||||||
|
@ -78,6 +79,12 @@ export default {
|
||||||
accountId: 'getCurrentAccountId',
|
accountId: 'getCurrentAccountId',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
currentContact() {
|
||||||
|
return this.$store.getters['contacts/getContact'](
|
||||||
|
this.chat.meta.sender.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
isActiveChat() {
|
isActiveChat() {
|
||||||
return this.currentChat.id === this.chat.id;
|
return this.currentChat.id === this.chat.id;
|
||||||
},
|
},
|
||||||
|
@ -93,6 +100,10 @@ export default {
|
||||||
isInboxNameVisible() {
|
isInboxNameVisible() {
|
||||||
return !this.activeInbox;
|
return !this.activeInbox;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
lastMessageInChat() {
|
||||||
|
return this.lastMessage(this.chat);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -102,6 +113,10 @@ export default {
|
||||||
router.push({ path: frontendURL(path) });
|
router.push({ path: frontendURL(path) });
|
||||||
},
|
},
|
||||||
extractMessageText(chatItem) {
|
extractMessageText(chatItem) {
|
||||||
|
if (!chatItem) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
const { content, attachments } = chatItem;
|
const { content, attachments } = chatItem;
|
||||||
|
|
||||||
if (content) {
|
if (content) {
|
||||||
|
|
|
@ -2,14 +2,14 @@
|
||||||
<div class="conv-header">
|
<div class="conv-header">
|
||||||
<div class="user">
|
<div class="user">
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
:src="chat.meta.sender.thumbnail"
|
:src="currentContact.thumbnail"
|
||||||
size="40px"
|
size="40px"
|
||||||
:badge="chat.meta.sender.channel"
|
:badge="currentContact.channel"
|
||||||
:username="chat.meta.sender.name"
|
:username="currentContact.name"
|
||||||
/>
|
/>
|
||||||
<div class="user--profile__meta">
|
<div class="user--profile__meta">
|
||||||
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
<h3 v-if="!isContactPanelOpen" class="user--name text-truncate">
|
||||||
{{ chat.meta.sender.name }}
|
{{ currentContact.name }}
|
||||||
</h3>
|
</h3>
|
||||||
<button
|
<button
|
||||||
class="user--profile__button clear button small"
|
class="user--profile__button clear button small"
|
||||||
|
@ -79,6 +79,13 @@ export default {
|
||||||
agents: 'agents/getVerifiedAgents',
|
agents: 'agents/getVerifiedAgents',
|
||||||
currentChat: 'getSelectedChat',
|
currentChat: 'getSelectedChat',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
currentContact() {
|
||||||
|
return this.$store.getters['contacts/getContact'](
|
||||||
|
this.chat.meta.sender.id
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
agentList() {
|
agentList() {
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
|
|
|
@ -16,6 +16,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
'assignee.changed': this.onAssigneeChanged,
|
'assignee.changed': this.onAssigneeChanged,
|
||||||
'conversation.typing_on': this.onTypingOn,
|
'conversation.typing_on': this.onTypingOn,
|
||||||
'conversation.typing_off': this.onTypingOff,
|
'conversation.typing_off': this.onTypingOff,
|
||||||
|
'conversation.contact_changed': this.onConversationContactChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +28,17 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
this.app.$store.dispatch('updateMessage', data);
|
this.app.$store.dispatch('updateMessage', data);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
onConversationContactChange = payload => {
|
||||||
|
const { meta = {}, id: conversationId } = payload;
|
||||||
|
const { sender } = meta || {};
|
||||||
|
if (conversationId) {
|
||||||
|
this.app.$store.dispatch('updateConversationContact', {
|
||||||
|
conversationId,
|
||||||
|
...sender,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
onAssigneeChanged = payload => {
|
onAssigneeChanged = payload => {
|
||||||
const { meta = {}, id } = payload;
|
const { meta = {}, id } = payload;
|
||||||
const { assignee } = meta || {};
|
const { assignee } = meta || {};
|
||||||
|
|
|
@ -1,6 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="contact-conversation--panel">
|
<div class="contact-conversation--panel">
|
||||||
<contact-details-item :title="$t('CONTACT_PANEL.CONVERSATIONS.TITLE')" />
|
<contact-details-item
|
||||||
|
:title="$t('CONTACT_PANEL.CONVERSATIONS.TITLE')"
|
||||||
|
icon="ion-chatboxes"
|
||||||
|
/>
|
||||||
<div v-if="!uiFlags.isFetching">
|
<div v-if="!uiFlags.isFetching">
|
||||||
<p v-if="!previousConversations.length" class="no-results">
|
<p v-if="!previousConversations.length" class="no-results">
|
||||||
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
|
{{ $t('CONTACT_PANEL.CONVERSATIONS.NO_RECORDS_FOUND') }}
|
||||||
|
@ -75,13 +78,11 @@ export default {
|
||||||
@import '~dashboard/assets/scss/mixins';
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
.contact-conversation--panel {
|
.contact-conversation--panel {
|
||||||
padding: $space-normal $space-normal $space-normal $space-medium;
|
padding: $space-normal;
|
||||||
padding-top: 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
padding: 0 $space-small;
|
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -25,7 +25,7 @@ export default {
|
||||||
@import '~dashboard/assets/scss/mixins';
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
.conv-details--item {
|
.conv-details--item {
|
||||||
padding-bottom: $space-medium;
|
padding-bottom: $space-normal;
|
||||||
|
|
||||||
&:last-child {
|
&:last-child {
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
|
|
|
@ -7,7 +7,7 @@
|
||||||
<div class="contact--info">
|
<div class="contact--info">
|
||||||
<thumbnail
|
<thumbnail
|
||||||
:src="contact.thumbnail"
|
:src="contact.thumbnail"
|
||||||
size="56px"
|
size="64px"
|
||||||
:badge="contact.channel"
|
:badge="contact.channel"
|
||||||
:username="contact.name"
|
:username="contact.name"
|
||||||
:status="contact.availability_status"
|
:status="contact.availability_status"
|
||||||
|
@ -57,14 +57,17 @@
|
||||||
>
|
>
|
||||||
{{ contact.additional_attributes.description }}
|
{{ contact.additional_attributes.description }}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="contact--actions">
|
||||||
|
<button
|
||||||
|
v-if="!currentChat.muted"
|
||||||
|
class="button small clear contact--mute small-6"
|
||||||
|
@click="mute"
|
||||||
|
>
|
||||||
|
{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<conversation-labels :conversation-id="conversationId" />
|
<div v-if="browser.browser_name" class="conversation--details">
|
||||||
<contact-conversations
|
|
||||||
v-if="contact.id"
|
|
||||||
:contact-id="contact.id"
|
|
||||||
:conversation-id="conversationId"
|
|
||||||
/>
|
|
||||||
<div v-if="browser" class="conversation--details">
|
|
||||||
<contact-details-item
|
<contact-details-item
|
||||||
v-if="browser.browser_name"
|
v-if="browser.browser_name"
|
||||||
:title="$t('CONTACT_PANEL.BROWSER')"
|
:title="$t('CONTACT_PANEL.BROWSER')"
|
||||||
|
@ -90,9 +93,12 @@
|
||||||
icon="ion-clock"
|
icon="ion-clock"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<a v-show="!currentChat.muted" class="contact--mute" @click="mute">
|
<conversation-labels :conversation-id="conversationId" />
|
||||||
{{ $t('CONTACT_PANEL.MUTE_CONTACT') }}
|
<contact-conversations
|
||||||
</a>
|
v-if="contact.id"
|
||||||
|
:contact-id="contact.id"
|
||||||
|
:conversation-id="conversationId"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -153,16 +159,16 @@ export default {
|
||||||
return `${platformName || ''} ${platformVersion || ''}`;
|
return `${platformName || ''} ${platformVersion || ''}`;
|
||||||
},
|
},
|
||||||
contactId() {
|
contactId() {
|
||||||
return this.currentConversationMetaData.contact?.id;
|
return this.currentChat.meta?.sender?.id;
|
||||||
},
|
},
|
||||||
contact() {
|
contact() {
|
||||||
return this.$store.getters['contacts/getContact'](this.contactId);
|
return this.$store.getters['contacts/getContact'](this.contactId);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
contactId(newContactId, prevContactId) {
|
conversationId(newConversationId, prevConversationId) {
|
||||||
if (newContactId && newContactId !== prevContactId) {
|
if (newConversationId && newConversationId !== prevConversationId) {
|
||||||
this.$store.dispatch('contacts/show', { id: newContactId });
|
this.$store.dispatch('contacts/show', { id: this.contactId });
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -186,11 +192,13 @@ export default {
|
||||||
|
|
||||||
.contact--panel {
|
.contact--panel {
|
||||||
@include border-normal-left;
|
@include border-normal-left;
|
||||||
|
|
||||||
|
background: white;
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
background: white;
|
|
||||||
overflow: auto;
|
overflow: auto;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
padding: $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.close-button {
|
.close-button {
|
||||||
|
@ -200,23 +208,29 @@ export default {
|
||||||
font-size: $font-size-default;
|
font-size: $font-size-default;
|
||||||
color: $color-heading;
|
color: $color-heading;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--profile {
|
.contact--profile {
|
||||||
padding: $space-medium $space-normal 0 $space-medium;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
padding: $space-medium 0 $space-one;
|
||||||
|
|
||||||
.user-thumbnail-box {
|
.user-thumbnail-box {
|
||||||
margin-right: $space-normal;
|
margin-right: $space-normal;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--details {
|
.contact--details {
|
||||||
|
margin-top: $space-small;
|
||||||
|
|
||||||
p {
|
p {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--info {
|
.contact--info {
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--name {
|
.contact--name {
|
||||||
|
@ -230,10 +244,13 @@ export default {
|
||||||
.contact--email {
|
.contact--email {
|
||||||
@include text-ellipsis;
|
@include text-ellipsis;
|
||||||
|
|
||||||
color: $color-body;
|
color: $color-gray;
|
||||||
display: block;
|
display: block;
|
||||||
line-height: $space-medium;
|
line-height: $space-medium;
|
||||||
text-decoration: underline;
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-woot;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.contact--bio {
|
.contact--bio {
|
||||||
|
@ -241,7 +258,8 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--details {
|
.conversation--details {
|
||||||
padding: $space-two $space-normal $space-two $space-medium;
|
border-top: 1px solid $color-border-light;
|
||||||
|
padding: $space-large $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--labels {
|
.conversation--labels {
|
||||||
|
@ -259,9 +277,18 @@ export default {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact-conversation--panel {
|
||||||
|
border-top: 1px solid $color-border-light;
|
||||||
|
}
|
||||||
|
|
||||||
.contact--mute {
|
.contact--mute {
|
||||||
color: $alert-color;
|
color: $alert-color;
|
||||||
display: block;
|
display: block;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.contact--actions {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -3,40 +3,44 @@
|
||||||
class="contact-conversation--panel sidebar-labels-wrap"
|
class="contact-conversation--panel sidebar-labels-wrap"
|
||||||
:class="hasEditedClass"
|
:class="hasEditedClass"
|
||||||
>
|
>
|
||||||
<div v-if="!conversationUiFlags.isFetching" class="wrap">
|
<div
|
||||||
<div class="contact-conversation--list">
|
v-if="!conversationUiFlags.isFetching"
|
||||||
<label class="select-tags">
|
class="contact-conversation--list"
|
||||||
{{ $t('CONTACT_PANEL.LABELS.TITLE') }}
|
>
|
||||||
<multiselect
|
<label class="select-tags">
|
||||||
v-model="selectedLabels"
|
<contact-details-item
|
||||||
:options="savedLabels"
|
:title="$t('CONTACT_PANEL.LABELS.TITLE')"
|
||||||
:tag-placeholder="$t('CONTACT_PANEL.LABELS.TAG_PLACEHOLDER')"
|
icon="ion-pricetags"
|
||||||
:placeholder="$t('CONTACT_PANEL.LABELS.PLACEHOLDER')"
|
/>
|
||||||
:multiple="true"
|
<multiselect
|
||||||
:taggable="true"
|
v-model="selectedLabels"
|
||||||
hide-selected
|
:options="savedLabels"
|
||||||
:show-labels="false"
|
:tag-placeholder="$t('CONTACT_PANEL.LABELS.TAG_PLACEHOLDER')"
|
||||||
@tag="addLabel"
|
:placeholder="$t('CONTACT_PANEL.LABELS.PLACEHOLDER')"
|
||||||
/>
|
:multiple="true"
|
||||||
</label>
|
:taggable="true"
|
||||||
<div class="row align-middle align-justify">
|
hide-selected
|
||||||
<span v-if="labelUiFlags.isError" class="error">{{
|
:show-labels="false"
|
||||||
$t('CONTACT_PANEL.LABELS.UPDATE_ERROR')
|
@tag="addLabel"
|
||||||
}}</span>
|
/>
|
||||||
<button
|
</label>
|
||||||
v-if="hasEdited"
|
<div class="row align-middle align-justify">
|
||||||
type="button"
|
<span v-if="labelUiFlags.isError" class="error">{{
|
||||||
class="button nice tiny"
|
$t('CONTACT_PANEL.LABELS.UPDATE_ERROR')
|
||||||
@click="onUpdateLabels"
|
}}</span>
|
||||||
>
|
<button
|
||||||
<spinner v-if="labelUiFlags.isUpdating" size="tiny" />
|
v-if="hasEdited"
|
||||||
{{
|
type="button"
|
||||||
labelUiFlags.isUpdating
|
class="button nice tiny"
|
||||||
? 'saving...'
|
@click="onUpdateLabels"
|
||||||
: $t('CONTACT_PANEL.LABELS.UPDATE_BUTTON')
|
>
|
||||||
}}
|
<spinner v-if="labelUiFlags.isUpdating" size="tiny" />
|
||||||
</button>
|
{{
|
||||||
</div>
|
labelUiFlags.isUpdating
|
||||||
|
? 'saving...'
|
||||||
|
: $t('CONTACT_PANEL.LABELS.UPDATE_BUTTON')
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<spinner v-else></spinner>
|
<spinner v-else></spinner>
|
||||||
|
@ -45,10 +49,12 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
import { mapGetters } from 'vuex';
|
||||||
|
import ContactDetailsItem from './ContactDetailsItem';
|
||||||
import Spinner from 'shared/components/Spinner';
|
import Spinner from 'shared/components/Spinner';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
ContactDetailsItem,
|
||||||
Spinner,
|
Spinner,
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -124,7 +130,7 @@ export default {
|
||||||
@import '~dashboard/assets/scss/mixins';
|
@import '~dashboard/assets/scss/mixins';
|
||||||
|
|
||||||
.contact-conversation--panel {
|
.contact-conversation--panel {
|
||||||
padding: $space-normal $space-normal $space-normal $space-medium;
|
padding: $space-normal;
|
||||||
}
|
}
|
||||||
|
|
||||||
.conversation--label {
|
.conversation--label {
|
||||||
|
@ -133,12 +139,8 @@ export default {
|
||||||
font-size: $font-size-small;
|
font-size: $font-size-small;
|
||||||
padding: $space-smaller;
|
padding: $space-smaller;
|
||||||
}
|
}
|
||||||
.wrap {
|
|
||||||
margin-top: $space-slab;
|
|
||||||
}
|
|
||||||
|
|
||||||
.select-tags {
|
.select-tags {
|
||||||
margin-top: $space-small;
|
|
||||||
.multiselect {
|
.multiselect {
|
||||||
&:hover {
|
&:hover {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
@ -156,6 +158,7 @@ export default {
|
||||||
.no-results-wrap {
|
.no-results-wrap {
|
||||||
padding: 0 $space-small;
|
padding: 0 $space-small;
|
||||||
}
|
}
|
||||||
|
|
||||||
.no-results {
|
.no-results {
|
||||||
margin: $space-normal 0 0 0;
|
margin: $space-normal 0 0 0;
|
||||||
color: $color-gray;
|
color: $color-gray;
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
/* eslint no-param-reassign: 0 */
|
/* eslint no-param-reassign: 0 */
|
||||||
import * as MutationHelpers from 'shared/helpers/vuex/mutationHelpers';
|
|
||||||
import * as types from '../mutation-types';
|
import * as types from '../mutation-types';
|
||||||
import ContactAPI from '../../api/contacts';
|
import ContactAPI from '../../api/contacts';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
records: [],
|
records: {},
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
isFetching: false,
|
isFetching: false,
|
||||||
isFetchingItem: false,
|
isFetchingItem: false,
|
||||||
|
@ -14,16 +14,14 @@ const state = {
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
getContacts($state) {
|
getContacts($state) {
|
||||||
return $state.records;
|
return Object.values($state.records);
|
||||||
},
|
},
|
||||||
getUIFlags($state) {
|
getUIFlags($state) {
|
||||||
return $state.uiFlags;
|
return $state.uiFlags;
|
||||||
},
|
},
|
||||||
getContact: $state => id => {
|
getContact: $state => id => {
|
||||||
const [contact = {}] = $state.records.filter(
|
const contact = $state.records[id];
|
||||||
record => record.id === Number(id)
|
return contact || {};
|
||||||
);
|
|
||||||
return contact;
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -71,9 +69,19 @@ export const mutations = {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
[types.default.SET_CONTACTS]: MutationHelpers.set,
|
[types.default.SET_CONTACTS]: ($state, data) => {
|
||||||
[types.default.SET_CONTACT_ITEM]: MutationHelpers.setSingleRecord,
|
data.forEach(contact => {
|
||||||
[types.default.EDIT_CONTACT]: MutationHelpers.update,
|
Vue.set($state.records, contact.id, contact);
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.default.SET_CONTACT_ITEM]: ($state, data) => {
|
||||||
|
Vue.set($state.records, data.id, data);
|
||||||
|
},
|
||||||
|
|
||||||
|
[types.default.EDIT_CONTACT]: ($state, data) => {
|
||||||
|
Vue.set($state.records, data.id, data);
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|
|
@ -1,6 +1,5 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import * as types from '../../mutation-types';
|
import * as types from '../../mutation-types';
|
||||||
|
|
||||||
import ConversationApi from '../../../api/inbox/conversation';
|
import ConversationApi from '../../../api/inbox/conversation';
|
||||||
import MessageApi from '../../../api/inbox/message';
|
import MessageApi from '../../../api/inbox/message';
|
||||||
import FBChannel from '../../../api/channel/fbChannel';
|
import FBChannel from '../../../api/channel/fbChannel';
|
||||||
|
@ -11,6 +10,10 @@ const actions = {
|
||||||
try {
|
try {
|
||||||
const response = await ConversationApi.show(conversationId);
|
const response = await ConversationApi.show(conversationId);
|
||||||
commit(types.default.ADD_CONVERSATION, response.data);
|
commit(types.default.ADD_CONVERSATION, response.data);
|
||||||
|
commit(
|
||||||
|
`contacts/${types.default.SET_CONTACT_ITEM}`,
|
||||||
|
response.data.meta.sender
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
@ -25,6 +28,10 @@ const actions = {
|
||||||
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
commit(types.default.SET_ALL_CONVERSATION, chatList);
|
||||||
commit(types.default.SET_CONV_TAB_META, metaData);
|
commit(types.default.SET_CONV_TAB_META, metaData);
|
||||||
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
commit(types.default.CLEAR_LIST_LOADING_STATUS);
|
||||||
|
commit(
|
||||||
|
`contacts/${types.default.SET_CONTACTS}`,
|
||||||
|
chatList.map(chat => chat.meta.sender)
|
||||||
|
);
|
||||||
dispatch(
|
dispatch(
|
||||||
'conversationPage/setCurrentPage',
|
'conversationPage/setCurrentPage',
|
||||||
{
|
{
|
||||||
|
@ -120,19 +127,13 @@ const actions = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
toggleStatus: async ({ commit, dispatch, getters }, data) => {
|
toggleStatus: async ({ commit }, data) => {
|
||||||
try {
|
try {
|
||||||
const nextChat = getters.getNextChatConversation;
|
|
||||||
const response = await ConversationApi.toggleStatus(data);
|
const response = await ConversationApi.toggleStatus(data);
|
||||||
commit(
|
commit(
|
||||||
types.default.RESOLVE_CONVERSATION,
|
types.default.RESOLVE_CONVERSATION,
|
||||||
response.data.payload.current_status
|
response.data.payload.current_status
|
||||||
);
|
);
|
||||||
if (nextChat) {
|
|
||||||
dispatch('setActiveChat', nextChat);
|
|
||||||
} else {
|
|
||||||
dispatch('clearSelectedState');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Handle error
|
// Handle error
|
||||||
}
|
}
|
||||||
|
@ -159,6 +160,10 @@ const actions = {
|
||||||
const { currentInbox } = state;
|
const { currentInbox } = state;
|
||||||
if (!currentInbox || Number(currentInbox) === conversation.inbox_id) {
|
if (!currentInbox || Number(currentInbox) === conversation.inbox_id) {
|
||||||
commit(types.default.ADD_CONVERSATION, conversation);
|
commit(types.default.ADD_CONVERSATION, conversation);
|
||||||
|
commit(
|
||||||
|
`contacts/${types.default.SET_CONTACT_ITEM}`,
|
||||||
|
conversation.meta.sender
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -203,6 +208,13 @@ const actions = {
|
||||||
commit(types.default.UPDATE_ASSIGNEE, data);
|
commit(types.default.UPDATE_ASSIGNEE, data);
|
||||||
},
|
},
|
||||||
|
|
||||||
|
updateConversationContact({ commit }, data) {
|
||||||
|
if (data.id) {
|
||||||
|
commit(`contacts/${types.default.SET_CONTACT_ITEM}`, data);
|
||||||
|
}
|
||||||
|
commit(types.default.UPDATE_CONVERSATION_CONTACT, data);
|
||||||
|
},
|
||||||
|
|
||||||
setActiveInbox({ commit }, inboxId) {
|
setActiveInbox({ commit }, inboxId) {
|
||||||
commit(types.default.SET_ACTIVE_INBOX, inboxId);
|
commit(types.default.SET_ACTIVE_INBOX, inboxId);
|
||||||
},
|
},
|
||||||
|
|
|
@ -8,9 +8,10 @@ export const getSelectedChatConversation = ({
|
||||||
|
|
||||||
// getters
|
// getters
|
||||||
const getters = {
|
const getters = {
|
||||||
getAllConversations: ({ allConversations }) => allConversations.sort(
|
getAllConversations: ({ allConversations }) =>
|
||||||
(a, b) => b.messages.last().created_at - a.messages.last().created_at
|
allConversations.sort(
|
||||||
),
|
(a, b) => b.messages.last()?.created_at - a.messages.last()?.created_at
|
||||||
|
),
|
||||||
getSelectedChat: ({ selectedChat }) => selectedChat,
|
getSelectedChat: ({ selectedChat }) => selectedChat,
|
||||||
getMineChats(_state) {
|
getMineChats(_state) {
|
||||||
const currentUserID = authAPI.getCurrentUser().id;
|
const currentUserID = authAPI.getCurrentUser().id;
|
||||||
|
@ -52,12 +53,12 @@ const getters = {
|
||||||
getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter,
|
getChatStatusFilter: ({ chatStatusFilter }) => chatStatusFilter,
|
||||||
getSelectedInbox: ({ currentInbox }) => currentInbox,
|
getSelectedInbox: ({ currentInbox }) => currentInbox,
|
||||||
getConvTabStats: ({ convTabStats }) => convTabStats,
|
getConvTabStats: ({ convTabStats }) => convTabStats,
|
||||||
getNextChatConversation: (_state) => {
|
getNextChatConversation: _state => {
|
||||||
const { selectedChat } = _state;
|
const { selectedChat } = _state;
|
||||||
const conversations = getters.getAllStatusChats(_state);
|
const conversations = getters.getAllStatusChats(_state);
|
||||||
if (conversations.length <= 1) {
|
if (conversations.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
};
|
}
|
||||||
const currentIndex = conversations.findIndex(
|
const currentIndex = conversations.findIndex(
|
||||||
conversation => conversation.id === selectedChat.id
|
conversation => conversation.id === selectedChat.id
|
||||||
);
|
);
|
||||||
|
|
|
@ -203,6 +203,16 @@ const mutations = {
|
||||||
chat.meta.assignee = payload.assignee;
|
chat.meta.assignee = payload.assignee;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
[types.default.UPDATE_CONVERSATION_CONTACT](
|
||||||
|
_state,
|
||||||
|
{ conversationId, ...payload }
|
||||||
|
) {
|
||||||
|
const [chat] = _state.allConversations.filter(c => c.id === conversationId);
|
||||||
|
if (chat) {
|
||||||
|
Vue.set(chat.meta, 'sender', payload);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
[types.default.SET_ACTIVE_INBOX](_state, inboxId) {
|
[types.default.SET_ACTIVE_INBOX](_state, inboxId) {
|
||||||
_state.currentInbox = inboxId;
|
_state.currentInbox = inboxId;
|
||||||
},
|
},
|
||||||
|
|
|
@ -4,14 +4,14 @@ import contactList from './fixtures';
|
||||||
describe('#getters', () => {
|
describe('#getters', () => {
|
||||||
it('getContacts', () => {
|
it('getContacts', () => {
|
||||||
const state = {
|
const state = {
|
||||||
records: contactList,
|
records: { 1: contactList[0] },
|
||||||
};
|
};
|
||||||
expect(getters.getContacts(state)).toEqual(contactList);
|
expect(getters.getContacts(state)).toEqual([contactList[0]]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('getContact', () => {
|
it('getContact', () => {
|
||||||
const state = {
|
const state = {
|
||||||
records: contactList,
|
records: { 2: contactList[1] },
|
||||||
};
|
};
|
||||||
expect(getters.getContact(state)(2)).toEqual(contactList[1]);
|
expect(getters.getContact(state)(2)).toEqual(contactList[1]);
|
||||||
});
|
});
|
||||||
|
|
|
@ -4,50 +4,54 @@ import { mutations } from '../../contacts';
|
||||||
describe('#mutations', () => {
|
describe('#mutations', () => {
|
||||||
describe('#SET_CONTACTS', () => {
|
describe('#SET_CONTACTS', () => {
|
||||||
it('set contact records', () => {
|
it('set contact records', () => {
|
||||||
const state = { records: [] };
|
const state = { records: {} };
|
||||||
mutations[types.default.SET_CONTACTS](state, [
|
mutations[types.default.SET_CONTACTS](state, [
|
||||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
]);
|
]);
|
||||||
expect(state.records).toEqual([
|
expect(state.records).toEqual({
|
||||||
{
|
1: {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'contact1',
|
name: 'contact1',
|
||||||
email: 'contact1@chatwoot.com',
|
email: 'contact1@chatwoot.com',
|
||||||
},
|
},
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#SET_CONTACT_ITEM', () => {
|
describe('#SET_CONTACT_ITEM', () => {
|
||||||
it('push contact data to the store', () => {
|
it('push contact data to the store', () => {
|
||||||
const state = {
|
const state = {
|
||||||
records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }],
|
records: {
|
||||||
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mutations[types.default.SET_CONTACT_ITEM](state, {
|
mutations[types.default.SET_CONTACT_ITEM](state, {
|
||||||
id: 2,
|
id: 2,
|
||||||
name: 'contact2',
|
name: 'contact2',
|
||||||
email: 'contact2@chatwoot.com',
|
email: 'contact2@chatwoot.com',
|
||||||
});
|
});
|
||||||
expect(state.records).toEqual([
|
expect(state.records).toEqual({
|
||||||
{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
{ id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
2: { id: 2, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('#EDIT_CONTACT', () => {
|
describe('#EDIT_CONTACT', () => {
|
||||||
it('update contact', () => {
|
it('update contact', () => {
|
||||||
const state = {
|
const state = {
|
||||||
records: [{ id: 1, name: 'contact1', email: 'contact1@chatwoot.com' }],
|
records: {
|
||||||
|
1: { id: 1, name: 'contact1', email: 'contact1@chatwoot.com' },
|
||||||
|
},
|
||||||
};
|
};
|
||||||
mutations[types.default.EDIT_CONTACT](state, {
|
mutations[types.default.EDIT_CONTACT](state, {
|
||||||
id: 1,
|
id: 1,
|
||||||
name: 'contact2',
|
name: 'contact2',
|
||||||
email: 'contact2@chatwoot.com',
|
email: 'contact2@chatwoot.com',
|
||||||
});
|
});
|
||||||
expect(state.records).toEqual([
|
expect(state.records).toEqual({
|
||||||
{ id: 1, name: 'contact2', email: 'contact2@chatwoot.com' },
|
1: { id: 1, name: 'contact2', email: 'contact2@chatwoot.com' },
|
||||||
]);
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -9,10 +9,16 @@ jest.mock('axios');
|
||||||
describe('#actions', () => {
|
describe('#actions', () => {
|
||||||
describe('#getConversation', () => {
|
describe('#getConversation', () => {
|
||||||
it('sends correct actions if API is success', async () => {
|
it('sends correct actions if API is success', async () => {
|
||||||
axios.get.mockResolvedValue({ data: { id: 1, meta: {} } });
|
axios.get.mockResolvedValue({
|
||||||
|
data: { id: 1, meta: { sender: { id: 1, name: 'Contact 1' } } },
|
||||||
|
});
|
||||||
await actions.getConversation({ commit }, 1);
|
await actions.getConversation({ commit }, 1);
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
[types.default.ADD_CONVERSATION, { id: 1, meta: {} }],
|
[
|
||||||
|
types.default.ADD_CONVERSATION,
|
||||||
|
{ id: 1, meta: { sender: { id: 1, name: 'Contact 1' } } },
|
||||||
|
],
|
||||||
|
['contacts/SET_CONTACT_ITEM', { id: 1, name: 'Contact 1' }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('sends correct actions if API is error', async () => {
|
it('sends correct actions if API is error', async () => {
|
||||||
|
|
|
@ -15,6 +15,7 @@ export default {
|
||||||
CLEAR_ALL_MESSAGES_LOADED: 'CLEAR_ALL_MESSAGES_LOADED',
|
CLEAR_ALL_MESSAGES_LOADED: 'CLEAR_ALL_MESSAGES_LOADED',
|
||||||
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
|
CHANGE_CHAT_STATUS_FILTER: 'CHANGE_CHAT_STATUS_FILTER',
|
||||||
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
UPDATE_ASSIGNEE: 'UPDATE_ASSIGNEE',
|
||||||
|
UPDATE_CONVERSATION_CONTACT: 'UPDATE_CONVERSATION_CONTACT',
|
||||||
|
|
||||||
// Active chat
|
// Active chat
|
||||||
CURRENT_CHAT_WINDOW: 'CURRENT_CHAT_WINDOW',
|
CURRENT_CHAT_WINDOW: 'CURRENT_CHAT_WINDOW',
|
||||||
|
|
8
app/javascript/shared/helpers/HTMLSanitizer.js
Normal file
8
app/javascript/shared/helpers/HTMLSanitizer.js
Normal file
|
@ -0,0 +1,8 @@
|
||||||
|
export const escapeHtml = (unsafe = '') => {
|
||||||
|
return unsafe
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''');
|
||||||
|
};
|
|
@ -1,6 +1,8 @@
|
||||||
|
import { escapeHtml } from './HTMLSanitizer';
|
||||||
|
|
||||||
class MessageFormatter {
|
class MessageFormatter {
|
||||||
constructor(message) {
|
constructor(message) {
|
||||||
this.message = message || '';
|
this.message = escapeHtml(message) || '';
|
||||||
}
|
}
|
||||||
|
|
||||||
formatMessage() {
|
formatMessage() {
|
||||||
|
|
|
@ -90,6 +90,13 @@ class ActionCableListener < BaseListener
|
||||||
broadcast(account, tokens, ASSIGNEE_CHANGED, conversation.push_event_data)
|
broadcast(account, tokens, ASSIGNEE_CHANGED, conversation.push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def conversation_contact_changed(event)
|
||||||
|
conversation, account = extract_conversation_and_account(event)
|
||||||
|
tokens = user_tokens(account, conversation.inbox.members)
|
||||||
|
|
||||||
|
broadcast(account, tokens, CONVERSATION_CONTACT_CHANGED, conversation.push_event_data)
|
||||||
|
end
|
||||||
|
|
||||||
def contact_created(event)
|
def contact_created(event)
|
||||||
contact, account = extract_contact_and_account(event)
|
contact, account = extract_contact_and_account(event)
|
||||||
tokens = user_tokens(account, account.agents)
|
tokens = user_tokens(account, account.agents)
|
||||||
|
|
|
@ -47,11 +47,15 @@ class Contact < ApplicationRecord
|
||||||
|
|
||||||
def push_event_data
|
def push_event_data
|
||||||
{
|
{
|
||||||
|
additional_attributes: additional_attributes,
|
||||||
|
email: email,
|
||||||
id: id,
|
id: id,
|
||||||
|
identifier: identifier,
|
||||||
name: name,
|
name: name,
|
||||||
|
phone_number: phone_number,
|
||||||
|
pubsub_token: pubsub_token,
|
||||||
thumbnail: avatar_url,
|
thumbnail: avatar_url,
|
||||||
type: 'contact',
|
type: 'contact'
|
||||||
pubsub_token: pubsub_token
|
|
||||||
}
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
|
@ -162,7 +162,8 @@ class Conversation < ApplicationRecord
|
||||||
CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? },
|
CONVERSATION_RESOLVED => -> { saved_change_to_status? && resolved? },
|
||||||
CONVERSATION_READ => -> { saved_change_to_user_last_seen_at? },
|
CONVERSATION_READ => -> { saved_change_to_user_last_seen_at? },
|
||||||
CONVERSATION_LOCK_TOGGLE => -> { saved_change_to_locked? },
|
CONVERSATION_LOCK_TOGGLE => -> { saved_change_to_locked? },
|
||||||
ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? }
|
ASSIGNEE_CHANGED => -> { saved_change_to_assignee_id? },
|
||||||
|
CONVERSATION_CONTACT_CHANGED => -> { saved_change_to_contact_id? }
|
||||||
}.each do |event, condition|
|
}.each do |event, condition|
|
||||||
condition.call && dispatcher_dispatch(event)
|
condition.call && dispatcher_dispatch(event)
|
||||||
end
|
end
|
||||||
|
|
|
@ -14,6 +14,7 @@ module Events::Types
|
||||||
CONVERSATION_OPENED = 'conversation.opened'
|
CONVERSATION_OPENED = 'conversation.opened'
|
||||||
CONVERSATION_RESOLVED = 'conversation.resolved'
|
CONVERSATION_RESOLVED = 'conversation.resolved'
|
||||||
CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle'
|
CONVERSATION_LOCK_TOGGLE = 'conversation.lock_toggle'
|
||||||
|
CONVERSATION_CONTACT_CHANGED = 'conversation.contact_changed'
|
||||||
ASSIGNEE_CHANGED = 'assignee.changed'
|
ASSIGNEE_CHANGED = 'assignee.changed'
|
||||||
CONVERSATION_TYPING_ON = 'conversation.typing_on'
|
CONVERSATION_TYPING_ON = 'conversation.typing_on'
|
||||||
CONVERSATION_TYPING_OFF = 'conversation.typing_off'
|
CONVERSATION_TYPING_OFF = 'conversation.typing_off'
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue