Added: - Autoscroll audit and reports - Date divider analysis and Telegram comparison - Bidirectional loading tests and results - Date group manager and utilities - Test scripts and HTML test pages - CSS for chat date groups - IntersectionObserver utilities
13 KiB
Аудит автоскролла в проекте EUSRR
📊 Сводная информация
Дата аудита: 13 января 2026
Цель: Найти все места автоскролла и оценить их влияние на UX
🔍 Найденные места автоскролла
1. ⚠️ userWebSocket.js (КРИТИЧНО)
Файл: backend/static/js/components/userWebSocket.js
Место 1: Initial Messages (строка 273-279)
if (options.messageRenderer && messages?.length) {
options.messageRenderer.renderMessages(messages);
if (!window.chatControllerV2) { // Есть проверка на V2
requestAnimationFrame(() => {
state.scrollEl.scrollTop = state.scrollEl.scrollHeight;
});
}
}
Статус: ✅ Исправлено (есть проверка !window.chatControllerV2)
Проблема: Срабатывает при WebSocket событии initial_messages
Место 2: New Message (строка 316)
if (isOwnMessage || isAtBottom) {
requestAnimationFrame(() => scrollToBottom());
}
Статус: ❌ АКТИВЕН! Дублирует логику ChatControllerV2
Проблема: Автоскролл при получении нового сообщения через WebSocket
Решение: Добавить проверку if (!window.chatControllerV2) перед автоскроллом
Место 3: scrollToBottom функция (строка 536-553)
function scrollToBottom(instant = false) {
if (!state.scrollEl) return;
if (instant) {
const prev = state.scrollEl.style.scrollBehavior;
state.scrollEl.style.scrollBehavior = 'auto';
state.scrollEl.scrollTop = state.scrollEl.scrollHeight;
if (prev) {
state.scrollEl.style.scrollBehavior = prev;
} else {
state.scrollEl.style.removeProperty('scroll-behavior');
}
} else {
state.scrollEl.scrollTop = state.scrollEl.scrollHeight;
}
}
Статус: ⚠️ Backward compatibility
Используется: Только для старой архитектуры (не V2)
2. 🎮 chatControllerV2.js
Файл: backend/static/js/controllers/chatControllerV2.js
Место 1: Начальная загрузка (строки 161-180)
// Нет anchor - скроллим в самый низ (как в Telegram)
await new Promise(resolve => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
requestAnimationFrame(() => {
// Тройной RAF для надежности
this.scrollElement.scrollTop = this.scrollElement.scrollHeight;
// ...
Статус: ✅ Правильно - только при первой загрузке БЕЗ anchor
Когда срабатывает: Открытие чата без последнего прочитанного сообщения
Место 2: Новое сообщение (строки 424-427)
if (shouldScroll) {
this.scrollManager.scrollToBottom({
instant: isMyMessage && !AUTOSCROLL_CONFIG.SMOOTH_SCROLL_FOR_OWN,
force: isMyMessage
});
Статус: ✅ Правильно - с проверкой isAtBottom для чужих сообщений
Логика:
- Свое сообщение → всегда скроллим
- Чужое сообщение + внизу → скроллим
- Чужое сообщение + читаем историю → НЕ скроллим (показываем индикатор)
Место 3: Кнопка "новые сообщения" (строки 671-675)
btn.addEventListener('click', () => {
this.scrollManager.scrollToBottom({
instant: !AUTOSCROLL_CONFIG.SMOOTH_SCROLL_ON_CLICK,
force: true
});
Статус: ✅ Правильно - только по клику пользователя
3. 📜 scrollManagerV2.js
Файл: backend/static/js/managers/scrollManagerV2.js
Метод scrollToBottom (строки 365-383)
scrollToBottom(options = {}) {
const { instant = false, force = false } = options;
if (!force && !this.isNearBottom()) {
return; // НЕ скроллим если пользователь читает историю
}
if (instant) {
this.scrollEl.scrollTop = this.scrollEl.scrollHeight;
} else {
requestAnimationFrame(() => {
this.scrollEl.scrollTop = this.scrollEl.scrollHeight;
});
}
}
Статус: ✅ Правильно - имеет проверку isNearBottom() и флаг force
4. 💬 messageRenderer.js
Файл: backend/static/js/components/messageRenderer.js
Inline onclick в reply-reference (строка 260)
onclick="document.querySelector('[data-message-id=\\\"${msg.reply_to.id}\\\"]')?.scrollIntoView({behavior:'smooth',block:'center'})"
Статус: ✅ Правильно - скролл к цитируемому сообщению по клику
Проблема: Нет - это ожидаемое поведение
5. 📝 postDetailModal.js
Файл: backend/static/js/components/postDetailModal.js
scrollToBottom в модальном окне (строка 366)
function scrollToBottom() {
window.scrollTo({ top: 0, behavior: 'smooth' });
}
Статус: ✅ Правильно - скролл комментариев в модальном окне
Область: Только для модального окна постов (не чат)
🐛 Обнаруженные проблемы
❌ Проблема 1: Дублирование автоскролла
Место: userWebSocket.js:316
Описание: При получении нового сообщения через WebSocket происходит автоскролл В userWebSocket И в chatControllerV2
Последствия:
- Двойной скролл при получении сообщения
- Конфликт логики (может скроллить когда не нужно)
- После прыжка на дату может автоматически вернуться вниз
Решение:
// Строка 316 userWebSocket.js
if (isOwnMessage || isAtBottom) {
// Добавить проверку на V2
if (!window.chatControllerV2) {
requestAnimationFrame(() => scrollToBottom());
}
}
⚠️ Проблема 2: loadNewer загружает не из Store
Место: scrollManagerV2.js метод loadMoreNewer()
Описание: При скролле вниз после прыжка на дату, метод loadNewer() загружает сообщения ПОСЛЕ текущего newestId в Store, а не после последнего видимого сообщения
Текущая логика:
- Пользователь прыгает на 1 декабря 2024
- Store содержит ~30 сообщений вокруг этой даты
- newestId = последнее из этих 30 сообщений (например 5 декабря)
- При скролле вниз loadNewer грузит сообщения ПОСЛЕ 5 декабря
- НО пропускает весь период с 6 декабря по сегодня!
Правильная архитектура (как Telegram):
[Store после loadAround]
├── messages: сообщения вокруг 1 декабря (30 шт)
├── boundaries:
│ ├── oldestId: самое старое сообщение в Store
│ ├── newestId: самое новое сообщение в Store
│ ├── hasMoreBefore: true (есть сообщения ДО oldestId)
│ ├── hasMoreAfter: true (есть сообщения ПОСЛЕ newestId ДО текущего момента)
│ └── actualNewestId: ID самого последнего сообщения в чате (с сервера)
Решение:
- При
loadAround()сохранять информацию о РЕАЛЬНОМ самом новом сообщении в чате hasMoreAfterдолжен означать "есть сообщения между newestId и actualNewestId"- При
loadNewer()проверятьhasMoreAfterи грузить следующий батч
✅ Рекомендации
1. Отключить дублирующийся автоскролл в userWebSocket
// userWebSocket.js строка 316
if (isOwnMessage || isAtBottom) {
if (!window.chatControllerV2) { // ← Добавить проверку
requestAnimationFrame(() => scrollToBottom());
}
}
2. Исправить loadNewer для корректной загрузки истории
Проблема: Когда прыгаем на дату, loadNewer грузит сообщения сразу после текущих, пропуская промежуточные
Варианты решения:
Вариант A: Добавить actualNewestId в boundaries
// При loadAround сохранять реальный newest
_updateBoundaries(chatId, {
newestId: messages[messages.length - 1].id, // Последнее загруженное
actualNewestId: result.actual_newest_id, // С сервера
hasMoreAfter: newestId < actualNewestId
});
Вариант B: Использовать timestamp вместо after_id
// loadNewer с timestamp вместо ID
const result = await this._fetchWithRetry(
buildUrl(API_ENDPOINTS.MESSAGES(chatId), {
after_timestamp: boundaries.newestTimestamp,
limit: this.config.HISTORY_LIMIT
}),
requestKey
);
3. Добавить debounce для loadNewer (уже есть! ✅)
4. Логирование для отладки
Добавить в chatControllerV2._handleNewMessage:
console.log('[Autoscroll Decision]', {
isMyMessage,
isAtBottom,
shouldScroll,
scrollTop: this.scrollManager.scrollEl.scrollTop,
scrollHeight: this.scrollManager.scrollEl.scrollHeight
});
📈 Приоритеты
- ВЫСОКИЙ: Исправить дублирование автоскролла (userWebSocket.js:316)
- ВЫСОКИЙ: Исправить loadNewer для правильной загрузки промежуточных сообщений
- СРЕДНИЙ: Добавить логирование для отладки автоскролла
- НИЗКИЙ: Оптимизировать количество RAF вызовов
🧪 Тестовые сценарии
Сценарий 1: Прыжок на старую дату
- Открыть чат
- Кликнуть на дату → выбрать старую дату (например 1 месяц назад)
- ✅ Должно перепрыгнуть мгновенно (без smooth scroll)
- ✅ НЕ должно автоматически скроллить вниз после прыжка
- Прокрутить вверх → должны загрузиться старые сообщения
- Прокрутить вниз → должны загрузиться промежуточные сообщения (НЕ последние!)
Сценарий 2: Получение нового сообщения
- Открыть чат
- Прокрутить к середине истории
- Получить новое сообщение через WebSocket
- ✅ НЕ должно автоматически скроллить (показать индикатор)
- Прокрутить вниз
- ✅ Должно автоматически скроллить к новым сообщениям
Сценарий 3: Отправка своего сообщения
- Открыть чат
- Прокрутить к середине истории
- Отправить свое сообщение
- ✅ Должно автоматически проскроллить вниз к своему сообщению
📝 Заключение
Основные проблемы:
- ❌ Дублирующийся автоскролл в userWebSocket и chatControllerV2
- ❌ loadNewer не учитывает промежуточные сообщения после прыжка на дату
Статус файлов:
- ✅
chatControllerV2.js- логика правильная - ✅
scrollManagerV2.js- методы реализованы корректно - ❌
userWebSocket.js- нужна проверка на V2 - ⚠️
messageLoaderV2.js- нужно исправить loadNewer для учета промежуточных сообщений