From 1dd01ae2c16f974d9f61a0451622e05df5f3f4ae 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 15:21:52 +0300 Subject: [PATCH] feat: Smart button visibility and load-to-present on click MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two types of bottom: 1. Historical bottom (hasMoreAfter=true): - Button ALWAYS visible (even at scroll bottom) - Click → loads latest 50 messages to present time - Renders and scrolls to bottom 2. Present bottom (hasMoreAfter=false): - Button hidden when isNearBottom() - Click → smart scroll (to unread or bottom) Changes: - _setupSmartButtonVisibility() monitors hasMoreAfter - _handleScrollButtonClick() loads history if hasMoreAfter - Added public loadLatest() method - Emits 'chat:messagesLoaded' event for UI updates Result: - Historical view: button always shows, loads to present - Present view: button hides at bottom, smart unread scroll - No more stuck in history without way to present --- .../static/js/controllers/chatControllerV2.js | 80 +++++++++++++++++-- backend/static/js/loaders/messageLoaderV2.js | 37 ++++++++- 2 files changed, 109 insertions(+), 8 deletions(-) diff --git a/backend/static/js/controllers/chatControllerV2.js b/backend/static/js/controllers/chatControllerV2.js index bdca43ff..e833f4b4 100644 --- a/backend/static/js/controllers/chatControllerV2.js +++ b/backend/static/js/controllers/chatControllerV2.js @@ -472,13 +472,13 @@ export class ChatControllerV2 { _handleWsNewMessage(detail) { const { message, chatId } = detail; if (chatId === this.chatId && message) { - this.loader.handleNewMessage(message); - - // 🎯 УМНЫЙ АВТОСКРОЛЛ: только если мы в "настоящем времени" const boundaries = this.loader.chatBoundaries.get(chatId); const isInPresentTime = boundaries && !boundaries.hasMoreAfter; if (isInPresentTime) { + // ✅ Настоящее время (hasMoreAfter=false) - обрабатываем как обычно + this.loader.handleNewMessage(message); + const isNearBottom = this.scrollManager.isNearBottom(); if (isNearBottom) { @@ -491,8 +491,12 @@ export class ChatControllerV2 { // ✅ Пользователь НЕ внизу → показываем badge this._incrementUnreadBadge(); } + } else { + // ❌ Исторический просмотр (hasMoreAfter=true) - НЕ рендерим, только badge + // Не добавляем в Store (не рендерим), только показываем что есть новые + this._incrementUnreadBadge(); + console.log('[ChatControllerV2] New message in historical view - showing badge only'); } - // Если hasMoreAfter = true (исторический низ) - ничего не делаем } } @@ -677,16 +681,80 @@ export class ChatControllerV2 { this._scrollBtn.addEventListener('click', () => this._handleScrollButtonClick()); + // Настраиваем умную видимость + this._setupSmartButtonVisibility(); + console.log('[ChatControllerV2] Smart scroll button initialized'); } + /** + * Настраивает умную видимость кнопки + * @private + */ + _setupSmartButtonVisibility() { + if (!this._scrollBtn) return; + + // Обновляем видимость при скролле + const updateVisibility = () => { + const boundaries = this.loader.chatBoundaries.get(this.chatId); + const hasMoreAfter = boundaries?.hasMoreAfter || false; + const isNearBottom = this.scrollManager.isNearBottom(); + + // Показываем кнопку если: + // 1. Пользователь НЕ внизу (любой случай) + // 2. ИЛИ hasMoreAfter=true (даже если внизу) + const shouldShow = !isNearBottom || hasMoreAfter; + + this._scrollBtn.classList.toggle('show', shouldShow); + }; + + // Обновляем при скролле + this.scrollElement.addEventListener('scroll', updateVisibility); + + // Обновляем при загрузке новых сообщений + window.addEventListener('chat:messagesLoaded', updateVisibility); + + // Начальное обновление + setTimeout(updateVisibility, 100); + } + /** * Обработчик клика на умную кнопку * @private */ - _handleScrollButtonClick() { + async _handleScrollButtonClick() { const boundaries = this.loader.chatBoundaries.get(this.chatId); - const isInPresentTime = boundaries && !boundaries.hasMoreAfter; + const hasMoreAfter = boundaries?.hasMoreAfter || false; + + if (hasMoreAfter) { + // 📥 ИСТОРИЧЕСКИЙ НИЗ: Загружаем всю историю до конца + console.log('[ChatControllerV2] Loading history to present time...'); + + try { + // Загружаем последние сообщения (настоящее время) + const result = await this.loader.loadLatest(this.chatId, 50); + + // Рендерим новые сообщения + await this.renderer.render(this.chatId); + + // Скроллим в низ + await new Promise(resolve => requestAnimationFrame(resolve)); + this.scrollElement.scrollTop = this.scrollElement.scrollHeight; + + console.log('[ChatControllerV2] Loaded to present, messages:', result.messages.length); + } catch (error) { + console.error('[ChatControllerV2] Failed to load history:', error); + // Если ошибка - просто скроллим в низ загруженного + this.scrollElement.scrollTop = this.scrollElement.scrollHeight; + } + + // Сбрасываем badge + this._resetUnreadBadge(); + return; + } + + // 🔵 НАСТОЯЩИЙ НИЗ: Умная логика + const isInPresentTime = !hasMoreAfter; if (this._unreadCount > 0 && this.lastReadMessageId && isInPresentTime) { // Режим "Перейти к непрочитанным" diff --git a/backend/static/js/loaders/messageLoaderV2.js b/backend/static/js/loaders/messageLoaderV2.js index fbf1dd90..de35cdfc 100644 --- a/backend/static/js/loaders/messageLoaderV2.js +++ b/backend/static/js/loaders/messageLoaderV2.js @@ -241,9 +241,37 @@ export class MessageLoaderV2 { * Загружает новые сообщения (после текущего newest) * Используется для прокрутки вниз после перехода по дате (как в Telegram) * @param {number} chatId - ID чата + * @param {number} [limit] - Количество сообщений (опционально) * @returns {Promise>} */ - async loadNewer(chatId) { + async loadNewer(chatId, limit = null) { + + /** + * Загружает последние сообщения (самые свежие в чате) + * Используется для перехода к настоящему времени из исторического просмотра + * @param {number} chatId - ID чата + * @param {number} [limit] - Количество сообщений + * @returns {Promise} + */ + async loadLatest(chatId, limit = 50) { + const requestKey = `latest_${chatId}`; + + console.log('[MessageLoaderV2] Loading latest messages to present time'); + + try { + const result = await this._loadLatest(chatId, limit, requestKey); + + // Эмитим событие + window.dispatchEvent(new CustomEvent('chat:messagesLoaded', { + detail: { chatId, count: result.messages.length, type: 'latest' } + })); + + return result; + } catch (error) { + console.error('[MessageLoaderV2] Failed to load latest:', error); + throw error; + } + } const requestKey = `newer_${chatId}`; const state = this._getLoadingState(chatId); @@ -277,7 +305,7 @@ export class MessageLoaderV2 { const result = await this._fetchWithRetry( buildUrl(API_ENDPOINTS.MESSAGES(chatId), { after_id: boundaries.newestId, - limit: this.config.HISTORY_LIMIT + limit: limit || this.config.HISTORY_LIMIT }), requestKey ); @@ -302,6 +330,11 @@ export class MessageLoaderV2 { this._updateBoundaries(chatId, { hasMoreAfter: false }); } + // Эмитим событие для обновления UI + window.dispatchEvent(new CustomEvent('chat:messagesLoaded', { + detail: { chatId, count: messages.length, type: 'newer' } + })); + return messages; } catch (error) {