/** * Message Renderer * Рендерит сообщения в DOM из JSON данных */ export class MessageRenderer { constructor(config = {}) { this.containerId = config.containerId || 'chatScroll'; this.currentUserId = config.currentUserId; this.currentUserAvatar = config.currentUserAvatar || ''; this.profileUrl = config.profileUrl || '/employees/profile/'; this.detailUrlTemplate = config.detailUrlTemplate || '/employees/detail/0/'; } /** * Рендерит массив сообщений (с разделителями дней) */ renderMessages(messages) { const container = document.getElementById(this.containerId); if (!container) { console.error('[MessageRenderer] Container not found:', this.containerId); return; } 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); }); } /** * Добавляет разделитель дня */ addDayDivider(dayText, container = null) { if (!container) { container = document.getElementById(this.containerId); } if (!container) return; const dividerHtml = `
${dayText}
`; container.insertAdjacentHTML('beforeend', dividerHtml); } /** * Форматирует дату в текст дня */ formatDay(date) { const today = new Date(); const yesterday = new Date(today); yesterday.setDate(yesterday.getDate() - 1); const isToday = date.toDateString() === today.toDateString(); const isYesterday = date.toDateString() === yesterday.toDateString(); if (isToday) return 'Сегодня'; if (isYesterday) return 'Вчера'; const options = { day: 'numeric', month: 'long', year: 'numeric' }; return date.toLocaleDateString('ru-RU', options); } /** * Рендерит одно сообщение */ renderMessage(msg, container = null) { if (!container) { container = document.getElementById(this.containerId); } // Проверяем, нет ли уже сообщения с таким ID (предотвращаем дубликаты) const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`); if (existingMessage) { console.log('[MessageRenderer] Message already exists, skipping:', msg.id); return; } const isOwn = msg.author_id === this.currentUserId; const messageHtml = this.buildMessageHtml(msg, isOwn); // Добавляем в конец контейнера container.insertAdjacentHTML('beforeend', messageHtml); } /** * Вставляет сообщение в начало (для истории) */ prependMessage(msg) { const container = document.getElementById(this.containerId); if (!container) return; // Проверяем, нет ли уже сообщения с таким ID (предотвращаем дубликаты) const existingMessage = container.querySelector(`[data-message-id="${msg.id}"]`); if (existingMessage) { console.log('[MessageRenderer] Message already exists (prepend), skipping:', msg.id); return; } const isOwn = msg.author_id === this.currentUserId; const messageHtml = this.buildMessageHtml(msg, isOwn); // Находим первое сообщение (не day-divider, не history-loader) const firstMsg = container.querySelector('.msg'); if (firstMsg) { firstMsg.insertAdjacentHTML('beforebegin', messageHtml); } else { container.insertAdjacentHTML('beforeend', messageHtml); } } /** * Строит HTML одного сообщения */ buildMessageHtml(msg, isOwn) { const authorUrl = isOwn ? this.profileUrl : this.detailUrlTemplate.replace('/0/', `/${msg.author_id}/`); const authorName = isOwn ? 'Вы' : (msg.author_name || 'Пользователь'); const avatarHtml = isOwn ? '' : ` ${msg.avatar ? `` : ''} `; const forwardedHtml = msg.is_forwarded && msg.forwarded_from ? `
Переслано от ${msg.forwarded_from.author_name || 'Пользователя'} ${msg.forwarded_from.chat_name ? ` из «${msg.forwarded_from.chat_name}»` : ''}
${msg.forwarded_from.created_at ? `
${msg.forwarded_from.created_at}
` : ''}
` : ''; const replyHtml = msg.reply_to ? `
${msg.reply_to.author_name || 'Пользователь'}
${this.escapeHtml(msg.reply_to.content || '[файл]')}
` : ''; const attachmentsHtml = msg.attachments?.length ? msg.attachments.map(att => this.buildAttachmentHtml(att)).join('') : ''; // Голосование (опрос) const pollHtml = msg.poll ? this.buildPollHtml(msg.poll) : ''; // Индикатор редактирования const editedIndicator = msg.is_edited ? `(изменено)` : ''; // Реакции теперь рендерятся через message-reactions-wrapper ВНЕ bubble // Старая система buildReactionsHtml() больше не используется const timeHtml = this.formatTime(msg.created_ts); // Аватар справа для своих сообщений const rightAvatarHtml = isOwn ? ` ${this.currentUserAvatar ? `` : ''} ` : ''; return `
${avatarHtml}
${authorName} · ${editedIndicator}
${forwardedHtml} ${replyHtml} ${msg.content ? `
${this.escapeHtml(msg.content)}
` : ''} ${attachmentsHtml} ${pollHtml}
${rightAvatarHtml}
`; } /** * Строит HTML вложения */ buildAttachmentHtml(attachment) { const fileName = attachment.file_name || attachment.original_filename || 'файл'; const isImage = attachment.file_url?.match(/\.(jpg|jpeg|png|gif|webp)$/i); if (isImage) { return `
${this.escapeHtml(fileName)}
`; } return `
${this.escapeHtml(fileName)}
`; } /** * Строит HTML для голосования (опроса) */ buildPollHtml(poll) { if (!poll || !poll.id) return ''; const totalVotes = poll.total_voters || 0; const isClosed = poll.is_closed || false; // Базовая структура - детали будут добавлены через ChatPoll.js return `
${this.escapeHtml(poll.question)}
${poll.options?.map(option => `
`).join('') || ''}
`; } /** * Форматирует дату/время */ formatTime(timestamp) { const date = new Date(timestamp); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${hours}:${minutes}`; } formatDate(timestamp) { const date = new Date(timestamp); const day = String(date.getDate()).padStart(2, '0'); const month = String(date.getMonth() + 1).padStart(2, '0'); const year = date.getFullYear(); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${day}.${month}.${year} ${hours}:${minutes}`; } /** * Экранирует HTML */ escapeHtml(text) { const div = document.createElement('div'); div.textContent = text; return div.innerHTML; } } /** * Фабричная функция для совместимости */ export function createMessageRenderer(config) { return new MessageRenderer(config); }