Refactor Conversation, Message API calls, store

This commit is contained in:
Pranav Raj Sreepuram
2019-10-27 19:01:59 +05:30
parent c21c839dca
commit 170f8716c5
13 changed files with 215 additions and 402 deletions

View File

@@ -3,7 +3,7 @@ class Messages::Outgoing::NormalBuilder
def initialize(user, conversation, params) def initialize(user, conversation, params)
@content = params[:message] @content = params[:message]
@private = ['1', 'true', 1].include? params[:private] @private = ['1', 'true', 1, true].include? params[:private]
@conversation = conversation @conversation = conversation
@user = user @user = user
@fb_id = params[:fb_id] @fb_id = params[:fb_id]

View File

@@ -3,22 +3,26 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
around_action :handle_with_exception around_action :handle_with_exception
def mark_seen 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 head :ok
end end
def typing_on 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 head :ok
end end
def typing_off 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 head :ok
end end
private private
def fb_bot
::Facebook::Messenger::Bot
end
def handle_with_exception def handle_with_exception
yield yield
rescue Facebook::Messenger::Error => e rescue Facebook::Messenger::Error => e
@@ -27,14 +31,24 @@ class Api::V1::FacebookIndicatorsController < Api::BaseController
def payload(action) def payload(action)
{ {
recipient: { id: params[:sender_id] }, recipient: { id: contact.source_id },
sender_action: action sender_action: action
} }
end end
def inbox
@inbox ||= current_account.inboxes.find(permitted_params[:inbox_id])
end
def set_access_token def set_access_token
# have to cache this
inbox = current_account.inboxes.find(params[:inbox_id])
@access_token = inbox.channel.page_access_token @access_token = inbox.channel.page_access_token
end 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 end

View File

@@ -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();

View File

@@ -13,6 +13,7 @@ const endPoints = {
logout: { logout: {
url: 'auth/sign_out', url: 'auth/sign_out',
}, },
me: { me: {
url: 'api/v1/conversations.json', url: 'api/v1/conversations.json',
params: { type: 0, page: 1 }, params: { type: 0, page: 1 },
@@ -23,28 +24,6 @@ const endPoints = {
params: { inbox_id: null }, 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: { fetchLabels: {
url: 'api/v1/labels.json', url: 'api/v1/labels.json',
}, },
@@ -86,60 +65,6 @@ const endPoints = {
params: { omniauth_token: '' }, 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: { reports: {
account(metric, from, to) { account(metric, from, to) {
return { return {

View File

@@ -1,104 +1,37 @@
/* eslint no-console: 0 */
/* global axios */ /* global axios */
/* eslint no-undef: "error" */ import ApiClient from '../ApiClient';
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from '../endPoints';
export default { class ConversationApi extends ApiClient {
fetchConversation(id) { constructor() {
const urlData = endPoints('conversations')(id); super('conversations');
const fetchPromise = new Promise((resolve, reject) => { }
axios
.get(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
});
});
return fetchPromise;
},
toggleStatus(id) { get({ inboxId, convStatus, assigneeStatus }) {
const urlData = endPoints('resolveConversation')(id); return axios.get(this.url, {
const fetchPromise = new Promise((resolve, reject) => { params: {
axios
.post(urlData.url)
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
});
});
return fetchPromise;
},
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));
});
});
return fetchPromise;
},
markSeen({ inboxId, senderId }) {
const urlData = endPoints('fbMarkSeen');
const fetchPromise = new Promise((resolve, reject) => {
axios
.post(urlData.url, {
inbox_id: inboxId, inbox_id: inboxId,
sender_id: senderId, conversation_status_id: convStatus,
}) assignee_type_id: assigneeStatus,
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
});
});
return fetchPromise;
}, },
});
}
fbTyping({ flag, inboxId, senderId }) { toggleStatus(conversationId) {
const urlData = endPoints('fbTyping')(flag); return axios.post(`${this.url}/${conversationId}/toggle_status`, {});
const fetchPromise = new Promise((resolve, reject) => { }
axios
.post(urlData.url, { assignAgent({ conversationId, agentId }) {
inbox_id: inboxId, axios.post(
sender_id: senderId, `${this.url}/${conversationId}/assignments?assignee_id=${agentId}`,
}) {}
.then(response => { );
resolve(response); }
})
.catch(error => {
reject(Error(error));
});
});
return fetchPromise;
},
markMessageRead({ id, lastSeen }) { markMessageRead({ id, lastSeen }) {
const urlData = endPoints('markMessageRead')(id); return axios.post(`${this.url}/${id}/update_last_seen`, {
urlData.params.agent_last_seen_at = lastSeen; 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 fetchPromise; }
},
}; export default new ConversationApi();

View File

@@ -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);
});
},
};

