Добавил гостевой режим, единые shine-ссылки и пометку о нестабильности мнений

This commit is contained in:
AidarKC
2026-05-20 16:14:59 +03:00
parent aa35d87885
commit 21413268f3
46 changed files with 1125 additions and 310 deletions
+105 -42
View File
@@ -10,6 +10,7 @@ import {
softHaptic,
writeChannelNotificationsState,
} from '../services/channels-ux.js';
import { makeShineChannelRoute } from '../services/shine-routes.js';
export const pageMeta = { id: 'channels-list', title: 'Каналы' };
@@ -43,12 +44,14 @@ function normalizeLoginInput(value) {
}
function buildChannelRouteFromSummary(summary, fallbackId) {
const ownerBch = summary?.channel?.ownerBlockchainName;
const ownerBch = String(summary?.channel?.ownerBlockchainName || '').trim();
const ownerLogin = String(summary?.channel?.ownerLogin || '').trim();
const channelName = String(summary?.channel?.channelName || '').trim();
if (ownerBch && channelName) {
return `channel/${encodeRoutePart(ownerBch)}/${encodeRoutePart(channelName)}`;
}
return `channel/${encodeRoutePart(String(summary?.channel?.ownerLogin || '').trim())}/${encodeRoutePart(channelName || fallbackId)}`;
return makeShineChannelRoute({
ownerLogin,
ownerBlockchainName: ownerBch,
channelName: channelName || fallbackId,
});
}
function avatarLetterFromName(name = '') {
@@ -408,7 +411,7 @@ function openChannelFinderModal({ navigate }) {
<div class="modal-card stack" style="max-width: min(96vw, 760px); width: min(96vw, 760px);">
<h3 class="modal-title">Поиск каналов</h3>
<p class="meta-muted">Введите логин (или начало логина), затем выберите пользователя и канал.</p>
<input id="channels-find-input" class="input" placeholder="Например: aid" autocomplete="off" />
<input id="channels-find-input" class="input" placeholder="Например: aidar" autocomplete="off" />
<div id="channels-find-suggest" class="channels-search-suggest" style="display:none"></div>
<div id="channels-find-list" class="channels-search-suggest" style="display:none"></div>
<div id="channels-find-error" class="meta-muted inline-error"></div>
@@ -463,8 +466,12 @@ function openChannelFinderModal({ navigate }) {
openBtn.textContent = 'Просмотреть';
openBtn.addEventListener('click', () => {
close();
const ownerPart = String(item.ownerBlockchainName || '').trim() || String(item.ownerLogin || '').trim();
navigate(`channel/${encodeRoutePart(ownerPart)}/${encodeRoutePart(item.channelName)}`);
const route = makeShineChannelRoute({
ownerLogin: String(item.ownerLogin || '').trim(),
ownerBlockchainName: String(item.ownerBlockchainName || '').trim(),
channelName: String(item.channelName || '').trim(),
});
if (route) navigate(route);
});
row.style.display = 'flex';
@@ -582,7 +589,11 @@ function openChannelFinderModal({ navigate }) {
function mapMockGroups() {
const mapRow = (channel) => ({
...channel,
route: `channel/${encodeRoutePart(String(channel.ownerName || 'channel'))}/${encodeRoutePart(String(channel.channelName || channel.title || channel.id))}`,
route: makeShineChannelRoute({
ownerLogin: String(channel.ownerName || 'channel'),
ownerBlockchainName: String(channel.ownerName || ''),
channelName: String(channel.channelName || channel.title || channel.id),
}),
tabCategory: channel.kind === 'own' || channel.kind === 'own-personal'
? 'my'
: 'feed',
@@ -683,6 +694,9 @@ function toListModel(groups) {
function renderEmptyState(activeTab, navigate) {
const wrap = document.createElement('div');
wrap.className = 'channels-empty-state channels-empty-state--compact channels-empty-state--silent';
if (!state.session.isAuthorized) {
return wrap;
}
const text = document.createElement('p');
text.className = 'meta-muted';
if (activeTab === 'feed') {
@@ -763,7 +777,14 @@ function renderDemoFallback(container, navigate, error, onRetry) {
<span class="channel-row-time">—</span>
</div>
`;
row.addEventListener('click', () => navigate(channel.route || `channel/${encodeRoutePart(String(channel.ownerName || 'channel'))}/${encodeRoutePart(String(channel.channelName || channel.id))}`));
row.addEventListener('click', () => {
const route = channel.route || makeShineChannelRoute({
ownerLogin: String(channel.ownerName || 'channel'),
ownerBlockchainName: String(channel.ownerName || ''),
channelName: String(channel.channelName || channel.id),
});
if (route) navigate(route);
});
list.append(row);
});
@@ -996,35 +1017,10 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
const main = renderChannelMain(channel, activeTab);
const isGuest = !state.session.isAuthorized;
const controls = document.createElement('div');
controls.className = 'channel-row-controls';
const menuButton = document.createElement('button');
menuButton.type = 'button';
menuButton.className = 'channel-menu-trigger';
menuButton.textContent = '…';
menuButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(menuButton);
listState.revealedCounters.add(channel.id);
if (listState.openMenuId === channel.id) {
closeChannelMenu(listState);
rerenderList();
return;
}
listState.openMenuId = channel.id;
openChannelMenu({
listState,
channel,
anchorEl: menuButton,
refreshFeed: async () => loadFeedAndRender({ screen, listState, contentEl: container, navigate }),
rerenderList,
});
rerenderList();
});
const time = document.createElement('span');
time.className = 'channel-row-time';
time.textContent = channel.lastMessageAt ? formatRelativeTime(channel.lastMessageAt) : '—';
@@ -1035,10 +1031,45 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
count.textContent = unreadCount > 0 ? String(unreadCount) : '';
count.classList.toggle('is-empty', unreadCount <= 0);
controls.append(menuButton, time, count);
if (!isGuest) {
const menuButton = document.createElement('button');
menuButton.type = 'button';
menuButton.className = 'channel-menu-trigger';
menuButton.textContent = '…';
menuButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(menuButton);
listState.revealedCounters.add(channel.id);
if (listState.openMenuId === channel.id) {
closeChannelMenu(listState);
rerenderList();
return;
}
listState.openMenuId = channel.id;
openChannelMenu({
listState,
channel,
anchorEl: menuButton,
refreshFeed: async () => loadFeedAndRender({ screen, listState, contentEl: container, navigate }),
rerenderList,
});
rerenderList();
});
controls.append(menuButton);
}
controls.append(time, count);
row.append(avatar, main, controls);
row.addEventListener('click', () => navigate(channel.route || `channel/${encodeRoutePart(String(channel.ownerName || 'channel'))}/${encodeRoutePart(String(channel.channelName || channel.id))}`));
row.addEventListener('click', () => {
const route = channel.route || makeShineChannelRoute({
ownerLogin: String(channel.ownerName || 'channel'),
ownerBlockchainName: String(channel.ownerName || ''),
channelName: String(channel.channelName || channel.id),
});
if (route) navigate(route);
});
list.append(row);
});
@@ -1057,9 +1088,9 @@ function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) {
}
if (tab === 'my') {
button.textContent = 'Создать канал';
button.textContent = 'Найти канал';
button.className = baseClass;
button.onclick = () => navigate('add-channel-view');
button.onclick = () => openChannelFinderModal({ navigate });
return;
}
@@ -1072,8 +1103,20 @@ async function loadFeedAndRender({ screen, listState, contentEl, navigate }) {
closeChannelMenu(listState);
renderSkeletonList(contentEl, 5);
if (!state.session.isAuthorized) {
setChannelsFeed(null, {});
listState.channels = [];
renderListContent({
screen,
container: contentEl,
listState,
navigate,
refreshFeed: async () => loadFeedAndRender({ screen, listState, contentEl, navigate }),
});
return;
}
try {
if (!state.session.login) throw new Error('not_authorized');
const feed = await authService.listSubscriptionsFeed(state.session.login, 200);
const groups = mapApiFeed(feed, listState.notificationsState);
@@ -1109,6 +1152,7 @@ export function render({ navigate, route }) {
const createSuccessFlash = pullCreateSuccessFlash();
const notificationsState = readChannelNotificationsState();
const isGuest = !state.session.isAuthorized;
const listState = {
activeTab: TAB_ORDER.includes(String(route?.params?.mode || '').trim())
? String(route?.params?.mode).trim()
@@ -1119,10 +1163,16 @@ export function render({ navigate, route }) {
channels: [],
menuCleanup: null,
};
if (isGuest && listState.activeTab === 'my') {
listState.activeTab = 'feed';
}
const contentEl = document.createElement('div');
contentEl.className = 'channels-list-content';
const topBarEl = document.createElement('div');
topBarEl.className = 'channels-top-bar';
const tabsEl = document.createElement('div');
tabsEl.className = 'channels-tabs';
const tabLabels = {
@@ -1130,6 +1180,7 @@ export function render({ navigate, route }) {
my: 'Мои каналы',
};
TAB_ORDER.forEach((tabKey) => {
if (isGuest && tabKey === 'my') return;
const tabBtn = document.createElement('button');
tabBtn.type = 'button';
tabBtn.className = `channels-tab-btn${listState.activeTab === tabKey ? ' is-active' : ''}`;
@@ -1142,6 +1193,15 @@ export function render({ navigate, route }) {
tabsEl.append(tabBtn);
});
const topActionBtn = document.createElement('button');
topActionBtn.type = 'button';
topActionBtn.className = 'secondary-btn channels-top-action-btn';
topActionBtn.textContent = 'Создать канал';
topActionBtn.addEventListener('click', () => navigate('add-channel-view'));
if (isGuest) topActionBtn.style.display = 'none';
topBarEl.append(tabsEl, topActionBtn);
const bottomCta = document.createElement('button');
bottomCta.type = 'button';
@@ -1169,6 +1229,9 @@ export function render({ navigate, route }) {
refreshFeed: reloadFeed,
});
const showCreate = !isGuest && listState.activeTab === 'my';
topActionBtn.style.display = showCreate ? '' : 'none';
updateBottomCta({
button: bottomCta,
listState,
@@ -1202,7 +1265,7 @@ export function render({ navigate, route }) {
rerenderList();
}, { passive: true });
screen.append(tabsEl, contentEl, bottomCta);
screen.append(topBarEl, contentEl, bottomCta);
if (createSuccessFlash) {
showToast(createSuccessFlash);