feat: Add a read indicator for web-widget channel (#4224)

Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
Fayaz Ahmed 2022-04-20 16:03:12 +05:30 committed by GitHub
parent f2f0d466f2
commit 2b2252b66e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 109 additions and 1 deletions

View file

@ -60,6 +60,7 @@
:readable-time="readableTime" :readable-time="readableTime"
:source-id="data.source_id" :source-id="data.source_id"
:inbox-id="data.inbox_id" :inbox-id="data.inbox_id"
:message-read="showReadTicks"
/> />
</div> </div>
<spinner v-if="isPending" size="tiny" /> <spinner v-if="isPending" size="tiny" />
@ -153,6 +154,14 @@ export default {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
hasUserReadMessage: {
type: Boolean,
default: false,
},
isWebWidgetInbox: {
type: Boolean,
default: false,
},
}, },
data() { data() {
return { return {
@ -268,6 +277,14 @@ export default {
isOutgoing() { isOutgoing() {
return this.data.message_type === MESSAGE_TYPE.OUTGOING; return this.data.message_type === MESSAGE_TYPE.OUTGOING;
}, },
showReadTicks() {
return (
(this.isOutgoing || this.isTemplate) &&
this.hasUserReadMessage &&
this.isWebWidgetInbox &&
!this.data.private
);
},
isTemplate() { isTemplate() {
return this.data.message_type === MESSAGE_TYPE.TEMPLATE; return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
}, },

View file

@ -48,6 +48,10 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:has-instagram-story="hasInstagramStory" :has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/> />
<li v-show="getUnreadCount != 0" class="unread--toast"> <li v-show="getUnreadCount != 0" class="unread--toast">
<span class="text-uppercase"> <span class="text-uppercase">
@ -66,6 +70,10 @@
:data="message" :data="message"
:is-a-tweet="isATweet" :is-a-tweet="isATweet"
:has-instagram-story="hasInstagramStory" :has-instagram-story="hasInstagramStory"
:has-user-read-message="
hasUserReadMessage(message.created_at, getLastSeenAt)
"
:is-web-widget-inbox="isAWebWidgetInbox"
/> />
</ul> </ul>
<div <div
@ -141,6 +149,7 @@ export default {
listLoadingStatus: 'getAllMessagesLoaded', listLoadingStatus: 'getAllMessagesLoaded',
getUnreadCount: 'getUnreadCount', getUnreadCount: 'getUnreadCount',
loadingChatList: 'getChatListLoadingStatus', loadingChatList: 'getChatListLoadingStatus',
conversationLastSeen: 'getConversationLastSeen',
}), }),
inboxId() { inboxId() {
return this.currentChat.inbox_id; return this.currentChat.inbox_id;
@ -241,6 +250,11 @@ export default {
} }
return 'arrow-chevron-left'; return 'arrow-chevron-left';
}, },
getLastSeenAt() {
if (this.conversationLastSeen) return this.conversationLastSeen;
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
return contactLastSeenAt;
},
}, },
watch: { watch: {

View file

@ -8,6 +8,13 @@
size="16" size="16"
/> />
</span> </span>
<fluent-icon
v-if="messageRead"
v-tooltip.top-start="$t('CHAT_LIST.MESSAGE_READ')"
icon="checkmark-double"
class="action--icon read-tick"
size="12"
/>
<fluent-icon <fluent-icon
v-if="isEmail" v-if="isEmail"
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')" v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
@ -120,6 +127,10 @@ export default {
type: [String, Number], type: [String, Number],
default: 0, default: 0,
}, },
messageRead: {
type: Boolean,
default: false,
},
}, },
computed: { computed: {
inbox() { inbox() {
@ -173,6 +184,10 @@ export default {
} }
.action--icon { .action--icon {
&.read-tick {
color: var(--v-100);
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
}
color: var(--white); color: var(--white);
} }

View file

@ -23,6 +23,7 @@ class ActionCableConnector extends BaseActionCableConnector {
'contact.updated': this.onContactUpdate, 'contact.updated': this.onContactUpdate,
'conversation.mentioned': this.onConversationMentioned, 'conversation.mentioned': this.onConversationMentioned,
'notification.created': this.onNotificationCreated, 'notification.created': this.onNotificationCreated,
'conversation.read': this.onConversationRead,
}; };
} }
@ -64,6 +65,11 @@ class ActionCableConnector extends BaseActionCableConnector {
this.fetchConversationStats(); this.fetchConversationStats();
}; };
onConversationRead = data => {
const { contact_last_seen_at: lastSeen } = data;
this.app.$store.dispatch('updateConversationRead', lastSeen);
};
onLogout = () => AuthAPI.logout(); onLogout = () => AuthAPI.logout();
onMessageCreated = data => { onMessageCreated = data => {

View file

@ -81,6 +81,7 @@
"NO_MESSAGES": "No Messages", "NO_MESSAGES": "No Messages",
"NO_CONTENT": "No content available", "NO_CONTENT": "No content available",
"HIDE_QUOTED_TEXT": "Hide Quoted Text", "HIDE_QUOTED_TEXT": "Hide Quoted Text",
"SHOW_QUOTED_TEXT": "Show Quoted Text" "SHOW_QUOTED_TEXT": "Show Quoted Text",
"MESSAGE_READ": "Read"
} }
} }