View File

@@ -1,55 +1,24 @@
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* global axios */ /* global axios */
/* eslint no-undef: "error" */ import ApiClient from '../ApiClient';
/* eslint no-unused-expressions: ["error", { "allowShortCircuit": true }] */
import endPoints from '../endPoints';
export default { class MessageApi extends ApiClient {
sendMessage([conversationId, message]) { constructor() {
const urlData = endPoints('sendMessage')(conversationId, message); super('conversations');
const fetchPromise = new Promise((resolve, reject) => { }
axios
.post(urlData.url, urlData.params)
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
});
});
return fetchPromise;
},
addPrivateNote([conversationId, message]) { create({ conversationId, message, private: isPrivate }) {
const urlData = endPoints('addPrivateNote')(conversationId, message); return axios.post(`${this.url}/${conversationId}/messages`, {
const fetchPromise = new Promise((resolve, reject) => { message,
axios private: isPrivate,
.post(urlData.url, urlData.params)
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
}); });
}); }
return fetchPromise;
},
fetchPreviousMessages({ id, before }) { getPreviousMessages({ conversationId, before }) {
const urlData = endPoints('conversations')(id); return axios.get(`${this.url}/${conversationId}`, {
urlData.params.before = before; params: { before },
const fetchPromise = new Promise((resolve, reject) => {
axios
.get(urlData.url, {
params: urlData.params,
})
.then(response => {
resolve(response);
})
.catch(error => {
reject(Error(error));
}); });
}); }
return fetchPromise; }
},
}; export default new MessageApi();

View File

