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

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 личного чата на мобильных устройствах: Доработан UX личного чата на мобильных устройствах:
- при открытой экранной клавиатуре нижний тулбар на 5 кнопок временно скрывается; - при открытой экранной клавиатуре нижний тулбар на 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 server.version=1.2.248

View File

@ -281,7 +281,27 @@ function scrollToLatestMessage(list) {
window.setTimeout(apply, 260); 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 = ''; list.innerHTML = '';
const messages = getChatMessages(chatId); const messages = getChatMessages(chatId);
let unreadSeparatorInserted = false; let unreadSeparatorInserted = false;
@ -336,9 +356,15 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
}); });
list.append(bubble); list.append(bubble);
}); });
if (scrollMode === 'unread' && !scrollToUnreadSeparator(list)) {
scrollToLatestMessage(list); scrollToLatestMessage(list);
} else if (scrollMode === 'latest') {
scrollToLatestMessage(list);
}
if (markAsRead) {
markChatRead(chatId); markChatRead(chatId);
} }
}
function preserveComposerSelection(input, callback) { function preserveComposerSelection(input, callback) {
if (!input || typeof callback !== 'function') { if (!input || typeof callback !== 'function') {
@ -383,6 +409,7 @@ export function render({ navigate, route }) {
const isSpeechToTextReady = isSpeechToTextConfigured(state.entrySettings); const isSpeechToTextReady = isSpeechToTextConfigured(state.entrySettings);
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings); const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase()); 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) => { const handleReadAloud = async (msg) => {
if (!isTextToSpeechConfigured(state.entrySettings)) { if (!isTextToSpeechConfigured(state.entrySettings)) {
@ -759,7 +786,7 @@ export function render({ navigate, route }) {
const updatedChatId = normalizeDmChatId(event?.detail?.chatId); const updatedChatId = normalizeDmChatId(event?.detail?.chatId);
if (updatedChatId !== chatId) return; if (updatedChatId !== chatId) return;
preserveComposerSelection(input, () => { preserveComposerSelection(input, () => {
renderLog(log, chatId, { onOpenActions: handleOpenActions }); renderLog(log, chatId, { onOpenActions: handleOpenActions, scrollMode: 'latest' });
}); });
window.requestAnimationFrame(() => scrollToLatestMessage(log)); window.requestAnimationFrame(() => scrollToLatestMessage(log));
void sendReadReceiptsForVisible(chatId); void sendReadReceiptsForVisible(chatId);
@ -771,9 +798,19 @@ export function render({ navigate, route }) {
wrap.append(log, form); wrap.append(log, form);
screen.append(wrap); 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.requestAnimationFrame(() => scrollToLatestMessage(log));
window.setTimeout(() => scrollToLatestMessage(log), 180); window.setTimeout(() => scrollToLatestMessage(log), 180);
}
window.setTimeout(() => markChatRead(chatId), 220);
void sendReadReceiptsForVisible(chatId); void sendReadReceiptsForVisible(chatId);
screen.cleanup = () => { screen.cleanup = () => {
setChatKeyboardOpen(false); setChatKeyboardOpen(false);