Corp/CHAT_PAGE_RENDERING_ARCHITECTURE.md

42 KiB
Raw Blame History

Архитектура рендеринга и наполнения страницы чата

📋 Оглавление

  1. Общая схема
  2. Серверная часть (Backend)
  3. Шаблон страницы (Template)
  4. JavaScript инициализация
  5. WebSocket соединение
  6. Рендеринг сообщений
  7. Поток данных

🎯 Общая схема

Страница чата работает по следующей архитектуре:

HTTP Request → Django View → HTML Template → DOM Ready → WebSocket Connect → Load Messages
                    ↓              ↓              ↓              ↓                  ↓
              Context Data   Initial HTML    Init JS       Real-time        Render to DOM

🔧 Серверная часть (Backend)

1. ChatDetailView (communications/views.py)

Класс: ChatDetailView(LoginRequiredMixin, DetailView, FormView)

Ответственность:

  • Проверка прав доступа пользователя к чату
  • Подготовка контекста для шаблона
  • Установка заголовков для отключения кэширования

Ключевые методы:

def get_object(self):
    """Получает объект чата с проверкой прав доступа"""
    # Фильтрует по типу чата: global/department/private/group
    # Проверяет membership для приватных чатов
def get_context_data(self, **kwargs):
    """Подготавливает контекст для шаблона"""
    # 1. Проверка доступа через _user_has_access()
    # 2. Загрузка сообщений (последние 50, в обратном порядке)
    # 3. Определение has_more (есть ли еще история)
    # 4. Вычисление last_read_at и first_unread_id
    # 5. Проверка прав на отправку сообщений

Контекст, передаваемый в шаблон:

{
    'chat': Chat,                    # Объект чата
    'messages': [...],               # Последние 50 сообщений (УСТАРЕЛО - не рендерятся)
    'messages_has_more': bool,       # Есть ли еще история
    'messages_oldest_id': int,       # ID самого старого сообщения
    'messages_oldest_ts': int,       # Timestamp самого старого сообщения
    'messages_page_size': 50,        # Размер страницы
    'participants': QuerySet,        # Участники чата
    'last_read_at': datetime,        # Время последнего прочтения
    'first_unread_id': int,          # ID первого непрочитанного
    'can_send_messages': bool,       # Может ли отправлять сообщения
    'is_announcement_creator': bool  # Создатель объявления
}

Отключение кэширования:

def dispatch(self, request, *args, **kwargs):
    response = super().dispatch(request, *args, **kwargs)
    response['Cache-Control'] = 'no-cache, no-store, must-revalidate, max-age=0'
    response['Pragma'] = 'no-cache'
    response['Expires'] = '0'
    return response

🖼️ Шаблон страницы (Template)

templates/communications/chat_detail.html

Структура DOM:

<div class="container chat-root" id="chatDetailApp" data-chat-id="..." data-user-id="...">
  <div class="row gx-0">
    <div class="col-12 chat-col">
      
      <!-- 1. Заголовок чата -->
      <div class="section-header">
        <!-- Иконка, название, кнопки управления -->
      </div>
      
      <!-- 2. Корпус чата -->
      <article class="card chat-box">
        
        <!-- 2.1 Предупреждения (блокировка, etc) -->
        <div class="alert">...</div>
        
        <!-- 2.2 Кнопка "вниз" -->
        <button id="scrollBtn" class="scroll-to-bottom">...</button>
        
        <!-- 2.3 Контейнер сообщений -->
        <div id="chatScroll" class="chat-scroll" 
             data-me-id="..."
             data-chat-id="..."
             data-fetch-url="..."
             data-oldest-id="..."
             data-has-more="...">
          
          <!-- Индикатор загрузки истории (при скролле вверх) -->
          <div class="history-loader d-none">...</div>
          
          <!-- Индикатор первоначальной загрузки -->
          <div class="initial-loader" id="initialLoader">
            <div class="spinner-border">...</div>
            <p>Загружаем сообщения...</p>
          </div>
          
          <!-- Сообщения добавляются через JavaScript -->
        </div>
        
        <!-- 2.4 Индикатор "печатает..." -->
        <div id="typing" class="d-none">...</div>
        
        <!-- 2.5 Композер (форма ввода) -->
        <div class="chat-composer">
          <form id="chatForm">
            <!-- Кнопки вложений, эмодзи -->
            <!-- Поле ввода textarea -->
            <!-- Кнопка отправки -->
          </form>
        </div>
        
      </article>
    </div>
  </div>
