Доработать верхнее меню списка каналов

This commit is contained in:
AidarKC
2026-08-12 17:12:57 +04:00
parent 8f32e82d14
commit b4b23afc10
3 changed files with 169 additions and 16 deletions
+160 -14
View File
@@ -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');
};
+8 -1
View File
@@ -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;