Files
libredesk/frontend/src/components/message/MessageList.vue
Abhinav Raut 0c01b53b09 feat(conversations): add trigram index for searching ref numbers
feat(messages): add trigram index for text content search
- feat: UI animations for conversation and messages list.
- Simplify websocket updates.
2025-01-19 23:10:53 +05:30

158 lines
5.1 KiB
Vue

<template>
<div class="flex flex-col relative h-full">
<div ref="threadEl" class="flex-1 overflow-y-auto" @scroll="handleScroll">
<div class="min-h-full pb-20 px-4">
<DotLoader v-if="conversationStore.messages.loading" />
<div
class="text-center mt-3"
v-if="conversationStore.messages.hasMore && !conversationStore.messages.loading"
>
<Button
size="sm"
variant="outline"
@click="conversationStore.fetchNextMessages"
class="transition-all duration-200 hover:bg-gray-100 dark:hover:bg-gray-700 hover:scale-105 active:scale-95"
>
<RefreshCw size="17" class="mr-2" />
Load more
</Button>
</div>
<TransitionGroup
enter-active-class="animate-slide-in"
leave-active-class="animate-slide-out"
tag="div"
class="space-y-4"
>
<div
v-for="message in conversationStore.conversationMessages"
:key="message.uuid"
:class="message.type === 'activity' ? 'my-2' : 'my-4'"
>
<div v-if="!message.private">
<ContactMessageBubble :message="message" v-if="message.type === 'incoming'" />
<AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" />
</div>
<div v-else-if="isPrivateNote(message)">
<AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" />
</div>
<div v-else-if="message.type === 'activity'">
<ActivityMessageBubble :message="message" />
</div>
</div>
</TransitionGroup>
</div>
</div>
<!-- Sticky container for the scroll arrow -->
<Transition
enter-active-class="transition ease-out duration-200"
enter-from-class="opacity-0 translate-y-1"
enter-to-class="opacity-100 translate-y-0"
leave-active-class="transition ease-in duration-150"
leave-from-class="opacity-100 translate-y-0"
leave-to-class="opacity-0 translate-y-1"
>
<div v-show="!isAtBottom" class="absolute bottom-6 right-6 z-10">
<button
@click="handleScrollToBottom"
class="w-10 h-10 rounded-full flex items-center justify-center shadow-lg border bg-white text-primary transition-colors duration-200 hover:bg-gray-100"
>
<ChevronDown size="18" />
</button>
<span
v-if="unReadMessages > 0"
class="absolute -top-1 -right-1 min-w-[20px] h-5 px-1.5 rounded-full bg-green-500 text-secondary text-xs font-medium flex items-center justify-center"
>
{{ unReadMessages }}
</span>
</div>
</Transition>
</div>
</template>
<script setup>
import { ref, onMounted, watch } from 'vue'
import ContactMessageBubble from './ContactMessageBubble.vue'
import ActivityMessageBubble from './ActivityMessageBubble.vue'
import AgentMessageBubble from './AgentMessageBubble.vue'
import { DotLoader } from '@/components/ui/loader'
import { useConversationStore } from '@/stores/conversation'
import { useUserStore } from '@/stores/user'
import { Button } from '@/components/ui/button'
import { RefreshCw, ChevronDown } from 'lucide-vue-next'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
const conversationStore = useConversationStore()
const userStore = useUserStore()
const threadEl = ref(null)
const emitter = useEmitter()
const isAtBottom = ref(true)
const unReadMessages = ref(0)
const currentConversationUUID = ref('')
const checkIfAtBottom = () => {
const thread = threadEl.value
if (thread) {
const tolerance = 100
const isBottom = thread.scrollHeight - thread.scrollTop - thread.clientHeight <= tolerance
isAtBottom.value = isBottom
}
}
const handleScroll = () => {
checkIfAtBottom()
}
const handleScrollToBottom = () => {
unReadMessages.value = 0
scrollToBottom()
}
const scrollToBottom = () => {
setTimeout(() => {
const thread = threadEl.value
if (thread) {
thread.scrollTop = thread.scrollHeight
checkIfAtBottom()
}
}, 50)
}
onMounted(() => {
checkIfAtBottom()
handleNewMessage()
})
const handleNewMessage = () => {
emitter.on(EMITTER_EVENTS.NEW_MESSAGE, (data) => {
if (data.conversation_uuid === conversationStore.current.uuid) {
if (data.message.sender_id === userStore.userID) {
scrollToBottom()
} else if (!isAtBottom.value) {
unReadMessages.value++
}
}
})
}
watch(
() => conversationStore.conversationMessages,
(messages) => {
// Scroll to bottom when conversation changes and there are new messages.
// New messages on next db page should not scroll to bottom.
if (messages.length > 0 && currentConversationUUID.value !== conversationStore.current?.uuid) {
currentConversationUUID.value = conversationStore.current.uuid
unReadMessages.value = 0
scrollToBottom()
}
}
)
const isPrivateNote = (message) => {
return message.type === 'outgoing' && message.private
}
</script>