/**
* Message Renderer
* Рендерит сообщения в DOM из JSON данных
*/
export class MessageRenderer {
constructor(config = {}) {
this.containerId = config.containerId || 'chatScroll';
this.currentUserId = config.currentUserId || config.meId; // Поддержка обоих параметров
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) {
return `
${this.buildMessageInnerHtml(msg, isOwn)}
`;
}
/**
* Строит внутренний HTML сообщения (без внешней обёртки .msg)
* Используется в chatMessageTemplates.js для создания DOM элементов
*/
buildMessageInnerHtml(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 ``;
}
return ``;
}
/**
* Строит HTML для голосования (опроса)
*/
buildPollHtml(poll) {
if (!poll || !poll.id) return '';
const totalVotes = poll.total_voters || 0;
const isClosed = poll.is_closed || false;
// Базовая структура - детали будут добавлены через ChatPoll.js
// Убираем лишние margin-классы: mt-2 у poll-widget, mb-3 у poll-question, mt-3 у poll-footer
return ``;
}
/**
* Форматирует дату/время
*/
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);
}