@@ -216,7 +216,7 @@ export default {
this.isLoadingPrevious = true; this.isLoadingPrevious = true;
this.$store this.$store
.dispatch('fetchPreviousMessages', { .dispatch('fetchPreviousMessages', {
id: this.currentChat.id, conversationId: this.currentChat.id,
before: this.getMessages.messages[0].id, before: this.getMessages.messages[0].id,
}) })
.then(() => { .then(() => {

View File

@@ -6,7 +6,9 @@
size="40px" size="40px"
:badge="chat.meta.sender.channel" :badge="chat.meta.sender.channel"
/> />
<h3 class="user--name">{{chat.meta.sender.name}}</h3> <h3 class="user--name">
{{ chat.meta.sender.name }}
</h3>
</div> </div>
<div class="flex-container"> <div class="flex-container">
<div class="multiselect-box ion-headphone"> <div class="multiselect-box ion-headphone">
@@ -14,24 +16,21 @@
v-model="currentChat.meta.assignee" v-model="currentChat.meta.assignee"
:options="agentList" :options="agentList"
label="name" label="name"
@select="assignAgent"
:allow-empty="true" :allow-empty="true"
deselect-label="Remove" deselect-label="Remove"
placeholder="Select Agent" placeholder="Select Agent"
selected-label='' selected-label=""
select-label="Assign" select-label="Assign"
track-by="id" track-by="id"
@select="assignAgent"
@remove="removeAgent" @remove="removeAgent"
/> />
</div> </div>
<ResolveButton /> <ResolveButton />
</div> </div>
</div> </div>
</template> </template>
<script> <script>
/* eslint no-console: 0 */ /* eslint no-console: 0 */
/* eslint no-param-reassign: 0 */ /* eslint no-param-reassign: 0 */
/* eslint no-shadow: 0 */ /* eslint no-shadow: 0 */
@@ -40,12 +39,14 @@
import { mapGetters } from 'vuex'; import { mapGetters } from 'vuex';
import Thumbnail from '../Thumbnail'; import Thumbnail from '../Thumbnail';
import ResolveButton from '../../buttons/ResolveButton'; import ResolveButton from '../../buttons/ResolveButton';
import EmojiInput from '../emoji/EmojiInput';
export default { export default {
props: [ components: {
'chat', Thumbnail,
], ResolveButton,
},
props: ['chat'],
data() { data() {
return { return {
@@ -75,8 +76,12 @@ export default {
methods: { methods: {
assignAgent(agent) { assignAgent(agent) {
this.$store.dispatch('assignAgent', [this.currentChat.id, agent.id]).then((response) => { this.$store
console.log('assignAgent', response); .dispatch('assignAgent', {
conversationId: this.currentChat.id,
agentId: agent.id,
})
.then(() => {
bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT')); bus.$emit('newToastMessage', this.$t('CONVERSATION.CHANGE_AGENT'));
}); });
}, },
@@ -85,11 +90,5 @@ export default {
console.log(agent.email); console.log(agent.email);
}, },
}, },
components: {
Thumbnail,
ResolveButton,
EmojiInput,
},
}; };
</script> </script>

View File

@@ -148,10 +148,13 @@ export default {
if (messageHasOnlyNewLines) { if (messageHasOnlyNewLines) {
return; return;
} }
const messageAction = this.isPrivate ? 'addPrivateNote' : 'sendMessage';
if (this.message.length !== 0 && !this.showCannedModal) { if (this.message.length !== 0 && !this.showCannedModal) {
this.$store this.$store
.dispatch(messageAction, [this.currentChat.id, this.message]) .dispatch('sendMessage', {
conversationId: this.currentChat.id,
message: this.message,
private: this.isPrivate,
})
.then(() => { .then(() => {
this.$emit('scrollToMessage'); this.$emit('scrollToMessage');
}); });
@@ -202,15 +205,15 @@ export default {
markSeen() { markSeen() {
this.$store.dispatch('markSeen', { this.$store.dispatch('markSeen', {
inboxId: this.currentChat.inbox_id, 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', { this.$store.dispatch('toggleTyping', {
flag, status,
inboxId: this.currentChat.inbox_id, inboxId: this.currentChat.inbox_id,
senderId: this.currentChat.meta.sender.id, contactId: this.currentChat.meta.sender.id,
}); });
}, },
disableButton() { disableButton() {

View File

@@ -1,23 +1,24 @@
import Vue from 'vue'; import Vue from 'vue';
import * as types from '../../mutation-types'; import * as types from '../../mutation-types';
import ChatList from '../../../api/inbox';
import ConversationApi from '../../../api/inbox/conversation'; 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 // actions
const actions = { const actions = {
fetchAllConversations({ commit }, fetchParams) { fetchAllConversations: async ({ commit }, params) => {
commit(types.default.SET_LIST_LOADING_STATUS); commit(types.default.SET_LIST_LOADING_STATUS);
ChatList.fetchAllConversations(fetchParams, response => { try {
commit(types.default.SET_ALL_CONVERSATION, { const response = await ConversationApi.get(params);
chats: response.data.data.payload, const { data } = response.data;
}); const { payload: chatList, meta: metaData } = data;
commit(types.default.SET_CONV_TAB_META, { commit(types.default.SET_ALL_CONVERSATION, chatList);
meta: response.data.data.meta, commit(types.default.SET_CONV_TAB_META, metaData);
});
commit(types.default.CLEAR_LIST_LOADING_STATUS); commit(types.default.CLEAR_LIST_LOADING_STATUS);
}); } catch (error) {
// Handle error
}
}, },
emptyAllConversations({ commit }) { emptyAllConversations({ commit }) {
@@ -28,25 +29,19 @@ const actions = {
commit(types.default.CLEAR_CURRENT_CHAT_WINDOW); commit(types.default.CLEAR_CURRENT_CHAT_WINDOW);
}, },
fetchPreviousMessages({ commit }, data) { fetchPreviousMessages: async ({ commit }, data) => {
const donePromise = new Promise(resolve => { try {
messageApi const response = await MessageApi.getPreviousMessages(data);
.fetchPreviousMessages(data)
.then(response => {
commit(types.default.SET_PREVIOUS_CONVERSATIONS, { commit(types.default.SET_PREVIOUS_CONVERSATIONS, {
id: data.id, id: data.conversationId,
data: response.data.payload, data: response.data.payload,
}); });
if (response.data.payload.length < 20) { if (response.data.payload.length < 20) {
commit(types.default.SET_ALL_MESSAGES_LOADED); commit(types.default.SET_ALL_MESSAGES_LOADED);
} }
resolve(); } catch (error) {
}) // Handle error
.catch(error => { }
console.log(error);
});
});
return donePromise;
}, },
setActiveChat(store, data) { setActiveChat(store, data) {
@@ -60,15 +55,15 @@ const actions = {
if (data.dataFetched === undefined) { if (data.dataFetched === undefined) {
donePromise = new Promise(resolve => { donePromise = new Promise(resolve => {
localDispatch('fetchPreviousMessages', { localDispatch('fetchPreviousMessages', {
id: data.id, conversationId: data.id,
before: data.messages[0].id, before: data.messages[0].id,
}) })
.then(() => { .then(() => {
Vue.set(data, 'dataFetched', true); Vue.set(data, 'dataFetched', true);
resolve(); resolve();
}) })
.catch(error => { .catch(() => {
console.log(error); // Handle error
}); });
}); });
} else { } else {
@@ -80,49 +75,37 @@ const actions = {
return donePromise; return donePromise;
}, },
assignAgent({ commit }, data) { assignAgent: async ({ commit }, { conversationId, agentId }) => {
return new Promise(resolve => { try {
ConversationApi.assignAgent(data).then(response => { const response = await ConversationApi.assignAgent({
conversationId,
agentId,
});
commit(types.default.ASSIGN_AGENT, response.data); commit(types.default.ASSIGN_AGENT, response.data);
resolve(response.data); } catch (error) {
}); // Handle error
}); }
}, },
toggleStatus({ commit }, data) { toggleStatus: async ({ commit }, data) => {
return new Promise(resolve => { try {
ConversationApi.toggleStatus(data).then(response => { const response = await ConversationApi.toggleStatus(data);
commit( commit(
types.default.RESOLVE_CONVERSATION, types.default.RESOLVE_CONVERSATION,
response.data.payload.current_status response.data.payload.current_status
); );
resolve(response.data); } catch (error) {
}); // Handle error
}); }
}, },
sendMessage({ commit }, data) { sendMessage: async ({ commit }, data) => {
return new Promise(resolve => { try {
messageApi const response = await MessageApi.create(data);
.sendMessage(data) commit(types.default.SEND_MESSAGE, response.data);
.then(response => { } catch (error) {
commit(types.default.SEND_MESSAGE, response); // Handle error
resolve(); }
})
.catch();
});
},
addPrivateNote({ commit }, data) {
return new Promise(resolve => {
messageApi
.addPrivateNote(data)
.then(response => {
commit(types.default.SEND_MESSAGE, response);
resolve();
})
.catch();
});
}, },
addMessage({ commit }, message) { addMessage({ commit }, message) {
@@ -133,39 +116,33 @@ const actions = {
commit(types.default.ADD_CONVERSATION, conversation); commit(types.default.ADD_CONVERSATION, conversation);
}, },
toggleTyping({ commit }, data) { toggleTyping: async ({ commit }, { status, inboxId, contactId }) => {
return new Promise(resolve => { try {
ConversationApi.fbTyping(data) await FBChannel.toggleTyping({ status, inboxId, contactId });
.then(() => { commit(types.default.FB_TYPING, { status });
commit(types.default.FB_TYPING, data); } catch (error) {
resolve(); // Handle error
}) }
.catch();
});
}, },
markSeen({ commit }, data) { markSeen: async ({ commit }, data) => {
return new Promise(resolve => { try {
ConversationApi.markSeen(data) await FBChannel.markSeen(data);
.then(response => { commit(types.default.MARK_SEEN);
commit(types.default.MARK_SEEN, response); } catch (error) {
resolve(); // Handle error
}) }
.catch();
});
}, },
markMessagesRead({ commit }, data) { markMessagesRead: async ({ commit }, data) => {
setTimeout(() => { setTimeout(() => {
commit(types.default.MARK_MESSAGE_READ, data); commit(types.default.MARK_MESSAGE_READ, data);
}, 4000); }, 4000);
return new Promise(resolve => { try {
ConversationApi.markMessageRead(data) await ConversationApi.markMessageRead(data);
.then(() => { } catch (error) {
resolve(); // Handle error
}) }
.catch();
});
}, },
setChatFilter({ commit }, data) { setChatFilter({ commit }, data) {

View File

@@ -30,10 +30,8 @@ const state = {
// mutations // mutations
const mutations = { const mutations = {
[types.default.SET_ALL_CONVERSATION](_state, data) { [types.default.SET_ALL_CONVERSATION](_state, chatList) {
if (data) { _state.allConversations.push(...chatList);
_state.allConversations.push(...data.chats);
}
}, },
[types.default.EMPTY_ALL_CONVERSATION](_state) { [types.default.EMPTY_ALL_CONVERSATION](_state) {
@@ -70,12 +68,17 @@ const mutations = {
} }
}, },
[types.default.SET_CONV_TAB_META](_state, { meta }) { [types.default.SET_CONV_TAB_META](
if (meta) { _state,
Vue.set(_state.convTabStats, 'overdueCount', meta.overdue_count); {
Vue.set(_state.convTabStats, 'allConvCount', meta.all_count); overdue_count: overdueCount,
Vue.set(_state.convTabStats, 'openCount', meta.open_count); 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) { [types.default.CURRENT_CHAT_WINDOW](_state, activeChat) {
@@ -115,11 +118,11 @@ const mutations = {
_state.selectedChat.status = status; _state.selectedChat.status = status;
}, },
[types.default.SEND_MESSAGE](_state, response) { [types.default.SEND_MESSAGE](_state, data) {
const [chat] = getSelectedChatConversation(_state); const [chat] = getSelectedChatConversation(_state);
const previousMessageIds = chat.messages.map(m => m.id); const previousMessageIds = chat.messages.map(m => m.id);
if (!previousMessageIds.includes(response.data.id)) { if (!previousMessageIds.includes(data.id)) {
chat.messages.push(response.data); chat.messages.push(data);
} }
}, },
@@ -141,14 +144,12 @@ const mutations = {
_state.allConversations.push(conversation); _state.allConversations.push(conversation);
}, },
[types.default.MARK_SEEN](_state, response) { [types.default.MARK_SEEN](_state) {
if (response.status === 200) {
_state.selectedChat.seen = true; _state.selectedChat.seen = true;
}
}, },
[types.default.FB_TYPING](_state, { flag }) { [types.default.FB_TYPING](_state, { status }) {
_state.selectedChat.agentTyping = flag; _state.selectedChat.agentTyping = status;
}, },
[types.default.SET_LIST_LOADING_STATUS](_state) { [types.default.SET_LIST_LOADING_STATUS](_state) {

View File

@@ -94,7 +94,7 @@ class Conversation < ApplicationRecord
user_name = Current.user&.name 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? create_assignee_change(user_name) if saved_change_to_assignee_id?
end end