Skip to content

Chat ​

Полностью рабочий пример чата, объединяющий VxChatMessage и VxChatInput. Прокрутите наверх, чтобы загрузить более старые сообщения.

T
Team Chat
Alice, Bob, You
Alice
Hey! How's the project going?
Alice
I saw your PR this morning, looks great10:00 AM
Thanks! Almost done with the chat components
Just finishing up the input bar with emoji picker and file drop10:02 AM
You
Alice
Alice
Nice! Can you share the design file?10:04 AM
chat-design-v3.fig14.2 MB
Sure, here it is10:05 AM
You
Alice
Alice
meeting-notes.pdf2.4 MB
Perfect, downloading now10:06 AM
Bob
Bob
Hey team, just joined! What did I miss?10:08 AM
Welcome Bob! We're discussing the new chat UI10:09 AM
You

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>