SHA256
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:
+104
-5
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user