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.
This commit is contained in:
Abhinav Raut
2025-01-19 23:10:24 +05:30
parent 29c341d5f3
commit 0c01b53b09
62 changed files with 1809 additions and 1181 deletions

View File

@@ -1,44 +1,74 @@
<template>
<div ref="threadEl" class="overflow-y-scroll relative h-full" @scroll="handleScroll">
<div class="min-h-full relative pb-20">
<div class="text-center mt-3" v-if="conversationStore.messages.hasMore && !conversationStore.messages.loading">
<Button variant="ghost" @click="conversationStore.fetchNextMessages">
<RefreshCw size="17" class="mr-2" />
Load more
</Button>
</div>
<div v-for="message in conversationStore.conversationMessages" :key="message.uuid"
:class="message.type === 'activity' ? 'm-4' : 'm-6'">
<div v-if="conversationStore.messages.loading">
<MessagesSkeleton></MessagesSkeleton>
<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>
<div v-else>
<div v-if="!message.private">
<ContactMessageBubble :message="message" v-if="message.type === 'incoming'" />
<AgentMessageBubble :message="message" v-if="message.type === 'outgoing'" />
<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>
<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 -->
<div v-show="!isAtBottom" class="sticky bottom-6 flex justify-end px-6">
<div class="relative">
<button @click="handleScrollToBottom" class="w-8 h-8 rounded-full flex items-center justify-center shadow">
<ArrowDown size="20" />
<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-primary text-white text-xs font-medium flex items-center justify-center">
<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>
</div>
</Transition>
</div>
</template>
@@ -47,11 +77,11 @@ import { ref, onMounted, watch } from 'vue'
import ContactMessageBubble from './ContactMessageBubble.vue'
import ActivityMessageBubble from './ActivityMessageBubble.vue'
import AgentMessageBubble from './AgentMessageBubble.vue'
import MessagesSkeleton from './MessagesSkeleton.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, ArrowDown } from 'lucide-vue-next'
import { RefreshCw, ChevronDown } from 'lucide-vue-next'
import { useEmitter } from '@/composables/useEmitter'
import { EMITTER_EVENTS } from '@/constants/emitterEvents'
@@ -61,6 +91,7 @@ const threadEl = ref(null)
const emitter = useEmitter()
const isAtBottom = ref(true)
const unReadMessages = ref(0)
const currentConversationUUID = ref('')
const checkIfAtBottom = () => {
const thread = threadEl.value
@@ -82,7 +113,6 @@ const handleScrollToBottom = () => {
const scrollToBottom = () => {
setTimeout(() => {
console.log('scrolling..')
const thread = threadEl.value
if (thread) {
thread.scrollTop = thread.scrollHeight
@@ -92,8 +122,11 @@ const scrollToBottom = () => {
}
onMounted(() => {
scrollToBottom()
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) {
@@ -103,18 +136,22 @@ onMounted(() => {
}
}
})
})
}
// On conversation change scroll to the bottom
watch(
() => conversationStore.current.uuid,
() => {
unReadMessages.value = 0
scrollToBottom()
() => 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>
</script>