SHA256
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов, цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли. - Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие — редактор цветов с экспортом/импортом. - Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале», создание канала с адресом из названия; лента открывается на свежих постах. - Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке. - Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка; настройки, кошелёк, сеансы — меню-списки. - Нижняя панель: иконки без подписей, бейджи на иконках. - confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу. - docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
166 lines
7.6 KiB
JavaScript
166 lines
7.6 KiB
JavaScript
import { resolveToolbarActive } from '../router.js';
|
|
import { state, authService } from '../state.js';
|
|
import { openAuthRequiredModal } from '../services/auth-required-modal.js';
|
|
import { SHINE_CONNECTIONS_LOGO_SRC } from './shine-logo.js';
|
|
|
|
const TOOLBAR_ICONS = {
|
|
'messages-list': '<g transform="translate(12 12) scale(.88) translate(-11.5 -13)"><path d="M21 11.5a8.5 8.5 0 0 1-8.5 8.5H4l-2 2v-9.5A8.5 8.5 0 0 1 10.5 4H13a8 8 0 0 1 8 7.5Z"/><circle cx="8" cy="12" r="1.05" class="toolbar-svg-dot"/><circle cx="11.7" cy="12" r="1.05" class="toolbar-svg-dot"/><circle cx="15.4" cy="12" r="1.05" class="toolbar-svg-dot"/></g>',
|
|
'channels-list': '<rect x="4" y="3" width="16" height="18" rx="3"/><path d="M8 8h8M8 12h8M8 16h5"/>',
|
|
'notifications-view': '<path d="M6.2 16.8V11a5.8 5.8 0 0 1 11.6 0v5.8l1.7 1.7h-15Z"/><path d="M10.2 21a2 2 0 0 0 3.6 0"/>',
|
|
'profile-view': '<circle cx="12" cy="6.8" r="3.6"/><path d="M4.4 21v-2.1a5.6 5.6 0 0 1 5.6-5.6h4a5.6 5.6 0 0 1 5.6 5.6V21Z"/>',
|
|
};
|
|
|
|
const ITEMS = [
|
|
{ pageId: 'messages-list', label: 'Личные' },
|
|
{ pageId: 'channels-list', label: 'Каналы' },
|
|
{ pageId: 'network-view', label: 'Связи', mandala: true },
|
|
{ pageId: 'notifications-view', label: 'Уведомления' },
|
|
{ pageId: 'profile-view', label: 'Профиль' },
|
|
];
|
|
|
|
function iconHtml(item, extra = '') {
|
|
const glyph = item.mandala
|
|
? `<img class="toolbar-mandala" src="${SHINE_CONNECTIONS_LOGO_SRC}" alt="" aria-hidden="true" />`
|
|
: `<svg class="toolbar-svg" viewBox="0 0 24 24" aria-hidden="true">${TOOLBAR_ICONS[item.pageId]}</svg>`;
|
|
return `<span class="toolbar-icon${item.mandala ? ' toolbar-icon--mandala' : ''}">${glyph}${extra}</span>`;
|
|
}
|
|
|
|
function normalizeCounters(payload = {}) {
|
|
const notifications = payload?.notifications || {};
|
|
const notificationTotal = Math.max(0, Number(payload?.notificationsUnreadCount ?? (
|
|
Number(notifications.replies || 0) + Number(notifications.connections || 0) + Number(notifications.events || 0)
|
|
)) || 0);
|
|
return {
|
|
dmUnreadCount: Math.max(0, Number(payload?.dmUnreadCount || 0) || 0),
|
|
channelsUnreadCount: Math.max(0, Number(payload?.channelsUnreadCount || 0) || 0),
|
|
notificationsUnreadCount: notificationTotal,
|
|
notifications: {
|
|
replies: Math.max(0, Number(notifications.replies ?? payload?.notificationRepliesUnreadCount ?? 0) || 0),
|
|
connections: Math.max(0, Number(notifications.connections ?? payload?.notificationConnectionsUnreadCount ?? 0) || 0),
|
|
events: Math.max(0, Number(notifications.events ?? payload?.notificationEventsUnreadCount ?? 0) || 0),
|
|
},
|
|
};
|
|
}
|
|
|
|
function setCounterState(payload = {}) {
|
|
state.userCounters = normalizeCounters(payload);
|
|
state.notificationUnreadTotal = state.userCounters.notificationsUnreadCount;
|
|
}
|
|
|
|
function renderBadge(btn, count, ariaLabel, extraClass = '') {
|
|
if (!btn) return;
|
|
let badge = btn.querySelector('.toolbar-unread-badge');
|
|
if (count <= 0) { badge?.remove(); return; }
|
|
if (!badge) {
|
|
badge = document.createElement('span');
|
|
badge.className = `toolbar-unread-badge${extraClass ? ` ${extraClass}` : ''}`;
|
|
(btn.querySelector('.toolbar-icon') || btn).append(badge);
|
|
}
|
|
badge.textContent = count > 99 ? '99+' : String(count);
|
|
badge.setAttribute('aria-label', `${ariaLabel}: ${count}`);
|
|
}
|
|
|
|
function applyCountersToMountedToolbars() {
|
|
const c = normalizeCounters(state.userCounters);
|
|
document.querySelectorAll('.toolbar').forEach((toolbar) => {
|
|
renderBadge(toolbar.querySelector('[data-toolbar-page="messages-list"]'), c.dmUnreadCount, 'Непрочитанных личных сообщений');
|
|
renderBadge(toolbar.querySelector('[data-toolbar-page="channels-list"]'), c.channelsUnreadCount, 'Непрочитанных сообщений в каналах');
|
|
renderBadge(toolbar.querySelector('[data-toolbar-page="notifications-view"]'), c.notificationsUnreadCount, 'Новых уведомлений', 'notification-toolbar-badge');
|
|
});
|
|
}
|
|
|
|
let countersPushBound = false;
|
|
function ensureCountersPushBound() {
|
|
if (countersPushBound) return;
|
|
countersPushBound = true;
|
|
authService.onEvent('UserCountersChanged', (event) => {
|
|
setCounterState(event?.payload || {});
|
|
applyCountersToMountedToolbars();
|
|
});
|
|
}
|
|
|
|
async function refreshUserCounters() {
|
|
if (!state.session.isAuthorized) return;
|
|
try {
|
|
setCounterState(await authService.getUserCounters());
|
|
applyCountersToMountedToolbars();
|
|
} catch {
|
|
// Keep the last known counters; realtime push or the next refresh can recover.
|
|
}
|
|
}
|
|
|
|
function navigateWithGuestRules(pageId, navigate) {
|
|
if (state.session.isAuthorized) {
|
|
navigate(pageId);
|
|
return;
|
|
}
|
|
if (pageId === 'messages-list') {
|
|
openAuthRequiredModal({
|
|
title: 'Личные сообщения недоступны',
|
|
text: 'Вы не авторизованы. Для личных сообщений сначала войдите в систему.',
|
|
});
|
|
return;
|
|
}
|
|
if (pageId === 'profile-view') {
|
|
openAuthRequiredModal({
|
|
title: 'Профиль недоступен',
|
|
text: 'Вы не авторизованы. Для профиля сначала войдите в систему.',
|
|
});
|
|
return;
|
|
}
|
|
if (pageId === 'notifications-view') {
|
|
openAuthRequiredModal({
|
|
title: 'Уведомления недоступны',
|
|
text: 'Вы не авторизованы. Для уведомлений сначала войдите в систему.',
|
|
});
|
|
return;
|
|
}
|
|
navigate(pageId);
|
|
}
|
|
|
|
export function renderToolbar(currentPageId, navigate) {
|
|
const root = document.createElement('nav');
|
|
root.className = 'toolbar';
|
|
const active = resolveToolbarActive(currentPageId);
|
|
ensureCountersPushBound();
|
|
const counters = normalizeCounters(state.userCounters);
|
|
|
|
ITEMS.forEach((item) => {
|
|
const btn = document.createElement('button');
|
|
const isProfile = item.pageId === 'profile-view';
|
|
const isMessages = item.pageId === 'messages-list';
|
|
const isNetwork = item.pageId === 'network-view';
|
|
const isNotifications = item.pageId === 'notifications-view';
|
|
btn.type = 'button';
|
|
if (item.pageId === active) btn.setAttribute('aria-current', 'page');
|
|
btn.dataset.toolbarPage = item.pageId;
|
|
btn.className = `toolbar-btn${item.pageId === active ? ' active' : ''}${isProfile ? ' toolbar-btn-profile' : ''}${isMessages ? ' toolbar-btn-messages' : ''}${isNetwork ? ' toolbar-btn-network' : ''}${item.hero ? ' toolbar-btn-hero' : ''}`;
|
|
if (isProfile) {
|
|
btn.innerHTML = `
|
|
${iconHtml(item, `<span id="toolbar-connection-indicator" class="toolbar-connection-indicator is-unknown">
|
|
<span class="toolbar-connection-dot" aria-hidden="true"></span>
|
|
</span>`)}
|
|
<span class="sr-only">${item.label}</span>
|
|
`;
|
|
} else if (isNetwork) {
|
|
btn.innerHTML = `${iconHtml(item)}<span class="sr-only">${item.label}</span>`;
|
|
} else {
|
|
btn.innerHTML = `${iconHtml(item)}<span class="sr-only">${item.label}</span>`;
|
|
}
|
|
btn.title = item.label;
|
|
if (isMessages) renderBadge(btn, counters.dmUnreadCount, 'Непрочитанных личных сообщений');
|
|
if (item.pageId === 'channels-list') renderBadge(btn, counters.channelsUnreadCount, 'Непрочитанных сообщений в каналах');
|
|
if (isNotifications) renderBadge(btn, counters.notificationsUnreadCount, 'Новых уведомлений', 'notification-toolbar-badge');
|
|
if (item.pageId === 'channels-list') {
|
|
btn.addEventListener('click', () => navigate('channels-list'));
|
|
} else {
|
|
btn.addEventListener('click', () => navigateWithGuestRules(item.pageId, navigate));
|
|
}
|
|
root.append(btn);
|
|
});
|
|
|
|
if (state.session.isAuthorized) void refreshUserCounters();
|
|
|
|
return root;
|
|
}
|