diff --git a/app/builders/messages/outgoing/normal_builder.rb b/app/builders/messages/outgoing/normal_builder.rb index 2e08d7804d121..8517f4504328f 100644 --- a/app/builders/messages/outgoing/normal_builder.rb +++ b/app/builders/messages/outgoing/normal_builder.rb @@ -3,7 +3,7 @@ class Messages::Outgoing::NormalBuilder def initialize(user, conversation, params) @content = params[:message] - @private = ['1', 'true', 1].include? params[:private] + @private = ['1', 'true', 1, true].include? params[:private] @conversation = conversation @user = user @fb_id = params[:fb_id] diff --git a/app/controllers/api/v1/facebook_indicators_controller.rb b/app/controllers/api/v1/facebook_indicators_controller.rb index 2f4d4b0bc321a..dccf508c9cb7d 100644 --- a/app/controllers/api/v1/facebook_indicators_controller.rb +++ b/app/controllers/api/v1/facebook_indicators_controller.rb @@ -3,22 +3,26 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController around_action :handle_with_exception def mark_seen - Facebook::Messenger::Bot.deliver(payload('mark_seen'), access_token: @access_token) + fb_bot.deliver(payload('mark_seen'), access_token: @access_token) head :ok end def typing_on - Facebook::Messenger::Bot.deliver(payload('typing_on'), access_token: @access_token) + fb_bot.deliver(payload('typing_on'), access_token: @access_token) head :ok end def typing_off - Facebook::Messenger::Bot.deliver(payload('typing_off'), access_token: @access_token) + fb_bot.deliver(payload('typing_off'), access_token: @access_token) head :ok end private + def fb_bot + ::Facebook::Messenger::Bot + end + def handle_with_exception yield rescue Facebook::Messenger::Error => e @@ -27,14 +31,24 @@ def handle_with_exception def payload(action) { - recipient: { id: params[:sender_id] }, + recipient: { id: contact.source_id }, sender_action: action } end + def inbox + @inbox ||= current_account.inboxes.find(permitted_params[:inbox_id]) + end + def set_access_token - # have to cache this - inbox = current_account.inboxes.find(params[:inbox_id]) @access_token = inbox.channel.page_access_token end + + def contact + @contact ||= inbox.contact_inboxes.find_by!(contact_id: permitted_params[:contact_id]) + end + + def permitted_params + params.permit(:inbox_id, :contact_id) + end end diff --git a/app/javascript/dashboard/api/channel/fbChannel.js b/app/javascript/dashboard/api/channel/fbChannel.js new file mode 100644 index 0000000000000..2159001305e20 --- /dev/null +++ b/app/javascript/dashboard/api/channel/fbChannel.js @@ -0,0 +1,24 @@ +/* global axios */ +import ApiClient from '../ApiClient'; + +class FBChannel extends ApiClient { + constructor() { + super('facebook_indicators'); + } + + markSeen({ inboxId, contactId }) { + return axios.post(`${this.url}/mark_seen`, { + inbox_id: inboxId, + contact_id: contactId, + }); + } + + toggleTyping({ status, inboxId, contactId }) { + return axios.post(`${this.url}/typing_${status}`, { + inbox_id: inboxId, + contact_id: contactId, + }); + } +} + +export default new FBChannel(); diff --git a/app/javascript/dashboard/api/endPoints.js b/app/javascript/dashboard/api/endPoints.js index c9e175aef9789..fd7499848efcf 100644 --- a/app/javascript/dashboard/api/endPoints.js +++ b/app/javascript/dashboard/api/endPoints.js @@ -13,6 +13,7 @@ const endPoints = { logout: { url: 'auth/sign_out', }, + me: { url: 'api/v1/conversations.json', params: { type: 0, page: 1 }, @@ -23,28 +24,6 @@ const endPoints = { params: { inbox_id: null }, }, - conversations(id) { - return { url: `api/v1/conversations/${id}.json`, params: { before: null } }; - }, - - resolveConversation(id) { - return { url: `api/v1/conversations/${id}/toggle_status.json` }; - }, - - sendMessage(conversationId, message) { - return { - url: `api/v1/conversations/${conversationId}/messages.json`, - params: { message }, - }; - }, - - addPrivateNote(conversationId, message) { - return { - url: `api/v1/conversations/${conversationId}/messages.json?`, - params: { message, private: 'true' }, - }; - }, - fetchLabels: { url: 'api/v1/labels.json', }, @@ -86,60 +65,6 @@ const endPoints = { params: { omniauth_token: '' }, }, - assignAgent(conversationId, AgentId) { - return { - url: `/api/v1/conversations/${conversationId}/assignments?assignee_id=${AgentId}`, - }; - }, - - fbMarkSeen: { - url: 'api/v1/facebook_indicators/mark_seen', - }, - - fbTyping(status) { - return { - url: `api/v1/facebook_indicators/typing_${status}`, - }; - }, - - markMessageRead(id) { - return { - url: `api/v1/conversations/${id}/update_last_seen`, - params: { - agent_last_seen_at: null, - }, - }; - }, - - // Canned Response [GET, POST, PUT, DELETE] - cannedResponse: { - get() { - return { - url: 'api/v1/canned_responses', - }; - }, - getOne({ id }) { - return { - url: `api/v1/canned_responses/${id}`, - }; - }, - post() { - return { - url: 'api/v1/canned_responses', - }; - }, - put(id) { - return { - url: `api/v1/canned_responses/${id}`, - }; - }, - delete(id) { - return { - url: `api/v1/canned_responses/${id}`, - }; - }, - }, - reports: { account(metric, from, to) { return { diff --git a/app/javascript/dashboard/api/inbox/conversation.js b/app/javascript/dashboard/api/inbox/conversation.js index 92bfc987f7db5..c43f41c70d14c 100644 --- a/app/javascript/dashboard/api/inbox/conversation.js +++ b/app/javascript/dashboard/api/inbox/conversation.js @@ -1,104 +1,37 @@ -/* eslint no-console: 0 */ /* global axios */ -/* eslint no-undef: "error" */ -/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ -import endPoints from '../endPoints'; +import ApiClient from '../ApiClient'; -export default { - fetchConversation(id) { - const urlData = endPoints('conversations')(id); - const fetchPromise = new Promise((resolve, reject) => { - axios - .get(urlData.url) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); - }); - return fetchPromise; - }, - - toggleStatus(id) { - const urlData = endPoints('resolveConversation')(id); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); - }); - return fetchPromise; - }, +class ConversationApi extends ApiClient { + constructor() { + super('conversations'); + } - assignAgent([id, agentId]) { - const urlData = endPoints('assignAgent')(id, agentId); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); + get({ inboxId, convStatus, assigneeStatus }) { + return axios.get(this.url, { + params: { + inbox_id: inboxId, + conversation_status_id: convStatus, + assignee_type_id: assigneeStatus, + }, }); - return fetchPromise; - }, + } - markSeen({ inboxId, senderId }) { - const urlData = endPoints('fbMarkSeen'); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, { - inbox_id: inboxId, - sender_id: senderId, - }) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); - }); - return fetchPromise; - }, + toggleStatus(conversationId) { + return axios.post(`${this.url}/${conversationId}/toggle_status`, {}); + } - fbTyping({ flag, inboxId, senderId }) { - const urlData = endPoints('fbTyping')(flag); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, { - inbox_id: inboxId, - sender_id: senderId, - }) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); - }); - return fetchPromise; - }, + assignAgent({ conversationId, agentId }) { + axios.post( + `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`, + {} + ); + } markMessageRead({ id, lastSeen }) { - const urlData = endPoints('markMessageRead')(id); - urlData.params.agent_last_seen_at = lastSeen; - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, urlData.params) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); + return axios.post(`${this.url}/${id}/update_last_seen`, { + agent_last_seen_at: lastSeen, }); - return fetchPromise; - }, -}; + } +} + +export default new ConversationApi(); diff --git a/app/javascript/dashboard/api/inbox/index.js b/app/javascript/dashboard/api/inbox/index.js deleted file mode 100644 index 228fa7f799fd6..0000000000000 --- a/app/javascript/dashboard/api/inbox/index.js +++ /dev/null @@ -1,32 +0,0 @@ -/* eslint no-console: 0 */ -/* global axios */ -/* eslint no-undef: "error" */ -/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ -import endPoints from '../endPoints'; - -export default { - fetchAllConversations(params, callback) { - const urlData = endPoints('getInbox'); - - if (params.inbox !== 0) { - urlData.params.inbox_id = params.inbox; - } else { - urlData.params.inbox_id = null; - } - urlData.params = { - ...urlData.params, - conversation_status_id: params.convStatus, - assignee_type_id: params.assigneeStatus, - }; - axios - .get(urlData.url, { - params: urlData.params, - }) - .then(response => { - callback(response); - }) - .catch(error => { - console.log(error); - }); - }, -}; diff --git a/app/javascript/dashboard/api/inbox/message.js b/app/javascript/dashboard/api/inbox/message.js index 1c6eebae61767..367e3e00cd22a 100644 --- a/app/javascript/dashboard/api/inbox/message.js +++ b/app/javascript/dashboard/api/inbox/message.js @@ -1,55 +1,24 @@ /* eslint no-console: 0 */ /* global axios */ -/* eslint no-undef: "error" */ -/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */ -import endPoints from '../endPoints'; +import ApiClient from '../ApiClient'; -export default { - sendMessage([conversationId, message]) { - const urlData = endPoints('sendMessage')(conversationId, message); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, urlData.params) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); - }); - return fetchPromise; - }, +class MessageApi extends ApiClient { + constructor() { + super('conversations'); + } - addPrivateNote([conversationId, message]) { - const urlData = endPoints('addPrivateNote')(conversationId, message); - const fetchPromise = new Promise((resolve, reject) => { - axios - .post(urlData.url, urlData.params) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); + create({ conversationId, message, private: isPrivate }) { + return axios.post(`${this.url}/${conversationId}/messages`, { + message, + private: isPrivate, }); - return fetchPromise; - }, + } - fetchPreviousMessages({ id, before }) { - const urlData = endPoints('conversations')(id); - urlData.params.before = before; - const fetchPromise = new Promise((resolve, reject) => { - axios - .get(urlData.url, { - params: urlData.params, - }) - .then(response => { - resolve(response); - }) - .catch(error => { - reject(Error(error)); - }); + getPreviousMessages({ conversationId, before }) { + return axios.get(`${this.url}/${conversationId}`, { + params: { before }, }); - return fetchPromise; - }, -}; + } +} + +export default new MessageApi(); diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue index aa8847e6bc4b1..682c3b4f0d63d 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationBox.vue @@ -216,7 +216,7 @@ export default { this.isLoadingPrevious = true; this.$store .dispatch('fetchPreviousMessages', { - id: this.currentChat.id, + conversationId: this.currentChat.id, before: this.getMessages.messages[0].id, }) .then(() => { diff --git a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue index cddaf02e7331c..cdeb5b6beba11 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ConversationHeader.vue @@ -6,7 +6,9 @@ size="40px" :badge="chat.meta.sender.channel" /> -

