/** * @fileoverview Chat Message Templates - HTML шаблоны для рендеринга сообщений чата * @module components/chatMessageTemplates */ import { esc } from '../utils/stringUtils.js'; /** * Дополняет число нулём слева * @param {number} n - Число * @returns {string} Строка с ведущим нулём */ const z2 = n => (n < 10 ? '0' : '') + n; /** * Извлекает timestamp из сообщения * @param {Object} msg - Объект сообщения * @returns {number} Unix timestamp в миллисекундах */ export function toTimestamp(msg) { if (msg && msg.created_ts != null && !isNaN(Number(msg.created_ts))) { return Number(msg.created_ts); } if (msg && msg.created) { const t = Date.parse(msg.created); if (!isNaN(t)) return t; } return Date.now(); } /** * Форматирует timestamp в HH:MM * @param {number} ts - Unix timestamp * @returns {string} Время в формате HH:MM */ export function formatTime(ts) { const d = new Date(ts); return `${z2(d.getHours())}:${z2(d.getMinutes())}`; } /** * Форматирует timestamp в DD.MM.YYYY * @param {number} ts - Unix timestamp * @returns {string} Дата в формате DD.MM.YYYY */ export function formatDay(ts) { const d = new Date(ts); return `${z2(d.getDate())}.${z2(d.getMonth() + 1)}.${d.getFullYear()}`; } /** * Создаёт HTML для аватара * @param {string} url - URL аватара * @returns {string} HTML аватара */ export function avatarHTML(url) { return url ? `` : ``; } /** * Создаёт HTML для имени пользователя с ссылкой * @param {number|string} userId - ID пользователя * @param {string} name - Имя пользователя * @param {string} url - URL профиля * @param {number|string} meId - ID текущего пользователя * @param {string} profileUrl - URL профиля текущего пользователя * @param {string} detailUrlTemplate - Шаблон URL деталей сотрудника * @returns {string} HTML имени с ссылкой */ export function nameHTML(userId, name, url, meId, profileUrl, detailUrlTemplate) { if (Number(userId) === Number(meId)) { return `Вы`; } if (url) { return `${esc(name || 'Сотрудник')}`; } const detailUrl = detailUrlTemplate.replace('/0/', `/${userId}/`); return `${esc(name || 'Сотрудник')}`; } /** * Форматирует размер файла * @param {number} bytes - Размер в байтах * @returns {string} Размер в человекочитаемом формате */ export function formatFileSize(bytes) { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'; return (bytes / (1024 * 1024)).toFixed(1) + ' MB'; } /** * Создаёт разделитель дня * @param {string} dateStr - Дата в текстовом формате * @returns {HTMLElement} DOM элемент разделителя */ export function createDayDivider(dateStr) { const div = document.createElement('div'); div.className = 'day-divider'; div.innerHTML = `${esc(dateStr)}`; return div; } /** * Создаёт элемент сообщения * @param {Object} msg - Объект сообщения * @param {number|string} msg.id - ID сообщения * @param {number|string} msg.author_id - ID автора * @param {string} msg.author_name - Имя автора * @param {string} msg.author - Альтернативное имя автора * @param {string} msg.author_url - URL профиля автора * @param {string} msg.avatar - URL аватара автора * @param {string} msg.content - Текст сообщения * @param {Object} options - Опции рендеринга * @param {number|string} options.meId - ID текущего пользователя * @param {string} options.profileUrl - URL профиля текущего пользователя * @param {string} options.detailUrlTemplate - Шаблон URL деталей сотрудника * @param {Object.} options.avatarMap - Карта аватаров * @returns {HTMLElement} DOM элемент сообщения */ export function createMessageElement(msg, options = {}) { const { meId, profileUrl = '/employees/profile/', detailUrlTemplate = '/employees/0/', avatarMap = {} } = options; const mine = Number(msg.author_id) === Number(meId); const isPending = Boolean(msg.is_pending); const ts = toTimestamp(msg); // Создание обёртки const wrap = document.createElement('div'); wrap.className = `d-flex mb-3 msg ${mine ? '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)); } wrap.setAttribute('data-ts', String(ts)); wrap.setAttribute('data-author-id', String(msg.author_id || '')); // Формирование контента const who = nameHTML( msg.author_id, msg.author_name || msg.author, msg.author_url, meId, profileUrl, detailUrlTemplate ); const time = formatTime(ts); const text = esc(msg.content || '').replace(/\n/g, '
'); // Формирование информации о пересылке let forwardedHTML = ''; if (msg.is_forwarded && msg.forwarded_from) { const fwd = msg.forwarded_from; const fwdAuthor = esc(fwd.author_name || 'Неизвестный'); const fwdTime = fwd.created_at ? fwd.created_at : ''; const fwdChat = fwd.chat_name ? ` из «${esc(fwd.chat_name)}»` : ''; forwardedHTML = `
Переслано от ${fwdAuthor}${fwdChat}
${fwdTime ? `
${fwdTime}
` : ''}
`; } // Формирование информации об ответе let replyHTML = ''; if (msg.reply_to) { const reply = msg.reply_to; const replyAuthor = esc(reply.author_name || 'Неизвестный'); const replyContent = esc(reply.content || ''); const replyPreview = replyContent.substring(0, 50); const replyFull = replyContent.length > 50 ? replyPreview + '...' : replyPreview; replyHTML = `
${replyAuthor}
${replyFull}
`; } // Формирование вложений let attachmentsHTML = ''; if (msg.has_attachments && msg.attachments && msg.attachments.length > 0) { attachmentsHTML = '
'; msg.attachments.forEach((att, index) => { attachmentsHTML += '
'; if (att.file_type === 'image') { attachmentsHTML += ` ${esc(att.file_name)} `; } else if (att.file_type === 'audio') { attachmentsHTML += `
${esc(att.file_name)}
`; } else if (att.file_type === 'video') { attachmentsHTML += `
${esc(att.file_name)}
`; } else { const fileSize = att.file_size ? formatFileSize(att.file_size) : ''; attachmentsHTML += `
${esc(att.file_name)}
${fileSize}
`; } attachmentsHTML += '
'; }); attachmentsHTML += '
'; } const pendingStatus = isPending ? '
Отправляем…
' : ''; // Рендер голосования let pollHTML = ''; if (msg.poll) { const poll = msg.poll; pollHTML += `
${esc(poll.question)}
`; poll.options.forEach(option => { pollHTML += `
`; }); pollHTML += `
`; } const bubble = `
${who} · ${time}
${replyHTML} ${forwardedHTML} ${text ? text : ''} ${pollHTML} ${attachmentsHTML} ${pendingStatus}
`; // Аватар const avaUrl = msg.avatar || avatarMap[msg.author_id] || ""; const ava = avatarHTML(avaUrl); // Финальная сборка if (!mine) { const link = msg.author_url || detailUrlTemplate.replace('/0/', `/${msg.author_id}/`); wrap.innerHTML = `${ava}${bubble}`; } else { wrap.innerHTML = `${bubble}${ava}`; } return wrap; }