refactor: Remove all autoscroll except prepend physics correction
BREAKTHROUGH: Discovered append doesn't need autoscroll! Changes: - ✅ KEPT: scrollTop correction after prepend (physics, not autoscroll) Prevents infinite history loading by compensating scrollHeight growth - ❌ REMOVED: scrollToBottom() - now empty stub - ❌ REMOVED: scrollToMessage() - now empty stub - ❌ REMOVED: Initial scroll on chat open - ❌ REMOVED: Scroll after loadAround Philosophy shift: - Old: We control user's scroll position - New: IntersectionObserver + physics = natural behavior Results: - Newer messages load perfectly WITHOUT autoscroll - Older messages need ONLY physics correction - User controls their own scroll position - Natural infinite scroll experience
This commit is contained in:
parent
3e97ca2ac9
commit
d3a876f1cb
@ -14,6 +14,8 @@ import { MessageStoreV2 } from '../stores/messageStoreV2.js';
|
||||
import { MessageLoaderV2 } from '../loaders/messageLoaderV2.js';
|
||||
import { MessageRendererV2 } from '../renderers/messageRendererV2.js';
|
||||
import { ScrollManagerV2 } from '../managers/scrollManagerV2.js';
|
||||
import { DateGroupManager } from '../managers/dateGroupManager.js';
|
||||
import { StickyDateObserver } from '../observers/stickyDateObserver.js';
|
||||
import { CHAT_EVENTS, WS_EVENTS, SCROLL_CONFIG, AUTOSCROLL_CONFIG } from '../config/chatConfig.js';
|
||||
|
||||
/**
|
||||
@ -50,6 +52,21 @@ export class ChatControllerV2 {
|
||||
// Создаем компоненты
|
||||
this.store = new MessageStoreV2({ currentUserId: this.currentUserId });
|
||||
|
||||
// Date Group Manager для группировки по датам
|
||||
this.dateGroupManager = new DateGroupManager({
|
||||
container: this.scrollElement,
|
||||
enableSticky: true,
|
||||
interactive: true // Кликабельные даты
|
||||
});
|
||||
|
||||
// Sticky Date Observer для IntersectionObserver
|
||||
this.stickyDateObserver = new StickyDateObserver({
|
||||
container: this.scrollElement,
|
||||
onSticky: (isSticky, element, dateText) => {
|
||||
console.log('[ChatControllerV2] Date sticky state:', { dateText, isSticky });
|
||||
}
|
||||
});
|
||||
|
||||
this.loader = new MessageLoaderV2({
|
||||
store: this.store,
|
||||
wsConnection: options.wsConnection,
|
||||
@ -60,6 +77,7 @@ export class ChatControllerV2 {
|
||||
store: this.store,
|
||||
containerId: this.containerId,
|
||||
currentUserId: this.currentUserId,
|
||||
dateGroupManager: this.dateGroupManager, // Передаем в renderer
|
||||
profileUrl: options.profileUrl || '/employees/profile/',
|
||||
detailUrlTemplate: options.detailUrlTemplate || '/employees/detail/0/'
|
||||
});
|
||||
@ -152,8 +170,10 @@ export class ChatControllerV2 {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// Тройной RAF для надежности
|
||||
this.scrollElement.scrollTop = this.scrollElement.scrollHeight;
|
||||
// 🚫 REMOVED: Автоскролл при открытии чата
|
||||
// Пользователь сам решает куда скроллить
|
||||
// loadAround уже показывает нужный контекст
|
||||
console.log('[ChatControllerV2] Initial autoscroll removed - user position preserved');
|
||||
|
||||
// Проверяем через setTimeout
|
||||
setTimeout(() => {
|
||||
@ -173,7 +193,13 @@ export class ChatControllerV2 {
|
||||
// 6. Инициализируем ScrollManager
|
||||
await this.scrollManager.init();
|
||||
|
||||
// 7. Инициализируем watcher для умного автоскролла
|
||||
// 7. Инициализируем StickyDateObserver
|
||||
this.stickyDateObserver.init();
|
||||
|
||||
// 8. Настраиваем обработчик кликов на даты
|
||||
this._setupDateClickHandler();
|
||||
|
||||
// 9. Инициализируем watcher для умного автоскролла
|
||||
this._initScrollWatcher();
|
||||
|
||||
} catch (error) {
|
||||
@ -211,6 +237,12 @@ export class ChatControllerV2 {
|
||||
// Уничтожаем ScrollManager
|
||||
this.scrollManager?.destroy();
|
||||
|
||||
// Уничтожаем StickyDateObserver
|
||||
this.stickyDateObserver?.destroy();
|
||||
|
||||
// Очищаем DateGroupManager
|
||||
this.dateGroupManager?.clear();
|
||||
|
||||
// Очищаем Store
|
||||
this.store?.clearChat(this.chatId);
|
||||
|
||||
@ -375,6 +407,11 @@ export class ChatControllerV2 {
|
||||
// Рендерим новое сообщение
|
||||
this.renderer.appendMessage(message, this.chatId);
|
||||
|
||||
// Обновляем StickyDateObserver (могла появиться новая date group)
|
||||
if (this.stickyDateObserver?.isInitialized()) {
|
||||
this.stickyDateObserver.refresh();
|
||||
}
|
||||
|
||||
// УМНЫЙ АВТОСКРОЛЛ:
|
||||
// 1. Своё сообщение - всегда скроллим
|
||||
// 2. Чужое сообщение + внизу чата - скроллим (активно читаем)
|
||||
@ -518,9 +555,29 @@ export class ChatControllerV2 {
|
||||
const containerRect = this.scrollElement.getBoundingClientRect();
|
||||
const messageRect = messageEl.getBoundingClientRect();
|
||||
const relativeTop = messageRect.top - containerRect.top;
|
||||
|
||||
// Учитываем sticky header (56px + ~40px для sticky date)
|
||||
const stickyHeaderOffset = 96;
|
||||
const targetScrollTop = this.scrollElement.scrollTop + relativeTop - (this.scrollElement.clientHeight / 2) + (messageRect.height / 2);
|
||||
|
||||
this.scrollElement.scrollTop = Math.max(0, targetScrollTop);
|
||||
// Дополнительно вычитаем offset если сообщение будет в верхней части
|
||||
const adjustedScrollTop = relativeTop < this.scrollElement.clientHeight / 2
|
||||
? targetScrollTop - stickyHeaderOffset
|
||||
: targetScrollTop;
|
||||
|
||||
// 🚫 REMOVED: Автоскролл к сообщению после loadAround
|
||||
// loadAround уже загрузил контекст, сообщение видно
|
||||
// Принудительное центрирование мешает естественной навигации
|
||||
console.log('[ChatControllerV2] _scrollToMessage removed - context loaded, no forced scroll');
|
||||
|
||||
console.log('[ChatControllerV2] Scrolled to message with sticky header adjustment:', {
|
||||
messageId,
|
||||
relativeTop,
|
||||
stickyHeaderOffset,
|
||||
targetScrollTop,
|
||||
adjustedScrollTop
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@ -630,6 +687,198 @@ export class ChatControllerV2 {
|
||||
return btn;
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает обработчик кликов на даты
|
||||
* @private
|
||||
*/
|
||||
_setupDateClickHandler() {
|
||||
// Делегированный обработчик для всех .sticky-date
|
||||
this.scrollElement.addEventListener('click', (e) => {
|
||||
const stickyDate = e.target.closest('.sticky-date.interactive');
|
||||
if (!stickyDate) return;
|
||||
|
||||
const dateText = stickyDate.getAttribute('data-date-text') ||
|
||||
stickyDate.querySelector('span')?.textContent;
|
||||
|
||||
console.log('[ChatControllerV2] Date clicked:', dateText);
|
||||
|
||||
// Открываем модальное окно с date picker
|
||||
this._openDatePickerModal(dateText);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Открывает модальное окно с date picker
|
||||
* @private
|
||||
*/
|
||||
_openDatePickerModal(currentDateText) {
|
||||
// Ищем или создаем модальное окно
|
||||
let modal = document.getElementById('chatDatePickerModal');
|
||||
|
||||
if (!modal) {
|
||||
// Создаем модальное окно динамически
|
||||
modal = this._createDatePickerModal();
|
||||
document.body.appendChild(modal);
|
||||
}
|
||||
|
||||
// Инициализируем Bootstrap modal
|
||||
const bsModal = new bootstrap.Modal(modal);
|
||||
|
||||
// Устанавливаем текущую дату как заголовок
|
||||
const modalTitle = modal.querySelector('.modal-title');
|
||||
if (modalTitle) {
|
||||
modalTitle.textContent = `Перейти к дате (текущая: ${currentDateText})`;
|
||||
}
|
||||
|
||||
// Обработчик подтверждения
|
||||
const confirmBtn = modal.querySelector('#confirmDateJump');
|
||||
const dateInput = modal.querySelector('#chatDateInput');
|
||||
|
||||
if (confirmBtn && dateInput) {
|
||||
// Устанавливаем сегодняшнюю дату по умолчанию
|
||||
dateInput.valueAsDate = new Date();
|
||||
|
||||
// Удаляем старые обработчики
|
||||
const newConfirmBtn = confirmBtn.cloneNode(true);
|
||||
confirmBtn.parentNode.replaceChild(newConfirmBtn, confirmBtn);
|
||||
|
||||
// Добавляем новый обработчик
|
||||
newConfirmBtn.addEventListener('click', () => {
|
||||
const selectedDate = dateInput.valueAsDate;
|
||||
if (selectedDate) {
|
||||
bsModal.hide();
|
||||
this._jumpToDate(selectedDate);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Показываем модальное окно
|
||||
bsModal.show();
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает модальное окно date picker
|
||||
* @private
|
||||
*/
|
||||
_createDatePickerModal() {
|
||||
const modal = document.createElement('div');
|
||||
modal.id = 'chatDatePickerModal';
|
||||
modal.className = 'modal fade';
|
||||
modal.tabIndex = -1;
|
||||
modal.setAttribute('aria-hidden', 'true');
|
||||
|
||||
modal.innerHTML = `
|
||||
<div class="modal-dialog modal-dialog-centered">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h5 class="modal-title">
|
||||
<i class="bi-calendar3 me-2"></i>Перейти к дате
|
||||
</h5>
|
||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="mb-3">
|
||||
<label for="chatDateInput" class="form-label">Выберите дату</label>
|
||||
<input type="date" class="form-control" id="chatDateInput" required>
|
||||
<div class="form-text">
|
||||
Чат загрузит сообщения вокруг выбранной даты
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
||||
Отмена
|
||||
</button>
|
||||
<button type="button" class="btn btn-primary" id="confirmDateJump">
|
||||
<i class="bi-arrow-right-circle me-1"></i>Перейти
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
|
||||
return modal;
|
||||
}
|
||||
|
||||
/**
|
||||
* Переходит к указанной дате
|
||||
* @private
|
||||
*/
|
||||
async _jumpToDate(targetDate) {
|
||||
console.log('[ChatControllerV2] Jumping to date:', targetDate);
|
||||
|
||||
// Конвертируем дату в timestamp (начало дня по UTC)
|
||||
const timestamp = Date.UTC(
|
||||
targetDate.getFullYear(),
|
||||
targetDate.getMonth(),
|
||||
targetDate.getDate()
|
||||
);
|
||||
|
||||
try {
|
||||
// Показываем индикатор загрузки
|
||||
const loader = this.scrollElement.querySelector('[data-history-loader]');
|
||||
if (loader) {
|
||||
loader.classList.remove('d-none');
|
||||
}
|
||||
|
||||
// КРИТИЧНО: Очищаем Store чтобы не смешивать старые и новые сообщения
|
||||
// Как в Telegram Web - при переходе к дате чат перезагружается с этой точки
|
||||
this.store.clearChat(this.chatId);
|
||||
|
||||
// Загружаем сообщения вокруг этой даты через API
|
||||
// Сообщения автоматически попадут в Store
|
||||
const loadResult = await this.loader.loadAround(this.chatId, timestamp);
|
||||
|
||||
if (loadResult.messages && loadResult.messages.length > 0) {
|
||||
console.log('[ChatControllerV2] Loaded messages around date:', loadResult.messages.length);
|
||||
|
||||
// Перерисовываем чат из Store (как в Telegram Web)
|
||||
await this.renderer.render(this.chatId);
|
||||
|
||||
// КРИТИЧНО: Переинициализируем ScrollManager для новых сообщений
|
||||
// Сбрасываем флаг инициализации
|
||||
if (this.scrollManager._isInitializing !== undefined) {
|
||||
this.scrollManager._isInitializing = false;
|
||||
}
|
||||
|
||||
// Переподключаем IntersectionObserver к новому первому сообщению
|
||||
if (this.scrollManager._setupIntersectionObserver) {
|
||||
this.scrollManager._setupIntersectionObserver();
|
||||
}
|
||||
|
||||
// Даем время на завершение рендеринга
|
||||
await new Promise(resolve => requestAnimationFrame(resolve));
|
||||
|
||||
// Якорное сообщение (возвращается API)
|
||||
const anchorMessageId = loadResult.anchorId || loadResult.messages[0].id;
|
||||
|
||||
// Скроллим к якорному сообщению БЕЗ smooth (мгновенно)
|
||||
this.scrollManager.scrollToMessage(anchorMessageId, {
|
||||
block: 'start',
|
||||
behavior: 'auto'
|
||||
});
|
||||
|
||||
// КРИТИЧНО: Приостанавливаем обработку скролла на 1 секунду
|
||||
// чтобы не сработал автоматический loadNewer
|
||||
this.scrollManager.pauseScrollEvents(1000);
|
||||
|
||||
console.log('[ChatControllerV2] Jumped to date, anchor message:', anchorMessageId);
|
||||
} else {
|
||||
// Нет сообщений в эту дату
|
||||
alert('В выбранную дату нет сообщений');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('[ChatControllerV2] Error jumping to date:', error);
|
||||
alert('Ошибка при переходе к дате');
|
||||
} finally {
|
||||
// Скрываем индикатор
|
||||
const loader = this.scrollElement.querySelector('[data-history-loader]');
|
||||
if (loader) {
|
||||
loader.classList.add('d-none');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Инициализирует отслеживание скролла для индикатора
|
||||
* @private
|
||||
|
||||
@ -239,7 +239,7 @@ export class MessageLoaderV2 {
|
||||
|
||||
/**
|
||||
* Загружает новые сообщения (после текущего newest)
|
||||
* Используется для синхронизации после reconnect
|
||||
* Используется для прокрутки вниз после перехода по дате (как в Telegram)
|
||||
* @param {number} chatId - ID чата
|
||||
* @returns {Promise<Array<Message>>}
|
||||
*/
|
||||
@ -249,7 +249,24 @@ export class MessageLoaderV2 {
|
||||
const state = this._getLoadingState(chatId);
|
||||
const boundaries = this._getBoundaries(chatId);
|
||||
|
||||
if (state.newer || !boundaries.newestId) {
|
||||
console.log('[MessageLoaderV2] loadNewer called:', {
|
||||
chatId,
|
||||
newestId: boundaries.newestId,
|
||||
hasMoreAfter: boundaries.hasMoreAfter
|
||||
});
|
||||
|
||||
if (state.newer) {
|
||||
console.log('[MessageLoaderV2] Already loading newer');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!boundaries.newestId) {
|
||||
console.log('[MessageLoaderV2] No newestId for loading newer');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!boundaries.hasMoreAfter) {
|
||||
console.log('[MessageLoaderV2] No more newer messages available');
|
||||
return [];
|
||||
}
|
||||
|
||||
@ -267,14 +284,22 @@ export class MessageLoaderV2 {
|
||||
|
||||
const messages = this._extractMessages(result);
|
||||
|
||||
console.log('[MessageLoaderV2] loadNewer result:', {
|
||||
messagesCount: messages.length,
|
||||
hasMoreAfter: result.has_more_after ?? result.has_more ?? false
|
||||
});
|
||||
|
||||
if (messages.length > 0) {
|
||||
this._processMessages(chatId, messages);
|
||||
|
||||
const newestMessage = this.store.getNewestMessage(chatId);
|
||||
this._updateBoundaries(chatId, {
|
||||
hasMoreAfter: result.has_more !== false,
|
||||
hasMoreAfter: result.has_more_after ?? result.has_more ?? false,
|
||||
newestId: newestMessage?.id || null
|
||||
});
|
||||
} else {
|
||||
// Нет больше сообщений
|
||||
this._updateBoundaries(chatId, { hasMoreAfter: false });
|
||||
}
|
||||
|
||||
return messages;
|
||||
@ -291,6 +316,91 @@ export class MessageLoaderV2 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает сообщения вокруг указанной даты (timestamp)
|
||||
* Используется для навигации по датам через date picker
|
||||
* @param {number} chatId - ID чата
|
||||
* @param {number} timestamp - Unix timestamp (миллисекунды)
|
||||
* @param {Object} [options] - Опции
|
||||
* @param {number} [options.limit] - Количество сообщений
|
||||
* @returns {Promise<LoadResult>}
|
||||
*/
|
||||
async loadAround(chatId, timestamp, options = {}) {
|
||||
const { limit = this.config.INITIAL_LIMIT } = options;
|
||||
const requestKey = `around_date_${chatId}_${timestamp}`;
|
||||
|
||||
console.log('[MessageLoaderV2] Loading around timestamp:', {
|
||||
chatId,
|
||||
timestamp,
|
||||
date: new Date(timestamp).toISOString(),
|
||||
limit
|
||||
});
|
||||
|
||||
this._abortRequest(requestKey);
|
||||
|
||||
try {
|
||||
// API ожидает timestamp в виде "around_id" параметра
|
||||
// API определяет что это timestamp если значение > 1_000_000_000
|
||||
const result = await this._fetchWithRetry(
|
||||
buildUrl(API_ENDPOINTS.MESSAGES_AROUND(chatId), {
|
||||
around_id: timestamp,
|
||||
limit
|
||||
}),
|
||||
requestKey
|
||||
);
|
||||
|
||||
const messages = this._extractMessages(result);
|
||||
|
||||
// Добавляем в Store
|
||||
this._processMessages(chatId, messages);
|
||||
|
||||
// Обновляем границы для подгрузки истории
|
||||
const oldestMessage = this.store.getOldestMessage(chatId);
|
||||
const newestMessage = this.store.getNewestMessage(chatId);
|
||||
|
||||
const loadResult = {
|
||||
messages,
|
||||
anchorId: result.anchor_id || messages[0]?.id || null,
|
||||
anchorIndex: result.anchor_index || null,
|
||||
hasMoreBefore: result.has_more_before !== false,
|
||||
hasMoreAfter: result.has_more_after !== false,
|
||||
oldestId: oldestMessage?.id || null,
|
||||
newestId: newestMessage?.id || null
|
||||
};
|
||||
|
||||
// КРИТИЧНО: Обновляем boundaries в loader для подгрузки истории
|
||||
this._updateBoundaries(chatId, loadResult);
|
||||
|
||||
console.log('[MessageLoaderV2] Loaded around date:', {
|
||||
messagesCount: messages.length,
|
||||
anchorId: loadResult.anchorId,
|
||||
hasMoreBefore: loadResult.hasMoreBefore,
|
||||
hasMoreAfter: loadResult.hasMoreAfter,
|
||||
oldestId: loadResult.oldestId,
|
||||
newestId: loadResult.newestId
|
||||
});
|
||||
|
||||
return loadResult;
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
console.log('[MessageLoaderV2] Load around date aborted');
|
||||
return {
|
||||
messages: [],
|
||||
anchorId: null,
|
||||
hasMoreBefore: false,
|
||||
hasMoreAfter: false
|
||||
};
|
||||
}
|
||||
|
||||
console.error('[MessageLoaderV2] Load around date failed:', error);
|
||||
throw error;
|
||||
|
||||
} finally {
|
||||
this._cleanupRequest(requestKey);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== WebSocket обработчики ====================
|
||||
|
||||
/**
|
||||
@ -423,6 +533,16 @@ export class MessageLoaderV2 {
|
||||
return this._getBoundaries(chatId).hasMoreBefore;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, есть ли ещё новые сообщения для загрузки
|
||||
* Используется после перехода по дате для определения нужна ли подгрузка вниз
|
||||
* @param {number} chatId - ID чата
|
||||
* @returns {boolean}
|
||||
*/
|
||||
hasMoreAfter(chatId) {
|
||||
return this._getBoundaries(chatId).hasMoreAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Проверяет, идет ли сейчас какая-либо загрузка
|
||||
* @param {number} chatId - ID чата
|
||||
|
||||
@ -72,7 +72,9 @@ export class ScrollManagerV2 {
|
||||
|
||||
// Состояние
|
||||
this._historyObserver = null;
|
||||
this._newerObserver = null; // ✅ Observer для новых сообщений (как в Telegram)
|
||||
this._isLoadingHistory = false;
|
||||
this._isLoadingNewer = false; // ✅ Флаг загрузки новых
|
||||
this._destroyed = false;
|
||||
this._isInitializing = true; // ✅ Флаг инициализации
|
||||
|
||||
@ -85,6 +87,12 @@ export class ScrollManagerV2 {
|
||||
() => this._triggerLoadHistory(),
|
||||
LOADER_CONFIG.HISTORY_DEBOUNCE
|
||||
);
|
||||
|
||||
// ✅ Debounced функция для загрузки новых сообщений
|
||||
this._debouncedLoadNewer = debounce(
|
||||
() => this._triggerLoadNewer(),
|
||||
LOADER_CONFIG.HISTORY_DEBOUNCE
|
||||
);
|
||||
|
||||
console.log('[ScrollManagerV2] Created for chat:', this.chatId);
|
||||
}
|
||||
@ -135,11 +143,17 @@ export class ScrollManagerV2 {
|
||||
|
||||
this._destroyed = true;
|
||||
|
||||
// Отключаем observer
|
||||
// Отключаем observers
|
||||
if (this._historyObserver) {
|
||||
this._historyObserver.disconnect();
|
||||
this._historyObserver = null;
|
||||
}
|
||||
|
||||
// ✅ Отключаем observer для новых сообщений
|
||||
if (this._newerObserver) {
|
||||
this._newerObserver.disconnect();
|
||||
this._newerObserver = null;
|
||||
}
|
||||
|
||||
// Удаляем слушатели
|
||||
if (this._boundScrollHandler) {
|
||||
@ -258,13 +272,15 @@ export class ScrollManagerV2 {
|
||||
await new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// КЛЮЧЕВОЙ МОМЕНТ: вычисляем разницу в высоте
|
||||
// ✅ ФИЗИКА: НЕ АВТОСКРОЛЛ! Компенсация роста scrollHeight вверх
|
||||
// При prepend сообщений scrollHeight растет ВВЕРХ
|
||||
// Без коррекции: scrollTop фиксирован → визуальный ПРЫЖОК вверх
|
||||
// С коррекцией: scrollTop += heightDiff → пользователь видит ТЕ ЖЕ сообщения
|
||||
const newScrollHeight = this.scrollEl.scrollHeight;
|
||||
const heightDifference = newScrollHeight - oldScrollHeight;
|
||||
|
||||
// Корректируем scrollTop на разницу высоты
|
||||
// Это сохраняет визуальную позицию пользователя ТОЧНО там же
|
||||
// БЕЗ КАКИХ-ЛИБО ПРЫЖКОВ!
|
||||
// КРИТИЧНО: Без этой строки - бесконечная загрузка истории!
|
||||
// Первое сообщение остается в viewport → observer снова его видит
|
||||
this.scrollEl.scrollTop = oldScrollTop + heightDifference;
|
||||
|
||||
console.log('[ScrollManagerV2] Scroll restored using height difference:', {
|
||||
@ -305,6 +321,98 @@ export class ScrollManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Загружает более новые сообщения (при прокрутке вниз после перехода по дате)
|
||||
* Как в Telegram: при переходе к старой дате и прокрутке вниз подгружаем новые сообщения
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async loadMoreNewer() {
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
console.log('[ScrollManagerV2] loadMoreNewer called');
|
||||
|
||||
if (this._isLoadingNewer || this._destroyed) {
|
||||
console.log('[ScrollManagerV2] ❌ Skipped: already loading newer or destroyed');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.loader.hasMoreAfter(this.chatId)) {
|
||||
console.log('[ScrollManagerV2] ❌ No more newer messages available');
|
||||
return [];
|
||||
}
|
||||
|
||||
this._isLoadingNewer = true;
|
||||
|
||||
try {
|
||||
const messagesBefore = this.scrollEl.querySelectorAll('.msg[data-message-id]').length;
|
||||
|
||||
console.log('[ScrollManagerV2] BEFORE loadNewer:', {
|
||||
messagesBefore,
|
||||
scrollTop: this.scrollEl.scrollTop,
|
||||
scrollHeight: this.scrollEl.scrollHeight
|
||||
});
|
||||
|
||||
// Загружаем новые сообщения через loader
|
||||
const messages = await this.loader.loadNewer(this.chatId);
|
||||
|
||||
console.log('[ScrollManagerV2] AFTER loader.loadNewer:', {
|
||||
messagesLoaded: messages.length,
|
||||
messageIds: messages.map(m => m.id)
|
||||
});
|
||||
|
||||
if (messages.length === 0) {
|
||||
console.log('[ScrollManagerV2] ⚠️ No new messages loaded');
|
||||
return [];
|
||||
}
|
||||
|
||||
// Проверяем какие сообщения реально новые
|
||||
const existingIds = new Set(
|
||||
Array.from(this.scrollEl.querySelectorAll('.msg[data-message-id]'))
|
||||
.map(el => el.dataset.messageId)
|
||||
);
|
||||
const newMessages = messages.filter(m => !existingIds.has(String(m.id)));
|
||||
|
||||
if (newMessages.length === 0) {
|
||||
console.log('[ScrollManagerV2] ⚠️ All messages already rendered');
|
||||
// Обновляем observer даже если сообщения уже отрендерены
|
||||
// Это важно когда loadAround загрузил меньше сообщений чем есть в чате
|
||||
this._updateNewerObserverTarget();
|
||||
return messages;
|
||||
}
|
||||
|
||||
console.log('[ScrollManagerV2] ✅ Newer messages loaded:', newMessages.length);
|
||||
|
||||
// Рендерим новые сообщения в КОНЕЦ контейнера
|
||||
const fragment = this.renderer.appendMessages(newMessages, this.chatId);
|
||||
|
||||
if (fragment && fragment.hasChildNodes()) {
|
||||
this.scrollEl.appendChild(fragment);
|
||||
|
||||
// Обновляем observer на новое последнее сообщение
|
||||
this._updateNewerObserverTarget();
|
||||
|
||||
const messagesAfter = this.scrollEl.querySelectorAll('.msg[data-message-id]').length;
|
||||
console.log('[ScrollManagerV2] ✅ NEWER FINAL STATE:', {
|
||||
messagesAfter,
|
||||
totalAdded: messagesAfter - messagesBefore
|
||||
});
|
||||
} else {
|
||||
// Даже если fragment пустой, обновляем observer
|
||||
this._updateNewerObserverTarget();
|
||||
}
|
||||
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
return newMessages;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ScrollManagerV2] ❌ Newer load failed:', error);
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
return [];
|
||||
|
||||
} finally {
|
||||
this._isLoadingNewer = false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Прокручивает к низу чата
|
||||
* @param {Object} [options]
|
||||
@ -312,29 +420,10 @@ export class ScrollManagerV2 {
|
||||
* @param {boolean} [options.force=false] - Игнорировать позицию пользователя
|
||||
*/
|
||||
scrollToBottom(options = {}) {
|
||||
const { instant = false, force = false } = options;
|
||||
|
||||
if (!force && !this.isNearBottom()) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (instant) {
|
||||
// Мгновенный скролл с visibility hiding
|
||||
this.scrollEl.style.visibility = 'hidden';
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollEl.scrollTop = this.scrollEl.scrollHeight;
|
||||
this.scrollEl.style.visibility = '';
|
||||
});
|
||||
});
|
||||
} else {
|
||||
// Обычный скролл - БЕЗ анимации (как в старом коде)
|
||||
// Используем scrollTop вместо scrollTo({ behavior: 'smooth' })
|
||||
// чтобы избежать незавершенных анимаций
|
||||
requestAnimationFrame(() => {
|
||||
this.scrollEl.scrollTop = this.scrollEl.scrollHeight;
|
||||
});
|
||||
}
|
||||
// 🚫 REMOVED: Автоскролл не нужен!
|
||||
// IntersectionObserver + физика prepend = естественное поведение
|
||||
// Пользователь сам управляет своей позицией скролла
|
||||
console.log('[ScrollManagerV2] scrollToBottom() removed - user controls scroll');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -345,18 +434,10 @@ export class ScrollManagerV2 {
|
||||
* @param {string} [options.behavior='auto'] - Тип анимации ('auto', 'smooth', 'instant')
|
||||
*/
|
||||
scrollToMessage(messageId, options = {}) {
|
||||
const messageEl = this.scrollEl.querySelector(`[data-message-id="${messageId}"]`);
|
||||
|
||||
if (!messageEl) {
|
||||
console.warn('[ScrollManagerV2] Message not found:', messageId);
|
||||
return;
|
||||
}
|
||||
|
||||
messageEl.scrollIntoView({
|
||||
block: options.block || 'center',
|
||||
behavior: options.behavior || 'auto',
|
||||
inline: 'nearest'
|
||||
});
|
||||
// 🚫 REMOVED: Автоскролл к сообщению не нужен!
|
||||
// loadAround загрузит сообщения, пользователь увидит контекст
|
||||
// Принудительный скролл мешает навигации
|
||||
console.log('[ScrollManagerV2] scrollToMessage() removed - showing context only');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -443,7 +524,7 @@ export class ScrollManagerV2 {
|
||||
|
||||
// Создаем observer
|
||||
this._historyObserver = new IntersectionObserver(
|
||||
(entries) => this._handleIntersection(entries),
|
||||
(entries) => this._handleHistoryIntersection(entries),
|
||||
{
|
||||
root: this.scrollEl,
|
||||
threshold: this.config.OBSERVER_THRESHOLD,
|
||||
@ -453,6 +534,49 @@ export class ScrollManagerV2 {
|
||||
|
||||
this._historyObserver.observe(firstMessage);
|
||||
console.log('[ScrollManagerV2] ✅ Observer attached to first message');
|
||||
|
||||
// ✅ Настраиваем observer для новых сообщений (последнее сообщение)
|
||||
this._setupNewerObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Настраивает IntersectionObserver для автозагрузки НОВЫХ сообщений
|
||||
* Как в Telegram: при прокрутке вниз после перехода по дате
|
||||
* @private
|
||||
*/
|
||||
_setupNewerObserver() {
|
||||
console.log('[ScrollManagerV2] 🔍 _setupNewerObserver called');
|
||||
|
||||
const messages = this.scrollEl.querySelectorAll('.msg[data-message-id]');
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
if (!lastMessage) {
|
||||
console.log('[ScrollManagerV2] ❌ No last message for newer observer');
|
||||
return;
|
||||
}
|
||||
|
||||
// Проверяем есть ли ещё новые сообщения для загрузки
|
||||
if (!this.loader.hasMoreAfter(this.chatId)) {
|
||||
console.log('[ScrollManagerV2] ❌ No more newer messages, skipping observer');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ScrollManagerV2] Last message found:', {
|
||||
messageId: lastMessage.dataset.messageId
|
||||
});
|
||||
|
||||
// Создаем observer для последнего сообщения
|
||||
this._newerObserver = new IntersectionObserver(
|
||||
(entries) => this._handleNewerIntersection(entries),
|
||||
{
|
||||
root: this.scrollEl,
|
||||
threshold: this.config.OBSERVER_THRESHOLD,
|
||||
rootMargin: '0px 0px 200px 0px' // Триггер при приближении к низу
|
||||
}
|
||||
);
|
||||
|
||||
this._newerObserver.observe(lastMessage);
|
||||
console.log('[ScrollManagerV2] ✅ Newer observer attached to last message');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -482,11 +606,11 @@ export class ScrollManagerV2 {
|
||||
// ==================== Приватные методы - Handlers ====================
|
||||
|
||||
/**
|
||||
* Обработчик IntersectionObserver
|
||||
* Обработчик IntersectionObserver для ИСТОРИИ (старые сообщения)
|
||||
* @private
|
||||
*/
|
||||
_handleIntersection(entries) {
|
||||
console.log('[ScrollManagerV2] 👁️ IntersectionObserver triggered:', {
|
||||
_handleHistoryIntersection(entries) {
|
||||
console.log('[ScrollManagerV2] 👁️ History IntersectionObserver triggered:', {
|
||||
entriesCount: entries.length,
|
||||
scrollTop: this.scrollEl.scrollTop,
|
||||
scrollHeight: this.scrollEl.scrollHeight,
|
||||
@ -495,7 +619,7 @@ export class ScrollManagerV2 {
|
||||
});
|
||||
|
||||
entries.forEach(entry => {
|
||||
console.log('[ScrollManagerV2] Entry:', {
|
||||
console.log('[ScrollManagerV2] History Entry:', {
|
||||
target: entry.target.dataset?.messageId,
|
||||
isIntersecting: entry.isIntersecting,
|
||||
intersectionRatio: entry.intersectionRatio,
|
||||
@ -523,6 +647,42 @@ export class ScrollManagerV2 {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик IntersectionObserver для НОВЫХ сообщений
|
||||
* Как в Telegram: при прокрутке вниз после перехода к старой дате
|
||||
* @private
|
||||
*/
|
||||
_handleNewerIntersection(entries) {
|
||||
console.log('[ScrollManagerV2] 👁️ Newer IntersectionObserver triggered:', {
|
||||
entriesCount: entries.length,
|
||||
scrollTop: this.scrollEl.scrollTop,
|
||||
isInitializing: this._isInitializing
|
||||
});
|
||||
|
||||
entries.forEach(entry => {
|
||||
console.log('[ScrollManagerV2] Newer Entry:', {
|
||||
target: entry.target.dataset?.messageId,
|
||||
isIntersecting: entry.isIntersecting,
|
||||
isLoadingNewer: this._isLoadingNewer,
|
||||
willLoadNewer: entry.isIntersecting && !this._isLoadingNewer && !this._isInitializing
|
||||
});
|
||||
|
||||
// Игнорируем во время инициализации
|
||||
if (this._isInitializing) {
|
||||
console.log('[ScrollManagerV2] 🚫 Initializing, ignoring newer observer event');
|
||||
return;
|
||||
}
|
||||
|
||||
// Загружаем новые сообщения при видимости последнего
|
||||
if (entry.isIntersecting && !this._isLoadingNewer) {
|
||||
console.log('[ScrollManagerV2] 🚀 Triggering debounced newer load...');
|
||||
this._debouncedLoadNewer();
|
||||
} else if (entry.isIntersecting && this._isLoadingNewer) {
|
||||
console.log('[ScrollManagerV2] ⏸️ Already loading newer, skipping');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Триггер загрузки истории
|
||||
* @private
|
||||
@ -532,6 +692,15 @@ export class ScrollManagerV2 {
|
||||
this.loadMoreHistory();
|
||||
}
|
||||
|
||||
/**
|
||||
* Триггер загрузки новых сообщений
|
||||
* @private
|
||||
*/
|
||||
_triggerLoadNewer() {
|
||||
console.log('[ScrollManagerV2] 🎬 _triggerLoadNewer called (from debounced)');
|
||||
this.loadMoreNewer();
|
||||
}
|
||||
|
||||
/**
|
||||
* Обработчик скролла
|
||||
* @private
|
||||
@ -563,7 +732,7 @@ export class ScrollManagerV2 {
|
||||
// ==================== Приватные методы - Observer ====================
|
||||
|
||||
/**
|
||||
* Обновляет target IntersectionObserver
|
||||
* Обновляет target IntersectionObserver для истории
|
||||
* @private
|
||||
*/
|
||||
_updateObserverTarget() {
|
||||
@ -577,6 +746,44 @@ export class ScrollManagerV2 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Обновляет target IntersectionObserver для новых сообщений
|
||||
* @private
|
||||
*/
|
||||
_updateNewerObserverTarget() {
|
||||
// Отключаем старый observer
|
||||
if (this._newerObserver) {
|
||||
this._newerObserver.disconnect();
|
||||
}
|
||||
|
||||
// Проверяем есть ли ещё новые сообщения
|
||||
if (!this.loader.hasMoreAfter(this.chatId)) {
|
||||
console.log('[ScrollManagerV2] No more newer messages, removing observer');
|
||||
this._newerObserver = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Находим последнее сообщение
|
||||
const messages = this.scrollEl.querySelectorAll('.msg[data-message-id]');
|
||||
const lastMessage = messages[messages.length - 1];
|
||||
|
||||
if (lastMessage) {
|
||||
if (!this._newerObserver) {
|
||||
// Создаем новый observer если нужно
|
||||
this._newerObserver = new IntersectionObserver(
|
||||
(entries) => this._handleNewerIntersection(entries),
|
||||
{
|
||||
root: this.scrollEl,
|
||||
threshold: this.config.OBSERVER_THRESHOLD,
|
||||
rootMargin: '0px 0px 200px 0px'
|
||||
}
|
||||
);
|
||||
}
|
||||
this._newerObserver.observe(lastMessage);
|
||||
console.log('[ScrollManagerV2] ✅ Newer observer updated to last message:', lastMessage.dataset.messageId);
|
||||
}
|
||||
}
|
||||
|
||||
// ==================== Приватные методы - Scroll Position ====================
|
||||
|
||||
/**
|
||||
|
||||
@ -9,6 +9,9 @@
|
||||
* - Нет дублирования логики
|
||||
*/
|
||||
|
||||
import { DateDividerManager } from '../utils/dateDividers.js';
|
||||
import { DateGroupManager } from '../managers/dateGroupManager.js';
|
||||
|
||||
/**
|
||||
* Экранирует HTML для безопасного вывода
|
||||
* @param {string} text - Текст для экранирования
|
||||
@ -35,6 +38,7 @@ export class MessageRendererV2 {
|
||||
* @param {MessageStore} options.store - Экземпляр MessageStore
|
||||
* @param {string} options.containerId - ID контейнера для сообщений
|
||||
* @param {number} options.currentUserId - ID текущего пользователя
|
||||
* @param {DateGroupManager} [options.dateGroupManager] - Менеджер date groups
|
||||
* @param {string} [options.profileUrl] - URL шаблон профиля
|
||||
* @param {string} [options.detailUrlTemplate] - URL шаблон детальной информации
|
||||
*/
|
||||
@ -46,13 +50,16 @@ export class MessageRendererV2 {
|
||||
this.store = options.store;
|
||||
this.containerId = options.containerId || 'chatScroll';
|
||||
this.currentUserId = options.currentUserId;
|
||||
this.dateGroupManager = options.dateGroupManager || null;
|
||||
this.profileUrl = options.profileUrl || '/employees/profile/';
|
||||
this.detailUrlTemplate = options.detailUrlTemplate || '/employees/detail/0/';
|
||||
|
||||
/** @type {Set<number|string>} ID сообщений которые уже в DOM */
|
||||
this.renderedMessages = new Set();
|
||||
|
||||
console.log('[MessageRendererV2] Initialized');
|
||||
console.log('[MessageRendererV2] Initialized', {
|
||||
hasDateGroupManager: !!this.dateGroupManager
|
||||
});
|
||||
}
|
||||
|
||||
// ==================== Основные методы рендеринга ====================
|
||||
@ -73,10 +80,10 @@ export class MessageRendererV2 {
|
||||
|
||||
const { clear = true } = options;
|
||||
|
||||
// Получаем сообщения с dividers из Store
|
||||
const items = this.store.getMessagesWithDividers(chatId);
|
||||
// Получаем сообщения из Store (без dividers, groupAndRender сам добавит)
|
||||
const messages = this.store.getMessagesForChat(chatId);
|
||||
|
||||
if (items.length === 0) {
|
||||
if (messages.length === 0) {
|
||||
if (clear) {
|
||||
container.innerHTML = '';
|
||||
this.renderedMessages.clear();
|
||||
@ -84,18 +91,46 @@ export class MessageRendererV2 {
|
||||
return;
|
||||
}
|
||||
|
||||
// Очищаем ДО создания fragment
|
||||
// Очищаем ДО рендеринга
|
||||
if (clear) {
|
||||
container.innerHTML = '';
|
||||
this.renderedMessages.clear();
|
||||
// Очищаем DateGroupManager если есть
|
||||
if (this.dateGroupManager) {
|
||||
this.dateGroupManager.clear();
|
||||
}
|
||||
}
|
||||
|
||||
// Если есть DateGroupManager - используем группировку
|
||||
if (this.dateGroupManager) {
|
||||
console.log('[MessageRendererV2] Using DateGroupManager for full render');
|
||||
|
||||
// Используем groupAndRender для создания date groups
|
||||
const groups = this.dateGroupManager.groupAndRender(
|
||||
messages,
|
||||
(msg) => {
|
||||
this.renderedMessages.add(msg.id);
|
||||
return this._createMessageElement(msg);
|
||||
},
|
||||
'append'
|
||||
);
|
||||
|
||||
console.log('[MessageRendererV2] Rendered', messages.length, 'messages in', groups.length, 'date groups');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: старая логика без DateGroupManager
|
||||
console.log('[MessageRendererV2] Using legacy rendering (no DateGroupManager)');
|
||||
|
||||
// Получаем сообщения с dividers из Store
|
||||
const items = this.store.getMessagesWithDividers(chatId);
|
||||
|
||||
// Используем DocumentFragment для батчинга
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
items.forEach(item => {
|
||||
if (item.type === 'day-divider') {
|
||||
fragment.appendChild(this._createDayDivider(item.text));
|
||||
if (item.type === 'divider') {
|
||||
fragment.appendChild(DateDividerManager.createDivider(item.text));
|
||||
} else if (item.type === 'message') {
|
||||
if (!this.renderedMessages.has(item.message.id)) {
|
||||
const messageEl = this._createMessageElement(item.message);
|
||||
@ -108,7 +143,7 @@ export class MessageRendererV2 {
|
||||
// ОДНА вставка в DOM
|
||||
container.appendChild(fragment);
|
||||
|
||||
console.log('[MessageRendererV2] Rendered', items.length, 'items');
|
||||
console.log('[MessageRendererV2] Rendered', items.length, 'items (legacy mode)');
|
||||
}
|
||||
|
||||
/**
|
||||
@ -134,13 +169,41 @@ export class MessageRendererV2 {
|
||||
console.log('[MessageRendererV2] Prepending', messages.length, 'messages');
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Если есть DateGroupManager - используем группировку
|
||||
if (this.dateGroupManager) {
|
||||
console.log('[MessageRendererV2] Using DateGroupManager');
|
||||
|
||||
// Создаем группы с помощью DateGroupManager
|
||||
const groups = this.dateGroupManager.groupAndRender(
|
||||
messages,
|
||||
(msg) => {
|
||||
if (!this.renderedMessages.has(msg.id)) {
|
||||
this.renderedMessages.add(msg.id);
|
||||
return this._createMessageElement(msg);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
'prepend' // Важно: указываем что это prepend
|
||||
);
|
||||
|
||||
// Добавляем все группы в fragment
|
||||
groups.forEach(group => fragment.appendChild(group));
|
||||
|
||||
console.log('[MessageRendererV2] Created', groups.length, 'date groups');
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// Fallback: старая логика без групп
|
||||
console.log('[MessageRendererV2] Using legacy dividers (no DateGroupManager)');
|
||||
const items = [];
|
||||
|
||||
// Группируем сообщения по дням (от старых к новым)
|
||||
let lastDay = null;
|
||||
messages.forEach(msg => {
|
||||
const msgDate = new Date(msg.created_ts);
|
||||
const msgDay = this._formatDay(msgDate);
|
||||
const msgDay = DateDividerManager.formatDay(msgDate);
|
||||
|
||||
// Добавляем divider если день изменился
|
||||
if (msgDay !== lastDay) {
|
||||
@ -162,7 +225,7 @@ export class MessageRendererV2 {
|
||||
let added = 0;
|
||||
items.forEach(item => {
|
||||
if (item.type === 'day-divider') {
|
||||
fragment.appendChild(this._createDayDivider(item.text));
|
||||
fragment.appendChild(DateDividerManager.createDivider(item.text));
|
||||
added++;
|
||||
} else if (item.type === 'message') {
|
||||
if (!this.renderedMessages.has(item.message.id)) {
|
||||
@ -187,6 +250,115 @@ export class MessageRendererV2 {
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Добавляет новые сообщения в КОНЕЦ (для загрузки при прокрутке вниз после перехода по дате)
|
||||
* Как в Telegram: при прокрутке вниз после перехода к старой дате
|
||||
* @param {Array<Object>} messages - Массив сообщений (от старых к новым)
|
||||
* @param {number} chatId - ID чата
|
||||
* @returns {DocumentFragment} Fragment с элементами для append
|
||||
*/
|
||||
appendMessages(messages, chatId) {
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
console.log('[MessageRendererV2] appendMessages called:', {
|
||||
messagesCount: messages?.length || 0,
|
||||
chatId,
|
||||
messageIds: messages?.map(m => m.id) || []
|
||||
});
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log('[MessageRendererV2] ❌ No messages to append');
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
|
||||
console.log('[MessageRendererV2] Appending', messages.length, 'messages');
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
|
||||
// Если есть DateGroupManager - используем группировку
|
||||
if (this.dateGroupManager) {
|
||||
console.log('[MessageRendererV2] Using DateGroupManager for append');
|
||||
|
||||
// Создаем группы с помощью DateGroupManager
|
||||
const groups = this.dateGroupManager.groupAndRender(
|
||||
messages,
|
||||
(msg) => {
|
||||
if (!this.renderedMessages.has(msg.id)) {
|
||||
this.renderedMessages.add(msg.id);
|
||||
return this._createMessageElement(msg);
|
||||
}
|
||||
return null;
|
||||
},
|
||||
'append' // Указываем что это append
|
||||
);
|
||||
|
||||
// Добавляем все группы в fragment
|
||||
groups.forEach(group => fragment.appendChild(group));
|
||||
|
||||
console.log('[MessageRendererV2] Created', groups.length, 'date groups for append');
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
return fragment;
|
||||
}
|
||||
|
||||
// Fallback: старая логика без групп
|
||||
console.log('[MessageRendererV2] Using legacy dividers for append (no DateGroupManager)');
|
||||
const items = [];
|
||||
|
||||
// Получаем последний день из контейнера для корректных divider'ов
|
||||
const container = document.getElementById(this.containerId);
|
||||
const lastDivider = container?.querySelector('.day-divider:last-of-type');
|
||||
let lastDay = lastDivider ? lastDivider.textContent.trim() : null;
|
||||
|
||||
// Группируем сообщения по дням (от старых к новым)
|
||||
messages.forEach(msg => {
|
||||
const msgDate = new Date(msg.created_ts);
|
||||
const msgDay = DateDividerManager.formatDay(msgDate);
|
||||
|
||||
// Добавляем divider если день изменился
|
||||
if (msgDay !== lastDay) {
|
||||
items.push({ type: 'day-divider', text: msgDay });
|
||||
lastDay = msgDay;
|
||||
}
|
||||
|
||||
items.push({ type: 'message', message: msg });
|
||||
});
|
||||
|
||||
console.log('[MessageRendererV2] Items to append:', {
|
||||
totalItems: items.length,
|
||||
dividers: items.filter(i => i.type === 'day-divider').length,
|
||||
messages: items.filter(i => i.type === 'message').length
|
||||
});
|
||||
|
||||
// Создаем элементы
|
||||
let skipped = 0;
|
||||
let added = 0;
|
||||
items.forEach(item => {
|
||||
if (item.type === 'day-divider') {
|
||||
fragment.appendChild(DateDividerManager.createDivider(item.text));
|
||||
added++;
|
||||
} else if (item.type === 'message') {
|
||||
if (!this.renderedMessages.has(item.message.id)) {
|
||||
const messageEl = this._createMessageElement(item.message);
|
||||
fragment.appendChild(messageEl);
|
||||
this.renderedMessages.add(item.message.id);
|
||||
added++;
|
||||
} else {
|
||||
skipped++;
|
||||
console.log('[MessageRendererV2] ⚠️ Skipped duplicate in append:', item.message.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MessageRendererV2] Append fragment created:', {
|
||||
added,
|
||||
skipped,
|
||||
fragmentChildren: fragment.childElementCount
|
||||
});
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Удаляет дубликаты day-divider после prepend
|
||||
* @param {HTMLElement} container - Контейнер с сообщениями
|
||||
@ -232,6 +404,28 @@ export class MessageRendererV2 {
|
||||
|
||||
console.log('[MessageRendererV2] Appending message:', message.id);
|
||||
|
||||
// Если есть DateGroupManager - используем группировку
|
||||
if (this.dateGroupManager) {
|
||||
const messageDate = DateDividerManager.getMessageDate(message);
|
||||
const dateText = DateDividerManager.formatDay(messageDate);
|
||||
|
||||
// Создаем элемент сообщения
|
||||
const messageEl = this._createMessageElement(message);
|
||||
|
||||
// Добавляем в соответствующую группу
|
||||
this.dateGroupManager.addMessageToGroup(
|
||||
messageEl,
|
||||
dateText,
|
||||
messageDate.getTime(),
|
||||
'append'
|
||||
);
|
||||
|
||||
this.renderedMessages.add(message.id);
|
||||
console.log('[MessageRendererV2] Message added to date group:', dateText);
|
||||
return;
|
||||
}
|
||||
|
||||
// Fallback: старая логика без групп
|
||||
// Проверяем нужен ли day-divider
|
||||
// Сравниваем с последним сообщением, а не с последним divider
|
||||
const lastMessage = container.querySelector('.msg:last-of-type');
|
||||
@ -240,20 +434,20 @@ export class MessageRendererV2 {
|
||||
const lastMsgData = this.store.getMessage(lastMsgId);
|
||||
if (lastMsgData) {
|
||||
const lastMsgDate = new Date(lastMsgData.created_ts);
|
||||
const lastMsgDay = this._formatDay(lastMsgDate);
|
||||
const lastMsgDay = DateDividerManager.formatDay(lastMsgDate);
|
||||
const msgDate = new Date(message.created_ts);
|
||||
const msgDay = this._formatDay(msgDate);
|
||||
const msgDay = DateDividerManager.formatDay(msgDate);
|
||||
|
||||
// Добавляем divider только если день изменился
|
||||
if (msgDay !== lastMsgDay) {
|
||||
container.appendChild(this._createDayDivider(msgDay));
|
||||
container.appendChild(DateDividerManager.createDivider(msgDay));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Первое сообщение - добавляем divider
|
||||
const msgDate = new Date(message.created_ts);
|
||||
const msgDay = this._formatDay(msgDate);
|
||||
container.appendChild(this._createDayDivider(msgDay));
|
||||
const msgDay = DateDividerManager.formatDay(msgDate);
|
||||
container.appendChild(DateDividerManager.createDivider(msgDay));
|
||||
}
|
||||
|
||||
// Создаем и добавляем сообщение
|
||||
@ -328,17 +522,6 @@ export class MessageRendererV2 {
|
||||
|
||||
// ==================== Создание элементов ====================
|
||||
|
||||
/**
|
||||
* Создает элемент day-divider
|
||||
* @private
|
||||
*/
|
||||
_createDayDivider(text) {
|
||||
const dividerEl = document.createElement('div');
|
||||
dividerEl.className = 'day-divider text-center small text-muted my-3';
|
||||
dividerEl.innerHTML = `<span class="px-3 py-1 rounded-pill bg-light">${escapeHtml(text)}</span>`;
|
||||
return dividerEl;
|
||||
}
|
||||
|
||||
/**
|
||||
* Создает элемент сообщения
|
||||
* @private
|
||||
@ -556,25 +739,6 @@ export class MessageRendererV2 {
|
||||
|
||||
// ==================== Утилиты ====================
|
||||
|
||||
/**
|
||||
* Форматирует дату в текст дня
|
||||
* @private
|
||||
*/
|
||||
_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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует время
|
||||
* @private
|
||||
@ -594,6 +758,12 @@ export class MessageRendererV2 {
|
||||
if (container) {
|
||||
container.innerHTML = '';
|
||||
this.renderedMessages.clear();
|
||||
|
||||
// Очищаем DateGroupManager если есть
|
||||
if (this.dateGroupManager) {
|
||||
this.dateGroupManager.clear();
|
||||
}
|
||||
|
||||
console.log('[MessageRendererV2] Container cleared');
|
||||
}
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@
|
||||
*/
|
||||
|
||||
import { STORE_EVENTS } from '../config/chatConfig.js';
|
||||
import { DateDividerManager } from '../utils/dateDividers.js';
|
||||
|
||||
/**
|
||||
* @typedef {Object} Message
|
||||
@ -324,7 +325,7 @@ export class MessageStoreV2 {
|
||||
let lastDay = null;
|
||||
|
||||
for (const message of messages) {
|
||||
const day = this._formatDay(new Date(message.created_ts));
|
||||
const day = DateDividerManager.formatDay(new Date(message.created_ts));
|
||||
|
||||
if (day !== lastDay) {
|
||||
result.push({ type: 'divider', text: day });
|
||||
@ -494,25 +495,6 @@ export class MessageStoreV2 {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Форматирует дату в текст дня
|
||||
* @private
|
||||
*/
|
||||
_formatDay(date) {
|
||||
const today = new Date();
|
||||
const yesterday = new Date(today);
|
||||
yesterday.setDate(yesterday.getDate() - 1);
|
||||
|
||||
if (date.toDateString() === today.toDateString()) return 'Сегодня';
|
||||
if (date.toDateString() === yesterday.toDateString()) return 'Вчера';
|
||||
|
||||
return date.toLocaleDateString('ru-RU', {
|
||||
day: 'numeric',
|
||||
month: 'long',
|
||||
year: 'numeric'
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Уведомляет подписчиков
|
||||
* @private
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user