SHA256
Улучшить UX личного чата на мобильных
This commit is contained in:
@@ -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({
|
||||
<div class="dm-floating-menu-layer" id="chat-message-actions-layer">
|
||||
<div class="modal-card stack dm-message-actions-menu dm-message-actions-popover" id="${menuId}">
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-copy">Скопировать как текст</button>
|
||||
<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-read">Прочесть</button>
|
||||
${showReadAloud ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-read">Прочесть</button>' : ''}
|
||||
${canEdit ? '<button class="secondary-btn dm-message-action-btn" type="button" id="msg-action-edit">Изменить</button>' : ''}
|
||||
${canDelete ? '<button class="secondary-btn dm-message-action-btn dm-message-action-btn--danger" type="button" id="msg-action-delete">Удалить</button>' : ''}
|
||||
</div>
|
||||
@@ -263,8 +264,13 @@ function resolveMessageEditedTimeMs(msg) {
|
||||
|
||||
function scrollToLatestMessage(list) {
|
||||
if (!list) return;
|
||||
const scrollContainer = list.closest('.screen-content') || list.parentElement || list;
|
||||
const lastBubble = list.lastElementChild;
|
||||
const apply = () => {
|
||||
list.scrollTop = list.scrollHeight;
|
||||
if (lastBubble?.scrollIntoView) {
|
||||
lastBubble.scrollIntoView({ block: 'end', inline: 'nearest' });
|
||||
}
|
||||
scrollContainer.scrollTop = scrollContainer.scrollHeight;
|
||||
};
|
||||
apply();
|
||||
window.requestAnimationFrame(apply);
|
||||
@@ -334,6 +340,35 @@ function renderLog(list, chatId, { onOpenActions } = {}) {
|
||||
markChatRead(chatId);
|
||||
}
|
||||
|
||||
function preserveComposerSelection(input, callback) {
|
||||
if (!input || typeof callback !== 'function') {
|
||||
if (typeof callback === 'function') callback();
|
||||
return;
|
||||
}
|
||||
|
||||
const wasFocused = document.activeElement === input;
|
||||
const start = Number(input.selectionStart ?? input.value.length);
|
||||
const end = Number(input.selectionEnd ?? input.value.length);
|
||||
|
||||
callback();
|
||||
|
||||
if (!wasFocused) return;
|
||||
try {
|
||||
input.focus({ preventScroll: true });
|
||||
} catch {
|
||||
input.focus();
|
||||
}
|
||||
try {
|
||||
input.setSelectionRange(start, end);
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
function setChatKeyboardOpen(isOpen) {
|
||||
document.body.classList.toggle('chat-keyboard-open', !!isOpen);
|
||||
}
|
||||
|
||||
export function render({ navigate, route }) {
|
||||
const routeChatId = route.params.chatId || 'u1';
|
||||
const chatId = normalizeDmChatId(routeChatId) || 'u1';
|
||||
@@ -345,6 +380,8 @@ export function render({ navigate, route }) {
|
||||
|
||||
const screen = document.createElement('section');
|
||||
screen.className = 'stack dm-screen dm-chat-screen';
|
||||
const isSpeechToTextReady = isSpeechToTextConfigured(state.entrySettings);
|
||||
const isTextToSpeechReady = isTextToSpeechConfigured(state.entrySettings);
|
||||
const isKnownContact = (state.contacts || []).some((x) => String(x || '').toLowerCase() === String(chatId || '').toLowerCase());
|
||||
|
||||
const handleReadAloud = async (msg) => {
|
||||
@@ -431,7 +468,7 @@ export function render({ navigate, route }) {
|
||||
</div>
|
||||
<textarea class="input dm-input" name="message" rows="1" placeholder="Введите сообщение" maxlength="12000"></textarea>
|
||||
<div class="dm-actions-col">
|
||||
<button class="ghost-btn dm-voice-btn" type="button" id="chat-voice-input" title="Голосовой ввод">🎤</button>
|
||||
${isSpeechToTextReady ? '<button class="ghost-btn dm-voice-btn" type="button" id="chat-voice-input" title="Голосовой ввод">🎤</button>' : ''}
|
||||
<button class="primary-btn dm-send-btn dm-send-icon-btn" type="submit" title="Отправить">➤</button>
|
||||
</div>
|
||||
`;
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user