UI: доработать профиль и связи

This commit is contained in:
AidarKC
2026-09-01 15:24:39 +04:00
parent 390a2b988e
commit 632d56737b
8 changed files with 240 additions and 115 deletions
+1 -1
View File
@@ -1,2 +1,2 @@
client.version=1.10.1 client.version=1.10.2
server.version=1.8.1 server.version=1.8.1
@@ -0,0 +1,5 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" fill="none">
<circle cx="32" cy="32" r="27" stroke="currentColor" stroke-width="4" opacity="0.72"/>
<path d="M32 14l3.2 10.8L46 28l-10.8 3.2L32 42l-3.2-10.8L18 28l10.8-3.2L32 14z" stroke="currentColor" stroke-width="3" stroke-linejoin="round"/>
<path d="M13 51L51 13" stroke="currentColor" stroke-width="5" stroke-linecap="round"/>
</svg>

After

Width:  |  Height:  |  Size: 405 B

+107 -54
View File
@@ -5,10 +5,15 @@ import {
PROFILE_GENDER_FEMALE, PROFILE_GENDER_FEMALE,
PROFILE_GENDER_MALE, PROFILE_GENDER_MALE,
PROFILE_GENDER_UNKNOWN, PROFILE_GENDER_UNKNOWN,
PROFILE_ACCOUNT_ROLE_PRIMARY,
PROFILE_ACCOUNT_ROLE_NON_VOTING,
PROFILE_SHINE_SHINING,
PROFILE_SHINE_UNKNOWN,
PROFILE_SHINE_NOT_INTERESTED,
loadProfileSnapshot, loadProfileSnapshot,
saveProfileGender, saveProfileGender,
saveProfileParamBlock, saveProfileParamBlock,
saveProfileToggle, saveProfileStatus,
} from '../services/user-profile-params.js'; } from '../services/user-profile-params.js';
import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js'; import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js';
import { openAvatarWizard } from '../components/avatar-wizard.js'; import { openAvatarWizard } from '../components/avatar-wizard.js';
@@ -16,8 +21,17 @@ import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'profile-edit-view', title: 'Редактирование профиля' }; export const pageMeta = { id: 'profile-edit-view', title: 'Редактирование профиля' };
function toggleText(enabled) { function accountRoleLabel(value) {
return enabled ? 'Yes' : 'No'; if (value === PROFILE_ACCOUNT_ROLE_PRIMARY) return 'Основной аккаунт';
if (value === PROFILE_ACCOUNT_ROLE_NON_VOTING) return 'Не учитывать мой голос';
return 'Не указано';
}
function shineStatusLabel(value) {
if (value === PROFILE_SHINE_SHINING) return 'Сияющий';
if (value === PROFILE_SHINE_NOT_INTERESTED) return 'Сияние неинтересно';
if (value === PROFILE_SHINE_UNKNOWN) return 'Неизвестно';
return 'Не указано';
} }
function showLocalErrorAlert(prefix, error) { function showLocalErrorAlert(prefix, error) {
@@ -119,8 +133,8 @@ export function render({ navigate, chrome }) {
const badgesRow = document.createElement('div'); const badgesRow = document.createElement('div');
badgesRow.className = 'row'; badgesRow.className = 'row';
badgesRow.innerHTML = ` badgesRow.innerHTML = `
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="official">Официальный: No</button> <button class="badge profile-toggle-btn is-no" type="button" data-status="account_role">Аккаунт: Не указано</button>
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="shine">Сияющий: No</button> <button class="badge profile-toggle-btn is-no" type="button" data-status="shine">Сияние: Не указано</button>
`; `;
const status = document.createElement('div'); const status = document.createElement('div');
@@ -142,13 +156,14 @@ export function render({ navigate, chrome }) {
`; `;
const reloadBtn = topRow.querySelector('[data-reload="true"]'); const reloadBtn = topRow.querySelector('[data-reload="true"]');
const officialBtn = badgesRow.querySelector('[data-toggle="official"]'); const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]');
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]'); const shineBtn = badgesRow.querySelector('[data-status="shine"]');
const addRelativeBtn = relativesCard.querySelector('[data-add-relative="true"]'); const addRelativeBtn = relativesCard.querySelector('[data-add-relative="true"]');
const avatarActionEl = topRow.querySelector('[data-change-avatar="true"]'); const avatarActionEl = topRow.querySelector('[data-change-avatar="true"]');
let currentFields = []; let currentFields = [];
let currentToggles = []; let currentAccountRole = '';
let currentShineStatus = '';
let currentGender = PROFILE_GENDER_UNKNOWN; let currentGender = PROFILE_GENDER_UNKNOWN;
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
const identityEl = topRow.querySelector('[data-profile-identity="true"]'); const identityEl = topRow.querySelector('[data-profile-identity="true"]');
@@ -233,50 +248,76 @@ export function render({ navigate, chrome }) {
})); }));
} }
function updateToggleButton(button, prefix, enabled) { function updateStatusesUi() {
button.textContent = `${prefix}: ${toggleText(enabled)}`; if (accountRoleBtn) {
button.classList.remove('is-no', 'is-yes-official', 'is-yes-shine'); accountRoleBtn.textContent = `Аккаунт: ${accountRoleLabel(currentAccountRole)}`;
accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
if (!enabled) { accountRoleBtn.classList.add(currentAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY ? 'is-yes-official' : 'is-no');
button.classList.add('is-no');
return;
} }
if (shineBtn) {
if (prefix === 'Официальный') { shineBtn.textContent = `Сияние: ${shineStatusLabel(currentShineStatus)}`;
button.classList.add('is-yes-official'); shineBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
} else { if (currentShineStatus === PROFILE_SHINE_SHINING) shineBtn.classList.add('is-yes-shine');
button.classList.add('is-yes-shine'); else if (currentShineStatus === PROFILE_SHINE_NOT_INTERESTED) shineBtn.classList.add('is-not-interested');
else shineBtn.classList.add('is-no');
} }
} }
function updateTogglesUi() {
const official = currentToggles.find((item) => item.key === 'official') || { enabled: false };
const shine = currentToggles.find((item) => item.key === 'shine') || { enabled: false };
updateToggleButton(officialBtn, 'Официальный', official.enabled);
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
}
function updateGenderUi() { function updateGenderUi() {
const genderValueEl = listWrap.querySelector('[data-gender-value]'); const genderValueEl = listWrap.querySelector('[data-gender-value]');
if (!genderValueEl) return; if (!genderValueEl) return;
genderValueEl.textContent = genderLabel(currentGender); genderValueEl.textContent = genderLabel(currentGender);
} }
function openFieldEditModal({ label, value, placeholder = '' }) { function openStatusPickerModal({ title, value, options }) {
const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(null);
root.innerHTML = `
<div class="modal" id="profile-status-modal">
<div class="modal-card stack">
<h3 class="modal-title">${escapeHtml(title)}</h3>
<select class="input" id="profile-status-select">
${options.map((item) => `<option value="${escapeHtml(item.value)}" ${item.value === value ? 'selected' : ''}>${escapeHtml(item.label)}</option>`).join('')}
</select>
<div class="form-actions-grid">
<button class="secondary-btn" id="profile-status-cancel" type="button">Отмена</button>
<button class="primary-btn" id="profile-status-save" type="button">Сохранить</button>
</div>
</div>
</div>`;
return new Promise((resolve) => {
const modal = root.querySelector('#profile-status-modal');
const selectEl = root.querySelector('#profile-status-select');
const close = (next = null) => { root.innerHTML = ''; resolve(next); };
modal?.addEventListener('click', (event) => { if (event.target === modal) close(null); });
root.querySelector('#profile-status-cancel')?.addEventListener('click', () => close(null));
root.querySelector('#profile-status-save')?.addEventListener('click', () => close(selectEl?.value || null));
window.setTimeout(() => selectEl?.focus(), 0);
});
}
function openFieldEditModal({ label, value, placeholder = '', maxLength = 300, multiline = false }) {
const root = document.getElementById('modal-root'); const root = document.getElementById('modal-root');
if (!root) return Promise.resolve(null); if (!root) return Promise.resolve(null);
root.innerHTML = ` root.innerHTML = `
<div class="modal" id="profile-field-edit-modal"> <div class="modal" id="profile-field-edit-modal">
<div class="modal-card stack"> <div class="modal-card stack">
<h3 class="modal-title">Изменить: ${escapeHtml(label)}</h3> <h3 class="modal-title">Изменить: ${escapeHtml(label)}</h3>
<input ${multiline ? `<textarea
id="profile-field-edit-input"
class="input"
maxlength="${Number(maxLength || 300)}"
rows="${Number(maxLength || 300) > 1000 ? 12 : 4}"
placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}"
>${escapeHtml(String(value || ''))}</textarea>` : `<input
id="profile-field-edit-input" id="profile-field-edit-input"
class="input" class="input"
type="text" type="text"
maxlength="300" maxlength="${Number(maxLength || 300)}"
placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}" placeholder="${escapeHtml(placeholder || `Введите ${label.toLowerCase()}`)}"
value="${escapeHtml(String(value || ''))}" value="${escapeHtml(String(value || ''))}"
/> />`}
<div class="meta-muted">До ${Number(maxLength || 300)} символов.</div>
<div class="form-actions-grid"> <div class="form-actions-grid">
<button class="secondary-btn" id="profile-field-edit-cancel" type="button">Отмена</button> <button class="secondary-btn" id="profile-field-edit-cancel" type="button">Отмена</button>
<button class="primary-btn" id="profile-field-edit-save" type="button">Сохранить</button> <button class="primary-btn" id="profile-field-edit-save" type="button">Сохранить</button>
@@ -290,7 +331,7 @@ export function render({ navigate, chrome }) {
const inputEl = root.querySelector('#profile-field-edit-input'); const inputEl = root.querySelector('#profile-field-edit-input');
const saveEl = root.querySelector('#profile-field-edit-save'); const saveEl = root.querySelector('#profile-field-edit-save');
const cancelEl = root.querySelector('#profile-field-edit-cancel'); const cancelEl = root.querySelector('#profile-field-edit-cancel');
if (!(modal instanceof HTMLElement) || !(inputEl instanceof HTMLInputElement)) { if (!(modal instanceof HTMLElement) || (!(inputEl instanceof HTMLInputElement) && !(inputEl instanceof HTMLTextAreaElement))) {
root.innerHTML = ''; root.innerHTML = '';
resolve(null); resolve(null);
return; return;
@@ -307,7 +348,7 @@ export function render({ navigate, chrome }) {
cancelEl?.addEventListener('click', () => close(null)); cancelEl?.addEventListener('click', () => close(null));
saveEl?.addEventListener('click', () => close(inputEl.value)); saveEl?.addEventListener('click', () => close(inputEl.value));
inputEl.addEventListener('keydown', (event) => { inputEl.addEventListener('keydown', (event) => {
if (event.key === 'Enter') { if (!multiline && event.key === 'Enter') {
event.preventDefault(); event.preventDefault();
close(inputEl.value); close(inputEl.value);
} }
@@ -566,20 +607,21 @@ export function render({ navigate, chrome }) {
status.className = 'status-line'; status.className = 'status-line';
status.textContent = 'Загрузка параметров...'; status.textContent = 'Загрузка параметров...';
reloadBtn.disabled = true; reloadBtn.disabled = true;
officialBtn.disabled = true; accountRoleBtn.disabled = true;
shineBtn.disabled = true; shineBtn.disabled = true;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true; if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true;
try { try {
const snapshot = await loadProfileSnapshot(login); const snapshot = await loadProfileSnapshot(login);
currentFields = snapshot.fields; currentFields = snapshot.fields;
currentToggles = snapshot.toggles; currentAccountRole = snapshot.accountRole || '';
currentShineStatus = snapshot.shineStatus || '';
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN; currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
syncIdentity(); syncIdentity();
renderFields(currentFields); renderFields(currentFields);
updateTogglesUi(); updateStatusesUi();
updateGenderUi(); updateGenderUi();
updateAvatarUi(); updateAvatarUi();
@@ -591,7 +633,7 @@ export function render({ navigate, chrome }) {
showLocalErrorAlert('Ошибка загрузки параметров профиля', error); showLocalErrorAlert('Ошибка загрузки параметров профиля', error);
} finally { } finally {
reloadBtn.disabled = false; reloadBtn.disabled = false;
officialBtn.disabled = false; accountRoleBtn.disabled = false;
shineBtn.disabled = false; shineBtn.disabled = false;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false; if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false;
} }
@@ -628,27 +670,32 @@ export function render({ navigate, chrome }) {
} }
} }
async function onToggleClick(toggleKey) { async function onStatusClick(statusKey) {
const toggle = currentToggles.find((item) => item.key === toggleKey) || { enabled: false }; const isAccountRole = statusKey === 'account_role';
const nextEnabled = !toggle.enabled; const picked = await openStatusPickerModal({
const title = toggleKey === 'official' ? 'официальный' : 'сияющий'; title: isAccountRole ? 'Роль аккаунта' : 'Статус сияния',
value: isAccountRole ? currentAccountRole : currentShineStatus,
const confirmed = window.confirm( options: isAccountRole
`Хотите изменить «${title}» на ${toggleText(nextEnabled)}?\n` + ? [
'Будет создана запись в блокчейне.', { value: PROFILE_ACCOUNT_ROLE_PRIMARY, label: 'Основной аккаунт' },
); { value: PROFILE_ACCOUNT_ROLE_NON_VOTING, label: 'Не учитывать мой голос' },
if (!confirmed) return; ]
: [
{ value: PROFILE_SHINE_SHINING, label: 'Сияющий' },
{ value: PROFILE_SHINE_UNKNOWN, label: 'Неизвестно' },
{ value: PROFILE_SHINE_NOT_INTERESTED, label: 'Сияние мне неинтересно' },
],
});
if (!picked) return;
status.className = 'status-line'; status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...'; status.textContent = 'Сохранение в блокчейн...';
try { try {
await saveProfileToggle(login, toggleKey, nextEnabled); await saveProfileStatus(login, statusKey, picked);
await refreshProfileSnapshot(); await refreshProfileSnapshot();
} catch (error) { } catch (error) {
status.className = 'status-line is-unavailable'; status.className = 'status-line is-unavailable';
status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`; status.textContent = `Не удалось изменить ${statusKey}: ${error.message || 'ошибка сети'}`;
showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error); showLocalErrorAlert(`Ошибка изменения ${statusKey}`, error);
} }
} }
@@ -660,8 +707,14 @@ export function render({ navigate, chrome }) {
label: field.label, label: field.label,
value: field.value || '', value: field.value || '',
placeholder: field.placeholder || '', placeholder: field.placeholder || '',
maxLength: field.maxLength || 300,
multiline: Boolean(field.multiline),
}); });
if (entered === null) return; if (entered === null) return;
if (String(entered).length > Number(field.maxLength || 300)) {
window.alert(`Максимальная длина поля «${field.label}» — ${Number(field.maxLength || 300)} символов.`);
return;
}
status.className = 'status-line'; status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...'; status.textContent = 'Сохранение в блокчейн...';
@@ -786,8 +839,8 @@ export function render({ navigate, chrome }) {
}); });
reloadBtn.addEventListener('click', refreshProfileSnapshot); reloadBtn.addEventListener('click', refreshProfileSnapshot);
officialBtn.addEventListener('click', () => onToggleClick('official')); accountRoleBtn.addEventListener('click', () => onStatusClick('account_role'));
shineBtn.addEventListener('click', () => onToggleClick('shine')); shineBtn.addEventListener('click', () => onStatusClick('shine'));
addRelativeBtn?.addEventListener('click', onAddRelativeClick); addRelativeBtn?.addEventListener('click', onAddRelativeClick);
avatarActionEl?.addEventListener('click', () => { void onChangeAvatarClick(); }); avatarActionEl?.addEventListener('click', () => { void onChangeAvatarClick(); });
+27 -30
View File
@@ -81,7 +81,7 @@ function openProfileInfoModal({ title, text }) {
function officialInfoText() { function officialInfoText() {
return 'Можно создавать несколько альтернативных или анонимных каналов. ' return 'Можно создавать несколько альтернативных или анонимных каналов. '
+ 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.'; + 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
} }
function shineInfoText() { function shineInfoText() {
@@ -143,20 +143,21 @@ export function render({ navigate, chrome }) {
const badgesRow = document.createElement('div'); const badgesRow = document.createElement('div');
badgesRow.className = 'row'; badgesRow.className = 'row';
badgesRow.innerHTML = ` badgesRow.innerHTML = `
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="official">Официальный: No</button> <button class="badge profile-toggle-btn is-no" type="button" data-status="account_role">Аккаунт: Не указано</button>
<button class="badge profile-toggle-btn is-no" type="button" data-toggle="shine">Сияющий: No</button> <button class="badge profile-toggle-btn is-no" type="button" data-status="shine">Сияние: Не указано</button>
`; `;
const listWrap = document.createElement('div'); const listWrap = document.createElement('div');
listWrap.className = 'stack profile-param-list'; listWrap.className = 'stack profile-param-list';
const officialBtn = badgesRow.querySelector('[data-toggle="official"]'); const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]');
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]'); const shineBtn = badgesRow.querySelector('[data-status="shine"]');
const identityEl = topRow.querySelector('[data-profile-identity="true"]'); const identityEl = topRow.querySelector('[data-profile-identity="true"]');
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]'); const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
let currentFields = []; let currentFields = [];
let currentToggles = []; let currentAccountRole = '';
let currentShineStatus = '';
let currentGender = 'unknown'; let currentGender = 'unknown';
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
let currentStats = { let currentStats = {
@@ -193,22 +194,19 @@ export function render({ navigate, chrome }) {
})); }));
} }
function updateToggleButton(button, prefix, enabled) { function updateStatusesUi() {
button.textContent = `${prefix}: ${toggleText(enabled)}`; if (accountRoleBtn) {
button.classList.remove('is-no', 'is-yes-official', 'is-yes-shine'); const label = currentAccountRole === 'primary' ? 'Основной аккаунт' : currentAccountRole === 'non_voting' ? 'Не учитывать мой голос' : 'Не указано';
if (!enabled) { accountRoleBtn.textContent = `Аккаунт: ${label}`;
button.classList.add('is-no'); accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
return; accountRoleBtn.classList.add(currentAccountRole === 'primary' ? 'is-yes-official' : 'is-no');
} }
if (prefix === 'Официальный') button.classList.add('is-yes-official'); if (shineBtn) {
else button.classList.add('is-yes-shine'); const label = currentShineStatus === 'shining' ? 'Сияющий' : currentShineStatus === 'not_interested' ? 'Сияние неинтересно' : currentShineStatus === 'unknown' ? 'Неизвестно' : 'Не указано';
shineBtn.textContent = `Сияние: ${label}`;
shineBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
shineBtn.classList.add(currentShineStatus === 'shining' ? 'is-yes-shine' : currentShineStatus === 'not_interested' ? 'is-not-interested' : 'is-no');
} }
function updateTogglesUi() {
const official = currentToggles.find((item) => item.key === 'official') || { enabled: false };
const shine = currentToggles.find((item) => item.key === 'shine') || { enabled: false };
updateToggleButton(officialBtn, 'Официальный', official.enabled);
updateToggleButton(shineBtn, 'Сияющий', shine.enabled);
} }
function renderStats() { function renderStats() {
@@ -226,11 +224,11 @@ export function render({ navigate, chrome }) {
}); });
} }
officialBtn?.classList.add('profile-badge-trigger'); accountRoleBtn?.classList.add('profile-badge-trigger');
shineBtn?.classList.add('profile-badge-trigger'); shineBtn?.classList.add('profile-badge-trigger');
officialBtn?.addEventListener('click', () => { accountRoleBtn?.addEventListener('click', () => {
openProfileInfoModal({ openProfileInfoModal({
title: 'Официальный канал', title: 'Основной аккаунт',
text: officialInfoText(), text: officialInfoText(),
}); });
}); });
@@ -269,10 +267,8 @@ export function render({ navigate, chrome }) {
{ key: 'website', label: 'Веб', value: '127.0.0.1' }, { key: 'website', label: 'Веб', value: '127.0.0.1' },
{ key: 'phone', label: 'Телефон', value: profile.phone }, { key: 'phone', label: 'Телефон', value: profile.phone },
]; ];
currentToggles = [ currentAccountRole = '';
{ key: 'official', enabled: false }, currentShineStatus = '';
{ key: 'shine', enabled: false },
];
currentGender = 'unknown'; currentGender = 'unknown';
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
currentStats = { currentStats = {
@@ -283,7 +279,7 @@ export function render({ navigate, chrome }) {
}; };
syncIdentity(); syncIdentity();
updateAvatarUi(); updateAvatarUi();
updateTogglesUi(); updateStatusesUi();
renderFields(currentFields); renderFields(currentFields);
return; return;
} }
@@ -294,7 +290,8 @@ export function render({ navigate, chrome }) {
authService.getUser(login).catch(() => ({})), authService.getUser(login).catch(() => ({})),
]); ]);
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : []; currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : []; currentAccountRole = snapshot.accountRole || '';
currentShineStatus = snapshot.shineStatus || '';
currentGender = snapshot.gender || 'unknown'; currentGender = snapshot.gender || 'unknown';
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 }; currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
currentStats = { currentStats = {
@@ -305,7 +302,7 @@ export function render({ navigate, chrome }) {
}; };
syncIdentity(); syncIdentity();
updateAvatarUi(); updateAvatarUi();
updateTogglesUi(); updateStatusesUi();
renderFields(currentFields); renderFields(currentFields);
} catch (error) { } catch (error) {
// ignore status row in profile-view // ignore status row in profile-view
+11 -4
View File
@@ -42,7 +42,7 @@ function openProfileInfoModal({ title, text }) {
function officialInfoText() { function officialInfoText() {
return 'Можно создавать несколько альтернативных или анонимных каналов. ' return 'Можно создавать несколько альтернативных или анонимных каналов. '
+ 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.'; + 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
} }
function shineInfoText() { function shineInfoText() {
@@ -177,10 +177,15 @@ function renderIdentity(card) {
} }
function renderReadOnlyBadges(card) { function renderReadOnlyBadges(card) {
const accountRole = String(card.accountRole || '').trim().toLowerCase();
const shineStatus = String(card.shineStatus || '').trim().toLowerCase();
const accountLabel = accountRole === 'primary' ? 'Основной аккаунт' : accountRole === 'non_voting' ? 'Голос не учитывать' : '';
const shineLabel = shineStatus === 'shining' ? 'Сияющий' : '';
return ` return `
<div class="row wrap-row"> <div class="row wrap-row">
<button class="badge profile-badge-trigger ${card.official ? 'is-yes-official' : 'is-no'}" type="button" data-profile-info="official">Официальный: ${card.official ? 'Yes' : 'No'}</button> ${accountLabel ? `<button class="badge profile-badge-trigger ${accountRole === 'primary' ? 'is-yes-official' : 'is-no'}" type="button" data-profile-info="official">${escapeHtml(accountLabel)}</button>` : ''}
<button class="badge profile-badge-trigger ${card.shine ? 'is-yes-shine' : 'is-no'}" type="button" data-profile-info="shine">Сияющий: ${card.shine ? 'Yes' : 'No'}</button> ${shineLabel ? `<button class="badge profile-badge-trigger is-yes-shine" type="button" data-profile-info="shine">${escapeHtml(shineLabel)}</button>` : ''}
${shineStatus === 'not_interested' ? `<span class="profile-shine-not-interested" title="Сияние неинтересно"><img src="/assets/shine-status-not-interested.svg" alt="Сияние неинтересно" /></span>` : ''}
</div> </div>
`; `;
} }
@@ -193,6 +198,8 @@ function renderReadOnlyParams(card) {
{ label: 'Адрес', value: card.address }, { label: 'Адрес', value: card.address },
{ label: 'Web', value: card.web }, { label: 'Web', value: card.web },
{ label: 'Телефон', value: card.phone }, { label: 'Телефон', value: card.phone },
{ label: 'О себе', value: card.about },
{ label: 'Духовный путь', value: card.spiritualPath },
]; ];
return ` return `
@@ -341,7 +348,7 @@ export function render({ navigate, route }) {
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || ''); const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
if (infoKind === 'official') { if (infoKind === 'official') {
openProfileInfoModal({ openProfileInfoModal({
title: 'Официальный канал', title: 'Основной аккаунт',
text: officialInfoText(), text: officialInfoText(),
}); });
return; return;
+7 -2
View File
@@ -254,9 +254,14 @@ export async function loadUserProfileCard(login) {
address: fields.address || '', address: fields.address || '',
web: fields.web || '', web: fields.web || '',
phone: fields.phone || '', phone: fields.phone || '',
about: fields.about || '',
spiritualPath: fields.spiritual_path || '',
accountRole: String(snapshot?.accountRole || '').trim().toLowerCase(),
shineStatus: String(snapshot?.shineStatus || '').trim().toLowerCase(),
gender: String(snapshot?.gender || 'unknown').trim().toLowerCase() || 'unknown', gender: String(snapshot?.gender || 'unknown').trim().toLowerCase() || 'unknown',
official: Boolean(toggles.official), // Compatibility aliases for older graph/card consumers; values come only from the new profile schema.
shine: Boolean(toggles.shine), official: String(snapshot?.accountRole || '').trim().toLowerCase() === 'primary',
shine: String(snapshot?.shineStatus || '').trim().toLowerCase() === 'shining',
avatar: snapshot?.avatar?.txId avatar: snapshot?.avatar?.txId
? { ? {
ar: String(snapshot.avatar.txId).trim(), ar: String(snapshot.avatar.txId).trim(),
+50 -23
View File
@@ -12,12 +12,17 @@ export const profileFieldDefs = [
{ key: 'address', readKeys: ['address'], label: 'Адрес', placeholder: 'Город, улица, дом' }, { key: 'address', readKeys: ['address'], label: 'Адрес', placeholder: 'Город, улица, дом' },
{ key: 'web', readKeys: ['web'], label: 'Веб', placeholder: 'Сайт или профиль' }, { key: 'web', readKeys: ['web'], label: 'Веб', placeholder: 'Сайт или профиль' },
{ key: 'phone', readKeys: ['phone'], label: 'Телефон', placeholder: '+7 ...' }, { key: 'phone', readKeys: ['phone'], label: 'Телефон', placeholder: '+7 ...' },
{ key: 'about', readKeys: ['about'], label: 'О себе', placeholder: 'Коротко расскажите о себе', maxLength: 160, multiline: true },
{ key: 'spiritual_path', readKeys: ['spiritual_path'], label: 'Духовный путь', placeholder: 'Расскажите о своём духовном пути, опыте, практиках и взглядах', maxLength: 5000, multiline: true },
]; ];
export const profileToggleDefs = [ export const profileToggleDefs = []; // legacy boolean toggles are no longer used by the new UI.
{ key: 'official', label: 'Официальный' },
{ key: 'shine', label: 'Сияющий' }, export const PROFILE_ACCOUNT_ROLE_PRIMARY = 'primary';
]; export const PROFILE_ACCOUNT_ROLE_NON_VOTING = 'non_voting';
export const PROFILE_SHINE_SHINING = 'shining';
export const PROFILE_SHINE_UNKNOWN = 'unknown';
export const PROFILE_SHINE_NOT_INTERESTED = 'not_interested';
export const PROFILE_GENDER_MALE = 'male'; export const PROFILE_GENDER_MALE = 'male';
export const PROFILE_GENDER_FEMALE = 'female'; export const PROFILE_GENDER_FEMALE = 'female';
@@ -104,23 +109,31 @@ export async function loadProfileSnapshot(login) {
key: field.key, key: field.key,
label: field.label, label: field.label,
placeholder: field.placeholder, placeholder: field.placeholder,
maxLength: Number(field.maxLength || 300),
multiline: Boolean(field.multiline),
value: latest?.value || '', value: latest?.value || '',
timeMs: latest?.timeMs || 0, timeMs: latest?.timeMs || 0,
}); });
} }
const toggles = []; const latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
for (let i = 0; i < profileToggleDefs.length; i += 1) { const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
const toggle = profileToggleDefs[i]; const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
const latest = loadLatestByAliasesFromItems(items, [toggle.key]); ? rawAccountRole
toggles.push({ : '';
key: toggle.key,
label: toggle.label, const latestShine = loadLatestByAliasesFromItems(items, ['shine']);
enabled: latest ? parseToggleValue(latest.value) : false, const rawShine = String(latestShine?.value || '').trim().toLowerCase();
rawValue: latest?.value || 'no', let shineStatus = '';
timeMs: latest?.timeMs || 0, if (rawShine === PROFILE_SHINE_SHINING || rawShine === 'yes') shineStatus = PROFILE_SHINE_SHINING;
}); else if (rawShine === PROFILE_SHINE_UNKNOWN) shineStatus = PROFILE_SHINE_UNKNOWN;
} else if (rawShine === PROFILE_SHINE_NOT_INTERESTED) shineStatus = PROFILE_SHINE_NOT_INTERESTED;
// Legacy shine=no is intentionally ignored. Legacy official=yes/no is not read at all.
const toggles = [
{ key: 'account_role', label: 'Роль аккаунта', enabled: accountRole === PROFILE_ACCOUNT_ROLE_PRIMARY, rawValue: accountRole, timeMs: latestAccountRole?.timeMs || 0 },
{ key: 'shine', label: 'Сияние', enabled: shineStatus === PROFILE_SHINE_SHINING, rawValue: shineStatus, timeMs: latestShine?.timeMs || 0 },
];
const latestGender = loadLatestByAliasesFromItems(items, ['gender']); const latestGender = loadLatestByAliasesFromItems(items, ['gender']);
const gender = normalizeGenderValue(latestGender?.value || PROFILE_GENDER_UNKNOWN); const gender = normalizeGenderValue(latestGender?.value || PROFILE_GENDER_UNKNOWN);
@@ -145,6 +158,10 @@ export async function loadProfileSnapshot(login) {
return { return {
fields, fields,
toggles, toggles,
accountRole,
accountRoleTimeMs: latestAccountRole?.timeMs || 0,
shineStatus,
shineTimeMs: latestShine?.timeMs || 0,
gender, gender,
genderTimeMs: latestGender?.timeMs || 0, genderTimeMs: latestGender?.timeMs || 0,
avatar, avatar,
@@ -161,14 +178,24 @@ export async function saveProfileParamBlock(login, key, value) {
}); });
} }
export async function saveProfileToggle(login, key, enabled) { export async function saveProfileStatus(login, key, value) {
const cleanKey = String(key || '').trim();
const cleanValue = String(value || '').trim().toLowerCase();
if (cleanKey === 'account_role' && ![PROFILE_ACCOUNT_ROLE_PRIMARY, PROFILE_ACCOUNT_ROLE_NON_VOTING].includes(cleanValue)) {
throw new Error('Некорректное значение account_role');
}
if (cleanKey === 'shine' && ![PROFILE_SHINE_SHINING, PROFILE_SHINE_UNKNOWN, PROFILE_SHINE_NOT_INTERESTED].includes(cleanValue)) {
throw new Error('Некорректное значение shine');
}
const storagePwd = await getStoragePwd(); const storagePwd = await getStoragePwd();
await authService.addBlockUserParam({ await authService.addBlockUserParam({ login, param: cleanKey, value: cleanValue, storagePwd });
login, }
param: key,
value: enabled ? 'yes' : 'no', // Kept only for old callers outside the current UI. New code must use saveProfileStatus().
storagePwd, export async function saveProfileToggle(login, key, enabled) {
}); if (key === 'shine') return saveProfileStatus(login, 'shine', enabled ? PROFILE_SHINE_SHINING : PROFILE_SHINE_UNKNOWN);
if (key === 'official') throw new Error('Параметр official устарел. Используйте account_role.');
throw new Error(`Неизвестный legacy toggle: ${key}`);
} }
export async function saveProfileGender(login, gender) { export async function saveProfileGender(login, gender) {
+31
View File
@@ -10568,3 +10568,34 @@ body.chat-topbar-overlay .composer-slot {
line-height: 1.1 !important; line-height: 1.1 !important;
white-space: nowrap !important; white-space: nowrap !important;
} }
.badge.profile-toggle-btn.is-not-interested {
border-color: rgba(170, 180, 205, 0.38);
color: #d7deea;
background: rgba(116, 126, 148, 0.18);
}
.profile-shine-not-interested {
display: inline-flex;
align-items: center;
justify-content: center;
width: 32px;
height: 32px;
border-radius: 50%;
border: 1px solid rgba(170, 180, 205, 0.34);
background: rgba(116, 126, 148, 0.14);
color: #d7deea;
font-size: 20px;
line-height: 1;
}
.profile-param-value {
white-space: pre-wrap;
overflow-wrap: anywhere;
}
.profile-shine-not-interested img {
width: 22px;
height: 22px;
display: block;
}