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
19 changed files with 282 additions and 71 deletions

View File

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

View File

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

View File

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

View File

@@ -1,6 +1,7 @@
<template>
<div class="conv-header">
<div class="user">
<back-button v-if="showBackButton" :back-url="backButtonUrl" />
<Thumbnail
:src="currentContact.thumbnail"
size="40px"
@@ -47,19 +48,21 @@
</div>
</template>
<script>
import { hasPressedAltAndOKey } from 'shared/helpers/KeyboardHelpers';
import { mapGetters } from 'vuex';
import MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import agentMixin from '../../../mixins/agentMixin.js';
import BackButton from '../BackButton';
import differenceInHours from 'date-fns/differenceInHours';
import eventListenerMixins from 'shared/mixins/eventListenerMixins';
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 MoreActions from './MoreActions';
import Thumbnail from '../Thumbnail';
import wootConstants from '../../../constants';
import { conversationListPageURL } from 'dashboard/helper/URLHelper';
export default {
components: {
BackButton,
InboxName,
MoreActions,
Thumbnail,
@@ -74,6 +77,10 @@ export default {
type: Boolean,
default: false,
},
showBackButton: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({
@@ -83,6 +90,19 @@ export default {
chatMetadata() {
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() {
if (!this.isAWebWidgetInbox) {
return true;

View File

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

View File

@@ -170,7 +170,7 @@ export default {
};
},
computed: {
contentToBeParsed() {
emailMessageContent() {
const {
html_content: { full: fullHTMLContent } = {},
text_content: { full: fullTextContent } = {},
@@ -182,13 +182,19 @@ export default {
return false;
}
if (this.contentToBeParsed.includes('<blockquote')) {
if (this.emailMessageContent.includes('<blockquote')) {
return true;
}
return false;
},
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(
this.contentType,
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 (
this.formatMessage(
this.data.content,
@@ -331,6 +322,7 @@ export default {
'activity-wrap': !this.isBubble,
'is-pending': this.isPending,
'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
};
},
bubbleClass() {
@@ -342,6 +334,7 @@ export default {
'is-text': this.hasText,
'is-from-bot': this.isSentByBot,
'is-failed': this.isFailed,
'is-email': this.isEmailContentType,
};
},
isPending() {
@@ -518,6 +511,10 @@ export default {
}
}
.wrap.is-email {
--bubble-max-width: 84% !important;
}
.sender--info {
align-items: center;
color: var(--b-700);

View File

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

View File

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

View File

@@ -44,6 +44,26 @@ export const conversationUrl = ({
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 => {
/* 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;

View File

@@ -3,9 +3,33 @@ import {
conversationUrl,
isValidURL,
getLoginRedirectURL,
conversationListPageURL,
} from '../URLHelper';
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', () => {
it('should return direct conversation URL if activeInbox is nil', () => {
expect(conversationUrl({ accountId: 1, id: 1 })).toBe(

View File

@@ -1,6 +1,8 @@
{
"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",
"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.",

View File

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

View File

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