/**
* DateGroupManager - Менеджер группировки сообщений по датам
*
* Основано на архитектуре Telegram Web:
* - Группирует сообщения в date-группы
* - Управляет DOM структурой message-date-group
* - Поддерживает sticky positioning
*
* Структура DOM:
*
*
* @module managers/dateGroupManager
*/
import { DateDividerManager } from '../utils/dateDividers.js';
/**
* Менеджер для группировки сообщений по датам
*/
export class DateGroupManager {
/**
* @param {Object} options - Опции конфигурации
* @param {HTMLElement} options.container - Контейнер для групп
* @param {boolean} [options.enableSticky=true] - Включить sticky positioning
* @param {boolean} [options.interactive=false] - Сделать даты кликабельными
*/
constructor(options = {}) {
this.container = options.container;
this.enableSticky = options.enableSticky !== false;
this.interactive = options.interactive || false;
// Map для быстрого доступа к группам по дате
// ключ: текст даты ("Сегодня", "Вчера" и т.д.)
// значение: HTMLElement группы
this.dateGroups = new Map();
console.log('[DateGroupManager] Initialized', {
enableSticky: this.enableSticky,
interactive: this.interactive
});
}
/**
* Создает новую date группу
*
* @param {string} dateText - Текст даты ("Сегодня", "Вчера", "5 января 2026")
* @param {number} [timestamp] - Unix timestamp для data-атрибута
* @returns {HTMLElement} DOM элемент группы
*
* @example
* const group = manager.createDateGroup('Сегодня', Date.now());
* container.appendChild(group);
*/
createDateGroup(dateText, timestamp) {
// Основной контейнер группы
const groupEl = document.createElement('div');
groupEl.className = 'message-date-group';
groupEl.setAttribute('data-date', dateText);
if (timestamp) {
groupEl.setAttribute('data-timestamp', timestamp);
}
// Sticky date header
const stickyDate = DateDividerManager.createStickyDate(dateText, this.interactive);
// Контейнер для сообщений внутри группы
const messagesContainer = document.createElement('div');
messagesContainer.className = 'messages-container';
// Собираем структуру
groupEl.appendChild(stickyDate);
groupEl.appendChild(messagesContainer);
// Сохраняем в Map
this.dateGroups.set(dateText, groupEl);
console.log('[DateGroupManager] Created date group:', dateText);
return groupEl;
}
/**
* Получает существующую группу или создает новую
*
* @param {string} dateText - Текст даты
* @param {number} [timestamp] - Unix timestamp
* @returns {HTMLElement} DOM элемент группы
*
* @example
* const group = manager.getOrCreateGroup('Сегодня');
* group.querySelector('.messages-container').appendChild(messageEl);
*/
getOrCreateGroup(dateText, timestamp) {
let group = this.dateGroups.get(dateText);
if (!group) {
group = this.createDateGroup(dateText, timestamp);
// Добавляем в контейнер
if (this.container) {
this.container.appendChild(group);
}
}
return group;
}
/**
* Находит группу по тексту даты
*
* @param {string} dateText - Текст даты
* @returns {HTMLElement|null} DOM элемент группы или null
*/
findGroup(dateText) {
return this.dateGroups.get(dateText) || null;
}
/**
* Добавляет сообщение в соответствующую date группу
*
* @param {HTMLElement} messageEl - DOM элемент сообщения
* @param {string} dateText - Текст даты
* @param {number} [timestamp] - Unix timestamp сообщения
* @param {string} [position='append'] - Позиция: 'append' или 'prepend'
*
* @example
* manager.addMessageToGroup(messageEl, 'Сегодня', Date.now());
*/
addMessageToGroup(messageEl, dateText, timestamp, position = 'append') {
const group = this.getOrCreateGroup(dateText, timestamp);
const messagesContainer = group.querySelector('.messages-container');
if (position === 'prepend') {
messagesContainer.insertBefore(messageEl, messagesContainer.firstChild);
} else {
messagesContainer.appendChild(messageEl);
}
}
/**
* Группирует массив сообщений и создает date groups
* Основной метод для массового рендеринга
*
* @param {Array} messages - Массив сообщений (уже отсортированных)
* @param {Function} renderMessageFn - Функция рендеринга сообщения (msg) => HTMLElement
* @param {string} [position='append'] - Позиция добавления: 'append' или 'prepend'
* @returns {Array} Массив созданных date groups
*
* @example
* const groups = manager.groupAndRender(messages, (msg) => {
* const el = document.createElement('div');
* el.textContent = msg.content;
* return el;
* });
*/
groupAndRender(messages, renderMessageFn, position = 'append') {
const createdGroups = [];
let currentDateText = null;
let currentGroup = null;
for (const message of messages) {
const messageDate = DateDividerManager.getMessageDate(message);
const dateText = DateDividerManager.formatDay(messageDate);
// Если дата изменилась - создаем новую группу
if (dateText !== currentDateText) {
currentGroup = this.getOrCreateGroup(dateText, messageDate.getTime());
createdGroups.push(currentGroup);
currentDateText = dateText;
}
// Рендерим сообщение и добавляем в группу
const messageEl = renderMessageFn(message);
const messagesContainer = currentGroup.querySelector('.messages-container');
if (position === 'prepend') {
messagesContainer.insertBefore(messageEl, messagesContainer.firstChild);
} else {
messagesContainer.appendChild(messageEl);
}
}
console.log('[DateGroupManager] Grouped messages:', {
totalMessages: messages.length,
dateGroups: createdGroups.length
});
return createdGroups;
}
/**
* Обновляет текст даты в группе (для live update после полуночи)
*
* @param {string} oldDateText - Старый текст даты
* @param {string} newDateText - Новый текст даты
*
* @example
* // После полуночи "Сегодня" становится "Вчера"
* manager.updateGroupDate('Сегодня', 'Вчера');
*/
updateGroupDate(oldDateText, newDateText) {
const group = this.dateGroups.get(oldDateText);
if (!group) return;
// Обновляем атрибут
group.setAttribute('data-date', newDateText);
// Обновляем текст в sticky header
const stickyDate = group.querySelector('.sticky-date span');
if (stickyDate) {
stickyDate.textContent = newDateText;
}
// Обновляем Map
this.dateGroups.delete(oldDateText);
this.dateGroups.set(newDateText, group);
console.log('[DateGroupManager] Updated group date:', {
from: oldDateText,
to: newDateText
});
}
/**
* Удаляет группу по тексту даты
*
* @param {string} dateText - Текст даты
* @returns {boolean} true если группа была удалена
*/
removeGroup(dateText) {
const group = this.dateGroups.get(dateText);
if (!group) return false;
group.remove();
this.dateGroups.delete(dateText);
console.log('[DateGroupManager] Removed group:', dateText);
return true;
}
/**
* Удаляет пустые группы (без сообщений)
* Полезно после удаления сообщений
*
* @returns {number} Количество удаленных групп
*
* @example
* const removed = manager.removeEmptyGroups();
* console.log(`Removed ${removed} empty groups`);
*/
removeEmptyGroups() {
let removedCount = 0;
for (const [dateText, group] of this.dateGroups.entries()) {
const messagesContainer = group.querySelector('.messages-container');
if (!messagesContainer.hasChildNodes()) {
group.remove();
this.dateGroups.delete(dateText);
removedCount++;
}
}
if (removedCount > 0) {
console.log('[DateGroupManager] Removed empty groups:', removedCount);
}
return removedCount;
}
/**
* Очищает все группы
*
* @example
* manager.clear(); // Полная очистка перед перерисовкой
*/
clear() {
for (const group of this.dateGroups.values()) {
group.remove();
}
this.dateGroups.clear();
console.log('[DateGroupManager] Cleared all groups');
}
/**
* Получает все date groups в порядке появления в DOM
*
* @returns {Array} Массив групп
*/
getAllGroups() {
return Array.from(this.dateGroups.values());
}
/**
* Получает количество групп
*
* @returns {number}
*/
getGroupCount() {
return this.dateGroups.size;
}
/**
* Получает контейнер сообщений для конкретной даты
*
* @param {string} dateText - Текст даты
* @returns {HTMLElement|null} Контейнер сообщений или null
*
* @example
* const container = manager.getMessagesContainer('Сегодня');
* if (container) {
* console.log('Messages count:', container.children.length);
* }
*/
getMessagesContainer(dateText) {
const group = this.findGroup(dateText);
return group ? group.querySelector('.messages-container') : null;
}
/**
* Вставляет группу в правильное место (по timestamp)
* Используется при prepend сообщений (загрузка старых)
*
* @param {HTMLElement} newGroup - Новая группа
* @param {number} timestamp - Unix timestamp группы
*
* @example
* const oldGroup = manager.createDateGroup('5 января', oldTimestamp);
* manager.insertGroupSorted(oldGroup, oldTimestamp);
*/
insertGroupSorted(newGroup, timestamp) {
if (!this.container) {
console.warn('[DateGroupManager] No container set');
return;
}
const existingGroups = this.container.querySelectorAll('.message-date-group');
// Ищем правильное место для вставки
let insertBefore = null;
for (const group of existingGroups) {
const groupTimestamp = parseInt(group.getAttribute('data-timestamp') || '0');
if (timestamp < groupTimestamp) {
insertBefore = group;
break;
}
}
if (insertBefore) {
this.container.insertBefore(newGroup, insertBefore);
} else {
this.container.appendChild(newGroup);
}
}
/**
* Обновляет интерактивность дат (кликабельность)
*
* @param {boolean} interactive - Включить/выключить интерактивность
*/
setInteractive(interactive) {
this.interactive = interactive;
// Обновляем все существующие группы
for (const group of this.dateGroups.values()) {
const stickyDate = group.querySelector('.sticky-date');
if (stickyDate) {
if (interactive) {
stickyDate.classList.add('interactive');
} else {
stickyDate.classList.remove('interactive');
}
}
}
console.log('[DateGroupManager] Interactive mode:', interactive);
}
/**
* Получает статистику по группам
*
* @returns {Object} Статистика
*/
getStats() {
const stats = {
totalGroups: this.dateGroups.size,
groups: []
};
for (const [dateText, group] of this.dateGroups.entries()) {
const messagesContainer = group.querySelector('.messages-container');
stats.groups.push({
date: dateText,
messageCount: messagesContainer.children.length,
timestamp: group.getAttribute('data-timestamp')
});
}
return stats;
}
}
export default DateGroupManager;