SHA256
Доработать верхнее меню списка каналов
This commit is contained in:
@@ -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');
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user