Добавить user_settings и синхронизацию настроек

This commit is contained in:
AidarKC
2026-08-19 19:45:09 +04:00
parent fac166f186
commit 6a5c20a165
37 changed files with 2079 additions and 3 deletions
+30
View File
@@ -234,6 +234,12 @@ function buildThreadRoute(messageRef, selector) {
});
}
function buildChannelSettingsKey(selector, channelName) {
const ownerBch = String(selector?.ownerBlockchainName || '').trim();
const name = String(channelName || '').trim();
return `${ownerBch}/${name}`;
}
function firstNonEmptyText(...candidates) {
for (const candidate of candidates) {
if (typeof candidate !== 'string') continue;
@@ -1374,6 +1380,8 @@ async function loadFromApi(route, channelId) {
const messages = Array.isArray(payload.messages) ? payload.messages : [];
let reverseChannelMissingWarning = '';
let mergedMessages = [...messages];
const unreadCount = Number(channel?.unreadCount || 0);
const messagesCount = Number(channel?.messagesCount || mergedMessages.length || 0);
const currentLogin = currentSessionLogin;
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
@@ -1445,6 +1453,8 @@ async function loadFromApi(route, channelId) {
posts,
metaEvents: Array.isArray(payload?.metaEvents) ? payload.metaEvents : [],
reverseChannelMissingWarning,
unreadCount,
messagesCount,
isOwnChannel,
isSubscribed,
selector,
@@ -1905,6 +1915,13 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
screen.append(reverseWarning);
}
if (Number(channelData.unreadCount || 0) > 0) {
const unreadLine = document.createElement('div');
unreadLine.className = 'card channel-unread-line';
unreadLine.textContent = `Не прочитано: ${channelData.unreadCount}`;
screen.append(unreadLine);
}
const actionButton = document.createElement('button');
actionButton.className = 'destructive-btn channel-main-action';
actionButton.textContent = 'Подписаться на канал';
@@ -2294,6 +2311,19 @@ export function render({ navigate, route, chrome }) {
try {
const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null;
const lastSeenCount = Number(apiData?.messagesCount || (Array.isArray(apiData?.posts) ? apiData.posts.length : 0) || 0);
const settingKey = buildChannelSettingsKey(apiData?.selector, apiData?.channel?.name);
if (settingKey && state.session.login && state.session.storagePwdInMemory) {
void authService.upsertUserSetting({
login: state.session.login,
settingType: 1,
settingKey,
timeMs: Date.now(),
valueText: '',
valueNum: lastSeenCount,
storagePwd: state.session.storagePwdInMemory,
}).catch(() => {});
}
const titleLabel = apiData?.channel?.displayTitle || apiData?.channel?.name || 'channel';
const entrypointPosts = getEntrypointPosts(apiData?.posts);
const openEntrypointHistory = () => {
+57
View File
@@ -153,6 +153,10 @@ function makeClientPlatform() {
return 'Web';
}
function escapeUserSettingPart(value = '') {
return String(value ?? '').replace(/\\/g, '\\\\').replace(/\|/g, '\\|');
}
function hexToBytes(hex) {
const clean = String(hex || '').trim().toLowerCase();
if (!clean || clean.length % 2 !== 0) throw new Error('Некорректный hex');
@@ -2898,6 +2902,59 @@ export class AuthService {
return response.payload || {};
}
async upsertUserSetting({
login,
settingType,
settingKey,
timeMs,
valueText = '',
valueNum = 0,
storagePwd,
syncDelivery = false,
}) {
const cleanLogin = String(login || '').trim();
const cleanSettingKey = String(settingKey || '').trim();
const cleanValueText = String(valueText ?? '');
const cleanTimeMs = Number(timeMs);
const cleanSettingType = Number(settingType);
const cleanValueNum = Number(valueNum ?? 0);
if (!cleanLogin || !cleanSettingKey) throw new Error('Не переданы login/settingKey');
if (!Number.isFinite(cleanTimeMs) || cleanTimeMs <= 0) throw new Error('Не передан корректный timeMs');
if (!Number.isFinite(cleanSettingType)) throw new Error('Не передан корректный settingType');
if (!storagePwd) throw new Error('Не передан storagePwd для подписи UpsertUserSetting.');
const secrets = await loadEncryptedUserSecrets(cleanLogin, storagePwd);
const clientPrivPkcs8 = String(secrets?.clientKey || '').trim();
if (!clientPrivPkcs8) throw new Error('Не найден приватный clientKey');
const privateKey = await importPkcs8Ed25519(clientPrivPkcs8);
const clientKey = await publicKeyB64FromPkcs8Ed25519(clientPrivPkcs8);
const preimage = [
'SHiNe/UserSettings:',
escapeUserSettingPart(cleanLogin),
String(cleanSettingType),
escapeUserSettingPart(cleanSettingKey),
String(Math.trunc(cleanTimeMs)),
escapeUserSettingPart(cleanValueText),
String(Math.trunc(cleanValueNum)),
].join('|');
const signature = await signBase64(privateKey, preimage);
const response = await this.ws.request('UpsertUserSetting', {
login: cleanLogin,
setting_type: Math.trunc(cleanSettingType),
setting_key: cleanSettingKey,
time_ms: Math.trunc(cleanTimeMs),
value_text: cleanValueText,
value_num: Math.trunc(cleanValueNum),
client_key: clientKey,
signature,
sync_delivery: !!syncDelivery,
});
if (response.status !== 200) throw opError('UpsertUserSetting', response);
return response.payload || {};
}
async getTestFreeAvatarQuota() {
const response = await this.ws.request('TestGetFreeAvatarQuota', {});
if (response.status !== 200) throw opError('TestGetFreeAvatarQuota', response);