SHA256
Доработать верхнее меню списка каналов
This commit is contained in:
+1
-1
@@ -1,2 +1,2 @@
|
|||||||
client.version=1.5.37
|
client.version=1.5.38
|
||||||
server.version=1.4.12
|
server.version=1.4.12
|
||||||
|
|||||||
@@ -20,11 +20,16 @@ export const pageMeta = { id: 'channels-list', title: 'Каналы' };
|
|||||||
|
|
||||||
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
|
||||||
const MENU_OVERLAY_ID = 'channels-context-menu-overlay';
|
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_STORIES = 0;
|
||||||
const CHANNEL_TYPE_PERSONAL = 100;
|
const CHANNEL_TYPE_PERSONAL = 100;
|
||||||
const DIARY_CHANNEL_NAME = 'diary';
|
const DIARY_CHANNEL_NAME = 'diary';
|
||||||
const DIARY_DISPLAY_NAME = 'Дневник';
|
const DIARY_DISPLAY_NAME = 'Дневник';
|
||||||
|
|
||||||
|
const CHANNELS_VIEW_ALL = 'all';
|
||||||
|
const CHANNELS_VIEW_OWNED = 'owned';
|
||||||
|
const CHANNELS_VIEW_FOLLOWING = 'following';
|
||||||
|
|
||||||
function cleanChannelMessagePreview(text) {
|
function cleanChannelMessagePreview(text) {
|
||||||
const parsed = parseMessageAttachments(text);
|
const parsed = parseMessageAttachments(text);
|
||||||
const dmParsed = parseDmTechBlocks(String(parsed.text || ''));
|
const dmParsed = parseDmTechBlocks(String(parsed.text || ''));
|
||||||
@@ -228,6 +233,26 @@ function normalizeComparableLogin(value) {
|
|||||||
return normalizeLoginInput(value).toLowerCase();
|
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) {
|
function isFollowedUserVisible(targetLogin) {
|
||||||
const expected = normalizeComparableLogin(targetLogin);
|
const expected = normalizeComparableLogin(targetLogin);
|
||||||
if (!expected) return false;
|
if (!expected) return false;
|
||||||
@@ -617,6 +642,11 @@ function openChannelFinderModal({ navigate }) {
|
|||||||
function mapMockGroups() {
|
function mapMockGroups() {
|
||||||
const mapRow = (channel) => ({
|
const mapRow = (channel) => ({
|
||||||
...channel,
|
...channel,
|
||||||
|
sourceBucket: channel.kind === 'subscribed'
|
||||||
|
? 'followedChannels'
|
||||||
|
: channel.kind === 'followed-user-channel'
|
||||||
|
? 'followedUsers'
|
||||||
|
: 'own',
|
||||||
route: makeShineChannelRoute({
|
route: makeShineChannelRoute({
|
||||||
ownerLogin: String(channel.ownerName || 'channel'),
|
ownerLogin: String(channel.ownerName || 'channel'),
|
||||||
ownerBlockchainName: String(channel.ownerName || ''),
|
ownerBlockchainName: String(channel.ownerName || ''),
|
||||||
@@ -668,6 +698,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: rowId,
|
id: rowId,
|
||||||
|
sourceBucket: bucketKey,
|
||||||
route: buildChannelRouteFromSummary(summary, rowId),
|
route: buildChannelRouteFromSummary(summary, rowId),
|
||||||
ownerName: ownerLogin,
|
ownerName: ownerLogin,
|
||||||
ownerBlockchainName: summary?.channel?.ownerBlockchainName || '',
|
ownerBlockchainName: summary?.channel?.ownerBlockchainName || '',
|
||||||
@@ -721,6 +752,7 @@ function buildDiaryChannelRow(diaryPayload, ownRows = [], notificationsState = {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
id: rowId,
|
id: rowId,
|
||||||
|
sourceBucket: 'own',
|
||||||
route: makeShineChannelRoute({
|
route: makeShineChannelRoute({
|
||||||
ownerLogin,
|
ownerLogin,
|
||||||
ownerBlockchainName,
|
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 }) {
|
function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderList }) {
|
||||||
closeChannelMenu(listState, false);
|
closeChannelMenu(listState, false);
|
||||||
|
|
||||||
@@ -1060,7 +1184,11 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
|
|||||||
container.innerHTML = '';
|
container.innerHTML = '';
|
||||||
|
|
||||||
const allChannels = listState.channels || [];
|
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) {
|
if (!filtered.length) {
|
||||||
container.append(renderEmptyState());
|
container.append(renderEmptyState());
|
||||||
@@ -1228,10 +1356,12 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const isGuest = !state.session.isAuthorized;
|
const isGuest = !state.session.isAuthorized;
|
||||||
const listState = {
|
const listState = {
|
||||||
openMenuId: null,
|
openMenuId: null,
|
||||||
|
topMenuCleanup: null,
|
||||||
notificationsState,
|
notificationsState,
|
||||||
revealedCounters: new Set(),
|
revealedCounters: new Set(),
|
||||||
channels: [],
|
channels: [],
|
||||||
menuCleanup: null,
|
menuCleanup: null,
|
||||||
|
viewMode: normalizeChannelsViewMode(route),
|
||||||
};
|
};
|
||||||
|
|
||||||
const contentEl = document.createElement('div');
|
const contentEl = document.createElement('div');
|
||||||
@@ -1274,8 +1404,31 @@ export function render({ navigate, route, chrome }) {
|
|||||||
createInMyBtn.setAttribute('aria-label', 'Создать канал');
|
createInMyBtn.setAttribute('aria-label', 'Создать канал');
|
||||||
createInMyBtn.addEventListener('click', () => navigate('add-channel-view'));
|
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);
|
topBarLeft.append(backBtn, topTitle);
|
||||||
topBarRight.append(findChannelBtn, createInMyBtn);
|
topBarRight.append(findChannelBtn, createInMyBtn, topMenuBtn);
|
||||||
topBarEl.append(topBarLeft, topBarRight);
|
topBarEl.append(topBarLeft, topBarRight);
|
||||||
|
|
||||||
const bottomCta = document.createElement('button');
|
const bottomCta = document.createElement('button');
|
||||||
@@ -1284,18 +1437,9 @@ export function render({ navigate, route, chrome }) {
|
|||||||
const reloadFeed = async () => loadFeedAndRender({ screen, listState, contentEl, navigate });
|
const reloadFeed = async () => loadFeedAndRender({ screen, listState, contentEl, navigate });
|
||||||
|
|
||||||
const rerenderList = () => {
|
const rerenderList = () => {
|
||||||
try {
|
listState.viewMode = normalizeChannelsViewMode({ params: route?.params || {} });
|
||||||
const expectedPath = '/channels';
|
|
||||||
if (window.location.pathname !== expectedPath) {
|
|
||||||
window.history.replaceState({}, '', expectedPath);
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore history errors
|
|
||||||
}
|
|
||||||
|
|
||||||
const isTabEmpty = !(listState.channels || []).length;
|
|
||||||
|
|
||||||
closeChannelMenu(listState);
|
closeChannelMenu(listState);
|
||||||
|
closeTopChannelsMenu(listState);
|
||||||
|
|
||||||
renderListContent({
|
renderListContent({
|
||||||
screen,
|
screen,
|
||||||
@@ -1305,9 +1449,10 @@ export function render({ navigate, route, chrome }) {
|
|||||||
refreshFeed: reloadFeed,
|
refreshFeed: reloadFeed,
|
||||||
});
|
});
|
||||||
|
|
||||||
topTitle.textContent = 'Каналы';
|
topTitle.textContent = channelsViewTitle(listState.viewMode);
|
||||||
findChannelBtn.style.display = '';
|
findChannelBtn.style.display = '';
|
||||||
createInMyBtn.style.display = '';
|
createInMyBtn.style.display = '';
|
||||||
|
topMenuBtn.style.display = '';
|
||||||
if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
|
if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
|
||||||
|
|
||||||
updateBottomCta({ button: bottomCta });
|
updateBottomCta({ button: bottomCta });
|
||||||
@@ -1329,6 +1474,7 @@ export function render({ navigate, route, chrome }) {
|
|||||||
|
|
||||||
screen.cleanup = () => {
|
screen.cleanup = () => {
|
||||||
closeChannelMenu(listState);
|
closeChannelMenu(listState);
|
||||||
|
closeTopChannelsMenu(listState);
|
||||||
appScreen?.classList.remove('channels-scroll-clean');
|
appScreen?.classList.remove('channels-scroll-clean');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5065,7 +5065,8 @@ textarea.input {
|
|||||||
|
|
||||||
.channels-top-back-btn,
|
.channels-top-back-btn,
|
||||||
.channels-top-add-btn,
|
.channels-top-add-btn,
|
||||||
.channels-top-search-btn {
|
.channels-top-search-btn,
|
||||||
|
.channels-top-more-btn {
|
||||||
width: 36px;
|
width: 36px;
|
||||||
height: 36px;
|
height: 36px;
|
||||||
min-width: 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);
|
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 {
|
.channels-search-icon {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: block;
|
display: block;
|
||||||
|
|||||||
Reference in New Issue
Block a user