View file

@ -15,6 +15,9 @@ export default {
chat.private !== true chat.private !== true
).length; ).length;
}, },
hasUserReadMessage(createdAt, contactLastSeen) {
return !(contactLastSeen - createdAt < 0);
},
readMessages(m) { readMessages(m) {
return m.messages.filter( return m.messages.filter(
chat => chat.created_at * 1000 <= m.agent_last_seen_at * 1000 chat => chat.created_at * 1000 <= m.agent_last_seen_at * 1000

View file

@ -26,4 +26,11 @@ describe('#conversationMixin', () => {
conversationMixin.methods.unReadMessages(conversationFixture.conversation) conversationMixin.methods.unReadMessages(conversationFixture.conversation)
).toEqual(conversationFixture.unReadMessages); ).toEqual(conversationFixture.unReadMessages);
}); });
it('should return the user message read flag', () => {
const contactLastSeen = 1649856659;
const createdAt = 1649859419;
expect(
conversationMixin.methods.hasUserReadMessage(createdAt, contactLastSeen)
).toEqual(false);
});
}); });

View file

@ -199,6 +199,10 @@ const actions = {
} }
}, },
updateConversationRead({ commit }, timestamp) {
commit(types.SET_CONVERSATION_LAST_SEEN, timestamp);
},
updateMessage({ commit }, message) { updateMessage({ commit }, message) {
commit(types.ADD_MESSAGE, message); commit(types.ADD_MESSAGE, message);
}, },

View file

@ -91,6 +91,9 @@ const getters = {
value => value.id === Number(conversationId) value => value.id === Number(conversationId)
); );
}, },
getConversationLastSeen: _state => {
return _state.conversationLastSeen;
},
}; };
export default getters; export default getters;

View file

