Исправить профиль и список каналов

This commit is contained in:
2026-09-02 16:32:22 +03:00
parent e5556f3706
commit 0c5089fa79
4 changed files with 191 additions and 43 deletions
@@ -119,14 +119,14 @@ public final class UserProfileStateDAO {
sql="""
SELECT cn.owner_login, cn.slug, cn.display_name, cn.ava_ar, cn.owner_bch_name, cn.channel_root_block_number, encode(cn.channel_root_block_hash,'hex') root_hash
FROM channel_names_state cn WHERE LOWER(cn.owner_login)=LOWER(?) AND cn.channel_type_code=1
ORDER BY LOWER(cn.display_name), cn.slug LIMIT ? OFFSET ?
ORDER BY cn.display_name, cn.slug LIMIT ? OFFSET ?
""";
} else if ("following".equals(mode)) {
sql="""
SELECT DISTINCT cn.owner_login, cn.slug, cn.display_name, cn.ava_ar, cn.owner_bch_name, cn.channel_root_block_number, encode(cn.channel_root_block_hash,'hex') root_hash
FROM connections_state cs JOIN channel_names_state cn ON cn.owner_bch_name=cs.to_bch_name AND cn.channel_root_block_number=cs.to_block_number AND cn.channel_root_block_hash=cs.to_block_hash
WHERE LOWER(cs.login)=LOWER(?) AND cs.rel_type=30 AND cn.channel_type_code=1
ORDER BY LOWER(cn.display_name), cn.slug LIMIT ? OFFSET ?
ORDER BY cn.display_name, cn.slug LIMIT ? OFFSET ?
""";
} else throw new IllegalArgumentException("Unsupported channel mode: "+mode);
List<ChannelCard> out=new ArrayList<>();
+2 -2
View File
@@ -13,8 +13,8 @@ function parseAvatar(raw) {
}
const TITLES = {
friends: 'Друзья', close_friends: 'Близкие друзья', primary_received: 'Подтвердили основной аккаунт',
primary_given: 'Подтверждённые аккаунты', shine_received: 'Считают сияющим', shine_given: 'Подтверждённые сияющие',
friends: 'Друзья', close_friends: 'Близкие друзья', primary_received: 'Подтвердили аккаунт',
primary_given: 'Подтверждённые аккаунты', shine_received: 'Подтвердили сияние', shine_given: 'Подтверждённые сияющие',
channels_owned: 'Каналы', channels_following: 'Подписки на каналы',
};
+35 -36
View File
@@ -38,31 +38,12 @@ function metricHtml({ kind, label, value, glow = false, positionClass = '' }) {
</button>`;
}
function openProfileSheet(title, html) {
const root = document.getElementById('modal-root');
if (!root) return;
root.innerHTML = `
<div class="user-profile-sheet-backdrop" id="user-profile-sheet-backdrop">
<section class="user-profile-sheet" role="dialog" aria-modal="true" aria-label="${escapeHtml(title)}">
<div class="user-profile-sheet-handle" aria-hidden="true"></div>
<div class="user-profile-sheet-title">${escapeHtml(title)}</div>
<div class="user-profile-sheet-content">${html}</div>
</section>
</div>`;
const close = () => {
if (root.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = '';
};
root.querySelector('#user-profile-sheet-backdrop')?.addEventListener('click', (event) => {
if (event.target?.id === 'user-profile-sheet-backdrop') close();
});
function spiritualPathDetailHtml(card) {
const value = String(card?.spiritualPath || '').trim();
return `<div class="user-profile-detail-copy${value ? '' : ' is-muted'}">${escapeHtml(value || 'Не заполнено')}</div>`;
}
function spiritualPathSheetHtml(card) {
return `<div class="user-profile-sheet-copy">${escapeHtml(card?.spiritualPath || 'Не заполнено')}</div>`;
}
function contactsSheetHtml(card) {
function contactsDetailHtml(card) {
const rows = [
['Ссылки', card?.web],
['Телефон', card?.phone],
@@ -70,7 +51,7 @@ function contactsSheetHtml(card) {
].filter(([, value]) => String(value || '').trim());
if (!rows.length) {
return '<div class="user-profile-sheet-copy is-muted">Не заполнено</div>';
return '<div class="user-profile-detail-copy is-muted">Не заполнено</div>';
}
return rows.map(([label, value]) => `
<div class="user-profile-contact-row">
@@ -232,10 +213,15 @@ export function render({ navigate, route, chrome }) {
</div>
</div>` : ''}
<div class="user-profile-detail-links">
<button type="button" data-profile-detail="spiritual-path">Духовный путь</button>
<button type="button" data-profile-detail="contacts">Контакты</button>
</div>`;
<div class="user-profile-detail-links" aria-label="Дополнительная информация о пользователе">
<button type="button" data-profile-detail="spiritual-path" aria-pressed="false" aria-controls="user-profile-detail-panel">
<span class="user-profile-detail-tab-label">Духовный путь</span>
</button>
<button type="button" data-profile-detail="contacts" aria-pressed="false" aria-controls="user-profile-detail-panel">
<span class="user-profile-detail-tab-label">Контакты</span>
</button>
</div>
<section class="user-profile-detail-panel" id="user-profile-detail-panel" aria-live="polite" hidden></section>`;
const avatarSlot = body.querySelector('.user-profile-avatar-slot');
avatarSlot?.append(renderUserAvatar({
@@ -268,12 +254,27 @@ export function render({ navigate, route, chrome }) {
}
const detailButton = event.target.closest('[data-profile-detail]');
if (detailButton?.dataset.profileDetail === 'spiritual-path') {
openProfileSheet('Духовный путь', spiritualPathSheetHtml(card));
return;
}
if (detailButton?.dataset.profileDetail === 'contacts') {
openProfileSheet('Контакты', contactsSheetHtml(card));
if (detailButton) {
const detailKind = detailButton.dataset.profileDetail;
const detailPanel = body.querySelector('#user-profile-detail-panel');
if (!detailPanel) return;
body.querySelectorAll('[data-profile-detail]').forEach((button) => {
const active = button === detailButton;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
if (detailKind === 'spiritual-path') {
detailPanel.innerHTML = spiritualPathDetailHtml(card);
} else if (detailKind === 'contacts') {
detailPanel.innerHTML = contactsDetailHtml(card);
} else {
return;
}
detailPanel.hidden = false;
detailPanel.dataset.activeDetail = detailKind;
return;
}
@@ -340,8 +341,6 @@ export function render({ navigate, route, chrome }) {
screen.cleanup = () => {
document.removeEventListener('pointerdown', handleOutsidePointer);
const root = document.getElementById('modal-root');
if (root?.querySelector('#user-profile-sheet-backdrop')) root.innerHTML = '';
};
return screen;
+152 -3
View File
@@ -9912,7 +9912,7 @@ body.chat-topbar-overlay .composer-slot > * {
.user-profile-full-name {
width: min(88%, 340px);
margin: 0 auto clamp(8px, 1.4vh, 12px);
margin: -5px auto clamp(13px, 2vh, 17px);
color: rgba(239, 246, 255, 0.94);
font-size: clamp(16px, 4.5vw, 19px);
font-weight: 700;
@@ -11340,8 +11340,8 @@ body.chat-topbar-overlay .composer-slot {
.profile-stats-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:10px}
.profile-stat-card{display:flex;flex-direction:column;align-items:flex-start;gap:3px;text-align:left;cursor:pointer}
.profile-stat-card b{font-size:22px}.profile-stat-card span{font-size:12px;opacity:.78}
.profile-list-row{width:100%;align-items:center;gap:12px;text-align:left;cursor:pointer}
.profile-list-row-text{display:flex;flex-direction:column;gap:3px;min-width:0}.profile-list-row-text small{opacity:.68}
.profile-list-row{width:100%;align-items:center;justify-content:flex-start;gap:12px;text-align:left;cursor:pointer}
.profile-list-row-text{display:flex;flex:1 1 auto;flex-direction:column;align-items:flex-start;gap:3px;min-width:0;overflow:hidden}.profile-list-row-text b,.profile-list-row-text small{display:block;width:100%;text-align:left;overflow-wrap:anywhere}.profile-list-row-text small{opacity:.68}
.profile-bottom-actions{justify-content:center;gap:12px;position:sticky;bottom:12px;z-index:3}
.profile-about:empty{display:none}
@@ -11527,3 +11527,152 @@ body.chat-topbar-overlay .page-header.app-topbar-shell .header-center {
flex: 1 1 0 !important;
min-width: 0 !important;
}
/* ===== Профиль другого пользователя: встроенные вкладки деталей (2026-09-02) =====
* «Духовный путь» и «Контакты» переключают содержимое прямо в свободной области
* страницы. Активная кнопка отмечается отдельным сияющим эллипсом вокруг текста. */
.user-profile-screen {
display: flex;
flex-direction: column;
}
.user-profile-body {
flex: 1 0 auto;
}
.user-profile-detail-links {
flex: 0 0 auto;
}
.user-profile-detail-links button {
overflow: visible;
}
.user-profile-detail-tab-label {
position: relative;
z-index: 0;
display: inline-flex;
min-height: 34px;
align-items: center;
justify-content: center;
color: rgba(229, 239, 253, 0.86);
transition: color 150ms ease, filter 150ms ease;
}
.user-profile-detail-tab-label::before {
content: '';
position: absolute;
z-index: -1;
left: 50%;
top: 50%;
width: calc(100% + 24px);
min-width: 112px;
height: 34px;
transform: translate(-50%, -50%);
border: 1px solid transparent;
border-radius: 50%;
opacity: 0;
pointer-events: none;
transition: opacity 150ms ease, border-color 150ms ease, box-shadow 150ms ease;
}
.user-profile-detail-links button.is-active .user-profile-detail-tab-label {
color: #f4f9ff;
filter: brightness(1.08);
}
.user-profile-detail-links button.is-active .user-profile-detail-tab-label::before {
opacity: 1;
border-color: rgba(144, 218, 255, 0.9);
box-shadow:
0 0 5px rgba(139, 220, 255, 0.82),
0 0 13px rgba(72, 145, 255, 0.58),
0 0 26px rgba(72, 145, 255, 0.28),
inset 0 0 9px rgba(139, 220, 255, 0.16);
}
.user-profile-detail-panel {
width: min(92%, 360px);
flex: 1 1 auto;
min-height: clamp(104px, 18vh, 170px);
margin: clamp(7px, 1.5vh, 12px) auto 0;
padding: clamp(10px, 2vh, 16px) 4px 4px;
color: rgba(225, 234, 248, 0.88);
overflow-wrap: anywhere;
}
.user-profile-detail-panel[hidden] {
display: none !important;
}
.user-profile-detail-copy {
white-space: pre-wrap;
overflow-wrap: anywhere;
color: rgba(225, 234, 248, 0.88);
font-size: 14px;
line-height: 1.58;
}
.user-profile-detail-copy.is-muted {
color: rgba(194, 207, 229, 0.56);
text-align: center;
}
.user-profile-detail-panel .user-profile-contact-row {
padding: 4px 0;
}
@media (max-height: 700px) {
.user-profile-detail-panel {
min-height: 88px;
margin-top: 5px;
padding-top: 8px;
}
}
/* ===== Профиль другого пользователя: сияние активной вкладки от букв (2026-09-02) =====
* Без жёсткой рамки: синий свет рождается на глифах текста и мягко затухает
* в вытянутом эллиптическом ореоле вокруг подписи. */
.user-profile-detail-tab-label::before {
width: calc(100% + 34px);
min-width: 118px;
height: 40px;
border: 0 !important;
background: radial-gradient(
ellipse at center,
rgba(105, 218, 255, 0.20) 0%,
rgba(72, 160, 255, 0.14) 34%,
rgba(54, 110, 255, 0.07) 56%,
transparent 76%
);
box-shadow: none !important;
filter: blur(5px);
transform: translate(-50%, -50%) scale(0.88);
transition: opacity 160ms ease, transform 180ms ease, filter 180ms ease;
}
.user-profile-detail-links button.is-active .user-profile-detail-tab-label {
color: #effbff;
filter: none;
text-shadow:
0 0 1px rgba(245, 253, 255, 0.98),
0 0 4px rgba(126, 222, 255, 0.98),
0 0 8px rgba(78, 181, 255, 0.92),
0 0 15px rgba(55, 126, 255, 0.72),
0 0 25px rgba(55, 126, 255, 0.42);
}
.user-profile-detail-links button.is-active .user-profile-detail-tab-label::before {
opacity: 1;
border: 0 !important;
background: radial-gradient(
ellipse at center,
rgba(112, 224, 255, 0.22) 0%,
rgba(74, 166, 255, 0.16) 34%,
rgba(51, 107, 255, 0.08) 57%,
transparent 78%
);
box-shadow: none !important;
filter: blur(6px);
transform: translate(-50%, -50%) scale(1);
}