Chat
A full working chat example combining VxChatMessage and VxChatInput. Scroll to the top to load older messages.
Features
- Scroll-to-top loading — scroll up to load older messages with a spinner, preserves scroll position, shows "Beginning of conversation" when history is exhausted
- Send messages — type and press Enter or click send
- Auto-grouping — consecutive messages from the same sender at the same time are grouped with Telegram-style border radiuses
- Reply — right-click a message, click Reply, shows preview in input bar
- Copy — right-click, click Copy to clipboard
- Delete — right-click, click Delete removes the message
- Status progression — sent messages animate through sending, sent, delivered, read
- Auto-reply — a random reply comes back after 2-4 seconds
- File attachments — drop files on the input bar to upload, click files to download
- Photo/File action — click paperclip, then Photo or File opens native file picker
- Emoji picker — click the smile icon to pick emojis
- Voice button — shows when input is empty
vue
<script setup>
import { ref, reactive, nextTick } from 'vue'
const chatRef = ref(null)
const inputText = ref('')
const replyTo = ref(null)
const messages = ref([])
const loadingOlder = ref(false)
const hasMoreHistory = ref(true)
function scrollToBottom() {
nextTick(() => {
if (chatRef.value) chatRef.value.scrollTop = chatRef.value.scrollHeight
})
}
// Scroll-to-top loading
function onScroll() {
const el = chatRef.value
if (!el || loadingOlder.value || !hasMoreHistory.value) return
if (el.scrollTop <= 40) {
loadOlderMessages()
}
}
function loadOlderMessages() {
loadingOlder.value = true
const el = chatRef.value
const prevHeight = el ? el.scrollHeight : 0
fetchHistory().then((older) => {
if (older.length) {
messages.value.unshift(...older)
// Preserve scroll position
nextTick(() => {
if (el) el.scrollTop = el.scrollHeight - prevHeight
loadingOlder.value = false
})
} else {
hasMoreHistory.value = false
loadingOlder.value = false
}
})
}
function onSend(text) {
const msg = reactive({
id: nextId++,
name: 'You',
content: text,
time: new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }),
sent: true,
status: 'sending',
})
messages.value.push(msg)
scrollToBottom()
setTimeout(() => { msg.status = 'sent' }, 600)
setTimeout(() => { msg.status = 'delivered' }, 1500)
setTimeout(() => { msg.status = 'read' }, 3000)
}
</script>
<template>
<div style="display: flex; flex-direction: column; height: 500px;">
<div ref="chatRef" style="flex: 1; overflow-y: auto;" @scroll="onScroll">
<!-- Loader at top -->
<div v-if="loadingOlder" class="chat-loader">
<div class="chat-loader__spinner" />
</div>
<div v-else-if="!hasMoreHistory" class="chat-history-end">
Beginning of conversation
</div>
<VxChatMessage
v-for="(msg, i) in messages"
:key="msg.id"
v-bind="msg"
:prev-time="messages[i - 1]?.time"
:prev-sent="messages[i - 1]?.sent"
:next-time="messages[i + 1]?.time"
:next-sent="messages[i + 1]?.sent"
@action="(key, item) => onAction(key, item, msg)"
/>
</div>
<VxChatInput
v-model="inputText"
variant="outline"
:reply-to="replyTo"
@send="onSend"
@cancel-reply="replyTo = null"
@drop="onDrop"
/>
</div>
</template>