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 54f4097..a7f3d1f 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 @@ -6,6 +6,9 @@ - при открытии уже существующего личного чата стартовая позиция выбирается по хвосту переписки: - если есть непрочитанные сообщения, открытие происходит на линии `Новые сообщения`; - если непрочитанных нет, чат открывается сразу в самом низу. +- на мобильной экранной клавиатуре `Enter` больше не отправляет сообщение, а создаёт новую строку; +- отправка выполняется только кнопкой `Отправить`; +- после отправки фокус остаётся в поле ввода, чтобы экранная клавиатура не закрывалась автоматически. ## Что проверять @@ -17,6 +20,11 @@ - проверить то же поведение после прихода подтверждения отправки/перерисовки списка. - открыть существующий чат с непрочитанными сообщениями и убедиться, что видна линия `Новые сообщения` и сообщения ниже неё; - открыть существующий чат без непрочитанных сообщений и убедиться, что открыт конец переписки, а не начало. +- на телефоне нажать кнопку `Enter` на экранной клавиатуре и убедиться, что появляется новая строка, а сообщение не уходит; +- нажать кнопку отправки и убедиться, что сообщение отправилось, осталось видимым внизу и клавиатура не закрылась; +- отдельно проверить два сценария прокрутки после отправки: + - пользователь уже почти внизу и прокрутка идёт плавно; + - пользователь был заметно выше и чат догоняет новый хвост без лишних скачков. ## Ожидаемый результат @@ -24,6 +32,7 @@ - при наборе доступно больше вертикального места; - собственное только что отправленное сообщение сразу попадает в видимую область. - при открытии чата пользователь сразу попадает в актуальную часть переписки. +- мобильный ввод не конфликтует с привычным поведением экранной клавиатуры. ## Статус diff --git a/VERSION.properties b/VERSION.properties index 7e2e0ba..88a2994 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.2.265 +client.version=1.2.266 server.version=1.2.248 diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js index 63b4f56..b4636dd 100644 --- a/shine-UI/js/pages/chat-view.js +++ b/shine-UI/js/pages/chat-view.js @@ -281,6 +281,30 @@ function scrollToLatestMessage(list) { window.setTimeout(apply, 260); } +function scrollToLatestMessageSmart(list, { smoothIfNearBottom = false } = {}) { + if (!list) return; + const scrollContainer = list.closest('.screen-content') || list.parentElement || list; + const lastBubble = list.lastElementChild; + const maxScrollTop = Math.max(0, scrollContainer.scrollHeight - scrollContainer.clientHeight); + const distanceToBottom = Math.max(0, maxScrollTop - scrollContainer.scrollTop); + const useSmooth = smoothIfNearBottom && distanceToBottom <= 180; + + const apply = (behavior = useSmooth ? 'smooth' : 'auto') => { + if (lastBubble?.scrollIntoView) { + lastBubble.scrollIntoView({ block: 'end', inline: 'nearest', behavior }); + } + if (behavior === 'smooth' && typeof scrollContainer.scrollTo === 'function') { + scrollContainer.scrollTo({ top: scrollContainer.scrollHeight, behavior: 'smooth' }); + } else { + scrollContainer.scrollTop = scrollContainer.scrollHeight; + } + }; + + apply(useSmooth ? 'smooth' : 'auto'); + window.requestAnimationFrame(() => apply('auto')); + window.setTimeout(() => apply('auto'), useSmooth ? 220 : 90); +} + function scrollToUnreadSeparator(list) { if (!list) return false; const separator = list.querySelector('.chat-unread-separator'); @@ -619,7 +643,7 @@ export function render({ navigate, route }) { const editing = activeEdit; const tempId = editing ? '' : addOutgoingPendingMessage(chatId, text); renderLog(log, chatId, { onOpenActions: handleOpenActions }); - scrollToLatestMessage(log); + scrollToLatestMessageSmart(log, { smoothIfNearBottom: true }); try { let result; @@ -657,10 +681,16 @@ export function render({ navigate, route }) { } renderLog(log, chatId, { onOpenActions: handleOpenActions }); - scrollToLatestMessage(log); - window.requestAnimationFrame(() => scrollToLatestMessage(log)); - window.setTimeout(() => scrollToLatestMessage(log), 90); - window.setTimeout(() => scrollToLatestMessage(log), 220); + scrollToLatestMessageSmart(log, { smoothIfNearBottom: true }); + window.requestAnimationFrame(() => scrollToLatestMessageSmart(log, { smoothIfNearBottom: true })); + window.setTimeout(() => scrollToLatestMessageSmart(log, { smoothIfNearBottom: true }), 220); + if (input) { + try { + input.focus({ preventScroll: true }); + } catch { + input.focus(); + } + } addAppLogEntry({ level: 'info', source: 'outgoing-dm', @@ -735,27 +765,7 @@ export function render({ navigate, route }) { }); input?.addEventListener('keydown', async (event) => { if (event.key !== 'Enter') return; - if (event.ctrlKey) { - event.preventDefault(); - const start = Number(input.selectionStart ?? input.value.length); - const end = Number(input.selectionEnd ?? input.value.length); - const value = String(input.value || ''); - input.value = `${value.slice(0, start)}\n${value.slice(end)}`; - const nextPos = start + 1; - try { - input.setSelectionRange(nextPos, nextPos); - } catch { - // ignore - } - autoResizeComposer(input); - return; - } - event.preventDefault(); - const text = String(input.value || '').trim(); - if (!text) return; - input.value = ''; - autoResizeComposer(input); - await sendTextMessage(text); + window.requestAnimationFrame(() => autoResizeComposer(input)); }); form.querySelector('#chat-voice-input')?.addEventListener('click', async () => { @@ -773,6 +783,15 @@ export function render({ navigate, route }) { }); }); + form.querySelector('.dm-send-btn')?.addEventListener('pointerdown', (event) => { + event.preventDefault(); + try { + input?.focus({ preventScroll: true }); + } catch { + input?.focus(); + } + }); + form.addEventListener('submit', async (event) => { event.preventDefault(); const text = String(input.value || '').trim(); @@ -780,6 +799,7 @@ export function render({ navigate, route }) { input.value = ''; autoResizeComposer(input); await sendTextMessage(text); + focusInputToEnd(); }); const handleIncomingChatRefresh = async (event) => {