UI: правки личного чата и настроек

This commit is contained in:
AidarKC
2026-07-22 18:59:09 +04:00
parent f640451257
commit af62b51ef5
5 changed files with 88 additions and 48 deletions
+32 -29
View File
@@ -24,8 +24,7 @@ import {
hasTwemojiAsset,
stopAllTwemojiAnimations,
} from '../components/emoji-picker.js?v=202607152130';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import { isSpeechToTextConfigured, isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
import { isTextToSpeechConfigured, speakTextBySettings } from '../services/speech-tools-service.js';
import { showToast } from '../services/channels-ux.js';
import { buildDmReplyTechBlock, parseDmTechBlocks, sanitizeUserDmTextForSend } from '../services/dm-tech-blocks.js';
@@ -343,6 +342,20 @@ function autoResizeComposer(textarea) {
textarea.style.height = `${Math.min(180, Math.max(42, textarea.scrollHeight))}px`;
}
function createHeaderPhoneHandsetIcon() {
const ns = 'http://www.w3.org/2000/svg';
const svg = document.createElementNS(ns, 'svg');
svg.setAttribute('viewBox', '0 0 24 24');
svg.setAttribute('aria-hidden', 'true');
svg.setAttribute('class', 'header-icon-svg header-icon-svg--phone');
const path = document.createElementNS(ns, 'path');
path.setAttribute('d', 'M7.62 10.79a15.1 15.1 0 0 0 5.59 5.59l1.87-1.87a1 1 0 0 1 1.02-.24c1.12.37 2.31.57 3.52.57a1 1 0 0 1 1 1V20a1 1 0 0 1-1 1C10.61 21 3 13.39 3 4a1 1 0 0 1 1-1h3.37a1 1 0 0 1 1 1c0 1.21.2 2.4.57 3.52a1 1 0 0 1-.24 1.02l-1.08 1.27Z');
path.setAttribute('fill', 'currentColor');
svg.append(path);
return svg;
}
function openConfirmContactModal(targetLogin = '') {
const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(false);
@@ -762,7 +775,6 @@ 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 hasUnreadIncoming = getChatMessages(chatId).some((msg) => msg?.from === 'in' && msg?.unread);
@@ -834,10 +846,10 @@ export function render({ navigate, route }) {
leftAction: { label: '←', onClick: () => navigate('messages-list') },
rightActions: [
{
label: '☎',
title: 'Позвонить',
ariaLabel: 'Позвонить',
className: 'chat-header-icon-btn chat-header-call-btn',
iconNode: createHeaderPhoneHandsetIcon(),
onClick: handleStartCall,
},
{
@@ -948,8 +960,7 @@ export function render({ navigate, route }) {
<div class="emoji-picker-slot" id="chat-emoji-picker-slot" hidden></div>
<textarea class="input dm-input" name="message" rows="1" placeholder="Введите сообщение" maxlength="12000"></textarea>
<div class="dm-actions-col">
<button class="ghost-btn dm-emoji-btn" type="button" id="chat-emoji-toggle" aria-label="Эмодзи" title="Эмодзи"></button>
${isSpeechToTextReady ? '<button class="ghost-btn dm-voice-btn" type="button" id="chat-voice-input" title="Голосовой ввод">🎤</button>' : ''}
<button class="ghost-btn dm-emoji-btn" type="button" id="chat-emoji-toggle" aria-label="Эмодзи" title="Эмодзи"><span class="dm-emoji-btn__glyph">☺</span></button>
<button class="primary-btn dm-send-btn dm-send-icon-btn" type="submit" title="Отправить">➤</button>
</div>
`;
@@ -976,6 +987,11 @@ export function render({ navigate, route }) {
}
};
const syncComposerLayout = () => {
const height = Number.parseFloat(input?.style.height || '0');
form.classList.toggle('dm-chat-input--multiline', height > 52);
};
const rememberEmojiSelection = () => {
if (!input) return;
const valueLength = String(input.value || '').length;
@@ -1013,6 +1029,7 @@ export function render({ navigate, route }) {
}
}
autoResizeComposer(input);
syncComposerLayout();
};
if (emojiSlot) {
@@ -1053,6 +1070,7 @@ export function render({ navigate, route }) {
if (restoreDraft && input) {
input.value = draftToRestore || '';
autoResizeComposer(input);
syncComposerLayout();
focusInputToEnd();
}
};
@@ -1063,6 +1081,7 @@ export function render({ navigate, route }) {
syncEditBanner();
if (restoreDraft && input) {
autoResizeComposer(input);
syncComposerLayout();
focusInputToEnd();
}
};
@@ -1085,6 +1104,7 @@ export function render({ navigate, route }) {
if (input) {
input.value = String(parsed?.visibleText || '');
autoResizeComposer(input);
syncComposerLayout();
focusInputToEnd();
}
};
@@ -1246,6 +1266,7 @@ export function render({ navigate, route }) {
if (input) {
input.value = text;
autoResizeComposer(input);
syncComposerLayout();
focusInputToEnd();
}
addChatMessage(chatId, `${activeEdit ? 'Ошибка изменения' : 'Ошибка отправки'}: ${e.message || 'unknown'}`);
@@ -1358,9 +1379,11 @@ export function render({ navigate, route }) {
});
autoResizeComposer(input);
syncComposerLayout();
input?.addEventListener('input', () => {
rememberEmojiSelection();
autoResizeComposer(input);
syncComposerLayout();
});
input?.addEventListener('select', rememberEmojiSelection);
input?.addEventListener('keyup', rememberEmojiSelection);
@@ -1403,26 +1426,12 @@ export function render({ navigate, route }) {
if (!text) return;
input.value = '';
autoResizeComposer(input);
syncComposerLayout();
closeEmojiPicker();
await sendTextMessage(text);
focusInputToEnd();
});
form.querySelector('#chat-voice-input')?.addEventListener('click', async () => {
await openSpeechInputModal({
navigate,
onTextReady: (text) => {
const prev = String(input.value || '').trim();
input.value = prev ? `${prev} ${text}` : text;
autoResizeComposer(input);
},
onSendText: async (text) => sendTextMessage(text),
onSendQueued: () => {
showToast('Сообщение будет отправлено автоматически после распознавания', { timeoutMs: 1000 });
},
});
});
form.querySelector('.dm-send-btn')?.addEventListener('pointerdown', (event) => {
event.preventDefault();
try {
@@ -1438,6 +1447,7 @@ export function render({ navigate, route }) {
if (!text) return;
input.value = '';
autoResizeComposer(input);
syncComposerLayout();
closeEmojiPicker();
await sendTextMessage(text);
focusInputToEnd();
@@ -1464,13 +1474,6 @@ export function render({ navigate, route }) {
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(() => {
if (markChatRead(chatId) > 0) {
notifyUnreadStateUpdated();
@@ -1480,7 +1483,7 @@ export function render({ navigate, route }) {
window.requestAnimationFrame(() => {
boundScrollContainer = log.closest('.screen-content') || wrap.parentElement || wrap;
boundScrollContainer?.addEventListener('scroll', handleHistoryScroll, { passive: true });
void loadHistoryPage({ preserveScroll: false });
void loadHistoryPage({ preserveScroll: true });
});
screen.cleanup = () => {
setChatKeyboardOpen(false);
-5
View File
@@ -1,6 +1,5 @@
import { renderHeader } from '../components/header.js';
import { addAppLogEntry, authService, closeCurrentSessionAndSignOut, state } from '../state.js';
import { makeProfileLinksRoute } from '../services/shine-routes.js';
export const pageMeta = { id: 'settings-view', title: 'Настройки' };
@@ -41,7 +40,6 @@ export function render({ navigate }) {
const card = document.createElement('div');
card.className = 'card stack';
card.innerHTML = `
<button class="text-btn" type="button" id="settings-links">Показать связи</button>
<button class="text-btn" type="button" id="settings-device">Устройства</button>
<button class="text-btn" type="button" id="settings-remote-addblock">Remote AddBlock через homeserver</button>
<button class="text-btn" type="button" id="settings-servers">Настройки серверов</button>
@@ -50,9 +48,6 @@ export function render({ navigate }) {
<button class="text-btn" type="button" id="settings-signout">Завершить текущий сеанс</button>
`;
card.querySelector('#settings-links').addEventListener('click', () => {
navigate(makeProfileLinksRoute(state.session.login));
});
card.querySelector('#settings-device').addEventListener('click', () => navigate('device-view'));
card.querySelector('#settings-remote-addblock').addEventListener('click', () => navigate('remote-addblock-session-view'));
card.querySelector('#settings-servers').addEventListener('click', () => navigate('server-settings-view'));