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

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

View File

@ -0,0 +1,28 @@
# Общий список каналов без stories
- Краткое описание:
вкладка `Каналы` переведена на единый список без разделения на "мои" и "подписки".
Название канала в списке теперь показывается как `login_владельцаазваниеанала`.
Служебный канал `stories` скрыт из списка каналов, поиска, подписки и связанных UI-сценариев.
- Что проверять:
1. Открыть вкладку `Каналы`.
2. Убедиться, что сразу показывается один общий список.
3. Проверить, что свои и чужие каналы отображаются вместе.
4. Проверить формат названий: `ownerLogin/channelName`.
5. Открыть свой канал и убедиться, что внутри сохраняется UI владельца.
6. Открыть чужой канал и убедиться, что внутри сохраняется UI подписчика.
7. Проверить, что `stories` не отображается:
- в общем списке;
- в поиске каналов;
- в подписке на канал;
- в списках выбора канала для репоста.
- Ожидаемый результат:
- вкладка `Каналы` больше не делится на два режима;
- все видимые каналы идут единым списком;
- `stories` нигде не виден и не предлагается пользователю;
- переход в канал сохраняет корректный UI в зависимости от владельца.
- Статус:
`pending`

View File

@ -1,2 +1,2 @@
client.version=1.2.260 client.version=1.2.261
server.version=1.2.245 server.version=1.2.246

View File

@ -94,7 +94,7 @@ export function renderToolbar(currentPageId, navigate) {
btn.append(badge); btn.append(badge);
} }
if (item.pageId === 'channels-list') { if (item.pageId === 'channels-list') {
btn.addEventListener('click', () => navigate('channels-list/feed')); btn.addEventListener('click', () => navigate('channels-list'));
} else { } else {
btn.addEventListener('click', () => navigateWithGuestRules(item.pageId, navigate)); btn.addEventListener('click', () => navigateWithGuestRules(item.pageId, navigate));
} }

View File

