feat: add unread indicator
This also defers the marking of a message as unread until the user has actually scrolled to it
This commit is contained in:
@@ -0,0 +1,44 @@
|
||||
<script setup>
|
||||
import { vIntersectionObserver } from '@vueuse/components';
|
||||
import { computed } from 'vue';
|
||||
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
variant: {
|
||||
type: String,
|
||||
default: 'primary',
|
||||
validator: value => ['primary', 'secondary'].includes(value),
|
||||
},
|
||||
});
|
||||
|
||||
const emit = defineEmits(['click', 'intersect']);
|
||||
|
||||
function onIntersectionObserver([entry]) {
|
||||
const isVisible = entry?.isIntersecting || false;
|
||||
emit('intersect', isVisible);
|
||||
}
|
||||
|
||||
const classToApply = computed(() => {
|
||||
return {
|
||||
'bg-n-solid-blue text-n-blue-text': props.variant === 'primary',
|
||||
'shadow-sm border border-n-weak bg-n-solid-2 text-n-slate-11':
|
||||
props.variant === 'secondary',
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<div
|
||||
v-intersection-observer="onIntersectionObserver"
|
||||
:class="classToApply"
|
||||
class="flex py-1.5 pr-2 pl-3 rounded-full text-xs font-semibold my-2.5 mx-auto"
|
||||
@click="emit('click')"
|
||||
>
|
||||
{{ label }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -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 {
|
||||
</template>
|
||||
<template #unreadBadge>
|
||||
<li v-show="unreadMessageCount != 0" class="unread--toast">
|
||||
<span>
|
||||
{{ unreadMessageLabel }}
|
||||
</span>
|
||||
<UnreadIndicator
|
||||
:label="unreadMessageLabel"
|
||||
variant="primary"
|
||||
class="mx-auto"
|
||||
@intersect="onUnreadBadgeIntersect"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
<template #after>
|
||||
@@ -598,6 +632,13 @@ export default {
|
||||
class="flex items-center justify-center gap-1 absolute w-full -top-7 h-0"
|
||||
>
|
||||
<TypingIndicator />
|
||||
<UnreadIndicator
|
||||
v-if="showFloatingUnreadIndicator && unreadMessageCount > 0"
|
||||
variant="secondary"
|
||||
class="cursor-pointer"
|
||||
:label="unreadMessageLabel"
|
||||
@click="scrollToBottom"
|
||||
/>
|
||||
</div>
|
||||
<ReplyBox
|
||||
v-model:popout-reply-box="isPopOutReplyBox"
|
||||
|
||||
Reference in New Issue
Block a user