Channels UI + read/unread + unique views + style polish

This commit is contained in:
DrygMira
2026-04-23 13:36:01 +03:00
parent 78d6124f2a
commit c0dfa6c7ab
25 changed files with 2369 additions and 145 deletions
+38 -24
View File
@@ -416,6 +416,7 @@ function mapMockGroups() {
isOwnChannel: channel.kind === 'own' || channel.kind === 'own-personal',
notificationsEnabled: false,
messagesCount: Number(channel.messagesCount || 0),
unreadCount: 0,
lastMessageAt: 0,
ownerName: String(channel.ownerName || 'неизвестно'),
channelName: String(channel.channelName || channel.title || ''),
@@ -470,6 +471,7 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
channelDescription,
messagePreview: summary?.lastMessage?.text || 'Ждем ваших начинаний',
messagesCount: Number(summary?.messagesCount || 0),
unreadCount: Number(summary?.unreadCount || 0),
lastMessageAt: Number(summary?.lastMessage?.createdAtMs || 0),
tabCategory,
isOwnChannel: isOwn,
@@ -479,6 +481,20 @@ function mapApiChannelRow(summary, bucketKey, idx, index, notificationsState) {
};
}
function isSyntheticDefaultChannel(row) {
if (!row || !row.isOwnChannel) return false;
const name = String(row.channelName || '').trim();
if (name !== '0') return false;
const hasDescription = Boolean(String(row.channelDescription || '').trim());
const hasMessages = Number(row.messagesCount || 0) > 0;
const hasTimestamp = Number(row.lastMessageAt || 0) > 0;
const preview = String(row.messagePreview || '').trim();
const hasCustomPreview = preview && preview !== 'Ждем ваших начинаний';
return !hasDescription && !hasMessages && !hasTimestamp && !hasCustomPreview;
}
function pullCreateSuccessFlash() {
try {
const value = String(sessionStorage.getItem(CREATE_CHANNEL_FLASH_KEY) || '').trim();
@@ -491,7 +507,9 @@ function pullCreateSuccessFlash() {
function mapApiFeed(feed, notificationsState) {
const index = {};
const ownChannels = (feed?.ownedChannels || []).map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState));
const ownChannels = (feed?.ownedChannels || [])
.map((it, idx) => mapApiChannelRow(it, 'own', idx, index, notificationsState))
.filter((row) => !isSyntheticDefaultChannel(row));
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));
@@ -508,22 +526,7 @@ function toListModel(groups) {
function renderEmptyState(activeTab, navigate) {
const wrap = document.createElement('div');
wrap.className = 'channels-empty-state channels-empty-state--compact';
const text = document.createElement('div');
text.className = 'meta-muted';
text.textContent = 'В этом разделе нет сообщений';
wrap.append(text);
if (activeTab === 'my') {
const cta = document.createElement('button');
cta.type = 'button';
cta.className = 'secondary-btn';
cta.textContent = 'Создать первый канал';
cta.addEventListener('click', () => navigate('add-channel-view'));
wrap.append(cta);
}
wrap.className = 'channels-empty-state channels-empty-state--compact channels-empty-state--silent';
return wrap;
}
@@ -767,7 +770,7 @@ function renderChannelMain(channel, activeTab) {
preview.textContent = channel.messagePreview || 'Ждем ваших начинаний';
const meta = document.createElement('p');
meta.className = 'channel-row-owner';
meta.className = 'channel-row-owner channel-counter-meta';
meta.textContent = `Сообщений: ${channel.messagesCount || 0}`;
main.append(author, title, preview, meta);
@@ -790,7 +793,7 @@ function renderChannelMain(channel, activeTab) {
preview.textContent = channel.messagePreview || 'Ждем ваших начинаний';
const meta = document.createElement('p');
meta.className = 'channel-row-owner';
meta.className = 'channel-row-owner channel-counter-meta';
meta.textContent = `Сообщений: ${channel.messagesCount || 0}`;
main.prepend(title);
@@ -818,6 +821,8 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
filtered.forEach((channel) => {
const row = document.createElement('article');
row.className = 'channel-row';
const countersVisible = listState.revealedCounters.has(channel.id);
row.classList.toggle('is-counters-visible', countersVisible);
const avatar = document.createElement('div');
avatar.className = 'avatar';
@@ -835,6 +840,7 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
menuButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(menuButton);
listState.revealedCounters.add(channel.id);
if (listState.openMenuId === channel.id) {
closeChannelMenu(listState);
@@ -859,7 +865,9 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
const count = document.createElement('span');
count.className = 'unread channel-row-count';
count.textContent = String(channel.messagesCount || 0);
const unreadCount = Number(channel.unreadCount || 0);
count.textContent = unreadCount > 0 ? String(unreadCount) : '';
count.classList.toggle('is-empty', unreadCount <= 0);
controls.append(menuButton, time, count);
@@ -871,12 +879,13 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
container.append(list);
}
function updateBottomCta({ button, listState, navigate, onReload }) {
function updateBottomCta({ button, listState, navigate, onReload, isTabEmpty = false }) {
const tab = listState.activeTab;
const baseClass = `primary-btn channels-bottom-action${isTabEmpty && tab !== 'my' ? ' is-empty-lift' : ''}`;
if (tab === 'subscriptions') {
button.textContent = 'Подписаться на канал';
button.className = 'primary-btn channels-bottom-action';
button.className = baseClass;
button.onclick = () => openSimpleSubscribeModal({
kind: 'channel',
kindLabel: 'Подписка на канал',
@@ -888,7 +897,7 @@ function updateBottomCta({ button, listState, navigate, onReload }) {
if (tab === 'authors') {
button.textContent = 'Подписаться на автора';
button.className = 'primary-btn channels-bottom-action';
button.className = baseClass;
button.onclick = () => openSimpleSubscribeModal({
kind: 'user',
kindLabel: 'Подписка на автора',
@@ -899,7 +908,7 @@ function updateBottomCta({ button, listState, navigate, onReload }) {
}
button.textContent = 'Создать канал';
button.className = 'primary-btn channels-bottom-action';
button.className = baseClass;
button.onclick = () => navigate('add-channel-view');
}
@@ -948,6 +957,7 @@ export function render({ navigate }) {
activeTab: 'my',
openMenuId: null,
notificationsState,
revealedCounters: new Set(),
channels: [],
menuCleanup: null,
};
@@ -964,6 +974,8 @@ export function render({ navigate }) {
const reloadFeed = async () => loadFeedAndRender({ screen, listState, contentEl, navigate });
const rerenderList = () => {
const isTabEmpty = !(listState.channels || []).some((channel) => channel.tabCategory === listState.activeTab);
tabItems.forEach((tab) => {
const btn = tabs.querySelector(`[data-tab="${tab.key}"]`);
if (btn) btn.classList.toggle('is-active', tab.key === listState.activeTab);
@@ -984,6 +996,7 @@ export function render({ navigate }) {
listState,
navigate,
onReload: reloadFeed,
isTabEmpty,
});
};
@@ -1019,6 +1032,7 @@ export function render({ navigate }) {
listState,
navigate,
onReload: reloadFeed,
isTabEmpty: true,
});
loadFeedAndRender({ screen, listState, contentEl, navigate });