diff --git a/VERSION.properties b/VERSION.properties
index f83edfa4..3c67cf67 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.5.31
+client.version=1.5.32
server.version=1.4.10
diff --git a/shine-UI/index.html b/shine-UI/index.html
index a392a36e..fea3b7e3 100644
--- a/shine-UI/index.html
+++ b/shine-UI/index.html
@@ -40,7 +40,9 @@ window.__SHINE_BUILD_HASH__ = '20260806223040';
Сияние
diff --git a/shine-UI/js/app.js b/shine-UI/js/app.js
index a8d07478..50789928 100644
--- a/shine-UI/js/app.js
+++ b/shine-UI/js/app.js
@@ -151,6 +151,8 @@ const routes = {
};
const screenEl = document.getElementById('app-screen');
+const topbarEl = document.getElementById('topbar-slot');
+const composerEl = document.getElementById('composer-slot');
const toolbarEl = document.getElementById('toolbar-slot');
const appShellEl = document.querySelector('.app-shell');
const initialSplashEl = document.getElementById('initial-splash');
@@ -181,6 +183,7 @@ let hiddenDmAudioContext = null;
let hiddenDmAudioUnlocked = false;
let initialConnectionCompleted = false;
let orientationLockInFlight = false;
+let currentChromeCleanup = null;
const CALL_PUSH_PENDING_ACTION_KEY = 'shine-ui-call-push-pending-action-v1';
const GUEST_ALLOWED_PAGES = new Set([
'start-view',
@@ -204,6 +207,86 @@ initPwaInstallPromptHandling();
initCallUiOverlay();
setCallDebugReporter((payload) => authService.reportClientDebug(payload));
+function setShellMetricVar(name, valuePx) {
+ if (!appShellEl) return;
+ appShellEl.style.setProperty(name, `${Math.max(0, Math.ceil(Number(valuePx || 0)))}px`);
+}
+
+function attachSlotHeightObserver(slotEl, cssVarName) {
+ if (!slotEl || typeof ResizeObserver !== 'function') return null;
+ const sync = () => {
+ const visible = !slotEl.hidden && slotEl.childElementCount > 0;
+ setShellMetricVar(cssVarName, visible ? slotEl.offsetHeight : 0);
+ };
+ const observer = new ResizeObserver(sync);
+ observer.observe(slotEl);
+ sync();
+ return { observer, sync };
+}
+
+const topbarHeightObserver = attachSlotHeightObserver(topbarEl, '--topbar-height');
+const composerHeightObserver = attachSlotHeightObserver(composerEl, '--composer-height');
+const toolbarHeightObserver = attachSlotHeightObserver(toolbarEl, '--toolbar-height');
+
+function clearSlot(slotEl, cssVarName) {
+ if (!slotEl) return;
+ slotEl.innerHTML = '';
+ slotEl.hidden = true;
+ setShellMetricVar(cssVarName, 0);
+}
+
+function mountSlot(slotEl, cssVarName, node) {
+ if (!slotEl) return;
+ slotEl.innerHTML = '';
+ if (node instanceof Node) {
+ slotEl.append(node);
+ slotEl.hidden = false;
+ } else {
+ slotEl.hidden = true;
+ }
+ setShellMetricVar(cssVarName, !slotEl.hidden ? slotEl.offsetHeight : 0);
+}
+
+function createChromeController(showAppChrome) {
+ let topbarNode = null;
+ let composerNode = null;
+ let disposed = false;
+
+ const apply = () => {
+ if (disposed) return;
+ if (!showAppChrome) {
+ clearSlot(topbarEl, '--topbar-height');
+ clearSlot(composerEl, '--composer-height');
+ return;
+ }
+ mountSlot(topbarEl, '--topbar-height', topbarNode);
+ mountSlot(composerEl, '--composer-height', composerNode);
+ topbarHeightObserver?.sync?.();
+ composerHeightObserver?.sync?.();
+ };
+
+ return {
+ setTopbar(node = null) {
+ topbarNode = node instanceof Node ? node : null;
+ apply();
+ },
+ setComposer(node = null) {
+ composerNode = node instanceof Node ? node : null;
+ apply();
+ },
+ clear() {
+ topbarNode = null;
+ composerNode = null;
+ apply();
+ },
+ dispose() {
+ disposed = true;
+ clearSlot(topbarEl, '--topbar-height');
+ clearSlot(composerEl, '--composer-height');
+ },
+ };
+}
+
async function unlockHiddenDmAudio() {
try {
const Ctx = window.AudioContext || window.webkitAudioContext;
@@ -908,7 +991,14 @@ function renderPageFailureFallback(pageId, error) {
screenEl.append(wrap);
screenEl.classList.toggle('no-app-chrome', false);
+ if (typeof currentChromeCleanup === 'function') {
+ currentChromeCleanup();
+ currentChromeCleanup = null;
+ }
+ clearSlot(topbarEl, '--topbar-height');
+ clearSlot(composerEl, '--composer-height');
toolbarEl.innerHTML = '';
+ toolbarHeightObserver?.sync?.();
refreshConnectionUi();
}
@@ -932,10 +1022,17 @@ function renderApp() {
currentCleanup();
currentCleanup = null;
}
+ if (typeof currentChromeCleanup === 'function') {
+ currentChromeCleanup();
+ currentChromeCleanup = null;
+ }
try {
screenEl.innerHTML = '';
- const screen = page.render({ route, navigate });
+ const showAppChrome = page.pageMeta?.showAppChrome !== false;
+ const chrome = createChromeController(showAppChrome);
+ currentChromeCleanup = () => chrome.dispose();
+ const screen = page.render({ route, navigate, chrome });
if (!(screen instanceof Node)) {
throw new Error('Page render returned invalid node');
}
@@ -943,7 +1040,6 @@ function renderApp() {
screenEl.append(screen);
currentCleanup = typeof screen.cleanup === 'function' ? screen.cleanup : null;
- const showAppChrome = page.pageMeta?.showAppChrome !== false;
screenEl.classList.toggle('no-app-chrome', !showAppChrome);
screenEl.classList.toggle('preauth-flow', PRE_AUTH_PAGES.includes(pageId));
@@ -951,6 +1047,7 @@ function renderApp() {
if (showAppChrome) {
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
}
+ toolbarHeightObserver?.sync?.();
refreshConnectionUi();
} catch (error) {
console.error('[renderApp] controlled fallback', error);
@@ -968,6 +1065,7 @@ function refreshToolbarOnly() {
if (showAppChrome) {
toolbarEl.append(renderToolbar(page.pageMeta.id, navigate));
}
+ toolbarHeightObserver?.sync?.();
refreshConnectionUi();
}
diff --git a/shine-UI/js/pages/channel-thread-view.js b/shine-UI/js/pages/channel-thread-view.js
index cefcea25..8d68578d 100644
--- a/shine-UI/js/pages/channel-thread-view.js
+++ b/shine-UI/js/pages/channel-thread-view.js
@@ -1114,7 +1114,7 @@ function renderSkeleton(screen) {
return wrap;
}
-export function render({ navigate, route }) {
+export function render({ navigate, route, chrome }) {
const selector = parseThreadSelector(route);
const channelDisplayName = resolveChannelDisplayName(selector?.channel);
const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;
@@ -1134,6 +1134,7 @@ export function render({ navigate, route }) {
threadHeaderButton.classList.add('channel-header-route-btn');
threadHeaderButton.disabled = true;
}
+ chrome?.setTopbar(header);
const statusBox = document.createElement('div');
statusBox.className = 'card status-line is-unavailable channels-status';
@@ -1309,7 +1310,7 @@ export function render({ navigate, route }) {
},
};
- screen.append(header, statusBox);
+ screen.append(statusBox);
if (!selector) {
const invalid = document.createElement('div');
diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js
index c4bd8ef7..699c441b 100644
--- a/shine-UI/js/pages/channel-view.js
+++ b/shine-UI/js/pages/channel-view.js
@@ -2008,7 +2008,7 @@ function renderSkeleton(screen) {
return wrap;
}
-export function render({ navigate, route }) {
+export function render({ navigate, route, chrome }) {
const channelId = route.params.channelId || '';
const routeSelector = buildSelectorFromRoute(route, channelId);
const routeKey = `${routeSelector?.ownerBlockchainName || ''}:${routeSelector?.channelRootBlockNumber || ''}:${routeSelector?.channelRootBlockHash || ''}`;
@@ -2049,6 +2049,7 @@ export function render({ navigate, route }) {
channelEntrypointButton.disabled = true;
channelEntrypointButton.hidden = true;
}
+ chrome?.setTopbar(header);
const rerender = () => {
const current = document.querySelector('section.channels-screen--channel');
@@ -2282,7 +2283,6 @@ export function render({ navigate, route }) {
rerender();
};
- screen.append(header);
screen.append(statusBox);
const skeleton = renderSkeleton(screen);
diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js
index 93a50226..49ab2531 100644
--- a/shine-UI/js/pages/channels-list.js
+++ b/shine-UI/js/pages/channels-list.js
@@ -1213,7 +1213,7 @@ async function loadFeedAndRender({ screen, listState, contentEl, navigate }) {
}
}
-export function render({ navigate, route }) {
+export function render({ navigate, route, chrome }) {
const screen = document.createElement('section');
screen.className = 'stack channels-screen channels-screen--list';
const appScreen = document.getElementById('app-screen');
@@ -1310,7 +1310,8 @@ export function render({ navigate, route }) {
updateBottomCta({ button: bottomCta });
};
- screen.append(topBarEl, contentEl, bottomCta);
+ chrome?.setTopbar(topBarEl);
+ screen.append(contentEl, bottomCta);
if (createSuccessFlash) {
showToast(createSuccessFlash);
diff --git a/shine-UI/js/pages/chat-view.js b/shine-UI/js/pages/chat-view.js
index 9962ce1d..c6d921ea 100644
--- a/shine-UI/js/pages/chat-view.js
+++ b/shine-UI/js/pages/chat-view.js
@@ -785,7 +785,7 @@ function setChatKeyboardOpen(isOpen) {
document.body.classList.toggle('chat-keyboard-open', !!isOpen);
}
-export function render({ navigate, route }) {
+export function render({ navigate, route, chrome }) {
const routeChatId = route.params.chatId || 'u1';
const chatId = normalizeDmChatId(routeChatId) || 'u1';
const contact = directMessages.find((d) => normalizeDmChatId(d.id) === chatId) || {
@@ -871,7 +871,7 @@ export function render({ navigate, route }) {
const log = document.createElement('div');
log.className = 'messages-log dm-messages-log';
- screen.append(
+ chrome?.setTopbar(
renderHeader({
title: `Чат с ${contact.name}`,
leftAction: { label: '←', onClick: () => navigate('messages-list') },
@@ -941,7 +941,7 @@ export function render({ navigate, route }) {
},
},
],
- })
+ }),
);
if (!isKnownContact) {
@@ -1490,7 +1490,8 @@ export function render({ navigate, route }) {
window.visualViewport?.addEventListener('resize', syncKeyboardUi);
window.addEventListener('resize', syncKeyboardUi);
- wrap.append(historyLoader, log, form);
+ chrome?.setComposer(form);
+ wrap.append(historyLoader, log);
screen.append(wrap);
renderLog(log, chatId, {
onOpenActions: handleOpenActions,
diff --git a/shine-UI/js/pages/messages-list.js b/shine-UI/js/pages/messages-list.js
index 620d7ef3..4bd24af5 100644
--- a/shine-UI/js/pages/messages-list.js
+++ b/shine-UI/js/pages/messages-list.js
@@ -84,7 +84,7 @@ function formatChatRowTime(ts) {
const SVG_CHEVRON = '';
-export function render({ navigate }) {
+export function render({ navigate, chrome }) {
const screen = document.createElement('section');
screen.className = 'stack dm-screen dm-list-screen';
const login = String(state.session.login || '').trim();
@@ -248,7 +248,8 @@ export function render({ navigate }) {
}
}
- screen.append(head, divider, list);
+ chrome?.setTopbar(head);
+ screen.append(divider, list);
loadList();
return screen;
}
diff --git a/shine-UI/js/pages/profile-edit-view.js b/shine-UI/js/pages/profile-edit-view.js
index b86ac20c..fad1994e 100644
--- a/shine-UI/js/pages/profile-edit-view.js
+++ b/shine-UI/js/pages/profile-edit-view.js
@@ -87,13 +87,13 @@ function escapeHtml(text) {
.replaceAll("'", ''');
}
-export function render({ navigate }) {
+export function render({ navigate, chrome }) {
const login = state.session.login || profile.login;
const screen = document.createElement('section');
screen.className = 'stack profile-screen';
- screen.append(
+ chrome?.setTopbar(
renderHeader({
title: 'Редактирование профиля',
leftAction: { label: '←', onClick: () => navigate('profile-view') },
diff --git a/shine-UI/js/pages/profile-view.js b/shine-UI/js/pages/profile-view.js
index 4682f5bf..16661c25 100644
--- a/shine-UI/js/pages/profile-view.js
+++ b/shine-UI/js/pages/profile-view.js
@@ -92,28 +92,34 @@ function shineInfoText() {
+ '5) сияющие заботятся о мире: о людях, гармонии и общем благе.';
}
-export function render({ navigate }) {
+export function render({ navigate, chrome }) {
const login = state.session.login || profile.login;
const screen = document.createElement('section');
screen.className = 'stack profile-screen';
- const topActions = document.createElement('div');
- topActions.className = 'profile-top-actions';
- topActions.innerHTML = `
-
-
-
+ const topbar = document.createElement('header');
+ topbar.className = 'page-header app-topbar-shell app-topbar-shell--profile';
+ topbar.innerHTML = `
+
+ Профиль пользователя
+
`;
+ const topActions = topbar.querySelector('.profile-top-actions');
topActions.querySelector('[data-top-action="profile"]')?.addEventListener('click', () => navigate('profile-edit-view'));
- topActions.querySelector('[data-top-action="wallet"]')?.addEventListener('click', () => navigate('wallet'));
+ topActions.querySelector('[data-top-action="wallet"]')?.addEventListener('click', () => navigate('wallet-view'));
topActions.querySelector('[data-top-action="settings"]')?.addEventListener('click', () => navigate('settings-view'));
+ chrome?.setTopbar(topbar);
const card = document.createElement('div');
card.className = 'card stack profile-main-card';
@@ -264,7 +270,7 @@ export function render({ navigate }) {
}
}
- card.append(topActions, topRow, badgesRow, listWrap);
+ card.append(topRow, badgesRow, listWrap);
screen.append(card);
updateAvatarUi();
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 8f18cbad..80391fa9 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -13,6 +13,37 @@
padding-top: max(0px, env(safe-area-inset-top));
}
+.topbar-slot .page-header,
+.topbar-slot .dm-head,
+.topbar-slot .channels-top-bar {
+ margin-bottom: 0;
+ border-radius: 18px;
+ background: rgba(14, 21, 35, 0.92);
+ border: 1px solid rgba(212, 175, 55, 0.18);
+ box-shadow: 0 10px 26px rgba(4, 8, 16, 0.28);
+ backdrop-filter: blur(14px);
+}
+
+.topbar-slot .page-header {
+ padding: calc(10px + env(safe-area-inset-top)) 12px 10px;
+}
+
+.topbar-slot .dm-head,
+.topbar-slot .channels-top-bar {
+ padding-top: calc(10px + env(safe-area-inset-top));
+ padding-bottom: 10px;
+}
+
+.app-topbar-shell .page-title {
+ flex: 1 1 auto;
+ text-align: center;
+}
+
+.app-topbar-shell .header-left,
+.app-topbar-shell .header-actions {
+ min-width: 92px;
+}
+
.page-title {
font-size: 22px;
font-weight: 700;
@@ -1561,7 +1592,7 @@
}
.toolbar-btn-profile .toolbar-label-wrap {
- padding-bottom: 6px;
+ padding-bottom: 0;
}
.toolbar-btn-profile .toolbar-connection-indicator {
diff --git a/shine-UI/styles/layout.css b/shine-UI/styles/layout.css
index 34448a7c..77257890 100644
--- a/shine-UI/styles/layout.css
+++ b/shine-UI/styles/layout.css
@@ -25,6 +25,9 @@ body::before {
height: 100dvh;
position: relative;
--call-minimized-bar-height: 0px;
+ --topbar-height: 0px;
+ --composer-height: 0px;
+ --toolbar-height: 78px;
background: transparent;
border-left: 1px solid rgba(211, 170, 86, 0.2);
border-right: 1px solid rgba(211, 170, 86, 0.2);
@@ -33,6 +36,8 @@ body::before {
}
.screen-content,
+.topbar-slot,
+.composer-slot,
.toolbar-slot,
.connection-retry-banner {
z-index: 1;
@@ -40,15 +45,16 @@ body::before {
.screen-content {
position: absolute;
- top: var(--call-minimized-bar-height, 0px);
+ top: calc(var(--call-minimized-bar-height, 0px) + var(--topbar-height, 0px));
left: 0;
right: 0;
- bottom: 62px;
+ bottom: calc(var(--toolbar-height, 78px) + var(--composer-height, 0px));
overflow-y: auto;
padding: 14px 14px 24px;
}
.screen-content.no-app-chrome {
+ top: var(--call-minimized-bar-height, 0px);
bottom: 0;
padding-bottom: calc(24px + env(safe-area-inset-bottom));
}
@@ -62,6 +68,36 @@ body::before {
padding: 0;
}
+.topbar-slot[hidden],
+.composer-slot[hidden] {
+ display: none;
+}
+
+.topbar-slot {
+ position: absolute;
+ top: var(--call-minimized-bar-height, 0px);
+ left: 0;
+ right: 0;
+ padding: 0 12px;
+ pointer-events: none;
+}
+
+.topbar-slot > * {
+ pointer-events: auto;
+}
+
+.composer-slot {
+ position: absolute;
+ left: 0;
+ right: 0;
+ bottom: calc(var(--toolbar-height, 78px) + env(safe-area-inset-bottom));
+ padding: 0 12px 8px;
+ pointer-events: none;
+}
+
+.composer-slot > * {
+ pointer-events: auto;
+}
.toolbar-slot {
position: absolute;
@@ -74,10 +110,15 @@ body::before {
}
body.chat-keyboard-open .screen-content {
- bottom: 0;
+ bottom: var(--composer-height, 0px);
padding-bottom: calc(14px + env(safe-area-inset-bottom));
}
+body.chat-keyboard-open .composer-slot {
+ bottom: 0;
+ padding-bottom: calc(8px + env(safe-area-inset-bottom));
+}
+
body.chat-keyboard-open .toolbar-slot {
opacity: 0;
pointer-events: none;