From 5bc8219db52783e4d4bcb94aa1237714a9c5e602 Mon Sep 17 00:00:00 2001 From: Nithin David Thomas Date: Mon, 4 May 2020 23:07:56 +0530 Subject: [PATCH] Feature: Typing Indicator on widget and dashboard (#811) * Adds typing indicator for widget * typing indicator for agents in dashboard Co-authored-by: Pranav Raj Sreepuram --- .../v1/accounts/conversations_controller.rb | 10 ++- .../api/v1/widget/conversations_controller.rb | 27 +++++++ .../dashboard/api/inbox/conversation.js | 6 ++ .../dashboard/assets/images/typing.gif | Bin 0 -> 15940 bytes .../assets/scss/_foundation-settings.scss | 4 +- .../scss/widgets/_conversation-card.scss | 46 +++++++++--- .../scss/widgets/_conversation-view.scss | 35 ++++++++- .../widgets/conversation/MessagesView.vue | 42 +++++++++-- .../widgets/conversation/ReplyBox.vue | 21 ++---- .../dashboard/helper/actionCable.js | 41 +++++++++++ app/javascript/dashboard/helper/commons.js | 17 +++++ .../helper/{spec => specs}/URLHelper.spec.js | 0 .../dashboard/helper/specs/commons.spec.js | 26 +++++++ app/javascript/dashboard/store/index.js | 6 +- .../store/modules/conversationTypingStatus.js | 60 ++++++++++++++++ .../store/modules/conversations/actions.js | 6 +- .../store/modules/conversations/index.js | 28 +++----- .../conversationTypingStatus/actions.spec.js | 36 ++++++++++ .../conversationTypingStatus/getters.spec.js | 19 +++++ .../mutations.spec.js | 67 ++++++++++++++++++ .../dashboard/store/mutation-types.js | 6 +- app/javascript/widget/api/conversation.js | 9 ++- .../widget/assets/images/typing.gif | Bin 0 -> 15940 bytes .../widget/components/AgentTypingBubble.vue | 37 ++++++++++ .../widget/components/ChatInputArea.vue | 23 +++++- .../widget/components/ConversationWrap.vue | 26 ++++++- app/javascript/widget/helpers/actionCable.js | 31 ++++++++ .../widget/store/modules/conversation.js | 20 ++++++ .../specs/conversation/actions.spec.js | 9 +++ .../specs/conversation/getters.spec.js | 2 + .../specs/conversation/mutations.spec.js | 14 ++++ app/javascript/widget/views/About.vue | 5 -- app/listeners/action_cable_listener.rb | 26 ++++--- config/routes.rb | 5 ++ .../widget/conversations_controller_spec.rb | 27 +++++++ spec/listeners/action_cable_listener_spec.rb | 4 +- 36 files changed, 663 insertions(+), 78 deletions(-) create mode 100644 app/controllers/api/v1/widget/conversations_controller.rb create mode 100644 app/javascript/dashboard/assets/images/typing.gif rename app/javascript/dashboard/helper/{spec => specs}/URLHelper.spec.js (100%) create mode 100644 app/javascript/dashboard/helper/specs/commons.spec.js create mode 100644 app/javascript/dashboard/store/modules/conversationTypingStatus.js create mode 100644 app/javascript/dashboard/store/modules/specs/conversationTypingStatus/actions.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/conversationTypingStatus/getters.spec.js create mode 100644 app/javascript/dashboard/store/modules/specs/conversationTypingStatus/mutations.spec.js create mode 100644 app/javascript/widget/assets/images/typing.gif create mode 100644 app/javascript/widget/components/AgentTypingBubble.vue delete mode 100755 app/javascript/widget/views/About.vue create mode 100644 spec/controllers/api/v1/widget/conversations_controller_spec.rb 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 0000000000000000000000000000000000000000..dd9b1ca2b15bde2cec181518f4942305bd4b5ac4 GIT binary patch literal 15940 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 { @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 0000000000000000000000000000000000000000..dd9b1ca2b15bde2cec181518f4942305bd4b5ac4 GIT binary patch literal 15940 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 +
+
+
+
+
+ Agent is typing a message +
+
+
+
+ + + + + + 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 @@