</div>

Data-атрибуты контейнера:

<div id="chatDetailApp"
     data-chat-id="{{ chat.id }}"
     data-user-id="{{ user.id }}"
     data-user-name="{{ user.get_full_name }}"
     data-user-avatar="{{ user.avatar.url }}"
     data-upload-url="{% url 'api:v1:upload_message' %}"
     data-messages-url="{% url 'api:v1:chat_messages' chat.pk %}"
     data-mark-read-url="{% url 'communications:chat_mark_read' chat.pk %}"
     data-edit-url-template="/api/v1/communications/messages/{id}/edit/">

Важно: Сообщения из контекста messages больше НЕ рендерятся в шаблоне!
Вместо этого используется WebSocket для загрузки initial_messages.


⚙️ JavaScript инициализация

static/js/pages/chatDetail.js

Главный файл инициализации страницы чата.

Этапы инициализации:

// 1. Загрузка модулей
const modules = await Promise.all([
  import('../components/chatMarkRead.js'),
  import('../components/chatComposer.js'),
  import('../components/chatHistoryLoader.js'),
  import('../components/chatFormManager.js'),
  import('../components/messageRenderer.js'),
  import('../components/initialMessagesLoader.js')  // Не используется
]);

// 2. Ожидание DOM Ready
document.addEventListener('DOMContentLoaded', initWhenReady);

// 3. Чтение конфигурации из data-атрибутов
const config = {
  chatId: Number(appContainer.dataset.chatId),
  userId: Number(appContainer.dataset.userId),
  uploadUrl: appContainer.dataset.uploadUrl,
  messagesUrl: appContainer.dataset.messagesUrl,
  // ... и другие
};

// 4. Ожидание готовности WebSocket
waitForWebSocket().then((userWs) => {
  initializeComponents(config, userWs);
});

Инициализация компонентов:

function initializeComponents(config, userWs) {
  // 1. FormManager - управление формой
  const formManager = initChatFormManager({ ... });
  
  // 2. MarkRead - отметка прочитанных
  const markReadApi = initChatMarkRead({ ... });
  
  // 3. Composer - композер сообщений
  const composer = initChatComposer({ ... });
  
  // 4. HistoryLoader - загрузка старых сообщений
  const historyLoader = initChatHistoryLoader({ ... });
  
  // 5. MessageRenderer - рендеринг сообщений
  const messageRenderer = new MessageRenderer({ ... });
  
  // 6. Конфигурация WebSocket
  userWs.configure({
    scrollContainerId: 'chatScroll',
    messageRenderer: messageRenderer,
    markReadApi: markReadApi,
    // ...
  });
  
  // 7. Открытие чата в WebSocket
  userWs.openChat(config.chatId, true);  // true = загрузить историю
}

🌐 WebSocket соединение

static/js/components/userWebSocket.js

Единое WebSocket соединение для всех real-time обновлений.

Инициализация в base.html:

// templates/base.html
const userWs = initUserWebSocket({
  userId: {{ user.id }},
  badgeManager: badgeManager,
  onListUpdate: (data) => { /* ... */ }
});

window.userWebSocket = userWs;

// Уведомляем модули о готовности
window.dispatchEvent(new CustomEvent('user:ws-ready', {
  detail: { api: userWs }
}));

Подключение:

const proto = location.protocol === 'https:' ? 'wss' : 'ws';
const wsUrl = `${proto}://${location.host}/ws/`;
const ws = new WebSocket(wsUrl);

