UI: отправка UI-ошибок, персональный публичный чат, русские pending-файлы

This commit is contained in:
AidarKC
2026-05-14 14:16:03 +03:00
parent e73e103ac4
commit 56a69ab683
21 changed files with 488 additions and 153 deletions
+57 -100
View File
@@ -22,7 +22,6 @@ export const pageMeta = { id: 'channel-view', title: 'Канал' };
const pendingReactionActions = new Set();
const pendingScrollByRoute = new Map();
const revealedCountersByRoute = new Map();
function isChannelsDemoMode() {
try {
@@ -95,29 +94,6 @@ function blockRefToMessageKey(blockRef, fallbackBch = '') {
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 = '') {
const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
const url = new URL(window.location.href);
@@ -128,9 +104,9 @@ function buildAbsoluteRouteUrl(routePath = '') {
function buildSelectorFromRoute(route, channelId) {
const params = route?.params || {};
if (params.ownerLogin && params.channelName) {
if (params.ownerBlockchainName && params.channelName) {
return {
ownerLogin: String(params.ownerLogin || '').trim(),
ownerBlockchainName: String(params.ownerBlockchainName || '').trim(),
channelName: String(params.channelName || '').trim(),
};
}
@@ -157,15 +133,14 @@ function buildSelectorFromRoute(route, channelId) {
function buildThreadRoute(messageRef, selector) {
if (!messageRef || !selector) return '';
const ownerLogin = String(selector.ownerLogin || '').trim();
const ownerBlockchainName = String(selector.ownerBlockchainName || '').trim();
const channelName = String(selector.channelName || '').trim();
if (ownerLogin && channelName) {
if (ownerBlockchainName && channelName) {
return [
'channel',
encodeRoutePart(ownerLogin),
encodeRoutePart(ownerBlockchainName),
encodeRoutePart(channelName),
messageRef.blockNumber,
normalizeRouteHash(messageRef.blockHash),
].join('/');
}
return [
@@ -434,11 +409,16 @@ function mapApiMessageToPost(message, selector, localNumber) {
async function loadFromApi(route, channelId) {
let selector = buildSelectorFromRoute(route, channelId);
if (selector?.ownerLogin && selector?.channelName) {
const ownerFeed = await authService.listSubscriptionsFeed(selector.ownerLogin, 1000);
const ownChannels = Array.isArray(ownerFeed?.ownedChannels) ? ownerFeed.ownedChannels : [];
const channel = ownChannels.find((item) => (
String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
if (selector?.ownerBlockchainName && selector?.channelName) {
const ownFeed = await authService.listSubscriptionsFeed(state.session.login, 1000);
const allRows = [
...(Array.isArray(ownFeed?.ownedChannels) ? ownFeed.ownedChannels : []),
...(Array.isArray(ownFeed?.followedUsersChannels) ? ownFeed.followedUsersChannels : []),
...(Array.isArray(ownFeed?.followedChannels) ? ownFeed.followedChannels : []),
];
const channel = allRows.find((item) => (
String(item?.channel?.ownerBlockchainName || '').trim() === selector.ownerBlockchainName
&& String(item?.channel?.channelName || '').trim().toLowerCase() === selector.channelName.toLowerCase()
));
if (!channel?.channel?.ownerBlockchainName || channel?.channel?.channelRoot?.blockNumber == null) {
throw new Error('Канал не найден.');
@@ -447,7 +427,6 @@ async function loadFromApi(route, channelId) {
ownerBlockchainName: String(channel.channel.ownerBlockchainName),
channelRootBlockNumber: Number(channel.channel.channelRoot.blockNumber),
channelRootBlockHash: normalizeRouteHash(channel.channel.channelRoot.blockHash),
ownerLogin: selector.ownerLogin,
channelName: selector.channelName,
};
}
@@ -550,9 +529,7 @@ function applyPendingScroll(screen, routeKey) {
function renderPostCard(post, {
navigate,
routeKey,
selector,
canWrite,
onToggleLike,
onReply,
onShare,
@@ -598,72 +575,57 @@ function renderPostCard(post, {
if (refKey) {
card.dataset.messageKey = refKey;
}
const countersVisible = refKey ? isCounterVisible(routeKey, refKey) : true;
if (!countersVisible) {
card.classList.remove('is-counters-visible');
} else {
card.classList.add('is-counters-visible');
}
const revealCounters = () => {
if (!refKey) return;
revealCounter(routeKey, refKey);
card.classList.add('is-counters-visible');
};
card.addEventListener('click', revealCounters);
card.classList.add('is-counters-visible');
if (!post.messageRef || !selector) return card;
const actions = document.createElement('div');
actions.className = 'channel-message-actions';
if (canWrite) {
const actionKey = makeReactionActionKey(post.messageRef);
const isPending = actionKey ? pendingReactionActions.has(actionKey) : false;
const actionKey = makeReactionActionKey(post.messageRef);
const isPending = actionKey ? pendingReactionActions.has(actionKey) : false;
const likeButton = document.createElement('button');
likeButton.type = 'button';
likeButton.className = 'channel-action-item channel-action-like';
const isLiked = post.reactionState === 'liked';
if (isLiked) likeButton.classList.add('is-liked');
likeButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
<span class="channel-action-counter">${post.likesCount || 0}</span>
`;
likeButton.disabled = isPending;
likeButton.addEventListener('click', async (event) => {
animatePress(event.currentTarget);
if (isPending) return;
if (!isLiked) {
const ok = window.confirm('Поставить лайк?');
if (!ok) return;
}
revealCounters();
await longPressFeel(event.currentTarget, 130);
likeButton.disabled = true;
const labelEl = likeButton.querySelector('.channel-action-label');
if (labelEl) labelEl.textContent = 'Лайк...';
await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
});
const likeButton = document.createElement('button');
likeButton.type = 'button';
likeButton.className = 'channel-action-item channel-action-like';
const isLiked = post.reactionState === 'liked';
if (isLiked) likeButton.classList.add('is-liked');
likeButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">${isLiked ? '❤️' : '🤍'}</span>
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
<span class="channel-action-counter">${post.likesCount || 0}</span>
`;
likeButton.disabled = isPending;
likeButton.addEventListener('click', async (event) => {
animatePress(event.currentTarget);
if (isPending) return;
if (!isLiked) {
const ok = window.confirm('Поставить лайк?');
if (!ok) return;
}
await longPressFeel(event.currentTarget, 130);
likeButton.disabled = true;
const labelEl = likeButton.querySelector('.channel-action-label');
if (labelEl) labelEl.textContent = 'Лайк...';
await onToggleLike(post.messageRef, isLiked ? 'unlike' : 'like', { likeButton });
});
const replyButton = document.createElement('button');
replyButton.type = 'button';
replyButton.className = 'channel-action-item channel-action-reply';
replyButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true"></span>
<span class="channel-action-label">Ответить</span>
`;
replyButton.addEventListener('click', (event) => {
animatePress(event.currentTarget);
revealCounters();
openReplyModal({
navigate,
onSubmit: async (text) => onReply(post.messageRef, text),
});
const replyButton = document.createElement('button');
replyButton.type = 'button';
replyButton.className = 'channel-action-item channel-action-reply';
replyButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">💬</span>
<span class="channel-action-label">Ответить</span>
<span class="channel-action-counter">${post.repliesCount || 0}</span>
`;
replyButton.addEventListener('click', (event) => {
animatePress(event.currentTarget);
openReplyModal({
navigate,
onSubmit: async (text) => onReply(post.messageRef, text),
});
actions.append(likeButton, replyButton);
}
});
actions.append(likeButton, replyButton);
const openThreadButton = document.createElement('button');
openThreadButton.type = 'button';
@@ -671,11 +633,9 @@ function renderPostCard(post, {
openThreadButton.innerHTML = `
<span class="channel-action-icon" aria-hidden="true">#</span>
<span class="channel-action-label">Тред</span>
<span class="channel-action-counter">${post.repliesCount || 0}</span>
`;
openThreadButton.addEventListener('click', (event) => {
animatePress(event.currentTarget);
revealCounters();
const route = buildThreadRoute(post.messageRef, selector);
if (route) navigate(route);
});
@@ -690,7 +650,6 @@ function renderPostCard(post, {
shareButton.addEventListener('click', async (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
revealCounters();
const route = buildThreadRoute(post.messageRef, selector);
await onShare(route);
});
@@ -741,9 +700,7 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
channelData.posts.forEach((post) => {
const row = renderPostCard(post, {
navigate,
routeKey,
selector: channelData.selector,
canWrite: channelData.isOwnChannel,
onToggleLike: handlers.onToggleLike,
onReply: handlers.onReply,
onShare: handlers.onShare,