Feature: Customise widget for bot conversations (#834)
* Feature: Customise widget for bot conversations
This commit is contained in:
parent
05ea6308f2
commit
f28ec29b8c
24 changed files with 298 additions and 26 deletions
|
@ -106,3 +106,6 @@ CHARGEBEE_WEBHOOK_PASSWORD=
|
||||||
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
## generate a new key value here : https://d3v.one/vapid-key-generator/
|
||||||
# VAPID_PUBLIC_KEY=
|
# VAPID_PUBLIC_KEY=
|
||||||
# VAPID_PRIVATE_KEY=
|
# VAPID_PRIVATE_KEY=
|
||||||
|
|
||||||
|
## Bot Customizations
|
||||||
|
USE_INBOX_AVATAR_FOR_BOT=true
|
||||||
|
|
|
@ -3,6 +3,10 @@ class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
|
||||||
before_action :set_web_widget
|
before_action :set_web_widget
|
||||||
before_action :set_contact
|
before_action :set_contact
|
||||||
|
|
||||||
|
def index
|
||||||
|
@conversation = conversation
|
||||||
|
end
|
||||||
|
|
||||||
def toggle_typing
|
def toggle_typing
|
||||||
head :ok && return if conversation.nil?
|
head :ok && return if conversation.nil?
|
||||||
|
|
||||||
|
|
|
@ -24,6 +24,7 @@
|
||||||
class="button block"
|
class="button block"
|
||||||
type="submit"
|
type="submit"
|
||||||
:disabled="!isFormValid"
|
:disabled="!isFormValid"
|
||||||
|
:style="{ background: widgetColor, borderColor: widgetColor }"
|
||||||
>
|
>
|
||||||
{{ $t('COMPONENTS.FORM_BUBBLE.SUBMIT') }}
|
{{ $t('COMPONENTS.FORM_BUBBLE.SUBMIT') }}
|
||||||
</button>
|
</button>
|
||||||
|
@ -32,6 +33,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
export default {
|
export default {
|
||||||
props: {
|
props: {
|
||||||
items: {
|
items: {
|
||||||
|
@ -49,6 +51,9 @@ export default {
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
widgetColor: 'appConfig/getWidgetColor',
|
||||||
|
}),
|
||||||
isFormValid() {
|
isFormValid() {
|
||||||
return this.items.reduce((acc, { name }) => {
|
return this.items.reduce((acc, { name }) => {
|
||||||
return !!this.formValues[name] && acc;
|
return !!this.formValues[name] && acc;
|
||||||
|
|
|
@ -60,6 +60,8 @@ export default {
|
||||||
this.$store.dispatch('contacts/update', message);
|
this.$store.dispatch('contacts/update', message);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.$store.dispatch('conversationAttributes/get');
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions('appConfig', ['setWidgetColor']),
|
...mapActions('appConfig', ['setWidgetColor']),
|
||||||
|
|
|
@ -13,12 +13,16 @@ const sendAttachmentAPI = async attachment => {
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getConversationAPI = async ({ before }) => {
|
const getMessagesAPI = async ({ before }) => {
|
||||||
const urlData = endPoints.getConversation({ before });
|
const urlData = endPoints.getConversation({ before });
|
||||||
const result = await API.get(urlData.url, { params: urlData.params });
|
const result = await API.get(urlData.url, { params: urlData.params });
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getConversationAPI = async () => {
|
||||||
|
return API.get(`/api/v1/widget/conversations${window.location.search}`);
|
||||||
|
};
|
||||||
|
|
||||||
const toggleTyping = async ({ typingStatus }) => {
|
const toggleTyping = async ({ typingStatus }) => {
|
||||||
return API.post(
|
return API.post(
|
||||||
`/api/v1/widget/conversations/toggle_typing${window.location.search}`,
|
`/api/v1/widget/conversations/toggle_typing${window.location.search}`,
|
||||||
|
@ -26,4 +30,10 @@ const toggleTyping = async ({ typingStatus }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export { sendMessageAPI, getConversationAPI, sendAttachmentAPI, toggleTyping };
|
export {
|
||||||
|
sendMessageAPI,
|
||||||
|
getConversationAPI,
|
||||||
|
getMessagesAPI,
|
||||||
|
sendAttachmentAPI,
|
||||||
|
toggleTyping,
|
||||||
|
};
|
||||||
|
|
|
@ -53,6 +53,7 @@ import ImageBubble from 'widget/components/ImageBubble';
|
||||||
import FileBubble from 'widget/components/FileBubble';
|
import FileBubble from 'widget/components/FileBubble';
|
||||||
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
import Thumbnail from 'dashboard/components/widgets/Thumbnail';
|
||||||
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
import { MESSAGE_TYPE } from 'widget/helpers/constants';
|
||||||
|
import configMixin from '../mixins/configMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AgentMessage',
|
name: 'AgentMessage',
|
||||||
|
@ -63,7 +64,7 @@ export default {
|
||||||
UserMessage,
|
UserMessage,
|
||||||
FileBubble,
|
FileBubble,
|
||||||
},
|
},
|
||||||
mixins: [timeMixin],
|
mixins: [timeMixin, configMixin],
|
||||||
props: {
|
props: {
|
||||||
message: {
|
message: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -112,11 +113,17 @@ export default {
|
||||||
avatarUrl() {
|
avatarUrl() {
|
||||||
// eslint-disable-next-line
|
// eslint-disable-next-line
|
||||||
const BotImage = require('dashboard/assets/images/chatwoot_bot.png');
|
const BotImage = require('dashboard/assets/images/chatwoot_bot.png');
|
||||||
|
const displayImage = this.useInboxAvatarForBot
|
||||||
|
? this.inboxAvatarUrl
|
||||||
|
: BotImage;
|
||||||
|
|
||||||
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
|
if (this.message.message_type === MESSAGE_TYPE.TEMPLATE) {
|
||||||
return BotImage;
|
return displayImage;
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.message.sender ? this.message.sender.avatar_url : BotImage;
|
return this.message.sender
|
||||||
|
? this.message.sender.avatar_url
|
||||||
|
: displayImage;
|
||||||
},
|
},
|
||||||
hasRecordedResponse() {
|
hasRecordedResponse() {
|
||||||
return (
|
return (
|
||||||
|
|
|
@ -8,9 +8,15 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||||
'message.updated': this.onMessageUpdated,
|
'message.updated': this.onMessageUpdated,
|
||||||
'conversation.typing_on': this.onTypingOn,
|
'conversation.typing_on': this.onTypingOn,
|
||||||
'conversation.typing_off': this.onTypingOff,
|
'conversation.typing_off': this.onTypingOff,
|
||||||
|
'conversation.resolved': this.onStatusChange,
|
||||||
|
'conversation.opened': this.onStatusChange,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onStatusChange = data => {
|
||||||
|
this.app.$store.dispatch('conversationAttributes/update', data);
|
||||||
|
};
|
||||||
|
|
||||||
onMessageCreated = data => {
|
onMessageCreated = data => {
|
||||||
this.app.$store.dispatch('conversation/addMessage', data);
|
this.app.$store.dispatch('conversation/addMessage', data);
|
||||||
};
|
};
|
||||||
|
|
19
app/javascript/widget/mixins/configMixin.js
Normal file
19
app/javascript/widget/mixins/configMixin.js
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
hideInputForBotConversations() {
|
||||||
|
return window.chatwootWebChannel.hideInputForBotConversations;
|
||||||
|
},
|
||||||
|
useInboxAvatarForBot() {
|
||||||
|
return window.chatwootWidgetDefaults.useInboxAvatarForBot;
|
||||||
|
},
|
||||||
|
hasAConnectedAgentBot() {
|
||||||
|
return !!window.chatwootWebChannel.hasAConnectedAgentBot;
|
||||||
|
},
|
||||||
|
inboxAvatarUrl() {
|
||||||
|
return window.chatwootWebChannel.avatarUrl;
|
||||||
|
},
|
||||||
|
channelConfig() {
|
||||||
|
return window.chatwootWebChannel;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
35
app/javascript/widget/mixins/specs/configMixin.spec.js
Normal file
35
app/javascript/widget/mixins/specs/configMixin.spec.js
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import { createWrapper } from '@vue/test-utils';
|
||||||
|
import configMixin from '../configMixin';
|
||||||
|
import Vue from 'vue';
|
||||||
|
|
||||||
|
global.chatwootWebChannel = {
|
||||||
|
hideInputForBotConversations: true,
|
||||||
|
avatarUrl: 'https://test.url',
|
||||||
|
hasAConnectedAgentBot: 'AgentBot',
|
||||||
|
};
|
||||||
|
|
||||||
|
global.chatwootWidgetDefaults = {
|
||||||
|
useInboxAvatarForBot: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('configMixin', () => {
|
||||||
|
test('returns config', () => {
|
||||||
|
const Component = {
|
||||||
|
render() {},
|
||||||
|
title: 'TestComponent',
|
||||||
|
mixins: [configMixin],
|
||||||
|
};
|
||||||
|
const Constructor = Vue.extend(Component);
|
||||||
|
const vm = new Constructor().$mount();
|
||||||
|
const wrapper = createWrapper(vm);
|
||||||
|
expect(wrapper.vm.hideInputForBotConversations).toBe(true);
|
||||||
|
expect(wrapper.vm.hasAConnectedAgentBot).toBe(true);
|
||||||
|
expect(wrapper.vm.useInboxAvatarForBot).toBe(true);
|
||||||
|
expect(wrapper.vm.inboxAvatarUrl).toBe('https://test.url');
|
||||||
|
expect(wrapper.vm.channelConfig).toEqual({
|
||||||
|
hideInputForBotConversations: true,
|
||||||
|
avatarUrl: 'https://test.url',
|
||||||
|
hasAConnectedAgentBot: 'AgentBot',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -4,6 +4,7 @@ import agent from 'widget/store/modules/agent';
|
||||||
import appConfig from 'widget/store/modules/appConfig';
|
import appConfig from 'widget/store/modules/appConfig';
|
||||||
import contacts from 'widget/store/modules/contacts';
|
import contacts from 'widget/store/modules/contacts';
|
||||||
import conversation from 'widget/store/modules/conversation';
|
import conversation from 'widget/store/modules/conversation';
|
||||||
|
import conversationAttributes from 'widget/store/modules/conversationAttributes';
|
||||||
import conversationLabels from 'widget/store/modules/conversationLabels';
|
import conversationLabels from 'widget/store/modules/conversationLabels';
|
||||||
import events from 'widget/store/modules/events';
|
import events from 'widget/store/modules/events';
|
||||||
import message from 'widget/store/modules/message';
|
import message from 'widget/store/modules/message';
|
||||||
|
@ -16,6 +17,7 @@ export default new Vuex.Store({
|
||||||
appConfig,
|
appConfig,
|
||||||
contacts,
|
contacts,
|
||||||
conversation,
|
conversation,
|
||||||
|
conversationAttributes,
|
||||||
conversationLabels,
|
conversationLabels,
|
||||||
events,
|
events,
|
||||||
message,
|
message,
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import {
|
import {
|
||||||
sendMessageAPI,
|
sendMessageAPI,
|
||||||
getConversationAPI,
|
getMessagesAPI,
|
||||||
sendAttachmentAPI,
|
sendAttachmentAPI,
|
||||||
toggleTyping,
|
toggleTyping,
|
||||||
} from 'widget/api/conversation';
|
} from 'widget/api/conversation';
|
||||||
|
@ -116,7 +116,7 @@ export const actions = {
|
||||||
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
fetchOldConversations: async ({ commit }, { before } = {}) => {
|
||||||
try {
|
try {
|
||||||
commit('setConversationListLoading', true);
|
commit('setConversationListLoading', true);
|
||||||
const { data } = await getConversationAPI({ before });
|
const { data } = await getMessagesAPI({ before });
|
||||||
commit('setMessagesInConversation', data);
|
commit('setMessagesInConversation', data);
|
||||||
commit('setConversationListLoading', false);
|
commit('setConversationListLoading', false);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -0,0 +1,49 @@
|
||||||
|
import {
|
||||||
|
SET_CONVERSATION_ATTRIBUTES,
|
||||||
|
UPDATE_CONVERSATION_ATTRIBUTES,
|
||||||
|
} from '../types';
|
||||||
|
import { getConversationAPI } from '../../api/conversation';
|
||||||
|
|
||||||
|
const state = {
|
||||||
|
id: '',
|
||||||
|
status: '',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getters = {
|
||||||
|
getConversationParams: $state => $state,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const actions = {
|
||||||
|
get: async ({ commit }) => {
|
||||||
|
try {
|
||||||
|
const { data } = await getConversationAPI();
|
||||||
|
commit(SET_CONVERSATION_ATTRIBUTES, data);
|
||||||
|
} catch (error) {
|
||||||
|
// Ignore error
|
||||||
|
}
|
||||||
|
},
|
||||||
|
update({ commit }, data) {
|
||||||
|
commit(UPDATE_CONVERSATION_ATTRIBUTES, data);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mutations = {
|
||||||
|
[SET_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||||
|
$state.id = data.id;
|
||||||
|
$state.status = data.status;
|
||||||
|
},
|
||||||
|
[UPDATE_CONVERSATION_ATTRIBUTES]($state, data) {
|
||||||
|
if (data.id === $state.id) {
|
||||||
|
$state.id = data.id;
|
||||||
|
$state.status = data.status;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default {
|
||||||
|
namespaced: true,
|
||||||
|
state,
|
||||||
|
getters,
|
||||||
|
actions,
|
||||||
|
mutations,
|
||||||
|
};
|
|
@ -0,0 +1,32 @@
|
||||||
|
import { actions } from '../../conversationAttributes';
|
||||||
|
import { API } from 'widget/helpers/axios';
|
||||||
|
|
||||||
|
const commit = jest.fn();
|
||||||
|
jest.mock('widget/helpers/axios');
|
||||||
|
|
||||||
|
describe('#actions', () => {
|
||||||
|
describe('#update', () => {
|
||||||
|
it('sends mutation if api is success', async () => {
|
||||||
|
API.get.mockResolvedValue({ data: { id: 1, status: 'bot' } });
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([
|
||||||
|
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
it('doesnot send mutation if api is error', async () => {
|
||||||
|
API.get.mockRejectedValue({ message: 'Invalid Headers' });
|
||||||
|
await actions.get({ commit });
|
||||||
|
expect(commit.mock.calls).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#update', () => {
|
||||||
|
it('sends correct mutations', () => {
|
||||||
|
actions.update({ commit }, { id: 1, status: 'bot' });
|
||||||
|
expect(commit).toBeCalledWith('UPDATE_CONVERSATION_ATTRIBUTES', {
|
||||||
|
id: 1,
|
||||||
|
status: 'bot',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,14 @@
|
||||||
|
import { getters } from '../../conversationAttributes';
|
||||||
|
|
||||||
|
describe('#getters', () => {
|
||||||
|
it('getConversationParams', () => {
|
||||||
|
const state = {
|
||||||
|
id: 1,
|
||||||
|
status: 'bot',
|
||||||
|
};
|
||||||
|
expect(getters.getConversationParams(state)).toEqual({
|
||||||
|
id: 1,
|
||||||
|
status: 'bot',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,33 @@
|
||||||
|
import { mutations } from '../../conversationAttributes';
|
||||||
|
|
||||||
|
describe('#mutations', () => {
|
||||||
|
describe('#SET_CONVERSATION_ATTRIBUTES', () => {
|
||||||
|
it('set status of the conversation', () => {
|
||||||
|
const state = { id: '', status: '' };
|
||||||
|
mutations.SET_CONVERSATION_ATTRIBUTES(state, {
|
||||||
|
id: 1,
|
||||||
|
status: 'open',
|
||||||
|
});
|
||||||
|
expect(state).toEqual({ id: 1, status: 'open' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('#UPDATE_CONVERSATION_ATTRIBUTES', () => {
|
||||||
|
it('update status if it is same conversation', () => {
|
||||||
|
const state = { id: 1, status: 'bot' };
|
||||||
|
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||||
|
id: 1,
|
||||||
|
status: 'open',
|
||||||
|
});
|
||||||
|
expect(state).toEqual({ id: 1, status: 'open' });
|
||||||
|
});
|
||||||
|
it('doesnot update status if it is not the same conversation', () => {
|
||||||
|
const state = { id: 1, status: 'bot' };
|
||||||
|
mutations.UPDATE_CONVERSATION_ATTRIBUTES(state, {
|
||||||
|
id: 2,
|
||||||
|
status: 'open',
|
||||||
|
});
|
||||||
|
expect(state).toEqual({ id: 1, status: 'bot' });
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -1 +1,3 @@
|
||||||
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
|
export const SET_WIDGET_COLOR = 'SET_WIDGET_COLOR';
|
||||||
|
export const SET_CONVERSATION_ATTRIBUTES = 'SET_CONVERSATION_ATTRIBUTES';
|
||||||
|
export const UPDATE_CONVERSATION_ATTRIBUTES = 'UPDATE_CONVERSATION_ATTRIBUTES';
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<div class="home">
|
<div class="home">
|
||||||
<div class="header-wrap">
|
<div class="header-wrap">
|
||||||
<ChatHeaderExpanded
|
<ChatHeaderExpanded
|
||||||
v-if="isHeaderExpanded"
|
v-if="isHeaderExpanded && !hideWelcomeHeader"
|
||||||
:intro-heading="introHeading"
|
:intro-heading="introHeading"
|
||||||
:intro-body="introBody"
|
:intro-body="introBody"
|
||||||
:avatar-url="channelConfig.avatarUrl"
|
:avatar-url="channelConfig.avatarUrl"
|
||||||
|
@ -16,7 +16,7 @@
|
||||||
<AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" />
|
<AvailableAgents v-if="showAvailableAgents" :agents="availableAgents" />
|
||||||
<ConversationWrap :grouped-messages="groupedMessages" />
|
<ConversationWrap :grouped-messages="groupedMessages" />
|
||||||
<div class="footer-wrap">
|
<div class="footer-wrap">
|
||||||
<div class="input-wrap">
|
<div v-if="showInputTextArea" class="input-wrap">
|
||||||
<ChatFooter />
|
<ChatFooter />
|
||||||
</div>
|
</div>
|
||||||
<branding></branding>
|
<branding></branding>
|
||||||
|
@ -33,6 +33,7 @@ import ChatHeaderExpanded from 'widget/components/ChatHeaderExpanded.vue';
|
||||||
import ChatHeader from 'widget/components/ChatHeader.vue';
|
import ChatHeader from 'widget/components/ChatHeader.vue';
|
||||||
import ConversationWrap from 'widget/components/ConversationWrap.vue';
|
import ConversationWrap from 'widget/components/ConversationWrap.vue';
|
||||||
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
import AvailableAgents from 'widget/components/AvailableAgents.vue';
|
||||||
|
import configMixin from '../mixins/configMixin';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'Home',
|
name: 'Home',
|
||||||
|
@ -44,30 +45,41 @@ export default {
|
||||||
Branding,
|
Branding,
|
||||||
AvailableAgents,
|
AvailableAgents,
|
||||||
},
|
},
|
||||||
|
mixins: [configMixin],
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
groupedMessages: 'conversation/getGroupedConversation',
|
groupedMessages: 'conversation/getGroupedConversation',
|
||||||
conversationSize: 'conversation/getConversationSize',
|
conversationSize: 'conversation/getConversationSize',
|
||||||
availableAgents: 'agent/availableAgents',
|
availableAgents: 'agent/availableAgents',
|
||||||
hasFetched: 'agent/uiFlags/hasFetched',
|
hasFetched: 'agent/uiFlags/hasFetched',
|
||||||
|
conversationAttributes: 'conversationAttributes/getConversationParams',
|
||||||
}),
|
}),
|
||||||
|
isOpen() {
|
||||||
|
return this.conversationAttributes.status === 'open';
|
||||||
|
},
|
||||||
|
showInputTextArea() {
|
||||||
|
if (this.hideInputForBotConversations) {
|
||||||
|
if (this.isOpen) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
isHeaderExpanded() {
|
isHeaderExpanded() {
|
||||||
return this.conversationSize === 0;
|
return this.conversationSize === 0;
|
||||||
},
|
},
|
||||||
channelConfig() {
|
|
||||||
return window.chatwootWebChannel;
|
|
||||||
},
|
|
||||||
showAvailableAgents() {
|
showAvailableAgents() {
|
||||||
return this.availableAgents.length > 0 && this.conversationSize < 1;
|
return this.availableAgents.length > 0 && this.conversationSize < 1;
|
||||||
},
|
},
|
||||||
introHeading() {
|
introHeading() {
|
||||||
return this.channelConfig.welcomeTitle || 'Hi there ! 🙌🏼';
|
return this.channelConfig.welcomeTitle;
|
||||||
},
|
},
|
||||||
introBody() {
|
introBody() {
|
||||||
return (
|
return this.channelConfig.welcomeTagline;
|
||||||
this.channelConfig.welcomeTagline ||
|
},
|
||||||
'We make it simple to connect with us. Ask us anything, or share your feedback.'
|
hideWelcomeHeader() {
|
||||||
);
|
return !(this.introHeading || this.introBody);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -32,14 +32,16 @@ class ActionCableListener < BaseListener
|
||||||
|
|
||||||
def conversation_resolved(event)
|
def conversation_resolved(event)
|
||||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||||
|
tokens = user_tokens(account, conversation.inbox.members) + [conversation.contact&.pubsub_token]
|
||||||
|
|
||||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_RESOLVED, conversation.push_event_data)
|
broadcast(tokens, CONVERSATION_RESOLVED, conversation.push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_opened(event)
|
def conversation_opened(event)
|
||||||
conversation, account, timestamp = extract_conversation_and_account(event)
|
conversation, account, timestamp = extract_conversation_and_account(event)
|
||||||
|
tokens = user_tokens(account, conversation.inbox.members) + [conversation.contact&.pubsub_token]
|
||||||
|
|
||||||
broadcast(user_tokens(account, conversation.inbox.members), CONVERSATION_OPENED, conversation.push_event_data)
|
broadcast(tokens, CONVERSATION_OPENED, conversation.push_event_data)
|
||||||
end
|
end
|
||||||
|
|
||||||
def conversation_lock_toggle(event)
|
def conversation_lock_toggle(event)
|
||||||
|
|
|
@ -2,12 +2,13 @@
|
||||||
#
|
#
|
||||||
# Table name: agent_bots
|
# Table name: agent_bots
|
||||||
#
|
#
|
||||||
# id :bigint not null, primary key
|
# id :bigint not null, primary key
|
||||||
# description :string
|
# description :string
|
||||||
# name :string
|
# hide_input_for_bot_conversations :boolean default(FALSE)
|
||||||
# outgoing_url :string
|
# name :string
|
||||||
# created_at :datetime not null
|
# outgoing_url :string
|
||||||
# updated_at :datetime not null
|
# created_at :datetime not null
|
||||||
|
# updated_at :datetime not null
|
||||||
#
|
#
|
||||||
|
|
||||||
class AgentBot < ApplicationRecord
|
class AgentBot < ApplicationRecord
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
if @conversation
|
||||||
|
json.id @conversation.display_id
|
||||||
|
json.inbox_id @conversation.inbox_id
|
||||||
|
json.status @conversation.status
|
||||||
|
end
|
|
@ -7,6 +7,8 @@
|
||||||
<script>
|
<script>
|
||||||
window.chatwootWebChannel = {
|
window.chatwootWebChannel = {
|
||||||
avatarUrl: '<%= @web_widget.inbox.avatar_url %>',
|
avatarUrl: '<%= @web_widget.inbox.avatar_url %>',
|
||||||
|
hasAConnectedAgentBot: '<%= @web_widget.inbox.agent_bot&.name %>',
|
||||||
|
hideInputForBotConversations: <%= ActiveModel::Type::Boolean.new.cast(@web_widget.inbox.agent_bot&.hide_input_for_bot_conversations) %>,
|
||||||
locale: '<%= @web_widget.account.locale %>',
|
locale: '<%= @web_widget.account.locale %>',
|
||||||
websiteName: '<%= @web_widget.inbox.name %>',
|
websiteName: '<%= @web_widget.inbox.name %>',
|
||||||
websiteToken: '<%= @web_widget.website_token %>',
|
websiteToken: '<%= @web_widget.website_token %>',
|
||||||
|
@ -14,6 +16,9 @@
|
||||||
welcomeTitle: '<%= @web_widget.welcome_title %>',
|
welcomeTitle: '<%= @web_widget.welcome_title %>',
|
||||||
widgetColor: '<%= @web_widget.widget_color %>',
|
widgetColor: '<%= @web_widget.widget_color %>',
|
||||||
}
|
}
|
||||||
|
window.chatwootWidgetDefaults = {
|
||||||
|
useInboxAvatarForBot: <%= ActiveModel::Type::Boolean.new.cast(ENV.fetch('USE_INBOX_AVATAR_FOR_BOT', false)) %>,
|
||||||
|
}
|
||||||
window.chatwootPubsubToken = '<%= @contact.pubsub_token %>'
|
window.chatwootPubsubToken = '<%= @contact.pubsub_token %>'
|
||||||
window.authToken = '<%= @token %>'
|
window.authToken = '<%= @token %>'
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,5 @@
|
||||||
|
class AddHideInputFlagToBotConfig < ActiveRecord::Migration[6.0]
|
||||||
|
def change
|
||||||
|
add_column :agent_bots, :hide_input_for_bot_conversations, :boolean, default: false
|
||||||
|
end
|
||||||
|
end
|
|
@ -10,7 +10,7 @@
|
||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema.define(version: 2020_05_04_144712) do
|
ActiveRecord::Schema.define(version: 2020_05_09_044639) do
|
||||||
|
|
||||||
# These are extensions that must be enabled in order to support this database
|
# These are extensions that must be enabled in order to support this database
|
||||||
enable_extension "pgcrypto"
|
enable_extension "pgcrypto"
|
||||||
|
@ -94,6 +94,7 @@ ActiveRecord::Schema.define(version: 2020_05_04_144712) do
|
||||||
t.string "outgoing_url"
|
t.string "outgoing_url"
|
||||||
t.datetime "created_at", precision: 6, null: false
|
t.datetime "created_at", precision: 6, null: false
|
||||||
t.datetime "updated_at", precision: 6, null: false
|
t.datetime "updated_at", precision: 6, null: false
|
||||||
|
t.boolean "hide_input_for_bot_conversations", default: false
|
||||||
end
|
end
|
||||||
|
|
||||||
create_table "attachments", id: :serial, force: :cascade do |t|
|
create_table "attachments", id: :serial, force: :cascade do |t|
|
||||||
|
|
|
@ -24,4 +24,22 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
describe 'POST /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
|
||||||
end
|
end
|
||||||
|
|
Loading…
Reference in a new issue