feat: Use vue-router on widget route management (#3415)

* feat: Add vue-router to widget

Co-authored-by: Pranav <pranav@chatwoot.com>

* Move to dynamic imports

* Move to routerMixin

* Fix popup button display

* Remove unnecessary import

* router -> route

* Fix open state

* Fix issues

* Remove used CSS

* Fix specs

* Fix specs

* Fix widgetColor specs

* Fix mutation specs

* Fixes broken lint errors

* Fixes issues with widget flow

Co-authored-by: Nithin <nithin@chatwoot.com>
Co-authored-by: Nithin David <1277421+nithindavid@users.noreply.github.com>
Co-authored-by: Muhsin Keloth <muhsinkeramam@gmail.com>
This commit is contained in:
Pranav Raj S 2022-01-12 02:55:27 -08:00 committed by GitHub
parent 991a42c417
commit 9c31d7c672
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 617 additions and 725 deletions

View file

@ -18,11 +18,11 @@ export default {
},
backgroundColor: {
type: String,
default: 'white',
default: '#c2e1ff',
},
color: {
type: String,
default: '',
default: '#1976cc',
},
customStyle: {
type: Object,

View file

@ -7,6 +7,7 @@ import ActionCableConnector from '../widget/helpers/actionCable';
import { getAlertAudio } from 'shared/helpers/AudioNotificationHelper';
import i18n from '../widget/i18n';
import router from '../widget/router';
Vue.use(VueI18n);
Vue.use(Vuelidate);
@ -22,6 +23,7 @@ Vue.config.productionTip = false;
window.onload = () => {
window.WOOT_WIDGET = new Vue({
router,
store,
i18n: i18nConfig,
render: h => h(App),

View file

@ -150,11 +150,14 @@ export const IFrameHelper = {
onBubbleClick(bubbleState);
},
closeWindow: () => {
onBubbleClick({ toggleValue: false });
removeUnreadClass();
},
onBubbleToggle: isOpen => {
IFrameHelper.sendMessage('toggle-open', { isOpen });
if (!isOpen) {
IFrameHelper.events.resetUnreadMode();
} else {
if (isOpen) {
IFrameHelper.pushEvent('webwidget.triggered');
}
},
@ -164,28 +167,6 @@ export const IFrameHelper = {
referrerHost,
});
},
setUnreadMode: message => {
const { unreadMessageCount } = message;
const { isOpen } = window.$chatwoot;
const toggleValue = true;
if (!isOpen && unreadMessageCount > 0) {
IFrameHelper.sendMessage('set-unread-view');
onBubbleClick({ toggleValue });
addUnreadClass();
}
},
setCampaignMode: () => {
const { isOpen } = window.$chatwoot;
const toggleValue = true;
if (!isOpen) {
onBubbleClick({ toggleValue });
addUnreadClass();
}
},
updateIframeHeight: message => {
const { extraHeight = 0, isFixedHeight } = message;
if (!extraHeight) return;
@ -193,11 +174,12 @@ export const IFrameHelper = {
IFrameHelper.setFrameHeightToFitContent(extraHeight, isFixedHeight);
},
resetUnreadMode: () => {
IFrameHelper.sendMessage('unset-unread-view');
removeUnreadClass();
setUnreadMode: () => {
addUnreadClass();
onBubbleClick({ toggleValue: true });
},
resetUnreadMode: () => removeUnreadClass(),
handleNotificationDot: event => {
if (window.$chatwoot.hideMessageBubble) {
return;
@ -253,14 +235,10 @@ export const IFrameHelper = {
}
},
toggleCloseButton: () => {
let isMobile = false;
if (window.matchMedia('(max-width: 668px)').matches) {
IFrameHelper.sendMessage('toggle-close-button', {
showClose: true,
});
} else {
IFrameHelper.sendMessage('toggle-close-button', {
showClose: false,
});
isMobile = true;
}
IFrameHelper.sendMessage('toggle-close-button', { isMobile });
},
};

View file

@ -2,6 +2,7 @@
"arrow-clockwise-outline": "M12 4.75a7.25 7.25 0 1 0 7.201 6.406c-.068-.588.358-1.156.95-1.156.515 0 .968.358 1.03.87a9.25 9.25 0 1 1-3.432-6.116V4.25a1 1 0 1 1 2.001 0v2.698l.034.052h-.034v.25a1 1 0 0 1-1 1h-3a1 1 0 1 1 0-2h.666A7.219 7.219 0 0 0 12 4.75Z",
"arrow-right-outline": "M13.267 4.209a.75.75 0 0 0-1.034 1.086l6.251 5.955H3.75a.75.75 0 0 0 0 1.5h14.734l-6.251 5.954a.75.75 0 0 0 1.034 1.087l7.42-7.067a.996.996 0 0 0 .3-.58.758.758 0 0 0-.001-.29.995.995 0 0 0-.3-.578l-7.419-7.067Z",
"attach-outline": "M11.772 3.743a6 6 0 0 1 8.66 8.302l-.19.197-8.8 8.798-.036.03a3.723 3.723 0 0 1-5.489-4.973.764.764 0 0 1 .085-.13l.054-.06.086-.088.142-.148.002.003 7.436-7.454a.75.75 0 0 1 .977-.074l.084.073a.75.75 0 0 1 .074.976l-.073.084-7.594 7.613a2.23 2.23 0 0 0 3.174 3.106l8.832-8.83A4.502 4.502 0 0 0 13 4.644l-.168.16-.013.014-9.536 9.536a.75.75 0 0 1-1.133-.977l.072-.084 9.549-9.55h.002Z",
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",
"dismiss-outline": "m4.397 4.554.073-.084a.75.75 0 0 1 .976-.073l.084.073L12 10.939l6.47-6.47a.75.75 0 1 1 1.06 1.061L13.061 12l6.47 6.47a.75.75 0 0 1 .072.976l-.073.084a.75.75 0 0 1-.976.073l-.084-.073L12 13.061l-6.47 6.47a.75.75 0 0 1-1.06-1.061L10.939 12l-6.47-6.47a.75.75 0 0 1-.072-.976l.073-.084-.073.084Z",
"document-outline": "M18.5 20a.5.5 0 0 1-.5.5H6a.5.5 0 0 1-.5-.5V4a.5.5 0 0 1 .5-.5h6V8a2 2 0 0 0 2 2h4.5v10Zm-5-15.379L17.378 8.5H14a.5.5 0 0 1-.5-.5V4.621Zm5.914 3.793-5.829-5.828c-.026-.026-.058-.046-.085-.07a2.072 2.072 0 0 0-.219-.18c-.04-.027-.086-.045-.128-.068-.071-.04-.141-.084-.216-.116a1.977 1.977 0 0 0-.624-.138C12.266 2.011 12.22 2 12.172 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V9.828a2 2 0 0 0-.586-1.414Z",

View file

@ -1,61 +1,67 @@
<template>
<router
:show-unread-view="showUnreadView"
:show-campaign-view="showCampaignView"
:is-mobile="isMobile"
:has-fetched="hasFetched"
:unread-message-count="unreadMessageCount"
:is-left-aligned="isLeftAligned"
:hide-message-bubble="hideMessageBubble"
:show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/>
<div
v-if="!conversationSize && isFetchingList"
class="flex flex-1 items-center h-full bg-black-25 justify-center"
>
<spinner size="" />
</div>
<div
v-else
class="flex flex-col justify-end h-full"
:class="{
'is-mobile': isMobile,
'is-widget-right': isRightAligned,
'is-bubble-hidden': hideMessageBubble,
}"
>
<router-view></router-view>
</div>
</template>
<script>
import { mapGetters, mapActions, mapMutations } from 'vuex';
import { mapGetters, mapActions } from 'vuex';
import { setHeader } from 'widget/helpers/axios';
import { IFrameHelper, RNHelper } from 'widget/helpers/utils';
import configMixin from './mixins/configMixin';
import availabilityMixin from 'widget/mixins/availability';
import Router from './views/Router';
import { getLocale } from './helpers/urlParamsHelper';
import { isEmptyObject } from 'widget/helpers/utils';
import Spinner from 'shared/components/Spinner.vue';
import routerMixin from './mixins/routerMixin';
import {
getExtraSpaceToScroll,
loadedEventConfig,
} from './helpers/IframeEventHelper';
import {
ON_AGENT_MESSAGE_RECEIVED,
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from './constants/widgetBusEvents';
export default {
name: 'App',
components: {
Router,
Spinner,
},
mixins: [availabilityMixin, configMixin],
mixins: [availabilityMixin, configMixin, routerMixin],
data() {
return {
showUnreadView: false,
showCampaignView: false,
isMobile: false,
hideMessageBubble: false,
widgetPosition: 'right',
showPopoutButton: false,
isWebWidgetTriggered: false,
isCampaignViewClicked: false,
isWidgetOpen: false,
};
},
computed: {
...mapGetters({
activeCampaign: 'campaign/getActiveCampaign',
campaigns: 'campaign/getCampaigns',
conversationSize: 'conversation/getConversationSize',
currentUser: 'contacts/getCurrentUser',
hasFetched: 'agent/getHasFetched',
hideMessageBubble: 'appConfig/getHideMessageBubble',
isFetchingList: 'conversation/getIsFetchingList',
isRightAligned: 'appConfig/isRightAligned',
isWidgetOpen: 'appConfig/getIsWidgetOpen',
messageCount: 'conversation/getMessageCount',
unreadMessageCount: 'conversation/getUnreadMessageCount',
campaigns: 'campaign/getCampaigns',
activeCampaign: 'campaign/getActiveCampaign',
}),
isLeftAligned() {
const isLeft = this.widgetPosition === 'left';
return isLeft;
},
isIFrame() {
return IFrameHelper.isIFrame();
},
@ -67,20 +73,11 @@ export default {
activeCampaign() {
this.setCampaignView();
},
showUnreadView(newVal) {
if (newVal) {
this.setIframeHeight(this.isMobile);
}
},
showCampaignView(newVal) {
if (newVal) {
this.setIframeHeight(this.isMobile);
}
},
},
mounted() {
const { websiteToken, locale } = window.chatwootWebChannel;
const { websiteToken, locale, widgetColor } = window.chatwootWebChannel;
this.setLocale(locale);
this.setWidgetColor(widgetColor);
if (this.isIFrame) {
this.registerListeners();
this.sendLoadedEvent();
@ -96,12 +93,15 @@ export default {
this.sendRNWebViewLoadedEvent();
}
this.$store.dispatch('conversationAttributes/getAttributes');
this.setWidgetColor(window.chatwootWebChannel);
this.registerUnreadEvents();
this.registerCampaignEvents();
},
methods: {
...mapActions('appConfig', ['setWidgetColor', 'setReferrerHost']),
...mapActions('appConfig', [
'setAppConfig',
'setReferrerHost',
'setWidgetColor',
]),
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
...mapActions('campaign', [
'initCampaigns',
@ -109,7 +109,6 @@ export default {
'resetCampaign',
]),
...mapActions('agent', ['fetchAvailableAgents']),
...mapMutations('events', ['toggleOpen']),
scrollConversationToBottom() {
const container = this.$el.querySelector('.conversation-wrap');
container.scrollTop = container.scrollHeight;
@ -136,68 +135,48 @@ export default {
this.$root.$i18n.locale = locale;
}
},
setPosition(position) {
const widgetPosition = position || 'right';
this.widgetPosition = widgetPosition;
},
setHideMessageBubble(hideBubble) {
this.hideMessageBubble = !!hideBubble;
},
registerUnreadEvents() {
bus.$on('on-agent-message-received', () => {
if (!this.isIFrame || this.isWidgetOpen) {
this.setUserLastSeen();
}
this.setUnreadView();
});
bus.$on('on-unread-view-clicked', () => {
this.unsetUnreadView();
this.setUserLastSeen();
bus.$on(ON_AGENT_MESSAGE_RECEIVED, this.setUnreadView);
bus.$on(ON_UNREAD_MESSAGE_CLICK, () => {
this.replaceRoute('messages').then(() => this.unsetUnreadView());
});
},
registerCampaignEvents() {
bus.$on('on-campaign-view-clicked', () => {
this.isCampaignViewClicked = true;
this.showCampaignView = false;
this.showUnreadView = false;
this.unsetUnreadView();
this.setUserLastSeen();
// Execute campaign only if pre-chat form (and require email too) is not enabled
if (
!(this.preChatFormEnabled && this.preChatFormOptions.requireEmail)
) {
bus.$on(ON_CAMPAIGN_MESSAGE_CLICK, () => {
const showPreChatForm =
this.preChatFormEnabled && this.preChatFormOptions.requireEmail;
const isUserEmailAvailable = !!this.currentUser.email;
if (showPreChatForm && !isUserEmailAvailable) {
this.replaceRoute('prechat-form');
} else {
this.replaceRoute('messages');
bus.$emit('execute-campaign', this.activeCampaign.id);
}
this.unsetUnreadView();
});
bus.$on('execute-campaign', campaignId => {
const { websiteToken } = window.chatwootWebChannel;
this.executeCampaign({ campaignId, websiteToken });
this.replaceRoute('messages');
});
},
setPopoutDisplay(showPopoutButton) {
this.showPopoutButton = showPopoutButton;
},
setCampaignView() {
const { messageCount, activeCampaign } = this;
const isCampaignReadyToExecute =
!isEmptyObject(activeCampaign) &&
!messageCount &&
!this.isWebWidgetTriggered;
!isEmptyObject(activeCampaign) && !messageCount;
if (this.isIFrame && isCampaignReadyToExecute) {
this.showCampaignView = true;
IFrameHelper.sendMessage({
event: 'setCampaignMode',
this.replaceRoute('campaigns').then(() => {
this.setIframeHeight(this.isMobile);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
this.setIframeHeight(this.isMobile);
}
},
setUnreadView() {
const { unreadMessageCount } = this;
if (this.isIFrame && unreadMessageCount > 0) {
IFrameHelper.sendMessage({
event: 'setUnreadMode',
unreadMessageCount,
if (this.isIFrame && unreadMessageCount > 0 && !this.isWidgetOpen) {
this.replaceRoute('unread-messages').then(() => {
this.setIframeHeight(this.isMobile);
IFrameHelper.sendMessage({ event: 'setUnreadMode' });
});
this.setIframeHeight(this.isMobile);
this.handleUnreadNotificationDot();
@ -222,14 +201,12 @@ export default {
createWidgetEvents(message) {
const { eventName } = message;
const isWidgetTriggerEvent = eventName === 'webwidget.triggered';
this.isWebWidgetTriggered = true;
if (
isWidgetTriggerEvent &&
(this.showUnreadView || this.showCampaignView)
['unread-messages', 'campaigns'].includes(this.$route.name)
) {
return;
}
this.setUserLastSeen();
this.$store.dispatch('events/create', { name: eventName });
},
registerListeners() {
@ -242,11 +219,9 @@ export default {
if (message.event === 'config-set') {
this.setLocale(message.locale);
this.setBubbleLabel();
this.setPosition(message.position);
this.fetchOldConversations().then(() => this.setUnreadView());
this.setPopoutDisplay(message.showPopoutButton);
this.fetchAvailableAgents(websiteToken);
this.setHideMessageBubble(message.hideMessageBubble);
this.setAppConfig(message);
this.$store.dispatch('contacts/get');
} else if (message.event === 'widget-visible') {
this.scrollConversationToBottom();
@ -260,7 +235,7 @@ export default {
window.referrerURL = referrerURL;
this.setReferrerHost(referrerHost);
} else if (message.event === 'toggle-close-button') {
this.isMobile = message.showClose;
this.isMobile = message.isMobile;
} else if (message.event === 'push-event') {
this.createWidgetEvents(message);
} else if (message.event === 'set-label') {
@ -282,20 +257,27 @@ export default {
} else if (message.event === 'set-locale') {
this.setLocale(message.locale);
this.setBubbleLabel();
} else if (message.event === 'set-unread-view') {
this.showUnreadView = true;
this.showCampaignView = false;
} else if (message.event === 'unset-unread-view') {
// Reset campaign, If widget opened via clciking on bubble button
if (!this.isCampaignViewClicked) {
} else if (message.event === 'toggle-open') {
this.$store.dispatch('appConfig/toggleWidgetOpen', message.isOpen);
if (
['home'].includes(this.$route.name) &&
message.isOpen &&
this.messageCount
) {
this.replaceRoute('messages');
}
if (
!message.isOpen &&
['unread-messages', 'campaigns'].includes(this.$route.name)
) {
this.$store.dispatch('conversation/setUserLastSeen');
this.unsetUnreadView();
this.replaceRoute('home');
}
if (!message.isOpen) {
this.resetCampaign();
}
this.showUnreadView = false;
this.showCampaignView = false;
this.handleUnreadNotificationDot();
} else if (message.event === 'toggle-open') {
this.isWidgetOpen = message.isOpen;
this.toggleOpen();
}
});
},

View file

@ -19,13 +19,6 @@ body {
height: 100%;
}
.woot-widget-wrap {
display: flex;
flex-direction: column;
height: 100%;
justify-content: flex-end;
}
.is-mobile {
display: block;

View file

@ -1,30 +1,31 @@
<template>
<div>
<footer v-if="!hideReplyBox" class="footer">
<ChatInputWrap
:on-send-message="handleSendMessage"
:on-send-attachment="handleSendAttachment"
/>
</footer>
<div v-else>
<custom-button
class="font-medium"
block
:bg-color="widgetColor"
:text-color="textColor"
@click="startNewConversation"
>
{{ $t('START_NEW_CONVERSATION') }}
</custom-button>
<custom-button
v-if="showEmailTranscriptButton"
type="clear"
class="font-normal"
@click="sendTranscript"
>
{{ $t('EMAIL_TRANSCRIPT.BUTTON_TEXT') }}
</custom-button>
</div>
<footer
v-if="!hideReplyBox"
class="shadow-sm rounded-lg bg-white mb-1 z-50 relative"
>
<chat-input-wrap
:on-send-message="handleSendMessage"
:on-send-attachment="handleSendAttachment"
/>
</footer>
<div v-else>
<custom-button
class="font-medium"
block
:bg-color="widgetColor"
:text-color="textColor"
@click="startNewConversation"
>
{{ $t('START_NEW_CONVERSATION') }}
</custom-button>
<custom-button
v-if="showEmailTranscriptButton"
type="clear"
class="font-normal"
@click="sendTranscript"
>
{{ $t('EMAIL_TRANSCRIPT.BUTTON_TEXT') }}
</custom-button>
</div>
</template>
@ -115,19 +116,8 @@ export default {
},
};
</script>
<!-- Add "scoped" attribute to limit CSS to this component only -->
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
.footer {
background: $color-white;
box-sizing: border-box;
width: 100%;
border-radius: 7px;
@include shadow-big;
}
.branding {
align-items: center;

View file

@ -1,9 +1,12 @@
<template>
<header class="header-collapsed">
<div class="header-branding">
<header class="flex justify-between p-5 w-full">
<div class="flex items-center">
<button v-if="showBackButton" @click="onBackButtonClick">
<fluent-icon icon="chevron-left" size="24" />
</button>
<img
v-if="avatarUrl"
class="inbox--avatar mr-3"
class="h-8 w-8 rounded-full mr-3"
:src="avatarUrl"
alt="avatar"
/>
@ -12,14 +15,13 @@
<span class="mr-1" v-html="title" />
<div
:class="
`status-view--badge rounded-full leading-4 ${
isOnline ? 'bg-green-500' : 'hidden'
}`
`h-2 w-2 rounded-full leading-4
${isOnline ? 'bg-green-500' : 'hidden'}`
"
/>
</div>
<div class="text-xs mt-1 text-black-700">
{{ replyWaitMeessage }}
{{ replyWaitMessage }}
</div>
</div>
</div>
@ -29,15 +31,19 @@
<script>
import { mapGetters } from 'vuex';
import HeaderActions from './HeaderActions';
import availabilityMixin from 'widget/mixins/availability';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import HeaderActions from './HeaderActions';
import routerMixin from 'widget/mixins/routerMixin';
export default {
name: 'ChatHeader',
components: {
FluentIcon,
HeaderActions,
},
mixins: [availabilityMixin],
mixins: [availabilityMixin, routerMixin],
props: {
avatarUrl: {
type: String,
@ -51,15 +57,17 @@ export default {
type: Boolean,
default: false,
},
showBackButton: {
type: Boolean,
default: false,
},
availableAgents: {
type: Array,
default: () => {},
},
},
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
}),
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
isOnline() {
const { workingHoursEnabled } = this.channelConfig;
const anyAgentOnline = this.availableAgents.length > 0;
@ -69,47 +77,16 @@ export default {
}
return anyAgentOnline;
},
replyWaitMeessage() {
replyWaitMessage() {
return this.isOnline
? this.replyTimeStatus
: this.$t('TEAM_AVAILABILITY.OFFLINE');
},
},
methods: {
onBackButtonClick() {
this.replaceRoute('home');
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables.scss';
@import '~widget/assets/scss/mixins.scss';
.header-collapsed {
display: flex;
justify-content: space-between;
padding: $space-two $space-medium;
width: 100%;
box-sizing: border-box;
.header-branding {
display: flex;
align-items: center;
img {
border-radius: 50%;
}
}
.title {
font-weight: $font-weight-medium;
}
.inbox--avatar {
height: 32px;
width: 32px;
}
}
.status-view--badge {
height: $space-small;
width: $space-small;
}
</style>

View file

@ -1,14 +1,14 @@
<template>
<header class="header-expanded bg-white py-8 px-6 relative box-border w-full">
<header class="header-expanded bg-white py-6 px-5 relative box-border w-full">
<div
class="flex items-start"
:class="[avatarUrl ? 'justify-between' : 'justify-end']"
>
<img v-if="avatarUrl" class="logo" :src="avatarUrl" />
<img v-if="avatarUrl" class="h-12 rounded-full" :src="avatarUrl" />
<header-actions :show-popout-button="showPopoutButton" />
</div>
<h2
class="text-slate-900 mt-6 text-4xl mb-3 font-normal"
class="text-slate-900 mt-5 text-3xl mb-3 font-normal"
v-html="introHeading"
/>
<p class="text-lg text-black-700 leading-normal" v-html="introBody" />
@ -48,17 +48,3 @@ export default {
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/mixins.scss';
$logo-size: 56px;
.header-expanded {
.logo {
width: $logo-size;
height: $logo-size;
border-radius: $logo-size;
}
}
</style>

View file

@ -90,6 +90,7 @@ export default {
computed: {
...mapGetters({
widgetColor: 'appConfig/getWidgetColor',
isWidgetOpen: 'appConfig/getIsWidgetOpen',
}),
showAttachment() {
return this.hasAttachmentsEnabled && this.userInput.length === 0;
@ -97,13 +98,10 @@ export default {
showSendButton() {
return this.userInput.length > 0;
},
isOpen() {
return this.$store.state.events.isOpen;
},
},
watch: {
isOpen(isOpen) {
if (isOpen) {
isWidgetOpen(isWidgetOpen) {
if (isWidgetOpen) {
this.focusInput();
}
},
@ -113,7 +111,7 @@ export default {
},
mounted() {
document.addEventListener('keypress', this.handleEnterKeyPress);
if (this.isOpen) {
if (this.isWidgetOpen) {
this.focusInput();
}
},

View file

@ -67,9 +67,7 @@ export default {
},
closeWindow() {
if (IFrameHelper.isIFrame()) {
IFrameHelper.sendMessage({
event: 'toggleBubble',
});
IFrameHelper.sendMessage({ event: 'closeWindow' });
} else if (RNHelper.isRNWebView) {
RNHelper.sendMessage({ type: 'close-widget' });
}

View file

@ -34,7 +34,7 @@
"
/>
<form-text-area
v-if="!activeCampaignExist"
v-if="!hasActiveCampaign"
v-model="message"
class="my-5"
:label="$t('PRE_CHAT_FORM.FIELDS.MESSAGE.LABEL')"
@ -63,7 +63,7 @@ import { mapGetters } from 'vuex';
import { getContrastingTextColor } from '@chatwoot/utils';
import { required, minLength, email } from 'vuelidate/lib/validators';
import { isEmptyObject } from 'widget/helpers/utils';
import routerMixin from 'widget/mixins/routerMixin';
export default {
components: {
FormInput,
@ -71,6 +71,7 @@ export default {
CustomButton,
Spinner,
},
mixins: [routerMixin],
props: {
options: {
type: Object,
@ -95,7 +96,7 @@ export default {
},
};
// For campaign, message field is not required
if (this.activeCampaignExist) {
if (this.hasActiveCampaign) {
return identityValidations;
}
if (this.options.requireEmail) {
@ -122,14 +123,14 @@ export default {
textColor() {
return getContrastingTextColor(this.widgetColor);
},
activeCampaignExist() {
hasActiveCampaign() {
return !isEmptyObject(this.activeCampaign);
},
shouldShowHeaderMessage() {
return this.activeCampaignExist || this.options.preChatMessage;
return this.hasActiveCampaign || this.options.preChatMessage;
},
headerMessage() {
if (this.activeCampaignExist) {
if (this.hasActiveCampaign) {
return this.$t('PRE_CHAT_FORM.CAMPAIGN_HEADER');
}
return this.options.preChatMessage;
@ -141,22 +142,12 @@ export default {
if (this.$v.$invalid) {
return;
}
// Check any active campaign exist or not
if (this.activeCampaignExist) {
bus.$emit('execute-campaign', this.activeCampaign.id);
this.$store.dispatch('contacts/update', {
user: {
email: this.emailAddress,
name: this.fullName,
},
});
} else {
this.$store.dispatch('conversation/createConversation', {
fullName: this.fullName,
emailAddress: this.emailAddress,
message: this.message,
});
}
this.$emit('submit', {
fullName: this.fullName,
emailAddress: this.emailAddress,
message: this.message,
activeCampaignId: this.activeCampaign.id,
});
},
},
};

View file

@ -1,5 +1,5 @@
<template>
<div class="px-4">
<div class="px-5">
<div class="flex items-center justify-between mb-4">
<div class="text-black-700">
<div class="text-base leading-5 font-medium mb-1">
@ -22,7 +22,9 @@
:text-color="textColor"
@click="startConversation"
>
{{ $t('START_CONVERSATION') }}
{{
hasConversation ? $t('CONTINUE_CONVERSATION') : $t('START_CONVERSATION')
}}
</custom-button>
</div>
</template>
@ -47,6 +49,10 @@ export default {
type: Array,
default: () => {},
},
hasConversation: {
type: Boolean,
default: false,
},
},
computed: {
...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),

View file

@ -21,6 +21,10 @@ import messageFormatterMixin from 'shared/mixins/messageFormatterMixin';
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
import configMixin from '../mixins/configMixin';
import { isEmptyObject } from 'widget/helpers/utils';
import {
ON_CAMPAIGN_MESSAGE_CLICK,
ON_UNREAD_MESSAGE_CLICK,
} from '../constants/widgetBusEvents';
export default {
name: 'UnreadMessage',
components: { Thumbnail },
@ -82,9 +86,9 @@ export default {
},
onClickMessage() {
if (this.campaignId) {
bus.$emit('on-campaign-view-clicked', this.campaignId);
bus.$emit(ON_CAMPAIGN_MESSAGE_CLICK, this.campaignId);
} else {
bus.$emit('on-unread-view-clicked');
bus.$emit(ON_UNREAD_MESSAGE_CLICK);
}
},
},

View file

@ -1,20 +1,16 @@
<template>
<div class="unread-wrap">
<div class="close-unread-wrap">
<button
v-if="showCloseButton"
class="button small close-unread-button"
@click="closeFullView"
>
<div class="flex items-center">
<button class="button small close-unread-button" @click="closeFullView">
<span class="flex items-center">
<fluent-icon class="mr-1" icon="dismiss" size="12" />
{{ $t('UNREAD_VIEW.CLOSE_MESSAGES_BUTTON') }}
</div>
</span>
</button>
</div>
<div class="unread-messages">
<unread-message
v-for="(message, index) in allMessages"
v-for="(message, index) in messages"
:key="message.id"
:message-type="message.messageType"
:message-id="message.id"
@ -29,12 +25,12 @@
<button
v-if="unreadMessageCount"
class="button clear-button"
@click="openFullView"
@click="openConversationView"
>
<div class="flex items-center">
<span class="flex items-center">
<fluent-icon class="mr-2" size="16" icon="arrow-right" />
{{ $t('UNREAD_VIEW.VIEW_MESSAGES_BUTTON') }}
</div>
</span>
</button>
</div>
</div>
@ -44,7 +40,7 @@
import { IFrameHelper } from 'widget/helpers/utils';
import { mapGetters } from 'vuex';
import configMixin from '../mixins/configMixin';
import { ON_UNREAD_MESSAGE_CLICK } from '../constants/widgetBusEvents';
import FluentIcon from 'shared/components/FluentIcon/Index.vue';
import UnreadMessage from 'widget/components/UnreadMessage.vue';
@ -56,58 +52,25 @@ export default {
},
mixins: [configMixin],
props: {
hasFetched: {
type: Boolean,
default: false,
},
unreadMessageCount: {
type: Number,
default: 0,
},
hideMessageBubble: {
type: Boolean,
default: false,
},
showUnreadView: {
type: Boolean,
default: false,
messages: {
type: Array,
required: true,
},
},
computed: {
...mapGetters({
unreadMessages: 'conversation/getUnreadTextMessages',
campaign: 'campaign/getActiveCampaign',
}),
showCloseButton() {
return this.unreadMessageCount;
},
...mapGetters({ unreadMessageCount: 'conversation/getUnreadMessageCount' }),
sender() {
const [firstMessage] = this.unreadMessages;
const [firstMessage] = this.messages;
return firstMessage.sender || {};
},
allMessages() {
if (this.showUnreadView) {
return this.unreadMessages;
}
const { sender, id: campaignId, message: content } = this.campaign;
return [
{
content,
sender,
campaignId,
},
];
},
},
methods: {
openFullView() {
bus.$emit('on-unread-view-clicked');
openConversationView() {
bus.$emit(ON_UNREAD_MESSAGE_CLICK);
},
closeFullView() {
if (IFrameHelper.isIFrame()) {
IFrameHelper.sendMessage({
event: 'toggleBubble',
});
IFrameHelper.sendMessage({ event: 'toggleBubble' });
}
},
getMessageContent(message) {

View file

@ -0,0 +1,118 @@
<template>
<div
class="w-full h-full bg-slate-50 flex flex-col"
@keydown.esc="closeWindow"
>
<div
class="header-wrap bg-white"
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
>
<transition
enter-active-class="transition-all delay-200 duration-300 ease-in"
leave-active-class="transition-all duration-200 ease-out"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<chat-header-expanded
v-if="!isHeaderCollapsed"
:intro-heading="channelConfig.welcomeTitle"
:intro-body="channelConfig.welcomeTagline"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="appConfig.showPopoutButton"
/>
<chat-header
v-if="isHeaderCollapsed"
:title="channelConfig.websiteName"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="appConfig.showPopoutButton"
:available-agents="availableAgents"
/>
</transition>
</div>
<banner />
<transition
enter-active-class="transition-all delay-300 duration-300 ease-in"
leave-active-class="transition-all duration-200 ease-out"
enter-class="opacity-0"
enter-to-class="opacity-100"
leave-class="opacity-100"
leave-to-class="opacity-0"
>
<router-view />
</transition>
<branding />
</div>
</template>
<script>
import Banner from '../Banner.vue';
import Branding from 'shared/components/Branding.vue';
import ChatHeader from '../ChatHeader.vue';
import ChatHeaderExpanded from '../ChatHeaderExpanded.vue';
import configMixin from '../../mixins/configMixin';
import { mapGetters } from 'vuex';
import { IFrameHelper } from 'widget/helpers/utils';
export default {
components: {
Banner,
Branding,
ChatHeader,
ChatHeaderExpanded,
},
mixins: [configMixin],
data() {
return {
showPopoutButton: false,
};
},
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
appConfig: 'appConfig/getAppConfig',
}),
isHeaderCollapsed() {
if (!this.hasIntroText) {
return true;
}
return this.$route.name !== 'home';
},
hasIntroText() {
return (
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
);
},
},
methods: {
closeWindow() {
IFrameHelper.sendMessage({ event: 'closeWindow' });
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables';
@import '~widget/assets/scss/mixins';
.header-wrap {
border-radius: $space-normal $space-normal 0 0;
flex-shrink: 0;
transition: max-height 300ms;
z-index: 99;
@include shadow-large;
&.expanded {
max-height: 16rem;
}
&.collapsed {
max-height: 4.5rem;
}
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
border-radius: 0;
}
}
</style>

View file

@ -0,0 +1,3 @@
export const ON_AGENT_MESSAGE_RECEIVED = 'ON_AGENT_MESSAGE_RECEIVED';
export const ON_UNREAD_MESSAGE_CLICK = 'ON_UNREAD_MESSAGE_CLICK';
export const ON_CAMPAIGN_MESSAGE_CLICK = 'ON_CAMPAIGN_MESSAGE_CLICK';

View file

@ -1,5 +1,6 @@
import BaseActionCableConnector from '../../shared/helpers/BaseActionCableConnector';
import { playNewMessageNotificationInWidget } from 'shared/helpers/AudioNotificationHelper';
import { ON_AGENT_MESSAGE_RECEIVED } from '../constants/widgetBusEvents';
class ActionCableConnector extends BaseActionCableConnector {
constructor(app, pubsubToken) {
@ -22,9 +23,7 @@ class ActionCableConnector extends BaseActionCableConnector {
onMessageCreated = data => {
this.app.$store
.dispatch('conversation/addOrUpdateMessage', data)
.then(() => {
window.bus.$emit('on-agent-message-received');
});
.then(() => window.bus.$emit(ON_AGENT_MESSAGE_RECEIVED));
if (data.sender_type === 'User') {
playNewMessageNotificationInWidget();
}

View file

@ -22,6 +22,7 @@
"IN_A_DAY": "Typically replies in a day"
},
"START_CONVERSATION": "Start Conversation",
"CONTINUE_CONVERSATION": "Continue conversation",
"START_NEW_CONVERSATION": "Start a new conversation",
"UNREAD_VIEW": {
"VIEW_MESSAGES_BUTTON": "See new messages",

View file

@ -25,10 +25,8 @@ export default {
let requireEmail = false;
let preChatMessage = '';
const options = window.chatwootWebChannel.preChatFormOptions || {};
if (!this.isOnNewConversation) {
requireEmail = options.require_email;
preChatMessage = options.pre_chat_message;
}
requireEmail = options.require_email;
preChatMessage = options.pre_chat_message;
return {
requireEmail,
preChatMessage,

View file

@ -0,0 +1,10 @@
export default {
methods: {
async replaceRoute(name) {
if (this.$route.name !== name) {
return this.$router.replace({ name });
}
return undefined;
},
},
};

View file

@ -1,24 +1,42 @@
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import ViewWithHeader from './components/layouts/ViewWithHeader.vue';
Vue.use(Router);
export default new Router({
mode: 'hash',
routes: [
{
path: '/',
name: 'home',
component: Home,
path: '/unread-messages',
name: 'unread-messages',
component: () => import('./views/UnreadMessages.vue'),
},
{
path: '/campaigns',
name: 'campaigns',
component: () => import('./views/Campaigns.vue'),
},
{
path: '/',
component: ViewWithHeader,
children: [
{
path: '',
name: 'home',
component: () => import('./views/Home.vue'),
},
{
path: '/prechat-form',
name: 'prechat-form',
component: () => import('./views/PreChatForm.vue'),
},
{
path: '/messages',
name: 'messages',
component: () => import('./views/Messages.vue'),
},
],
},
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (about.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () =>
// import(/* webpackChunkName: "about" */ './views/About.vue'),
// },
],
});

View file

@ -1,18 +1,43 @@
import { SET_REFERRER_HOST, SET_WIDGET_COLOR } from '../types';
import {
SET_REFERRER_HOST,
SET_WIDGET_APP_CONFIG,
SET_WIDGET_COLOR,
TOGGLE_WIDGET_OPEN,
} from '../types';
const state = {
showPopoutButton: false,
hideMessageBubble: false,
position: 'right',
isWebWidgetTriggered: false,
isCampaignViewClicked: false,
isWidgetOpen: false,
widgetColor: '',
referrerHost: '',
};
export const getters = {
getAppConfig: $state => $state,
isRightAligned: $state => $state.position === 'right',
getHideMessageBubble: $state => $state.hideMessageBubble,
getIsWidgetOpen: $state => $state.isWidgetOpen,
getWidgetColor: $state => $state.widgetColor,
getReferrerHost: $state => $state.referrerHost,
};
export const actions = {
setWidgetColor({ commit }, data) {
commit(SET_WIDGET_COLOR, data);
setAppConfig({ commit }, { showPopoutButton, position, hideMessageBubble }) {
commit(SET_WIDGET_APP_CONFIG, {
showPopoutButton: !!showPopoutButton,
position: position || 'right',
hideMessageBubble: !!hideMessageBubble,
});
},
toggleWidgetOpen({ commit }, isWidgetOpen) {
commit(TOGGLE_WIDGET_OPEN, isWidgetOpen);
},
setWidgetColor({ commit }, widgetColor) {
commit(SET_WIDGET_COLOR, widgetColor);
},
setReferrerHost({ commit }, referrerHost) {
commit(SET_REFERRER_HOST, referrerHost);
@ -20,8 +45,16 @@ export const actions = {
};
export const mutations = {
[SET_WIDGET_COLOR]($state, data) {
$state.widgetColor = data.widgetColor;
[SET_WIDGET_APP_CONFIG]($state, data) {
$state.showPopoutButton = data.showPopoutButton;
$state.position = data.position;
$state.hideMessageBubble = data.hideMessageBubble;
},
[TOGGLE_WIDGET_OPEN]($state, isWidgetOpen) {
$state.isWidgetOpen = isWidgetOpen;
},
[SET_WIDGET_COLOR]($state, widgetColor) {
$state.widgetColor = widgetColor;
},
[SET_REFERRER_HOST]($state, referrerHost) {
$state.referrerHost = referrerHost;

View file

@ -9,10 +9,8 @@ const state = {
records: [],
uiFlags: {
isError: false,
hasFetched: false,
},
activeCampaign: {},
campaignHasExecuted: false,
};
const resetCampaignTimers = (
@ -32,10 +30,8 @@ const resetCampaignTimers = (
};
export const getters = {
getHasFetched: $state => $state.uiFlags.hasFetched,
getCampaigns: $state => $state.records,
getActiveCampaign: $state => $state.activeCampaign,
getCampaignHasExecuted: $state => $state.campaignHasExecuted,
};
export const actions = {
@ -47,7 +43,6 @@ export const actions = {
const { data: campaigns } = await getCampaigns(websiteToken);
commit('setCampaigns', campaigns);
commit('setError', false);
commit('setHasFetched', true);
resetCampaignTimers(
campaigns,
currentURL,
@ -56,7 +51,6 @@ export const actions = {
);
} catch (error) {
commit('setError', true);
commit('setHasFetched', true);
}
},
initCampaigns: async (
@ -82,13 +76,13 @@ export const actions = {
{
commit,
rootState: {
events: { isOpen },
appConfig: { isWidgetOpen },
},
},
{ websiteToken, campaignId }
) => {
// Disable campaign execution if widget is opened
if (!isOpen) {
if (!isWidgetOpen) {
const { data: campaigns } = await getCampaigns(websiteToken);
// Check campaign is disabled or not
const campaign = campaigns.find(item => item.id === campaignId);
@ -100,11 +94,21 @@ export const actions = {
executeCampaign: async ({ commit }, { campaignId, websiteToken }) => {
try {
commit(
'conversation/setConversationUIFlag',
{ isCreating: true },
{ root: true }
);
await triggerCampaign({ campaignId, websiteToken });
commit('setCampaignExecuted');
commit('setActiveCampaign', {});
} catch (error) {
commit('setError', true);
} finally {
commit(
'conversation/setConversationUIFlag',
{ isCreating: false },
{ root: true }
);
}
},
resetCampaign: async ({ commit }) => {
@ -126,12 +130,6 @@ export const mutations = {
setError($state, value) {
Vue.set($state.uiFlags, 'isError', value);
},
setHasFetched($state, value) {
Vue.set($state.uiFlags, 'hasFetched', value);
},
setCampaignExecuted($state) {
Vue.set($state, 'campaignHasExecuted', true);
},
};
export default {

View file

@ -1,9 +1,5 @@
import events from 'widget/api/events';
const state = {
isOpen: false,
};
const actions = {
create: async (_, { name }) => {
try {
@ -14,16 +10,10 @@ const actions = {
},
};
const mutations = {
toggleOpen($state) {
$state.isOpen = !$state.isOpen;
},
};
export default {
namespaced: true,
state,
state: {},
getters: {},
actions,
mutations,
mutations: {},
};

View file

@ -18,7 +18,7 @@ describe('#mutations', () => {
});
});
describe('#setError', () => {
describe('#setHasFetched', () => {
it('set fetched flag', () => {
const state = { records: [], uiFlags: {} };
mutations.setHasFetched(state, true);

View file

@ -13,10 +13,8 @@ describe('#actions', () => {
describe('#setWidgetColor', () => {
it('creates actions properly', () => {
actions.setWidgetColor({ commit }, { widgetColor: '#eaeaea' });
expect(commit.mock.calls).toEqual([
['SET_WIDGET_COLOR', { widgetColor: '#eaeaea' }],
]);
actions.setWidgetColor({ commit }, '#eaeaea');
expect(commit.mock.calls).toEqual([['SET_WIDGET_COLOR', '#eaeaea']]);
});
});
});

View file

@ -12,7 +12,7 @@ describe('#mutations', () => {
describe('#SET_WIDGET_COLOR', () => {
it('sets widget color properly', () => {
const state = { widgetColor: '' };
mutations.SET_WIDGET_COLOR(state, { widgetColor: '#00bcd4' });
mutations.SET_WIDGET_COLOR(state, '#00bcd4');
expect(state.widgetColor).toEqual('#00bcd4');
});
});

View file

@ -24,7 +24,6 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([
['setCampaigns', campaigns],
['setError', false],
['setHasFetched', true],
]);
expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{
@ -50,10 +49,7 @@ describe('#actions', () => {
isInBusinessHours: true,
}
);
expect(commit.mock.calls).toEqual([
['setError', true],
['setHasFetched', true],
]);
expect(commit.mock.calls).toEqual([['setError', true]]);
});
});
describe('#initCampaigns', () => {
@ -99,7 +95,7 @@ describe('#actions', () => {
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: true },
appConfig: { isWidgetOpen: true },
},
},
{ campaignId: 32 }
@ -113,7 +109,7 @@ describe('#actions', () => {
getters: { getCampaigns: campaigns },
commit,
rootState: {
events: { isOpen: false },
appConfig: { isWidgetOpen: false },
},
},
{ campaignId: 1 }
@ -127,15 +123,52 @@ describe('#actions', () => {
API.post.mockResolvedValue({});
await actions.executeCampaign({ commit }, params);
expect(commit.mock.calls).toEqual([
['setCampaignExecuted'],
[
'conversation/setConversationUIFlag',
{
isCreating: true,
},
{
root: true,
},
],
['setActiveCampaign', {}],
[
'conversation/setConversationUIFlag',
{
isCreating: false,
},
{
root: true,
},
],
]);
});
it('sends correct actions if execute campaign API is failed', async () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
API.post.mockRejectedValue({ message: 'Authentication required' });
await actions.executeCampaign({ commit }, params);
expect(commit.mock.calls).toEqual([['setError', true]]);
expect(commit.mock.calls).toEqual([
[
'conversation/setConversationUIFlag',
{
isCreating: true,
},
{
root: true,
},
],
['setError', true],
[
'conversation/setConversationUIFlag',
{
isCreating: false,
},
{
root: true,
},
],
]);
});
});

View file

@ -129,17 +129,4 @@ describe('#getters', () => {
updated_at: '2021-05-03T04:53:36.354Z',
});
});
it('getCampaignHasExecuted', () => {
const state = {
records: [],
uiFlags: {
isError: false,
hasFetched: false,
},
activeCampaign: {},
campaignHasExecuted: false,
};
expect(getters.getCampaignHasExecuted(state)).toEqual(false);
});
});

View file

@ -18,14 +18,6 @@ describe('#mutations', () => {
});
});
describe('#setHasFetched', () => {
it('set fetched flag', () => {
const state = { records: [], uiFlags: {} };
mutations.setHasFetched(state, true);
expect(state.uiFlags.hasFetched).toEqual(true);
});
});
describe('#setActiveCampaign', () => {
it('set active campaign', () => {
const state = { records: [] };
@ -33,12 +25,4 @@ describe('#mutations', () => {
expect(state.activeCampaign).toEqual(campaigns[0]);
});
});
describe('#setCampaignExecuted', () => {
it('set campaign executed flag', () => {
const state = { records: [], uiFlags: {}, campaignHasExecuted: false };
mutations.setCampaignExecuted(state);
expect(state.campaignHasExecuted).toEqual(true);
});
});
});

View file

@ -1,6 +1,7 @@
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
export const SET_REFERRER_HOST = 'SET_REFERRER_HOST';
export const SET_CONVERSATION_ATTRIBUTES = 'SET_CONVERSATION_ATTRIBUTES';
export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
export const CLEAR_CONVERSATION_ATTRIBUTES = 'CLEAR_CONVERSATION_ATTRIBUTES';
export const SET_CONVERSATION_ATTRIBUTES = 'SET_CONVERSATION_ATTRIBUTES';
export const SET_WIDGET_APP_CONFIG = 'SET_WIDGET_APP_CONFIG';
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
export const TOGGLE_WIDGET_OPEN = 'TOGGLE_WIDGET_OPEN';
export const SET_REFERRER_HOST = 'SET_REFERRER_HOST';

View file

@ -0,0 +1,28 @@
<template>
<unread-message-list :messages="messages" />
</template>
<script>
import { mapGetters } from 'vuex';
import UnreadMessageList from '../components/UnreadMessageList.vue';
export default {
name: 'Campaigns',
components: {
UnreadMessageList,
},
computed: {
...mapGetters({ campaign: 'campaign/getActiveCampaign' }),
messages() {
const { sender, id: campaignId, message: content } = this.campaign;
return [
{
content,
sender,
campaignId,
},
];
},
},
};
</script>

View file

@ -1,113 +1,33 @@
<template>
<div
v-if="!conversationSize && isFetchingList"
class="flex flex-1 items-center h-full bg-black-25 justify-center"
>
<spinner size="" />
</div>
<div v-else class="home" @keydown.esc="closeChat">
<div
class="header-wrap bg-white"
:class="{ expanded: !isHeaderCollapsed, collapsed: isHeaderCollapsed }"
>
<transition
enter-active-class="transition-all delay-200 duration-300 ease"
leave-active-class="transition-all duration-200 ease-in"
enter-class="opacity-0 transform"
enter-to-class="opacity-100 transform"
leave-class="opacity-100 transform"
leave-to-class="opacity-0 transform"
>
<chat-header-expanded
v-if="!isHeaderCollapsed"
:intro-heading="channelConfig.welcomeTitle"
:intro-body="channelConfig.welcomeTagline"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="showPopoutButton"
/>
<chat-header
v-if="isHeaderCollapsed"
:title="channelConfig.websiteName"
:avatar-url="channelConfig.avatarUrl"
:show-popout-button="showPopoutButton"
:available-agents="availableAgents"
/>
</transition>
</div>
<banner />
<div class="flex flex-1 flex-col justify-end">
<div class="flex flex-1 overflow-auto">
<conversation-wrap
v-if="currentView === 'messageView'"
:grouped-messages="groupedMessages"
/>
<pre-chat-form
v-if="currentView === 'preChatFormView'"
:options="preChatFormOptions"
/>
</div>
<div class="footer-wrap">
<transition
enter-active-class="transition-all delay-300 duration-300 ease"
leave-active-class="transition-all duration-200 ease-in"
enter-class="opacity-0 transform"
enter-to-class="opacity-100 transform translate-y-0"
leave-class="opacity-100 transform translate-y-0"
leave-to-class="opacity-0 transform "
>
<div v-if="currentView === 'messageView'" class="input-wrap">
<chat-footer />
</div>
<team-availability
v-if="currentView === 'cardView'"
:available-agents="availableAgents"
@start-conversation="startConversation"
/>
</transition>
<branding></branding>
<!-- Load Converstion List Components Here -->
</div>
<team-availability
:available-agents="availableAgents"
:has-conversation="!!conversationSize"
@start-conversation="startConversation"
/>
</div>
</template>
<script>
import Branding from 'shared/components/Branding.vue';
import ChatFooter from 'widget/components/ChatFooter.vue';
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
import ChatHeader from 'widget/components/ChatHeader.vue';
import ConversationWrap from 'widget/components/ConversationWrap.vue';
import { IFrameHelper } from 'widget/helpers/utils';
import configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability';
import Spinner from 'shared/components/Spinner.vue';
import Banner from 'widget/components/Banner.vue';
import { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents';
import PreChatForm from '../components/PreChat/Form';
import { isEmptyObject } from 'widget/helpers/utils';
import routerMixin from 'widget/mixins/routerMixin';
export default {
name: 'Home',
components: {
Branding,
ChatFooter,
ChatHeader,
ChatHeaderExpanded,
ConversationWrap,
PreChatForm,
Spinner,
TeamAvailability,
Banner,
},
mixins: [configMixin],
mixins: [configMixin, routerMixin],
props: {
hasFetched: {
type: Boolean,
default: false,
},
showPopoutButton: {
type: Boolean,
default: false,
},
isCampaignViewClicked: {
type: Boolean,
default: false,
@ -122,57 +42,9 @@ export default {
computed: {
...mapGetters({
availableAgents: 'agent/availableAgents',
conversationAttributes: 'conversationAttributes/getConversationParams',
conversationSize: 'conversation/getConversationSize',
groupedMessages: 'conversation/getGroupedConversation',
isFetchingList: 'conversation/getIsFetchingList',
currentUser: 'contacts/getCurrentUser',
activeCampaign: 'campaign/getActiveCampaign',
getCampaignHasExecuted: 'campaign/getCampaignHasExecuted',
conversationSize: 'conversation/getConversationSize',
}),
currentView() {
const { email: currentUserEmail = '' } = this.currentUser;
if (this.isHeaderCollapsed) {
if (this.conversationSize) {
return 'messageView';
}
if (
!this.getCampaignHasExecuted &&
((this.preChatFormEnabled &&
!isEmptyObject(this.activeCampaign) &&
this.preChatFormOptions.requireEmail) ||
this.isOnNewConversation ||
(this.preChatFormEnabled && !currentUserEmail))
) {
return 'preChatFormView';
}
return 'messageView';
}
return 'cardView';
},
isOpen() {
return this.conversationAttributes.status === 'open';
},
fileUploadSizeLimit() {
return MAXIMUM_FILE_UPLOAD_SIZE;
},
isHeaderCollapsed() {
if (
!this.hasIntroText ||
this.conversationSize ||
this.isCampaignViewClicked
) {
return true;
}
return this.isOnCollapsedView;
},
hasIntroText() {
return (
this.channelConfig.welcomeTitle || this.channelConfig.welcomeTagline
);
},
},
mounted() {
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
@ -182,73 +54,11 @@ export default {
},
methods: {
startConversation() {
this.isOnCollapsedView = !this.isOnCollapsedView;
},
closeChat() {
IFrameHelper.sendMessage({ event: 'closeChat' });
if (this.preChatFormEnabled && !this.conversationSize) {
return this.replaceRoute('prechat-form');
}
return this.replaceRoute('messages');
},
},
};
</script>
<style scoped lang="scss">
@import '~widget/assets/scss/variables';
@import '~widget/assets/scss/mixins';
.home {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
flex-wrap: nowrap;
overflow: hidden;
background: $color-background;
.header-wrap {
border-radius: $space-normal $space-normal 0 0;
flex-shrink: 0;
transition: max-height 300ms;
z-index: 99;
@include shadow-large;
&.expanded {
max-height: 16rem;
}
&.collapsed {
max-height: 4.5rem;
}
@media only screen and (min-device-width: 320px) and (max-device-width: 667px) {
border-radius: 0;
}
}
.footer-wrap {
flex-shrink: 0;
width: 100%;
display: flex;
flex-direction: column;
position: relative;
&:before {
content: '';
position: absolute;
top: -$space-normal;
left: 0;
width: 100%;
height: $space-normal;
opacity: 0.1;
background: linear-gradient(
to top,
$color-background,
rgba($color-background, 0)
);
}
}
.input-wrap {
padding: 0 $space-two;
}
}
</style>

View file

@ -0,0 +1,27 @@
<template>
<div class="flex flex-col flex-1 overflow-hidden">
<div class="flex flex-1 overflow-auto">
<conversation-wrap :grouped-messages="groupedMessages" />
</div>
<div class="px-5">
<chat-footer />
</div>
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import ChatFooter from '../components/ChatFooter.vue';
import ConversationWrap from '../components/ConversationWrap.vue';
export default {
components: { ChatFooter, ConversationWrap },
computed: {
...mapGetters({
groupedMessages: 'conversation/getGroupedConversation',
}),
},
mounted() {
this.$store.dispatch('conversation/setUserLastSeen');
},
};
</script>

View file

@ -0,0 +1,49 @@
<template>
<div class="flex flex-1 overflow-auto">
<pre-chat-form :options="preChatFormOptions" @submit="onSubmit" />
</div>
</template>
<script>
import { mapGetters } from 'vuex';
import PreChatForm from '../components/PreChat/Form';
import configMixin from '../mixins/configMixin';
import routerMixin from '../mixins/routerMixin';
export default {
components: {
PreChatForm,
},
mixins: [configMixin, routerMixin],
computed: {
...mapGetters({
conversationSize: 'conversation/getConversationSize',
}),
},
watch: {
conversationSize(newSize, oldSize) {
if (!oldSize && newSize > oldSize) {
this.replaceRoute('messages');
}
},
},
methods: {
onSubmit({ fullName, emailAddress, message, activeCampaignId }) {
if (activeCampaignId) {
bus.$emit('execute-campaign', activeCampaignId);
this.$store.dispatch('contacts/update', {
user: {
email: emailAddress,
name: fullName,
},
});
} else {
this.$store.dispatch('conversation/createConversation', {
fullName: fullName,
emailAddress: emailAddress,
message: message,
});
}
},
},
};
</script>

View file

@ -1,82 +0,0 @@
<template>
<div
id="app"
class="woot-widget-wrap"
:class="{
'is-mobile': isMobile,
'is-widget-right': !isLeftAligned,
'is-bubble-hidden': hideMessageBubble,
}"
>
<home
v-if="showHomePage"
:has-fetched="hasFetched"
:unread-message-count="unreadMessageCount"
:show-popout-button="showPopoutButton"
:is-campaign-view-clicked="isCampaignViewClicked"
/>
<unread
v-else
:show-unread-view="showUnreadView"
:has-fetched="hasFetched"
:unread-message-count="unreadMessageCount"
:hide-message-bubble="hideMessageBubble"
/>
</div>
</template>
<script>
import Home from './Home';
import Unread from './Unread';
export default {
name: 'Router',
components: {
Home,
Unread,
},
props: {
hasFetched: {
type: Boolean,
default: false,
},
isMobile: {
type: Boolean,
default: false,
},
isLeftAligned: {
type: Boolean,
default: false,
},
showUnreadView: {
type: Boolean,
default: false,
},
showCampaignView: {
type: Boolean,
default: false,
},
hideMessageBubble: {
type: Boolean,
default: false,
},
unreadMessageCount: {
type: Number,
default: 0,
},
showPopoutButton: {
type: Boolean,
default: false,
},
isCampaignViewClicked: {
type: Boolean,
default: false,
},
},
computed: {
showHomePage() {
return !this.showUnreadView && !this.showCampaignView;
},
},
};
</script>

View file

@ -0,0 +1,20 @@
<template>
<unread-message-list :messages="messages" />
</template>
<script>
import { mapGetters } from 'vuex';
import UnreadMessageList from '../components/UnreadMessageList.vue';
export default {
name: 'UnreadMessages',
components: {
UnreadMessageList,
},
computed: {
...mapGetters({
messages: 'conversation/getUnreadTextMessages',
}),
},
};
</script>