feat: Add a read indicator for web-widget channel (#4224)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
@@ -60,6 +60,7 @@
|
||||
:readable-time="readableTime"
|
||||
:source-id="data.source_id"
|
||||
:inbox-id="data.inbox_id"
|
||||
:message-read="showReadTicks"
|
||||
/>
|
||||
</div>
|
||||
<spinner v-if="isPending" size="tiny" />
|
||||
@@ -153,6 +154,14 @@ export default {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
hasUserReadMessage: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
isWebWidgetInbox: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
@@ -268,6 +277,14 @@ export default {
|
||||
isOutgoing() {
|
||||
return this.data.message_type === MESSAGE_TYPE.OUTGOING;
|
||||
},
|
||||
showReadTicks() {
|
||||
return (
|
||||
(this.isOutgoing || this.isTemplate) &&
|
||||
this.hasUserReadMessage &&
|
||||
this.isWebWidgetInbox &&
|
||||
!this.data.private
|
||||
);
|
||||
},
|
||||
isTemplate() {
|
||||
return this.data.message_type === MESSAGE_TYPE.TEMPLATE;
|
||||
},
|
||||
|
||||
@@ -48,6 +48,10 @@
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
: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">
|
||||
<span class="text-uppercase">
|
||||
@@ -66,6 +70,10 @@
|
||||
:data="message"
|
||||
:is-a-tweet="isATweet"
|
||||
:has-instagram-story="hasInstagramStory"
|
||||
:has-user-read-message="
|
||||
hasUserReadMessage(message.created_at, getLastSeenAt)
|
||||
"
|
||||
:is-web-widget-inbox="isAWebWidgetInbox"
|
||||
/>
|
||||
</ul>
|
||||
<div
|
||||
@@ -141,6 +149,7 @@ export default {
|
||||
listLoadingStatus: 'getAllMessagesLoaded',
|
||||
getUnreadCount: 'getUnreadCount',
|
||||
loadingChatList: 'getChatListLoadingStatus',
|
||||
conversationLastSeen: 'getConversationLastSeen',
|
||||
}),
|
||||
inboxId() {
|
||||
return this.currentChat.inbox_id;
|
||||
@@ -241,6 +250,11 @@ export default {
|
||||
}
|
||||
return 'arrow-chevron-left';
|
||||
},
|
||||
getLastSeenAt() {
|
||||
if (this.conversationLastSeen) return this.conversationLastSeen;
|
||||
const { contact_last_seen_at: contactLastSeenAt } = this.currentChat;
|
||||
return contactLastSeenAt;
|
||||
},
|
||||
},
|
||||
|
||||
watch: {
|
||||
|
||||
@@ -8,6 +8,13 @@
|
||||
size="16"
|
||||
/>
|
||||
</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
|
||||
v-if="isEmail"
|
||||
v-tooltip.top-start="$t('CHAT_LIST.RECEIVED_VIA_EMAIL')"
|
||||
@@ -120,6 +127,10 @@ export default {
|
||||
type: [String, Number],
|
||||
default: 0,
|
||||
},
|
||||
messageRead: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
inbox() {
|
||||
@@ -173,6 +184,10 @@ export default {
|
||||
}
|
||||
|
||||
.action--icon {
|
||||
&.read-tick {
|
||||
color: var(--v-100);
|
||||
margin-top: calc(var(--space-micro) + var(--space-micro) / 2);
|
||||
}
|
||||
color: var(--white);
|
||||
}
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
'contact.updated': this.onContactUpdate,
|
||||
'conversation.mentioned': this.onConversationMentioned,
|
||||
'notification.created': this.onNotificationCreated,
|
||||
'conversation.read': this.onConversationRead,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -64,6 +65,11 @@ class ActionCableConnector extends BaseActionCableConnector {
|
||||
this.fetchConversationStats();
|
||||
};
|
||||
|
||||
onConversationRead = data => {
|
||||
const { contact_last_seen_at: lastSeen } = data;
|
||||
this.app.$store.dispatch('updateConversationRead', lastSeen);
|
||||
};
|
||||
|
||||
onLogout = () => AuthAPI.logout();
|
||||
|
||||
onMessageCreated = data => {
|
||||
|
||||
@@ -81,6 +81,7 @@
|
||||
"NO_MESSAGES": "No Messages",
|
||||
"NO_CONTENT": "No content available",
|
||||
"HIDE_QUOTED_TEXT": "Hide Quoted Text",
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text"
|
||||
"SHOW_QUOTED_TEXT": "Show Quoted Text",
|
||||
"MESSAGE_READ": "Read"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,6 +15,9 @@ export default {
|
||||
chat.private !== true
|
||||
).length;
|
||||
},
|
||||
hasUserReadMessage(createdAt, contactLastSeen) {
|
||||
return !(contactLastSeen - createdAt < 0);
|
||||
},
|
||||
readMessages(m) {
|
||||
return m.messages.filter(
|
||||
chat => chat.created_at * 1000 <= m.agent_last_seen_at * 1000
|
||||
|
||||
@@ -26,4 +26,11 @@ describe('#conversationMixin', () => {
|
||||
conversationMixin.methods.unReadMessages(conversationFixture.conversation)
|
||||
).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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -199,6 +199,10 @@ const actions = {
|
||||
}
|
||||
},
|
||||
|
||||
updateConversationRead({ commit }, timestamp) {
|
||||
commit(types.SET_CONVERSATION_LAST_SEEN, timestamp);
|
||||
},
|
||||
|
||||
updateMessage({ commit }, message) {
|
||||
commit(types.ADD_MESSAGE, message);
|
||||
},
|
||||
|
||||
@@ -91,6 +91,9 @@ const getters = {
|
||||
value => value.id === Number(conversationId)
|
||||
);
|
||||
},
|
||||
getConversationLastSeen: _state => {
|
||||
return _state.conversationLastSeen;
|
||||
},
|
||||
};
|
||||
|
||||
export default getters;
|
||||
|
||||
@@ -13,6 +13,7 @@ const state = {
|
||||
currentInbox: null,
|
||||
selectedChatId: null,
|
||||
appliedFilters: [],
|
||||
conversationLastSeen: null,
|
||||
};
|
||||
|
||||
// mutations
|
||||
@@ -33,6 +34,9 @@ export const mutations = {
|
||||
_state.allConversations = [];
|
||||
_state.selectedChatId = null;
|
||||
},
|
||||
[types.SET_CONVERSATION_LAST_SEEN](_state, timestamp) {
|
||||
_state.conversationLastSeen = timestamp;
|
||||
},
|
||||
[types.SET_ALL_MESSAGES_LOADED](_state) {
|
||||
const [chat] = getSelectedChatConversation(_state);
|
||||
Vue.set(chat, 'allMessagesLoaded', true);
|
||||
|
||||
@@ -372,6 +372,15 @@ describe('#actions', () => {
|
||||
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', () => {
|
||||
|
||||
@@ -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', () => {
|
||||
it('Returns cc in last email', () => {
|
||||
const state = {};
|
||||
|
||||
@@ -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', () => {
|
||||
it('update conversation custom attributes', () => {
|
||||
const custom_attributes = { order_id: 1001 };
|
||||
|
||||
@@ -21,6 +21,7 @@ export default {
|
||||
CLEAR_CONTACT_CONVERSATIONS: 'CLEAR_CONTACT_CONVERSATIONS',
|
||||
SET_CONVERSATION_FILTERS: 'SET_CONVERSATION_FILTERS',
|
||||
CLEAR_CONVERSATION_FILTERS: 'CLEAR_CONVERSATION_FILTERS',
|
||||
SET_CONVERSATION_LAST_SEEN: 'SET_CONVERSATION_LAST_SEEN',
|
||||
|
||||
SET_CURRENT_CHAT_WINDOW: 'SET_CURRENT_CHAT_WINDOW',
|
||||
CLEAR_CURRENT_CHAT_WINDOW: 'CLEAR_CURRENT_CHAT_WINDOW',
|
||||
|
||||
Reference in New Issue
Block a user