feat: Add a read indicator for web-widget channel (#4224)
Co-authored-by: Pranav Raj S <pranav@chatwoot.com>
This commit is contained in:
parent
f2f0d466f2
commit
2b2252b66e
15 changed files with 109 additions and 1 deletions
|
@ -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;
|
||||||
},
|
},
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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 => {
|
||||||
|
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -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);
|
||||||
},
|
},
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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', () => {
|
||||||
|
|
|
@ -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 = {};
|
||||||
|
|
|
@ -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 };
|
||||||
|
|
|
@ -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',
|
||||||
|
|
|
@ -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",
|
||||||
|
|
Loading…
Reference in a new issue