UI: обновить thread/counters, вкладку Каналы и сценарий просмотра+подписки
This commit is contained in:
parent
a2954071bd
commit
c0b0c99f53
@ -0,0 +1,25 @@
|
|||||||
|
# Thread: стабильная нижняя панель действий со счётчиками
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- На карточках сообщений во вкладке thread нижняя панель действий теперь всегда стабильная и содержит:
|
||||||
|
- сердечко + количество лайков;
|
||||||
|
- иконка ответа + количество ответов;
|
||||||
|
- иконка изменений + количество изменений (только если изменений больше 0);
|
||||||
|
- справа кнопку отправки (`↗ Отправить`).
|
||||||
|
- Логика изменений: `изменения = versionsTotal - 1`.
|
||||||
|
- Если `versionsTotal = 1`, поле изменений не показывается.
|
||||||
|
- Убрано поведение с появлением дополнительной верхней надписи/статистики после первого взаимодействия.
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
- Открыть любой thread и убедиться, что у каждой карточки внизу всегда видны кнопки/счётчики.
|
||||||
|
- Проверить, что лайк и ответ отображают корректные числа сразу, без дополнительного клика.
|
||||||
|
- Для сообщения с `versionsTotal = 1` убедиться, что поле изменений отсутствует.
|
||||||
|
- Для сообщения с `versionsTotal > 1` убедиться, что показывается `✏️ N`, где `N = versionsTotal - 1`.
|
||||||
|
- Проверить, что справа всегда есть `↗ Отправить`.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- Нижняя панель действий во thread ведёт себя одинаково и не меняет структуру после кликов/ответов.
|
||||||
|
- Счётчики соответствуют данным API.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@ -0,0 +1,24 @@
|
|||||||
|
# Каналы: новые табы + поиск/просмотр + подписка в канале
|
||||||
|
|
||||||
|
- краткое описание фичи:
|
||||||
|
- На вкладке «Каналы» верхние табы переставлены в порядок: `Чаты`, `Каналы`, `Мои`.
|
||||||
|
- По умолчанию открывается вкладка `Каналы` (центральная).
|
||||||
|
- Нижняя кнопка на вкладке `Каналы` переименована: `Найти канал` (вместо `Подписаться на канал`).
|
||||||
|
- В модальном поиске канала оставлен сценарий выбора по `user/channel` (и по имени канала через существующие подсказки), без использования формата `blockchain:number:hash`.
|
||||||
|
- В результатах поиска канала добавлена явная кнопка `Просмотреть` для перехода в канал.
|
||||||
|
- На экране канала кнопка `Подписаться на канал` показывается только если пользователь ещё не подписан.
|
||||||
|
- После подтверждённой подписки кнопка исчезает (повторный ререндер с обновлённым feed).
|
||||||
|
|
||||||
|
- что именно проверять:
|
||||||
|
- Открыть `Каналы`: убедиться, что порядок табов `Чаты | Каналы | Мои`, активна по умолчанию `Каналы`.
|
||||||
|
- На `Каналы` проверить нижнюю кнопку `Найти канал`.
|
||||||
|
- В `Найти канал` выбрать канал и нажать `Просмотреть`: должен открыться экран канала.
|
||||||
|
- На экране чужого канала (без подписки) нажать `Подписаться на канал`, подтвердить `Ок`.
|
||||||
|
- Убедиться, что после успешной подписки кнопка `Подписаться на канал` исчезает.
|
||||||
|
|
||||||
|
- ожидаемый результат:
|
||||||
|
- Пользователь находит и открывает канал через `Найти канал` → `Просмотреть`.
|
||||||
|
- Подписка выполняется на экране канала и не предлагается повторно сразу после успеха.
|
||||||
|
|
||||||
|
- статус:
|
||||||
|
- pending
|
||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.49
|
client.version=1.2.50
|
||||||
server.version=1.2.43
|
server.version=1.2.44
|
||||||
|
|||||||
@ -16,7 +16,6 @@ export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };
|
|||||||
|
|
||||||
const pendingReactionActions = new Set();
|
const pendingReactionActions = new Set();
|
||||||
const pendingThreadScroll = new Map();
|
const pendingThreadScroll = new Map();
|
||||||
const revealedCountersByRoute = new Map();
|
|
||||||
|
|
||||||
function logThreadRuntimeError(stage, error, context = {}) {
|
function logThreadRuntimeError(stage, error, context = {}) {
|
||||||
const message = String(error?.message || error || 'thread runtime error');
|
const message = String(error?.message || error || 'thread runtime error');
|
||||||
@ -67,29 +66,6 @@ function messageRefKey(messageRef) {
|
|||||||
return `${blockchainName}:${blockNumber}:${blockHash}`;
|
return `${blockchainName}:${blockNumber}:${blockHash}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getRevealedCounterSet(routeKey) {
|
|
||||||
const key = String(routeKey || '').trim();
|
|
||||||
if (!key) return new Set();
|
|
||||||
let bucket = revealedCountersByRoute.get(key);
|
|
||||||
if (!bucket) {
|
|
||||||
bucket = new Set();
|
|
||||||
revealedCountersByRoute.set(key, bucket);
|
|
||||||
}
|
|
||||||
return bucket;
|
|
||||||
}
|
|
||||||
|
|
||||||
function isCounterVisible(routeKey, counterKey) {
|
|
||||||
const key = String(counterKey || '').trim();
|
|
||||||
if (!key) return false;
|
|
||||||
return getRevealedCounterSet(routeKey).has(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function revealCounter(routeKey, counterKey) {
|
|
||||||
const key = String(counterKey || '').trim();
|
|
||||||
if (!key) return;
|
|
||||||
getRevealedCounterSet(routeKey).add(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
function buildAbsoluteRouteUrl(routePath = '') {
|
function buildAbsoluteRouteUrl(routePath = '') {
|
||||||
const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
|
const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
|
||||||
const url = new URL(window.location.href);
|
const url = new URL(window.location.href);
|
||||||
@ -299,7 +275,7 @@ function openReplyModal({ onSubmit, navigate }) {
|
|||||||
if (textEl) textEl.focus();
|
if (textEl) textEl.focus();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNodeCard(node, heading, handlers, localNumber, routeKey, options = {}) {
|
function renderNodeCard(node, heading, handlers, localNumber) {
|
||||||
const card = document.createElement('article');
|
const card = document.createElement('article');
|
||||||
card.className = 'card stack thread-node-card';
|
card.className = 'card stack thread-node-card';
|
||||||
|
|
||||||
@ -308,6 +284,7 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
const likes = Number(node?.likesCount || 0);
|
const likes = Number(node?.likesCount || 0);
|
||||||
const replies = Number(node?.repliesCount || 0);
|
const replies = Number(node?.repliesCount || 0);
|
||||||
const versions = Number(node?.versionsTotal || 1);
|
const versions = Number(node?.versionsTotal || 1);
|
||||||
|
const changes = Math.max(0, versions - 1);
|
||||||
|
|
||||||
const headingText = String(heading || '').trim();
|
const headingText = String(heading || '').trim();
|
||||||
if (headingText) {
|
if (headingText) {
|
||||||
@ -328,31 +305,10 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
body.className = 'thread-node-body';
|
body.className = 'thread-node-body';
|
||||||
body.textContent = text;
|
body.textContent = text;
|
||||||
|
|
||||||
const stats = document.createElement('p');
|
card.append(meta, body);
|
||||||
stats.className = 'thread-node-stats';
|
|
||||||
stats.textContent = `Лайки: ${likes}, ответы: ${replies}, версий: ${versions}`;
|
|
||||||
|
|
||||||
card.append(meta, body, stats);
|
|
||||||
|
|
||||||
const target = buildTargetFromNode(node);
|
const target = buildTargetFromNode(node);
|
||||||
const refKey = messageRefKey(target);
|
const refKey = messageRefKey(target);
|
||||||
const countersVisible = refKey ? isCounterVisible(routeKey, refKey) : true;
|
|
||||||
if (!countersVisible) {
|
|
||||||
card.classList.remove('is-counters-visible');
|
|
||||||
stats.classList.add('is-hidden');
|
|
||||||
} else {
|
|
||||||
card.classList.add('is-counters-visible');
|
|
||||||
stats.classList.remove('is-hidden');
|
|
||||||
}
|
|
||||||
|
|
||||||
const revealCounters = () => {
|
|
||||||
if (!refKey) return;
|
|
||||||
revealCounter(routeKey, refKey);
|
|
||||||
card.classList.add('is-counters-visible');
|
|
||||||
stats.classList.remove('is-hidden');
|
|
||||||
};
|
|
||||||
card.addEventListener('click', revealCounters);
|
|
||||||
|
|
||||||
if (!target || !handlers) return card;
|
if (!target || !handlers) return card;
|
||||||
|
|
||||||
if (refKey) card.dataset.messageKey = refKey;
|
if (refKey) card.dataset.messageKey = refKey;
|
||||||
@ -371,7 +327,7 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
likeButton.type = 'button';
|
likeButton.type = 'button';
|
||||||
likeButton.className = 'secondary-btn thread-like-btn';
|
likeButton.className = 'secondary-btn thread-like-btn';
|
||||||
if (isLiked) likeButton.classList.add('is-liked');
|
if (isLiked) likeButton.classList.add('is-liked');
|
||||||
likeButton.textContent = isPending ? '❤️ Лайк...' : (isLiked ? '❤️ Лайк' : '🤍 Лайк');
|
likeButton.textContent = isPending ? `❤️ ${likes}...` : `${isLiked ? '❤️' : '🤍'} ${likes}`;
|
||||||
likeButton.disabled = isPending;
|
likeButton.disabled = isPending;
|
||||||
likeButton.addEventListener('click', async (event) => {
|
likeButton.addEventListener('click', async (event) => {
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
@ -380,10 +336,9 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
const ok = window.confirm('Поставить лайк?');
|
const ok = window.confirm('Поставить лайк?');
|
||||||
if (!ok) return;
|
if (!ok) return;
|
||||||
}
|
}
|
||||||
revealCounters();
|
|
||||||
await longPressFeel(event.currentTarget, 130);
|
await longPressFeel(event.currentTarget, 130);
|
||||||
likeButton.disabled = true;
|
likeButton.disabled = true;
|
||||||
likeButton.textContent = 'Лайк...';
|
likeButton.textContent = `❤️ ${likes}...`;
|
||||||
try {
|
try {
|
||||||
await handlers.onToggleLike(target, isLiked ? 'unlike' : 'like');
|
await handlers.onToggleLike(target, isLiked ? 'unlike' : 'like');
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -399,16 +354,22 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
const replyButton = document.createElement('button');
|
const replyButton = document.createElement('button');
|
||||||
replyButton.type = 'button';
|
replyButton.type = 'button';
|
||||||
replyButton.className = 'secondary-btn thread-reply-btn';
|
replyButton.className = 'secondary-btn thread-reply-btn';
|
||||||
replyButton.textContent = '💬 Ответить';
|
replyButton.textContent = `💬 ${replies}`;
|
||||||
replyButton.addEventListener('click', (event) => {
|
replyButton.addEventListener('click', (event) => {
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
revealCounters();
|
|
||||||
openReplyModal({
|
openReplyModal({
|
||||||
navigate: handlers.navigate,
|
navigate: handlers.navigate,
|
||||||
onSubmit: async (textValue) => handlers.onReply(target, textValue),
|
onSubmit: async (textValue) => handlers.onReply(target, textValue),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const changedButton = document.createElement('button');
|
||||||
|
changedButton.type = 'button';
|
||||||
|
changedButton.className = 'secondary-btn thread-version-btn';
|
||||||
|
changedButton.textContent = `✏️ ${changes}`;
|
||||||
|
changedButton.disabled = true;
|
||||||
|
changedButton.style.display = changes > 0 ? '' : 'none';
|
||||||
|
|
||||||
const shareButton = document.createElement('button');
|
const shareButton = document.createElement('button');
|
||||||
shareButton.type = 'button';
|
shareButton.type = 'button';
|
||||||
shareButton.className = 'secondary-btn thread-share-btn';
|
shareButton.className = 'secondary-btn thread-share-btn';
|
||||||
@ -416,16 +377,15 @@ function renderNodeCard(node, heading, handlers, localNumber, routeKey, options
|
|||||||
shareButton.addEventListener('click', async (event) => {
|
shareButton.addEventListener('click', async (event) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
revealCounters();
|
|
||||||
await handlers.onShare(target);
|
await handlers.onShare(target);
|
||||||
});
|
});
|
||||||
|
|
||||||
actions.append(likeButton, replyButton, shareButton);
|
actions.append(likeButton, replyButton, changedButton, shareButton);
|
||||||
card.append(actions);
|
card.append(actions);
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderDescendants(items, handlers, nextNumber, routeKey, depth = 0) {
|
function renderDescendants(items, handlers, nextNumber, depth = 0) {
|
||||||
const wrap = document.createElement('div');
|
const wrap = document.createElement('div');
|
||||||
wrap.className = 'stack';
|
wrap.className = 'stack';
|
||||||
|
|
||||||
@ -433,13 +393,13 @@ function renderDescendants(items, handlers, nextNumber, routeKey, depth = 0) {
|
|||||||
normalized.forEach((branch, index) => {
|
normalized.forEach((branch, index) => {
|
||||||
try {
|
try {
|
||||||
const nodeNumber = nextNumber();
|
const nodeNumber = nextNumber();
|
||||||
const row = renderNodeCard(branch?.node, `Ответ ${index + 1}`, handlers, nodeNumber, routeKey);
|
const row = renderNodeCard(branch?.node, `Ответ ${index + 1}`, handlers, nodeNumber);
|
||||||
row.classList.add('thread-node-level');
|
row.classList.add('thread-node-level');
|
||||||
row.style.setProperty('--depth', String(Math.min(depth, 4)));
|
row.style.setProperty('--depth', String(Math.min(depth, 4)));
|
||||||
wrap.append(row);
|
wrap.append(row);
|
||||||
|
|
||||||
if (Array.isArray(branch?.children) && branch.children.length) {
|
if (Array.isArray(branch?.children) && branch.children.length) {
|
||||||
wrap.append(renderDescendants(branch.children, handlers, nextNumber, routeKey, depth + 1));
|
wrap.append(renderDescendants(branch.children, handlers, nextNumber, depth + 1));
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logThreadRuntimeError('render_descendants_branch', error, { depth, index });
|
logThreadRuntimeError('render_descendants_branch', error, { depth, index });
|
||||||
@ -655,7 +615,7 @@ export function render({ navigate, route }) {
|
|||||||
title.textContent = 'Предыдущие сообщения';
|
title.textContent = 'Предыдущие сообщения';
|
||||||
ancestorsWrap.append(title);
|
ancestorsWrap.append(title);
|
||||||
ancestors.forEach((node, index) => {
|
ancestors.forEach((node, index) => {
|
||||||
ancestorsWrap.append(renderNodeCard(node, `Предок ${index + 1}`, handlers, nextNumber(), routeKey));
|
ancestorsWrap.append(renderNodeCard(node, `Предок ${index + 1}`, handlers, nextNumber()));
|
||||||
});
|
});
|
||||||
screen.append(ancestorsWrap);
|
screen.append(ancestorsWrap);
|
||||||
}
|
}
|
||||||
@ -663,7 +623,7 @@ export function render({ navigate, route }) {
|
|||||||
if (focus) {
|
if (focus) {
|
||||||
const focusWrap = document.createElement('div');
|
const focusWrap = document.createElement('div');
|
||||||
focusWrap.className = 'stack thread-block thread-block--focus';
|
focusWrap.className = 'stack thread-block thread-block--focus';
|
||||||
focusWrap.append(renderNodeCard(focus, '', handlers, nextNumber(), routeKey, { showViews: false }));
|
focusWrap.append(renderNodeCard(focus, '', handlers, nextNumber()));
|
||||||
screen.append(focusWrap);
|
screen.append(focusWrap);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -675,7 +635,7 @@ export function render({ navigate, route }) {
|
|||||||
descendantsWrap.append(descendantsTitle);
|
descendantsWrap.append(descendantsTitle);
|
||||||
|
|
||||||
if (descendants.length) {
|
if (descendants.length) {
|
||||||
descendantsWrap.append(renderDescendants(descendants, handlers, nextNumber, routeKey));
|
descendantsWrap.append(renderDescendants(descendants, handlers, nextNumber));
|
||||||
} else {
|
} else {
|
||||||
const empty = document.createElement('div');
|
const empty = document.createElement('div');
|
||||||
empty.className = 'card meta-muted';
|
empty.className = 'card meta-muted';
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
import {
|
import {
|
||||||
authService,
|
authService,
|
||||||
getMessageReactionState,
|
getMessageReactionState,
|
||||||
|
setChannelsFeed,
|
||||||
setMessageReactionState,
|
setMessageReactionState,
|
||||||
state,
|
state,
|
||||||
} from '../state.js';
|
} from '../state.js';
|
||||||
@ -459,6 +460,13 @@ async function loadFromApi(route, channelId) {
|
|||||||
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
const messages = Array.isArray(payload.messages) ? payload.messages : [];
|
||||||
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
|
const posts = messages.map((message, index) => mapApiMessageToPost(message, selector, index + 1));
|
||||||
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
|
const ownerLogin = String(payload.channel?.ownerLogin || '').trim();
|
||||||
|
const isOwnChannel = ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase();
|
||||||
|
const followedRows = Array.isArray(state.channelsFeed?.followedChannels) ? state.channelsFeed.followedChannels : [];
|
||||||
|
const isSubscribed = followedRows.some((row) => (
|
||||||
|
String(row?.channel?.ownerBlockchainName || '') === String(selector.ownerBlockchainName || '')
|
||||||
|
&& Number(row?.channel?.channelRoot?.blockNumber) === Number(selector.channelRootBlockNumber)
|
||||||
|
&& normalizeRouteHash(row?.channel?.channelRoot?.blockHash) === normalizeRouteHash(selector.channelRootBlockHash)
|
||||||
|
));
|
||||||
|
|
||||||
return {
|
return {
|
||||||
channel: {
|
channel: {
|
||||||
@ -468,7 +476,8 @@ async function loadFromApi(route, channelId) {
|
|||||||
ownerName: ownerLogin || 'неизвестно',
|
ownerName: ownerLogin || 'неизвестно',
|
||||||
},
|
},
|
||||||
posts,
|
posts,
|
||||||
isOwnChannel: ownerLogin.toLowerCase() === (state.session.login || '').toLowerCase(),
|
isOwnChannel,
|
||||||
|
isSubscribed,
|
||||||
selector,
|
selector,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -762,7 +771,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
|||||||
onSubmit: async (bodyText) => handlers.onAddPost(bodyText),
|
onSubmit: async (bodyText) => handlers.onAddPost(bodyText),
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
} else {
|
} else if (!channelData.isSubscribed) {
|
||||||
actionButton.addEventListener('click', handlers.onSubscribeChannel);
|
actionButton.addEventListener('click', handlers.onSubscribeChannel);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -771,7 +780,11 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
|
|||||||
backButton.textContent = 'Назад к каналам';
|
backButton.textContent = 'Назад к каналам';
|
||||||
backButton.addEventListener('click', () => navigate('channels-list'));
|
backButton.addEventListener('click', () => navigate('channels-list'));
|
||||||
|
|
||||||
screen.append(head, actionButton, feed, backButton);
|
if (channelData.isOwnChannel || !channelData.isSubscribed) {
|
||||||
|
screen.append(head, actionButton, feed, backButton);
|
||||||
|
} else {
|
||||||
|
screen.append(head, feed, backButton);
|
||||||
|
}
|
||||||
|
|
||||||
applyPendingScroll(screen, routeKey);
|
applyPendingScroll(screen, routeKey);
|
||||||
return () => {
|
return () => {
|
||||||
@ -967,8 +980,11 @@ export function render({ navigate, route }) {
|
|||||||
unfollow: false,
|
unfollow: false,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const feed = await authService.listSubscriptionsFeed(login, 200);
|
||||||
|
setChannelsFeed(feed, state.channelsIndex);
|
||||||
softHaptic(15);
|
softHaptic(15);
|
||||||
showToast('Подписка на канал выполнена');
|
showToast('Подписка на канал выполнена');
|
||||||
|
rerender();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
showStatus(toUserMessage(error, 'Не удалось подписаться на канал.'));
|
showStatus(toUserMessage(error, 'Не удалось подписаться на канал.'));
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ 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', 'dialogs', 'my'];
|
const TAB_ORDER = ['dialogs', 'feed', 'my'];
|
||||||
|
|
||||||
function isChannelsDemoMode() {
|
function isChannelsDemoMode() {
|
||||||
try {
|
try {
|
||||||
@ -98,15 +98,6 @@ async function resolveChannelTargetFromInput(rawInput) {
|
|||||||
const input = String(rawInput || '').trim();
|
const input = String(rawInput || '').trim();
|
||||||
if (!input) throw new Error('Введите канал.');
|
if (!input) throw new Error('Введите канал.');
|
||||||
|
|
||||||
const bySelector = input.match(/^([A-Za-z0-9._-]+-\d+)\s*[:/]\s*(\d+)\s*[:/]\s*([A-Fa-f0-9]{1,64})$/);
|
|
||||||
if (bySelector) {
|
|
||||||
return {
|
|
||||||
ownerBlockchainName: bySelector[1],
|
|
||||||
rootBlockNumber: Number(bySelector[2]),
|
|
||||||
rootBlockHash: normalizeHash(bySelector[3]),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const byOwnerAndName = input.match(/^@?([^/#\s]+)\s*\/\s*#?(.+)$/);
|
const byOwnerAndName = input.match(/^@?([^/#\s]+)\s*\/\s*#?(.+)$/);
|
||||||
if (byOwnerAndName) {
|
if (byOwnerAndName) {
|
||||||
const ownerLogin = normalizeLoginInput(byOwnerAndName[1]);
|
const ownerLogin = normalizeLoginInput(byOwnerAndName[1]);
|
||||||
@ -242,7 +233,7 @@ function isFollowedChannelVisible(target) {
|
|||||||
|
|
||||||
function openSimpleSubscribeModal({ kind, kindLabel, submitLabel, unfollow = false, onSuccess }) {
|
function openSimpleSubscribeModal({ kind, kindLabel, submitLabel, unfollow = false, onSuccess }) {
|
||||||
const targetHint = kind === 'channel'
|
const targetHint = kind === 'channel'
|
||||||
? '<p class="meta-muted">Канал: user/channel, имя канала или bch:number:hash.</p>'
|
? '<p class="meta-muted">Канал: user/channel или имя канала.</p>'
|
||||||
: '<p class="meta-muted">Автор: @login или login.</p>';
|
: '<p class="meta-muted">Автор: @login или login.</p>';
|
||||||
const submitText = submitLabel || (unfollow ? 'Отписаться' : 'Подписаться');
|
const submitText = submitLabel || (unfollow ? 'Отписаться' : 'Подписаться');
|
||||||
const placeholder = kind === 'channel' ? '@owner/channel' : '@login';
|
const placeholder = kind === 'channel' ? '@owner/channel' : '@login';
|
||||||
@ -457,6 +448,38 @@ function openChannelFinderModal({ navigate }) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderChannelRows = (values) => {
|
||||||
|
channelsEl.innerHTML = '';
|
||||||
|
if (!values.length) {
|
||||||
|
channelsEl.style.display = 'none';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
channelsEl.style.display = '';
|
||||||
|
values.forEach((item) => {
|
||||||
|
const row = document.createElement('div');
|
||||||
|
row.className = 'channel-search-item';
|
||||||
|
|
||||||
|
const label = document.createElement('span');
|
||||||
|
label.textContent = item.label;
|
||||||
|
|
||||||
|
const openBtn = document.createElement('button');
|
||||||
|
openBtn.type = 'button';
|
||||||
|
openBtn.className = 'secondary-btn small-btn';
|
||||||
|
openBtn.textContent = 'Просмотреть';
|
||||||
|
openBtn.addEventListener('click', () => {
|
||||||
|
close();
|
||||||
|
navigate(`channel/${encodeRoutePart(item.ownerLogin)}/${encodeRoutePart(item.channelName)}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
row.style.display = 'flex';
|
||||||
|
row.style.alignItems = 'center';
|
||||||
|
row.style.justifyContent = 'space-between';
|
||||||
|
row.style.gap = '8px';
|
||||||
|
row.append(label, openBtn);
|
||||||
|
channelsEl.append(row);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
const loadChannelsForLogin = async (login, filterChannel = '') => {
|
const loadChannelsForLogin = async (login, filterChannel = '') => {
|
||||||
const ownerLogin = normalizeLoginInput(login);
|
const ownerLogin = normalizeLoginInput(login);
|
||||||
if (!ownerLogin) return;
|
if (!ownerLogin) return;
|
||||||
@ -469,10 +492,7 @@ function openChannelFinderModal({ navigate }) {
|
|||||||
.filter((name) => !needle || name.toLowerCase().includes(needle))
|
.filter((name) => !needle || name.toLowerCase().includes(needle))
|
||||||
.slice(0, 200)
|
.slice(0, 200)
|
||||||
.map((name) => ({ label: `${ownerLogin}/${name}`, ownerLogin, channelName: name }));
|
.map((name) => ({ label: `${ownerLogin}/${name}`, ownerLogin, channelName: name }));
|
||||||
renderButtons(channelsEl, channels, (item) => {
|
renderChannelRows(channels);
|
||||||
close();
|
|
||||||
navigate(`channel/${encodeRoutePart(item.ownerLogin)}/${encodeRoutePart(item.channelName)}`);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const refresh = createDebounced(async () => {
|
const refresh = createDebounced(async () => {
|
||||||
@ -999,19 +1019,14 @@ function renderListContent({ screen, container, listState, navigate, refreshFeed
|
|||||||
container.append(list);
|
container.append(list);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateBottomCta({ button, listState, navigate, onReload, isTabEmpty = false }) {
|
function updateBottomCta({ button, listState, navigate, isTabEmpty = false }) {
|
||||||
const tab = listState.activeTab;
|
const tab = listState.activeTab;
|
||||||
const baseClass = `primary-btn channels-bottom-action${isTabEmpty && tab !== 'my' ? ' is-empty-lift' : ''}`;
|
const baseClass = `primary-btn channels-bottom-action${isTabEmpty && tab !== 'my' ? ' is-empty-lift' : ''}`;
|
||||||
|
|
||||||
if (tab === 'feed') {
|
if (tab === 'feed') {
|
||||||
button.textContent = 'Подписаться на канал';
|
button.textContent = 'Найти канал';
|
||||||
button.className = baseClass;
|
button.className = baseClass;
|
||||||
button.onclick = () => openSimpleSubscribeModal({
|
button.onclick = () => openChannelFinderModal({ navigate });
|
||||||
kind: 'channel',
|
|
||||||
kindLabel: 'Подписка на канал',
|
|
||||||
submitLabel: 'Подписаться',
|
|
||||||
onSuccess: onReload,
|
|
||||||
});
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1078,7 +1093,7 @@ export function render({ navigate, route }) {
|
|||||||
const listState = {
|
const listState = {
|
||||||
activeTab: TAB_ORDER.includes(String(route?.params?.mode || '').trim())
|
activeTab: TAB_ORDER.includes(String(route?.params?.mode || '').trim())
|
||||||
? String(route?.params?.mode).trim()
|
? String(route?.params?.mode).trim()
|
||||||
: 'dialogs',
|
: 'feed',
|
||||||
openMenuId: null,
|
openMenuId: null,
|
||||||
notificationsState,
|
notificationsState,
|
||||||
revealedCounters: new Set(),
|
revealedCounters: new Set(),
|
||||||
@ -1140,7 +1155,6 @@ export function render({ navigate, route }) {
|
|||||||
button: bottomCta,
|
button: bottomCta,
|
||||||
listState,
|
listState,
|
||||||
navigate,
|
navigate,
|
||||||
onReload: reloadFeed,
|
|
||||||
isTabEmpty,
|
isTabEmpty,
|
||||||
});
|
});
|
||||||
tabsEl.querySelectorAll('.channels-tab-btn').forEach((btn, idx) => {
|
tabsEl.querySelectorAll('.channels-tab-btn').forEach((btn, idx) => {
|
||||||
@ -1180,7 +1194,6 @@ export function render({ navigate, route }) {
|
|||||||
button: bottomCta,
|
button: bottomCta,
|
||||||
listState,
|
listState,
|
||||||
navigate,
|
navigate,
|
||||||
onReload: reloadFeed,
|
|
||||||
isTabEmpty: true,
|
isTabEmpty: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user