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
+104 -5
View File
@@ -97,6 +97,21 @@ const DEFAULT_ARWEAVE_SERVER = 'https://arweave.net';
const DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS = 6000;
const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
function normalizeStoredSolanaServer(value) {
const raw = String(value || '').trim();
if (!raw) return DEFAULT_SOLANA_SERVER;
const normalized = raw.replace(/\/+$/u, '').toLowerCase();
if (
normalized === 'https://api.devnet.solana.com'
|| normalized === 'http://api.devnet.solana.com'
|| normalized === 'https://api.mainnet-beta.solana.com'
|| normalized === 'http://api.mainnet-beta.solana.com'
) {
return DEFAULT_SOLANA_SERVER;
}
return raw;
}
export function normalizeDmChatId(value) {
return String(value || '').trim().toLowerCase();
}
@@ -187,12 +202,13 @@ function persistEntrySettings(settings) {
try {
const payload = {
language: String(settings?.language || 'ru'),
solanaServer: String(settings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(settings?.solanaServer),
shineServer: String(settings?.shineServer || DEFAULT_SHINE_SERVER),
shineServerLogin: String(settings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(settings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
arweaveServer: String(settings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(settings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
remoteAddBlockSessionId: String(settings?.remoteAddBlockSessionId || ''),
statuses: {
solanaServer: String(settings?.statuses?.solanaServer || 'idle'),
shineServerLogin: String(settings?.statuses?.shineServerLogin || settings?.statuses?.shineServer || 'idle'),
@@ -242,6 +258,8 @@ function createInitialState({ withStoredSession = true } = {}) {
appLog: [],
incomingDedup: {},
knownMessageKeys: {},
pendingOutgoingReadByBaseKey: {},
pendingIncomingReadByBaseKey: {},
outgoingTempSeq: 1,
notificationsTab: 'replies',
pageLabelCollapsed: false,
@@ -255,12 +273,13 @@ function createInitialState({ withStoredSession = true } = {}) {
startHint: '',
entrySettings: {
language: String(storedEntrySettings?.language || 'ru'),
solanaServer: String(storedEntrySettings?.solanaServer || DEFAULT_SOLANA_SERVER),
solanaServer: normalizeStoredSolanaServer(storedEntrySettings?.solanaServer),
shineServer: String(LOCAL_WS_OVERRIDE_URL || storedEntrySettings?.shineServer || initialShineServer),
shineServerLogin: String(storedEntrySettings?.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE),
shineServerHttp: String(storedEntrySettings?.shineServerHttp || DEFAULT_SHINE_SERVER_HTTP_VALUE),
arweaveServer: String(storedEntrySettings?.arweaveServer || DEFAULT_ARWEAVE_SERVER),
callPreflightTimeoutMs: Math.max(1000, Math.min(20000, Number(storedEntrySettings?.callPreflightTimeoutMs || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS) || DEFAULT_CALL_PREFLIGHT_TIMEOUT_MS)),
remoteAddBlockSessionId: String(storedEntrySettings?.remoteAddBlockSessionId || ''),
statuses: {
solanaServer: String(storedEntrySettings?.statuses?.solanaServer || 'idle'),
shineServerLogin: String(storedEntrySettings?.statuses?.shineServerLogin || storedEntrySettings?.statuses?.shineServer || 'idle'),
@@ -274,6 +293,9 @@ function createInitialState({ withStoredSession = true } = {}) {
password: '',
passwordMode: 'single',
passwordWords: emptyPasswordWords(),
passwordWordsLinked: false,
usePromoCode: false,
promoCode: '',
sessionId: '',
storagePwd: '',
pendingKeyBundle: null,
@@ -297,7 +319,7 @@ function createInitialState({ withStoredSession = true } = {}) {
rootKey: 'Ключ root хранится в зашифрованном виде',
blockchainKey: 'Ключ blockchain хранится в зашифрованном виде',
clientKey: 'Ключ device хранится в зашифрованном виде',
saveRoot: false,
saveRoot: true,
saveBlockchain: true,
saveDevice: true,
},
@@ -323,6 +345,11 @@ function createInitialState({ withStoredSession = true } = {}) {
export const state = createInitialState();
export const authService = new AuthService(state.entrySettings.shineServer);
authService.setRemoteAddBlockSessionId(state.entrySettings.remoteAddBlockSessionId);
authService.setActiveSessionContext({
login: state.session.login,
sessionId: state.session.sessionId,
});
let onSessionReset = null;
let onSessionAuthorized = null;
@@ -541,32 +568,48 @@ export function markOutgoingSent(tempId, { messageKey = '', baseKey = '' } = {})
export function markOutgoingReadByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
let matched = false;
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'out') return;
if (row.baseKey === baseKey) {
matched = true;
row.secondTick = true;
persistMessageRecord(chatId, row);
}
});
});
if (matched) {
delete state.pendingOutgoingReadByBaseKey[baseKey];
} else {
state.pendingOutgoingReadByBaseKey[baseKey] = true;
}
return matched;
}
export function markIncomingReadByBaseKey(baseKey) {
if (!baseKey) return;
const keys = Object.keys(state.chats || {});
let matched = false;
keys.forEach((chatId) => {
const list = getChatMessages(chatId);
list.forEach((row) => {
if (row?.from !== 'in') return;
if (row.baseKey === baseKey) {
matched = true;
row.unread = false;
row.readReceiptSent = true;
persistMessageRecord(chatId, row);
}
});
});
if (matched) {
delete state.pendingIncomingReadByBaseKey[baseKey];
} else {
state.pendingIncomingReadByBaseKey[baseKey] = true;
}
return matched;
}
export function markReadReceiptSentByBaseKey(baseKey) {
@@ -634,6 +677,15 @@ export function addSignedMessageToChat({
row.firstTick = row.from === 'out';
row.secondTick = Boolean(existing?.secondTick);
row.readReceiptSent = Boolean(existing?.readReceiptSent);
if (row.baseKey && row.from === 'out' && state.pendingOutgoingReadByBaseKey[row.baseKey]) {
row.secondTick = true;
delete state.pendingOutgoingReadByBaseKey[row.baseKey];
}
if (row.baseKey && row.from === 'in' && state.pendingIncomingReadByBaseKey[row.baseKey]) {
row.unread = false;
row.readReceiptSent = true;
delete state.pendingIncomingReadByBaseKey[row.baseKey];
}
if (existingIndex < 0) {
list.push(row);
@@ -643,15 +695,57 @@ export function addSignedMessageToChat({
return true;
}
export function deleteSignedMessageByBaseKey(chatId, baseKey) {
const normalizedChatId = normalizeDmChatId(chatId);
const normalizedBaseKey = String(baseKey || '').trim();
if (!normalizedChatId || !normalizedBaseKey) return false;
const list = getChatMessages(normalizedChatId);
let changed = false;
for (let i = list.length - 1; i >= 0; i -= 1) {
const row = list[i];
if (String(row?.baseKey || '').trim() !== normalizedBaseKey) continue;
changed = true;
removeStoredMessageRecord(String(row?.messageKey || '').trim());
list.splice(i, 1);
}
if (changed) {
sortChatMessagesInPlace(normalizedChatId);
}
return changed;
}
export function deleteConversationMessagesBefore(chatId, boundaryTimeMs) {
const normalizedChatId = normalizeDmChatId(chatId);
const boundary = Number(boundaryTimeMs || 0);
if (!normalizedChatId || !Number.isFinite(boundary) || boundary <= 0) return 0;
const list = getChatMessages(normalizedChatId);
let removed = 0;
for (let i = list.length - 1; i >= 0; i -= 1) {
const row = list[i];
const rowTime = resolveChatMessageTimeMs(row);
if (!Number.isFinite(rowTime) || rowTime >= boundary) continue;
removed += 1;
removeStoredMessageRecord(String(row?.messageKey || '').trim());
list.splice(i, 1);
}
if (removed > 0) {
sortChatMessagesInPlace(normalizedChatId);
}
return removed;
}
export function markChatRead(chatId) {
const normalizedChatId = normalizeDmChatId(chatId);
const list = getChatMessages(normalizedChatId);
let changed = 0;
list.forEach((row) => {
if (row?.from === 'in') {
if (row?.from === 'in' && row?.unread) {
row.unread = false;
persistMessageRecord(normalizedChatId, row);
changed += 1;
}
});
return changed;
}
export function setContacts(list) {
@@ -716,7 +810,7 @@ export function checkServerAvailability(address) {
}
export async function saveEntrySettings(nextSettings) {
const nextSolanaServer = String(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextSolanaServer = normalizeStoredSolanaServer(nextSettings?.solanaServer || state.entrySettings.solanaServer || DEFAULT_SOLANA_SERVER);
const nextShineServerLogin = String(nextSettings?.shineServerLogin || state.entrySettings.shineServerLogin || DEFAULT_SHINE_SERVER_LOGIN_VALUE).trim().toLowerCase()
|| DEFAULT_SHINE_SERVER_LOGIN_VALUE;
let forcedShineServer = LOCAL_WS_OVERRIDE_URL || '';
@@ -745,6 +839,7 @@ export async function saveEntrySettings(nextSettings) {
tools: normalizeToolsSettings(nextSettings.tools || state.entrySettings.tools),
};
persistEntrySettings(state.entrySettings);
authService.setRemoteAddBlockSessionId(state.entrySettings.remoteAddBlockSessionId);
await authService.reconnect(state.entrySettings.shineServer);
state.startHint = `Настройки входа сохранены. SHiNE: ${state.entrySettings.shineServerHttp}`;
}
@@ -788,6 +883,7 @@ export function authorizeSession({
login,
sessionId,
});
authService.setActiveSessionContext({ login, sessionId });
state.startHint = '';
if (onSessionAuthorized) {
onSessionAuthorized();
@@ -833,6 +929,8 @@ function resetStateForSignedOut() {
state.appLog = next.appLog;
state.incomingDedup = next.incomingDedup;
state.knownMessageKeys = next.knownMessageKeys;
state.pendingOutgoingReadByBaseKey = next.pendingOutgoingReadByBaseKey;
state.pendingIncomingReadByBaseKey = next.pendingIncomingReadByBaseKey;
state.outgoingTempSeq = next.outgoingTempSeq;
state.notificationsTab = next.notificationsTab;
state.pageLabelCollapsed = next.pageLabelCollapsed;
@@ -877,6 +975,7 @@ export async function terminateCurrentSession({ infoMessage = '', closeServerSes
resetStateForSignedOut();
await clearStoredMessages().catch(() => {});
authService.close();
authService.clearActiveSessionContext();
if (infoMessage) {
state.startHint = infoMessage;
}