From 8a0dc90a294cdf938a5e328acd93f5842ebd4d14 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Mon, 17 Mar 2025 17:41:45 +0530 Subject: [PATCH] feat: add unread indicator This also defers the marking of a message as unread until the user has actually scrolled to it --- .../Conversation/Chips/UnreadIndicator.vue | 44 +++++++++++++ .../widgets/conversation/MessagesView.vue | 61 ++++++++++++++++--- 2 files changed, 95 insertions(+), 10 deletions(-) create mode 100644 app/javascript/dashboard/components-next/Conversation/Chips/UnreadIndicator.vue diff --git a/app/javascript/dashboard/components-next/Conversation/Chips/UnreadIndicator.vue b/app/javascript/dashboard/components-next/Conversation/Chips/UnreadIndicator.vue new file mode 100644 index 000000000..66d89f0cb --- /dev/null +++ b/app/javascript/dashboard/components-next/Conversation/Chips/UnreadIndicator.vue @@ -0,0 +1,44 @@ + + + diff --git a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue index 00f2289ab..a8ccceeb8 100644 --- a/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue +++ b/app/javascript/dashboard/components/widgets/conversation/MessagesView.vue @@ -11,6 +11,7 @@ import ReplyBox from './ReplyBox.vue'; import Message from './Message.vue'; import NextMessageList from 'next/message/MessageList.vue'; import TypingIndicator from 'next/Conversation/Chips/TypingIndicator.vue'; +import UnreadIndicator from 'next/Conversation/Chips/UnreadIndicator.vue'; import ConversationLabelSuggestion from './conversation/LabelSuggestion.vue'; import Banner from 'dashboard/components/ui/Banner.vue'; @@ -43,6 +44,7 @@ export default { NextMessageList, ReplyBox, TypingIndicator, + UnreadIndicator, Banner, ConversationLabelSuggestion, }, @@ -110,6 +112,9 @@ export default { data() { return { isLoadingPrevious: true, + // this is only used to show a chip if the user has scrolled far enough + // we will hide this the moment the user reaches the start of the unread messages + showFloatingUnreadIndicator: false, heightBeforeLoad: null, conversationPanel: null, hasUserScrolled: false, @@ -230,9 +235,9 @@ export default { this.unreadMessageCount > 9 ? '9+' : this.unreadMessageCount; const label = this.unreadMessageCount > 1 - ? 'CONVERSATION.UNREAD_MESSAGES' - : 'CONVERSATION.UNREAD_MESSAGE'; - return `${count} ${this.$t(label)}`; + ? this.$t('CONVERSATION.UNREAD_MESSAGES') + : this.$t('CONVERSATION.UNREAD_MESSAGE'); + return `${count} ${label}`; }, isInstagramDM() { return this.conversationType === 'instagram_direct_message'; @@ -344,7 +349,6 @@ export default { this.scrollToBottomIfNotScrolled(); } }); - this.makeMessagesRead(); }, addScrollListener() { this.conversationPanel = this.$el.querySelector('.conversation-panel'); @@ -356,7 +360,7 @@ export default { removeScrollListener() { this.conversationPanel.removeEventListener('scroll', this.handleScroll); }, - scrollToBottomIfNotScrolled() { + isNearBottom(offset = 200) { // In case the user has already scrolled to a point in the message, we should // not scroll to the bottom against the intent of the user. const clientHeight = this.conversationPanel.clientHeight; @@ -371,13 +375,20 @@ export default { // if the user is at the bottom or close to the bottom, we can skip scrolling // we add a 200px margin, this has enough space to accomodate new messages // while also resetting position to the bottom if the user has scrolled but not significantly - const isNearBottom = scrollTop > scrollHeight - clientHeight - 200; + return scrollTop > scrollHeight - clientHeight - offset; + }, + scrollToBottomIfNotScrolled() { + const isNearBottom = this.isNearBottom(); - if (this.hasUserScrolled && !isNearBottom) return; + if (this.hasUserScrolled && !isNearBottom) { + this.showFloatingUnreadIndicator = true; + return; + } this.scrollToBottom(); }, scrollToBottom() { + this.showFloatingUnreadIndicator = false; this.isProgrammaticScroll = true; let relevantMessages = []; @@ -404,6 +415,10 @@ export default { ).slice(-1); } + if (this.unreadMessageCount !== 0) { + this.makeMessagesRead(); + } + this.conversationPanel.scrollTop = calculateScrollTop( this.conversationPanel.scrollHeight, this.$el.scrollHeight, @@ -457,6 +472,14 @@ export default { } else { this.hasUserScrolled = true; } + + // in case the user has scrolled to the bottom manually + // we trigger the makeMessagesRead method if there are unread messages + // TODO: debounce this check to ensure this does not affect performance + if (this.isNearBottom(50) && this.unreadMessageCount !== 0) { + this.makeMessagesRead(); + } + emitter.emit(BUS_EVENTS.ON_MESSAGE_LIST_SCROLL); this.fetchPreviousMessages(e.target.scrollTop); }, @@ -464,6 +487,14 @@ export default { makeMessagesRead() { this.$store.dispatch('markMessagesRead', { id: this.currentChat.id }); }, + // when the fixed unread badge is visible + // we trigger the makeMessagesRead method + onUnreadBadgeIntersect(visible) { + if (visible) { + this.showFloatingUnreadIndicator = false; + this.makeMessagesRead(); + } + }, getInReplyToMessage(parentMessage) { if (!parentMessage) return {}; const inReplyToMessageId = parentMessage.content_attributes?.in_reply_to; @@ -520,9 +551,12 @@ export default {