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
new file mode 100644
index 0000000..ad4c523
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-06-25_0645_chat_mobile_keyboard_and_autoscroll.md
@@ -0,0 +1,24 @@
+## Краткое описание
+
+Доработан UX личного чата на мобильных устройствах:
+- при открытой экранной клавиатуре нижний тулбар на 5 кнопок временно скрывается;
+- после отправки собственного сообщения чат автоматически прокручивается вниз так, чтобы новое сообщение было видно сразу.
+
+## Что проверять
+
+- открыть личный чат на телефоне;
+- тапнуть в поле ввода и убедиться, что нижний тулбар скрывается после появления клавиатуры;
+- закрыть клавиатуру и убедиться, что тулбар возвращается;
+- отправить короткое сообщение, находясь не в самом низу переписки;
+- убедиться, что после отправки экран прокручивается вниз и новое сообщение видно сразу;
+- проверить то же поведение после прихода подтверждения отправки/перерисовки списка.
+
+## Ожидаемый результат
+
+- клавиатура не конфликтует по высоте с нижним тулбаром;
+- при наборе доступно больше вертикального места;
+- собственное только что отправленное сообщение сразу попадает в видимую область.
+
+## Статус
+
+`pending`
diff --git a/VERSION.properties b/VERSION.properties
index 62408f5..d5a53d2 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.263
+client.version=1.2.264
server.version=1.2.248
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js
index e3a6eaf..d641ae6 100644
--- a/shine-UI/js/app.js
+++ b/shine-UI/js/app.js
@@ -745,6 +745,19 @@ function renderApp() {
}
}
+function refreshToolbarOnly() {
+ const route = getRoute();
+ const pageId = route.pageId || (state.session.isAuthorized ? 'messages-list' : 'start-view');
+ const page = routes[pageId] || routes['start-view'];
+ const showAppChrome = page.pageMeta?.showAppChrome !== false;
+
+ toolbarEl.innerHTML = '';
+ if (showAppChrome) {
+ toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
+ }
+ refreshConnectionUi();
+}
+
async function tryAutoLogin() {
if (!state.session.login || !state.session.sessionId) return;
try {
@@ -987,7 +1000,18 @@ async function init() {
}
const pageId = getRoute().pageId || '';
- if (pageId === 'chat-view' || pageId === 'messages-list' || shouldRefreshToolbarUnread) {
+ if (pageId === 'chat-view') {
+ window.dispatchEvent(new CustomEvent('shine-chat-messages-updated', {
+ detail: {
+ chatId,
+ messageType,
+ messageKey,
+ },
+ }));
+ if (shouldRefreshToolbarUnread) {
+ refreshToolbarOnly();
+ }
+ } else if (pageId === 'messages-list' || shouldRefreshToolbarUnread) {
renderApp();
}
});
diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index f9e9af7..c698b37 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -17,7 +17,7 @@ import {
} from '../state.js';
import { startOutgoingCall } from '../services/call-service.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
-import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
+import { isSpeechToTextConfigured, isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
import { showToast } from '../services/channels-ux.js';
export const pageMeta = { id: 'chat-view', title: 'Чат' };
@@ -60,6 +60,7 @@ function openMessageActionsMenu({
anchorX = 0,
anchorY = 0,
messageText = '',
+ showReadAloud = true,
canEdit = false,
canDelete = false,
onReadAloud,
@@ -74,7 +75,7 @@ function openMessageActionsMenu({
-
+ ${isSpeechToTextReady ? '' : ''}
`;
@@ -441,6 +478,17 @@ export function render({ navigate, route }) {
const editBannerText = form.querySelector('#chat-edit-banner-text');
const editCancelBtn = form.querySelector('#chat-edit-cancel');
let activeEdit = null;
+ let inputFocused = false;
+ const baseViewportHeight = Math.max(window.visualViewport?.height || 0, window.innerHeight || 0);
+
+ const syncKeyboardUi = () => {
+ const viewportHeight = Math.max(window.visualViewport?.height || 0, window.innerHeight || 0);
+ const viewportShrunk = baseViewportHeight - viewportHeight > 120;
+ setChatKeyboardOpen(inputFocused && viewportShrunk);
+ if (inputFocused) {
+ window.requestAnimationFrame(() => scrollToLatestMessage(log));
+ }
+ };
const syncEditBanner = () => {
if (!editBanner || !editBannerText) return;
@@ -544,6 +592,7 @@ export function render({ navigate, route }) {
const editing = activeEdit;
const tempId = editing ? '' : addOutgoingPendingMessage(chatId, text);
renderLog(log, chatId, { onOpenActions: handleOpenActions });
+ scrollToLatestMessage(log);
try {
let result;
@@ -621,6 +670,7 @@ export function render({ navigate, route }) {
anchorX: Number(event?.clientX || 0),
anchorY: Number(event?.clientY || 0),
messageText: msg?.text || '',
+ showReadAloud: isTextToSpeechReady,
canEdit: msg?.from === 'out' && Number(msg?.messageType || 0) === 2,
canDelete: msg?.from === 'out' && Number(msg?.messageType || 0) === 2,
onReadAloud: async () => handleReadAloud(msg),
@@ -648,8 +698,14 @@ export function render({ navigate, route }) {
autoResizeComposer(input);
input?.addEventListener('input', () => autoResizeComposer(input));
input?.addEventListener('focus', () => {
+ inputFocused = true;
+ syncKeyboardUi();
scrollToLatestMessage(log);
});
+ input?.addEventListener('blur', () => {
+ inputFocused = false;
+ setChatKeyboardOpen(false);
+ });
input?.addEventListener('keydown', async (event) => {
if (event.key !== 'Enter') return;
if (event.ctrlKey) {
@@ -699,12 +755,32 @@ export function render({ navigate, route }) {
await sendTextMessage(text);
});
+ const handleIncomingChatRefresh = async (event) => {
+ const updatedChatId = normalizeDmChatId(event?.detail?.chatId);
+ if (updatedChatId !== chatId) return;
+ preserveComposerSelection(input, () => {
+ renderLog(log, chatId, { onOpenActions: handleOpenActions });
+ });
+ window.requestAnimationFrame(() => scrollToLatestMessage(log));
+ void sendReadReceiptsForVisible(chatId);
+ };
+
+ window.addEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
+ window.visualViewport?.addEventListener('resize', syncKeyboardUi);
+ window.addEventListener('resize', syncKeyboardUi);
+
wrap.append(log, form);
screen.append(wrap);
renderLog(log, chatId, { onOpenActions: handleOpenActions });
window.requestAnimationFrame(() => scrollToLatestMessage(log));
window.setTimeout(() => scrollToLatestMessage(log), 180);
void sendReadReceiptsForVisible(chatId);
+ screen.cleanup = () => {
+ setChatKeyboardOpen(false);
+ window.removeEventListener('shine-chat-messages-updated', handleIncomingChatRefresh);
+ window.visualViewport?.removeEventListener('resize', syncKeyboardUi);
+ window.removeEventListener('resize', syncKeyboardUi);
+ };
return screen;
}
diff --git a/shine-UI/styles/layout.css b/shine-UI/styles/layout.css
index 91d90b3..4b44cc0 100644
--- a/shine-UI/styles/layout.css
+++ b/shine-UI/styles/layout.css
@@ -65,6 +65,18 @@ body::before {
bottom: 0;
padding: 10px 12px calc(10px + env(safe-area-inset-bottom));
background: linear-gradient(180deg, rgba(7, 12, 23, 0) 0%, rgba(6, 11, 22, 0.96) 44%);
+ transition: opacity 0.18s ease, transform 0.18s ease;
+}
+
+body.chat-keyboard-open .screen-content {
+ bottom: 0;
+ padding-bottom: calc(14px + env(safe-area-inset-bottom));
+}
+
+body.chat-keyboard-open .toolbar-slot {
+ opacity: 0;
+ pointer-events: none;
+ transform: translateY(calc(100% + env(safe-area-inset-bottom)));
}
.connection-retry-banner {