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: { backgroundColor: {
type: String, type: String,
default: 'white', default: '#c2e1ff',
}, },
color: { color: {
type: String, type: String,
default: '', default: '#1976cc',
}, },
customStyle: { customStyle: {
type: Object, type: Object,

View file

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

View file

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

View file

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

View file

@ -1,9 +1,12 @@
<template> <template>
<header class="header-collapsed"> <header class="flex justify-between p-5 w-full">
<div class="header-branding"> <div class="flex items-center">
<button v-if="showBackButton" @click="onBackButtonClick">
<fluent-icon icon="chevron-left" size="24" />
</button>
<img <img
v-if="avatarUrl" v-if="avatarUrl"
class="inbox--avatar mr-3" class="h-8 w-8 rounded-full mr-3"
:src="avatarUrl" :src="avatarUrl"
alt="avatar" alt="avatar"
/> />
@ -12,14 +15,13 @@
<span class="mr-1" v-html="title" /> <span class="mr-1" v-html="title" />
<div <div
:class=" :class="
`status-view--badge rounded-full leading-4 ${ `h-2 w-2 rounded-full leading-4
isOnline ? 'bg-green-500' : 'hidden' ${isOnline ? 'bg-green-500' : 'hidden'}`
}`
" "
/> />
</div> </div>
<div class="text-xs mt-1 text-black-700"> <div class="text-xs mt-1 text-black-700">
{{ replyWaitMeessage }} {{ replyWaitMessage }}
</div> </div>
</div> </div>
</div> </div>
@ -29,15 +31,19 @@
<script> <script>
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import HeaderActions from './HeaderActions';
import availabilityMixin from 'widget/mixins/availability'; 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 { export default {
name: 'ChatHeader', name: 'ChatHeader',
components: { components: {
FluentIcon,
HeaderActions, HeaderActions,
}, },
mixins: [availabilityMixin], mixins: [availabilityMixin, routerMixin],
props: { props: {
avatarUrl: { avatarUrl: {
type: String, type: String,
@ -51,15 +57,17 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showBackButton: {
type: Boolean,
default: false,
},
availableAgents: { availableAgents: {
type: Array, type: Array,
default: () => {}, default: () => {},
}, },
}, },
computed: { computed: {
...mapGetters({ ...mapGetters({ widgetColor: 'appConfig/getWidgetColor' }),
widgetColor: 'appConfig/getWidgetColor',
}),
isOnline() { isOnline() {
const { workingHoursEnabled } = this.channelConfig; const { workingHoursEnabled } = this.channelConfig;
const anyAgentOnline = this.availableAgents.length > 0; const anyAgentOnline = this.availableAgents.length > 0;
@ -69,47 +77,16 @@ export default {
} }
return anyAgentOnline; return anyAgentOnline;
}, },
replyWaitMeessage() { replyWaitMessage() {
return this.isOnline return this.isOnline
? this.replyTimeStatus ? this.replyTimeStatus
: this.$t('TEAM_AVAILABILITY.OFFLINE'); : this.$t('TEAM_AVAILABILITY.OFFLINE');
}, },
}, },
methods: {
onBackButtonClick() {
this.replaceRoute('home');
},
},
}; };
</script> </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> <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 <div
class="flex items-start" class="flex items-start"
:class="[avatarUrl ? 'justify-between' : 'justify-end']" :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" /> <header-actions :show-popout-button="showPopoutButton" />
</div> </div>
<h2 <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" v-html="introHeading"
/> />
<p class="text-lg text-black-700 leading-normal" v-html="introBody" /> <p class="text-lg text-black-700 leading-normal" v-html="introBody" />
@ -48,17 +48,3 @@ export default {
}, },
}; };
</script> </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: { computed: {
...mapGetters({ ...mapGetters({
widgetColor: 'appConfig/getWidgetColor', widgetColor: 'appConfig/getWidgetColor',
isWidgetOpen: 'appConfig/getIsWidgetOpen',
}), }),
showAttachment() { showAttachment() {
return this.hasAttachmentsEnabled && this.userInput.length === 0; return this.hasAttachmentsEnabled && this.userInput.length === 0;
@ -97,13 +98,10 @@ export default {
showSendButton() { showSendButton() {
return this.userInput.length > 0; return this.userInput.length > 0;
}, },
isOpen() {
return this.$store.state.events.isOpen;
},
}, },
watch: { watch: {
isOpen(isOpen) { isWidgetOpen(isWidgetOpen) {
if (isOpen) { if (isWidgetOpen) {
this.focusInput(); this.focusInput();
} }
}, },
@ -113,7 +111,7 @@ export default {
}, },
mounted() { mounted() {
document.addEventListener('keypress', this.handleEnterKeyPress); document.addEventListener('keypress', this.handleEnterKeyPress);
if (this.isOpen) { if (this.isWidgetOpen) {
this.focusInput(); this.focusInput();
} }
}, },

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -25,10 +25,8 @@ export default {
let requireEmail = false; let requireEmail = false;
let preChatMessage = ''; let preChatMessage = '';
const options = window.chatwootWebChannel.preChatFormOptions || {}; const options = window.chatwootWebChannel.preChatFormOptions || {};
if (!this.isOnNewConversation) { requireEmail = options.require_email;
requireEmail = options.require_email; preChatMessage = options.pre_chat_message;
preChatMessage = options.pre_chat_message;
}
return { return {
requireEmail, requireEmail,
preChatMessage, 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 Vue from 'vue';
import Router from 'vue-router'; import Router from 'vue-router';
import Home from './views/Home.vue'; import ViewWithHeader from './components/layouts/ViewWithHeader.vue';
Vue.use(Router); Vue.use(Router);
export default new Router({ export default new Router({
mode: 'hash',
routes: [ routes: [
{ {
path: '/', path: '/unread-messages',
name: 'home', name: 'unread-messages',
component: Home, 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 = { const state = {
showPopoutButton: false,
hideMessageBubble: false,
position: 'right',
isWebWidgetTriggered: false,
isCampaignViewClicked: false,
isWidgetOpen: false,
widgetColor: '', widgetColor: '',
referrerHost: '', referrerHost: '',
}; };
export const getters = { export const getters = {
getAppConfig: $state => $state,
isRightAligned: $state => $state.position === 'right',
getHideMessageBubble: $state => $state.hideMessageBubble,
getIsWidgetOpen: $state => $state.isWidgetOpen,
getWidgetColor: $state => $state.widgetColor, getWidgetColor: $state => $state.widgetColor,
getReferrerHost: $state => $state.referrerHost, getReferrerHost: $state => $state.referrerHost,
}; };
export const actions = { export const actions = {
setWidgetColor({ commit }, data) { setAppConfig({ commit }, { showPopoutButton, position, hideMessageBubble }) {
commit(SET_WIDGET_COLOR, data); 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) { setReferrerHost({ commit }, referrerHost) {
commit(SET_REFERRER_HOST, referrerHost); commit(SET_REFERRER_HOST, referrerHost);
@ -20,8 +45,16 @@ export const actions = {
}; };
export const mutations = { export const mutations = {
[SET_WIDGET_COLOR]($state, data) { [SET_WIDGET_APP_CONFIG]($state, data) {
$state.widgetColor = data.widgetColor; $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) { [SET_REFERRER_HOST]($state, referrerHost) {
$state.referrerHost = referrerHost; $state.referrerHost = referrerHost;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -24,7 +24,6 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
['setCampaigns', campaigns], ['setCampaigns', campaigns],
['setError', false], ['setError', false],
['setHasFetched', true],
]); ]);
expect(campaignTimer.initTimers).toHaveBeenCalledWith( expect(campaignTimer.initTimers).toHaveBeenCalledWith(
{ {
@ -50,10 +49,7 @@ describe('#actions', () => {
isInBusinessHours: true, isInBusinessHours: true,
} }
); );
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([['setError', true]]);
['setError', true],
['setHasFetched', true],
]);
}); });
}); });
describe('#initCampaigns', () => { describe('#initCampaigns', () => {
@ -99,7 +95,7 @@ describe('#actions', () => {
getters: { getCampaigns: campaigns }, getters: { getCampaigns: campaigns },
commit, commit,
rootState: { rootState: {
events: { isOpen: true }, appConfig: { isWidgetOpen: true },
}, },
}, },
{ campaignId: 32 } { campaignId: 32 }
@ -113,7 +109,7 @@ describe('#actions', () => {
getters: { getCampaigns: campaigns }, getters: { getCampaigns: campaigns },
commit, commit,
rootState: { rootState: {
events: { isOpen: false }, appConfig: { isWidgetOpen: false },
}, },
}, },
{ campaignId: 1 } { campaignId: 1 }
@ -127,15 +123,52 @@ describe('#actions', () => {
API.post.mockResolvedValue({}); API.post.mockResolvedValue({});
await actions.executeCampaign({ commit }, params); await actions.executeCampaign({ commit }, params);
expect(commit.mock.calls).toEqual([ expect(commit.mock.calls).toEqual([
['setCampaignExecuted'], [
'conversation/setConversationUIFlag',
{
isCreating: true,
},
{
root: true,
},
],
['setActiveCampaign', {}], ['setActiveCampaign', {}],
[
'conversation/setConversationUIFlag',
{
isCreating: false,
},
{
root: true,
},
],
]); ]);
}); });
it('sends correct actions if execute campaign API is failed', async () => { it('sends correct actions if execute campaign API is failed', async () => {
const params = { campaignId: 12, websiteToken: 'XDsafmADasd' }; const params = { campaignId: 12, websiteToken: 'XDsafmADasd' };
API.post.mockRejectedValue({ message: 'Authentication required' }); API.post.mockRejectedValue({ message: 'Authentication required' });
await actions.executeCampaign({ commit }, params); 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', 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', () => { describe('#setActiveCampaign', () => {
it('set active campaign', () => { it('set active campaign', () => {
const state = { records: [] }; const state = { records: [] };
@ -33,12 +25,4 @@ describe('#mutations', () => {
expect(state.activeCampaign).toEqual(campaigns[0]); 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 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> <template>
<div <div class="flex flex-1 flex-col justify-end">
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 overflow-auto"> <div class="flex flex-1 overflow-auto">
<conversation-wrap <!-- Load Converstion List Components Here -->
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>
</div> </div>
<team-availability
:available-agents="availableAgents"
:has-conversation="!!conversationSize"
@start-conversation="startConversation"
/>
</div> </div>
</template> </template>
<script> <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 configMixin from '../mixins/configMixin';
import TeamAvailability from 'widget/components/TeamAvailability'; 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 { mapGetters } from 'vuex';
import { MAXIMUM_FILE_UPLOAD_SIZE } from 'shared/constants/messages';
import { BUS_EVENTS } from 'shared/constants/busEvents'; import { BUS_EVENTS } from 'shared/constants/busEvents';
import PreChatForm from '../components/PreChat/Form'; import routerMixin from 'widget/mixins/routerMixin';
import { isEmptyObject } from 'widget/helpers/utils';
export default { export default {
name: 'Home', name: 'Home',
components: { components: {
Branding,
ChatFooter,
ChatHeader,
ChatHeaderExpanded,
ConversationWrap,
PreChatForm,
Spinner,
TeamAvailability, TeamAvailability,
Banner,
}, },
mixins: [configMixin], mixins: [configMixin, routerMixin],
props: { props: {
hasFetched: { hasFetched: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
showPopoutButton: {
type: Boolean,
default: false,
},
isCampaignViewClicked: { isCampaignViewClicked: {
type: Boolean, type: Boolean,
default: false, default: false,
@ -122,57 +42,9 @@ export default {
computed: { computed: {
...mapGetters({ ...mapGetters({
availableAgents: 'agent/availableAgents', availableAgents: 'agent/availableAgents',
conversationAttributes: 'conversationAttributes/getConversationParams',
conversationSize: 'conversation/getConversationSize',
groupedMessages: 'conversation/getGroupedConversation',
isFetchingList: 'conversation/getIsFetchingList',
currentUser: 'contacts/getCurrentUser',
activeCampaign: 'campaign/getActiveCampaign', 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() { mounted() {
bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => { bus.$on(BUS_EVENTS.START_NEW_CONVERSATION, () => {
@ -182,73 +54,11 @@ export default {
}, },
methods: { methods: {
startConversation() { startConversation() {
this.isOnCollapsedView = !this.isOnCollapsedView; if (this.preChatFormEnabled && !this.conversationSize) {
}, return this.replaceRoute('prechat-form');
closeChat() { }
IFrameHelper.sendMessage({ event: 'closeChat' }); return this.replaceRoute('messages');
}, },
}, },
}; };
</script> </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>