From b4b23afc1045fe99dd1b4df376a1b8e1bb20c86d817c8bab36ad0c524b579e86 Mon Sep 17 00:00:00 2001 From: AidarKC Date: Wed, 12 Aug 2026 17:12:57 +0400 Subject: [PATCH] =?UTF-8?q?=D0=94=D0=BE=D1=80=D0=B0=D0=B1=D0=BE=D1=82?= =?UTF-8?q?=D0=B0=D1=82=D1=8C=20=D0=B2=D0=B5=D1=80=D1=85=D0=BD=D0=B5=D0=B5?= =?UTF-8?q?=20=D0=BC=D0=B5=D0=BD=D1=8E=20=D1=81=D0=BF=D0=B8=D1=81=D0=BA?= =?UTF-8?q?=D0=B0=20=D0=BA=D0=B0=D0=BD=D0=B0=D0=BB=D0=BE=D0=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- VERSION.properties | 2 +- shine-UI/js/pages/channels-list.js | 174 ++++++++++++++++++++++++++--- shine-UI/styles/components.css | 9 +- 3 files changed, 169 insertions(+), 16 deletions(-) diff --git a/VERSION.properties b/VERSION.properties index 0b4bf2ae..3edb3292 100644 --- a/VERSION.properties +++ b/VERSION.properties @@ -1,2 +1,2 @@ -client.version=1.5.37 +client.version=1.5.38 server.version=1.4.12 diff --git a/shine-UI/js/pages/channels-list.js b/shine-UI/js/pages/channels-list.js index 2d7b932c..df1ef38f 100644 --- a/shine-UI/js/pages/channels-list.js +++ b/shine-UI/js/pages/channels-list.js @@ -20,11 +20,16 @@ export const pageMeta = { id: 'channels-list', title: 'Каналы' }; const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success'; const MENU_OVERLAY_ID = 'channels-context-menu-overlay'; +const TOP_MENU_OVERLAY_ID = 'channels-top-menu-overlay'; const CHANNEL_TYPE_STORIES = 0; const CHANNEL_TYPE_PERSONAL = 100; const DIARY_CHANNEL_NAME = 'diary'; const DIARY_DISPLAY_NAME = 'Дневник'; +const CHANNELS_VIEW_ALL = 'all'; +const CHANNELS_VIEW_OWNED = 'owned'; +const CHANNELS_VIEW_FOLLOWING = 'following'; + function cleanChannelMessagePreview(text) { const parsed = parseMessageAttachments(text); const dmParsed = parseDmTechBlocks(String(parsed.text || '')); @@ -228,6 +233,26 @@ function normalizeComparableLogin(value) { return normalizeLoginInput(value).toLowerCase(); } +function normalizeChannelsViewMode(route = null) { + const mode = String(route?.params?.mode || '').trim().toLowerCase(); + const scope = String(route?.params?.scope || '').trim().toLowerCase(); + if (mode === 'my' || scope === 'owned') return CHANNELS_VIEW_OWNED; + if (mode === 'following' || scope === 'following') return CHANNELS_VIEW_FOLLOWING; + return CHANNELS_VIEW_ALL; +} + +function buildChannelsViewRoute(mode) { + if (mode === CHANNELS_VIEW_OWNED) return 'channels/my'; + if (mode === CHANNELS_VIEW_FOLLOWING) return 'channels/following'; + return 'channels'; +} + +function channelsViewTitle(mode) { + if (mode === CHANNELS_VIEW_OWNED) return 'Мои каналы'; + if (mode === CHANNELS_VIEW_FOLLOWING) return 'Подписки'; + return 'Каналы'; +} + function isFollowedUserVisible(targetLogin) { const expected = normalizeComparableLogin(targetLogin); if (!expected) return false; @@ -617,6 +642,11 @@ function openChannelFinderModal({ navigate }) { function mapMockGroups() { const mapRow = (channel) => ({ ...channel, + sourceBucket: channel.kind === 'subscribed' + ? 'followedChannels' + : channel.kind === 'followed-user-channel' + ? 'followedUsers' + : 'own', route: makeShineChannelRoute({ ownerLogin: String(channel.ownerName || 'channel'), ownerBlockchainName: String(channel.ownerName || ''), @@ -668,6 +698,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) { return { id: rowId, + sourceBucket: bucketKey, route: buildChannelRouteFromSummary(summary, rowId), ownerName: ownerLogin, ownerBlockchainName: summary?.channel?.ownerBlockchainName || '', @@ -721,6 +752,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = { return { id: rowId, + sourceBucket: 'own', route: makeShineChannelRoute({ ownerLogin, ownerBlockchainName, @@ -891,6 +923,98 @@ function closeChannelMenu(listState, clearOpenMenuId = true) { } } +function closeTopChannelsMenu(listState) { + if (typeof listState.topMenuCleanup === 'function') { + listState.topMenuCleanup(); + } + listState.topMenuCleanup = null; + const root = document.getElementById('modal-root'); + if (root) { + const overlay = root.querySelector(`#${TOP_MENU_OVERLAY_ID}`); + if (overlay) overlay.remove(); + } +} + +function openTopChannelsMenu({ + listState, + anchorEl, + navigate, + onSubscribeChannel, + onFindChannel, +}) { + closeTopChannelsMenu(listState); + const root = document.getElementById('modal-root'); + if (!root || !anchorEl) return; + + const rect = anchorEl.getBoundingClientRect(); + const menuWidth = Math.min(280, Math.max(220, window.innerWidth - 28)); + let left = rect.right - menuWidth; + left = Math.max(12, Math.min(left, window.innerWidth - menuWidth - 12)); + + const estimatedHeight = 320; + let top = rect.bottom + 8; + if (top + estimatedHeight > window.innerHeight - 10) { + top = Math.max(12, rect.top - estimatedHeight - 8); + } + + const overlay = document.createElement('div'); + overlay.id = TOP_MENU_OVERLAY_ID; + overlay.className = 'channels-menu-overlay'; + + const menu = document.createElement('div'); + menu.className = 'channel-menu-wrap channel-menu-wrap--portal'; + menu.style.left = `${Math.round(left)}px`; + menu.style.top = `${Math.round(top)}px`; + menu.style.width = `${Math.round(menuWidth)}px`; + + const items = [ + { label: 'Все каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_ALL)) }, + { label: 'Мои каналы', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_OWNED)) }, + { label: 'Подписки', action: () => navigate(buildChannelsViewRoute(CHANNELS_VIEW_FOLLOWING)) }, + { divider: true }, + { label: 'Новый канал', action: () => navigate('add-channel-view') }, + { label: 'Создать канал', action: () => onSubscribeChannel?.() }, + { divider: true }, + { label: 'Просмотреть канал', action: () => onFindChannel?.() }, + ]; + + items.forEach((item) => { + if (item.divider) { + const divider = document.createElement('div'); + divider.className = 'channel-menu-divider'; + divider.style.height = '1px'; + divider.style.background = 'rgba(255,255,255,0.08)'; + divider.style.margin = '6px 0'; + menu.append(divider); + return; + } + const btn = document.createElement('button'); + btn.type = 'button'; + btn.className = 'channel-menu-item'; + btn.textContent = item.label; + btn.addEventListener('click', () => { + closeTopChannelsMenu(listState); + item.action?.(); + }); + menu.append(btn); + }); + + overlay.append(menu); + root.append(overlay); + + const onOverlayClick = (event) => { + if (event.target === overlay) closeTopChannelsMenu(listState); + }; + const onWindowResize = () => closeTopChannelsMenu(listState); + + overlay.addEventListener('click', onOverlayClick); + window.addEventListener('resize', onWindowResize); + listState.topMenuCleanup = () => { + overlay.removeEventListener('click', onOverlayClick); + window.removeEventListener('resize', onWindowResize); + }; +} + function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderList }) { closeChannelMenu(listState, false); @@ -1060,7 +1184,11 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed container.innerHTML = ''; const allChannels = listState.channels || []; - const filtered = allChannels; + const filtered = allChannels.filter((channel) => { + if (listState.viewMode === CHANNELS_VIEW_OWNED) return channel.isOwnChannel === true; + if (listState.viewMode === CHANNELS_VIEW_FOLLOWING) return channel.sourceBucket === 'followedChannels'; + return true; + }); if (!filtered.length) { container.append(renderEmptyState()); @@ -1228,10 +1356,12 @@ export function render({ navigate, route, chrome }) { const isGuest = !state.session.isAuthorized; const listState = { openMenuId: null, + topMenuCleanup: null, notificationsState, revealedCounters: new Set(), channels: [], menuCleanup: null, + viewMode: normalizeChannelsViewMode(route), }; const contentEl = document.createElement('div'); @@ -1274,8 +1404,31 @@ export function render({ navigate, route, chrome }) { createInMyBtn.setAttribute('aria-label', 'Создать канал'); createInMyBtn.addEventListener('click', () => navigate('add-channel-view')); + const topMenuBtn = document.createElement('button'); + topMenuBtn.type = 'button'; + topMenuBtn.className = 'icon-btn channels-top-more-btn'; + topMenuBtn.setAttribute('aria-label', 'Ещё действия'); + topMenuBtn.title = 'Ещё действия'; + topMenuBtn.textContent = '⋮'; + topMenuBtn.addEventListener('click', (event) => { + event.stopPropagation(); + animatePress(topMenuBtn); + openTopChannelsMenu({ + listState, + anchorEl: topMenuBtn, + navigate, + onFindChannel: () => openChannelFinderModal({ navigate }), + onSubscribeChannel: () => openSimpleSubscribeModal({ + kind: 'channel', + kindLabel: 'Добавить канал', + submitLabel: 'Добавить', + onSuccess: async () => loadFeedAndRender({ screen, listState, contentEl, navigate }), + }), + }); + }); + topBarLeft.append(backBtn, topTitle); - topBarRight.append(findChannelBtn, createInMyBtn); + topBarRight.append(findChannelBtn, createInMyBtn, topMenuBtn); topBarEl.append(topBarLeft, topBarRight); const bottomCta = document.createElement('button'); @@ -1284,18 +1437,9 @@ export function render({ navigate, route, chrome }) { const reloadFeed = async () => loadFeedAndRender({ screen, listState, contentEl, navigate }); const rerenderList = () => { - try { - const expectedPath = '/channels'; - if (window.location.pathname !== expectedPath) { - window.history.replaceState({}, '', expectedPath); - } - } catch { - // ignore history errors - } - - const isTabEmpty = !(listState.channels || []).length; - + listState.viewMode = normalizeChannelsViewMode({ params: route?.params || {} }); closeChannelMenu(listState); + closeTopChannelsMenu(listState); renderListContent({ screen, @@ -1305,9 +1449,10 @@ export function render({ navigate, route, chrome }) { refreshFeed: reloadFeed, }); - topTitle.textContent = 'Каналы'; + topTitle.textContent = channelsViewTitle(listState.viewMode); findChannelBtn.style.display = ''; createInMyBtn.style.display = ''; + topMenuBtn.style.display = ''; if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle); updateBottomCta({ button: bottomCta }); @@ -1329,6 +1474,7 @@ export function render({ navigate, route, chrome }) { screen.cleanup = () => { closeChannelMenu(listState); + closeTopChannelsMenu(listState); appScreen?.classList.remove('channels-scroll-clean'); }; diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css index 7df32a6a..8452c55b 100644 --- a/shine-UI/styles/components.css +++ b/shine-UI/styles/components.css @@ -5065,7 +5065,8 @@ textarea.input { .channels-top-back-btn, .channels-top-add-btn, -.channels-top-search-btn { +.channels-top-search-btn, +.channels-top-more-btn { width: 36px; height: 36px; min-width: 36px; @@ -5089,6 +5090,12 @@ textarea.input { box-shadow: inset 0 0 0 1px rgba(214, 249, 255, 0.2), 0 0 18px rgba(93, 211, 255, 0.38); } +.channels-top-more-btn { + font-size: 20px; + line-height: 1; + padding-bottom: 1px; +} + .channels-search-icon { position: relative; display: block;