feat: Improve email rendering, introduce a new layout for emails (#5039)

Co-authored-by: Fayaz Ahmed <15716057+fayazara@users.noreply.github.com>
This commit is contained in:
Pranav Raj S 2022-08-01 10:53:50 +05:30 committed by GitHub
parent ef9ea99b91
commit 2c372fe315
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
19 changed files with 282 additions and 71 deletions

View file

@ -296,7 +296,7 @@
@include margin($zero $space-normal); @include margin($zero $space-normal);
--bubble-max-width: 49.6rem; --bubble-max-width: 49.6rem;
max-width: Min(var(--bubble-max-width), 85%); max-width: Min(var(--bubble-max-width), 84%);
.sender--name { .sender--name {
font-size: $font-size-mini; font-size: $font-size-mini;

View file

@ -1,5 +1,11 @@
<template> <template>
<div class="conversations-list-wrap"> <div
class="conversations-list-wrap"
:class="{
hide: !showConversationList,
'list--full-width': isOnExpandedLayout,
}"
>
<slot /> <slot />
<div <div
class="chat-list__top" class="chat-list__top"
@ -46,7 +52,7 @@
<woot-button <woot-button
v-else v-else
v-tooltip.top-end="$t('FILTER.TOOLTIP_LABEL')" v-tooltip.right="$t('FILTER.TOOLTIP_LABEL')"
variant="clear" variant="clear"
color-scheme="secondary" color-scheme="secondary"
icon="filter" icon="filter"
@ -210,6 +216,14 @@ export default {
type: [String, Number], type: [String, Number],
default: 0, default: 0,
}, },
showConversationList: {
default: true,
type: Boolean,
},
isOnExpandedLayout: {
default: false,
type: Boolean,
},
}, },
data() { data() {
return { return {
@ -696,6 +710,17 @@ export default {
@include breakpoint(xxxlarge up) { @include breakpoint(xxxlarge up) {
flex-basis: 46rem; flex-basis: 46rem;
} }
&.hide {
display: none;
}
&.list--full-width {
width: 100%;
@include breakpoint(xxxlarge up) {
flex-basis: 100%;
}
}
} }
.filter--actions { .filter--actions {
display: flex; display: flex;

View file

@ -1,9 +1,13 @@
<template> <template>
<div class="conversation-details-wrap"> <div
class="conversation-details-wrap"
:class="{ 'with-border-left': !isOnExpandedLayout }"
>
<conversation-header <conversation-header
v-if="currentChat.id" v-if="currentChat.id"
:chat="currentChat" :chat="currentChat"
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
:show-back-button="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
<woot-tabs <woot-tabs
@ -26,7 +30,7 @@
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
@contact-panel-toggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
<empty-state v-else /> <empty-state v-else :is-on-expanded-layout="isOnExpandedLayout" />
<div v-show="showContactPanel" class="conversation-sidebar-wrap"> <div v-show="showContactPanel" class="conversation-sidebar-wrap">
<contact-panel <contact-panel
v-if="showContactPanel" v-if="showContactPanel"
@ -71,6 +75,10 @@ export default {
type: Boolean, type: Boolean,
default: true, default: true,
}, },
isOnExpandedLayout: {
type: Boolean,
default: true,
},
}, },
data() { data() {
return { activeIndex: 0 }; return { activeIndex: 0 };
@ -134,8 +142,11 @@ export default {
flex-direction: column; flex-direction: column;
min-width: 0; min-width: 0;
width: 100%; width: 100%;
border-left: 1px solid var(--color-border);
background: var(--color-background-light); background: var(--color-background-light);
&.with-border-left {
border-left: 1px solid var(--color-border);
}
} }
.dashboard-app--tabs { .dashboard-app--tabs {

View file

@ -1,6 +1,7 @@
<template> <template>
<div class="conv-header"> <div class="conv-header">
<div class="user"> <div class="user">
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
<Thumbnail <Thumbnail
:src="currentContact.thumbnail" :src="currentContact.thumbnail"
size="40px" size="40px"
@ -47,19 +48,21 @@
</div> </div>
</template> </template>
<script> <script>
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import agentMixin from '../../../mixins/agentMixin.js'; import agentMixin from '../../../mixins/agentMixin.js';
import BackButton from '../BackButton';
import differenceInHours from 'date-fns/differenceInHours';
import eventListenerMixins from 'shared/mixins/eventListenerMixins'; import eventListenerMixins from 'shared/mixins/eventListenerMixins';
import inboxMixin from 'shared/mixins/inboxMixin'; import inboxMixin from 'shared/mixins/inboxMixin';
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import wootConstants from '../../../constants';
import differenceInHours from 'date-fns/differenceInHours';
import InboxName from '../InboxName'; import InboxName from '../InboxName';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import wootConstants from '../../../constants';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
export default { export default {
components: { components: {
BackButton,
InboxName, InboxName,
MoreActions, MoreActions,
Thumbnail, Thumbnail,
@ -74,6 +77,10 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showBackButton: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({
@ -83,6 +90,19 @@ export default {
chatMetadata() { chatMetadata() {
return this.chat.meta; return this.chat.meta;
}, },
backButtonUrl() {
const {
params: { accountId, inbox_id: inboxId, label, teamId },
name,
} = this.$route;
return conversationListPageURL({
accountId,
inboxId,
label,
teamId,
conversationType: name === 'conversation_mentions' ? 'mention' : '',
});
},
isHMACVerified() { isHMACVerified() {
if (!this.isAWebWidgetInbox) { if (!this.isAWebWidgetInbox) {
return true; return true;

View file

@ -35,7 +35,7 @@
<!-- No conversation selected --> <!-- No conversation selected -->
<div v-else-if="allConversations.length && !currentChat.id"> <div v-else-if="allConversations.length && !currentChat.id">
<img src="~dashboard/assets/images/chat.svg" alt="No Chat" /> <img src="~dashboard/assets/images/chat.svg" alt="No Chat" />
<span>{{ $t('CONVERSATION.404') }}</span> <span>{{ conversationMissingMessage }}</span>
</div> </div>
</div> </div>
</div> </div>
@ -51,6 +51,12 @@ export default {
OnboardingView, OnboardingView,
}, },
mixins: [accountMixin, adminMixin], mixins: [accountMixin, adminMixin],
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
computed: { computed: {
...mapGetters({ ...mapGetters({
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
@ -65,6 +71,12 @@ export default {
} }
return this.$t('CONVERSATION.LOADING_CONVERSATIONS'); return this.$t('CONVERSATION.LOADING_CONVERSATIONS');
}, },
conversationMissingMessage() {
if (!this.isOnExpandedLayout) {
return this.$t('CONVERSATION.SELECT_A_CONVERSATION');
}
return this.$t('CONVERSATION.404');
},
newInboxURL() { newInboxURL() {
return this.addAccountScoping('settings/inboxes/new'); return this.addAccountScoping('settings/inboxes/new');
}, },

View file

@ -170,7 +170,7 @@ export default {
}; };
}, },
computed: { computed: {
contentToBeParsed() { emailMessageContent() {
const { const {
html_content: { full: fullHTMLContent } = {}, html_content: { full: fullHTMLContent } = {},
text_content: { full: fullTextContent } = {}, text_content: { full: fullTextContent } = {},
@ -182,13 +182,19 @@ export default {
return false; return false;
} }
if (this.contentToBeParsed.includes('<blockquote')) { if (this.emailMessageContent.includes('<blockquote')) {
return true; return true;
} }
return false; return false;
}, },
message() { message() {
// If the message is an email, emailMessageContent would be present
// In that case, we would use letter package to render the email
if (this.emailMessageContent && this.isIncoming) {
return this.emailMessageContent;
}
const botMessageContent = generateBotMessageContent( const botMessageContent = generateBotMessageContent(
this.contentType, this.contentType,
this.contentAttributes, this.contentAttributes,
@ -200,21 +206,6 @@ export default {
}, },
} }
); );
const {
email: { content_type: contentType = '' } = {},
} = this.contentAttributes;
if (this.contentToBeParsed && this.isIncoming) {
const parsedContent = this.stripStyleCharacters(this.contentToBeParsed);
if (parsedContent) {
// This is a temporary fix for line-breaks in text/plain emails
// Now, It is not rendered properly in the email preview.
// FIXME: Remove this once we have a better solution for rendering text/plain emails
return contentType.includes('text/plain')
? parsedContent.replace(/\n/g, '<br />')
: parsedContent;
}
}
return ( return (
this.formatMessage( this.formatMessage(
this.data.content, this.data.content,
@ -331,6 +322,7 @@ export default {
'activity-wrap': !this.isBubble, 'activity-wrap': !this.isBubble,
'is-pending': this.isPending, 'is-pending': this.isPending,
'is-failed': this.isFailed, 'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
}; };
}, },
bubbleClass() { bubbleClass() {
@ -342,6 +334,7 @@ export default {
'is-text': this.hasText, 'is-text': this.hasText,
'is-from-bot': this.isSentByBot, 'is-from-bot': this.isSentByBot,
'is-failed': this.isFailed, 'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
}; };
}, },
isPending() { isPending() {
@ -518,6 +511,10 @@ export default {
} }
} }
.wrap.is-email {
--bubble-max-width: 84% !important;
}
.sender--info { .sender--info {
align-items: center; align-items: center;
color: var(--b-700); color: var(--b-700);

View file

@ -6,7 +6,8 @@
'hide--quoted': !showQuotedContent, 'hide--quoted': !showQuotedContent,
}" }"
> >
<div v-dompurify-html="message" class="text-content" /> <div v-if="!isEmail" v-dompurify-html="message" class="text-content" />
<letter v-else class="text-content" :html="message" />
<button <button
v-if="displayQuotedButton" v-if="displayQuotedButton"
class="quoted-text--button" class="quoted-text--button"
@ -25,7 +26,10 @@
</template> </template>
<script> <script>
import Letter from 'vue-letter';
export default { export default {
components: { Letter },
props: { props: {
message: { message: {
type: String, type: String,
@ -65,14 +69,16 @@ export default {
padding-left: var(--space-two); padding-left: var(--space-two);
} }
table { table {
all: revert; margin: 0;
border: 0;
td { td {
all: revert; margin: 0;
border: 0;
} }
tr { tr {
all: revert; border-bottom: 0 !important;
} }
} }

View file

@ -12,6 +12,10 @@ export default {
SNOOZED: 'snoozed', SNOOZED: 'snoozed',
ALL: 'all', ALL: 'all',
}, },
LAYOUT_TYPES: {
CONDENSED: 'condensed',
EXPANDED: 'expanded',
},
DOCS_URL: '//www.chatwoot.com/docs/product/', DOCS_URL: '//www.chatwoot.com/docs/product/',
}; };
export const DEFAULT_REDIRECT_URL = '/app/'; export const DEFAULT_REDIRECT_URL = '/app/';

View file

@ -44,6 +44,26 @@ export const conversationUrl = ({
return url; return url;
}; };
export const conversationListPageURL = ({
accountId,
conversationType = '',
inboxId,
label,
teamId,
}) => {
let url = `accounts/${accountId}/dashboard`;
if (label) {
url = `accounts/${accountId}/label/${label}`;
} else if (teamId) {
url = `accounts/${accountId}/team/${teamId}`;
} else if (conversationType === 'mention') {
url = `accounts/${accountId}/mentions/conversations`;
} else if (inboxId) {
url = `accounts/${accountId}/inbox/${inboxId}`;
}
return frontendURL(url);
};
export const isValidURL = value => { export const isValidURL = value => {
/* eslint-disable no-useless-escape */ /* eslint-disable no-useless-escape */
const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm; const URL_REGEX = /^https?:\/\/(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_\+.~#?&//=]*)$/gm;

View file

@ -3,9 +3,33 @@ import {
conversationUrl, conversationUrl,
isValidURL, isValidURL,
getLoginRedirectURL, getLoginRedirectURL,
conversationListPageURL,
} from '../URLHelper'; } from '../URLHelper';
describe('#URL Helpers', () => { describe('#URL Helpers', () => {
describe('conversationListPageURL', () => {
it('should return url to dashboard', () => {
expect(conversationListPageURL({ accountId: 1 })).toBe(
'/app/accounts/1/dashboard'
);
});
it('should return url to inbox', () => {
expect(conversationListPageURL({ accountId: 1, inboxId: 1 })).toBe(
'/app/accounts/1/inbox/1'
);
});
it('should return url to label', () => {
expect(conversationListPageURL({ accountId: 1, label: 'support' })).toBe(
'/app/accounts/1/label/support'
);
});
it('should return url to team', () => {
expect(conversationListPageURL({ accountId: 1, teamId: 1 })).toBe(
'/app/accounts/1/team/1'
);
});
});
describe('conversationUrl', () => { describe('conversationUrl', () => {
it('should return direct conversation URL if activeInbox is nil', () => { it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl({ accountId: 1, id: 1 })).toBe( expect(conversationUrl({ accountId: 1, id: 1 })).toBe(

View file

@ -1,6 +1,8 @@
{ {
"CONVERSATION": { "CONVERSATION": {
"404": "Please select a conversation from left pane", "SELECT_A_CONVERSATION": "Please select a conversation from left pane",
"404": "Sorry, we cannot find the conversation. Please try again",
"SWITCH_VIEW_LAYOUT": "Switch the layout",
"DASHBOARD_APP_TAB_MESSAGES": "Messages", "DASHBOARD_APP_TAB_MESSAGES": "Messages",
"UNVERIFIED_SESSION": "The identity of this user is not verified", "UNVERIFIED_SESSION": "The identity of this user is not verified",
"NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.", "NO_MESSAGE_1": "Uh oh! Looks like there are no messages from customers in your inbox.",

View file

@ -1,18 +1,25 @@
<template> <template>
<section class="conversation-page"> <section class="conversation-page">
<chat-list <chat-list
:show-conversation-list="showConversationList"
:conversation-inbox="inboxId" :conversation-inbox="inboxId"
:label="label" :label="label"
:team-id="teamId" :team-id="teamId"
:conversation-type="conversationType" :conversation-type="conversationType"
:folders-id="foldersId" :folders-id="foldersId"
:is-on-expanded-layout="isOnExpandedLayout"
@conversation-load="onConversationLoad" @conversation-load="onConversationLoad"
> >
<pop-over-search /> <pop-over-search
:is-on-expanded-layout="isOnExpandedLayout"
@toggle-conversation-layout="toggleConversationLayout"
/>
</chat-list> </chat-list>
<conversation-box <conversation-box
v-if="showMessageView"
:inbox-id="inboxId" :inbox-id="inboxId"
:is-contact-panel-open="isContactPanelOpen" :is-contact-panel-open="isContactPanelOpen"
:is-on-expanded-layout="isOnExpandedLayout"
@contact-panel-toggle="onToggleContactPanel" @contact-panel-toggle="onToggleContactPanel"
/> />
</section> </section>
@ -25,6 +32,7 @@ import ConversationBox from '../../../components/widgets/conversation/Conversati
import PopOverSearch from './search/PopOverSearch'; import PopOverSearch from './search/PopOverSearch';
import uiSettingsMixin from 'dashboard/mixins/uiSettings'; import uiSettingsMixin from 'dashboard/mixins/uiSettings';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import wootConstants from 'dashboard/constants';
export default { export default {
components: { components: {
@ -69,6 +77,21 @@ export default {
chatList: 'getAllConversations', chatList: 'getAllConversations',
currentChat: 'getSelectedChat', currentChat: 'getSelectedChat',
}), }),
showConversationList() {
return this.isOnExpandedLayout ? !this.conversationId : true;
},
showMessageView() {
return this.conversationId ? true : !this.isOnExpandedLayout;
},
isOnExpandedLayout() {
const {
LAYOUT_TYPES: { CONDENSED },
} = wootConstants;
const {
conversation_display_type: conversationDisplayType = CONDENSED,
} = this.uiSettings;
return conversationDisplayType !== CONDENSED;
},
isContactPanelOpen() { isContactPanelOpen() {
if (this.currentChat.id) { if (this.currentChat.id) {
const { const {
@ -101,6 +124,17 @@ export default {
this.$store.dispatch('setActiveInbox', this.inboxId); this.$store.dispatch('setActiveInbox', this.inboxId);
this.setActiveChat(); this.setActiveChat();
}, },
toggleConversationLayout() {
const { LAYOUT_TYPES } = wootConstants;
const {
conversation_display_type: conversationDisplayType = LAYOUT_TYPES.CONDENSED,
} = this.uiSettings;
const newViewType =
conversationDisplayType === LAYOUT_TYPES.CONDENSED
? LAYOUT_TYPES.EXPANDED
: LAYOUT_TYPES.CONDENSED;
this.updateUISettings({ conversation_display_type: newViewType });
},
fetchConversationIfUnavailable() { fetchConversationIfUnavailable() {
if (!this.conversationId) { if (!this.conversationId) {
return; return;

View file

@ -11,6 +11,10 @@
:placeholder="$t('CONVERSATION.SEARCH_MESSAGES')" :placeholder="$t('CONVERSATION.SEARCH_MESSAGES')"
@focus="onSearch" @focus="onSearch"
/> />
<switch-layout
:is-on-expanded-layout="isOnExpandedLayout"
@toggle="$emit('toggle-conversation-layout')"
/>
</div> </div>
<div v-if="showSearchBox" class="results-wrap"> <div v-if="showSearchBox" class="results-wrap">
<div class="show-results"> <div class="show-results">
@ -55,10 +59,11 @@ import { mapGetters } from 'vuex';
import timeMixin from '../../../../mixins/time'; import timeMixin from '../../../../mixins/time';
import ResultItem from './ResultItem'; import ResultItem from './ResultItem';
import messageFormatterMixin from 'shared/mixins/messageFormatterMixin'; import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import SwitchLayout from './SwitchLayout.vue';
export default { export default {
components: { components: {
ResultItem, ResultItem,
SwitchLayout,
}, },
directives: { directives: {
@ -71,6 +76,13 @@ export default {
mixins: [timeMixin, messageFormatterMixin, clickaway], mixins: [timeMixin, messageFormatterMixin, clickaway],
props: {
isOnExpandedLayout: {
type: Boolean,
required: true,
},
},
data() { data() {
return { return {
searchTerm: '', searchTerm: '',

View file

@ -0,0 +1,59 @@
<template>
<button
v-tooltip.left="$t('CONVERSATION.SWITCH_VIEW_LAYOUT')"
class="layout-switch__container"
@click="toggle"
>
<div
class="layout-switch__btn left"
:class="{ active: !isOnExpandedLayout }"
>
<fluent-icon icon="panel-sidebar" :size="16" />
</div>
<div
class="layout-switch__btn right"
:class="{ active: isOnExpandedLayout }"
>
<fluent-icon icon="panel-contract" :size="16" />
</div>
</button>
</template>
<script>
export default {
props: {
isOnExpandedLayout: {
type: Boolean,
default: false,
},
},
methods: {
toggle() {
this.$emit('toggle');
},
},
};
</script>
<style lang="scss" soped>
.layout-switch__container {
align-items: center;
background-color: var(--s-100);
border-radius: var(--border-radius-medium);
cursor: pointer;
display: flex;
padding: var(--space-micro);
.layout-switch__btn {
border-radius: var(--border-radius-normal);
color: var(--s-400);
display: flex;
padding: var(--space-micro) var(--space-smaller);
&.active {
background-color: var(--white);
color: var(--w-500);
}
}
}
</style>

View file

@ -103,6 +103,8 @@
"microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z", "microphone-play-outline": "M8,5.14V19.14L19,12.14L8,5.14Z",
"number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z", "number-symbol-outline": "M10.987 2.89a.75.75 0 1 0-1.474-.28L8.494 7.999 3.75 8a.75.75 0 1 0 0 1.5l4.46-.002-.946 5-4.514.002a.75.75 0 0 0 0 1.5l4.23-.002-.967 5.116a.75.75 0 1 0 1.474.278l1.02-5.395 5.474-.002-.968 5.119a.75.75 0 1 0 1.474.278l1.021-5.398 4.742-.002a.75.75 0 1 0 0-1.5l-4.458.002.946-5 4.512-.002a.75.75 0 1 0 0-1.5l-4.229.002.966-5.104a.75.75 0 0 0-1.474-.28l-1.018 5.385-5.474.002.966-5.107Zm-1.25 6.608 5.474-.003-.946 5-5.474.002.946-5Z",
"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", "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",
"panel-sidebar-outline": "M4.75 4A2.75 2.75 0 0 0 2 6.75v10.5A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75ZM9 18.5v-13h10.25c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9ZM5.5 3.5h4M5.5 5.5h4M5.5 7.5h4M5.5 9.5h4",
"panel-contract-outline": "M14.193 14.751a.75.75 0 0 0 1.059.056l2.5-2.25a.75.75 0 0 0 0-1.114l-2.5-2.25a.75.75 0 0 0-1.004 1.115l1.048.942H11.75a.75.75 0 1 0 0 1.5h3.546l-1.048.942a.75.75 0 0 0-.055 1.06ZM2 6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75ZM9 5.5v13h10.25c.69 0 1.25-.56 1.25-1.25V6.75c0-.69-.56-1.25-1.25-1.25H9Z",
"pen-outline": "M7.5 2.75a.75.75 0 0 0-1.5 0v3a1.75 1.75 0 0 0 1.543 1.738L6.527 9.993a3.868 3.868 0 0 0 .119 3.143l3.99 7.95c.283.566.803.914 1.364.914s1.08-.348 1.365-.913l3.99-7.951c.481-.96.526-2.137.118-3.143l-1.016-2.505A1.75 1.75 0 0 0 18 5.75v-3a.75.75 0 0 0-1.5 0v3a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-3Zm7.343 4.75 1.24 3.057c.247.61.217 1.336-.07 1.906l-3.263 6.504v-6.668a1.5 1.5 0 1 0-1.5 0v6.668l-3.264-6.504a2.368 2.368 0 0 1-.069-1.906L9.157 7.5h5.686Z", "pen-outline": "M7.5 2.75a.75.75 0 0 0-1.5 0v3a1.75 1.75 0 0 0 1.543 1.738L6.527 9.993a3.868 3.868 0 0 0 .119 3.143l3.99 7.95c.283.566.803.914 1.364.914s1.08-.348 1.365-.913l3.99-7.951c.481-.96.526-2.137.118-3.143l-1.016-2.505A1.75 1.75 0 0 0 18 5.75v-3a.75.75 0 0 0-1.5 0v3a.25.25 0 0 1-.25.25h-8.5a.25.25 0 0 1-.25-.25v-3Zm7.343 4.75 1.24 3.057c.247.61.217 1.336-.07 1.906l-3.263 6.504v-6.668a1.5 1.5 0 1 0-1.5 0v6.668l-3.264-6.504a2.368 2.368 0 0 1-.069-1.906L9.157 7.5h5.686Z",
"people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z", "people-outline": "M4 13.999 13 14a2 2 0 0 1 1.995 1.85L15 16v1.5C14.999 21 11.284 22 8.5 22c-2.722 0-6.335-.956-6.495-4.27L2 17.5v-1.501c0-1.054.816-1.918 1.85-1.995L4 14ZM15.22 14H20c1.054 0 1.918.816 1.994 1.85L22 16v1c-.001 3.062-2.858 4-5 4a7.16 7.16 0 0 1-2.14-.322c.336-.386.607-.827.802-1.327A6.19 6.19 0 0 0 17 19.5l.267-.006c.985-.043 3.086-.363 3.226-2.289L20.5 17v-1a.501.501 0 0 0-.41-.492L20 15.5h-4.051a2.957 2.957 0 0 0-.595-1.34L15.22 14H20h-4.78ZM4 15.499l-.1.01a.51.51 0 0 0-.254.136.506.506 0 0 0-.136.253l-.01.101V17.5c0 1.009.45 1.722 1.417 2.242.826.445 2.003.714 3.266.753l.317.005.317-.005c1.263-.039 2.439-.308 3.266-.753.906-.488 1.359-1.145 1.412-2.057l.005-.186V16a.501.501 0 0 0-.41-.492L13 15.5l-9-.001ZM8.5 3a4.5 4.5 0 1 1 0 9 4.5 4.5 0 0 1 0-9Zm9 2a3.5 3.5 0 1 1 0 7 3.5 3.5 0 0 1 0-7Zm-9-.5c-1.654 0-3 1.346-3 3s1.346 3 3 3 3-1.346 3-3-1.346-3-3-3Zm9 2c-1.103 0-2 .897-2 2s.897 2 2 2 2-.897 2-2-.897-2-2-2Z",
"people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z", "people-team-outline": "M14.754 10c.966 0 1.75.784 1.75 1.75v4.749a4.501 4.501 0 0 1-9.002 0V11.75c0-.966.783-1.75 1.75-1.75h5.502Zm0 1.5H9.252a.25.25 0 0 0-.25.25v4.749a3.001 3.001 0 0 0 6.002 0V11.75a.25.25 0 0 0-.25-.25ZM3.75 10h3.381a2.738 2.738 0 0 0-.618 1.5H3.75a.25.25 0 0 0-.25.25v3.249a2.501 2.501 0 0 0 3.082 2.433c.085.504.24.985.453 1.432A4.001 4.001 0 0 1 2 14.999V11.75c0-.966.784-1.75 1.75-1.75Zm13.125 0h3.375c.966 0 1.75.784 1.75 1.75V15a4 4 0 0 1-5.03 3.866c.214-.448.369-.929.455-1.433A2.5 2.5 0 0 0 20.5 15v-3.25a.25.25 0 0 0-.25-.25h-2.757a2.738 2.738 0 0 0-.618-1.5ZM12 3a3 3 0 1 1 0 6 3 3 0 0 1 0-6Zm6.5 1a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm-13 0a2.5 2.5 0 1 1 0 5 2.5 2.5 0 0 1 0-5Zm6.5.5a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3Zm6.5 1a1 1 0 1 0 0 2 1 1 0 0 0 0-2Zm-13 0a1 1 0 1 0 0 2 1 1 0 0 0 0-2Z",
@ -145,6 +147,10 @@
"subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z", "subtract-solid": "M3.997 13H20a1 1 0 1 0 0-2H3.997a1 1 0 1 0 0 2Z",
"drag-outline": "M15 3.707V8.5a.5.5 0 0 0 1 0V3.707l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.499.499 0 0 0-.708 0l-2 2a.5.5 0 0 0 .708.708L15 3.707ZM2 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm0 5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm.5 4.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM15 16.293V11.5a.5.5 0 0 1 1 0v4.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L15 16.293Z", "drag-outline": "M15 3.707V8.5a.5.5 0 0 0 1 0V3.707l1.146 1.147a.5.5 0 0 0 .708-.708l-2-2a.499.499 0 0 0-.708 0l-2 2a.5.5 0 0 0 .708.708L15 3.707ZM2 4.5a.5.5 0 0 1 .5-.5h6a.5.5 0 0 1 0 1h-6a.5.5 0 0 1-.5-.5Zm0 5a.5.5 0 0 1 .5-.5h9a.5.5 0 0 1 0 1h-9a.5.5 0 0 1-.5-.5Zm.5 4.5a.5.5 0 0 0 0 1h6a.5.5 0 0 0 0-1h-6ZM15 16.293V11.5a.5.5 0 0 1 1 0v4.793l1.146-1.147a.5.5 0 0 1 .708.708l-2 2a.5.5 0 0 1-.708 0l-2-2a.5.5 0 0 1 .708-.708L15 16.293Z",
"pane-open-outline": "M14.807 9.249a.75.75 0 0 0-1.059-.056l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 0 0 1.004-1.115l-1.048-.942h3.546a.75.75 0 1 0 0-1.5h-3.546l1.048-.942a.75.75 0 0 0 .055-1.059ZM2 17.251A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75A2.75 2.75 0 0 0 2 6.75v10.5Zm2.75 1.25c-.69 0-1.25-.56-1.25-1.25V6.749c0-.69.56-1.25 1.25-1.25h3.254V18.5H4.75Zm4.754 0V5.5h9.746c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9.504Z", "pane-open-outline": "M14.807 9.249a.75.75 0 0 0-1.059-.056l-2.5 2.25a.75.75 0 0 0 0 1.114l2.5 2.25a.75.75 0 0 0 1.004-1.115l-1.048-.942h3.546a.75.75 0 1 0 0-1.5h-3.546l1.048-.942a.75.75 0 0 0 .055-1.059ZM2 17.251A2.75 2.75 0 0 0 4.75 20h14.5A2.75 2.75 0 0 0 22 17.25V6.75A2.75 2.75 0 0 0 19.25 4H4.75A2.75 2.75 0 0 0 2 6.75v10.5Zm2.75 1.25c-.69 0-1.25-.56-1.25-1.25V6.749c0-.69.56-1.25 1.25-1.25h3.254V18.5H4.75Zm4.754 0V5.5h9.746c.69 0 1.25.56 1.25 1.25v10.5c0 .69-.56 1.25-1.25 1.25H9.504Z",
"table-switch-outline": [
"M8.78 1.22a.75.75 0 0 0-1.06 1.06l.72.72H6.25A3.25 3.25 0 0 0 3 6.25v2.19l-.72-.72a.75.75 0 0 0-1.06 1.06l2 2a.748.748 0 0 0 1.06 0l2-2a.747.747 0 0 0 0-1.06.75.75 0 0 0-1.06 0l-.72.72V6.25c0-.966.784-1.75 1.75-1.75h2.19l-.72.72a.75.75 0 0 0 .78 1.237.75.75 0 0 0 .28-.177l2-2a.75.75 0 0 0 0-1.06l-2-2Z",
"M11.832 3a1.755 1.755 0 0 1 0 1.5H14v4h-4V6.475l-.513.512a1.742 1.742 0 0 1-.987.495V8.5H7.482c-.052.361-.217.71-.495.987L6.475 10H8.5v4h-4v-2.168a1.755 1.755 0 0 1-1.5 0v5.918A3.25 3.25 0 0 0 6.25 21h11.5A3.25 3.25 0 0 0 21 17.75V6.25A3.25 3.25 0 0 0 17.75 3h-5.918ZM10 14v-4h4v4h-4Zm5.5 0v-4h4v4h-4ZM14 15.5v4h-4v-4h4Zm1.5 4v-4h4v2.25a1.75 1.75 0 0 1-1.75 1.75H15.5Zm0-11v-4h2.25c.966 0 1.75.784 1.75 1.75V8.5h-4Zm-11 7h4v4H6.25a1.75 1.75 0 0 1-1.75-1.75V15.5Z"
],
"pane-close-outline": "M9.193 9.249a.75.75 0 0 1 1.059-.056l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.004-1.115l1.048-.942H6.75a.75.75 0 1 1 0-1.5h3.546l-1.048-.942a.75.75 0 0 1-.055-1.06ZM22 17.25A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5Zm-2.75 1.25c.69 0 1.25-.56 1.25-1.25V6.749c0-.69-.56-1.25-1.25-1.25h-3.254V18.5h3.254Zm-4.754 0V5.5H4.75c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h9.746Z", "pane-close-outline": "M9.193 9.249a.75.75 0 0 1 1.059-.056l2.5 2.25a.75.75 0 0 1 0 1.114l-2.5 2.25a.75.75 0 0 1-1.004-1.115l1.048-.942H6.75a.75.75 0 1 1 0-1.5h3.546l-1.048-.942a.75.75 0 0 1-.055-1.06ZM22 17.25A2.75 2.75 0 0 1 19.25 20H4.75A2.75 2.75 0 0 1 2 17.25V6.75A2.75 2.75 0 0 1 4.75 4h14.5A2.75 2.75 0 0 1 22 6.75v10.5Zm-2.75 1.25c.69 0 1.25-.56 1.25-1.25V6.749c0-.69-.56-1.25-1.25-1.25h-3.254V18.5h3.254Zm-4.754 0V5.5H4.75c-.69 0-1.25.56-1.25 1.25v10.5c0 .69.56 1.25 1.25 1.25h9.746Z",
"chevron-left-solid": "M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0Z", "chevron-left-solid": "M15.707 4.293a1 1 0 0 1 0 1.414L9.414 12l6.293 6.293a1 1 0 0 1-1.414 1.414l-7-7a1 1 0 0 1 0-1.414l7-7a1 1 0 0 1 1.414 0Z",
"chevron-right-solid": "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" "chevron-right-solid": "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"

View file

@ -1,5 +1,4 @@
import MessageFormatter from '../helpers/MessageFormatter'; import MessageFormatter from '../helpers/MessageFormatter';
import DOMPurify from 'dompurify';
export default { export default {
methods: { methods: {
@ -22,25 +21,5 @@ export default {
return `${description.slice(0, 97)}...`; return `${description.slice(0, 97)}...`;
}, },
stripStyleCharacters(message) {
return DOMPurify.sanitize(message, {
FORBID_TAGS: ['style'],
FORBID_ATTR: [
'id',
'class',
'style',
'bgcolor',
'valign',
'width',
'face',
'color',
'height',
'lang',
'align',
'size',
'border',
],
});
},
}, },
}; };

View file

@ -14,17 +14,4 @@ describe('messageFormatterMixin', () => {
'Chatwoot is an opensource tool. https://www.chatwoot.com' 'Chatwoot is an opensource tool. https://www.chatwoot.com'
); );
}); });
it('stripStyleCharacters returns message without style tags', () => {
const Component = {
render() {},
mixins: [messageFormatterMixin],
};
const wrapper = shallowMount(Component);
const message =
'<b style="max-width:100%">Chatwoot is an opensource tool. https://www.chatwoot.com</b><style type="css">.message{}</style>';
expect(wrapper.vm.stripStyleCharacters(message)).toMatch(
'<b>Chatwoot is an opensource tool. https://www.chatwoot.com</b>'
);
});
}); });

View file

@ -67,6 +67,7 @@
"vue-dompurify-html": "^2.5.2", "vue-dompurify-html": "^2.5.2",
"vue-easytable": "2.5.5", "vue-easytable": "2.5.5",
"vue-i18n": "8.24.3", "vue-i18n": "8.24.3",
"vue-letter": "^0.1.3",
"vue-loader": "15.9.6", "vue-loader": "15.9.6",
"vue-multiselect": "~2.1.6", "vue-multiselect": "~2.1.6",
"vue-router": "~3.5.2", "vue-router": "~3.5.2",

View file

@ -9941,6 +9941,11 @@ lazy-universal-dotenv@^3.0.1:
dotenv "^8.0.0" dotenv "^8.0.0"
dotenv-expand "^5.1.0" dotenv-expand "^5.1.0"
lettersanitizer@^1.0.0:
version "1.0.2"
resolved "https://registry.yarnpkg.com/lettersanitizer/-/lettersanitizer-1.0.2.tgz#902b7b4deea0e52f0400e0d268921f489f5ce100"
integrity sha512-f035TAen0M3Oh5Nhe8hp2uA5zQwN1rIDdSfC1VNLXd1XoeIm1r/whVCB1YPQH/uN1KhRrjCJcHl0moCpXj0X8A==
leven@^3.1.0: leven@^3.1.0:
version "3.1.0" version "3.1.0"
resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2"
@ -15743,6 +15748,13 @@ vue-jest@4:
extract-from-css "^0.4.4" extract-from-css "^0.4.4"
source-map "0.5.6" source-map "0.5.6"
vue-letter@^0.1.3:
version "0.1.3"
resolved "https://registry.yarnpkg.com/vue-letter/-/vue-letter-0.1.3.tgz#29cfb1768e80382bdebbaeada3d012fd37c47f52"
integrity sha512-tobFN1Ri3QJuPJGG+/6ITZuW9DnJ0trIF8AZqhexVKS49lMI560J/DYryTWrITjb7xYicJfDEMXtAFDxxcG3qQ==
dependencies:
lettersanitizer "^1.0.0"
vue-loader@15.9.6: vue-loader@15.9.6:
version "15.9.6" version "15.9.6"
resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b" resolved "https://registry.yarnpkg.com/vue-loader/-/vue-loader-15.9.6.tgz#f4bb9ae20c3a8370af3ecf09b8126d38ffdb6b8b"