Открытие чата:

function openChat(chatId, loadHistory = false) {
  send({
    action: 'open_chat',
    chat_id: chatId,
    load_history: loadHistory
  });
}

📨 Загрузка сообщений через WebSocket

Backend: communications/consumers.py

ChatConsumer.connect():

async def connect(self):
    # 1. Проверка аутентификации
    user = self.scope.get("user")
    
    # 2. Проверка прав доступа к чату
    chat = await self._get_chat(self.chat_id)
    if not await self._user_can_access(chat, user):
        await self.close(code=4403)
        return
    
    # 3. Подключение к группе
    await self.channel_layer.group_add(self.group_name, self.channel_name)
    await self.accept()
    
    # 4. Отправка начальной истории
    await self._send_initial_messages()
    
    # 5. Отметка как прочитанное
    await self._mark_read(self.chat, user)
    
    # 6. Запуск ping цикла
    self.ping_task = asyncio.create_task(self._ping_loop())

_send_initial_messages():

async def _send_initial_messages(self):
    """Отправить начальную историю сообщений при подключении"""
    messages = await self._get_initial_messages(self.chat_id, limit=50)
    
    await self.send_json({
        "type": "initial_messages",
        "messages": messages
    })

@database_sync_to_async
def _get_initial_messages(self, chat_id: int, limit: int = 50):
    """Получить последние N сообщений из чата"""
    messages = Message.objects.filter(
        chat_id=chat_id
    ).select_related(
        'author'
    ).prefetch_related(
        'attachments',
        'reactions__user'
    ).order_by('-created_at')[:limit]
    
    # Возвращаем в прямом порядке (старые -> новые)
    return [serialize_message(msg) for msg in reversed(list(messages))]

Формат сообщения:

def serialize_message(m: Message) -> dict:
    return {
        "id": m.id,
        "chat_id": m.chat_id,
        "author_id": m.author_id,
        "author_name": m.author.get_full_name() or m.author.username,
        "avatar": m.author.avatar.url if hasattr(m.author, 'avatar') and m.author.avatar else None,
        "content": m.content,
        "created_ts": int(m.created_at.timestamp() * 1000),
        "is_edited": m.is_edited,
        "edited_at": m.edited_at.isoformat() if m.edited_at else None,
        "attachments": [
            {
                "id": att.id,
                "file_url": att.file.url if att.file else None,
                "file_name": att.file.name.split('/')[-1] if att.file else "file",
                "file_type": att.file_type or "unknown"
            }
            for att in m.attachments.all()
        ],
        "reactions_summary": m.get_reactions_summary(),
        "is_forwarded": m.is_forwarded,
        "forwarded_from": { ... } if m.is_forwarded else None,
        "reply_to": { ... } if m.reply_to else None,
        "poll": { ... } if hasattr(m, 'poll') else None,
    }

🎨 Рендеринг сообщений

static/js/components/messageRenderer.js

Класс MessageRenderer:

class MessageRenderer {
  constructor(config) {
    this.containerId = config.containerId;
    this.currentUserId = config.currentUserId;
    this.currentUserAvatar = config.currentUserAvatar;
    this.profileUrl = config.profileUrl;
    this.detailUrlTemplate = config.detailUrlTemplate;
  }
  
  /**
   * Рендерит массив сообщений (с разделителями дней)
   */
  renderMessages(messages) {
    const container = document.getElementById(this.containerId);
    
    let lastDay = null;
    messages.forEach(msg => {
      // Добавляем разделитель дня если нужно
      const msgDate = new Date(msg.created_ts);
      const msgDay = this.formatDay(msgDate);
      
      if (msgDay !== lastDay) {
        this.addDayDivider(msgDay, container);
        lastDay = msgDay;
      }
      
      this.renderMessage(msg, container);
    });
  }
  
