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:
parent
991a42c417
commit
9c31d7c672
38 changed files with 617 additions and 725 deletions
|
@ -18,11 +18,11 @@ export default {
|
|||
},
|
||||
backgroundColor: {
|
||||
type: String,
|
||||
default: 'white',
|
||||
default: '#c2e1ff',
|
||||
},
|
||||
color: {
|
||||
type: String,
|
||||
default: '',
|
||||
default: '#1976cc',
|
||||
},
|
||||
customStyle: {
|
||||
type: Object,
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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 });
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
},
|
||||
|
|
|
@ -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' });
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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' }),
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -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) {
|
118
app/javascript/widget/components/layouts/ViewWithHeader.vue
Normal file
118
app/javascript/widget/components/layouts/ViewWithHeader.vue
Normal 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>
|
3
app/javascript/widget/constants/widgetBusEvents.js
Normal file
3
app/javascript/widget/constants/widgetBusEvents.js
Normal 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';
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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,
|
||||
|
|
10
app/javascript/widget/mixins/routerMixin.js
Normal file
10
app/javascript/widget/mixins/routerMixin.js
Normal file
|
@ -0,0 +1,10 @@
|
|||
export default {
|
||||
methods: {
|
||||
async replaceRoute(name) {
|
||||
if (this.$route.name !== name) {
|
||||
return this.$router.replace({ name });
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
};
|
|
@ -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'),
|
||||
// },
|
||||
],
|
||||
});
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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: {},
|
||||
};
|
||||
|
|
|
@ -18,7 +18,7 @@ describe('#mutations', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('#setError', () => {
|
||||
describe('#setHasFetched', () => {
|
||||
it('set fetched flag', () => {
|
||||
const state = { records: [], uiFlags: {} };
|
||||
mutations.setHasFetched(state, true);
|
||||
|
|
|
@ -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']]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
],
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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';
|
||||
|
|
28
app/javascript/widget/views/Campaigns.vue
Normal file
28
app/javascript/widget/views/Campaigns.vue
Normal 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>
|
|
@ -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>
|
||||
|
|
27
app/javascript/widget/views/Messages.vue
Normal file
27
app/javascript/widget/views/Messages.vue
Normal 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>
|
49
app/javascript/widget/views/PreChatForm.vue
Normal file
49
app/javascript/widget/views/PreChatForm.vue
Normal 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>
|
|
@ -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>
|
20
app/javascript/widget/views/UnreadMessages.vue
Normal file
20
app/javascript/widget/views/UnreadMessages.vue
Normal 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>
|
Loading…
Reference in a new issue