diff --git a/backend/static/js/components/chatComposer.js b/backend/static/js/components/chatComposer.js index 3d24296b..6238c10a 100644 --- a/backend/static/js/components/chatComposer.js +++ b/backend/static/js/components/chatComposer.js @@ -3,6 +3,8 @@ import { createMessageElement } from './chatMessageTemplates.js'; /** * Chat Composer module * Отвечает за ввод текста, прикрепление файлов и отправку сообщений через REST API. + * + * РЕФАКТОРИНГ: Теперь использует MessageRenderer для создания pending сообщений */ const DEFAULT_CONFIG = { @@ -49,6 +51,7 @@ class ChatComposer { this.meAvatar = options.meAvatar || form.dataset.meAvatar || this.scrollEl?.dataset.meAvatar || ''; this.profileUrl = options.profileUrl || form.dataset.profileUrl || this.options.profileUrl; this.detailUrlTemplate = options.detailUrlTemplate || form.dataset.detailUrlTemplate || this.options.detailUrlTemplate; + this.messageRenderer = options.messageRenderer || null; // РЕФАКТОРИНГ: Принимаем MessageRenderer this.selectedFiles = []; this.typingThrottle = 1200; this.lastTypingSent = 0; @@ -544,11 +547,19 @@ class ChatComposer { is_pending: true }; - const element = createMessageElement(msg, { - meId: this.meId, - profileUrl: this.profileUrl, - detailUrlTemplate: this.detailUrlTemplate - }); + // РЕФАКТОРИНГ: Используем MessageRenderer если доступен, иначе fallback на createMessageElement + let element; + if (this.messageRenderer) { + element = this.messageRenderer.createMessageElement(msg, true); // true = isPending + } else { + // Fallback для обратной совместимости + element = createMessageElement(msg, { + meId: this.meId, + profileUrl: this.profileUrl, + detailUrlTemplate: this.detailUrlTemplate + }); + } + element.dataset.pendingId = pendingId; this.scrollEl.appendChild(element); this.scrollEl.scrollTop = this.scrollEl.scrollHeight; diff --git a/backend/static/js/components/chatHistoryLoader.js b/backend/static/js/components/chatHistoryLoader.js index 83ad386f..0a486627 100644 --- a/backend/static/js/components/chatHistoryLoader.js +++ b/backend/static/js/components/chatHistoryLoader.js @@ -5,6 +5,12 @@ import { toTimestamp } from './chatMessageTemplates.js'; +/** + * Chat History Loader - загрузка ранних сообщений при прокрутке вверх + * + * РЕФАКТОРИНГ: Теперь использует MessageRenderer для создания элементов сообщений + */ + const DEFAULT_CONFIG = { scrollSelector: '#chatScroll', loaderSelector: '[data-history-loader]', @@ -67,6 +73,7 @@ export function initChatHistoryLoader(options = {}) { const profileUrl = scrollEl.dataset.profileUrl || '/employees/profile/'; const detailUrlTemplate = scrollEl.dataset.detailUrlTemplate || '/employees/0/'; const avatarMap = getAvatarMapSnapshot(); + const messageRenderer = options.messageRenderer || null; // РЕФАКТОРИНГ: Принимаем MessageRenderer const state = { loading: false, @@ -113,6 +120,7 @@ export function initChatHistoryLoader(options = {}) { profileUrl, detailUrlTemplate, avatarMap, + messageRenderer, state } = ctx; @@ -129,12 +137,20 @@ export function initChatHistoryLoader(options = {}) { } prevDay = day; - const element = createMessageElement(msg, { - meId, - profileUrl, - detailUrlTemplate, - avatarMap - }); + // РЕФАКТОРИНГ: Используем MessageRenderer если доступен + let element; + if (messageRenderer) { + element = messageRenderer.createMessageElement(msg, false); + } else { + // Fallback для обратной совместимости + element = createMessageElement(msg, { + meId, + profileUrl, + detailUrlTemplate, + avatarMap + }); + } + element.classList.remove('message-pending', 'message-pending--resolved'); fragment.appendChild(element); }); @@ -186,6 +202,7 @@ export function initChatHistoryLoader(options = {}) { profileUrl, detailUrlTemplate, avatarMap, + messageRenderer, state }); state.oldestId = String(payload.messages[0].id); diff --git a/backend/static/js/components/messageRenderer.js b/backend/static/js/components/messageRenderer.js index 5dd1af98..ddb3f9a9 100644 --- a/backend/static/js/components/messageRenderer.js +++ b/backend/static/js/components/messageRenderer.js @@ -120,6 +120,60 @@ export class MessageRenderer { } } + /** + * Создаёт DOM элемент сообщения без вставки в контейнер + * Используется для pending сообщений, где нужен прямой доступ к элементу + * @param {Object} msg - Объект сообщения + * @param {boolean} isPending - Является ли сообщение pending + * @returns {HTMLElement} DOM элемент сообщения + */ + createMessageElement(msg, isPending = false) { + const isOwn = msg.author_id === this.currentUserId; + + const wrap = document.createElement('div'); + wrap.className = `d-flex mb-3 msg ${isOwn ? 'justify-content-end' : 'justify-content-start'}`; + + if (isPending) { + wrap.classList.add('message-pending'); + } + + if (msg.id != null) { + wrap.setAttribute('data-id', String(msg.id)); + wrap.setAttribute('data-message-id', String(msg.id)); + const reactionsData = msg.reactions_summary || {}; + wrap.setAttribute('data-reactions', JSON.stringify(reactionsData)); + } + + const ts = msg.created_ts || Date.now(); + wrap.setAttribute('data-ts', String(ts)); + wrap.setAttribute('data-author-id', String(msg.author_id || '')); + wrap.setAttribute('data-is-edited', String(msg.is_edited || false)); + wrap.setAttribute('data-edited-at', String(msg.edited_at || '')); + + // Используем buildMessageInnerHtml для единообразного рендеринга + const htmlContent = this.buildMessageInnerHtml(msg, isOwn); + + // Добавляем статус отправки для pending сообщений + if (isPending) { + const tempDiv = document.createElement('div'); + tempDiv.innerHTML = htmlContent; + + const bubble = tempDiv.querySelector('.bubble'); + if (bubble) { + const pendingStatus = document.createElement('div'); + pendingStatus.className = 'message-status small text-secondary mt-2'; + pendingStatus.innerHTML = 'Отправляем…'; + bubble.appendChild(pendingStatus); + } + + wrap.innerHTML = tempDiv.innerHTML; + } else { + wrap.innerHTML = htmlContent; + } + + return wrap; + } + /** * Строит HTML одного сообщения */ diff --git a/backend/static/js/pages/chatDetail.js b/backend/static/js/pages/chatDetail.js index fe035785..1f0a0544 100644 --- a/backend/static/js/pages/chatDetail.js +++ b/backend/static/js/pages/chatDetail.js @@ -146,6 +146,19 @@ function initializeComponents(config, userWs) { console.log('[ChatDetail] MarkRead initialized'); + // ========== MESSAGE RENDERER ========== + console.log('[ChatDetail] Initializing MessageRenderer...'); + + const messageRenderer = new MessageRenderer({ + containerId: 'chatScroll', + currentUserId: config.userId, + currentUserAvatar: config.userAvatar || '', + profileUrl: config.profileUrl || '/employees/profile/', + detailUrlTemplate: config.detailUrlTemplate || '/employees/detail/0/' + }); + + console.log('[ChatDetail] MessageRenderer initialized'); + // ========== COMPOSER ========== console.log('[ChatDetail] Initializing Composer...'); const composer = initChatComposer({ @@ -155,7 +168,8 @@ function initializeComponents(config, userWs) { uploadUrl: config.uploadUrl, meId: config.userId, meName: config.userName, - meAvatar: config.userAvatar + meAvatar: config.userAvatar, + messageRenderer: messageRenderer // Передаём MessageRenderer }); if (!composer) { @@ -176,24 +190,12 @@ function initializeComponents(config, userWs) { scrollContainerId: 'chatScroll', fetchUrl: config.messagesUrl, avatarMap: avatarMap, - meId: config.userId + meId: config.userId, + messageRenderer: messageRenderer // Передаём MessageRenderer }); console.log('[ChatDetail] HistoryLoader initialized'); - // ========== MESSAGE RENDERER ========== - console.log('[ChatDetail] Initializing MessageRenderer...'); - - const messageRenderer = new MessageRenderer({ - containerId: 'chatScroll', - currentUserId: config.userId, - currentUserAvatar: config.userAvatar || '', - profileUrl: config.profileUrl || '/employees/profile/', - detailUrlTemplate: config.detailUrlTemplate || '/employees/detail/0/' - }); - - console.log('[ChatDetail] MessageRenderer initialized'); - // ========== CONTEXT MENU ========== // Инициализация контекстного меню перенесена в chat-detail-enhanced.js // для избежания конфликтов и дублирования