SHA256
Добавил гостевой режим, единые shine-ссылки и пометку о нестабильности мнений
This commit is contained in:
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user