@ -13,6 +13,7 @@ const state = {
currentInbox: null, currentInbox: null,
selectedChatId: null, selectedChatId: null,
appliedFilters: [], appliedFilters: [],
conversationLastSeen: null,
}; };
// mutations // mutations
@ -33,6 +34,9 @@ export const mutations = {
_state.allConversations = []; _state.allConversations = [];
_state.selectedChatId = null; _state.selectedChatId = null;
}, },
[types.SET_CONVERSATION_LAST_SEEN](_state, timestamp) {
_state.conversationLastSeen = timestamp;
},
[types.SET_ALL_MESSAGES_LOADED](_state) { [types.SET_ALL_MESSAGES_LOADED](_state) {
const [chat] = getSelectedChatConversation(_state); const [chat] = getSelectedChatConversation(_state);
Vue.set(chat, 'allMessagesLoaded', true); Vue.set(chat, 'allMessagesLoaded', true);

View file

@ -372,6 +372,15 @@ describe('#actions', () => {
expect(commit.mock.calls).toEqual([[types.CLEAR_CONVERSATION_FILTERS]]); expect(commit.mock.calls).toEqual([[types.CLEAR_CONVERSATION_FILTERS]]);
}); });
}); });
describe('#updateConversationRead', () => {
it('commits the correct mutation and sets the contact_last_seen', () => {
actions.updateConversationRead({ commit }, 1649856659);
expect(commit.mock.calls).toEqual([
[types.SET_CONVERSATION_LAST_SEEN, 1649856659],
]);
});
});
}); });
describe('#deleteMessage', () => { describe('#deleteMessage', () => {

View file

@ -132,6 +132,16 @@ describe('#getters', () => {
}); });
}); });
describe('#getConversationLastSeen', () => {
it('getConversationLastSeen', () => {
const timestamp = 1649856659;
const state = {
conversationLastSeen: timestamp,
};
expect(getters.getConversationLastSeen(state)).toEqual(timestamp);
});
});
describe('#getLastEmailInSelectedChat', () => { describe('#getLastEmailInSelectedChat', () => {
it('Returns cc in last email', () => { it('Returns cc in last email', () => {
const state = {}; const state = {};

View file

@ -187,6 +187,18 @@ describe('#mutations', () => {
]); ]);
}); });
describe('#SET_CONVERSATION_LAST_SEEN', () => {
it('sets conversation last seen timestamp', () => {
const state = {
conversationLastSeen: null,
};
mutations[types.SET_CONVERSATION_LAST_SEEN](state, 1649856659);
expect(state.conversationLastSeen).toEqual(1649856659);
});
});
describe('#UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES', () => { describe('#UPDATE_CONVERSATION_CUSTOM_ATTRIBUTES', () => {
it('update conversation custom attributes', () => { it('update conversation custom attributes', () => {
const custom_attributes = { order_id: 1001 }; const custom_attributes = { order_id: 1001 };

View file

@ -21,6 +21,7 @@ export default {
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS', CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS', SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS',
CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS', CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS',
SET_CONVERSATION_LAST_SEEN: 'SET_CONVERSATION_LAST_SEEN',
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW', SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW', CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',

View file

@ -37,6 +37,8 @@
"checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z", "checkmark-circle-solid": "M12 2c5.523 0 10 4.477 10 10s-4.477 10-10 10S2 17.523 2 12 6.477 2 12 2Zm3.22 6.97-4.47 4.47-1.97-1.97a.75.75 0 0 0-1.06 1.06l2.5 2.5a.75.75 0 0 0 1.06 0l5-5a.75.75 0 1 0-1.06-1.06Z",
"checkmark-square-solid": "M18 3a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12zm-1.53 4.97L10 14.44l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l7-7a.75.75 0 0 0-1.06-1.06z", "checkmark-square-solid": "M18 3a3 3 0 0 1 3 3v12a3 3 0 0 1-3 3H6a3 3 0 0 1-3-3V6a3 3 0 0 1 3-3h12zm-1.53 4.97L10 14.44l-2.47-2.47a.75.75 0 0 0-1.06 1.06l3 3a.75.75 0 0 0 1.06 0l7-7a.75.75 0 0 0-1.06-1.06z",
"checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z", "checkmark-outline": "M4.53 12.97a.75.75 0 0 0-1.06 1.06l4.5 4.5a.75.75 0 0 0 1.06 0l11-11a.75.75 0 0 0-1.06-1.06L8.5 16.94l-3.97-3.97Z",
"checkmark-solid": "m8.5 16.586l-3.793-3.793a1 1 0 0 0-1.414 1.414l4.5 4.5a1 1 0 0 0 1.414 0l11-11a1 1 0 0 0-1.414-1.414L8.5 16.586Z",
"checkmark-double-outline": "M10.2929 16.8787C9.90237 17.2692 9.90237 17.9024 10.2929 18.2929C10.6834 18.6834 11.3166 18.6834 11.7071 18.2929L23.2929 6.70711C23.6834 6.31658 23.6834 5.68342 23.2929 5.29289C22.9024 4.90237 22.2692 4.90237 21.8787 5.29289L10.2929 16.8787ZM2.70711 11.7929L6.5 15.5858L16.7929 5.29289C17.1834 4.90237 17.8166 4.90237 18.2071 5.29289C18.5976 5.68342 18.5976 6.31658 18.2071 6.70711L7.20711 17.7071C6.81658 18.0976 6.18342 18.0976 5.79289 17.7071L1.29289 13.2071C0.902369 12.8166 0.902369 12.1834 1.29289 11.7929C1.68342 11.4024 2.31658 11.4024 2.70711 11.7929Z",
"chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z", "chevron-down-outline": "M4.22 8.47a.75.75 0 0 1 1.06 0L12 15.19l6.72-6.72a.75.75 0 1 1 1.06 1.06l-7.25 7.25a.75.75 0 0 1-1.06 0L4.22 9.53a.75.75 0 0 1 0-1.06Z",
"chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z", "chevron-left-outline": "M15.53 4.22a.75.75 0 0 1 0 1.06L8.81 12l6.72 6.72a.75.75 0 1 1-1.06 1.06l-7.25-7.25a.75.75 0 0 1 0-1.06l7.25-7.25a.75.75 0 0 1 1.06 0Z",
"chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z", "chevron-right-outline": "M8.293 4.293a1 1 0 0 0 0 1.414L14.586 12l-6.293 6.293a1 1 0 1 0 1.414 1.414l7-7a1 1 0 0 0 0-1.414l-7-7a1 1 0 0 0-1.414 0Z",