SHA256
Обновить список каналов и кнопку сообщения
This commit is contained in:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user