До ${Number(maxLength || 300)} символов.
@@ -290,7 +331,7 @@ export function render({ navigate, chrome }) {
const inputEl = root.querySelector('#profile-field-edit-input');
const saveEl = root.querySelector('#profile-field-edit-save');
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 = '';
resolve(null);
return;
@@ -307,7 +348,7 @@ export function render({ navigate, chrome }) {
cancelEl?.addEventListener('click', () => close(null));
saveEl?.addEventListener('click', () => close(inputEl.value));
inputEl.addEventListener('keydown', (event) => {
- if (event.key === 'Enter') {
+ if (!multiline && event.key === 'Enter') {
event.preventDefault();
close(inputEl.value);
}
@@ -566,20 +607,21 @@ export function render({ navigate, chrome }) {
status.className = 'status-line';
status.textContent = 'Загрузка параметров...';
reloadBtn.disabled = true;
- officialBtn.disabled = true;
+ accountRoleBtn.disabled = true;
shineBtn.disabled = true;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true;
try {
const snapshot = await loadProfileSnapshot(login);
currentFields = snapshot.fields;
- currentToggles = snapshot.toggles;
+ currentAccountRole = snapshot.accountRole || '';
+ currentShineStatus = snapshot.shineStatus || '';
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
syncIdentity();
renderFields(currentFields);
- updateTogglesUi();
+ updateStatusesUi();
updateGenderUi();
updateAvatarUi();
@@ -591,7 +633,7 @@ export function render({ navigate, chrome }) {
showLocalErrorAlert('Ошибка загрузки параметров профиля', error);
} finally {
reloadBtn.disabled = false;
- officialBtn.disabled = false;
+ accountRoleBtn.disabled = false;
shineBtn.disabled = false;
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false;
}
@@ -628,27 +670,32 @@ export function render({ navigate, chrome }) {
}
}
- async function onToggleClick(toggleKey) {
- const toggle = currentToggles.find((item) => item.key === toggleKey) || { enabled: false };
- const nextEnabled = !toggle.enabled;
- const title = toggleKey === 'official' ? 'официальный' : 'сияющий';
-
- const confirmed = window.confirm(
- `Хотите изменить «${title}» на ${toggleText(nextEnabled)}?\n` +
- 'Будет создана запись в блокчейне.',
- );
- if (!confirmed) return;
-
+ async function onStatusClick(statusKey) {
+ const isAccountRole = statusKey === 'account_role';
+ const picked = await openStatusPickerModal({
+ title: isAccountRole ? 'Роль аккаунта' : 'Статус сияния',
+ value: isAccountRole ? currentAccountRole : currentShineStatus,
+ options: isAccountRole
+ ? [
+ { value: PROFILE_ACCOUNT_ROLE_PRIMARY, label: 'Основной аккаунт' },
+ { value: PROFILE_ACCOUNT_ROLE_NON_VOTING, label: 'Не учитывать мой голос' },
+ ]
+ : [
+ { value: PROFILE_SHINE_SHINING, label: 'Сияющий' },
+ { value: PROFILE_SHINE_UNKNOWN, label: 'Неизвестно' },
+ { value: PROFILE_SHINE_NOT_INTERESTED, label: 'Сияние мне неинтересно' },
+ ],
+ });
+ if (!picked) return;
status.className = 'status-line';
status.textContent = 'Сохранение в блокчейн...';
-
try {
- await saveProfileToggle(login, toggleKey, nextEnabled);
+ await saveProfileStatus(login, statusKey, picked);
await refreshProfileSnapshot();
} catch (error) {
status.className = 'status-line is-unavailable';
- status.textContent = `Не удалось изменить ${toggleKey}: ${error.message || 'ошибка сети'}`;
- showLocalErrorAlert(`Ошибка изменения ${toggleKey}`, error);
+ status.textContent = `Не удалось изменить ${statusKey}: ${error.message || 'ошибка сети'}`;
+ showLocalErrorAlert(`Ошибка изменения ${statusKey}`, error);
}
}
@@ -660,8 +707,14 @@ export function render({ navigate, chrome }) {
label: field.label,
value: field.value || '',
placeholder: field.placeholder || '',
+ maxLength: field.maxLength || 300,
+ multiline: Boolean(field.multiline),
});
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.textContent = 'Сохранение в блокчейн...';
@@ -786,8 +839,8 @@ export function render({ navigate, chrome }) {
});
reloadBtn.addEventListener('click', refreshProfileSnapshot);
- officialBtn.addEventListener('click', () => onToggleClick('official'));
- shineBtn.addEventListener('click', () => onToggleClick('shine'));
+ accountRoleBtn.addEventListener('click', () => onStatusClick('account_role'));
+ shineBtn.addEventListener('click', () => onStatusClick('shine'));
addRelativeBtn?.addEventListener('click', onAddRelativeClick);
avatarActionEl?.addEventListener('click', () => { void onChangeAvatarClick(); });
diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js
index 1aa8b097..5a3b0d72 100644
--- a/shine-UI/js/pages/profile-view.js
+++ b/shine-UI/js/pages/profile-view.js
@@ -81,7 +81,7 @@ function openProfileInfoModal({ title, text }) {
function officialInfoText() {
return 'Можно создавать несколько альтернативных или анонимных каналов. '
- + 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.';
+ + 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
}
function shineInfoText() {
@@ -143,20 +143,21 @@ export function render({ navigate, chrome }) {
const badgesRow = document.createElement('div');
badgesRow.className = 'row';
badgesRow.innerHTML = `
-
-
+
+
`;
const listWrap = document.createElement('div');
listWrap.className = 'stack profile-param-list';
- const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
- const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
+ const accountRoleBtn = badgesRow.querySelector('[data-status="account_role"]');
+ const shineBtn = badgesRow.querySelector('[data-status="shine"]');
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
const avatarSlotEl = topRow.querySelector('[data-profile-avatar-slot="true"]');
let currentFields = [];
- let currentToggles = [];
+ let currentAccountRole = '';
+ let currentShineStatus = '';
let currentGender = 'unknown';
let currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
let currentStats = {
@@ -193,22 +194,19 @@ export function render({ navigate, chrome }) {
}));
}
- function updateToggleButton(button, prefix, enabled) {
- button.textContent = `${prefix}: ${toggleText(enabled)}`;
- button.classList.remove('is-no', 'is-yes-official', 'is-yes-shine');
- if (!enabled) {
- button.classList.add('is-no');
- return;
+ function updateStatusesUi() {
+ if (accountRoleBtn) {
+ const label = currentAccountRole === 'primary' ? 'Основной аккаунт' : currentAccountRole === 'non_voting' ? 'Не учитывать мой голос' : 'Не указано';
+ accountRoleBtn.textContent = `Аккаунт: ${label}`;
+ accountRoleBtn.classList.remove('is-no', 'is-yes-official', 'is-yes-shine', 'is-not-interested');
+ accountRoleBtn.classList.add(currentAccountRole === 'primary' ? 'is-yes-official' : 'is-no');
+ }
+ if (shineBtn) {
+ 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');
}
- if (prefix === 'Официальный') button.classList.add('is-yes-official');
- else button.classList.add('is-yes-shine');
- }
-
- 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() {
@@ -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');
- officialBtn?.addEventListener('click', () => {
+ accountRoleBtn?.addEventListener('click', () => {
openProfileInfoModal({
- title: 'Официальный канал',
+ title: 'Основной аккаунт',
text: officialInfoText(),
});
});
@@ -269,10 +267,8 @@ export function render({ navigate, chrome }) {
{ key: 'website', label: 'Веб', value: '127.0.0.1' },
{ key: 'phone', label: 'Телефон', value: profile.phone },
];
- currentToggles = [
- { key: 'official', enabled: false },
- { key: 'shine', enabled: false },
- ];
+ currentAccountRole = '';
+ currentShineStatus = '';
currentGender = 'unknown';
currentAvatar = { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
currentStats = {
@@ -283,7 +279,7 @@ export function render({ navigate, chrome }) {
};
syncIdentity();
updateAvatarUi();
- updateTogglesUi();
+ updateStatusesUi();
renderFields(currentFields);
return;
}
@@ -294,7 +290,8 @@ export function render({ navigate, chrome }) {
authService.getUser(login).catch(() => ({})),
]);
currentFields = Array.isArray(snapshot.fields) ? snapshot.fields : [];
- currentToggles = Array.isArray(snapshot.toggles) ? snapshot.toggles : [];
+ currentAccountRole = snapshot.accountRole || '';
+ currentShineStatus = snapshot.shineStatus || '';
currentGender = snapshot.gender || 'unknown';
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', sha256Hex: '', timeMs: 0 };
currentStats = {
@@ -305,7 +302,7 @@ export function render({ navigate, chrome }) {
};
syncIdentity();
updateAvatarUi();
- updateTogglesUi();
+ updateStatusesUi();
renderFields(currentFields);
} catch (error) {
// ignore status row in profile-view
diff --git a/shine-UI/js/pages/user-profile-view.js b/shine-UI/js/pages/user-profile-view.js
index d1c83744..199ac91e 100644
--- a/shine-UI/js/pages/user-profile-view.js
+++ b/shine-UI/js/pages/user-profile-view.js
@@ -42,7 +42,7 @@ function openProfileInfoModal({ title, text }) {
function officialInfoText() {
return 'Можно создавать несколько альтернативных или анонимных каналов. '
- + 'Но для корректного учёта голосов на одного реального человека используется только один официальный канал.';
+ + 'Но для корректного учёта голосов на одного реального человека используется только один основной аккаунт.';
}
function shineInfoText() {
@@ -177,10 +177,15 @@ function renderIdentity(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 `
-
-
+ ${accountLabel ? `
` : ''}
+ ${shineLabel ? `
` : ''}
+ ${shineStatus === 'not_interested' ? `

` : ''}
`;
}
@@ -193,6 +198,8 @@ function renderReadOnlyParams(card) {
{ label: 'Адрес', value: card.address },
{ label: 'Web', value: card.web },
{ label: 'Телефон', value: card.phone },
+ { label: 'О себе', value: card.about },
+ { label: 'Духовный путь', value: card.spiritualPath },
];
return `
@@ -341,7 +348,7 @@ export function render({ navigate, route }) {
const infoKind = String(infoBtn?.getAttribute('data-profile-info') || '');
if (infoKind === 'official') {
openProfileInfoModal({
- title: 'Официальный канал',
+ title: 'Основной аккаунт',
text: officialInfoText(),
});
return;
diff --git a/shine-UI/js/services/user-connections.js b/shine-UI/js/services/user-connections.js
index d28155d1..c6c77af1 100644
--- a/shine-UI/js/services/user-connections.js
+++ b/shine-UI/js/services/user-connections.js
@@ -254,9 +254,14 @@ export async function loadUserProfileCard(login) {
address: fields.address || '',
web: fields.web || '',
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',
- official: Boolean(toggles.official),
- shine: Boolean(toggles.shine),
+ // Compatibility aliases for older graph/card consumers; values come only from the new profile schema.
+ official: String(snapshot?.accountRole || '').trim().toLowerCase() === 'primary',
+ shine: String(snapshot?.shineStatus || '').trim().toLowerCase() === 'shining',
avatar: snapshot?.avatar?.txId
? {
ar: String(snapshot.avatar.txId).trim(),
diff --git a/shine-UI/js/services/user-profile-params.js b/shine-UI/js/services/user-profile-params.js
index 8b5ec081..230d9bac 100644
--- a/shine-UI/js/services/user-profile-params.js
+++ b/shine-UI/js/services/user-profile-params.js
@@ -12,12 +12,17 @@ export const profileFieldDefs = [
{ key: 'address', readKeys: ['address'], label: 'Адрес', placeholder: 'Город, улица, дом' },
{ key: 'web', readKeys: ['web'], label: 'Веб', placeholder: 'Сайт или профиль' },
{ 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 = [
- { key: 'official', label: 'Официальный' },
- { key: 'shine', label: 'Сияющий' },
-];
+export const profileToggleDefs = []; // legacy boolean toggles are no longer used by the new UI.
+
+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_FEMALE = 'female';
@@ -104,23 +109,31 @@ export async function loadProfileSnapshot(login) {
key: field.key,
label: field.label,
placeholder: field.placeholder,
+ maxLength: Number(field.maxLength || 300),
+ multiline: Boolean(field.multiline),
value: latest?.value || '',
timeMs: latest?.timeMs || 0,
});
}
- const toggles = [];
- for (let i = 0; i < profileToggleDefs.length; i += 1) {
- const toggle = profileToggleDefs[i];
- const latest = loadLatestByAliasesFromItems(items, [toggle.key]);
- toggles.push({
- key: toggle.key,
- label: toggle.label,
- enabled: latest ? parseToggleValue(latest.value) : false,
- rawValue: latest?.value || 'no',
- timeMs: latest?.timeMs || 0,
- });
- }
+ const latestAccountRole = loadLatestByAliasesFromItems(items, ['account_role']);
+ const rawAccountRole = String(latestAccountRole?.value || '').trim().toLowerCase();
+ const accountRole = rawAccountRole === PROFILE_ACCOUNT_ROLE_PRIMARY || rawAccountRole === PROFILE_ACCOUNT_ROLE_NON_VOTING
+ ? rawAccountRole
+ : '';
+
+ const latestShine = loadLatestByAliasesFromItems(items, ['shine']);
+ const rawShine = String(latestShine?.value || '').trim().toLowerCase();
+ let shineStatus = '';
+ 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 gender = normalizeGenderValue(latestGender?.value || PROFILE_GENDER_UNKNOWN);
@@ -145,6 +158,10 @@ export async function loadProfileSnapshot(login) {
return {
fields,
toggles,
+ accountRole,
+ accountRoleTimeMs: latestAccountRole?.timeMs || 0,
+ shineStatus,
+ shineTimeMs: latestShine?.timeMs || 0,
gender,
genderTimeMs: latestGender?.timeMs || 0,
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();
- await authService.addBlockUserParam({
- login,
- param: key,
- value: enabled ? 'yes' : 'no',
- storagePwd,
- });
+ await authService.addBlockUserParam({ login, param: cleanKey, value: cleanValue, storagePwd });
+}
+
+// Kept only for old callers outside the current UI. New code must use saveProfileStatus().
+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) {
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 5a01aafb..c34a8bf9 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -10568,3 +10568,34 @@ body.chat-topbar-overlay .composer-slot {
line-height: 1.1 !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;
+}