@ -178,7 +178,11 @@ function allFeedSummaries() {
...(feed.ownedChannels || []), ...(feed.ownedChannels || []),
...(feed.followedUsersChannels || []), ...(feed.followedUsersChannels || []),
...(feed.followedChannels || []), ...(feed.followedChannels || []),
]; ].filter((summary) => {
const typeCode = Number(summary?.channel?.channelTypeCode ?? 1);
const channelName = String(summary?.channel?.channelName || '').trim().toLowerCase();
return typeCode !== 0 && channelName !== 'stories';
});
} }
function resolveChannelDisplayName(channelSelector) { function resolveChannelDisplayName(channelSelector) {
@ -394,7 +398,7 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
.map((item, index) => { .map((item, index) => {
const owner = String(item?.ownerLogin || '').trim(); const owner = String(item?.ownerLogin || '').trim();
const name = String(item?.channelName || '').trim(); const name = String(item?.channelName || '').trim();
const label = `${owner || 'my'} / ${name || 'stories'}`; const label = `${owner || 'my'}/${name || 'channel'}`;
return `<option value="${index}">${label}</option>`; return `<option value="${index}">${label}</option>`;
}) })
.join(''); .join('');
@ -936,10 +940,12 @@ export function render({ navigate, route }) {
return { return {
ownerLogin: String(row?.channel?.ownerLogin || '').trim(), ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
channelName: String(row?.channel?.channelName || '').trim(), channelName: String(row?.channel?.channelName || '').trim(),
channelTypeCode: Number(row?.channel?.channelTypeCode ?? 1),
selector: selectorRow, selector: selectorRow,
}; };
}) })
.filter(Boolean); .filter(Boolean)
.filter((item) => Number(item.channelTypeCode) !== 0 && String(item.channelName || '').trim().toLowerCase() !== 'stories');
if (!channels.length) throw new Error('У вас пока нет каналов для репоста.'); if (!channels.length) throw new Error('У вас пока нет каналов для репоста.');
openRepostModal({ openRepostModal({

View File

@ -27,6 +27,7 @@ import {
} from '../services/shine-routes.js'; } from '../services/shine-routes.js';
export const pageMeta = { id: 'channel-view', title: 'Канал' }; export const pageMeta = { id: 'channel-view', title: 'Канал' };
const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_PERSONAL = 100;
const pendingReactionActions = new Set(); const pendingReactionActions = new Set();
@ -226,6 +227,12 @@ function latestVersionText(versions) {
return ''; return '';
} }
function isStoriesChannel(channel = null) {
const typeCode = Number(channel?.channelTypeCode ?? channel?.channel?.channelTypeCode ?? 1);
const name = String(channel?.channelName || channel?.channel?.channelName || '').trim().toLowerCase();
return typeCode === CHANNEL_TYPE_STORIES || name === 'stories';
}
function resolveMessageText(message) { function resolveMessageText(message) {
return firstNonEmptyText( return firstNonEmptyText(
message?.text, message?.text,
@ -363,10 +370,11 @@ function openRepostModal({ navigate, channels = [], onSubmit }) {
const root = document.getElementById('modal-root'); const root = document.getElementById('modal-root');
const options = (Array.isArray(channels) ? channels : []) const options = (Array.isArray(channels) ? channels : [])
.filter((item) => item?.selector?.ownerBlockchainName && Number.isFinite(Number(item?.selector?.channelRootBlockNumber))) .filter((item) => item?.selector?.ownerBlockchainName && Number.isFinite(Number(item?.selector?.channelRootBlockNumber)))
.filter((item) => !isStoriesChannel(item))
.map((item, index) => { .map((item, index) => {
const owner = String(item?.ownerLogin || '').trim(); const owner = String(item?.ownerLogin || '').trim();
const name = String(item?.channelName || '').trim(); const name = String(item?.channelName || '').trim();
const label = `${owner || 'my'} / ${name || 'stories'}`; const label = `${owner || 'my'}/${name || 'channel'}`;
return `<option value="${index}">${label}</option>`; return `<option value="${index}">${label}</option>`;
}) })
.join(''); .join('');
@ -451,9 +459,6 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
<h3 class="modal-title">Новое сообщение в канале</h3> <h3 class="modal-title">Новое сообщение в канале</h3>
<p class="meta-muted">${channelName}</p> <p class="meta-muted">${channelName}</p>
<textarea id="channel-message-text" class="input" rows="6" maxlength="2000" placeholder="Текст сообщения"></textarea> <textarea id="channel-message-text" class="input" rows="6" maxlength="2000" placeholder="Текст сообщения"></textarea>
<div class="row wrap-row">
<button class="ghost-btn" id="channel-message-voice" type="button">🎤 Голосом</button>
</div>
<div class="meta-muted inline-error" id="channel-message-error"></div> <div class="meta-muted inline-error" id="channel-message-error"></div>
<div class="form-actions-grid"> <div class="form-actions-grid">
<button class="secondary-btn" id="channel-message-cancel" type="button">Отмена</button> <button class="secondary-btn" id="channel-message-cancel" type="button">Отмена</button>
@ -480,15 +485,6 @@ function openAddMessageModal({ channelName, onSubmit, navigate }) {
}; };
root.querySelector('#channel-message-cancel')?.addEventListener('click', close); root.querySelector('#channel-message-cancel')?.addEventListener('click', close);
root.querySelector('#channel-message-voice')?.addEventListener('click', async () => {
await openSpeechInputModal({
navigate,
onTextReady: (text) => {
const prev = String(textEl?.value || '').trim();
if (textEl) textEl.value = prev ? `${prev} ${text}` : text;
},
});
});
submitEl?.addEventListener('click', async () => { submitEl?.addEventListener('click', async () => {
if (inFlight) return; if (inFlight) return;
@ -735,6 +731,9 @@ async function loadFromApi(route, channelId) {
const ownerLogin = String(payload.channel?.ownerLogin || '').trim(); const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
const channelName = String(payload.channel?.channelName || '').trim(); const channelName = String(payload.channel?.channelName || '').trim();
const channelTypeCode = Number(payload.channel?.channelTypeCode ?? 1); const channelTypeCode = Number(payload.channel?.channelTypeCode ?? 1);
if (channelTypeCode === CHANNEL_TYPE_STORIES) {
throw new Error('Канал stories скрыт из пользовательского интерфейса.');
}
const canResolveReverse = ( const canResolveReverse = (
channelTypeCode === CHANNEL_TYPE_PERSONAL channelTypeCode === CHANNEL_TYPE_PERSONAL
&& !!currentLogin && !!currentLogin
@ -1085,6 +1084,15 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
actionButton.className = 'destructive-btn channel-main-action'; actionButton.className = 'destructive-btn channel-main-action';
actionButton.textContent = 'Подписаться на канал'; actionButton.textContent = 'Подписаться на канал';
const addMessageButton = document.createElement('button');
addMessageButton.type = 'button';
addMessageButton.className = 'primary-btn channel-main-action channel-main-action--compose';
addMessageButton.textContent = 'Добавить сообщение';
addMessageButton.addEventListener('click', (event) => {
animatePress(event.currentTarget);
handlers.onAddMessage();
});
const feed = document.createElement('div'); const feed = document.createElement('div');
feed.className = 'stack channel-feed'; feed.className = 'stack channel-feed';
const postsByKey = new Map(); const postsByKey = new Map();
@ -1124,8 +1132,8 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
backButton.addEventListener('click', () => navigate('channels-list')); backButton.addEventListener('click', () => navigate('channels-list'));
if (channelData.isOwnChannel) { if (channelData.isOwnChannel) {
screen.append(feed); screen.append(feed, addMessageButton);
} else if (!channelData.isSubscribed) { } else if (!channelData.isSubscribed && !isStoriesChannel(channelData.channel)) {
screen.append(actionButton, feed, backButton); screen.append(actionButton, feed, backButton);
} else { } else {
screen.append(feed, backButton); screen.append(feed, backButton);
@ -1257,10 +1265,12 @@ export function render({ navigate, route }) {
return { return {
ownerLogin: String(row?.channel?.ownerLogin || '').trim(), ownerLogin: String(row?.channel?.ownerLogin || '').trim(),
channelName: String(row?.channel?.channelName || '').trim(), channelName: String(row?.channel?.channelName || '').trim(),
channelTypeCode: Number(row?.channel?.channelTypeCode ?? 1),
selector, selector,
}; };
}) })
.filter(Boolean); .filter(Boolean)
.filter((item) => !isStoriesChannel(item));
}; };
const isSameChannelSelector = (a, b) => ( const isSameChannelSelector = (a, b) => (
@ -1359,7 +1369,7 @@ export function render({ navigate, route }) {
try { try {
const apiData = await loadFromApi(route, channelId); const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null; activeSelector = apiData?.selector || null;
const channelRouteLabel = `Канал: ${apiData?.channel?.ownerName || 'owner'}/${apiData?.channel?.name || 'channel'}`; const channelRouteLabel = `Канал: ${apiData?.channel?.ownerName || 'owner'} / ${apiData?.channel?.name || 'channel'}`;
const ownChannelLabel = `Ваш канал: ${apiData?.channel?.name || 'channel'}`; const ownChannelLabel = `Ваш канал: ${apiData?.channel?.name || 'channel'}`;
if (channelHeaderButton) { if (channelHeaderButton) {
channelHeaderButton.textContent = apiData?.isOwnChannel ? ownChannelLabel : channelRouteLabel; channelHeaderButton.textContent = apiData?.isOwnChannel ? ownChannelLabel : channelRouteLabel;
@ -1369,33 +1379,22 @@ export function render({ navigate, route }) {
openAboutChannelModal(apiData.channel); openAboutChannelModal(apiData.channel);
}; };
} }
if (apiData?.isOwnChannel) {
const headerActions = header.querySelector('.header-actions');
if (headerActions) {
const addBtn = document.createElement('button');
addBtn.type = 'button';
addBtn.className = 'icon-btn channel-header-add-btn';
addBtn.textContent = 'Добавить сообщение';
addBtn.addEventListener('click', (event) => {
animatePress(event.currentTarget);
openAddMessageModal({
channelName: apiData?.channel?.name || '',
navigate,
onSubmit: async (bodyText) => {
try {
await onAddPost(bodyText);
showStatus('');
} catch (error) {
throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
}
},
});
});
headerActions.append(addBtn);
}
}
skeleton.remove(); skeleton.remove();
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, { cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
onAddMessage: () => {
openAddMessageModal({
channelName: apiData?.channel?.name || '',
navigate,
onSubmit: async (bodyText) => {
try {
await onAddPost(bodyText);
showStatus('');
} catch (error) {
throw new Error(toUserMessage(error, 'Не удалось добавить сообщение.'));
}
},
});
},
onToggleLike: async (messageRef, action) => { onToggleLike: async (messageRef, action) => {
try { try {
await onToggleLike(messageRef, action); await onToggleLike(messageRef, action);

View File

@ -19,7 +19,6 @@ 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 CHANNEL_TYPE_STORIES = 0; const CHANNEL_TYPE_STORIES = 0;
const CHANNEL_TYPE_PERSONAL = 100; const CHANNEL_TYPE_PERSONAL = 100;
const TAB_ORDER = ['feed', 'my'];
function isChannelsDemoMode() { function isChannelsDemoMode() {
try { 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 = '') { function avatarLetterFromName(name = '') {
const first = Array.from(String(name || '').trim())[0] || '#'; const first = Array.from(String(name || '').trim())[0] || '#';
return first.toUpperCase(); return first.toUpperCase();
@ -66,7 +79,7 @@ function allFeedSummaries() {
...(feed.ownedChannels || []), ...(feed.ownedChannels || []),
...(feed.followedUsersChannels || []), ...(feed.followedUsersChannels || []),
...(feed.followedChannels || []), ...(feed.followedChannels || []),
]; ].filter(isVisibleChannelSummary);
} }
function uniqueBy(items, keySelector) { function uniqueBy(items, keySelector) {
@ -110,7 +123,7 @@ async function resolveChannelTargetFromInput(rawInput) {
} }
const ownerFeed = await authService.listSubscriptionsFeed(ownerLogin, 500); 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) => ( const matches = ownChannels.filter((item) => (
String(item?.channel?.channelName || '').trim().toLowerCase() === channelName String(item?.channel?.channelName || '').trim().toLowerCase() === channelName
)); ));
@ -209,7 +222,9 @@ function isFollowedUserVisible(targetLogin) {
const rows = Array.isArray(state.channelsFeed?.followedUsersChannels) const rows = Array.isArray(state.channelsFeed?.followedUsersChannels)
? 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) { function isFollowedChannelVisible(target) {
@ -221,7 +236,7 @@ function isFollowedChannelVisible(target) {
const expectedHash = normalizeHash(target?.rootBlockHash); const expectedHash = normalizeHash(target?.rootBlockHash);
if (!expectedBch || !Number.isFinite(expectedNo)) return false; if (!expectedBch || !Number.isFinite(expectedNo)) return false;
return rows.some((row) => { return rows.filter(isVisibleChannelSummary).some((row) => {
const rowBch = String(row?.channel?.ownerBlockchainName || ''); const rowBch = String(row?.channel?.ownerBlockchainName || '');
const rowNo = Number(row?.channel?.channelRoot?.blockNumber); const rowNo = Number(row?.channel?.channelRoot?.blockNumber);
const rowHash = normalizeHash(row?.channel?.channelRoot?.blockHash); const rowHash = normalizeHash(row?.channel?.channelRoot?.blockHash);
@ -488,7 +503,7 @@ function openChannelFinderModal({ navigate }) {
const ownerLogin = normalizeLoginInput(login); const ownerLogin = normalizeLoginInput(login);
if (!ownerLogin) return; if (!ownerLogin) return;
const feed = await authService.listSubscriptionsFeed(ownerLogin, 1000); 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 needle = String(filterChannel || '').trim().toLowerCase();
const channels = rows const channels = rows
.map((item) => ({ .map((item) => ({
@ -499,7 +514,7 @@ function openChannelFinderModal({ navigate }) {
.filter((item) => !needle || item.channelName.toLowerCase().includes(needle)) .filter((item) => !needle || item.channelName.toLowerCase().includes(needle))
.slice(0, 200) .slice(0, 200)
.map((item) => ({ .map((item) => ({
label: `${ownerLogin}/${item.channelName}`, label: `${ownerLogin} / ${item.channelName}`,
ownerLogin, ownerLogin,
ownerBlockchainName: item.ownerBlockchainName, ownerBlockchainName: item.ownerBlockchainName,
channelName: item.channelName, channelName: item.channelName,
@ -595,9 +610,6 @@ function mapMockGroups() {
ownerBlockchainName: String(channel.ownerName || ''), ownerBlockchainName: String(channel.ownerName || ''),
channelName: String(channel.channelName || channel.title || channel.id), channelName: String(channel.channelName || channel.title || channel.id),
}), }),
tabCategory: channel.kind === 'own' || channel.kind === 'own-personal'
? 'my'
: 'feed',
messagePreview: channel.lastMessage || 'Ждем ваших начинаний', messagePreview: channel.lastMessage || 'Ждем ваших начинаний',
isSubscribed: channel.kind !== 'own' && channel.kind !== 'own-personal', isSubscribed: channel.kind !== 'own' && channel.kind !== 'own-personal',
isOwnChannel: channel.kind === 'own' || channel.kind === 'own-personal', isOwnChannel: channel.kind === 'own' || channel.kind === 'own-personal',
@ -612,15 +624,18 @@ function mapMockGroups() {
const ownChannels = mockChannels const ownChannels = mockChannels
.filter((channel) => channel.kind === 'own-personal' || channel.kind === 'own') .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 const followedUserChannels = mockChannels
.filter((channel) => channel.kind === 'followed-user-channel') .filter((channel) => channel.kind === 'followed-user-channel')
.map((item) => ({ ...mapRow(item), tabCategory: 'feed' })); .map((item) => mapRow(item))
.filter((item) => !isStoriesChannel(item));
const subscribedChannels = mockChannels const subscribedChannels = mockChannels
.filter((channel) => channel.kind === 'subscribed') .filter((channel) => channel.kind === 'subscribed')
.map((item) => ({ ...mapRow(item), tabCategory: 'feed' })); .map((item) => mapRow(item))
.filter((item) => !isStoriesChannel(item));
return { ownChannels, followedUserChannels, subscribedChannels, index: {} }; return { ownChannels, followedUserChannels, subscribedChannels, index: {} };
} }
@ -635,9 +650,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
const channelTypeCode = Number(summary?.channel?.channelTypeCode ?? 1); const channelTypeCode = Number(summary?.channel?.channelTypeCode ?? 1);
const channelTypeVersion = Number(summary?.channel?.channelTypeVersion ?? 1); const channelTypeVersion = Number(summary?.channel?.channelTypeVersion ?? 1);
const isOwn = bucketKey === 'own'; const isOwn = bucketKey === 'own';
const tabCategory = isOwn ? 'my' : 'feed'; const title = `${ownerLogin} / ${channelName}`;
const title = isOwn ? channelName : `${ownerLogin}/${channelName}`;
return { return {
id: rowId, id: rowId,
@ -656,7 +669,6 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
messagesCount: Number(summary?.messagesCount || 0), messagesCount: Number(summary?.messagesCount || 0),
unreadCount: Number(summary?.unreadCount || 0), unreadCount: Number(summary?.unreadCount || 0),
lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0), lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0),
tabCategory,
isOwnChannel: isOwn, isOwnChannel: isOwn,
isSubscribed: !isOwn, isSubscribed: !isOwn,
notificationsEnabled: notificationsState[rowId] === true, notificationsEnabled: notificationsState[rowId] === true,
@ -677,9 +689,14 @@ function pullCreateSuccessFlash() {
function mapApiFeed(feed, notificationsState) { function mapApiFeed(feed, notificationsState) {
const index = {}; const index = {};
const ownChannels = (feed?.ownedChannels || []) const ownChannels = (feed?.ownedChannels || [])
.filter(isVisibleChannelSummary)
.map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState)); .map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState));
const followedUserChannels = (feed?.followedUsersChannels || []).map((it, idx) => mapApiChannelRow(it, 'followedUsers', idx, index, notificationsState)); const followedUserChannels = (feed?.followedUsersChannels || [])
const subscribedChannels = (feed?.followedChannels || []).map((it, idx) => mapApiChannelRow(it, 'followedChannels', idx, index, notificationsState)); .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 }; return { ownChannels, followedUserChannels, subscribedChannels, index };
} }
@ -692,7 +709,7 @@ function toListModel(groups) {
]; ];
} }
function renderEmptyState(activeTab, navigate) { function renderEmptyState() {
const wrap = document.createElement('div'); const wrap = document.createElement('div');
wrap.className = 'channels-empty-state channels-empty-state--compact channels-empty-state--silent'; wrap.className = 'channels-empty-state channels-empty-state--compact channels-empty-state--silent';
if (!state.session.isAuthorized) { if (!state.session.isAuthorized) {
@ -700,13 +717,7 @@ function renderEmptyState(activeTab, navigate) {
} }
const text = document.createElement('p'); const text = document.createElement('p');
text.className = 'meta-muted'; text.className = 'meta-muted';
if (activeTab === 'feed') { text.textContent = 'У вас пока нет доступных каналов.';
text.textContent = 'Нет подписок и найденных каналов.';
} else if (activeTab === 'my') {
text.textContent = 'У вас пока нет каналов.';
} else {
text.textContent = 'Пусто.';
}
wrap.append(text); wrap.append(text);
return wrap; 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'); const main = document.createElement('div');
main.className = 'channel-row-main'; 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'); const title = document.createElement('strong');
title.className = 'channel-row-title'; 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'); const desc = document.createElement('p');
desc.className = 'channel-row-description'; desc.className = 'channel-row-description';
desc.textContent = channel.channelDescription; desc.textContent = channel.channelDescription;
@ -993,11 +983,10 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
container.innerHTML = ''; container.innerHTML = '';
const allChannels = listState.channels || []; const allChannels = listState.channels || [];
const activeTab = listState.activeTab; const filtered = allChannels;
const filtered = allChannels.filter((channel) => channel.tabCategory === activeTab);
if (!filtered.length) { if (!filtered.length) {
container.append(renderEmptyState(activeTab, navigate)); container.append(renderEmptyState());
return; return;
} }
@ -1016,7 +1005,7 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
avatar.className = 'avatar'; avatar.className = 'avatar';
avatar.textContent = channel.avatar; avatar.textContent = channel.avatar;
const main = renderChannelMain(channel, activeTab); const main = renderChannelMain(channel);
const isGuest = !state.session.isAuthorized; const isGuest = !state.session.isAuthorized;
const controls = document.createElement('div'); const controls = document.createElement('div');
@ -1078,23 +1067,7 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
} }
function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) { function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) {
const tab = listState.activeTab; const baseClass = `primary-btn channels-bottom-action${isTabEmpty ? ' is-empty-lift' : ''}`;
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;
}
button.textContent = 'Поиск каналов'; button.textContent = 'Поиск каналов';
button.className = baseClass; button.className = baseClass;
button.onclick = () => openChannelFinderModal({ navigate }); button.onclick = () => openChannelFinderModal({ navigate });
@ -1155,18 +1128,12 @@ export function render({ navigate, route }) {
const isGuest = !state.session.isAuthorized; const isGuest = !state.session.isAuthorized;
const listState = { const listState = {
activeTab: TAB_ORDER.includes(String(route?.params?.mode || '').trim())
? String(route?.params?.mode).trim()
: 'feed',
openMenuId: null, openMenuId: null,
notificationsState, notificationsState,
revealedCounters: new Set(), revealedCounters: new Set(),
channels: [], channels: [],
menuCleanup: null, menuCleanup: null,
}; };
if (isGuest && listState.activeTab === 'my') {
listState.activeTab = 'feed';
}
const contentEl = document.createElement('div'); const contentEl = document.createElement('div');
contentEl.className = 'channels-list-content'; contentEl.className = 'channels-list-content';
@ -1187,16 +1154,6 @@ export function render({ navigate, route }) {
const topTitle = document.createElement('strong'); const topTitle = document.createElement('strong');
topTitle.className = 'channels-top-title'; 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'); const topBarRight = document.createElement('div');
topBarRight.className = 'channels-top-right'; topBarRight.className = 'channels-top-right';
@ -1214,18 +1171,8 @@ export function render({ navigate, route }) {
createInMyBtn.setAttribute('aria-label', 'Создать канал'); createInMyBtn.setAttribute('aria-label', 'Создать канал');
createInMyBtn.addEventListener('click', () => navigate('add-channel-view')); 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); topBarLeft.append(backBtn, topTitle);
topBarRight.append(myChannelsBtn, findChannelBtn, createInMyBtn); topBarRight.append(findChannelBtn, createInMyBtn);
topBarEl.append(topBarLeft, topBarRight); topBarEl.append(topBarLeft, topBarRight);
const bottomCta = document.createElement('button'); const bottomCta = document.createElement('button');
@ -1235,7 +1182,7 @@ export function render({ navigate, route }) {
const rerenderList = () => { const rerenderList = () => {
try { try {
const expectedPath = `/channels-list/${listState.activeTab}`; const expectedPath = '/channels-list';
if (window.location.pathname !== expectedPath) { if (window.location.pathname !== expectedPath) {
window.history.replaceState({}, '', expectedPath); window.history.replaceState({}, '', expectedPath);
} }
@ -1243,7 +1190,7 @@ export function render({ navigate, route }) {
// ignore history errors // ignore history errors
} }
const isTabEmpty = !(listState.channels || []).some((channel) => channel.tabCategory === listState.activeTab); const isTabEmpty = !(listState.channels || []).length;
closeChannelMenu(listState); closeChannelMenu(listState);
@ -1255,23 +1202,10 @@ export function render({ navigate, route }) {
refreshFeed: reloadFeed, refreshFeed: reloadFeed,
}); });
if (listState.activeTab === 'my' && !isGuest) { topTitle.textContent = 'Каналы';
myChannelsBtn.style.display = 'none'; findChannelBtn.style.display = '';
topTitle.textContent = 'Мои каналы'; createInMyBtn.style.display = '';
findChannelBtn.style.display = 'none'; if (topTitle.parentElement !== topBarLeft) topBarLeft.append(topTitle);
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);
}
updateBottomCta({ updateBottomCta({
button: bottomCta, 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); screen.append(topBarEl, contentEl, bottomCta);
if (createSuccessFlash) { if (createSuccessFlash) {

View File

@ -4861,6 +4861,19 @@ html, body { overflow-x: hidden; }
margin-top: 6px !important; margin-top: 6px !important;
} }
.channels-screen--channel .channel-main-action--compose {
position: sticky;
bottom: -12px;
margin: 16px 20px 0;
width: calc(100% - 40px);
background: linear-gradient(135deg, #f5cf4f, #e2ad1f);
border: 1px solid rgba(255, 215, 97, 0.85);
color: #2f2200;
font-weight: 700;
box-shadow: 0 14px 28px rgba(226, 173, 31, 0.24);
z-index: 3;
}
.toolbar { .toolbar {
background: rgba(18, 24, 38, 0.4); background: rgba(18, 24, 38, 0.4);
backdrop-filter: blur(25px); backdrop-filter: blur(25px);