diff --git a/app/controllers/api/v1/accounts/conversations_controller.rb b/app/controllers/api/v1/accounts/conversations_controller.rb
index 5b39014af..05eaa5861 100644
--- a/app/controllers/api/v1/accounts/conversations_controller.rb
+++ b/app/controllers/api/v1/accounts/conversations_controller.rb
@@ -25,11 +25,10 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
end
def toggle_typing_status
- user = current_user.presence || @resource
if params[:typing_status] == 'on'
- Rails.configuration.dispatcher.dispatch(CONVERSATION_TYPING_ON, Time.zone.now, conversation: @conversation, user: user)
+ trigger_typing_event(CONVERSATION_TYPING_ON)
elsif params[:typing_status] == 'off'
- Rails.configuration.dispatcher.dispatch(CONVERSATION_TYPING_OFF, Time.zone.now, conversation: @conversation)
+ trigger_typing_event(CONVERSATION_TYPING_OFF)
end
head :ok
end
@@ -42,6 +41,11 @@ class Api::V1::Accounts::ConversationsController < Api::BaseController
private
+ def trigger_typing_event(event)
+ user = current_user.presence || @resource
+ Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: @conversation, user: user)
+ end
+
def parsed_last_seen_at
DateTime.strptime(params[:agent_last_seen_at].to_s, '%s')
end
diff --git a/app/controllers/api/v1/widget/conversations_controller.rb b/app/controllers/api/v1/widget/conversations_controller.rb
new file mode 100644
index 000000000..20a14b7f8
--- /dev/null
+++ b/app/controllers/api/v1/widget/conversations_controller.rb
@@ -0,0 +1,27 @@
+class Api::V1::Widget::ConversationsController < Api::V1::Widget::BaseController
+ include Events::Types
+ before_action :set_web_widget
+ before_action :set_contact
+
+ def toggle_typing
+ head :ok if conversation.nil?
+
+ if permitted_params[:typing_status] == 'on'
+ trigger_typing_event(CONVERSATION_TYPING_ON)
+ elsif permitted_params[:typing_status] == 'off'
+ trigger_typing_event(CONVERSATION_TYPING_OFF)
+ end
+
+ head :ok
+ end
+
+ private
+
+ def trigger_typing_event(event)
+ Rails.configuration.dispatcher.dispatch(event, Time.zone.now, conversation: conversation, user: @contact)
+ end
+
+ def permitted_params
+ params.permit(:id, :typing_status, :website_token)
+ end
+end
diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js
index d5212957a..18ca9a60b 100644
--- a/app/javascript/dashboard/api/inbox/conversation.js
+++ b/app/javascript/dashboard/api/inbox/conversation.js
@@ -33,6 +33,12 @@ class ConversationApi extends ApiClient {
agent_last_seen_at: lastSeen,
});
}
+
+ toggleTyping({ conversationId, status }) {
+ return axios.post(`${this.url}/${conversationId}/toggle_typing_status`, {
+ typing_status: status,
+ });
+ }
}
export default new ConversationApi();
diff --git a/app/javascript/dashboard/assets/images/typing.gif b/app/javascript/dashboard/assets/images/typing.gif
new file mode 100644
index 000000000..dd9b1ca2b
Binary files /dev/null and b/app/javascript/dashboard/assets/images/typing.gif differ
diff --git a/app/javascript/dashboard/assets/scss/_foundation-settings.scss b/app/javascript/dashboard/assets/scss/_foundation-settings.scss
index 58ecfec33..169198dae 100644
--- a/app/javascript/dashboard/assets/scss/_foundation-settings.scss
+++ b/app/javascript/dashboard/assets/scss/_foundation-settings.scss
@@ -377,8 +377,8 @@ $form-button-radius: $global-radius;
// 20. Label
// ---------
-$label-background: $primary-color;
-$label-color: $white;
+$label-background: lighten($primary-color, 40%);
+$label-color: $primary-color;
$label-color-alt: $black;
$label-palette: $foundation-palette;
$label-font-size: $font-size-micro;
diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
index 62eeb268f..498a5e9d2 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-card.scss
@@ -1,25 +1,53 @@
.conversation {
@include flex;
@include flex-shrink;
- @include padding($space-normal $zero $zero $space-normal);
- border-left: 4px solid transparent;
+ @include padding(0 0 0 $space-normal);
+ align-items: center;
+ border-bottom: 1px solid transparent;
+ border-left: $space-micro solid transparent;
+ border-top: 1px solid transparent;
cursor: pointer;
position: relative;
&.active {
background: $color-background;
+ border-bottom-color: $color-border-light;
border-left-color: $color-woot;
+ border-top-color: $color-border-light;
+
+ .conversation--details {
+ border-top-color: transparent;
+ }
+
+ +.conversation .conversation--details {
+ border-top-color: transparent;
+ }
}
+ &:first-child {
+ .conversation--details {
+ border-top-color: transparent;
+ }
+ }
+
+ &:nth-last-child(2) {
+ .conversation--details {
+ border-bottom-color: $color-border-light;
+ }
+ }
+
+
.conversation--details {
- @include margin($zero $zero $zero $space-one);
+ @include margin(0 0 0 $space-one);
@include border-light-bottom;
- @include padding($zero $zero $space-slab $zero);
+ @include border-light-top;
+ @include padding($space-slab 0);
+ border-bottom-color: transparent;
}
.conversation--user {
font-size: $font-size-small;
- margin-bottom: $zero;
+ margin-bottom: 0;
text-transform: capitalize;
.label {
@@ -39,7 +67,7 @@
font-weight: $font-weight-normal;
height: $space-medium;
line-height: $space-medium;
- margin: $zero;
+ margin: 0;
max-width: 96%;
overflow: hidden;
text-overflow: ellipsis;
@@ -54,20 +82,20 @@
.conversation--meta {
@include flex;
- display: block;
flex-direction: column;
position: absolute;
right: $space-normal;
top: $space-normal;
.unread {
- $unread-size: $space-two - $space-micro;
+ $unread-size: $space-normal;
@include round-corner;
+ @include light-shadow;
background: darken($success-color, 3%);
color: $color-white;
display: none;
font-size: $font-size-micro;
- font-weight: $font-weight-medium;
+ font-weight: $font-weight-black;
height: $unread-size;
line-height: $unread-size;
margin-left: auto;
diff --git a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
index 8cc89b741..6aef3e3f9 100644
--- a/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
+++ b/app/javascript/dashboard/assets/scss/widgets/_conversation-view.scss
@@ -1,5 +1,5 @@
@mixin bubble-with-types {
- @include padding($space-one $space-normal);
+ @include padding($space-small $space-normal);
@include margin($zero);
background: $color-woot;
border-radius: $space-one;
@@ -204,12 +204,14 @@
height: 100%;
margin-bottom: $space-small;
overflow-y: auto;
+ position: relative;
}
.conversation-panel>li {
@include flex;
@include flex-shrink;
@include margin($zero $zero $space-micro);
+ position: relative;
&:first-child {
margin-top: auto;
@@ -393,3 +395,34 @@
}
}
}
+
+.conversation-footer {
+ display: flex;
+ flex-direction: column;
+ position: relative;
+}
+
+.typing-indicator-wrap {
+ align-items: center;
+ display: flex;
+ height: 0;
+ position: absolute;
+ top: -$space-large;
+ width: 100%;
+
+ .typing-indicator {
+ @include elegant-card;
+ @include round-corner;
+ background: $color-white;
+ color: $color-light-gray;
+ font-size: $font-size-mini;
+ font-weight: $font-weight-bold;
+ margin: $space-one auto;
+ padding: $space-small $space-normal $space-small $space-two;
+
+ .gif {
+ margin-left: $space-small;
+ width: $space-medium;
+ }
+ }
+}
diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
index ac43005c6..69d711955 100644
--- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue
@@ -27,10 +27,22 @@
:data="message"
/>
-
+
@@ -42,6 +54,7 @@ import ConversationHeader from './ConversationHeader';
import ReplyBox from './ReplyBox';
import Message from './Message';
import conversationMixin from '../../../mixins/conversations';
+import { getTypingUsersText } from '../../../helper/commons';
export default {
components: {
@@ -81,6 +94,27 @@ export default {
loadingChatList: 'getChatListLoadingStatus',
}),
+ typingUsersList() {
+ const userList = this.$store.getters[
+ 'conversationTypingStatus/getUserList'
+ ](this.currentChat.id);
+ return userList;
+ },
+ isAnyoneTyping() {
+ const userList = this.typingUsersList;
+ return userList.length !== 0;
+ },
+ typingUserNames() {
+ const userList = this.typingUsersList;
+
+ if (this.isAnyoneTyping) {
+ const userListAsName = getTypingUsersText(userList);
+ return userListAsName;
+ }
+
+ return '';
+ },
+
getMessages() {
const [chat] = this.allConversations.filter(
c => c.id === this.currentChat.id
diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
index 7d21d2394..4f73291b2 100644
--- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
+++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue
@@ -20,8 +20,8 @@
class="input"
type="text"
:placeholder="$t(messagePlaceHolder())"
- @click="onClick()"
- @blur="onBlur()"
+ @focus="onFocus"
+ @blur="onBlur"
/>
{
this.app.$store.dispatch('updateConversation', data);
};
+
+ onTypingOn = ({ conversation, user }) => {
+ const conversationId = conversation.id;
+
+ this.clearTimer(conversationId);
+ this.app.$store.dispatch('conversationTypingStatus/create', {
+ conversationId,
+ user,
+ });
+ this.initTimer({ conversation, user });
+ };
+
+ onTypingOff = ({ conversation, user }) => {
+ const conversationId = conversation.id;
+
+ this.clearTimer(conversationId);
+ this.app.$store.dispatch('conversationTypingStatus/destroy', {
+ conversationId,
+ user,
+ });
+ };
+
+ clearTimer = conversationId => {
+ const timerEvent = this.CancelTyping[conversationId];
+
+ if (timerEvent) {
+ clearTimeout(timerEvent);
+ this.CancelTyping[conversationId] = null;
+ }
+ };
+
+ initTimer = ({ conversation, user }) => {
+ const conversationId = conversation.id;
+ // Turn off typing automatically after 30 seconds
+ this.CancelTyping[conversationId] = setTimeout(() => {
+ this.onTypingOff({ conversation, user });
+ }, 30000);
+ };
}
export default {
diff --git a/app/javascript/dashboard/helper/commons.js b/app/javascript/dashboard/helper/commons.js
index a3af2dbaf..c32d2717a 100644
--- a/app/javascript/dashboard/helper/commons.js
+++ b/app/javascript/dashboard/helper/commons.js
@@ -9,3 +9,20 @@ export default () => {
});
}
};
+
+export const getTypingUsersText = (users = []) => {
+ const count = users.length;
+ if (count === 1) {
+ const [user] = users;
+ return `${user.name} is typing`;
+ }
+
+ if (count === 2) {
+ const [first, second] = users;
+ return `${first.name} and ${second.name} are typing`;
+ }
+
+ const [user] = users;
+ const rest = users.length - 1;
+ return `${user.name} and ${rest} others are typing`;
+};
diff --git a/app/javascript/dashboard/helper/spec/URLHelper.spec.js b/app/javascript/dashboard/helper/specs/URLHelper.spec.js
similarity index 100%
rename from app/javascript/dashboard/helper/spec/URLHelper.spec.js
rename to app/javascript/dashboard/helper/specs/URLHelper.spec.js
diff --git a/app/javascript/dashboard/helper/specs/commons.spec.js b/app/javascript/dashboard/helper/specs/commons.spec.js
new file mode 100644
index 000000000..180846496
--- /dev/null
+++ b/app/javascript/dashboard/helper/specs/commons.spec.js
@@ -0,0 +1,26 @@
+import { getTypingUsersText } from '../commons';
+
+describe('#getTypingUsersText', () => {
+ it('returns the correct text is there is only one typing user', () => {
+ expect(getTypingUsersText([{ name: 'Pranav' }])).toEqual(
+ 'Pranav is typing'
+ );
+ });
+
+ it('returns the correct text is there are two typing users', () => {
+ expect(
+ getTypingUsersText([{ name: 'Pranav' }, { name: 'Nithin' }])
+ ).toEqual('Pranav and Nithin are typing');
+ });
+
+ it('returns the correct text is there are more than two users are typing', () => {
+ expect(
+ getTypingUsersText([
+ { name: 'Pranav' },
+ { name: 'Nithin' },
+ { name: 'Subin' },
+ { name: 'Sojan' },
+ ])
+ ).toEqual('Pranav and 3 others are typing');
+ });
+});
diff --git a/app/javascript/dashboard/store/index.js b/app/javascript/dashboard/store/index.js
index b565e38d6..68f4a7372 100755
--- a/app/javascript/dashboard/store/index.js
+++ b/app/javascript/dashboard/store/index.js
@@ -10,6 +10,7 @@ import contactConversations from './modules/contactConversations';
import contacts from './modules/contacts';
import conversationLabels from './modules/conversationLabels';
import conversationMetadata from './modules/conversationMetadata';
+import conversationTypingStatus from './modules/conversationTypingStatus';
import conversationPage from './modules/conversationPage';
import conversations from './modules/conversations';
import inboxes from './modules/inboxes';
@@ -22,22 +23,23 @@ import accounts from './modules/accounts';
Vue.use(Vuex);
export default new Vuex.Store({
modules: {
+ accounts,
agents,
auth,
billing,
cannedResponse,
Channel,
- contacts,
contactConversations,
+ contacts,
conversationLabels,
conversationMetadata,
conversationPage,
conversations,
+ conversationTypingStatus,
inboxes,
inboxMembers,
reports,
userNotificationSettings,
webhooks,
- accounts,
},
});
diff --git a/app/javascript/dashboard/store/modules/conversationTypingStatus.js b/app/javascript/dashboard/store/modules/conversationTypingStatus.js
new file mode 100644
index 000000000..4ad9f3cb4
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/conversationTypingStatus.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import * as types from '../mutation-types';
+
+const state = {
+ records: {},
+};
+
+export const getters = {
+ getUserList: $state => id => {
+ return $state.records[Number(id)] || [];
+ },
+};
+
+export const actions = {
+ create: ({ commit }, { conversationId, user }) => {
+ commit(types.default.ADD_USER_TYPING_TO_CONVERSATION, {
+ conversationId,
+ user,
+ });
+ },
+ destroy: ({ commit }, { conversationId, user }) => {
+ commit(types.default.REMOVE_USER_TYPING_FROM_CONVERSATION, {
+ conversationId,
+ user,
+ });
+ },
+};
+
+export const mutations = {
+ [types.default.ADD_USER_TYPING_TO_CONVERSATION]: (
+ $state,
+ { conversationId, user }
+ ) => {
+ const records = $state.records[conversationId] || [];
+ const hasUserRecordAlready = !!records.filter(
+ record => record.id === user.id && record.type === user.type
+ ).length;
+ if (!hasUserRecordAlready) {
+ Vue.set($state.records, conversationId, [...records, user]);
+ }
+ },
+ [types.default.REMOVE_USER_TYPING_FROM_CONVERSATION]: (
+ $state,
+ { conversationId, user }
+ ) => {
+ const records = $state.records[conversationId] || [];
+ const updatedRecords = records.filter(
+ record => record.id !== user.id || record.type !== user.type
+ );
+ Vue.set($state.records, conversationId, updatedRecords);
+ },
+};
+
+export default {
+ namespaced: true,
+ state,
+ getters,
+ actions,
+ mutations,
+};
diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js
index 1ae9ba7e6..275be8112 100644
--- a/app/javascript/dashboard/store/modules/conversations/actions.js
+++ b/app/javascript/dashboard/store/modules/conversations/actions.js
@@ -160,10 +160,10 @@ const actions = {
commit(types.default.UPDATE_CONVERSATION, conversation);
},
- toggleTyping: async ({ commit }, { status, inboxId, contactId }) => {
+ toggleTyping: async ({ commit }, { status, conversationId }) => {
try {
- await FBChannel.toggleTyping({ status, inboxId, contactId });
- commit(types.default.FB_TYPING, { status });
+ commit(types.default.SET_AGENT_TYPING, { status });
+ await ConversationApi.toggleTyping({ status, conversationId });
} catch (error) {
// Handle error
}
diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js
index 80294456a..9c9a0f22b 100644
--- a/app/javascript/dashboard/store/modules/conversations/index.js
+++ b/app/javascript/dashboard/store/modules/conversations/index.js
@@ -6,6 +6,14 @@ import getters, { getSelectedChatConversation } from './getters';
import actions from './actions';
import wootConstants from '../../../constants';
+const initialSelectedChat = {
+ id: null,
+ meta: {},
+ status: null,
+ seen: false,
+ agentTyping: 'off',
+ dataFetched: false,
+};
const state = {
allConversations: [],
convTabStats: {
@@ -13,14 +21,7 @@ const state = {
unAssignedCount: 0,
allCount: 0,
},
- selectedChat: {
- id: null,
- meta: {},
- status: null,
- seen: false,
- agentTyping: 'off',
- dataFetched: false,
- },
+ selectedChat: { ...initialSelectedChat },
listLoadingStatus: true,
chatStatusFilter: wootConstants.STATUS_TYPE.OPEN,
currentInbox: null,
@@ -42,14 +43,7 @@ const mutations = {
},
[types.default.EMPTY_ALL_CONVERSATION](_state) {
_state.allConversations = [];
- _state.selectedChat = {
- id: null,
- meta: {},
- status: null,
- seen: false,
- agentTyping: 'off',
- dataFetched: false,
- };
+ _state.selectedChat = { ...initialSelectedChat };
},
[types.default.SET_ALL_MESSAGES_LOADED](_state) {
const [chat] = getSelectedChatConversation(_state);
@@ -175,7 +169,7 @@ const mutations = {
_state.selectedChat.seen = true;
},
- [types.default.FB_TYPING](_state, { status }) {
+ [types.default.SET_AGENT_TYPING](_state, { status }) {
_state.selectedChat.agentTyping = status;
},
diff --git a/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/actions.spec.js b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/actions.spec.js
new file mode 100644
index 000000000..f9e62940c
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/actions.spec.js
@@ -0,0 +1,36 @@
+import { actions } from '../../conversationTypingStatus';
+import * as types from '../../../mutation-types';
+
+const commit = jest.fn();
+
+describe('#actions', () => {
+ describe('#create', () => {
+ it('sends correct actions', () => {
+ actions.create(
+ { commit },
+ { conversationId: 1, user: { id: 1, name: 'user-1' } }
+ );
+ expect(commit.mock.calls).toEqual([
+ [
+ types.default.ADD_USER_TYPING_TO_CONVERSATION,
+ { conversationId: 1, user: { id: 1, name: 'user-1' } },
+ ],
+ ]);
+ });
+ });
+
+ describe('#destroy', () => {
+ it('sends correct actions', () => {
+ actions.destroy(
+ { commit },
+ { conversationId: 1, user: { id: 1, name: 'user-1' } }
+ );
+ expect(commit.mock.calls).toEqual([
+ [
+ types.default.REMOVE_USER_TYPING_FROM_CONVERSATION,
+ { conversationId: 1, user: { id: 1, name: 'user-1' } },
+ ],
+ ]);
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/getters.spec.js b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/getters.spec.js
new file mode 100644
index 000000000..b7ed64631
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/getters.spec.js
@@ -0,0 +1,19 @@
+import { getters } from '../../conversationTypingStatus';
+
+describe('#getters', () => {
+ it('getUserList', () => {
+ const state = {
+ records: {
+ 1: [
+ { id: 1, name: 'user-1' },
+ { id: 2, name: 'user-2' },
+ ],
+ },
+ };
+ expect(getters.getUserList(state)(1)).toEqual([
+ { id: 1, name: 'user-1' },
+ { id: 2, name: 'user-2' },
+ ]);
+ expect(getters.getUserList(state)(2)).toEqual([]);
+ });
+});
diff --git a/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/mutations.spec.js b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/mutations.spec.js
new file mode 100644
index 000000000..00266b415
--- /dev/null
+++ b/app/javascript/dashboard/store/modules/specs/conversationTypingStatus/mutations.spec.js
@@ -0,0 +1,67 @@
+import * as types from '../../../mutation-types';
+import { mutations } from '../../conversationTypingStatus';
+
+describe('#mutations', () => {
+ describe('#ADD_USER_TYPING_TO_CONVERSATION', () => {
+ it('add user to state', () => {
+ const state = { records: {} };
+ mutations[types.default.ADD_USER_TYPING_TO_CONVERSATION](state, {
+ conversationId: 1,
+ user: { id: 1, type: 'contact', name: 'user-1' },
+ });
+ expect(state.records).toEqual({
+ 1: [{ id: 1, type: 'contact', name: 'user-1' }],
+ });
+ });
+
+ it('doesnot add user if user already exist', () => {
+ const state = {
+ records: {
+ 1: [{ id: 1, type: 'contact', name: 'user-1' }],
+ },
+ };
+ mutations[types.default.ADD_USER_TYPING_TO_CONVERSATION](state, {
+ conversationId: 1,
+ user: { id: 1, type: 'contact', name: 'user-1' },
+ });
+ expect(state.records).toEqual({
+ 1: [{ id: 1, type: 'contact', name: 'user-1' }],
+ });
+ });
+
+ it('add user to state if no matching user profiles are seen', () => {
+ const state = {
+ records: {
+ 1: [{ id: 1, type: 'user', name: 'user-1' }],
+ },
+ };
+ mutations[types.default.ADD_USER_TYPING_TO_CONVERSATION](state, {
+ conversationId: 1,
+ user: { id: 1, type: 'contact', name: 'user-1' },
+ });
+ expect(state.records).toEqual({
+ 1: [
+ { id: 1, type: 'user', name: 'user-1' },
+ { id: 1, type: 'contact', name: 'user-1' },
+ ],
+ });
+ });
+ });
+
+ describe('#REMOVE_USER_TYPING_FROM_CONVERSATION', () => {
+ it('remove add user if user exist', () => {
+ const state = {
+ records: {
+ 1: [{ id: 1, type: 'contact', name: 'user-1' }],
+ },
+ };
+ mutations[types.default.REMOVE_USER_TYPING_FROM_CONVERSATION](state, {
+ conversationId: 1,
+ user: { id: 1, type: 'contact', name: 'user-1' },
+ });
+ expect(state.records).toEqual({
+ 1: [],
+ });
+ });
+ });
+});
diff --git a/app/javascript/dashboard/store/mutation-types.js b/app/javascript/dashboard/store/mutation-types.js
index 6d3e626e7..e6ee84775 100755
--- a/app/javascript/dashboard/store/mutation-types.js
+++ b/app/javascript/dashboard/store/mutation-types.js
@@ -28,7 +28,7 @@ export default {
ADD_MESSAGE: 'ADD_MESSAGE',
MARK_SEEN: 'MARK_SEEN',
MARK_MESSAGE_READ: 'MARK_MESSAGE_READ',
- FB_TYPING: 'FB_TYPING',
+ SET_AGENT_TYPING: 'SET_AGENT_TYPING',
SET_PREVIOUS_CONVERSATIONS: 'SET_PREVIOUS_CONVERSATIONS',
SET_ACTIVE_INBOX: 'SET_ACTIVE_INBOX',
@@ -104,4 +104,8 @@ export default {
// Notification Settings
SET_USER_NOTIFICATION_UI_FLAG: 'SET_USER_NOTIFICATION_UI_FLAG',
SET_USER_NOTIFICATION: 'SET_USER_NOTIFICATION',
+
+ // User Typing
+ ADD_USER_TYPING_TO_CONVERSATION: 'ADD_USER_TYPING_TO_CONVERSATION',
+ REMOVE_USER_TYPING_FROM_CONVERSATION: 'REMOVE_USER_TYPING_FROM_CONVERSATION',
};
diff --git a/app/javascript/widget/api/conversation.js b/app/javascript/widget/api/conversation.js
index 3bacc3a36..67b7bbeb2 100755
--- a/app/javascript/widget/api/conversation.js
+++ b/app/javascript/widget/api/conversation.js
@@ -19,4 +19,11 @@ const getConversationAPI = async ({ before }) => {
return result;
};
-export { sendMessageAPI, getConversationAPI, sendAttachmentAPI };
+const toggleTyping = async ({ typingStatus }) => {
+ return API.post(
+ `/api/v1/widget/conversations/toggle_typing${window.location.search}`,
+ { typing_status: typingStatus }
+ );
+};
+
+export { sendMessageAPI, getConversationAPI, sendAttachmentAPI, toggleTyping };
diff --git a/app/javascript/widget/assets/images/typing.gif b/app/javascript/widget/assets/images/typing.gif
new file mode 100644
index 000000000..dd9b1ca2b
Binary files /dev/null and b/app/javascript/widget/assets/images/typing.gif differ
diff --git a/app/javascript/widget/components/AgentTypingBubble.vue b/app/javascript/widget/components/AgentTypingBubble.vue
new file mode 100644
index 000000000..d55b2dd7c
--- /dev/null
+++ b/app/javascript/widget/components/AgentTypingBubble.vue
@@ -0,0 +1,37 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/javascript/widget/components/ChatInputArea.vue b/app/javascript/widget/components/ChatInputArea.vue
index 167032951..bc335a72c 100755
--- a/app/javascript/widget/components/ChatInputArea.vue
+++ b/app/javascript/widget/components/ChatInputArea.vue
@@ -5,6 +5,8 @@
:placeholder="placeholder"
:value="value"
@input="$emit('input', $event.target.value)"
+ @focus="onFocus"
+ @blur="onBlur"
/>
@@ -17,8 +19,25 @@ export default {
ResizableTextarea,
},
props: {
- placeholder: String,
- value: String,
+ placeholder: {
+ type: String,
+ default: '',
+ },
+ value: {
+ type: String,
+ default: '',
+ },
+ },
+ methods: {
+ onBlur() {
+ this.toggleTyping('off');
+ },
+ onFocus() {
+ this.toggleTyping('on');
+ },
+ toggleTyping(typingStatus) {
+ this.$store.dispatch('conversation/toggleUserTyping', { typingStatus });
+ },
},
};
diff --git a/app/javascript/widget/components/ConversationWrap.vue b/app/javascript/widget/components/ConversationWrap.vue
index bcdabe62b..7e5758d33 100755
--- a/app/javascript/widget/components/ConversationWrap.vue
+++ b/app/javascript/widget/components/ConversationWrap.vue
@@ -1,23 +1,29 @@