SHA256
Добавить аватар профиля через Arweave и мастер загрузки
This commit is contained in:
@@ -11,6 +11,8 @@ import {
|
||||
saveProfileToggle,
|
||||
} from '../services/user-profile-params.js';
|
||||
import { buildIdentityLines, loadUserProfileCard } from '../services/user-connections.js';
|
||||
import { buildArweaveDataUrl } from '../services/arweave-file-service.js';
|
||||
import { openAvatarWizard } from '../components/avatar-wizard.js';
|
||||
|
||||
export const pageMeta = { id: 'profile-view', title: 'Профиль' };
|
||||
|
||||
@@ -102,7 +104,13 @@ export function render({ navigate }) {
|
||||
topRow.className = 'row';
|
||||
topRow.innerHTML = `
|
||||
<div class="row" style="gap:12px; align-items:center;">
|
||||
<div class="avatar large">${profile.avatarInitials}</div>
|
||||
<div class="profile-avatar-block">
|
||||
<div class="avatar large profile-avatar" data-profile-avatar="true">
|
||||
<span data-avatar-fallback="true">${profile.avatarInitials}</span>
|
||||
<img src="" alt="Аватар" data-avatar-image="true" hidden />
|
||||
</div>
|
||||
<button class="ghost-btn profile-avatar-change-btn" type="button" data-change-avatar="true">Сменить аватар</button>
|
||||
</div>
|
||||
<div class="profile-identity-lines" data-profile-identity="true">
|
||||
<div class="profile-identity-line profile-identity-login">${String(login || '').trim() || 'unknown'}</div>
|
||||
</div>
|
||||
@@ -139,11 +147,15 @@ export function render({ navigate }) {
|
||||
const officialBtn = badgesRow.querySelector('[data-toggle="official"]');
|
||||
const shineBtn = badgesRow.querySelector('[data-toggle="shine"]');
|
||||
const addRelativeBtn = relativesCard.querySelector('[data-add-relative="true"]');
|
||||
const changeAvatarBtn = topRow.querySelector('[data-change-avatar="true"]');
|
||||
|
||||
let currentFields = [];
|
||||
let currentToggles = [];
|
||||
let currentGender = PROFILE_GENDER_UNKNOWN;
|
||||
let currentAvatar = { value: '', source: '', txId: '', timeMs: 0 };
|
||||
const identityEl = topRow.querySelector('[data-profile-identity="true"]');
|
||||
const avatarImageEl = topRow.querySelector('[data-avatar-image="true"]');
|
||||
const avatarFallbackEl = topRow.querySelector('[data-avatar-fallback="true"]');
|
||||
|
||||
function openGenderPickerModal(initialGender) {
|
||||
const root = document.getElementById('modal-root');
|
||||
@@ -207,6 +219,58 @@ export function render({ navigate }) {
|
||||
)).join('');
|
||||
}
|
||||
|
||||
function buildAvatarFallbackText() {
|
||||
const firstName = String(currentFields.find((field) => field.key === 'first_name')?.value || '').trim();
|
||||
const lastName = String(currentFields.find((field) => field.key === 'last_name')?.value || '').trim();
|
||||
const fromNames = `${firstName.charAt(0)}${lastName.charAt(0)}`.toUpperCase();
|
||||
if (fromNames.trim()) return fromNames;
|
||||
|
||||
const loginFirst = String(login || '').trim().charAt(0).toUpperCase();
|
||||
if (loginFirst) return loginFirst;
|
||||
|
||||
return String(profile.avatarInitials || '??').trim().slice(0, 2).toUpperCase();
|
||||
}
|
||||
|
||||
function showAvatarFallback() {
|
||||
if (avatarImageEl instanceof HTMLImageElement) {
|
||||
avatarImageEl.hidden = true;
|
||||
avatarImageEl.removeAttribute('src');
|
||||
}
|
||||
if (avatarFallbackEl instanceof HTMLElement) {
|
||||
avatarFallbackEl.hidden = false;
|
||||
avatarFallbackEl.textContent = buildAvatarFallbackText();
|
||||
}
|
||||
}
|
||||
|
||||
function updateAvatarUi() {
|
||||
if (!(avatarImageEl instanceof HTMLImageElement)) return;
|
||||
const txId = String(currentAvatar?.txId || '').trim();
|
||||
if (!txId) {
|
||||
showAvatarFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
let avatarUrl = '';
|
||||
try {
|
||||
avatarUrl = buildArweaveDataUrl({
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
txId,
|
||||
});
|
||||
} catch {
|
||||
showAvatarFallback();
|
||||
return;
|
||||
}
|
||||
|
||||
avatarImageEl.onload = () => {
|
||||
if (avatarFallbackEl instanceof HTMLElement) avatarFallbackEl.hidden = true;
|
||||
avatarImageEl.hidden = false;
|
||||
};
|
||||
avatarImageEl.onerror = () => {
|
||||
showAvatarFallback();
|
||||
};
|
||||
avatarImageEl.src = avatarUrl;
|
||||
}
|
||||
|
||||
function updateToggleButton(button, prefix, enabled) {
|
||||
button.textContent = `${prefix}: ${toggleText(enabled)}`;
|
||||
button.classList.remove('is-no', 'is-yes-official', 'is-yes-shine');
|
||||
@@ -545,6 +609,7 @@ export function render({ navigate }) {
|
||||
officialBtn.disabled = true;
|
||||
shineBtn.disabled = true;
|
||||
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = true;
|
||||
if (changeAvatarBtn instanceof HTMLButtonElement) changeAvatarBtn.disabled = true;
|
||||
const genderActionBtn = listWrap.querySelector('[data-edit-gender="true"]');
|
||||
if (genderActionBtn instanceof HTMLButtonElement) {
|
||||
genderActionBtn.disabled = true;
|
||||
@@ -555,11 +620,13 @@ export function render({ navigate }) {
|
||||
currentFields = snapshot.fields;
|
||||
currentToggles = snapshot.toggles;
|
||||
currentGender = snapshot.gender || PROFILE_GENDER_UNKNOWN;
|
||||
currentAvatar = snapshot.avatar || { value: '', source: '', txId: '', timeMs: 0 };
|
||||
|
||||
syncIdentity();
|
||||
renderFields(currentFields);
|
||||
updateTogglesUi();
|
||||
updateGenderUi();
|
||||
updateAvatarUi();
|
||||
|
||||
status.className = 'status-line';
|
||||
status.textContent = '';
|
||||
@@ -572,6 +639,7 @@ export function render({ navigate }) {
|
||||
officialBtn.disabled = false;
|
||||
shineBtn.disabled = false;
|
||||
if (addRelativeBtn instanceof HTMLButtonElement) addRelativeBtn.disabled = false;
|
||||
if (changeAvatarBtn instanceof HTMLButtonElement) changeAvatarBtn.disabled = false;
|
||||
const genderActionBtnAfter = listWrap.querySelector('[data-edit-gender="true"]');
|
||||
if (genderActionBtnAfter instanceof HTMLButtonElement) {
|
||||
genderActionBtnAfter.disabled = false;
|
||||
@@ -579,6 +647,37 @@ export function render({ navigate }) {
|
||||
}
|
||||
}
|
||||
|
||||
async function onChangeAvatarClick() {
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Открываем мастер аватара...';
|
||||
|
||||
try {
|
||||
await openAvatarWizard({
|
||||
login,
|
||||
storagePwd: state.session.storagePwdInMemory,
|
||||
gateway: state.entrySettings.arweaveServer,
|
||||
navigate,
|
||||
onStatus: (message) => {
|
||||
status.className = 'status-line';
|
||||
status.textContent = String(message || '');
|
||||
},
|
||||
onAvatarSaved: async () => {
|
||||
status.className = 'status-line';
|
||||
status.textContent = 'Сохраняем аватар в профиль...';
|
||||
await refreshProfileSnapshot();
|
||||
status.className = 'status-line is-available';
|
||||
status.textContent = 'Аватар обновлён.';
|
||||
},
|
||||
});
|
||||
if (!status.textContent) {
|
||||
status.className = 'status-line';
|
||||
}
|
||||
} catch (error) {
|
||||
status.className = 'status-line is-unavailable';
|
||||
status.textContent = error?.message || 'Не удалось открыть мастер аватара.';
|
||||
}
|
||||
}
|
||||
|
||||
async function onToggleClick(toggleKey) {
|
||||
const toggle = currentToggles.find((item) => item.key === toggleKey) || { enabled: false };
|
||||
const nextEnabled = !toggle.enabled;
|
||||
@@ -742,10 +841,12 @@ export function render({ navigate }) {
|
||||
officialBtn.addEventListener('click', () => onToggleClick('official'));
|
||||
shineBtn.addEventListener('click', () => onToggleClick('shine'));
|
||||
addRelativeBtn?.addEventListener('click', onAddRelativeClick);
|
||||
changeAvatarBtn?.addEventListener('click', () => { void onChangeAvatarClick(); });
|
||||
|
||||
card.append(topRow, badgesRow, status, listWrap, relativesCard);
|
||||
screen.append(card);
|
||||
|
||||
showAvatarFallback();
|
||||
refreshProfileSnapshot();
|
||||
|
||||
return screen;
|
||||
|
||||
@@ -316,7 +316,18 @@ export function render({ navigate }) {
|
||||
`;
|
||||
const addressEl = addressCard.querySelector('#wallet-address-value');
|
||||
|
||||
card.append(balanceWrap, addressCard);
|
||||
const helpCard = document.createElement('details');
|
||||
helpCard.className = 'card';
|
||||
helpCard.style.padding = '10px';
|
||||
helpCard.innerHTML = `
|
||||
<summary style="cursor:pointer; font-weight:600;">Как получен этот адрес?</summary>
|
||||
<p class="meta-muted" style="margin-top:8px;">
|
||||
SHiNE берёт ваш локальный device.key и по стандарту SAWD-v1 получает из него нативный Arweave-кошелёк.
|
||||
Приватный ключ не отправляется на сервер. После первого расчёта он хранится только в зашифрованном контейнере этого устройства.
|
||||
</p>
|
||||
`;
|
||||
|
||||
card.append(balanceWrap, addressCard, helpCard);
|
||||
|
||||
const actions = document.createElement('div');
|
||||
actions.className = 'stack';
|
||||
@@ -412,7 +423,18 @@ export function render({ navigate }) {
|
||||
setStatus('Генерация Arweave-кошелька...');
|
||||
|
||||
try {
|
||||
arweaveWalletCtx = await getArweaveWalletFromStoredDeviceKey(sessionArgsOrThrow());
|
||||
arweaveWalletCtx = await getArweaveWalletFromStoredDeviceKey({
|
||||
...sessionArgsOrThrow(),
|
||||
onStatus: (message) => {
|
||||
const text = String(message || '').trim();
|
||||
if (!text) return;
|
||||
if (text.includes('впервые получаем Arweave-кошелёк')) {
|
||||
setStatus('Сейчас мы впервые получаем Arweave-кошелёк из вашего приватного device key. Это может занять немного времени. После этого кошелёк будет храниться только в зашифрованном контейнере этого устройства.');
|
||||
return;
|
||||
}
|
||||
setStatus(text);
|
||||
},
|
||||
});
|
||||
if (modeToken !== activeModeToken) return;
|
||||
walletAddress = arweaveWalletCtx.address;
|
||||
addressEl.textContent = walletAddress;
|
||||
|
||||
Reference in New Issue
Block a user