  /**
   * Рендерит одно сообщение
   */
  renderMessage(msg, container) {
    // Проверка на дубликаты
    const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`);
    if (existingMessage) {
      console.log('Message already exists, skipping:', msg.id);
      return;
    }
    
    const isOwn = msg.author_id === this.currentUserId;
    const messageHtml = this.buildMessageHtml(msg, isOwn);
    
    // Добавляем в конец контейнера
    container.insertAdjacentHTML('beforeend', messageHtml);
  }
}

Структура HTML сообщения:

<div class="d-flex mb-3 msg"
     data-id="123"
     data-message-id="123"
     data-ts="1701734400000"
     data-author-id="5"
     data-is-edited="false"
     data-reactions='{"👍": 2, "❤️": 1}'>
  
  <!-- Аватар (только для чужих сообщений слева) -->
  <a class="me-2" href="/employees/5/">
    <span class="mini-ava border">
      <img src="/media/avatars/user.jpg" alt="">
    </span>
  </a>
  
  <div class="d-flex flex-column">
    <!-- Имя и время -->
    <div class="small text-secondary">
      <a href="/employees/5/">Иван Иванов</a> · <time>12:30</time>
    </div>
    
    <!-- Bubble с содержимым -->
    <div class="bubble bubble-other">
      
      <!-- Индикатор пересылки (если есть) -->
      <div class="forwarded-indicator">...</div>
      
      <!-- Ответ на сообщение (если есть) -->
      <div class="reply-reference">...</div>
      
      <!-- Текст сообщения -->
      Привет, как дела?
      
      <!-- Индикатор редактирования -->
      <span class="message-edited-indicator">(изменено)</span>
      
      <!-- Голосование (если есть) -->
      <div class="poll-widget">...</div>
      
      <!-- Вложения (если есть) -->
      <div class="message-attachments">...</div>
    </div>
    
    <!-- Реакции -->
    <div class="message-reactions-wrapper">...</div>
  </div>
  
  <!-- Аватар справа для своих сообщений -->
  <a class="ms-2" href="/employees/profile/">...</a>
</div>

🔄 Поток данных

1. Начальная загрузка страницы

┌─────────────────────────────────────────────────────────────────┐
│ HTTP GET /communications/chats/123/                             │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ ChatDetailView.get()                                            │
│ ├─ get_queryset() → Проверка доступа                           │
│ ├─ get_context_data()                                           │
│ │  ├─ messages = последние 50 (НЕ ИСПОЛЬЗУЕТСЯ)                │
│ │  ├─ has_more = True/False                                     │
│ │  ├─ last_read_at                                              │
│ │  └─ first_unread_id                                           │
│ └─ render(chat_detail.html)                                     │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ HTML с пустым контейнером #chatScroll                           │
│ + индикатор загрузки #initialLoader                             │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ JS: DOMContentLoaded                                            │
│ ├─ chatDetail.js → initWhenReady()                              │
│ ├─ Чтение config из data-атрибутов                             │
│ └─ waitForWebSocket()                                           │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ base.html → initUserWebSocket()                                 │
│ ├─ ws = new WebSocket('ws://host/ws/')                          │
│ └─ dispatchEvent('user:ws-ready')                               │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ chatDetail.js → initializeComponents()                          │
│ ├─ FormManager                                                  │
│ ├─ MarkRead                                                     │
│ ├─ Composer                                                     │
│ ├─ HistoryLoader                                                │
│ ├─ MessageRenderer                                              │
│ └─ userWs.openChat(chatId, true)                                │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ WebSocket → { action: 'open_chat', chat_id: 123 }              │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend: ChatConsumer.connect()                                 │
│ ├─ _send_initial_messages()                                     │
│ │  └─ _get_initial_messages(limit=50)                           │
│ └─ send_json({ type: 'initial_messages', messages: [...] })     │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ userWebSocket.js → handleInitialMessages()                      │
│ └─ messageRenderer.renderMessage() для каждого сообщения        │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ messageRenderer.js → renderMessage()                            │
│ ├─ Проверка дубликатов                                          │
│ ├─ Добавление разделителей дней                                 │
│ ├─ buildMessageHtml()                                           │
│ └─ container.insertAdjacentHTML('beforeend', html)              │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM обновлён → сообщения видны пользователю                     │
│ ├─ Скролл вниз                                                  │
│ ├─ Удаление #initialLoader                                      │
│ ├─ dispatchEvent('chat:initial-messages-loaded')                │
│ └─ Инициализация poll виджетов                                  │
└─────────────────────────────────────────────────────────────────┘

2. Получение нового сообщения

┌─────────────────────────────────────────────────────────────────┐
│ Другой пользователь отправляет сообщение                        │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend: ChatConsumer → channel_layer.group_send()              │
│ → broadcast в chat_123                                          │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ WebSocket → { type: 'new_message', message: {...} }            │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ userWebSocket.js → handleNewMessage()                           │
│ └─ messageRenderer.renderMessage(message)                       │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM обновлён → новое сообщение появилось в чате                 │
│ ├─ Автоскролл (если внизу)                                      │
│ └─ Отметка как прочитанное (если в viewport)                    │
└─────────────────────────────────────────────────────────────────┘

3. Загрузка истории (скролл вверх)

┌─────────────────────────────────────────────────────────────────┐
│ Пользователь скроллит вверх                                     │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ chatHistoryLoader.js → IntersectionObserver срабатывает         │
│ └─ loadMoreMessages()                                           │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ API Request: GET /api/v1/chats/123/messages/                    │
│ ?before_id=456&limit=50                                         │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ Backend: ChatMessagesAPIView.get()                              │
│ └─ Возвращает { messages: [...], has_more: true }              │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ chatHistoryLoader.js → прикрепляет сообщения в начало           │
│ └─ messageRenderer.prependMessage() для каждого                 │
└───────────────────────────────┬─────────────────────────────────┘
                                │
                                ▼
┌─────────────────────────────────────────────────────────────────┐
│ DOM обновлён → старые сообщения видны                           │
│ └─ Сохранение позиции скролла                                   │
└─────────────────────────────────────────────────────────────────┘

📊 Компоненты и их взаимодействие

┌─────────────────────────────────────────────────────────────────┐
│                         chatDetail.js                           │
│                    (Оркестратор компонентов)                    │
└────────────────┬────────────────────────────────┬───────────────┘
                 │                                │
     ┌───────────▼──────────┐         ┌──────────▼──────────┐
     │   userWebSocket.js   │◄────────┤  messageRenderer.js │
     │ (WebSocket соединение)│         │  (Рендеринг HTML)   │
     └───────────┬──────────┘         └─────────────────────┘
                 │
     ┌───────────▼──────────┐
     │  chatComposer.js     │
     │  (Форма отправки)    │
     └──────────────────────┘
                 │
     ┌───────────▼──────────┐
     │  chatMarkRead.js     │
     │  (Отметка прочитанных)│
     └──────────────────────┘
                 │
     ┌───────────▼──────────┐
     │chatHistoryLoader.js  │
     │ (Загрузка истории)   │
     └──────────────────────┘

🔑 Ключевые особенности

1. ✅ Отключен серверный рендеринг сообщений

  • Раньше: Django рендерил messages через {% for message in messages %}
  • Сейчас: WebSocket отправляет initial_messages после подключения
  • Преимущество: Единый механизм рендеринга (JS), меньше дублирования кода

2. ✅ Предотвращение дубликатов

const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`);
if (existingMessage) {
  console.log('Message already exists, skipping:', msg.id);
  return;
}

3. ✅ Автоматические разделители дней

let lastDay = null;
messages.forEach(msg => {
  const msgDay = this.formatDay(msgDate);
  if (msgDay !== lastDay) {
    this.addDayDivider(msgDay, container);
    lastDay = msgDay;
  }
  this.renderMessage(msg, container);
});

4. ✅ Ленивая загрузка истории

