Chat
Полностью рабочий пример чата, объединяющий VxChatMessage и VxChatInput. Прокрутите наверх, чтобы загрузить более старые сообщения.
Features
- Scroll-to-top loading — прокручивайте наверх, чтобы подгружать старые сообщения со спиннером, позиция скролла сохраняется, при исчерпании истории показывается "Beginning of conversation"
- Send messages — введите текст и нажмите Enter или кликните отправить
- Auto-grouping — подряд идущие сообщения одного отправителя с одинаковым временем группируются со скруглениями в стиле Telegram
- Reply — правый клик по сообщению, нажмите Reply — появляется превью в поле ввода
- Copy — правый клик, нажмите Copy для копирования в буфер обмена
- Delete — правый клик, нажмите Delete для удаления сообщения
- Status progression — отправленные сообщения анимированно проходят sending, sent, delivered, read
- Auto-reply — случайный ответ приходит через 2–4 секунды
- File attachments — перетащите файлы на поле ввода для загрузки, кликните по файлу для скачивания
- Photo/File action — кликните по скрепке, затем Photo или File открывает системный выбор файлов
- Emoji picker — кликните по иконке смайла для выбора эмодзи
- Voice button — показывается, когда поле ввода пустое
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>