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 @@