Улучшить открытие личного чата

This commit is contained in:
AidarKC 2026-06-25 10:55:17 +04:00
parent 0f3c4a621d
commit 827d2e9c3e
3 changed files with 51 additions and 8 deletions

View File

@ -3,6 +3,9 @@
Доработан UX личного чата на мобильных устройствах:
- при открытой экранной клавиатуре нижний тулбар на 5 кнопок временно скрывается;
- после отправки собственного сообщения чат автоматически прокручивается вниз так, чтобы новое сообщение было видно сразу.
- при открытии уже существующего личного чата стартовая позиция выбирается по хвосту переписки:
- если есть непрочитанные сообщения, открытие происходит на линии `Новые сообщения`;
- если непрочитанных нет, чат открывается сразу в самом низу.
## Что проверять
@ -12,12 +15,15 @@
- отправить короткое сообщение, находясь не в самом низу переписки;
- убедиться, что после отправки экран прокручивается вниз и новое сообщение видно сразу;
- проверить то же поведение после прихода подтверждения отправки/перерисовки списка.
- открыть существующий чат с непрочитанными сообщениями и убедиться, что видна линия `Новые сообщения` и сообщения ниже неё;
- открыть существующий чат без непрочитанных сообщений и убедиться, что открыт конец переписки, а не начало.
## Ожидаемый результат
- клавиатура не конфликтует по высоте с нижним тулбаром;
- при наборе доступно больше вертикального места;
- собственное только что отправленное сообщение сразу попадает в видимую область.
- при открытии чата пользователь сразу попадает в актуальную часть переписки.
## Статус

View File

@ -1,2 +1,2 @@
client.version=1.2.264
client.version=1.2.265
server.version=1.2.248

View File

@ -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,9 +356,15 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
});
list.append(bubble);
});
if (scrollMode === 'unread' && !scrollToUnreadSeparator(list)) {
scrollToLatestMessage(list);
} else if (scrollMode === 'latest') {
scrollToLatestMessage(list);
}
if (markAsRead) {
markChatRead(chatId);
}
}
function preserveComposerSelection(input, callback) {
if (!input || typeof callback !== 'function') {
@ -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 });
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);