Merge origin/main (+56: DM v1 E2EE, промокоды, recovery key) + локальные фичи

Слито и разрешено вручную:
- chat-view.js: база — новый DM-чат агента (ответы/реплаи, ревизии, клавиатурный
  UX, голосовой ввод); поверх встроен эмодзи-пикер (Telegram-набор): кнопка ☺,
  вставка в позицию курсора, анимированные эмодзи в пузырях (renderMessageText
  поверх displayText), остановка анимаций в cleanup.
- register-view.js: база — версия агента (промокоды, глаз пароля, TEMP-заглушка
  коротких логинов); возвращена компактная ссылка «Вопросы о регистрации»
  (FAQ-экран у агента осиротел — снова достижим).
- components.css: шапка «Связей» — вариант агента (sticky + grid minmax,
  заголовок сокращается, кнопки не обрезаются); стили эмодзи-пикера сохранены.
- Заметки Pending_Features (шапка связей, поиск каналов, локальный вход)
  переложены в новую структуру docs/Pending_Features.
- VERSION: client 1.2.314, server 1.2.292 (продолжение нумерации origin).

Проверено локально: старт → «Открыть локальный тестовый режим» → Личные →
чат: эмодзи-пикер открывается, эмодзи вставляется, PNG-ассеты грузятся;
регистрация: промокод/12 слов/FAQ-ссылка работают; консоль чистая.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Pixel
2026-07-16 11:41:41 +03:00
co-authored by Claude Opus 4.8
374 changed files with 18484 additions and 3887 deletions
+19 -4
View File
@@ -10,6 +10,7 @@ import {
import { loadCurrentRelations } from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
import { loadProfileSnapshot } from '../services/user-profile-params.js';
import { parseDmTechBlocks } from '../services/dm-tech-blocks.js';
export const pageMeta = { id: 'messages-list', title: 'Личные сообщения' };
const dmAvatarSnapshotCache = new Map();
@@ -64,6 +65,12 @@ function createDmAvatar(login) {
return avatarEl;
}
function resolveLastMessagePreview(text = '') {
const parsed = parseDmTechBlocks(String(text || ''));
const display = String(parsed.displayText || '').trim();
return display || '';
}
function formatChatRowTime(ts) {
const value = Number(ts || 0);
if (!Number.isFinite(value) || value <= 0) return '';
@@ -88,12 +95,14 @@ export function render({ navigate }) {
<div class="dm-head-brand">
<div class="dm-head-hex">${(login[0] || 'A').toUpperCase()}</div>
<div class="dm-head-id">
<span class="dm-head-name">${login}</span>
<span class="dm-head-name"></span>
</div>
</div>
<h1 class="dm-head-title">Контакты</h1>
<button type="button" class="dm-head-plus" aria-label="Новый диалог">+</button>
`;
const headName = head.querySelector('.dm-head-name');
if (headName) headName.textContent = login;
head.querySelector('.dm-head-plus')?.addEventListener('click', () => navigate('contact-search-view'));
const divider = document.createElement('div');
@@ -113,19 +122,25 @@ export function render({ navigate }) {
row.innerHTML = `
<div class="dm-row-main">
<div class="dm-row-titleline dm-row-titlewrap">
<strong class="dm-row-title">${item.name}</strong>
<strong class="dm-row-title"></strong>
${item.notInContacts ? '<span class="dm-contact-note">не в контактах</span>' : ''}
</div>
<p class="dm-row-last-message">${item.lastMessage}</p>
<p class="dm-row-last-message"></p>
</div>
<div class="dm-row-meta-col">
${item.unread ? `<span class="dm-unread-badge">${item.unread > 99 ? '99+' : item.unread}</span>` : '<span class="dm-row-meta-spacer" aria-hidden="true"></span>'}
<div class="dm-row-meta-line">
${item.time ? `<span class="dm-row-time">${item.time}</span>` : '<span class="dm-row-time dm-row-time--empty"></span>'}
${item.time ? '<span class="dm-row-time"></span>' : '<span class="dm-row-time dm-row-time--empty"></span>'}
<span class="dm-chevron">${SVG_CHEVRON}</span>
</div>
</div>
`;
const titleEl = row.querySelector('.dm-row-title');
const previewEl = row.querySelector('.dm-row-last-message');
const timeEl = row.querySelector('.dm-row-time');
if (titleEl) titleEl.textContent = String(item.name || '');
if (previewEl) previewEl.textContent = resolveLastMessagePreview(item.lastMessage) || 'Диалог пока пуст.';
if (timeEl) timeEl.textContent = String(item.time || '');
row.prepend(avatarWrap);
row.addEventListener('click', () => navigate(`chat-view/${encodeURIComponent(normalizeDmChatId(item.id))}`));
return row;