{{chat.meta.sender.name}}

+

+ {{ chat.meta.sender.name }} +

@@ -14,24 +16,21 @@ v-model="currentChat.meta.assignee" :options="agentList" label="name" - @select="assignAgent" :allow-empty="true" deselect-label="Remove" placeholder="Select Agent" - selected-label='' + selected-label="" select-label="Assign" track-by="id" + @select="assignAgent" @remove="removeAgent" />
- - diff --git a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue index 24bcb818e9bdc..e49d3a5d2cef0 100644 --- a/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue +++ b/app/javascript/dashboard/components/widgets/conversation/ReplyBox.vue @@ -148,10 +148,13 @@ export default { if (messageHasOnlyNewLines) { return; } - const messageAction = this.isPrivate ? 'addPrivateNote' : 'sendMessage'; if (this.message.length !== 0 && !this.showCannedModal) { this.$store - .dispatch(messageAction, [this.currentChat.id, this.message]) + .dispatch('sendMessage', { + conversationId: this.currentChat.id, + message: this.message, + private: this.isPrivate, + }) .then(() => { this.$emit('scrollToMessage'); }); @@ -202,15 +205,15 @@ export default { markSeen() { this.$store.dispatch('markSeen', { inboxId: this.currentChat.inbox_id, - senderId: this.currentChat.meta.sender.id, + contactId: this.currentChat.meta.sender.id, }); }, - toggleTyping(flag) { + toggleTyping(status) { this.$store.dispatch('toggleTyping', { - flag, + status, inboxId: this.currentChat.inbox_id, - senderId: this.currentChat.meta.sender.id, + contactId: this.currentChat.meta.sender.id, }); }, disableButton() { diff --git a/app/javascript/dashboard/store/modules/conversations/actions.js b/app/javascript/dashboard/store/modules/conversations/actions.js index a168071ff6278..4aa6b5b5f0119 100644 --- a/app/javascript/dashboard/store/modules/conversations/actions.js +++ b/app/javascript/dashboard/store/modules/conversations/actions.js @@ -1,23 +1,24 @@ import Vue from 'vue'; import * as types from '../../mutation-types'; -import ChatList from '../../../api/inbox'; import ConversationApi from '../../../api/inbox/conversation'; -import messageApi from '../../../api/inbox/message'; +import MessageApi from '../../../api/inbox/message'; +import FBChannel from '../../../api/channel/fbChannel'; // actions const actions = { - fetchAllConversations({ commit }, fetchParams) { + fetchAllConversations: async ({ commit }, params) => { commit(types.default.SET_LIST_LOADING_STATUS); - ChatList.fetchAllConversations(fetchParams, response => { - commit(types.default.SET_ALL_CONVERSATION, { - chats: response.data.data.payload, - }); - commit(types.default.SET_CONV_TAB_META, { - meta: response.data.data.meta, - }); + try { + const response = await ConversationApi.get(params); + const { data } = response.data; + const { payload: chatList, meta: metaData } = data; + commit(types.default.SET_ALL_CONVERSATION, chatList); + commit(types.default.SET_CONV_TAB_META, metaData); commit(types.default.CLEAR_LIST_LOADING_STATUS); - }); + } catch (error) { + // Handle error + } }, emptyAllConversations({ commit }) { @@ -28,25 +29,19 @@ const actions = { commit(types.default.CLEAR_CURRENT_CHAT_WINDOW); }, - fetchPreviousMessages({ commit }, data) { - const donePromise = new Promise(resolve => { - messageApi - .fetchPreviousMessages(data) - .then(response => { - commit(types.default.SET_PREVIOUS_CONVERSATIONS, { - id: data.id, - data: response.data.payload, - }); - if (response.data.payload.length < 20) { - commit(types.default.SET_ALL_MESSAGES_LOADED); - } - resolve(); - }) - .catch(error => { - console.log(error); - }); - }); - return donePromise; + fetchPreviousMessages: async ({ commit }, data) => { + try { + const response = await MessageApi.getPreviousMessages(data); + commit(types.default.SET_PREVIOUS_CONVERSATIONS, { + id: data.conversationId, + data: response.data.payload, + }); + if (response.data.payload.length < 20) { + commit(types.default.SET_ALL_MESSAGES_LOADED); + } + } catch (error) { + // Handle error + } }, setActiveChat(store, data) { @@ -60,15 +55,15 @@ const actions = { if (data.dataFetched === undefined) { donePromise = new Promise(resolve => { localDispatch('fetchPreviousMessages', { - id: data.id, + conversationId: data.id, before: data.messages[0].id, }) .then(() => { Vue.set(data, 'dataFetched', true); resolve(); }) - .catch(error => { - console.log(error); + .catch(() => { + // Handle error }); }); } else { @@ -80,49 +75,37 @@ const actions = { return donePromise; }, - assignAgent({ commit }, data) { - return new Promise(resolve => { - ConversationApi.assignAgent(data).then(response => { - commit(types.default.ASSIGN_AGENT, response.data); - resolve(response.data); - }); - }); - }, - - toggleStatus({ commit }, data) { - return new Promise(resolve => { - ConversationApi.toggleStatus(data).then(response => { - commit( - types.default.RESOLVE_CONVERSATION, - response.data.payload.current_status - ); - resolve(response.data); + assignAgent: async ({ commit }, { conversationId, agentId }) => { + try { + const response = await ConversationApi.assignAgent({ + conversationId, + agentId, }); - }); + commit(types.default.ASSIGN_AGENT, response.data); + } catch (error) { + // Handle error + } }, - sendMessage({ commit }, data) { - return new Promise(resolve => { - messageApi - .sendMessage(data) - .then(response => { - commit(types.default.SEND_MESSAGE, response); - resolve(); - }) - .catch(); - }); + toggleStatus: async ({ commit }, data) => { + try { + const response = await ConversationApi.toggleStatus(data); + commit( + types.default.RESOLVE_CONVERSATION, + response.data.payload.current_status + ); + } catch (error) { + // Handle error + } }, - addPrivateNote({ commit }, data) { - return new Promise(resolve => { - messageApi - .addPrivateNote(data) - .then(response => { - commit(types.default.SEND_MESSAGE, response); - resolve(); - }) - .catch(); - }); + sendMessage: async ({ commit }, data) => { + try { + const response = await MessageApi.create(data); + commit(types.default.SEND_MESSAGE, response.data); + } catch (error) { + // Handle error + } }, addMessage({ commit }, message) { @@ -133,39 +116,33 @@ const actions = { commit(types.default.ADD_CONVERSATION, conversation); }, - toggleTyping({ commit }, data) { - return new Promise(resolve => { - ConversationApi.fbTyping(data) - .then(() => { - commit(types.default.FB_TYPING, data); - resolve(); - }) - .catch(); - }); + toggleTyping: async ({ commit }, { status, inboxId, contactId }) => { + try { + await FBChannel.toggleTyping({ status, inboxId, contactId }); + commit(types.default.FB_TYPING, { status }); + } catch (error) { + // Handle error + } }, - markSeen({ commit }, data) { - return new Promise(resolve => { - ConversationApi.markSeen(data) - .then(response => { - commit(types.default.MARK_SEEN, response); - resolve(); - }) - .catch(); - }); + markSeen: async ({ commit }, data) => { + try { + await FBChannel.markSeen(data); + commit(types.default.MARK_SEEN); + } catch (error) { + // Handle error + } }, - markMessagesRead({ commit }, data) { + markMessagesRead: async ({ commit }, data) => { setTimeout(() => { commit(types.default.MARK_MESSAGE_READ, data); }, 4000); - return new Promise(resolve => { - ConversationApi.markMessageRead(data) - .then(() => { - resolve(); - }) - .catch(); - }); + try { + await ConversationApi.markMessageRead(data); + } catch (error) { + // Handle error + } }, setChatFilter({ commit }, data) { diff --git a/app/javascript/dashboard/store/modules/conversations/index.js b/app/javascript/dashboard/store/modules/conversations/index.js index da48a3c7b4098..9a2f30beaab92 100644 --- a/app/javascript/dashboard/store/modules/conversations/index.js +++ b/app/javascript/dashboard/store/modules/conversations/index.js @@ -30,10 +30,8 @@ const state = { // mutations const mutations = { - [types.default.SET_ALL_CONVERSATION](_state, data) { - if (data) { - _state.allConversations.push(...data.chats); - } + [types.default.SET_ALL_CONVERSATION](_state, chatList) { + _state.allConversations.push(...chatList); }, [types.default.EMPTY_ALL_CONVERSATION](_state) { @@ -70,12 +68,17 @@ const mutations = { } }, - [types.default.SET_CONV_TAB_META](_state, { meta }) { - if (meta) { - Vue.set(_state.convTabStats, 'overdueCount', meta.overdue_count); - Vue.set(_state.convTabStats, 'allConvCount', meta.all_count); - Vue.set(_state.convTabStats, 'openCount', meta.open_count); - } + [types.default.SET_CONV_TAB_META]( + _state, + { + overdue_count: overdueCount, + all_count: allCount, + open_count: openCount, + } = {} + ) { + Vue.set(_state.convTabStats, 'overdueCount', overdueCount); + Vue.set(_state.convTabStats, 'allConvCount', allCount); + Vue.set(_state.convTabStats, 'openCount', openCount); }, [types.default.CURRENT_CHAT_WINDOW](_state, activeChat) { @@ -115,11 +118,11 @@ const mutations = { _state.selectedChat.status = status; }, - [types.default.SEND_MESSAGE](_state, response) { + [types.default.SEND_MESSAGE](_state, data) { const [chat] = getSelectedChatConversation(_state); const previousMessageIds = chat.messages.map(m => m.id); - if (!previousMessageIds.includes(response.data.id)) { - chat.messages.push(response.data); + if (!previousMessageIds.includes(data.id)) { + chat.messages.push(data); } }, @@ -141,14 +144,12 @@ const mutations = { _state.allConversations.push(conversation); }, - [types.default.MARK_SEEN](_state, response) { - if (response.status === 200) { - _state.selectedChat.seen = true; - } + [types.default.MARK_SEEN](_state) { + _state.selectedChat.seen = true; }, - [types.default.FB_TYPING](_state, { flag }) { - _state.selectedChat.agentTyping = flag; + [types.default.FB_TYPING](_state, { status }) { + _state.selectedChat.agentTyping = status; }, [types.default.SET_LIST_LOADING_STATUS](_state) { diff --git a/app/models/conversation.rb b/app/models/conversation.rb index 8881e9363ae9d..eda85ea152f55 100644 --- a/app/models/conversation.rb +++ b/app/models/conversation.rb @@ -94,7 +94,7 @@ def create_activity user_name = Current.user&.name - create_status_change_message(user_name) if saved_change_to_assignee_id? + create_status_change_message(user_name) if saved_change_to_status? create_assignee_change(user_name) if saved_change_to_assignee_id? end