diff --git a/Dev_Docs/Pending_Features/2026-06-25_0645_chat_mobile_keyboard_and_autoscroll.md b/Dev_Docs/Pending_Features/2026-06-25_0645_chat_mobile_keyboard_and_autoscroll.md index ad4c523..54f4097 100644 --- a/Dev_Docs/Pending_Features/2026-06-25_0645_chat_mobile_keyboard_and_autoscroll.md +++ b/Dev_Docs/Pending_Features/2026-06-25_0645_chat_mobile_keyboard_and_autoscroll.md @@ -3,6 +3,9 @@ Доработан UX личного чата на мобильных устройствах: - при открытой экранной клавиатуре нижний тулбар на 5 кнопок временно скрывается; - после отправки собственного сообщения чат автоматически прокручивается вниз так, чтобы новое сообщение было видно сразу. +- при открытии уже существующего личного чата стартовая позиция выбирается по хвосту переписки: + - если есть непрочитанные сообщения, открытие происходит на линии `Новые сообщения`; + - если непрочитанных нет, чат открывается сразу в самом низу. ## Что проверять @@ -12,12 +15,15 @@ - отправить короткое сообщение, находясь не в самом низу переписки; - убедиться, что после отправки экран прокручивается вниз и новое сообщение видно сразу; - проверить то же поведение после прихода подтверждения отправки/перерисовки списка. +- открыть существующий чат с непрочитанными сообщениями и убедиться, что видна линия `Новые сообщения` и сообщения ниже неё; +- открыть существующий чат без непрочитанных сообщений и убедиться, что открыт конец переписки, а не начало. ## Ожидаемый результат - клавиатура не конфликтует по высоте с нижним тулбаром; - при наборе доступно больше вертикального места; - собственное только что отправленное сообщение сразу попадает в видимую область. +- при открытии чата пользователь сразу попадает в актуальную часть переписки. ## Статус diff --git a/VERSION.properties b/VERSION.properties index d5a53d2..7e2e0ba 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.264 +client.version=1.2.265 server.version=1.2.248 diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index c698b37..63b4f56 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -281,7 +281,27 @@ function scrollToLatestMessage(list) { window.setTimeout(apply, 260); } -function renderLog(list, chatId, { onOpenActions } = {}) { +function scrollToUnreadSeparator(list) { + if (!list) return false; + const separator = list.querySelector('.chat-unread-separator'); + if (!separator) return false; + const scrollContainer = list.closest('.screen-content') || list.parentElement || list; + const apply = () => { + if (separator?.scrollIntoView) { + separator.scrollIntoView({ block: 'start', inline: 'nearest' }); + } + const bottomSlack = 72; + scrollContainer.scrollTop = Math.max(0, scrollContainer.scrollTop - bottomSlack); + }; + apply(); + window.requestAnimationFrame(apply); + window.requestAnimationFrame(() => window.requestAnimationFrame(apply)); + window.setTimeout(apply, 60); + window.setTimeout(apply, 160); + return true; +} + +function renderLog(list, chatId, { onOpenActions, markAsRead = true, scrollMode = 'latest' } = {}) { list.innerHTML = ''; const messages = getChatMessages(chatId); let unreadSeparatorInserted = false; @@ -336,8 +356,14 @@ function renderLog(list, chatId, { onOpenActions } = {}) { }); list.append(bubble); }); - scrollToLatestMessage(list); - markChatRead(chatId); + if (scrollMode === 'unread' && !scrollToUnreadSeparator(list)) { + scrollToLatestMessage(list); + } else if (scrollMode === 'latest') { + scrollToLatestMessage(list); + } + if (markAsRead) { + markChatRead(chatId); + } } function preserveComposerSelection(input, callback) { @@ -383,6 +409,7 @@ export function render({ navigate, route }) { const isSpeechToTextReady = isSpeechToTextConfigured(state.entrySettings); const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings); const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase()); + const hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread); const handleReadAloud = async (msg) => { if (!isTextToSpeechConfigured(state.entrySettings)) { @@ -759,7 +786,7 @@ export function render({ navigate, route }) { const updatedChatId = normalizeDmChatId(event?.detail?.chatId); if (updatedChatId !== chatId) return; preserveComposerSelection(input, () => { - renderLog(log, chatId, { onOpenActions: handleOpenActions }); + renderLog(log, chatId, { onOpenActions: handleOpenActions, scrollMode: 'latest' }); }); window.requestAnimationFrame(() => scrollToLatestMessage(log)); void sendReadReceiptsForVisible(chatId); @@ -771,9 +798,19 @@ export function render({ navigate, route }) { wrap.append(log, form); screen.append(wrap); - renderLog(log, chatId, { onOpenActions: handleOpenActions }); - window.requestAnimationFrame(() => scrollToLatestMessage(log)); - window.setTimeout(() => scrollToLatestMessage(log), 180); + renderLog(log, chatId, { + onOpenActions: handleOpenActions, + markAsRead: false, + scrollMode: hasUnreadIncoming ? 'unread' : 'latest', + }); + if (hasUnreadIncoming) { + window.requestAnimationFrame(() => scrollToUnreadSeparator(log)); + window.setTimeout(() => scrollToUnreadSeparator(log), 180); + } else { + window.requestAnimationFrame(() => scrollToLatestMessage(log)); + window.setTimeout(() => scrollToLatestMessage(log), 180); + } + window.setTimeout(() => markChatRead(chatId), 220); void sendReadReceiptsForVisible(chatId); screen.cleanup = () => { setChatKeyboardOpen(false);