SHA256
UI: новый общий стиль, тёмная/светлая тема, настраиваемая палитра
- styles/main.css: роли цветов (по умолчанию «Индиго»), шкала отступов/скруглений/шрифтов, цвета отношений для обеих тем; жёсткие цвета в стилях заменены на роли. - Палитра: пресеты и личные правки, «Оформление» Авто/День/Ночь, долгое нажатие — редактор цветов с экспортом/импортом. - Каналы: пузыри постов автора, плашки дней, строка «Написать в канал…», «О канале», создание канала с адресом из названия; лента открывается на свежих постах. - Чаты: плоский список, чипы-фильтры, пузыри, плашки дней; нижняя панель скрыта в переписке. - Корневые разделы — единая шапка; профиль и чужой профиль — общая карточка; настройки, кошелёк, сеансы — меню-списки. - Нижняя панель: иконки без подписей, бейджи на иконках. - confirmDialog вместо window.confirm/alert; на телефоне диалоги — шторки снизу. - docs/UI-Design/ISSUES-for-dev.md — найденные ошибки сервера/UI. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
import { confirmDialog } from '../components/confirm-dialog.js';
|
||||
import { iconHtml } from '../components/ui-icon.js';
|
||||
import { rememberChannelPosition, readChannelPosition, restoreChannelPosition } from '../services/channel-view-state.js';
|
||||
import { attachMessageMenu } from '../components/message-menu.js';
|
||||
@@ -8,6 +9,7 @@ import { toUserMessage } from '../services/ui-error-texts.js';
|
||||
import {
|
||||
animatePress,
|
||||
createSkeletonCard,
|
||||
formatRelativeTime,
|
||||
longPressFeel,
|
||||
shareOrCopyLink,
|
||||
showToast,
|
||||
@@ -25,7 +27,7 @@ import {
|
||||
import { loadProfileSnapshot } from '../services/user-profile-params.js';
|
||||
import { extractLoginFromBlockchainName, makeProfileRoute, makeShineChannelRoute, makeShineMessageRoute } from '../services/shine-routes.js';
|
||||
|
||||
export const pageMeta = { id: 'channel-thread-view', title: 'Тред', shellMode: { scrollbar: 'hidden', topFade: false, contentUnderTopbar: false, contentWidth: 'wide' } };
|
||||
export const pageMeta = { id: 'channel-thread-view', title: 'Обсуждение', hideToolbar: true, shellMode: { scrollbar: 'hidden', topFade: false, contentUnderTopbar: false, contentWidth: 'wide' } };
|
||||
const MSG_SUBTYPE_TEXT_POST = 10;
|
||||
const MSG_SUBTYPE_TEXT_RATING = 30;
|
||||
const MSG_SUBTYPE_TEXT_ENTRYPOINT = 100;
|
||||
@@ -200,6 +202,13 @@ function allFeedSummaries() {
|
||||
});
|
||||
}
|
||||
|
||||
const channelTitleByLabel = new Map();
|
||||
|
||||
function rememberChannelTitle(label, channel) {
|
||||
const title = String(channel?.displayName || channel?.displayTitle || '').trim();
|
||||
if (label && title) channelTitleByLabel.set(label, title);
|
||||
}
|
||||
|
||||
function resolveChannelDisplayName(channelSelector) {
|
||||
const rootNumber = channelSelector?.channelRootBlockNumber ?? channelSelector?.rootBlockNumber;
|
||||
const rootHashRaw = channelSelector?.channelRootBlockHash ?? channelSelector?.rootBlockHash;
|
||||
@@ -214,7 +223,9 @@ function resolveChannelDisplayName(channelSelector) {
|
||||
&& normalizeRouteHash(summary?.channel?.channelRoot?.blockHash) === rootHash
|
||||
));
|
||||
if (!found) return '';
|
||||
return `${found.channel?.ownerLogin || 'неизвестно'}/${found.channel?.channelName || 'канал'}`;
|
||||
const label = `${found.channel?.ownerLogin || 'неизвестно'}/${found.channel?.channelName || 'канал'}`;
|
||||
rememberChannelTitle(label, found.channel);
|
||||
return label;
|
||||
}
|
||||
|
||||
function resolveChannelHeadingFromNode(node) {
|
||||
@@ -299,7 +310,9 @@ async function resolveChannelDisplayNameFromServer(channelSelector) {
|
||||
if (!row?.channel?.channelName) return '';
|
||||
|
||||
channelSelector.channelRootBlockHash = normalizeRouteHash(row?.channel?.channelRoot?.blockHash);
|
||||
return `${row.channel.ownerLogin || ownerLogin}/${row.channel.channelName}`;
|
||||
const label = `${row.channel.ownerLogin || ownerLogin}/${row.channel.channelName}`;
|
||||
rememberChannelTitle(label, row.channel);
|
||||
return label;
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -446,7 +459,7 @@ function openBlockchainDetailsModal(details, { isActive = () => true } = {}) {
|
||||
<div class="modal" id="thread-blockchain-details-modal">
|
||||
<div class="modal-card stack blockchain-details-card">
|
||||
<h3 class="modal-title">Данные блокчейна сообщения</h3>
|
||||
<p class="meta-muted">Это технические данные записи SHiNE. По ним можно увидеть цепочку автора, номер записи, хэш и подпись, если она есть в ответе сервера.</p>
|
||||
<p class="meta-muted">Это технические данные записи Сияния. По ним можно увидеть цепочку автора, номер записи, хэш и подпись, если она есть в ответе сервера.</p>
|
||||
<div class="blockchain-details-grid">
|
||||
<span>Автор</span><strong>${escapeHtml(details.authorLogin)}</strong>
|
||||
<span>Блокчейн</span><code>${escapeHtml(details.authorBlockchainName)}</code>
|
||||
@@ -702,7 +715,8 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
}
|
||||
const timestamp = document.createElement('div');
|
||||
timestamp.className = 'channel-message-time';
|
||||
timestamp.textContent = node?.createdAtMs ? new Date(node.createdAtMs).toLocaleString() : '—';
|
||||
timestamp.textContent = node?.createdAtMs ? formatRelativeTime(node.createdAtMs) : '—';
|
||||
if (node?.createdAtMs) timestamp.title = new Date(node.createdAtMs).toLocaleString('ru-RU');
|
||||
authorBlock.append(title, timestamp);
|
||||
authorTile.append(avatar, authorBlock);
|
||||
headRow.append(authorTile);
|
||||
@@ -786,7 +800,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
likeButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">${iconHtml('heart', isLiked)}</span>
|
||||
<span class="channel-action-label">${isPending ? 'Лайк...' : 'Лайк'}</span>
|
||||
<span class="channel-action-counter">${likes}</span>
|
||||
<span class="channel-action-counter">${Number(likes) > 0 ? likes : ''}</span>
|
||||
`;
|
||||
likeButton.setAttribute('aria-pressed', String(isLiked));
|
||||
setActionTitle(likeButton, isPending ? 'Лайк…' : `${isLiked ? 'Убрать отметку «Нравится»' : 'Нравится'}, ${likes}`);
|
||||
@@ -817,7 +831,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
replyButton.innerHTML = `
|
||||
<span class="channel-action-icon" aria-hidden="true">${iconHtml('message')}</span>
|
||||
<span class="channel-action-label">Ответить</span>
|
||||
<span class="channel-action-counter">${replies}</span>
|
||||
<span class="channel-action-counter">${Number(replies) > 0 ? replies : ''}</span>
|
||||
`;
|
||||
setActionTitle(replyButton, 'Ответить');
|
||||
replyButton.addEventListener('click', (event) => {
|
||||
@@ -856,7 +870,7 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
const discussionButton = document.createElement('button');
|
||||
discussionButton.type = 'button';
|
||||
discussionButton.className = 'ui-button channel-action-item';
|
||||
discussionButton.innerHTML = `<span class="channel-action-icon">${iconHtml('message')}</span><span>${replies}</span>`;
|
||||
discussionButton.innerHTML = `<span class="channel-action-icon">${iconHtml('message')}</span>${Number(replies) > 0 ? `<span>${replies}</span>` : ''}`;
|
||||
discussionButton.setAttribute('aria-label', `Открыть обсуждение, ответов: ${replies}`);
|
||||
discussionButton.addEventListener('click', () => handlers.onOpenThread(target));
|
||||
actions.append(likeButton, discussionButton, shareButton, replyButton);
|
||||
@@ -871,8 +885,6 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
setActionTitle(originalButton, 'Оригинал');
|
||||
originalButton.addEventListener('click', (event) => {
|
||||
event.stopPropagation();
|
||||
const ok = window.confirm('Перейти к оригинальному сообщению?');
|
||||
if (!ok) return;
|
||||
const ownerLogin = extractLoginFromBlockchainName(repostTarget.blockchainName);
|
||||
if (!ownerLogin) return;
|
||||
handlers.navigate(makeShineMessageRoute({
|
||||
@@ -927,7 +939,12 @@ function renderNodeCard(node, heading, handlers, localNumber) {
|
||||
});
|
||||
menuItems.unshift({ label: 'Редактировать', action: () => editButton.click() });
|
||||
menuItems.push({ label: 'Удалить', danger: true, action: async () => {
|
||||
if (!window.confirm('Скрыть сообщение из ленты? Предыдущие версии останутся в блокчейне и истории изменений.')) return;
|
||||
if (!await confirmDialog({
|
||||
title: 'Удалить сообщение?',
|
||||
text: 'Сообщение скроется из ленты. Предыдущие версии останутся в блокчейне и в истории изменений.',
|
||||
confirmLabel: 'Удалить',
|
||||
danger: true,
|
||||
})) return;
|
||||
try { await handlers.onEdit(target, '', { isChannelPost, isDelete: true }); }
|
||||
catch (error) { if (handlers.isActive()) showToast(toUserMessage(error, 'Не удалось удалить сообщение.')); }
|
||||
} });
|
||||
@@ -1026,21 +1043,18 @@ export function render({ navigate, route, chrome }) {
|
||||
const threadHeaderButton = document.createElement('button');
|
||||
threadHeaderButton.type = 'button';
|
||||
threadHeaderButton.className = 'icon-btn channel-header-route-btn app-topbar-title-action';
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
const threadHeaderTitle = document.createElement('span');
|
||||
threadHeaderTitle.className = 'channel-header-title';
|
||||
threadHeaderTitle.textContent = 'Обсуждение';
|
||||
const threadHeaderChannel = document.createElement('span');
|
||||
threadHeaderChannel.className = 'channel-header-owner';
|
||||
threadHeaderChannel.textContent = '…';
|
||||
threadHeaderButton.append(threadHeaderTitle, threadHeaderChannel);
|
||||
threadHeaderButton.disabled = true;
|
||||
|
||||
const header = createTopBar({
|
||||
center: threadHeaderButton,
|
||||
back: { label: '<', onClick: () => navigate(resolveThreadPreviousInChannels(selector, activeResolvedChannelLabel)) },
|
||||
actions: [
|
||||
{
|
||||
label: '↑',
|
||||
title: 'К списку каналов',
|
||||
ariaLabel: 'К списку каналов',
|
||||
className: 'channel-thread-list-btn',
|
||||
onClick: () => navigate('channels-list'),
|
||||
},
|
||||
],
|
||||
});
|
||||
header.classList.add('channel-thread-topbar');
|
||||
chrome?.setTopbar(header);
|
||||
@@ -1050,7 +1064,7 @@ export function render({ navigate, route, chrome }) {
|
||||
statusBox.style.display = 'none';
|
||||
|
||||
const ensureActive = () => {
|
||||
if (disposed) throw new Error('Экран треда уже закрыт.');
|
||||
if (disposed) throw new Error('Экран обсуждения уже закрыт.');
|
||||
};
|
||||
|
||||
const showStatus = (message) => {
|
||||
@@ -1175,10 +1189,10 @@ export function render({ navigate, route, chrome }) {
|
||||
onShare: async (target) => {
|
||||
try {
|
||||
const routePath = buildThreadRouteFromTarget(target, selector);
|
||||
if (!routePath) throw new Error('Не удалось подготовить ссылку на тред.');
|
||||
if (!routePath) throw new Error('Не удалось подготовить ссылку на обсуждение.');
|
||||
const result = await shareOrCopyLink({
|
||||
title: 'SHiNE · Тред',
|
||||
text: 'Сообщение из треда SHiNE',
|
||||
title: 'Сияние · Обсуждение',
|
||||
text: 'Сообщение из обсуждения в Сиянии',
|
||||
url: buildAbsoluteRouteUrl(routePath),
|
||||
});
|
||||
if (disposed) return;
|
||||
@@ -1192,7 +1206,7 @@ export function render({ navigate, route, chrome }) {
|
||||
onOpenThread: (target) => {
|
||||
const routePath = buildThreadRouteFromTarget(target, selector);
|
||||
if (!routePath) {
|
||||
showStatus('Не удалось определить путь до треда.');
|
||||
showStatus('Не удалось определить ссылку на обсуждение.');
|
||||
return;
|
||||
}
|
||||
navigate(routePath);
|
||||
@@ -1264,14 +1278,14 @@ export function render({ navigate, route, chrome }) {
|
||||
showStatus('');
|
||||
selector = parseThreadSelector(route);
|
||||
activeResolvedChannelLabel = resolveChannelDisplayName(selector?.channel);
|
||||
threadHeaderButton.textContent = 'Тред в канале: ...';
|
||||
threadHeaderChannel.textContent = '…';
|
||||
threadHeaderButton.disabled = true;
|
||||
threadHeaderButton.onclick = null;
|
||||
|
||||
if (!selector) {
|
||||
const invalid = document.createElement('div');
|
||||
invalid.className = 'card meta-muted';
|
||||
invalid.textContent = 'Некорректный идентификатор треда в адресе страницы.';
|
||||
invalid.textContent = 'Обсуждение не найдено: неверная ссылка.';
|
||||
screen.append(invalid);
|
||||
return;
|
||||
}
|
||||
@@ -1382,7 +1396,8 @@ export function render({ navigate, route, chrome }) {
|
||||
const fallbackChannel = String(selector?.channel?.ownerBlockchainName || '').trim() || 'неизвестно';
|
||||
const resolvedChannelTitle = resolvedChannelLabel || fallbackChannel;
|
||||
if (threadHeaderButton) {
|
||||
threadHeaderButton.textContent = `Обсуждение · ${resolvedChannelTitle}`;
|
||||
threadHeaderChannel.textContent = `в канале «${channelTitleByLabel.get(resolvedChannelTitle) || resolvedChannelTitle}»`;
|
||||
threadHeaderButton.title = 'Открыть канал';
|
||||
threadHeaderButton.disabled = false;
|
||||
threadHeaderButton.onclick = (event) => {
|
||||
event.preventDefault();
|
||||
@@ -1422,8 +1437,8 @@ export function render({ navigate, route, chrome }) {
|
||||
composer.className = 'channel-composer';
|
||||
const reply = document.createElement('button');
|
||||
reply.type = 'button';
|
||||
reply.className = 'primary-btn';
|
||||
reply.textContent = state.session.isAuthorized ? 'Написать ответ' : 'Войти и ответить';
|
||||
reply.className = 'channel-compose-bar';
|
||||
reply.innerHTML = `<span class="channel-compose-bar__text">${state.session.isAuthorized ? 'Написать ответ…' : 'Войти и ответить'}</span><span class="channel-compose-bar__icon" aria-hidden="true">${iconHtml('plus')}</span>`;
|
||||
reply.addEventListener('click', () => {
|
||||
const parsed = parseMessageAttachments(resolveNodeText(focus));
|
||||
openReplyModal({
|
||||
@@ -1448,7 +1463,7 @@ export function render({ navigate, route, chrome }) {
|
||||
descendantsWrap.append(renderDescendants(descendants, handlers, nextNumber));
|
||||
} else {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'card meta-muted';
|
||||
empty.className = 'empty-note';
|
||||
empty.textContent = 'Пока нет ответов. Начните обсуждение.';
|
||||
descendantsWrap.append(empty);
|
||||
}
|
||||
@@ -1478,7 +1493,7 @@ export function render({ navigate, route, chrome }) {
|
||||
if (hadContent) { showStatus(toUserMessage(error, 'Не удалось обновить обсуждение.')); return; }
|
||||
const failed = document.createElement('div');
|
||||
failed.className = 'card meta-muted';
|
||||
failed.textContent = `Не удалось загрузить тред: ${toUserMessage(error, 'неизвестная ошибка')}`;
|
||||
failed.textContent = `Не удалось загрузить обсуждение: ${toUserMessage(error, 'неизвестная ошибка')}`;
|
||||
const retry = document.createElement('button');
|
||||
retry.type = 'button';
|
||||
retry.className = 'primary-btn';
|
||||
|
||||
Reference in New Issue
Block a user