mirror of
https://github.com/lingble/chatwoot.git
synced 2025-11-01 19:48:08 +00:00
Adds unread message bubbles for widget (#943)
Co-authored-by: Sojan <sojan@pepalo.com> Co-authored-by: Pranav Raj S <pranav@thoughtwoot.com>
This commit is contained in:
committed by
GitHub
parent
6a7d810c95
commit
49db9c5d8a
@@ -5,6 +5,14 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
|||||||
@conversation = conversation
|
@conversation = conversation
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def update_last_seen
|
||||||
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
conversation.user_last_seen_at = DateTime.now.utc
|
||||||
|
conversation.save!
|
||||||
|
head :ok
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_typing
|
def toggle_typing
|
||||||
head :ok && return if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Vuelidate from 'vuelidate';
|
|||||||
import VueI18n from 'vue-i18n';
|
import VueI18n from 'vue-i18n';
|
||||||
import store from '../widget/store';
|
import store from '../widget/store';
|
||||||
import App from '../widget/App.vue';
|
import App from '../widget/App.vue';
|
||||||
import router from '../widget/router';
|
|
||||||
import ActionCableConnector from '../widget/helpers/actionCable';
|
import ActionCableConnector from '../widget/helpers/actionCable';
|
||||||
import i18n from '../widget/i18n';
|
import i18n from '../widget/i18n';
|
||||||
|
|
||||||
@@ -15,10 +14,13 @@ Object.keys(i18n).forEach(lang => {
|
|||||||
Vue.locale(lang, i18n[lang]);
|
Vue.locale(lang, i18n[lang]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Event Bus
|
||||||
|
window.bus = new Vue();
|
||||||
|
|
||||||
Vue.config.productionTip = false;
|
Vue.config.productionTip = false;
|
||||||
|
|
||||||
window.onload = () => {
|
window.onload = () => {
|
||||||
window.WOOT_WIDGET = new Vue({
|
window.WOOT_WIDGET = new Vue({
|
||||||
router,
|
|
||||||
store,
|
store,
|
||||||
render: h => h(App),
|
render: h => h(App),
|
||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|||||||
@@ -61,3 +61,7 @@ export const addClass = (elm, classes) => {
|
|||||||
export const toggleClass = (elm, classes) => {
|
export const toggleClass = (elm, classes) => {
|
||||||
classHelper(classes, 'toggle', elm);
|
classHelper(classes, 'toggle', elm);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const removeClass = (elm, classes) => {
|
||||||
|
classHelper(classes, 'remove', elm);
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import Cookies from 'js-cookie';
|
import Cookies from 'js-cookie';
|
||||||
import { wootOn, loadCSS } from './DOMHelpers';
|
import { wootOn, loadCSS, addClass, removeClass } from './DOMHelpers';
|
||||||
import {
|
import {
|
||||||
body,
|
body,
|
||||||
widgetHolder,
|
widgetHolder,
|
||||||
@@ -29,7 +29,8 @@ export const IFrameHelper = {
|
|||||||
|
|
||||||
iframe.id = 'chatwoot_live_chat_widget';
|
iframe.id = 'chatwoot_live_chat_widget';
|
||||||
iframe.style.visibility = 'hidden';
|
iframe.style.visibility = 'hidden';
|
||||||
widgetHolder.className = `woot-widget-holder woot--hide woot-elements--${window.$chatwoot.position}`;
|
const HolderclassName = `woot-widget-holder woot--hide woot-elements--${window.$chatwoot.position}`;
|
||||||
|
addClass(widgetHolder, HolderclassName);
|
||||||
widgetHolder.appendChild(iframe);
|
widgetHolder.appendChild(iframe);
|
||||||
body.appendChild(widgetHolder);
|
body.appendChild(widgetHolder);
|
||||||
IFrameHelper.initPostMessageCommunication();
|
IFrameHelper.initPostMessageCommunication();
|
||||||
@@ -92,6 +93,8 @@ export const IFrameHelper = {
|
|||||||
window.$chatwoot.hasLoaded = true;
|
window.$chatwoot.hasLoaded = true;
|
||||||
IFrameHelper.sendMessage('config-set', {
|
IFrameHelper.sendMessage('config-set', {
|
||||||
locale: window.$chatwoot.locale,
|
locale: window.$chatwoot.locale,
|
||||||
|
position: window.$chatwoot.position,
|
||||||
|
hideMessageBubble: window.$chatwoot.hideMessageBubble,
|
||||||
});
|
});
|
||||||
IFrameHelper.onLoad(message.config.channelConfig);
|
IFrameHelper.onLoad(message.config.channelConfig);
|
||||||
IFrameHelper.setCurrentUrl();
|
IFrameHelper.setCurrentUrl();
|
||||||
@@ -105,6 +108,37 @@ export const IFrameHelper = {
|
|||||||
toggleBubble: () => {
|
toggleBubble: () => {
|
||||||
onBubbleClick();
|
onBubbleClick();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
onBubbleToggle: isOpen => {
|
||||||
|
if (!isOpen) {
|
||||||
|
IFrameHelper.events.resetUnreadMode();
|
||||||
|
} else {
|
||||||
|
IFrameHelper.pushEvent('webwidget.triggered');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setUnreadMode: message => {
|
||||||
|
const { unreadMessageCount } = message;
|
||||||
|
const { isOpen } = window.$chatwoot;
|
||||||
|
const toggleValue = true;
|
||||||
|
|
||||||
|
if (!isOpen && unreadMessageCount > 0) {
|
||||||
|
IFrameHelper.sendMessage('set-unread-view');
|
||||||
|
onBubbleClick({ toggleValue });
|
||||||
|
const holderEl = document.querySelector('.woot-widget-holder');
|
||||||
|
addClass(holderEl, 'has-unread-view');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
resetUnreadMode: () => {
|
||||||
|
IFrameHelper.sendMessage('unset-unread-view');
|
||||||
|
IFrameHelper.events.removeUnreadClass();
|
||||||
|
},
|
||||||
|
|
||||||
|
removeUnreadClass: () => {
|
||||||
|
const holderEl = document.querySelector('.woot-widget-holder');
|
||||||
|
removeClass(holderEl, 'has-unread-view');
|
||||||
|
},
|
||||||
},
|
},
|
||||||
pushEvent: eventName => {
|
pushEvent: eventName => {
|
||||||
IFrameHelper.sendMessage('push-event', { eventName });
|
IFrameHelper.sendMessage('push-event', { eventName });
|
||||||
@@ -125,7 +159,8 @@ export const IFrameHelper = {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const closeIcon = closeBubble;
|
const closeIcon = closeBubble;
|
||||||
closeIcon.className = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
const closeIconclassName = `woot-elements--${window.$chatwoot.position} woot-widget-bubble woot--close woot--hide`;
|
||||||
|
addClass(closeIcon, closeIconclassName);
|
||||||
|
|
||||||
chatIcon.style.background = widgetColor;
|
chatIcon.style.background = widgetColor;
|
||||||
closeIcon.style.background = widgetColor;
|
closeIcon.style.background = widgetColor;
|
||||||
@@ -143,9 +178,13 @@ export const IFrameHelper = {
|
|||||||
},
|
},
|
||||||
toggleCloseButton: () => {
|
toggleCloseButton: () => {
|
||||||
if (window.matchMedia('(max-width: 668px)').matches) {
|
if (window.matchMedia('(max-width: 668px)').matches) {
|
||||||
IFrameHelper.sendMessage('toggle-close-button', { showClose: true });
|
IFrameHelper.sendMessage('toggle-close-button', {
|
||||||
|
showClose: true,
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
IFrameHelper.sendMessage('toggle-close-button', { showClose: false });
|
IFrameHelper.sendMessage('toggle-close-button', {
|
||||||
|
showClose: false,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -31,13 +31,17 @@ export const createNotificationBubble = () => {
|
|||||||
return notificationBubble;
|
return notificationBubble;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const onBubbleClick = () => {
|
export const onBubbleClick = (props = {}) => {
|
||||||
window.$chatwoot.isOpen = !window.$chatwoot.isOpen;
|
const { toggleValue } = props;
|
||||||
toggleClass(chatBubble, 'woot--hide');
|
const { isOpen } = window.$chatwoot;
|
||||||
toggleClass(closeBubble, 'woot--hide');
|
if (isOpen !== toggleValue) {
|
||||||
toggleClass(widgetHolder, 'woot--hide');
|
const newIsOpen = toggleValue === undefined ? !isOpen : toggleValue;
|
||||||
if (window.$chatwoot.isOpen) {
|
window.$chatwoot.isOpen = newIsOpen;
|
||||||
IFrameHelper.pushEvent('webwidget.triggered');
|
|
||||||
|
toggleClass(chatBubble, 'woot--hide');
|
||||||
|
toggleClass(closeBubble, 'woot--hide');
|
||||||
|
toggleClass(widgetHolder, 'woot--hide');
|
||||||
|
IFrameHelper.events.onBubbleToggle(newIsOpen);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,22 +1,57 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app" class="woot-widget-wrap" :class="{ 'is-mobile': isMobile }">
|
<router
|
||||||
<router-view />
|
:show-unread-view="showUnreadView"
|
||||||
</div>
|
:is-mobile="isMobile"
|
||||||
|
:grouped-messages="groupedMessages"
|
||||||
|
:unread-messages="unreadMessages"
|
||||||
|
:conversation-size="conversationSize"
|
||||||
|
:available-agents="availableAgents"
|
||||||
|
:has-fetched="hasFetched"
|
||||||
|
:conversation-attributes="conversationAttributes"
|
||||||
|
:unread-message-count="unreadMessageCount"
|
||||||
|
:is-left-aligned="isLeftAligned"
|
||||||
|
:hide-message-bubble="hideMessageBubble"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex';
|
/* global bus */
|
||||||
|
|
||||||
|
import Vue from 'vue';
|
||||||
|
import { mapGetters, mapActions } from 'vuex';
|
||||||
import { setHeader } from 'widget/helpers/axios';
|
import { setHeader } from 'widget/helpers/axios';
|
||||||
import { IFrameHelper } from 'widget/helpers/utils';
|
import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
import Vue from 'vue';
|
|
||||||
|
import Router from './views/Router';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'App',
|
name: 'App',
|
||||||
|
components: {
|
||||||
|
Router,
|
||||||
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
showUnreadView: false,
|
||||||
isMobile: false,
|
isMobile: false,
|
||||||
|
hideMessageBubble: false,
|
||||||
|
widgetPosition: 'right',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
groupedMessages: 'conversation/getGroupedConversation',
|
||||||
|
unreadMessages: 'conversation/getUnreadTextMessages',
|
||||||
|
conversationSize: 'conversation/getConversationSize',
|
||||||
|
availableAgents: 'agent/availableAgents',
|
||||||
|
hasFetched: 'agent/getHasFetched',
|
||||||
|
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||||
|
unreadMessageCount: 'conversation/getUnreadMessageCount',
|
||||||
|
}),
|
||||||
|
isLeftAligned() {
|
||||||
|
const isLeft = this.widgetPosition === 'left';
|
||||||
|
return isLeft;
|
||||||
|
},
|
||||||
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const { websiteToken, locale } = window.chatwootWebChannel;
|
const { websiteToken, locale } = window.chatwootWebChannel;
|
||||||
this.setLocale(locale);
|
this.setLocale(locale);
|
||||||
@@ -42,9 +77,13 @@ export default {
|
|||||||
|
|
||||||
const message = JSON.parse(e.data.replace(wootPrefix, ''));
|
const message = JSON.parse(e.data.replace(wootPrefix, ''));
|
||||||
if (message.event === 'config-set') {
|
if (message.event === 'config-set') {
|
||||||
this.fetchOldConversations();
|
this.fetchOldConversations().then(() => {
|
||||||
|
this.setUnreadView();
|
||||||
|
});
|
||||||
this.fetchAvailableAgents(websiteToken);
|
this.fetchAvailableAgents(websiteToken);
|
||||||
this.setLocale(message.locale);
|
this.setLocale(message.locale);
|
||||||
|
this.setPosition(message.position);
|
||||||
|
this.setHideMessageBubble(message.hideMessageBubble);
|
||||||
} else if (message.event === 'widget-visible') {
|
} else if (message.event === 'widget-visible') {
|
||||||
this.scrollConversationToBottom();
|
this.scrollConversationToBottom();
|
||||||
} else if (message.event === 'set-current-url') {
|
} else if (message.event === 'set-current-url') {
|
||||||
@@ -52,7 +91,7 @@ export default {
|
|||||||
} else if (message.event === 'toggle-close-button') {
|
} else if (message.event === 'toggle-close-button') {
|
||||||
this.isMobile = message.showClose;
|
this.isMobile = message.showClose;
|
||||||
} else if (message.event === 'push-event') {
|
} else if (message.event === 'push-event') {
|
||||||
this.$store.dispatch('events/create', { name: message.eventName });
|
this.createWidgetEvents(message);
|
||||||
} else if (message.event === 'set-label') {
|
} else if (message.event === 'set-label') {
|
||||||
this.$store.dispatch('conversationLabels/create', message.label);
|
this.$store.dispatch('conversationLabels/create', message.label);
|
||||||
} else if (message.event === 'remove-label') {
|
} else if (message.event === 'remove-label') {
|
||||||
@@ -61,14 +100,19 @@ export default {
|
|||||||
this.$store.dispatch('contacts/update', message);
|
this.$store.dispatch('contacts/update', message);
|
||||||
} else if (message.event === 'set-locale') {
|
} else if (message.event === 'set-locale') {
|
||||||
this.setLocale(message.locale);
|
this.setLocale(message.locale);
|
||||||
|
} else if (message.event === 'set-unread-view') {
|
||||||
|
this.showUnreadView = true;
|
||||||
|
} else if (message.event === 'unset-unread-view') {
|
||||||
|
this.showUnreadView = false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
this.$store.dispatch('conversationAttributes/get');
|
this.$store.dispatch('conversationAttributes/get');
|
||||||
|
this.registerUnreadEvents();
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('appConfig', ['setWidgetColor']),
|
...mapActions('appConfig', ['setWidgetColor']),
|
||||||
...mapActions('conversation', ['fetchOldConversations']),
|
...mapActions('conversation', ['fetchOldConversations', 'setUserLastSeen']),
|
||||||
...mapActions('agent', ['fetchAvailableAgents']),
|
...mapActions('agent', ['fetchAvailableAgents']),
|
||||||
scrollConversationToBottom() {
|
scrollConversationToBottom() {
|
||||||
const container = this.$el.querySelector('.conversation-wrap');
|
const container = this.$el.querySelector('.conversation-wrap');
|
||||||
@@ -80,6 +124,43 @@ export default {
|
|||||||
Vue.config.lang = locale;
|
Vue.config.lang = locale;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
setPosition(position) {
|
||||||
|
const widgetPosition = position || 'right';
|
||||||
|
this.widgetPosition = widgetPosition;
|
||||||
|
},
|
||||||
|
setHideMessageBubble(hideBubble) {
|
||||||
|
this.hideMessageBubble = !!hideBubble;
|
||||||
|
},
|
||||||
|
registerUnreadEvents() {
|
||||||
|
bus.$on('on-agent-message-recieved', () => this.setUnreadView());
|
||||||
|
bus.$on('on-unread-view-clicked', () => {
|
||||||
|
this.unsetUnreadView();
|
||||||
|
this.setUserLastSeen();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
setUnreadView() {
|
||||||
|
const { unreadMessageCount } = this;
|
||||||
|
if (IFrameHelper.isIFrame() && unreadMessageCount > 0) {
|
||||||
|
IFrameHelper.sendMessage({
|
||||||
|
event: 'setUnreadMode',
|
||||||
|
unreadMessageCount,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
unsetUnreadView() {
|
||||||
|
if (IFrameHelper.isIFrame()) {
|
||||||
|
IFrameHelper.sendMessage({ event: 'resetUnreadMode' });
|
||||||
|
}
|
||||||
|
},
|
||||||
|
createWidgetEvents(message) {
|
||||||
|
const { eventName } = message;
|
||||||
|
const isWidgetTriggerEvent = eventName === 'webwidget.triggered';
|
||||||
|
if (isWidgetTriggerEvent && this.showUnreadView) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.setUserLastSeen();
|
||||||
|
this.$store.dispatch('events/create', { name: eventName });
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -30,10 +30,18 @@ const toggleTyping = async ({ typingStatus }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const setUserLastSeenAt = async ({ lastSeen }) => {
|
||||||
|
return API.post(
|
||||||
|
`/api/v1/widget/conversations/update_last_seen${window.location.search}`,
|
||||||
|
{ user_last_seen_at: lastSeen }
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export {
|
export {
|
||||||
sendMessageAPI,
|
sendMessageAPI,
|
||||||
getConversationAPI,
|
getConversationAPI,
|
||||||
getMessagesAPI,
|
getMessagesAPI,
|
||||||
sendAttachmentAPI,
|
sendAttachmentAPI,
|
||||||
toggleTyping,
|
toggleTyping,
|
||||||
|
setUserLastSeenAt,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -60,6 +60,11 @@ $color-body: #3c4858;
|
|||||||
$color-heading: #1f2d3d;
|
$color-heading: #1f2d3d;
|
||||||
$color-error: #ff382d;
|
$color-error: #ff382d;
|
||||||
|
|
||||||
|
// Color-palettes
|
||||||
|
|
||||||
|
$color-primary-light: #c7e3ff;
|
||||||
|
$color-primary-dark: darken($color-woot, 20%);
|
||||||
|
|
||||||
|
|
||||||
// Thumbnail
|
// Thumbnail
|
||||||
$thumbnail-radius: 4rem;
|
$thumbnail-radius: 4rem;
|
||||||
@@ -110,3 +115,7 @@ $spinkit-size: 1.6rem !default;
|
|||||||
|
|
||||||
// Break points
|
// Break points
|
||||||
$break-point-medium: 667px;
|
$break-point-medium: 667px;
|
||||||
|
|
||||||
|
// Timing functions
|
||||||
|
|
||||||
|
$ease-in-cubic: cubic-bezier(.17, .67, .83, .67);
|
||||||
|
|||||||
@@ -11,6 +11,18 @@ export const SDK_CSS = ` .woot-widget-holder {
|
|||||||
transition-duration: 0.5s, 0.5s;
|
transition-duration: 0.5s, 0.5s;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.woot-widget-holder.has-unread-view {
|
||||||
|
box-shadow: none !important;
|
||||||
|
-moz-box-shadow: none !important;
|
||||||
|
-o-box-shadow: none !important;
|
||||||
|
-webkit-box-shadow: none !important;
|
||||||
|
-o-border-radius: 0 !important;
|
||||||
|
-moz-border-radius: 0 !important;
|
||||||
|
-webkit-border-radius: 0 !important;
|
||||||
|
border-radius: 0 !important;
|
||||||
|
bottom: 94px;
|
||||||
|
}
|
||||||
|
|
||||||
.woot-widget-holder iframe {
|
.woot-widget-holder iframe {
|
||||||
width: 100% !important;
|
width: 100% !important;
|
||||||
height: 100% !important;
|
height: 100% !important;
|
||||||
@@ -94,7 +106,7 @@ export const SDK_CSS = ` .woot-widget-holder {
|
|||||||
visibility: hidden !important;
|
visibility: hidden !important;
|
||||||
z-index: -1 !important;
|
z-index: -1 !important;
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
bottom: 60px;
|
bottom: -20000px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@media only screen and (max-width: 667px) {
|
@media only screen and (max-width: 667px) {
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div class="chat-bubble-wrap">
|
||||||
<div
|
<div
|
||||||
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
v-if="!isCards && !isOptions && !isForm && !isArticle"
|
||||||
class="chat-bubble agent"
|
class="chat-bubble agent"
|
||||||
|
|||||||
@@ -19,7 +19,9 @@ class ActionCableConnector extends BaseActionCableConnector {
|
|||||||
};
|
};
|
||||||
|
|
||||||
onMessageCreated = data => {
|
onMessageCreated = data => {
|
||||||
this.app.$store.dispatch('conversation/addMessage', data);
|
this.app.$store.dispatch('conversation/addMessage', data).then(() => {
|
||||||
|
window.bus.$emit('on-agent-message-recieved');
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
onMessageUpdated = data => {
|
onMessageUpdated = data => {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@
|
|||||||
"OTHERS_ARE_AVAILABLE": "others are available",
|
"OTHERS_ARE_AVAILABLE": "others are available",
|
||||||
"AND": "and"
|
"AND": "and"
|
||||||
},
|
},
|
||||||
|
"UNREAD_VIEW": {
|
||||||
|
"VIEW_MESSAGES_BUTTON": "See new messages",
|
||||||
|
"CLOSE_MESSAGES_BUTTON": "Close"
|
||||||
|
},
|
||||||
"POWERED_BY": "Powered by Chatwoot",
|
"POWERED_BY": "Powered by Chatwoot",
|
||||||
"EMAIL_PLACEHOLDER": "Please enter your email",
|
"EMAIL_PLACEHOLDER": "Please enter your email",
|
||||||
"CHAT_PLACEHOLDER": "Type your message"
|
"CHAT_PLACEHOLDER": "Type your message"
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ const state = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const getters = {
|
export const getters = {
|
||||||
|
getHasFetched: $state => $state.uiFlags.hasFetched,
|
||||||
availableAgents: $state =>
|
availableAgents: $state =>
|
||||||
$state.records.filter(agent => agent.availability_status === 'online'),
|
$state.records.filter(agent => agent.availability_status === 'online'),
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import {
|
|||||||
getMessagesAPI,
|
getMessagesAPI,
|
||||||
sendAttachmentAPI,
|
sendAttachmentAPI,
|
||||||
toggleTyping,
|
toggleTyping,
|
||||||
|
setUserLastSeenAt,
|
||||||
} from 'widget/api/conversation';
|
} from 'widget/api/conversation';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
import { playNotificationAudio } from 'shared/helpers/AudioNotificationHelper';
|
import { playNotificationAudio } from 'shared/helpers/AudioNotificationHelper';
|
||||||
@@ -59,10 +60,22 @@ export const findUndeliveredMessage = (messageInbox, { content }) =>
|
|||||||
message => message.content === content && message.status === 'in_progress'
|
message => message.content === content && message.status === 'in_progress'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
export const onNewMessageCreated = data => {
|
||||||
|
const { message_type: messageType } = data;
|
||||||
|
const isIncomingMessage = messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
|
||||||
|
if (isIncomingMessage) {
|
||||||
|
playNotificationAudio();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export const DEFAULT_CONVERSATION = 'default';
|
export const DEFAULT_CONVERSATION = 'default';
|
||||||
|
|
||||||
const state = {
|
const state = {
|
||||||
conversations: {},
|
conversations: {},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: undefined,
|
||||||
|
},
|
||||||
uiFlags: {
|
uiFlags: {
|
||||||
allMessagesLoaded: false,
|
allMessagesLoaded: false,
|
||||||
isFetchingList: false,
|
isFetchingList: false,
|
||||||
@@ -93,6 +106,31 @@ export const getters = {
|
|||||||
}));
|
}));
|
||||||
},
|
},
|
||||||
getIsFetchingList: _state => _state.uiFlags.isFetchingList,
|
getIsFetchingList: _state => _state.uiFlags.isFetchingList,
|
||||||
|
getUnreadMessageCount: _state => {
|
||||||
|
const { userLastSeenAt } = _state.meta;
|
||||||
|
console.log(userLastSeenAt);
|
||||||
|
const count = Object.values(_state.conversations).filter(chat => {
|
||||||
|
const { created_at: createdAt, message_type: messageType } = chat;
|
||||||
|
const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
const hasNotSeen = userLastSeenAt
|
||||||
|
? createdAt * 1000 > userLastSeenAt * 1000
|
||||||
|
: true;
|
||||||
|
return hasNotSeen && isOutGoing;
|
||||||
|
}).length;
|
||||||
|
return count;
|
||||||
|
},
|
||||||
|
getUnreadTextMessages: (_state, _getters) => {
|
||||||
|
const unreadCount = _getters.getUnreadMessageCount;
|
||||||
|
const allMessages = [...Object.values(_state.conversations)];
|
||||||
|
console.log(unreadCount);
|
||||||
|
const unreadAgentMessages = allMessages.filter(message => {
|
||||||
|
const { message_type: messageType } = message;
|
||||||
|
return messageType === MESSAGE_TYPE.OUTGOING;
|
||||||
|
});
|
||||||
|
const maxUnreadCount = Math.min(unreadCount, 3);
|
||||||
|
const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount);
|
||||||
|
return allUnreadMessages;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const actions = {
|
export const actions = {
|
||||||
@@ -112,7 +150,9 @@ export const actions = {
|
|||||||
file_type: fileType,
|
file_type: fileType,
|
||||||
status: 'in_progress',
|
status: 'in_progress',
|
||||||
};
|
};
|
||||||
const tempMessage = createTemporaryMessage({ attachments: [attachment] });
|
const tempMessage = createTemporaryMessage({
|
||||||
|
attachments: [attachment],
|
||||||
|
});
|
||||||
commit('pushMessageToConversation', tempMessage);
|
commit('pushMessageToConversation', tempMessage);
|
||||||
try {
|
try {
|
||||||
const { data } = await sendAttachmentAPI(params);
|
const { data } = await sendAttachmentAPI(params);
|
||||||
@@ -136,12 +176,9 @@ export const actions = {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
addMessage({ commit }, data) {
|
addMessage: async ({ commit }, data) => {
|
||||||
if (data.message_type === MESSAGE_TYPE.OUTGOING) {
|
|
||||||
playNotificationAudio();
|
|
||||||
}
|
|
||||||
|
|
||||||
commit('pushMessageToConversation', data);
|
commit('pushMessageToConversation', data);
|
||||||
|
onNewMessageCreated(data);
|
||||||
},
|
},
|
||||||
|
|
||||||
updateMessage({ commit }, data) {
|
updateMessage({ commit }, data) {
|
||||||
@@ -156,7 +193,21 @@ export const actions = {
|
|||||||
try {
|
try {
|
||||||
await toggleTyping(data);
|
await toggleTyping(data);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// console error
|
// IgnoreError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
setUserLastSeen: async ({ commit, getters: appGetters }) => {
|
||||||
|
if (!appGetters.getConversationSize) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastSeen = Date.now() / 1000;
|
||||||
|
try {
|
||||||
|
commit('setMetaUserLastSeenAt', lastSeen);
|
||||||
|
await setUserLastSeenAt({ lastSeen });
|
||||||
|
} catch (error) {
|
||||||
|
// IgnoreError
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
@@ -224,6 +275,10 @@ export const mutations = {
|
|||||||
const isTyping = status === 'on';
|
const isTyping = status === 'on';
|
||||||
$state.uiFlags.isAgentTyping = isTyping;
|
$state.uiFlags.isAgentTyping = isTyping;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
setMetaUserLastSeenAt($state, lastSeen) {
|
||||||
|
$state.meta.userLastSeenAt = lastSeen;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
|||||||
@@ -17,7 +17,9 @@ export const actions = {
|
|||||||
get: async ({ commit }) => {
|
get: async ({ commit }) => {
|
||||||
try {
|
try {
|
||||||
const { data } = await getConversationAPI();
|
const { data } = await getConversationAPI();
|
||||||
|
const { user_last_seen_at: lastSeen } = data;
|
||||||
commit(SET_CONVERSATION_ATTRIBUTES, data);
|
commit(SET_CONVERSATION_ATTRIBUTES, data);
|
||||||
|
commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// Ignore error
|
// Ignore error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,4 +86,12 @@ describe('#actions', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('#setUserLastSeen', () => {
|
||||||
|
it('sends correct mutations', () => {
|
||||||
|
const lastSeen = Math.abs(Date.now() / 1000);
|
||||||
|
actions.setUserLastSeen({ commit }, { lastSeen });
|
||||||
|
expect(commit).toBeCalledWith('setMetaUserLastSeenAt', lastSeen);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -262,4 +262,171 @@ describe('#getters', () => {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('getUnreadMessageCount returns', () => {
|
||||||
|
it('0 if there are no messages and last seen is undefined', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('0 if there are no messages and last seen is present', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unread count if there are messages and last seen is before messages created-at', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
content: 'Thanks for the help',
|
||||||
|
created_at: 1574075964,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1574092218,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: 1474075964,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUnreadMessageCount(state)).toEqual(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unread count if there are messages and last seen is after messages created-at', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
content: 'Thanks for the help',
|
||||||
|
created_at: 1574075964,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1574092218,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
id: 3,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1574092218,
|
||||||
|
message_type: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: 1674075964,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(getters.getUnreadMessageCount(state)).toEqual(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getUnreadTextMessages returns', () => {
|
||||||
|
it('no messages if there are no messages and last seen is undefined', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: undefined,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('0 if there are no messages and last seen is present', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: Date.now(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
|
||||||
|
).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('only unread text messages from agent if there are messages and last seen is before messages created-at', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
content: 'Thanks for the help',
|
||||||
|
created_at: 1574075964,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1574092218,
|
||||||
|
message_type: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
id: 1,
|
||||||
|
content: 'Thanks for the help',
|
||||||
|
created_at: 1574075964,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('unread messages omitting seen messages ', () => {
|
||||||
|
const state = {
|
||||||
|
conversations: {
|
||||||
|
1: {
|
||||||
|
id: 1,
|
||||||
|
content: 'Thanks for the help',
|
||||||
|
created_at: 1574075964,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
2: {
|
||||||
|
id: 2,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1674075965,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
3: {
|
||||||
|
id: 3,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1574092218,
|
||||||
|
message_type: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
meta: {
|
||||||
|
userLastSeenAt: 1674075964,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
expect(
|
||||||
|
getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
|
||||||
|
).toEqual([
|
||||||
|
{
|
||||||
|
id: 2,
|
||||||
|
content: 'Yes, It makes sense',
|
||||||
|
created_at: 1674075965,
|
||||||
|
message_type: 1,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ describe('#actions', () => {
|
|||||||
await actions.get({ commit });
|
await actions.get({ commit });
|
||||||
expect(commit.mock.calls).toEqual([
|
expect(commit.mock.calls).toEqual([
|
||||||
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
|
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
|
||||||
|
['conversation/setMetaUserLastSeenAt', undefined, { root: true }],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
it('doesnot send mutation if api is error', async () => {
|
it('doesnot send mutation if api is error', async () => {
|
||||||
|
|||||||
@@ -25,8 +25,6 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapGetters } from 'vuex';
|
|
||||||
|
|
||||||
import Branding from 'widget/components/Branding.vue';
|
import Branding from 'widget/components/Branding.vue';
|
||||||
import ChatFooter from 'widget/components/ChatFooter.vue';
|
import ChatFooter from 'widget/components/ChatFooter.vue';
|
||||||
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
||||||
@@ -46,14 +44,33 @@ export default {
|
|||||||
AvailableAgents,
|
AvailableAgents,
|
||||||
},
|
},
|
||||||
mixins: [configMixin],
|
mixins: [configMixin],
|
||||||
|
props: {
|
||||||
|
groupedMessages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
conversationSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
availableAgents: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
hasFetched: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
conversationAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
unreadMessageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
|
||||||
groupedMessages: 'conversation/getGroupedConversation',
|
|
||||||
conversationSize: 'conversation/getConversationSize',
|
|
||||||
availableAgents: 'agent/availableAgents',
|
|
||||||
hasFetched: 'agent/uiFlags/hasFetched',
|
|
||||||
conversationAttributes: 'conversationAttributes/getConversationParams',
|
|
||||||
}),
|
|
||||||
isOpen() {
|
isOpen() {
|
||||||
return this.conversationAttributes.status === 'open';
|
return this.conversationAttributes.status === 'open';
|
||||||
},
|
},
|
||||||
|
|||||||
86
app/javascript/widget/views/Router.vue
Normal file
86
app/javascript/widget/views/Router.vue
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
<template>
|
||||||
|
<div
|
||||||
|
id="app"
|
||||||
|
class="woot-widget-wrap"
|
||||||
|
:class="{ 'is-mobile': isMobile, 'is-widget-right': !isLeftAligned }"
|
||||||
|
>
|
||||||
|
<home
|
||||||
|
v-if="!showUnreadView"
|
||||||
|
:grouped-messages="groupedMessages"
|
||||||
|
:conversation-size="conversationSize"
|
||||||
|
:available-agents="availableAgents"
|
||||||
|
:has-fetched="hasFetched"
|
||||||
|
:conversation-attributes="conversationAttributes"
|
||||||
|
:unread-message-count="unreadMessageCount"
|
||||||
|
/>
|
||||||
|
<unread
|
||||||
|
v-else
|
||||||
|
:unread-messages="unreadMessages"
|
||||||
|
:conversation-size="conversationSize"
|
||||||
|
:available-agents="availableAgents"
|
||||||
|
:has-fetched="hasFetched"
|
||||||
|
:conversation-attributes="conversationAttributes"
|
||||||
|
: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: {
|
||||||
|
groupedMessages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
unreadMessages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
conversationSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
availableAgents: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
hasFetched: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isMobile: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
isLeftAligned: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
showUnreadView: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
hideMessageBubble: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
conversationAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
unreadMessageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
207
app/javascript/widget/views/Unread.vue
Normal file
207
app/javascript/widget/views/Unread.vue
Normal file
@@ -0,0 +1,207 @@
|
|||||||
|
<template>
|
||||||
|
<div class="unread-wrap">
|
||||||
|
<div class="close-unread-wrap">
|
||||||
|
<button
|
||||||
|
v-if="showCloseButton"
|
||||||
|
class="button small close-unread-button"
|
||||||
|
@click="closeFullView"
|
||||||
|
>
|
||||||
|
<i class="ion-close-round" />
|
||||||
|
{{ $t('UNREAD_VIEW.CLOSE_MESSAGES_BUTTON') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="unread-messages">
|
||||||
|
<agent-bubble
|
||||||
|
v-for="message in unreadMessages"
|
||||||
|
:key="message.id"
|
||||||
|
:message-id="message.id"
|
||||||
|
:message="message.content"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<button
|
||||||
|
v-if="unreadMessageCount"
|
||||||
|
class="button clear-button"
|
||||||
|
@click="openFullView"
|
||||||
|
>
|
||||||
|
<i class="ion-arrow-right-c" />
|
||||||
|
{{ $t('UNREAD_VIEW.VIEW_MESSAGES_BUTTON') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
/* global bus */
|
||||||
|
|
||||||
|
import { IFrameHelper } from 'widget/helpers/utils';
|
||||||
|
import AgentBubble from 'widget/components/AgentMessageBubble.vue';
|
||||||
|
import configMixin from '../mixins/configMixin';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'Unread',
|
||||||
|
components: {
|
||||||
|
AgentBubble,
|
||||||
|
},
|
||||||
|
mixins: [configMixin],
|
||||||
|
props: {
|
||||||
|
unreadMessages: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
conversationSize: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
availableAgents: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
|
hasFetched: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
conversationAttributes: {
|
||||||
|
type: Object,
|
||||||
|
default: () => {},
|
||||||
|
},
|
||||||
|
unreadMessageCount: {
|
||||||
|
type: Number,
|
||||||
|
default: 0,
|
||||||
|
},
|
||||||
|
hideMessageBubble: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
showCloseButton() {
|
||||||
|
return this.unreadMessageCount && this.hideMessageBubble;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
openFullView() {
|
||||||
|
bus.$emit('on-unread-view-clicked');
|
||||||
|
},
|
||||||
|
closeFullView() {
|
||||||
|
if (IFrameHelper.isIFrame()) {
|
||||||
|
IFrameHelper.sendMessage({
|
||||||
|
event: 'toggleBubble',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
<style lang="scss" scoped>
|
||||||
|
@import '~widget/assets/scss/woot.scss';
|
||||||
|
.unread-wrap {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
background: transparent;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.unread-messages {
|
||||||
|
padding-bottom: $space-small;
|
||||||
|
}
|
||||||
|
|
||||||
|
.clear-button {
|
||||||
|
background: transparent;
|
||||||
|
color: $color-woot;
|
||||||
|
padding: 0;
|
||||||
|
border: 0;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
font-size: $font-size-medium;
|
||||||
|
transition: all 0.3s $ease-in-cubic;
|
||||||
|
margin-left: $space-smaller;
|
||||||
|
padding-right: $space-one;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
transform: translateX($space-smaller);
|
||||||
|
color: $color-primary-dark;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-unread-button {
|
||||||
|
background: $color-background;
|
||||||
|
color: $color-gray;
|
||||||
|
border: 0;
|
||||||
|
font-weight: $font-weight-bold;
|
||||||
|
font-size: $font-size-small;
|
||||||
|
transition: all 0.3s $ease-in-cubic;
|
||||||
|
margin-bottom: $space-slab;
|
||||||
|
border-radius: $space-normal;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: $color-body;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-unread-wrap {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<style lang="scss">
|
||||||
|
@import '~widget/assets/scss/woot.scss';
|
||||||
|
|
||||||
|
.unread-messages {
|
||||||
|
width: 100%;
|
||||||
|
margin-top: auto;
|
||||||
|
padding-bottom: $space-small;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
overflow-y: auto;
|
||||||
|
|
||||||
|
.chat-bubble-wrap {
|
||||||
|
margin-bottom: $space-smaller;
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
.chat-bubble {
|
||||||
|
border: 1px solid $color-border-dark;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .chat-bubble-wrap {
|
||||||
|
.chat-bubble {
|
||||||
|
border-top-left-radius: $space-smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child .chat-bubble {
|
||||||
|
border-bottom-left-radius: $space-two;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-widget-right .unread-wrap {
|
||||||
|
text-align: right;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
.chat-bubble-wrap {
|
||||||
|
.chat-bubble {
|
||||||
|
border-radius: $space-two;
|
||||||
|
border-bottom-right-radius: $space-smaller;
|
||||||
|
}
|
||||||
|
|
||||||
|
+ .chat-bubble-wrap {
|
||||||
|
.chat-bubble {
|
||||||
|
border-top-right-radius: $space-smaller;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&:last-child .chat-bubble {
|
||||||
|
border-bottom-right-radius: $space-two;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.close-unread-wrap {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
if @conversation
|
if @conversation
|
||||||
json.id @conversation.display_id
|
json.id @conversation.display_id
|
||||||
json.inbox_id @conversation.inbox_id
|
json.inbox_id @conversation.inbox_id
|
||||||
|
json.user_last_seen_at @conversation.user_last_seen_at.to_i
|
||||||
json.status @conversation.status
|
json.status @conversation.status
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -109,8 +109,9 @@ Rails.application.routes.draw do
|
|||||||
namespace :widget do
|
namespace :widget do
|
||||||
resources :events, only: [:create]
|
resources :events, only: [:create]
|
||||||
resources :messages, only: [:index, :create, :update]
|
resources :messages, only: [:index, :create, :update]
|
||||||
resources :conversations do
|
resources :conversations, only: [:index] do
|
||||||
collection do
|
collection do
|
||||||
|
post :update_last_seen
|
||||||
post :toggle_typing
|
post :toggle_typing
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -9,6 +9,24 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||||||
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
|
||||||
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
|
||||||
|
|
||||||
|
describe 'GET /api/v1/widget/conversations' do
|
||||||
|
context 'with a conversation' do
|
||||||
|
it 'returns the correct conversation params' do
|
||||||
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
|
get '/api/v1/widget/conversations',
|
||||||
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
|
expect(response).to have_http_status(:success)
|
||||||
|
json_response = JSON.parse(response.body)
|
||||||
|
|
||||||
|
expect(json_response['id']).to eq(conversation.display_id)
|
||||||
|
expect(json_response['status']).to eq(conversation.status)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/widget/conversations/toggle_typing' do
|
describe 'POST /api/v1/widget/conversations/toggle_typing' do
|
||||||
context 'with a conversation' do
|
context 'with a conversation' do
|
||||||
it 'dispatches the correct typing status' do
|
it 'dispatches the correct typing status' do
|
||||||
@@ -25,20 +43,20 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
describe 'POST /api/v1/widget/conversations' do
|
describe 'POST /api/v1/widget/conversations/update_last_seen' do
|
||||||
context 'with a conversation' do
|
context 'with a conversation' do
|
||||||
it 'returns the correct conversation params' do
|
it 'returns the correct conversation params' do
|
||||||
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
allow(Rails.configuration.dispatcher).to receive(:dispatch)
|
||||||
get '/api/v1/widget/conversations',
|
expect(conversation.user_last_seen_at).to eq(nil)
|
||||||
headers: { 'X-Auth-Token' => token },
|
|
||||||
params: { website_token: web_widget.website_token },
|
post '/api/v1/widget/conversations/update_last_seen',
|
||||||
as: :json
|
headers: { 'X-Auth-Token' => token },
|
||||||
|
params: { website_token: web_widget.website_token },
|
||||||
|
as: :json
|
||||||
|
|
||||||
expect(response).to have_http_status(:success)
|
expect(response).to have_http_status(:success)
|
||||||
json_response = JSON.parse(response.body)
|
|
||||||
|
|
||||||
expect(json_response['id']).to eq(conversation.display_id)
|
expect(conversation.reload.user_last_seen_at).not_to eq(nil)
|
||||||
expect(json_response['status']).to eq(conversation.status)
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ FactoryBot.define do
|
|||||||
factory :conversation do
|
factory :conversation do
|
||||||
status { 'open' }
|
status { 'open' }
|
||||||
display_id { rand(10_000_000) }
|
display_id { rand(10_000_000) }
|
||||||
user_last_seen_at { Time.current }
|
|
||||||
agent_last_seen_at { Time.current }
|
agent_last_seen_at { Time.current }
|
||||||
locked { false }
|
locked { false }
|
||||||
identifier { SecureRandom.hex }
|
identifier { SecureRandom.hex }
|
||||||
|
|||||||
Reference in New Issue
Block a user