fix(chat-v2): исправлена загрузка истории - silent mode + prepend
- ScrollManagerV2: использует prepend + height difference вместо полного ре-рендера - MessageLoaderV2: фильтрует новые сообщения, возвращает только их - MessageStoreV2: silent mode для загрузки истории (без событий) - MessageRendererV2: детальное логирование для диагностики Проблема: Store emit'ил события → appendMessage добавлял в конец → prependMessages пропускал дубликаты Решение: silent mode отключает события → отрисовка только через prepend → сообщения в начало
This commit is contained in:
parent
0375337543
commit
fcee247256
@ -184,7 +184,21 @@ export class MessageLoaderV2 {
|
||||
const messages = this._extractMessages(result);
|
||||
|
||||
if (messages.length > 0) {
|
||||
this._processMessages(chatId, messages);
|
||||
// Определяем какие сообщения НОВЫЕ (не были в Store)
|
||||
const existingIds = new Set(
|
||||
this.store.getMessagesForChat(chatId).map(m => m.id)
|
||||
);
|
||||
const newMessages = messages.filter(m => !existingIds.has(m.id));
|
||||
|
||||
console.log('[MessageLoaderV2] Filtering messages:', {
|
||||
total: messages.length,
|
||||
existing: messages.length - newMessages.length,
|
||||
new: newMessages.length
|
||||
});
|
||||
|
||||
// Добавляем в Store БЕЗ событий (silent mode)
|
||||
// Отрисовка будет через prependMessages
|
||||
this._processMessages(chatId, messages, { silent: true });
|
||||
|
||||
// Обновляем границы
|
||||
const oldestMessage = this.store.getOldestMessage(chatId);
|
||||
@ -192,12 +206,15 @@ export class MessageLoaderV2 {
|
||||
hasMoreBefore: result.has_more !== false,
|
||||
oldestId: oldestMessage?.id || null
|
||||
});
|
||||
|
||||
// Возвращаем только НОВЫЕ сообщения
|
||||
return newMessages;
|
||||
} else {
|
||||
this._updateBoundaries(chatId, { hasMoreBefore: false });
|
||||
}
|
||||
|
||||
console.log('[MessageLoaderV2] History loaded:', messages.length, 'messages');
|
||||
return messages;
|
||||
return [];
|
||||
|
||||
} catch (error) {
|
||||
if (error.name === 'AbortError') {
|
||||
@ -447,12 +464,22 @@ export class MessageLoaderV2 {
|
||||
requestKey
|
||||
);
|
||||
|
||||
const messages = this._extractMessages(result);
|
||||
|
||||
// ВАЖНО: Сначала добавляем в Store, затем берем границы из Store
|
||||
this._processMessages(chatId, messages);
|
||||
|
||||
const oldestMessage = this.store.getOldestMessage(chatId);
|
||||
const newestMessage = this.store.getNewestMessage(chatId);
|
||||
|
||||
return {
|
||||
messages: this._extractMessages(result),
|
||||
messages,
|
||||
anchorId: result.anchor_id || aroundId,
|
||||
anchorIndex: result.anchor_index || null,
|
||||
hasMoreBefore: result.has_more_before !== false,
|
||||
hasMoreAfter: result.has_more_after !== false
|
||||
hasMoreAfter: result.has_more_after !== false,
|
||||
oldestId: oldestMessage?.id || null,
|
||||
newestId: newestMessage?.id || null
|
||||
};
|
||||
}
|
||||
|
||||
@ -468,11 +495,22 @@ export class MessageLoaderV2 {
|
||||
requestKey
|
||||
);
|
||||
|
||||
const messages = this._extractMessages(result);
|
||||
|
||||
// ВАЖНО: Сначала добавляем в Store, затем берем границы из Store
|
||||
// Store гарантирует правильную сортировку по timestamp
|
||||
this._processMessages(chatId, messages);
|
||||
|
||||
const oldestMessage = this.store.getOldestMessage(chatId);
|
||||
const newestMessage = this.store.getNewestMessage(chatId);
|
||||
|
||||
return {
|
||||
messages: this._extractMessages(result),
|
||||
messages,
|
||||
anchorId: null,
|
||||
hasMoreBefore: result.has_more !== false,
|
||||
hasMoreAfter: false
|
||||
hasMoreAfter: false,
|
||||
oldestId: oldestMessage?.id || null,
|
||||
newestId: newestMessage?.id || null
|
||||
};
|
||||
}
|
||||
|
||||
@ -575,8 +613,12 @@ export class MessageLoaderV2 {
|
||||
/**
|
||||
* Обрабатывает и сохраняет сообщения в Store
|
||||
* @private
|
||||
* @param {number} chatId
|
||||
* @param {Array} messages
|
||||
* @param {Object} [options]
|
||||
* @param {boolean} [options.silent=false] - Не emit'ить события
|
||||
*/
|
||||
_processMessages(chatId, messages) {
|
||||
_processMessages(chatId, messages, options = {}) {
|
||||
// Добавляем chat_id если отсутствует
|
||||
messages.forEach(msg => {
|
||||
if (!msg.chat_id) {
|
||||
@ -584,7 +626,7 @@ export class MessageLoaderV2 {
|
||||
}
|
||||
});
|
||||
|
||||
this.store.addMessages(messages);
|
||||
this.store.addMessages(messages, options);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -135,38 +135,147 @@ export class ScrollManagerV2 {
|
||||
* @returns {Promise<Array>}
|
||||
*/
|
||||
async loadMoreHistory() {
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
console.log('[ScrollManagerV2] loadMoreHistory called');
|
||||
|
||||
if (this._isLoadingHistory || this._destroyed) {
|
||||
console.log('[ScrollManagerV2] ❌ Skipped: already loading or destroyed');
|
||||
return [];
|
||||
}
|
||||
|
||||
if (!this.loader.hasMoreHistory(this.chatId)) {
|
||||
console.log('[ScrollManagerV2] ❌ No more history available');
|
||||
return [];
|
||||
}
|
||||
|
||||
this._isLoadingHistory = true;
|
||||
|
||||
try {
|
||||
// Сохраняем позицию
|
||||
const savedPosition = this._saveScrollPosition();
|
||||
// Отключаем smooth scroll для мгновенных изменений
|
||||
const originalScrollBehavior = this.scrollEl.style.scrollBehavior;
|
||||
this.scrollEl.style.scrollBehavior = 'auto';
|
||||
|
||||
// Сохраняем высоты ДО загрузки
|
||||
const oldScrollHeight = this.scrollEl.scrollHeight;
|
||||
const oldScrollTop = this.scrollEl.scrollTop;
|
||||
const messagesBefore = this.scrollEl.querySelectorAll('.msg[data-message-id]').length;
|
||||
|
||||
console.log('[ScrollManagerV2] BEFORE load:', {
|
||||
oldScrollHeight,
|
||||
oldScrollTop,
|
||||
messagesBefore
|
||||
});
|
||||
|
||||
// Загружаем историю
|
||||
const messages = await this.loader.loadHistory(this.chatId);
|
||||
|
||||
if (messages.length > 0) {
|
||||
// Ре-рендерим
|
||||
await this.renderer.render(this.chatId);
|
||||
|
||||
// Восстанавливаем позицию
|
||||
this._restoreScrollPosition(savedPosition);
|
||||
|
||||
// Обновляем observer
|
||||
this._updateObserverTarget();
|
||||
console.log('[ScrollManagerV2] AFTER loader.loadHistory:', {
|
||||
messagesLoaded: messages.length,
|
||||
messageIds: messages.map(m => m.id)
|
||||
});
|
||||
|
||||
// Проверяем есть ли еще история ПОСЛЕ загрузки
|
||||
const stillHasMore = this.loader.hasMoreHistory(this.chatId);
|
||||
|
||||
if (messages.length === 0 || !stillHasMore) {
|
||||
console.log('[ScrollManagerV2] ⚠️ Stopping: no messages or no more history', {
|
||||
messagesLength: messages.length,
|
||||
stillHasMore
|
||||
});
|
||||
this.scrollEl.style.scrollBehavior = originalScrollBehavior;
|
||||
return [];
|
||||
}
|
||||
|
||||
console.log('[ScrollManagerV2] ✅ History loaded:', messages.length, 'messages');
|
||||
|
||||
// КЛЮЧЕВОЙ МОМЕНТ: НЕ делаем полный render!
|
||||
// Создаем fragment с новыми сообщениями
|
||||
const fragment = this.renderer.prependMessages(messages, this.chatId);
|
||||
|
||||
console.log('[ScrollManagerV2] Fragment created:', {
|
||||
hasChildNodes: fragment.hasChildNodes(),
|
||||
childCount: fragment.childElementCount
|
||||
});
|
||||
|
||||
// Если fragment пустой (все сообщения уже отрендерены), выходим
|
||||
if (!fragment.hasChildNodes()) {
|
||||
console.log('[ScrollManagerV2] ⚠️ All messages already rendered');
|
||||
this.scrollEl.style.scrollBehavior = originalScrollBehavior;
|
||||
return messages;
|
||||
}
|
||||
|
||||
// ВАЖНО: Проверяем и удаляем потенциальные дубликаты divider
|
||||
// ПЕРЕД prepend, чтобы избежать визуальных скачков
|
||||
const firstFragmentChild = fragment.firstElementChild;
|
||||
if (firstFragmentChild && firstFragmentChild.classList.contains('day-divider')) {
|
||||
const firstContainerChild = this.scrollEl.firstElementChild;
|
||||
if (firstContainerChild && firstContainerChild.classList.contains('day-divider')) {
|
||||
const fragmentDividerText = firstFragmentChild.textContent.trim();
|
||||
const containerDividerText = firstContainerChild.textContent.trim();
|
||||
|
||||
// Если dividers одинаковые, удаляем из fragment
|
||||
if (fragmentDividerText === containerDividerText) {
|
||||
fragment.removeChild(firstFragmentChild);
|
||||
console.log('[ScrollManagerV2] Removed duplicate divider:', fragmentDividerText);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend fragment в НАЧАЛО контейнера (как в Telegram)
|
||||
this.scrollEl.prepend(fragment);
|
||||
|
||||
const messagesAfterPrepend = this.scrollEl.querySelectorAll('.msg[data-message-id]').length;
|
||||
console.log('[ScrollManagerV2] AFTER prepend:', {
|
||||
messagesAfterPrepend,
|
||||
added: messagesAfterPrepend - messagesBefore
|
||||
});
|
||||
|
||||
// КЛЮЧЕВОЕ ОТЛИЧИЕ Telegram: ДВОЙНОЙ RAF для стабильности!
|
||||
// Первый RAF - браузер обновляет layout
|
||||
// Второй RAF - браузер гарантированно применил все изменения
|
||||
await new Promise(resolve => {
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(() => {
|
||||
// КЛЮЧЕВОЙ МОМЕНТ: вычисляем разницу в высоте
|
||||
const newScrollHeight = this.scrollEl.scrollHeight;
|
||||
const heightDifference = newScrollHeight - oldScrollHeight;
|
||||
|
||||
// Корректируем scrollTop на разницу высоты
|
||||
// Это сохраняет визуальную позицию пользователя ТОЧНО там же
|
||||
// БЕЗ КАКИХ-ЛИБО ПРЫЖКОВ!
|
||||
this.scrollEl.scrollTop = oldScrollTop + heightDifference;
|
||||
|
||||
console.log('[ScrollManagerV2] Scroll restored using height difference:', {
|
||||
oldScrollHeight,
|
||||
newScrollHeight,
|
||||
heightDifference,
|
||||
oldScrollTop,
|
||||
newScrollTop: this.scrollEl.scrollTop
|
||||
});
|
||||
|
||||
// Обновляем observer на новое первое сообщение
|
||||
this._updateObserverTarget();
|
||||
|
||||
// Восстанавливаем scroll-behavior
|
||||
this.scrollEl.style.scrollBehavior = originalScrollBehavior;
|
||||
|
||||
const finalMessages = this.scrollEl.querySelectorAll('.msg[data-message-id]').length;
|
||||
console.log('[ScrollManagerV2] ✅ FINAL STATE:', {
|
||||
finalMessages,
|
||||
totalAdded: finalMessages - messagesBefore
|
||||
});
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
return messages;
|
||||
|
||||
} catch (error) {
|
||||
console.error('[ScrollManagerV2] History load failed:', error);
|
||||
console.error('[ScrollManagerV2] ❌ History load failed:', error);
|
||||
console.log('[ScrollManagerV2] ======================================');
|
||||
return [];
|
||||
|
||||
} finally {
|
||||
|
||||
@ -118,8 +118,16 @@ export class MessageRendererV2 {
|
||||
* @returns {DocumentFragment} Fragment с элементами для prepend
|
||||
*/
|
||||
prependMessages(messages, chatId) {
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
console.log('[MessageRendererV2] prependMessages called:', {
|
||||
messagesCount: messages?.length || 0,
|
||||
chatId,
|
||||
messageIds: messages?.map(m => m.id) || []
|
||||
});
|
||||
|
||||
if (!messages || messages.length === 0) {
|
||||
console.log('[MessageRendererV2] No messages to prepend');
|
||||
console.log('[MessageRendererV2] ❌ No messages to prepend');
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
return document.createDocumentFragment();
|
||||
}
|
||||
|
||||
@ -143,19 +151,39 @@ export class MessageRendererV2 {
|
||||
items.push({ type: 'message', message: msg });
|
||||
});
|
||||
|
||||
console.log('[MessageRendererV2] Items to render:', {
|
||||
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(this._createDayDivider(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:', item.message.id);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
console.log('[MessageRendererV2] Fragment created:', {
|
||||
added,
|
||||
skipped,
|
||||
fragmentChildren: fragment.childElementCount
|
||||
});
|
||||
console.log('[MessageRendererV2] ======================================');
|
||||
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
@ -65,6 +65,7 @@ export class MessageStoreV2 {
|
||||
|
||||
// Batch mode
|
||||
this._batchMode = false;
|
||||
this._silentMode = false;
|
||||
this._pendingNotifications = [];
|
||||
|
||||
console.log('[MessageStoreV2] Initialized');
|
||||
@ -111,15 +112,21 @@ export class MessageStoreV2 {
|
||||
/**
|
||||
* Добавляет множество сообщений (batch)
|
||||
* @param {Array<Message>} messages - Сообщения
|
||||
* @param {Object} [options] - Опции
|
||||
* @param {boolean} [options.silent=false] - Не emit'ить события
|
||||
* @returns {number} Количество добавленных
|
||||
*/
|
||||
addMessages(messages) {
|
||||
addMessages(messages, options = {}) {
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const { silent = false } = options;
|
||||
|
||||
// Включаем batch mode
|
||||
this._batchMode = true;
|
||||
const oldSilent = this._silentMode;
|
||||
this._silentMode = silent;
|
||||
let addedCount = 0;
|
||||
|
||||
try {
|
||||
@ -131,11 +138,17 @@ export class MessageStoreV2 {
|
||||
} finally {
|
||||
// Выключаем batch mode и отправляем все уведомления
|
||||
this._batchMode = false;
|
||||
this._flushNotifications();
|
||||
this._silentMode = oldSilent;
|
||||
|
||||
if (!silent) {
|
||||
this._flushNotifications();
|
||||
}
|
||||
}
|
||||
|
||||
// Отправляем групповое уведомление
|
||||
this._notify(STORE_EVENTS.MESSAGES_LOADED, { count: addedCount });
|
||||
// Отправляем групповое уведомление только если не silent
|
||||
if (!silent) {
|
||||
this._notify(STORE_EVENTS.MESSAGES_LOADED, { count: addedCount });
|
||||
}
|
||||
|
||||
return addedCount;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user