UI: DM список метаданных и Enter/Ctrl+Enter в чате

This commit is contained in:
AidarKC 2026-05-19 15:50:42 +03:00
parent c6d310184b
commit 8325cbec84
5 changed files with 74 additions and 6 deletions

View File

@ -0,0 +1,24 @@
# Личные сообщения: правая мета-колонка и Enter/Ctrl+Enter
- Краткое описание:
- В списке `Личные сообщения` обновлена правая колонка карточки диалога:
- сверху отображается бейдж количества непрочитанных (если есть);
- снизу маленьким шрифтом отображается дата/время последнего сообщения;
- если сообщений нет, вместо времени отображается `-`.
- В экране чата нижний блок ввода закреплён (sticky) и остаётся на месте при прокрутке.
- В поле ввода чата изменено поведение клавиш:
- `Enter` отправляет сообщение;
- `Ctrl+Enter` добавляет перенос строки и не отправляет сообщение.
- Что проверять:
- В карточках диалогов справа корректно показываются непрочитанные/время/прочерк.
- В чате нижний блок ввода не уезжает при прокрутке истории.
- `Enter` отправляет сообщение из textarea.
- `Ctrl+Enter` вставляет новую строку в textarea.
- Ожидаемый результат:
- Список диалогов показывает полезную мета-информацию в стабильном формате.
- Ввод сообщений в чате работает в привычной схеме Enter/многострочность.
- Статус:
- `pending`

View File

@ -1,2 +1,2 @@
client.version=1.2.71 client.version=1.2.72
server.version=1.2.65 server.version=1.2.66

View File

@ -416,6 +416,16 @@ export function render({ navigate, route }) {
const input = form.elements.message; const input = form.elements.message;
autoResizeComposer(input); autoResizeComposer(input);
input?.addEventListener('input', () => autoResizeComposer(input)); input?.addEventListener('input', () => autoResizeComposer(input));
input?.addEventListener('keydown', async (event) => {
if (event.key !== 'Enter') return;
if (event.ctrlKey) return;
event.preventDefault();
const text = String(input.value || '').trim();
if (!text) return;
input.value = '';
autoResizeComposer(input);
await sendTextMessage(text);
});
form.querySelector('#chat-voice-input')?.addEventListener('click', async () => { form.querySelector('#chat-voice-input')?.addEventListener('click', async () => {
await openSpeechInputModal({ await openSpeechInputModal({

View File

@ -11,6 +11,17 @@ import { loadCurrentRelations } from '../services/user-connections.js';
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' }; export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
function formatChatRowTime(ts) {
const value = Number(ts || 0);
if (!Number.isFinite(value) || value <= 0) return '-';
return new Intl.DateTimeFormat('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(new Date(value));
}
export function render({ navigate }) { export function render({ navigate }) {
const screen = document.createElement('section'); const screen = document.createElement('section');
screen.className = 'stack dm-screen dm-list-screen'; screen.className = 'stack dm-screen dm-list-screen';
@ -38,9 +49,9 @@ export function render({ navigate }) {
</div> </div>
<p class="meta-muted" style="margin-top:4px;">${item.lastMessage}</p> <p class="meta-muted" style="margin-top:4px;">${item.lastMessage}</p>
</div> </div>
<div style="display:grid; justify-items:end; gap:6px;"> <div class="dm-row-meta-col">
<span class="meta-muted">${item.time}</span>
${item.unread ? `<span class="unread">${item.unread}</span>` : '<span></span>'} ${item.unread ? `<span class="unread">${item.unread}</span>` : '<span></span>'}
<span class="meta-muted dm-row-time">${item.time}</span>
</div> </div>
`; `;
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`)); row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(item.id)}`));
@ -59,12 +70,13 @@ export function render({ navigate }) {
const chat = getChatMessages(login); const chat = getChatMessages(login);
const lastChat = chat[chat.length - 1]; const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length; const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return { return {
id: login, id: login,
initials: (login[0] || '?').toUpperCase(), initials: (login[0] || '?').toUpperCase(),
name: preview?.name || login, name: preview?.name || login,
lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.', lastMessage: lastChat?.text || preview?.lastMessage || 'Диалог пока пуст.',
time: preview?.time || '—', time: formatChatRowTime(lastTimeMs),
unread, unread,
notInContacts: false, notInContacts: false,
}; };
@ -81,12 +93,13 @@ export function render({ navigate }) {
const chat = getChatMessages(login); const chat = getChatMessages(login);
const lastChat = chat[chat.length - 1]; const lastChat = chat[chat.length - 1];
const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length; const unread = chat.filter((m) => m?.from === 'in' && m?.unread).length;
const lastTimeMs = Number(lastChat?.createdAtMs || 0);
return { return {
id: login, id: login,
initials: (login[0] || '?').toUpperCase(), initials: (login[0] || '?').toUpperCase(),
name: login, name: login,
lastMessage: lastChat?.text || 'Диалог пока пуст.', lastMessage: lastChat?.text || 'Диалог пока пуст.',
time: 'сейчас', time: formatChatRowTime(lastTimeMs),
unread, unread,
notInContacts: true, notInContacts: true,
}; };

View File

@ -3427,6 +3427,19 @@ textarea.input {
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32); box-shadow: 0 4px 16px rgba(0, 0, 0, 0.32);
} }
.dm-row-meta-col {
display: grid;
justify-items: end;
align-content: start;
gap: 6px;
min-width: 64px;
}
.dm-row-time {
font-size: 11px;
line-height: 1.2;
}
.dm-chat-wrap { .dm-chat-wrap {
gap: 12px; gap: 12px;
} }
@ -3465,6 +3478,14 @@ textarea.input {
gap: 10px; gap: 10px;
grid-template-columns: 1fr auto; grid-template-columns: 1fr auto;
align-items: end; align-items: end;
position: sticky;
bottom: 0;
z-index: 10;
padding: 10px;
border-top: 1px solid rgba(212, 175, 55, 0.22);
background: rgba(8, 12, 20, 0.9);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
} }
.dm-voice-btn { .dm-voice-btn {