From aec1bfbca48beb9a3f35734e13b51042844d0e9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=98=D0=B3=D0=BE=D1=80=D1=8C=20=D0=9D=D0=B0=D0=B4=D0=B5?= =?UTF-8?q?=D0=B8=D0=BD?= Date: Tue, 13 Jan 2026 14:41:24 +0300 Subject: [PATCH] feat: Add instant positioning to last read message on chat open MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Problem: After removing autoscroll, last read message wasn't visible on initial load Solution: - ✨ NEW: _positionToMessage() - instant viewport positioning WITHOUT scroll animation - Sets scrollTop BEFORE user sees chat (no visible scrolling) - Centers last read message with sticky header offset (96px) - Falls back to bottom position for new chats Difference: - ❌ Old: render → scrollIntoView() → visible scroll animation - ✅ New: render → scrollTop = X → instant positioning Result: - Chat opens IMMEDIATELY showing last read message - NO visible scrolling or animation - Telegram-like behavior: position is set, not scrolled to - Natural user experience --- .../static/js/controllers/chatControllerV2.js | 82 ++++++++++++------- 1 file changed, 52 insertions(+), 30 deletions(-) diff --git a/backend/static/js/controllers/chatControllerV2.js b/backend/static/js/controllers/chatControllerV2.js index 37e2ee04..2fe7db0b 100644 --- a/backend/static/js/controllers/chatControllerV2.js +++ b/backend/static/js/controllers/chatControllerV2.js @@ -152,40 +152,17 @@ export class ChatControllerV2 { // Ждем один frame чтобы браузер применил visibility await new Promise(resolve => requestAnimationFrame(resolve)); - // 5. Скроллим к нужной позиции + // 5. ✨ НОВЫЙ ПОДХОД: Мгновенное позиционирование БЕЗ скролла if (loadResult.anchorId) { - // Есть последнее прочитанное - скроллим к нему - await this._scrollToMessage(loadResult.anchorId); + // Есть lastRead - СРАЗУ позиционируем на него + await this._positionToMessage(loadResult.anchorId); } else { - // Нет anchor - скроллим в самый низ (как в Telegram) - console.log('[ChatControllerV2] Scrolling to bottom (no anchor)...', { - scrollHeightBefore: this.scrollElement.scrollHeight, - scrollTopBefore: this.scrollElement.scrollTop, - clientHeight: this.scrollElement.clientHeight - }); - - // Используем ПРЯМОЕ присвоение scrollTop (как в legacy коде) - // scrollTo() НЕ работает на первичной загрузке + // Нет anchor - позиционируем в самый низ (новый чат) await new Promise(resolve => { requestAnimationFrame(() => { - requestAnimationFrame(() => { - requestAnimationFrame(() => { - // 🚫 REMOVED: Автоскролл при открытии чата - // Пользователь сам решает куда скроллить - // loadAround уже показывает нужный контекст - console.log('[ChatControllerV2] Initial autoscroll removed - user position preserved'); - - // Проверяем через setTimeout - setTimeout(() => { - console.log('[ChatControllerV2] After scrollTop:', { - scrollHeight: this.scrollElement.scrollHeight, - scrollTop: this.scrollElement.scrollTop, - success: this.scrollElement.scrollTop > 0 - }); - resolve(); - }, 10); - }); - }); + this.scrollElement.scrollTop = this.scrollElement.scrollHeight; + console.log('[ChatControllerV2] Initial position: bottom (no anchor)'); + resolve(); }); }); } @@ -537,6 +514,51 @@ export class ChatControllerV2 { } } + /** + * 🆕 Мгновенно позиционирует viewport на сообщение БЕЗ скроллинга + * Вызывается ТОЛЬКО при начальной загрузке + * @private + */ + async _positionToMessage(messageId) { + if (!messageId) return false; + + // Ждем один RAF для надежности + await new Promise(resolve => requestAnimationFrame(resolve)); + + const messageEl = this.scrollElement.querySelector(`[data-message-id="${messageId}"]`); + + if (messageEl) { + // Вычисляем позицию для центрирования сообщения + 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); + + // Дополнительно вычитаем offset если сообщение будет в верхней части + const adjustedScrollTop = relativeTop < this.scrollElement.clientHeight / 2 + ? targetScrollTop - stickyHeaderOffset + : targetScrollTop; + + // ✨ МГНОВЕННАЯ установка позиции - НЕ скролл, а начальная позиция! + this.scrollElement.scrollTop = Math.max(0, adjustedScrollTop); + + console.log('[ChatControllerV2] Initial position set to message:', { + messageId, + scrollTop: this.scrollElement.scrollTop, + relativeTop, + noScrollAnimation: true + }); + + return true; + } + + console.warn('[ChatControllerV2] Message not found for positioning:', messageId); + return false; + } + /** * Прокручивает к сообщению * @private