mirror of
https://github.com/abhinavxd/libredesk.git
synced 2025-11-04 22:13:25 +00:00
feat(messages): add trigram index for text content search - feat: UI animations for conversation and messages list. - Simplify websocket updates.
158 lines
5.1 KiB
Vue
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>
|