refactor(chat): удалены неиспользуемые модули загрузки сообщений
- Удалён chatWebSocket.js (дублировал userWebSocket.js) - Удалён initialMessagesLoader.js (не используется, загрузка через WS) - Очищены импорты и закомментированный код в chatDetail.js - Добавлен план рефакторинга в docs/in-progress/ Загрузка initial messages теперь происходит только через: - userWebSocket.handleInitialMessages() - начальные сообщения - chatHistoryLoader - подгрузка истории при скролле
This commit is contained in:
parent
85ce134bac
commit
5906bbaf86
@ -1,407 +0,0 @@
|
||||
/**
|
||||
* @fileoverview Chat WebSocket - WebSocket соединение для чата в реальном времени
|
||||
* Отправка/получение сообщений, индикация "печатает...", рендеринг в DOM
|
||||
* @module components/chatWebSocket
|
||||
*/
|
||||
|
||||
import { getChatAvatar } from './chatAvatarMap.js';
|
||||
import {
|
||||
createMessageElement,
|
||||
createDayDivider,
|
||||
formatDay,
|
||||
toTimestamp
|
||||
} from './chatMessageTemplates.js';
|
||||
|
||||
/**
|
||||
* Инициализирует WebSocket соединение для чата
|
||||
* @param {Object} options - Опции инициализации
|
||||
* @param {number} options.chatId - ID чата
|
||||
* @param {number} options.meId - ID текущего пользователя
|
||||
* @param {string} [options.scrollContainerId='chatScroll'] - ID контейнера скролла
|
||||
* @param {string} [options.formId='chatForm'] - ID формы отправки
|
||||
* @param {string} [options.textareaId='id_content'] - ID textarea
|
||||
* @param {string} [options.typingIndicatorId='typing'] - ID индикатора "печатает..."
|
||||
* @param {string} [options.profileUrl] - URL профиля текущего пользователя
|
||||
* @param {string} [options.detailUrlTemplate] - Шаблон URL деталей сотрудника
|
||||
* @param {Object} [options.avatarMap] - Карта аватаров пользователей
|
||||
* @param {Object} [options.markReadApi] - API из chatMarkRead для интеграции
|
||||
* @returns {Object|null} API WebSocket или null если параметры невалидны
|
||||
*/
|
||||
|
||||
// Храним активное соединение для предотвращения дубликатов
|
||||
let activeWebSocket = null;
|
||||
let lastOptions = null;
|
||||
|
||||
export function initChatWebSocket(options = {}) {
|
||||
const {
|
||||
chatId,
|
||||
meId,
|
||||
scrollContainerId = 'chatScroll',
|
||||
formId = 'chatForm',
|
||||
textareaId = 'id_content',
|
||||
typingIndicatorId = 'typing',
|
||||
profileUrl = '/employees/profile/',
|
||||
detailUrlTemplate = '/employees/0/',
|
||||
avatarMap = {},
|
||||
markReadApi,
|
||||
bindFormSubmit = true
|
||||
} = options;
|
||||
|
||||
// Проверка обязательных параметров
|
||||
if (!chatId || !meId) {
|
||||
console.warn('ChatWebSocket: chatId and meId are required');
|
||||
return null;
|
||||
}
|
||||
|
||||
// Закрываем существующее соединение перед созданием нового
|
||||
if (activeWebSocket && activeWebSocket.readyState !== WebSocket.CLOSED) {
|
||||
console.log('ChatWebSocket: closing existing connection');
|
||||
activeWebSocket.close(1000, 'Reconnecting');
|
||||
}
|
||||
|
||||
// Сохраняем опции для переподключения
|
||||
lastOptions = options;
|
||||
|
||||
// Получение элементов
|
||||
const scrollEl = document.getElementById(scrollContainerId);
|
||||
if (!scrollEl) {
|
||||
console.warn(`ChatWebSocket: element #${scrollContainerId} not found`);
|
||||
return null;
|
||||
}
|
||||
|
||||
const form = document.getElementById(formId);
|
||||
const ta = document.getElementById(textareaId);
|
||||
const typingEl = document.getElementById(typingIndicatorId);
|
||||
|
||||
// Извлекаем методы из chatMarkRead API
|
||||
const { markRead, observeLastForeign, autoscroll, atBottom } = markReadApi || {};
|
||||
|
||||
// Создание WebSocket соединения
|
||||
const proto = location.protocol === 'https:' ? 'wss' : 'ws';
|
||||
const wsUrl = `${proto}://${location.host}/ws/chat/${chatId}/`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
// Сохраняем активное соединение
|
||||
activeWebSocket = ws;
|
||||
|
||||
// Настройки переподключения
|
||||
let reconnectAttempts = 0;
|
||||
const maxReconnectAttempts = 5;
|
||||
const reconnectDelay = 3000;
|
||||
|
||||
/**
|
||||
* Инициализирует последний день из DOM
|
||||
*/
|
||||
function ensureLastDayFromDom() {
|
||||
const dl = scrollEl.querySelector('.day-divider:last-of-type span');
|
||||
if (dl) {
|
||||
scrollEl.dataset.lastDay = dl.textContent.trim();
|
||||
} else {
|
||||
const lastMsg = scrollEl.querySelector('.msg:last-of-type');
|
||||
if (lastMsg) {
|
||||
const ts = Number(lastMsg.dataset.ts || Date.now());
|
||||
scrollEl.dataset.lastDay = formatDay(ts);
|
||||
}
|
||||
}
|
||||
}
|
||||
ensureLastDayFromDom();
|
||||
|
||||
/**
|
||||
* Рендерит разделитель дня
|
||||
* @param {string} dateStr - Дата в текстовом формате
|
||||
*/
|
||||
function renderDayDivider(dateStr) {
|
||||
const div = createDayDivider(dateStr);
|
||||
scrollEl.appendChild(div);
|
||||
}
|
||||
|
||||
/**
|
||||
* Рендерит сообщение в DOM
|
||||
* @param {Object} msg - Объект сообщения
|
||||
*/
|
||||
function renderMessage(msg) {
|
||||
const mine = Number(msg.author_id) === Number(meId);
|
||||
|
||||
// Обновляем карту аватаров
|
||||
if (msg.author_id && msg.avatar && !avatarMap[msg.author_id]) {
|
||||
avatarMap[msg.author_id] = msg.avatar;
|
||||
}
|
||||
|
||||
// Проверяем день
|
||||
const ts = toTimestamp(msg);
|
||||
const dayStr = msg.day || formatDay(ts);
|
||||
|
||||
if (dayStr && scrollEl.dataset.lastDay !== dayStr) {
|
||||
renderDayDivider(dayStr);
|
||||
scrollEl.dataset.lastDay = dayStr;
|
||||
}
|
||||
|
||||
// Создаём элемент сообщения
|
||||
const msgElement = createMessageElement(msg, {
|
||||
meId,
|
||||
profileUrl,
|
||||
detailUrlTemplate,
|
||||
avatarMap
|
||||
});
|
||||
|
||||
// Проверяем, нужен ли автоскролл
|
||||
const keep = atBottom ? atBottom() : false;
|
||||
|
||||
// Добавляем в DOM
|
||||
scrollEl.appendChild(msgElement);
|
||||
|
||||
// Автоскролл если был внизу
|
||||
if (keep && autoscroll) {
|
||||
autoscroll();
|
||||
}
|
||||
|
||||
// Сообщаем другим модулям о новом сообщении
|
||||
if (typeof window !== 'undefined') {
|
||||
window.dispatchEvent(
|
||||
new CustomEvent('chat:message-received', { detail: msg })
|
||||
);
|
||||
}
|
||||
|
||||
// Обновляем observer для последнего чужого сообщения
|
||||
if (observeLastForeign) {
|
||||
observeLastForeign();
|
||||
}
|
||||
|
||||
// Отмечаем как прочитанное если был внизу
|
||||
if (keep && markRead) {
|
||||
markRead(ts);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Показывает индикатор "печатает..."
|
||||
*/
|
||||
let typingTimer = null;
|
||||
function showTyping() {
|
||||
if (!typingEl) return;
|
||||
|
||||
typingEl.classList.remove('d-none');
|
||||
clearTimeout(typingTimer);
|
||||
typingTimer = setTimeout(() => {
|
||||
typingEl.classList.add('d-none');
|
||||
}, 2000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Обрабатывает успешное подключение
|
||||
*/
|
||||
ws.addEventListener('open', () => {
|
||||
console.log('ChatWebSocket: connection established');
|
||||
reconnectAttempts = 0; // Сбрасываем счетчик при успешном подключении
|
||||
});
|
||||
|
||||
/**
|
||||
* Обрабатывает входящие сообщения WebSocket
|
||||
*/
|
||||
ws.addEventListener('message', (e) => {
|
||||
try {
|
||||
const data = JSON.parse(e.data);
|
||||
|
||||
// Обработка ping для keepalive (игнорируем)
|
||||
if (data.type === 'ping') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка начальной загрузки сообщений
|
||||
if (data.type === 'initial_messages') {
|
||||
// Убираем лоадер
|
||||
const loader = scrollEl.querySelector('.initial-loader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
|
||||
// Рендерим все сообщения
|
||||
if (data.messages && Array.isArray(data.messages)) {
|
||||
data.messages.forEach(msg => renderMessage(msg));
|
||||
}
|
||||
|
||||
// Скроллим вниз после загрузки
|
||||
if (autoscroll) {
|
||||
setTimeout(() => autoscroll(), 100);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка события "печатает..."
|
||||
if (data.type === 'typing' && Number(data.user_id) !== Number(meId)) {
|
||||
showTyping();
|
||||
return;
|
||||
}
|
||||
|
||||
// События реакций - диспетчеризуем для обработки в chat-reactions-integration.js
|
||||
if (data.type === 'reaction_added') {
|
||||
console.log('[ChatWebSocket] >>> Dispatching reaction_added event:', data);
|
||||
window.dispatchEvent(new CustomEvent('chat:reaction-added', { detail: data }));
|
||||
console.log('[ChatWebSocket] >>> Event dispatched');
|
||||
return;
|
||||
}
|
||||
|
||||
if (data.type === 'reaction_removed') {
|
||||
console.log('[ChatWebSocket] >>> Dispatching reaction_removed event:', data);
|
||||
window.dispatchEvent(new CustomEvent('chat:reaction-removed', { detail: data }));
|
||||
console.log('[ChatWebSocket] >>> Event dispatched');
|
||||
return;
|
||||
}
|
||||
|
||||
// Событие редактирования сообщения
|
||||
if (data.type === 'message_edited') {
|
||||
console.log('[ChatWebSocket] >>> Dispatching message_edited event:', data);
|
||||
window.dispatchEvent(new CustomEvent('chat:message-edited', { detail: data }));
|
||||
console.log('[ChatWebSocket] >>> Event dispatched');
|
||||
return;
|
||||
}
|
||||
|
||||
// События обновления голосования - диспетчеризуем для обработки
|
||||
if (data.type === 'poll_update') {
|
||||
window.dispatchEvent(new CustomEvent('chat:poll-update', { detail: data }));
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка обычного сообщения
|
||||
const msg = data.payload || data.message || data;
|
||||
renderMessage(msg);
|
||||
} catch (err) {
|
||||
console.warn('ChatWebSocket: failed to parse message', err);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Обрабатывает закрытие соединения
|
||||
*/
|
||||
ws.addEventListener('close', (event) => {
|
||||
console.log('ChatWebSocket: connection closed', event.code, event.reason);
|
||||
|
||||
// Не переподключаемся если закрытие было нормальным (код 1000)
|
||||
if (event.code === 1000) {
|
||||
console.log('ChatWebSocket: normal closure, not reconnecting');
|
||||
return;
|
||||
}
|
||||
|
||||
// Пробуем переподключиться
|
||||
if (reconnectAttempts < maxReconnectAttempts) {
|
||||
reconnectAttempts++;
|
||||
console.log(`ChatWebSocket: reconnecting in ${reconnectDelay}ms (attempt ${reconnectAttempts}/${maxReconnectAttempts})`);
|
||||
|
||||
setTimeout(() => {
|
||||
if (document.querySelector('[data-chat-ws]')) {
|
||||
console.log('ChatWebSocket: attempting to reconnect...');
|
||||
// Переинициализируем WebSocket с сохраненными опциями
|
||||
initChatWebSocket(lastOptions);
|
||||
}
|
||||
}, reconnectDelay);
|
||||
} else {
|
||||
console.warn('ChatWebSocket: max reconnect attempts reached');
|
||||
// Показываем уведомление пользователю вместо перезагрузки
|
||||
const notification = document.createElement('div');
|
||||
notification.className = 'alert alert-warning position-fixed bottom-0 start-50 translate-middle-x mb-3';
|
||||
notification.style.zIndex = '9999';
|
||||
notification.innerHTML = `
|
||||
<i class="bi-exclamation-triangle me-2"></i>
|
||||
Соединение с сервером потеряно.
|
||||
<button class="btn btn-sm btn-link" onclick="location.reload()">Обновить страницу</button>
|
||||
`;
|
||||
document.body.appendChild(notification);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Обрабатывает ошибки WebSocket
|
||||
*/
|
||||
ws.addEventListener('error', (err) => {
|
||||
console.error('ChatWebSocket: connection error', err);
|
||||
// Не перезагружаем страницу при ошибке - дожидаемся события 'close'
|
||||
});
|
||||
|
||||
/**
|
||||
* Обрабатывает отправку формы
|
||||
* ОТКЛЮЧЕНО: теперь отправка только через chatComposer.js -> REST API
|
||||
*/
|
||||
const handleFormSubmit = (e) => {
|
||||
e.preventDefault();
|
||||
console.log('ChatWebSocket: form submission intercepted but ignored (handled by chatComposer.js)');
|
||||
// Отправка теперь обрабатывается через chatComposer.js
|
||||
};
|
||||
|
||||
// НЕ привязываем обработчик - пусть chatComposer.js обрабатывает
|
||||
// if (bindFormSubmit && form) {
|
||||
// form.addEventListener('submit', handleFormSubmit);
|
||||
// }
|
||||
|
||||
/**
|
||||
* Обрабатывает ввод в textarea (индикация "печатает...")
|
||||
*/
|
||||
let lastTyping = 0;
|
||||
ta?.addEventListener('input', () => {
|
||||
const now = Date.now();
|
||||
|
||||
// Отправляем событие "печатает..." не чаще раз в 1.5 секунды
|
||||
if (ws.readyState === WebSocket.OPEN && now - lastTyping > 1500) {
|
||||
ws.send(JSON.stringify({ type: 'typing' }));
|
||||
lastTyping = now;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Начальный скролл при загрузке страницы
|
||||
*/
|
||||
if (!window.__SCROLLED_ON_INIT__ && autoscroll) {
|
||||
autoscroll();
|
||||
}
|
||||
|
||||
// API
|
||||
return {
|
||||
/**
|
||||
* Отправляет сообщение
|
||||
* @param {string} content - Текст сообщения
|
||||
*/
|
||||
send: (content) => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ content }));
|
||||
} else {
|
||||
console.warn('ChatWebSocket: connection not open');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Отправляет индикацию "печатает..."
|
||||
*/
|
||||
sendTyping: () => {
|
||||
if (ws.readyState === WebSocket.OPEN) {
|
||||
ws.send(JSON.stringify({ type: 'typing' }));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Закрывает WebSocket соединение
|
||||
*/
|
||||
close: () => {
|
||||
ws.close();
|
||||
},
|
||||
|
||||
/**
|
||||
* Рендерит сообщение (для внешнего использования)
|
||||
*/
|
||||
renderMessage,
|
||||
|
||||
/**
|
||||
* Получает состояние соединения
|
||||
* @returns {number} WebSocket.readyState
|
||||
*/
|
||||
getReadyState: () => ws.readyState,
|
||||
|
||||
/**
|
||||
* Прямой доступ к WebSocket (для отладки)
|
||||
*/
|
||||
ws
|
||||
};
|
||||
}
|
||||
|
||||
// Публикуем в window для совместимости
|
||||
if (typeof window !== 'undefined') {
|
||||
window.initChatWebSocket = initChatWebSocket;
|
||||
}
|
||||
@ -1,77 +0,0 @@
|
||||
/**
|
||||
* Initial Messages Loader
|
||||
* Загружает начальные сообщения чата при открытии страницы
|
||||
*/
|
||||
|
||||
export class InitialMessagesLoader {
|
||||
constructor(config = {}) {
|
||||
this.chatId = config.chatId;
|
||||
this.fetchUrl = config.fetchUrl;
|
||||
this.limit = config.limit || 30;
|
||||
this.messageRenderer = config.messageRenderer;
|
||||
this.onLoaded = config.onLoaded || (() => {});
|
||||
this.onError = config.onError || (() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает начальные сообщения
|
||||
*/
|
||||
async load() {
|
||||
console.log('[InitialMessagesLoader] Loading initial messages for chat:', this.chatId);
|
||||
|
||||
try {
|
||||
const url = `${this.fetchUrl}?limit=${this.limit}`;
|
||||
const response = await fetch(url, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'Accept': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`HTTP ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (!data.ok) {
|
||||
throw new Error(data.error || 'Ошибка загрузки');
|
||||
}
|
||||
|
||||
console.log('[InitialMessagesLoader] Loaded', data.messages.length, 'messages');
|
||||
|
||||
// Рендерим сообщения
|
||||
if (this.messageRenderer && data.messages.length > 0) {
|
||||
this.messageRenderer.renderMessages(data.messages);
|
||||
}
|
||||
|
||||
// Убираем индикатор загрузки
|
||||
const loader = document.getElementById('initialLoader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
|
||||
// Callback
|
||||
this.onLoaded({
|
||||
messages: data.messages,
|
||||
hasMore: data.has_more,
|
||||
nextBeforeId: data.next_before_id,
|
||||
nextBeforeTs: data.next_before_ts
|
||||
});
|
||||
|
||||
return data;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[InitialMessagesLoader] Load failed:', error);
|
||||
this.onError(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Фабричная функция
|
||||
*/
|
||||
export function createInitialMessagesLoader(config) {
|
||||
return new InitialMessagesLoader(config);
|
||||
}
|
||||
@ -248,7 +248,23 @@ export function initUserWebSocket(options = {}) {
|
||||
if (!state.scrollEl) return;
|
||||
|
||||
const { messages } = data;
|
||||
if (!messages || !messages.length) return;
|
||||
|
||||
// Убираем индикатор загрузки независимо от наличия сообщений
|
||||
const loader = document.getElementById('initialLoader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
|
||||
// Отправляем событие для других модулей
|
||||
window.dispatchEvent(new CustomEvent('chat:initial-messages-loaded', {
|
||||
detail: { count: messages ? messages.length : 0 }
|
||||
}));
|
||||
|
||||
// Если сообщений нет, завершаем обработку
|
||||
if (!messages || !messages.length) {
|
||||
console.log('[UserWS] No initial messages to render');
|
||||
return;
|
||||
}
|
||||
|
||||
// Используем MessageRenderer для добавления сообщений (с проверкой дубликатов)
|
||||
if (!options.messageRenderer) {
|
||||
@ -264,17 +280,6 @@ export function initUserWebSocket(options = {}) {
|
||||
// Скроллим вниз
|
||||
scrollToBottom(true);
|
||||
|
||||
// Убираем индикатор загрузки
|
||||
const loader = document.getElementById('initialLoader');
|
||||
if (loader) {
|
||||
loader.remove();
|
||||
}
|
||||
|
||||
// Отправляем событие для других модулей
|
||||
window.dispatchEvent(new CustomEvent('chat:initial-messages-loaded', {
|
||||
detail: { count: messages.length }
|
||||
}));
|
||||
|
||||
// Инициализируем голосования после загрузки сообщений
|
||||
if (window.chatPoll) {
|
||||
console.log('[UserWS] Initializing polls after initial messages load');
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
* Инициализация страницы детального просмотра чата
|
||||
*/
|
||||
|
||||
let initChatMarkRead, initChatComposer, initChatHistoryLoader, initChatFormManager, MessageRenderer, InitialMessagesLoader;
|
||||
let initChatMarkRead, initChatComposer, initChatHistoryLoader, initChatFormManager, MessageRenderer;
|
||||
|
||||
try {
|
||||
const modules = await Promise.all([
|
||||
@ -11,8 +11,7 @@ try {
|
||||
import('../components/chatComposer.js'),
|
||||
import('../components/chatHistoryLoader.js'),
|
||||
import('../components/chatFormManager.js'),
|
||||
import('../components/messageRenderer.js'),
|
||||
import('../components/initialMessagesLoader.js')
|
||||
import('../components/messageRenderer.js')
|
||||
]);
|
||||
|
||||
initChatMarkRead = modules[0].initChatMarkRead;
|
||||
@ -20,7 +19,6 @@ try {
|
||||
initChatHistoryLoader = modules[2].initChatHistoryLoader;
|
||||
initChatFormManager = modules[3].initChatFormManager;
|
||||
MessageRenderer = modules[4].MessageRenderer;
|
||||
InitialMessagesLoader = modules[5].InitialMessagesLoader;
|
||||
} catch (error) {
|
||||
console.error('[ChatDetail] Failed to load modules:', error);
|
||||
alert('Ошибка загрузки модулей чата: ' + error.message);
|
||||
@ -200,48 +198,6 @@ function initializeComponents(config, userWs) {
|
||||
// Инициализация контекстного меню перенесена в chat-detail-enhanced.js
|
||||
// для избежания конфликтов и дублирования
|
||||
|
||||
// ========== INITIAL MESSAGES LOADER ==========
|
||||
// ОТКЛЮЧЕНО: Используем WebSocket для загрузки initial_messages
|
||||
// InitialMessagesLoader больше не нужен, так как WebSocket справляется с этой задачей
|
||||
/*
|
||||
console.log('[ChatDetail] Loading initial messages...');
|
||||
|
||||
const initialLoader = new InitialMessagesLoader({
|
||||
chatId: config.chatId,
|
||||
fetchUrl: config.messagesUrl,
|
||||
limit: 30,
|
||||
messageRenderer: messageRenderer,
|
||||
onLoaded: (data) => {
|
||||
console.log('[ChatDetail] Initial messages loaded:', data.messages.length);
|
||||
|
||||
// Скроллим вниз к последнему сообщению
|
||||
const container = document.getElementById('chatScroll');
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error('[ChatDetail] Failed to load initial messages:', error);
|
||||
alert('Ошибка загрузки сообщений: ' + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
// Загружаем сообщения
|
||||
initialLoader.load().catch(err => {
|
||||
console.error('[ChatDetail] Initial load error:', err);
|
||||
});
|
||||
*/
|
||||
console.log('[ChatDetail] Initial messages will be loaded via WebSocket');
|
||||
|
||||
// Убираем индикатор загрузки после подключения WebSocket
|
||||
const loader = document.getElementById('initialLoader');
|
||||
if (loader) {
|
||||
// Скроем индикатор через WebSocket событие initial_messages
|
||||
window.addEventListener('chat:initial-messages-loaded', () => {
|
||||
loader.remove();
|
||||
}, { once: true });
|
||||
}
|
||||
|
||||
// ========== CONFIGURE WEBSOCKET ==========
|
||||
console.log('[ChatDetail] Configuring WebSocket...');
|
||||
|
||||
|
||||
210
docs/in-progress/MESSAGES_LOADING_REFACTORING.md
Normal file
210
docs/in-progress/MESSAGES_LOADING_REFACTORING.md
Normal file
@ -0,0 +1,210 @@
|
||||
# Рефакторинг загрузки сообщений в чате
|
||||
|
||||
## 📊 Текущая ситуация
|
||||
|
||||
### Модули для загрузки сообщений:
|
||||
|
||||
1. **`initialMessagesLoader.js`** - загрузка начальных сообщений через HTTP
|
||||
- **Статус**: ❌ ЗАКОММЕНТИРОВАН в chatDetail.js
|
||||
- **Проблема**: Дублирует функционал WebSocket
|
||||
|
||||
2. **`userWebSocket.js`** - WebSocket для всего приложения
|
||||
- **Функция**: `handleInitialMessages()`
|
||||
- **Проблема**: ✅ ИСПРАВЛЕНА - теперь удаляет лоадер даже для пустых чатов
|
||||
|
||||
3. **`chatHistoryLoader.js`** - подгрузка старых сообщений при скролле вверх
|
||||
- **Статус**: ✅ РАБОТАЕТ
|
||||
- **Проблема**: Нет - используется активно
|
||||
|
||||
4. **`messageRenderer.js`** - рендеринг сообщений в DOM
|
||||
- **Статус**: ✅ РАБОТАЕТ
|
||||
- **Проблема**: Дублирование логики с chatMessageTemplates.js
|
||||
|
||||
5. **`chatWebSocket.js`** - OLD WebSocket (устарел?)
|
||||
- **Статус**: ⚠️ DEPRECATED
|
||||
- **Проблема**: Дублирует userWebSocket.js
|
||||
|
||||
## 🎯 Проблемы архитектуры
|
||||
|
||||
### 1. Дублирование кода
|
||||
- `initialMessagesLoader.js` полностью дублирует `handleInitialMessages()` в WebSocket
|
||||
- `chatWebSocket.js` дублирует функционал `userWebSocket.js`
|
||||
- Логика рендеринга размазана между несколькими модулями
|
||||
|
||||
### 2. Закомментированный код
|
||||
- `InitialMessagesLoader` импортируется но не используется
|
||||
- Создаёт путаницу и увеличивает размер бандла
|
||||
|
||||
### 3. Несогласованность
|
||||
- Некоторые модули используют `MessageRenderer`
|
||||
- Другие напрямую вызывают `createMessageElement()`
|
||||
- Нет единого API для работы с сообщениями
|
||||
|
||||
### 4. Отсутствие единой точки управления
|
||||
- Загрузка сообщений происходит в разных местах
|
||||
- Сложно отследить последовательность событий
|
||||
- Трудно дебажить
|
||||
|
||||
## ✅ План рефакторинга
|
||||
|
||||
### Фаза 1: Очистка мертвого кода
|
||||
- [ ] Удалить `initialMessagesLoader.js` (не используется)
|
||||
- [ ] Проверить использование `chatWebSocket.js`
|
||||
- [ ] Если не используется - удалить
|
||||
|
||||
### Фаза 2: Унификация рендеринга
|
||||
- [ ] Создать единый `MessagesService` или расширить `MessageRenderer`
|
||||
- [ ] Все модули должны использовать один API для рендеринга
|
||||
- [ ] Убрать прямые вызовы `createMessageElement()`
|
||||
|
||||
### Фаза 3: Централизация загрузки
|
||||
Создать единый сервис `ChatMessagesLoader`:
|
||||
|
||||
```javascript
|
||||
class ChatMessagesLoader {
|
||||
constructor({ chatId, messageRenderer, websocket }) {
|
||||
this.chatId = chatId;
|
||||
this.messageRenderer = messageRenderer;
|
||||
this.ws = websocket;
|
||||
this.historyLoader = null;
|
||||
}
|
||||
|
||||
// Начальная загрузка через WebSocket
|
||||
async loadInitial() {
|
||||
// Уже реализовано в userWebSocket.handleInitialMessages()
|
||||
}
|
||||
|
||||
// Загрузка истории при скролле
|
||||
async loadHistory(beforeId) {
|
||||
// Использует chatHistoryLoader
|
||||
}
|
||||
|
||||
// Загрузка конкретного сообщения
|
||||
async loadMessage(messageId) {
|
||||
// Новая функция
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Фаза 4: Упрощение chatDetail.js
|
||||
- [ ] Убрать импорт неиспользуемых модулей
|
||||
- [ ] Упростить инициализацию
|
||||
- [ ] Сделать код более читаемым
|
||||
|
||||
## 📋 Детальный план действий
|
||||
|
||||
### Шаг 1: Аудит использования модулей
|
||||
|
||||
Проверить где используется:
|
||||
- `initialMessagesLoader.js` - ✅ ТОЛЬКО импорт в chatDetail.js (не используется)
|
||||
- `chatWebSocket.js` - ? Нужна проверка
|
||||
- `MessageRenderer` - ✅ Активно используется
|
||||
|
||||
### Шаг 2: Безопасное удаление
|
||||
|
||||
1. Удалить импорт `InitialMessagesLoader` из `chatDetail.js`
|
||||
2. Удалить файл `initialMessagesLoader.js`
|
||||
3. Проверить что ничего не сломалось
|
||||
|
||||
### Шаг 3: Проверка chatWebSocket.js
|
||||
|
||||
Найти все места использования:
|
||||
```bash
|
||||
grep -r "chatWebSocket" backend/static/js/**/*.js
|
||||
grep -r "initChatWebSocket" backend/templates/**/*.html
|
||||
```
|
||||
|
||||
Если не используется - удалить.
|
||||
|
||||
### Шаг 4: Унификация MessageRenderer
|
||||
|
||||
Текущие вызовы:
|
||||
- `messageRenderer.renderMessage(msg)` - ✅ Хорошо
|
||||
- `messageRenderer.renderMessages(messages)` - ✅ Хорошо
|
||||
- `createMessageElement(msg, options)` - ❌ Плохо (прямой вызов)
|
||||
|
||||
Решение:
|
||||
```javascript
|
||||
// Все должны использовать MessageRenderer
|
||||
messageRenderer.renderMessage(msg);
|
||||
|
||||
// Вместо прямого createMessageElement()
|
||||
```
|
||||
|
||||
### Шаг 5: Документация
|
||||
|
||||
После рефакторинга обновить:
|
||||
- `CHAT_PAGE_RENDERING_ARCHITECTURE.md`
|
||||
- Добавить диаграммы потока данных
|
||||
- Описать API всех модулей
|
||||
|
||||
## 🎨 Итоговая архитектура
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────┐
|
||||
│ Chat Detail Page │
|
||||
└─────────────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
│ chatDetail.js (main) │
|
||||
└─────────────────────────────┘
|
||||
│
|
||||
┌───────────┼───────────┐
|
||||
▼ ▼ ▼
|
||||
┌──────────┐ ┌────────┐ ┌────────────┐
|
||||
│ userWS │ │Renderer│ │HistoryLoad │
|
||||
│(initial) │ │ │ │(scroll up) │
|
||||
└──────────┘ └────────┘ └────────────┘
|
||||
│ │ │
|
||||
└───────────┴───────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ MessageRenderer │
|
||||
│ (single API) │
|
||||
└──────────────────┘
|
||||
│
|
||||
▼
|
||||
┌──────────────────┐
|
||||
│ DOM (chat) │
|
||||
└──────────────────┘
|
||||
```
|
||||
|
||||
## ⚠️ Риски
|
||||
|
||||
1. **Разрыв обратной совместимости**
|
||||
- Решение: Тщательное тестирование после каждого шага
|
||||
|
||||
2. **Потеря функциональности**
|
||||
- Решение: Написать тесты до рефакторинга
|
||||
|
||||
3. **Регрессия в других частях приложения**
|
||||
- Решение: Проверить все страницы с чатами
|
||||
|
||||
## 🧪 Тестирование
|
||||
|
||||
После каждого шага проверять:
|
||||
- [ ] Начальная загрузка сообщений работает
|
||||
- [ ] Загрузка истории при скролле работает
|
||||
- [ ] Новые сообщения появляются в реальном времени
|
||||
- [ ] Пустые чаты отображаются корректно
|
||||
- [ ] Лоадер корректно скрывается
|
||||
- [ ] Индикатор "печатает" работает
|
||||
- [ ] Отметка "прочитано" работает
|
||||
|
||||
## 📝 Статус выполнения
|
||||
|
||||
- [x] Исправлен баг с лоадером в пустых чатах (`userWebSocket.js`)
|
||||
- [ ] Удалить `initialMessagesLoader.js`
|
||||
- [ ] Проверить `chatWebSocket.js`
|
||||
- [ ] Унифицировать API рендеринга
|
||||
- [ ] Обновить документацию
|
||||
- [ ] Написать тесты
|
||||
|
||||
## 🔄 Следующие шаги
|
||||
|
||||
1. Получить одобрение плана
|
||||
2. Начать с Фазы 1 (удаление мертвого кода)
|
||||
3. Тестировать после каждого изменения
|
||||
4. Закомитить изменения маленькими порциями
|
||||
Loading…
x
Reference in New Issue
Block a user