@qor/chat-layer

Интеграция

Интеграция @qor/chat-layer с внешним приложением — регистрация пользователей, создание чатов, хуки.

Жизненный цикл подключения

App запускается
  │
  ├─ Plugin: socket.client.ts → io() → создание Socket.IO клиента
  │
  └─ Plugin: chat-auth.client.ts → watch(auth.isAuthenticated)
       │
       ├─ isAuthenticated = true → connect(userId) → socket.connect()
       │    │
       │    └─ Socket.IO handshake: auth: { token, userId }
       │         │
       │         ├─ Сервер: verifyChatToken(token) → найти ChatUser
       │         │    │
       │         │    ├─ OK → socket.join(rooms), online status
       │         │    └─ FAIL → disconnect
       │         │
       │         └─ Клиент: loadConversations() → GET /api/v1/chat/conversations
       │
       └─ isAuthenticated = false → disconnect()

Регистрация пользователей

Chat layer хранит свои записи ChatUser. Каждый пользователь из внешней системы должен быть зарегистрирован по externalId (UUID из auth layer).

Вариант 1: Автоматически при создании диалога

При вызове POST /api/v1/chat/conversations с participantIds сервер выполняет upsert для каждого участника:

// Сервер автоматически создаст ChatUser, если его нет
await $fetch('/api/v1/chat/conversations', {
  method: 'POST',
  body: {
    type: 'single',
    participantIds: ['user-uuid-1', 'user-uuid-2'],
  },
})

Вариант 2: Через webhook

Внешняя система вызывает webhook при создании/обновлении пользователя:

// POST /api/v1/chat/users/webhook
await $fetch('https://chat.api.example.com/api/v1/chat/users/webhook', {
  method: 'POST',
  body: {
    externalId: 'user-uuid',
    displayName: 'Иван Петров',
    avatarUrl: 'https://cdn.example.com/avatars/ivan.jpg',
  },
})

Вариант 3: При первом подключении к Socket.IO

Сервер создаёт ChatUser при первом handshake, если пользователь с таким externalId не найден.

Создание диалогов из внешнего приложения

Личный чат

// service-care или другое приложение
async function openChat(targetUserId: string) {
  const conv = await $fetch('/api/v1/chat/conversations', {
    method: 'POST',
    body: {
      type: 'single',
      participantIds: [currentUserId, targetUserId],
    },
  })

  // Редирект на чат
  navigateTo(`/chats/${conv.id}`)
}

Групповой чат

// Создание группы (например, для инцидента)
const group = await $fetch('/api/v1/chat/conversations', {
  method: 'POST',
  body: {
    type: 'group',
    name: 'Инцидент #1234',
    participantIds: [operatorId, dispatcherId, guardId],
  },
})

Интеграция с Auth Layer

Chat layer расширяет @qor/auth-layer и ожидает наличия composable useAuth():

// Автоматически в chat-auth.client.ts
const auth = useAuth()

watch(
  () => [auth.isAuthenticated.value, auth.user.value?.id],
  ([isAuth, userId]) => {
    if (isAuth && userId) connect(userId)
    else disconnect()
  },
  { immediate: true },
)

Требования к Auth Layer

ПолеОписание
auth.isAuthenticatedRef<boolean> — статус авторизации
auth.user.idstring — UUID пользователя (используется как externalId)
localStorage['qor-auth-token']Токен для Socket.IO handshake

Интеграция с @qor/ui

Chat layer использует компоненты Nuxt UI 4 с темой @qor/ui. Стили пузырей сообщений адаптированы под Glass Design System:

  • Свои сообщения: bg-primary с text-white
  • Чужие сообщения: bg-elevated/80 dark:bg-neutral-800/80
  • Тема chatMessage облегчена: без gradient/backdrop-blur/border

Кастомизация

Переопределение компонентов

Любой компонент можно переопределить, создав файл с тем же именем в app/components/:

app/components/ChatEmpty.vue
<!-- Переопределяет ChatEmpty из chat-layer -->
<template>
  <div class="flex flex-col items-center justify-center h-full">
    <img src="/custom-empty.svg" class="w-48">
    <p>Нет активных чатов</p>
  </div>
</template>

Кастомные стили

app/assets/css/main.css
@import "tailwindcss";
@import "@nuxt/ui";

/* Переопределение стилей чата */
.chat-bubble-own {
  background-color: var(--color-primary-500);
}

Расширение типов

app/types/chat-extended.ts
import type { ChatMessage } from '@qor/chat-layer/app/types/chat'

// Расширение сообщения кастомными полями
interface ExtendedMessage extends ChatMessage {
  priority?: 'normal' | 'urgent'
  incidentId?: string
}

Playground

Для локальной разработки используйте playground:

# Из корня монорепо
pnpm dev:chat

# Или из директории слоя
cd layers/chat
pnpm dev

Playground запускается на порту 3335 с ssr: false и включает:

  • Auth layer с OTP-авторизацией
  • Полный набор компонентов чата
  • Локальный Socket.IO сервер + SQLite

Тестирование между пользователями

  1. Откройте playground в двух разных браузерах (или обычный + инкогнито)
  2. Авторизуйтесь разными пользователями
  3. Создайте диалог через REST API или webhook
  4. Обменивайтесь сообщениями в реальном времени