{
- this.app.$store.dispatch('conversation/addMessage', data);
+ this.app.$store.dispatch('conversation/addMessage', data).then(() => {
+ window.bus.$emit('on-agent-message-recieved');
+ });
};
onMessageUpdated = data => {
diff --git a/app/javascript/widget/i18n/locale/en.json b/app/javascript/widget/i18n/locale/en.json
index eeebff666..011adeae1 100644
--- a/app/javascript/widget/i18n/locale/en.json
+++ b/app/javascript/widget/i18n/locale/en.json
@@ -14,6 +14,10 @@
"OTHERS_ARE_AVAILABLE": "others are available",
"AND": "and"
},
+ "UNREAD_VIEW": {
+ "VIEW_MESSAGES_BUTTON": "See new messages",
+ "CLOSE_MESSAGES_BUTTON": "Close"
+ },
"POWERED_BY": "Powered by Chatwoot",
"EMAIL_PLACEHOLDER": "Please enter your email",
"CHAT_PLACEHOLDER": "Type your message"
diff --git a/app/javascript/widget/store/modules/agent.js b/app/javascript/widget/store/modules/agent.js
index 85b44bac8..9da668827 100644
--- a/app/javascript/widget/store/modules/agent.js
+++ b/app/javascript/widget/store/modules/agent.js
@@ -11,6 +11,7 @@ const state = {
};
export const getters = {
+ getHasFetched: $state => $state.uiFlags.hasFetched,
availableAgents: $state =>
$state.records.filter(agent => agent.availability_status === 'online'),
};
diff --git a/app/javascript/widget/store/modules/conversation.js b/app/javascript/widget/store/modules/conversation.js
index 92b602045..14e3abbf1 100755
--- a/app/javascript/widget/store/modules/conversation.js
+++ b/app/javascript/widget/store/modules/conversation.js
@@ -5,6 +5,7 @@ import {
getMessagesAPI,
sendAttachmentAPI,
toggleTyping,
+ setUserLastSeenAt,
} from 'widget/api/conversation';
import { MESSAGE_TYPE } from 'widget/helpers/constants';
import { playNotificationAudio } from 'shared/helpers/AudioNotificationHelper';
@@ -59,10 +60,22 @@ export const findUndeliveredMessage = (messageInbox, { content }) =>
message => message.content === content && message.status === 'in_progress'
);
+export const onNewMessageCreated = data => {
+ const { message_type: messageType } = data;
+ const isIncomingMessage = messageType === MESSAGE_TYPE.OUTGOING;
+
+ if (isIncomingMessage) {
+ playNotificationAudio();
+ }
+};
+
export const DEFAULT_CONVERSATION = 'default';
const state = {
conversations: {},
+ meta: {
+ userLastSeenAt: undefined,
+ },
uiFlags: {
allMessagesLoaded: false,
isFetchingList: false,
@@ -93,6 +106,31 @@ export const getters = {
}));
},
getIsFetchingList: _state => _state.uiFlags.isFetchingList,
+ getUnreadMessageCount: _state => {
+ const { userLastSeenAt } = _state.meta;
+ console.log(userLastSeenAt);
+ const count = Object.values(_state.conversations).filter(chat => {
+ const { created_at: createdAt, message_type: messageType } = chat;
+ const isOutGoing = messageType === MESSAGE_TYPE.OUTGOING;
+ const hasNotSeen = userLastSeenAt
+ ? createdAt * 1000 > userLastSeenAt * 1000
+ : true;
+ return hasNotSeen && isOutGoing;
+ }).length;
+ return count;
+ },
+ getUnreadTextMessages: (_state, _getters) => {
+ const unreadCount = _getters.getUnreadMessageCount;
+ const allMessages = [...Object.values(_state.conversations)];
+ console.log(unreadCount);
+ const unreadAgentMessages = allMessages.filter(message => {
+ const { message_type: messageType } = message;
+ return messageType === MESSAGE_TYPE.OUTGOING;
+ });
+ const maxUnreadCount = Math.min(unreadCount, 3);
+ const allUnreadMessages = unreadAgentMessages.splice(-maxUnreadCount);
+ return allUnreadMessages;
+ },
};
export const actions = {
@@ -112,7 +150,9 @@ export const actions = {
file_type: fileType,
status: 'in_progress',
};
- const tempMessage = createTemporaryMessage({ attachments: [attachment] });
+ const tempMessage = createTemporaryMessage({
+ attachments: [attachment],
+ });
commit('pushMessageToConversation', tempMessage);
try {
const { data } = await sendAttachmentAPI(params);
@@ -136,12 +176,9 @@ export const actions = {
}
},
- addMessage({ commit }, data) {
- if (data.message_type === MESSAGE_TYPE.OUTGOING) {
- playNotificationAudio();
- }
-
+ addMessage: async ({ commit }, data) => {
commit('pushMessageToConversation', data);
+ onNewMessageCreated(data);
},
updateMessage({ commit }, data) {
@@ -156,7 +193,21 @@ export const actions = {
try {
await toggleTyping(data);
} catch (error) {
- // console error
+ // IgnoreError
+ }
+ },
+
+ setUserLastSeen: async ({ commit, getters: appGetters }) => {
+ if (!appGetters.getConversationSize) {
+ return;
+ }
+
+ const lastSeen = Date.now() / 1000;
+ try {
+ commit('setMetaUserLastSeenAt', lastSeen);
+ await setUserLastSeenAt({ lastSeen });
+ } catch (error) {
+ // IgnoreError
}
},
};
@@ -224,6 +275,10 @@ export const mutations = {
const isTyping = status === 'on';
$state.uiFlags.isAgentTyping = isTyping;
},
+
+ setMetaUserLastSeenAt($state, lastSeen) {
+ $state.meta.userLastSeenAt = lastSeen;
+ },
};
export default {
diff --git a/app/javascript/widget/store/modules/conversationAttributes.js b/app/javascript/widget/store/modules/conversationAttributes.js
index 7f0a82d56..b4cbdeec0 100644
--- a/app/javascript/widget/store/modules/conversationAttributes.js
+++ b/app/javascript/widget/store/modules/conversationAttributes.js
@@ -17,7 +17,9 @@ export const actions = {
get: async ({ commit }) => {
try {
const { data } = await getConversationAPI();
+ const { user_last_seen_at: lastSeen } = data;
commit(SET_CONVERSATION_ATTRIBUTES, data);
+ commit('conversation/setMetaUserLastSeenAt', lastSeen, { root: true });
} catch (error) {
// Ignore error
}
diff --git a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
index 73c523231..d8c300727 100644
--- a/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversation/actions.spec.js
@@ -86,4 +86,12 @@ describe('#actions', () => {
});
});
});
+
+ describe('#setUserLastSeen', () => {
+ it('sends correct mutations', () => {
+ const lastSeen = Math.abs(Date.now() / 1000);
+ actions.setUserLastSeen({ commit }, { lastSeen });
+ expect(commit).toBeCalledWith('setMetaUserLastSeenAt', lastSeen);
+ });
+ });
});
diff --git a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js
index 99f1c983a..2eb448a09 100644
--- a/app/javascript/widget/store/modules/specs/conversation/getters.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversation/getters.spec.js
@@ -262,4 +262,171 @@ describe('#getters', () => {
},
]);
});
+
+ describe('getUnreadMessageCount returns', () => {
+ it('0 if there are no messages and last seen is undefined', () => {
+ const state = {
+ conversations: {},
+ meta: {
+ userLastSeenAt: undefined,
+ },
+ };
+ expect(getters.getUnreadMessageCount(state)).toEqual(0);
+ });
+
+ it('0 if there are no messages and last seen is present', () => {
+ const state = {
+ conversations: {},
+ meta: {
+ userLastSeenAt: Date.now(),
+ },
+ };
+ expect(getters.getUnreadMessageCount(state)).toEqual(0);
+ });
+
+ it('unread count if there are messages and last seen is before messages created-at', () => {
+ const state = {
+ conversations: {
+ 1: {
+ id: 1,
+ content: 'Thanks for the help',
+ created_at: 1574075964,
+ message_type: 1,
+ },
+ 2: {
+ id: 2,
+ content: 'Yes, It makes sense',
+ created_at: 1574092218,
+ message_type: 1,
+ },
+ },
+ meta: {
+ userLastSeenAt: 1474075964,
+ },
+ };
+ expect(getters.getUnreadMessageCount(state)).toEqual(2);
+ });
+
+ it('unread count if there are messages and last seen is after messages created-at', () => {
+ const state = {
+ conversations: {
+ 1: {
+ id: 1,
+ content: 'Thanks for the help',
+ created_at: 1574075964,
+ message_type: 1,
+ },
+ 2: {
+ id: 2,
+ content: 'Yes, It makes sense',
+ created_at: 1574092218,
+ message_type: 1,
+ },
+ 3: {
+ id: 3,
+ content: 'Yes, It makes sense',
+ created_at: 1574092218,
+ message_type: 0,
+ },
+ },
+ meta: {
+ userLastSeenAt: 1674075964,
+ },
+ };
+ expect(getters.getUnreadMessageCount(state)).toEqual(0);
+ });
+ });
+
+ describe('getUnreadTextMessages returns', () => {
+ it('no messages if there are no messages and last seen is undefined', () => {
+ const state = {
+ conversations: {},
+ meta: {
+ userLastSeenAt: undefined,
+ },
+ };
+ expect(
+ getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
+ ).toEqual([]);
+ });
+
+ it('0 if there are no messages and last seen is present', () => {
+ const state = {
+ conversations: {},
+ meta: {
+ userLastSeenAt: Date.now(),
+ },
+ };
+ expect(
+ getters.getUnreadTextMessages(state, { getUnreadMessageCount: 0 })
+ ).toEqual([]);
+ });
+
+ it('only unread text messages from agent if there are messages and last seen is before messages created-at', () => {
+ const state = {
+ conversations: {
+ 1: {
+ id: 1,
+ content: 'Thanks for the help',
+ created_at: 1574075964,
+ message_type: 1,
+ },
+ 2: {
+ id: 2,
+ content: 'Yes, It makes sense',
+ created_at: 1574092218,
+ message_type: 0,
+ },
+ },
+ };
+ expect(
+ getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
+ ).toEqual([
+ {
+ id: 1,
+ content: 'Thanks for the help',
+ created_at: 1574075964,
+ message_type: 1,
+ },
+ ]);
+ });
+
+ it('unread messages omitting seen messages ', () => {
+ const state = {
+ conversations: {
+ 1: {
+ id: 1,
+ content: 'Thanks for the help',
+ created_at: 1574075964,
+ message_type: 1,
+ },
+ 2: {
+ id: 2,
+ content: 'Yes, It makes sense',
+ created_at: 1674075965,
+ message_type: 1,
+ },
+ 3: {
+ id: 3,
+ content: 'Yes, It makes sense',
+ created_at: 1574092218,
+ message_type: 0,
+ },
+ },
+ meta: {
+ userLastSeenAt: 1674075964,
+ },
+ };
+ expect(
+ getters.getUnreadTextMessages(state, { getUnreadMessageCount: 1 })
+ ).toEqual([
+ {
+ id: 2,
+ content: 'Yes, It makes sense',
+ created_at: 1674075965,
+ message_type: 1,
+ },
+ ]);
+ });
+ });
});
diff --git a/app/javascript/widget/store/modules/specs/conversationAttributes/actions.spec.js b/app/javascript/widget/store/modules/specs/conversationAttributes/actions.spec.js
index 6267dd56a..15a447e85 100644
--- a/app/javascript/widget/store/modules/specs/conversationAttributes/actions.spec.js
+++ b/app/javascript/widget/store/modules/specs/conversationAttributes/actions.spec.js
@@ -11,6 +11,7 @@ describe('#actions', () => {
await actions.get({ commit });
expect(commit.mock.calls).toEqual([
['SET_CONVERSATION_ATTRIBUTES', { id: 1, status: 'bot' }],
+ ['conversation/setMetaUserLastSeenAt', undefined, { root: true }],
]);
});
it('doesnot send mutation if api is error', async () => {
diff --git a/app/javascript/widget/views/Home.vue b/app/javascript/widget/views/Home.vue
index 6e8724f68..127d8ca64 100755
--- a/app/javascript/widget/views/Home.vue
+++ b/app/javascript/widget/views/Home.vue
@@ -25,8 +25,6 @@
diff --git a/app/javascript/widget/views/Unread.vue b/app/javascript/widget/views/Unread.vue
new file mode 100644
index 000000000..2707f0638
--- /dev/null
+++ b/app/javascript/widget/views/Unread.vue
@@ -0,0 +1,207 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/app/views/api/v1/widget/conversations/index.json.jbuilder b/app/views/api/v1/widget/conversations/index.json.jbuilder
index 5c15a8364..8eb943785 100644
--- a/app/views/api/v1/widget/conversations/index.json.jbuilder
+++ b/app/views/api/v1/widget/conversations/index.json.jbuilder
@@ -1,5 +1,6 @@
if @conversation
json.id @conversation.display_id
json.inbox_id @conversation.inbox_id
+ json.user_last_seen_at @conversation.user_last_seen_at.to_i
json.status @conversation.status
end
diff --git a/config/routes.rb b/config/routes.rb
index 7c41848a4..0023e81ad 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -109,8 +109,9 @@ Rails.application.routes.draw do
namespace :widget do
resources :events, only: [:create]
resources :messages, only: [:index, :create, :update]
- resources :conversations do
+ resources :conversations, only: [:index] do
collection do
+ post :update_last_seen
post :toggle_typing
end
end
diff --git a/spec/controllers/api/v1/widget/conversations_controller_spec.rb b/spec/controllers/api/v1/widget/conversations_controller_spec.rb
index 7b3cc6538..8056b552f 100644
--- a/spec/controllers/api/v1/widget/conversations_controller_spec.rb
+++ b/spec/controllers/api/v1/widget/conversations_controller_spec.rb
@@ -9,6 +9,24 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
let(:payload) { { source_id: contact_inbox.source_id, inbox_id: web_widget.inbox.id } }
let(:token) { ::Widget::TokenService.new(payload: payload).generate_token }
+ describe 'GET /api/v1/widget/conversations' do
+ context 'with a conversation' do
+ it 'returns the correct conversation params' do
+ allow(Rails.configuration.dispatcher).to receive(:dispatch)
+ get '/api/v1/widget/conversations',
+ headers: { 'X-Auth-Token' => token },
+ params: { website_token: web_widget.website_token },
+ as: :json
+
+ expect(response).to have_http_status(:success)
+ json_response = JSON.parse(response.body)
+
+ expect(json_response['id']).to eq(conversation.display_id)
+ expect(json_response['status']).to eq(conversation.status)
+ end
+ end
+ end
+
describe 'POST /api/v1/widget/conversations/toggle_typing' do
context 'with a conversation' do
it 'dispatches the correct typing status' do
@@ -25,20 +43,20 @@ RSpec.describe '/api/v1/widget/conversations/toggle_typing', type: :request do
end
end
- describe 'POST /api/v1/widget/conversations' do
+ describe 'POST /api/v1/widget/conversations/update_last_seen' do
context 'with a conversation' do
it 'returns the correct conversation params' do
allow(Rails.configuration.dispatcher).to receive(:dispatch)
- get '/api/v1/widget/conversations',
- headers: { 'X-Auth-Token' => token },
- params: { website_token: web_widget.website_token },
- as: :json
+ expect(conversation.user_last_seen_at).to eq(nil)
+
+ post '/api/v1/widget/conversations/update_last_seen',
+ headers: { 'X-Auth-Token' => token },
+ params: { website_token: web_widget.website_token },
+ as: :json
expect(response).to have_http_status(:success)
- json_response = JSON.parse(response.body)
- expect(json_response['id']).to eq(conversation.display_id)
- expect(json_response['status']).to eq(conversation.status)
+ expect(conversation.reload.user_last_seen_at).not_to eq(nil)
end
end
end
diff --git a/spec/factories/conversations.rb b/spec/factories/conversations.rb
index ca5960a51..e3647cc64 100644
--- a/spec/factories/conversations.rb
+++ b/spec/factories/conversations.rb
@@ -4,7 +4,6 @@ FactoryBot.define do
factory :conversation do
status { 'open' }
display_id { rand(10_000_000) }
- user_last_seen_at { Time.current }
agent_last_seen_at { Time.current }
locked { false }
identifier { SecureRandom.hex }