Обновить список каналов и кнопку сообщения

This commit is contained in:
AidarKC
2026-06-24 14:59:08 +04:00
parent 77f5759d60
commit f9a15ab192
7 changed files with 146 additions and 188 deletions
+52 -140
View File
@@ -19,7 +19,6 @@ const CREATE_CHANNEL_FLASH_KEY = 'shine-channels-create-success';
const MENU_OVERLAY_ID = 'channels-context-menu-overlay';
const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PERSONAL = 100;
const TAB_ORDER = ['feed', 'my'];
function isChannelsDemoMode() {
try {
@@ -55,6 +54,20 @@ function buildChannelRouteFromSummary(summary, fallbackId) {
});
}
function isStoriesChannel(summaryOrChannel = null) {
const typeCode = Number(summaryOrChannel?.channel?.channelTypeCode ?? summaryOrChannel?.channelTypeCode ?? 1);
const channelName = String(summaryOrChannel?.channel?.channelName || summaryOrChannel?.channelName || '').trim().toLowerCase();
return typeCode === CHANNEL_TYPE_STORIES || channelName === 'stories';
}
function isVisibleChannelSummary(summary) {
if (!summary?.channel) return false;
if (isStoriesChannel(summary)) return false;
const ownerLogin = String(summary.channel.ownerLogin || '').trim();
const channelName = String(summary.channel.channelName || '').trim();
return !!ownerLogin && !!channelName;
}
function avatarLetterFromName(name = '') {
const first = Array.from(String(name || '').trim())[0] || '#';
return first.toUpperCase();
@@ -66,7 +79,7 @@ function allFeedSummaries() {
...(feed.ownedChannels || []),
...(feed.followedUsersChannels || []),
...(feed.followedChannels || []),
];
].filter(isVisibleChannelSummary);
}
function uniqueBy(items, keySelector) {
@@ -110,7 +123,7 @@ async function resolveChannelTargetFromInput(rawInput) {
}
const ownerFeed = await authService.listSubscriptionsFeed(ownerLogin, 500);
const ownChannels = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
const ownChannels = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels.filter(isVisibleChannelSummary) : [];
const matches = ownChannels.filter((item) => (
String(item?.channel?.channelName || '').trim().toLowerCase() === channelName
));
@@ -209,7 +222,9 @@ function isFollowedUserVisible(targetLogin) {
const rows = Array.isArray(state.channelsFeed?.followedUsersChannels)
? state.channelsFeed.followedUsersChannels
: [];
return rows.some((row) => normalizeComparableLogin(row?.channel?.ownerLogin) === expected);
return rows
.filter(isVisibleChannelSummary)
.some((row) => normalizeComparableLogin(row?.channel?.ownerLogin) === expected);
}
function isFollowedChannelVisible(target) {
@@ -221,7 +236,7 @@ function isFollowedChannelVisible(target) {
const expectedHash = normalizeHash(target?.rootBlockHash);
if (!expectedBch || !Number.isFinite(expectedNo)) return false;
return rows.some((row) => {
return rows.filter(isVisibleChannelSummary).some((row) => {
const rowBch = String(row?.channel?.ownerBlockchainName || '');
const rowNo = Number(row?.channel?.channelRoot?.blockNumber);
const rowHash = normalizeHash(row?.channel?.channelRoot?.blockHash);
@@ -488,7 +503,7 @@ function openChannelFinderModal({ navigate }) {
const ownerLogin = normalizeLoginInput(login);
if (!ownerLogin) return;
const feed = await authService.listSubscriptionsFeed(ownerLogin, 1000);
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels.filter(isVisibleChannelSummary) : [];
const needle = String(filterChannel || '').trim().toLowerCase();
const channels = rows
.map((item) => ({
@@ -499,7 +514,7 @@ function openChannelFinderModal({ navigate }) {
.filter((item) => !needle || item.channelName.toLowerCase().includes(needle))
.slice(0, 200)
.map((item) => ({
label: `${ownerLogin}/${item.channelName}`,
label: `${ownerLogin} / ${item.channelName}`,
ownerLogin,
ownerBlockchainName: item.ownerBlockchainName,
channelName: item.channelName,
@@ -595,9 +610,6 @@ function mapMockGroups() {
ownerBlockchainName: String(channel.ownerName || ''),
channelName: String(channel.channelName || channel.title || channel.id),
}),
tabCategory: channel.kind === 'own' || channel.kind === 'own-personal'
? 'my'
: 'feed',
messagePreview: channel.lastMessage || 'Ждем ваших начинаний',
isSubscribed: channel.kind !== 'own' && channel.kind !== 'own-personal',
isOwnChannel: channel.kind === 'own' || channel.kind === 'own-personal',
@@ -612,15 +624,18 @@ function mapMockGroups() {
const ownChannels = mockChannels
.filter((channel) => channel.kind === 'own-personal' || channel.kind === 'own')
.map((item) => ({ ...mapRow(item), tabCategory: 'my' }));
.map((item) => mapRow(item))
.filter((item) => !isStoriesChannel(item));
const followedUserChannels = mockChannels
.filter((channel) => channel.kind === 'followed-user-channel')
.map((item) => ({ ...mapRow(item), tabCategory: 'feed' }));
.map((item) => mapRow(item))
.filter((item) => !isStoriesChannel(item));
const subscribedChannels = mockChannels
.filter((channel) => channel.kind === 'subscribed')
.map((item) => ({ ...mapRow(item), tabCategory: 'feed' }));
.map((item) => mapRow(item))
.filter((item) => !isStoriesChannel(item));
return { ownChannels, followedUserChannels, subscribedChannels, index: {} };
}
@@ -635,9 +650,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
const channelTypeCode = Number(summary?.channel?.channelTypeCode ?? 1);
const channelTypeVersion = Number(summary?.channel?.channelTypeVersion ?? 1);
const isOwn = bucketKey === 'own';
const tabCategory = isOwn ? 'my' : 'feed';
const title = isOwn ? channelName : `${ownerLogin}/${channelName}`;
const title = `${ownerLogin} / ${channelName}`;
return {
id: rowId,
@@ -656,7 +669,6 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
messagesCount: Number(summary?.messagesCount || 0),
unreadCount: Number(summary?.unreadCount || 0),
lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0),
tabCategory,
isOwnChannel: isOwn,
isSubscribed: !isOwn,
notificationsEnabled: notificationsState[rowId] === true,
@@ -677,9 +689,14 @@ function pullCreateSuccessFlash() {
function mapApiFeed(feed, notificationsState) {
const index = {};
const ownChannels = (feed?.ownedChannels || [])
.filter(isVisibleChannelSummary)
.map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState));
const followedUserChannels = (feed?.followedUsersChannels || []).map((it, idx) => mapApiChannelRow(it, 'followedUsers', idx, index, notificationsState));
const subscribedChannels = (feed?.followedChannels || []).map((it, idx) => mapApiChannelRow(it, 'followedChannels', idx, index, notificationsState));
const followedUserChannels = (feed?.followedUsersChannels || [])
.filter(isVisibleChannelSummary)
.map((it, idx) => mapApiChannelRow(it, 'followedUsers', idx, index, notificationsState));
const subscribedChannels = (feed?.followedChannels || [])
.filter(isVisibleChannelSummary)
.map((it, idx) => mapApiChannelRow(it, 'followedChannels', idx, index, notificationsState));
return { ownChannels, followedUserChannels, subscribedChannels, index };
}
@@ -692,7 +709,7 @@ function toListModel(groups) {
];
}
function renderEmptyState(activeTab, navigate) {
function renderEmptyState() {
const wrap = document.createElement('div');
wrap.className = 'channels-empty-state channels-empty-state--compact channels-empty-state--silent';
if (!state.session.isAuthorized) {
@@ -700,13 +717,7 @@ function renderEmptyState(activeTab, navigate) {
}
const text = document.createElement('p');
text.className = 'meta-muted';
if (activeTab === 'feed') {
text.textContent = 'Нет подписок и найденных каналов.';
} else if (activeTab === 'my') {
text.textContent = 'У вас пока нет каналов.';
} else {
text.textContent = 'Пусто.';
}
text.textContent = 'У вас пока нет доступных каналов.';
wrap.append(text);
return wrap;
@@ -940,36 +951,15 @@ function openChannelMenu({ listState, channel, anchorEl, refreshFeed, rerenderLi
};
}
function renderChannelMain(channel, activeTab) {
function renderChannelMain(channel) {
const main = document.createElement('div');
main.className = 'channel-row-main';
if (activeTab === 'feed') {
const author = document.createElement('p');
author.className = 'channel-row-author';
author.textContent = `@${channel.ownerName}`;
const title = document.createElement('strong');
title.className = 'channel-row-title';
title.textContent = channel.channelName ? `#${channel.channelName}` : channel.title;
const preview = document.createElement('p');
preview.className = 'channel-row-message';
preview.textContent = channel.messagePreview || 'Ждем ваших начинаний';
const meta = document.createElement('p');
meta.className = 'channel-row-owner channel-counter-meta';
meta.textContent = `Сообщений: ${channel.messagesCount || 0}`;
main.append(author, title, preview, meta);
return main;
}
const title = document.createElement('strong');
title.className = 'channel-row-title';
title.textContent = activeTab === 'my' ? channel.channelName : channel.title;
title.textContent = channel.title;
if (activeTab === 'my' && channel.channelDescription) {
if (channel.channelDescription) {
const desc = document.createElement('p');
desc.className = 'channel-row-description';
desc.textContent = channel.channelDescription;
@@ -993,11 +983,10 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
container.innerHTML = '';
const allChannels = listState.channels || [];
const activeTab = listState.activeTab;
const filtered = allChannels.filter((channel) => channel.tabCategory === activeTab);
const filtered = allChannels;
if (!filtered.length) {
container.append(renderEmptyState(activeTab, navigate));
container.append(renderEmptyState());
return;
}
@@ -1016,7 +1005,7 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
avatar.className = 'avatar';
avatar.textContent = channel.avatar;
const main = renderChannelMain(channel, activeTab);
const main = renderChannelMain(channel);
const isGuest = !state.session.isAuthorized;
const controls = document.createElement('div');
@@ -1078,23 +1067,7 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
}
function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) {
const tab = listState.activeTab;
const baseClass = `primary-btn channels-bottom-action${isTabEmpty && tab !== 'my' ? ' is-empty-lift' : ''}`;
if (tab === 'feed') {
button.textContent = 'Найти канал';
button.className = baseClass;
button.onclick = () => openChannelFinderModal({ navigate });
return;
}
if (tab === 'my') {
button.textContent = 'Найти канал';
button.className = baseClass;
button.onclick = () => openChannelFinderModal({ navigate });
return;
}
const baseClass = `primary-btn channels-bottom-action${isTabEmpty ? ' is-empty-lift' : ''}`;
button.textContent = 'Поиск каналов';
button.className = baseClass;
button.onclick = () => openChannelFinderModal({ navigate });
@@ -1155,18 +1128,12 @@ export function render({ navigate, route }) {
const isGuest = !state.session.isAuthorized;
const listState = {
activeTab: TAB_ORDER.includes(String(route?.params?.mode || '').trim())
? String(route?.params?.mode).trim()
: 'feed',
openMenuId: null,
notificationsState,
revealedCounters: new Set(),
channels: [],
menuCleanup: null,
};
if (isGuest && listState.activeTab === 'my') {
listState.activeTab = 'feed';
}
const contentEl = document.createElement('div');
contentEl.className = 'channels-list-content';
@@ -1187,16 +1154,6 @@ export function render({ navigate, route }) {
const topTitle = document.createElement('strong');
topTitle.className = 'channels-top-title';
const myChannelsBtn = document.createElement('button');
myChannelsBtn.type = 'button';
myChannelsBtn.className = 'secondary-btn channels-top-switch-btn';
myChannelsBtn.textContent = 'Мои каналы';
myChannelsBtn.addEventListener('click', () => {
if (listState.activeTab === 'my') return;
listState.activeTab = 'my';
rerenderList();
});
const topBarRight = document.createElement('div');
topBarRight.className = 'channels-top-right';
@@ -1214,18 +1171,8 @@ export function render({ navigate, route }) {
createInMyBtn.setAttribute('aria-label', 'Создать канал');
createInMyBtn.addEventListener('click', () => navigate('add-channel-view'));
const switchToAllBtn = document.createElement('button');
switchToAllBtn.type = 'button';
switchToAllBtn.className = 'secondary-btn channels-top-switch-btn';
switchToAllBtn.textContent = 'Все каналы';
switchToAllBtn.addEventListener('click', () => {
if (listState.activeTab === 'feed') return;
listState.activeTab = 'feed';
rerenderList();
});
topBarLeft.append(backBtn, topTitle);
topBarRight.append(myChannelsBtn, findChannelBtn, createInMyBtn);
topBarRight.append(findChannelBtn, createInMyBtn);
topBarEl.append(topBarLeft, topBarRight);
const bottomCta = document.createElement('button');
@@ -1235,7 +1182,7 @@ export function render({ navigate, route }) {
const rerenderList = () => {
try {
const expectedPath = `/channels-list/${listState.activeTab}`;
const expectedPath = '/channels-list';
if (window.location.pathname !== expectedPath) {
window.history.replaceState({}, '', expectedPath);
}
@@ -1243,7 +1190,7 @@ export function render({ navigate, route }) {
// ignore history errors
}
const isTabEmpty = !(listState.channels || []).some((channel) => channel.tabCategory === listState.activeTab);
const isTabEmpty = !(listState.channels || []).length;
closeChannelMenu(listState);
@@ -1255,23 +1202,10 @@ export function render({ navigate, route }) {
refreshFeed: reloadFeed,
});
if (listState.activeTab === 'my' && !isGuest) {
myChannelsBtn.style.display = 'none';
topTitle.textContent = 'Мои каналы';
findChannelBtn.style.display = 'none';
switchToAllBtn.style.display = '';
createInMyBtn.style.display = '';
if (!switchToAllBtn.isConnected) topBarLeft.append(switchToAllBtn);
if (topTitle.parentElement !== topBarRight) topBarRight.prepend(topTitle);
} else {
myChannelsBtn.style.display = isGuest ? 'none' : '';
topTitle.textContent = 'Все каналы';
findChannelBtn.style.display = '';
switchToAllBtn.style.display = 'none';
createInMyBtn.style.display = 'none';
if (switchToAllBtn.isConnected) switchToAllBtn.remove();
if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
}
topTitle.textContent = 'Каналы';
findChannelBtn.style.display = '';
createInMyBtn.style.display = '';
if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
updateBottomCta({
button: bottomCta,
@@ -1281,28 +1215,6 @@ export function render({ navigate, route }) {
});
};
let touchStartX = 0;
let touchStartY = 0;
contentEl.addEventListener('touchstart', (event) => {
const p = event.changedTouches?.[0];
if (!p) return;
touchStartX = p.clientX;
touchStartY = p.clientY;
}, { passive: true });
contentEl.addEventListener('touchend', (event) => {
const p = event.changedTouches?.[0];
if (!p) return;
const dx = p.clientX - touchStartX;
const dy = p.clientY - touchStartY;
if (Math.abs(dx) < 45 || Math.abs(dx) < Math.abs(dy)) return;
const index = TAB_ORDER.indexOf(listState.activeTab);
if (index < 0) return;
if (dx < 0 && index < TAB_ORDER.length - 1) listState.activeTab = TAB_ORDER[index + 1];
if (dx > 0 && index > 0) listState.activeTab = TAB_ORDER[index - 1];
if (isGuest && listState.activeTab === 'my') listState.activeTab = 'feed';
rerenderList();
}, { passive: true });
screen.append(topBarEl, contentEl, bottomCta);
if (createSuccessFlash) {