  • IntersectionObserver отслеживает элемент .history-loader
  • При видимости → загружает старые сообщения
  • Сохраняет позицию скролла

5. ✅ Real-time обновления

  • Новые сообщения → new_message
  • Редактирование → message_updated
  • Удаление → message_deleted
  • Реакции → reaction_added/removed
  • Голосования → poll_update
  • Индикатор печати → typing_start/stop

🛠️ Порядок инициализации

1. HTTP Request → ChatDetailView
2. Render HTML Template
3. DOM Ready
4. Load JS Modules (chatDetail.js, components/*)
5. Read Config from data-attributes
6. Wait for WebSocket Ready (user:ws-ready event)
7. Initialize Components:
   ├─ FormManager
   ├─ MarkRead
   ├─ Composer
   ├─ HistoryLoader
   └─ MessageRenderer
8. Configure WebSocket with components
9. Open Chat: userWs.openChat(chatId, true)
10. WebSocket → Backend: { action: 'open_chat' }
11. Backend → WebSocket: { type: 'initial_messages' }
12. Render Messages → DOM
13. Remove #initialLoader
14. Scroll to Bottom
15. Initialize Polls
16. Ready! ✓

📝 Итоговая схема

┌──────────────────────────────────────────────────────────────────────┐
│                         СТРАНИЦА ЧАТА                                │
├──────────────────────────────────────────────────────────────────────┤
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 1. DJANGO VIEW (Backend)                                    │   │
│  │    - Проверка доступа                                       │   │
│  │    - Подготовка контекста (без messages)                    │   │
│  │    - Рендеринг HTML шаблона                                 │   │
│  └────────────────────────┬────────────────────────────────────┘   │
│                           │                                          │
│                           ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 2. HTML TEMPLATE                                            │   │
│  │    - Пустой контейнер #chatScroll                           │   │
│  │    - Индикатор загрузки #initialLoader                      │   │
│  │    - Data-атрибуты с конфигом                               │   │
│  │    - Форма композера #chatForm                              │   │
│  └────────────────────────┬────────────────────────────────────┘   │
│                           │                                          │
│                           ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 3. JAVASCRIPT INIT (chatDetail.js)                          │   │
│  │    - Загрузка модулей                                       │   │
│  │    - Ожидание WebSocket                                     │   │
│  │    - Инициализация компонентов                              │   │
│  └────────────────────────┬────────────────────────────────────┘   │
│                           │                                          │
│                           ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 4. WEBSOCKET CONNECTION (userWebSocket.js)                  │   │
│  │    - openChat(chatId, loadHistory=true)                     │   │
│  │    - Backend → initial_messages                             │   │
│  └────────────────────────┬────────────────────────────────────┘   │
│                           │                                          │
│                           ▼                                          │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 5. MESSAGE RENDERING (messageRenderer.js)                   │   │
│  │    - renderMessages(messages)                               │   │
│  │    - Разделители дней                                       │   │
│  │    - Проверка дубликатов                                    │   │
│  │    - Вставка в DOM                                          │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
│  ┌─────────────────────────────────────────────────────────────┐   │
│  │ 6. REAL-TIME UPDATES                                        │   │
│  │    - new_message → добавить                                 │   │
│  │    - message_updated → обновить                             │   │
│  │    - message_deleted → удалить                              │   │
│  │    - reaction_added/removed → обновить реакции              │   │
│  │    - poll_update → обновить голосование                     │   │
│  └─────────────────────────────────────────────────────────────┘   │
│                                                                      │
└──────────────────────────────────────────────────────────────────────┘

📚 Связанные документы

  • CHAT_RENDERING_ANALYSIS.md - Подробный анализ архитектуры рендеринга
  • WEBSOCKET_UNIFIED_MIGRATION.md - Миграция на единое WebSocket соединение
  • CHAT_CACHING_FIX.md - Отключение кэширования страницы чата
  • MESSAGE_EDITING_ANALYSIS.md - Механизм редактирования сообщений
  • POLLS_IMPLEMENTATION.md - Реализация голосований

Документ обновлён: 4 декабря 2025 г.