SHA256
UI: доработать профиль и связи
This commit is contained in:
+1
-1
@@ -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 |
@@ -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(); });
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user