diff --git a/Dev_Docs/Pending_Features/2026-05-19_1428_шапка-канала-и-унификация-тред-карточек.md b/Dev_Docs/Pending_Features/2026-05-19_1428_шапка-канала-и-унификация-тред-карточек.md
new file mode 100644
index 0000000..da13232
--- /dev/null
+++ b/Dev_Docs/Pending_Features/2026-05-19_1428_шапка-канала-и-унификация-тред-карточек.md
@@ -0,0 +1,27 @@
+# Шапка канала и унификация карточек в треде
+
+- Краткое описание:
+ - В `channel-view` убрана отдельная кнопка `О канале` из тела экрана.
+ - В шапке добавлена одна кнопка-лейбл формата `owner/channel` на уровне стрелки назад.
+ - Нажатие по кнопке `owner/channel` открывает тот же модал «О канале».
+ - В `channel-thread-view` карточки сообщений приведены к виду, аналогичному карточкам в канале:
+ - верхняя плитка автора (аватар, логин, номер, время),
+ - действия `Лайк`, `Ответить`, `Тред`, `Отправить`.
+ - Клик по телу карточки в треде теперь открывает вложенный (более глубокий) тред этого сообщения.
+ - Уменьшены отступы между карточками/блоками в треде.
+
+- Что проверять:
+ - В канале в шапке справа отображается единая кнопка `owner/channel`.
+ - Кнопка `owner/channel` открывает модал «О канале».
+ - Старой кнопки `О канале` в контенте экрана нет.
+ - В треде визуал карточек совпадает по паттерну с каналом.
+ - В треде клик по телу сообщения ведёт глубже в тред.
+ - Клик по плитке автора в треде ведёт в профиль пользователя.
+ - Межкарточные отступы в треде компактнее.
+
+- Ожидаемый результат:
+ - Шапка канала и карточки треда выглядят и работают единообразно.
+ - Навигация по вложенным тредам выполняется кликом по сообщению.
+
+- Статус:
+ - `pending`
diff --git a/VERSION.properties b/VERSION.properties
index 8edead6..2f5c123 100644
--- a/VERSION.properties
+++ b/VERSION.properties
@@ -1,2 +1,2 @@
-client.version=1.2.68
-server.version=1.2.62
+client.version=1.2.69
+server.version=1.2.63
diff --git a/shine-UI/js/pages/channel-thread-view.js b/shine-UI/js/pages/channel-thread-view.js
index 211f586..c81ed54 100644
--- a/shine-UI/js/pages/channel-thread-view.js
+++ b/shine-UI/js/pages/channel-thread-view.js
@@ -314,14 +314,12 @@ function openReplyModal({ onSubmit, navigate }) {
function renderNodeCard(node, heading, handlers, localNumber) {
const card = document.createElement('article');
- card.className = 'card stack thread-node-card';
+ card.className = 'card stack thread-node-card channel-message-card';
const author = node?.authorLogin || 'автор';
const text = resolveNodeText(node) || '(пусто)';
const likes = Number(node?.likesCount || 0);
const replies = Number(node?.repliesCount || 0);
- const versions = Number(node?.versionsTotal || 1);
- const changes = Math.max(0, versions - 1);
const headingText = String(heading || '').trim();
if (headingText) {
@@ -331,18 +329,36 @@ function renderNodeCard(node, heading, handlers, localNumber) {
card.append(headingEl);
}
- const meta = document.createElement('p');
- meta.className = 'thread-node-meta';
- meta.innerHTML = `
- ${author}
- · #${localNumber}
- `;
+ const authorTile = document.createElement('button');
+ authorTile.type = 'button';
+ authorTile.className = 'channel-message-author-tile';
+
+ const avatar = document.createElement('div');
+ avatar.className = 'channel-message-avatar';
+ avatar.textContent = String(author || 'A').trim().charAt(0).toUpperCase() || 'A';
+
+ const authorBlock = document.createElement('div');
+ authorBlock.className = 'channel-message-author';
+ const title = document.createElement('div');
+ title.className = 'channel-message-title author-line';
+ const loginEl = document.createElement('span');
+ loginEl.className = 'author-line-login';
+ loginEl.textContent = author;
+ const numberEl = document.createElement('span');
+ numberEl.className = 'author-line-num';
+ numberEl.textContent = `· #${localNumber}`;
+ const timestamp = document.createElement('div');
+ timestamp.className = 'channel-message-time';
+ timestamp.textContent = node?.createdAtMs ? new Date(node.createdAtMs).toLocaleString() : '—';
+ title.append(loginEl, numberEl);
+ authorBlock.append(title, timestamp);
+ authorTile.append(avatar, authorBlock);
const body = document.createElement('p');
- body.className = 'thread-node-body';
+ body.className = 'channel-message-body';
body.textContent = text;
- card.append(meta, body);
+ card.append(authorTile, body);
const target = buildTargetFromNode(node);
const refKey = messageRefKey(target);
@@ -358,15 +374,20 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const isLiked = getMessageReactionState(target) === 'liked';
const actions = document.createElement('div');
- actions.className = 'thread-node-actions';
+ actions.className = 'thread-node-actions channel-message-actions';
const likeButton = document.createElement('button');
likeButton.type = 'button';
- likeButton.className = 'secondary-btn thread-like-btn';
+ likeButton.className = 'channel-action-item thread-like-btn';
if (isLiked) likeButton.classList.add('is-liked');
- likeButton.textContent = isPending ? `❤️ ${likes}...` : `${isLiked ? '❤️' : '🤍'} ${likes}`;
+ likeButton.innerHTML = `
+ ${isLiked ? '❤️' : '🤍'}
+ ${isPending ? 'Лайк...' : 'Лайк'}
+ ${likes}
+ `;
likeButton.disabled = isPending;
likeButton.addEventListener('click', async (event) => {
+ event.stopPropagation();
animatePress(event.currentTarget);
if (isPending) return;
if (!isLiked) {
@@ -375,7 +396,6 @@ function renderNodeCard(node, heading, handlers, localNumber) {
}
await longPressFeel(event.currentTarget, 130);
likeButton.disabled = true;
- likeButton.textContent = `❤️ ${likes}...`;
try {
await handlers.onToggleLike(target, isLiked ? 'unlike' : 'like');
} catch (error) {
@@ -390,9 +410,14 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const replyButton = document.createElement('button');
replyButton.type = 'button';
- replyButton.className = 'secondary-btn thread-reply-btn';
- replyButton.textContent = `💬 ${replies}`;
+ replyButton.className = 'channel-action-item thread-reply-btn';
+ replyButton.innerHTML = `
+ 💬
+ Ответить
+ ${replies}
+ `;
replyButton.addEventListener('click', (event) => {
+ event.stopPropagation();
animatePress(event.currentTarget);
openReplyModal({
navigate: handlers.navigate,
@@ -400,17 +425,13 @@ function renderNodeCard(node, heading, handlers, localNumber) {
});
});
- 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');
shareButton.type = 'button';
- shareButton.className = 'secondary-btn thread-share-btn';
- shareButton.textContent = '↗ Отправить';
+ shareButton.className = 'channel-action-item thread-share-btn';
+ shareButton.innerHTML = `
+ ↗
+ Отправить
+ `;
shareButton.addEventListener('click', async (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
@@ -419,16 +440,28 @@ function renderNodeCard(node, heading, handlers, localNumber) {
const openThreadButton = document.createElement('button');
openThreadButton.type = 'button';
- openThreadButton.className = 'secondary-btn thread-open-btn';
- openThreadButton.textContent = '🧵 В тред';
+ openThreadButton.className = 'channel-action-item thread-open-btn';
+ openThreadButton.innerHTML = `
+ #
+ Тред
+ `;
openThreadButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
handlers.onOpenThread(target);
});
- actions.append(likeButton, replyButton, changedButton, shareButton, openThreadButton);
+ actions.append(likeButton, replyButton, openThreadButton, shareButton);
card.append(actions);
+ authorTile.addEventListener('click', (event) => {
+ event.stopPropagation();
+ const login = String(node?.authorLogin || '').trim();
+ if (!login) return;
+ handlers.navigate(`user/${encodeRoutePart(login)}`);
+ });
+ card.addEventListener('click', () => {
+ handlers.onOpenThread(target);
+ });
return card;
}
diff --git a/shine-UI/js/pages/channel-view.js b/shine-UI/js/pages/channel-view.js
index 38c2c20..58b0364 100644
--- a/shine-UI/js/pages/channel-view.js
+++ b/shine-UI/js/pages/channel-view.js
@@ -766,36 +766,11 @@ function renderPostCard(post, {
}
function renderBody(screen, navigate, routeKey, channelData, handlers) {
- const head = document.createElement('div');
- head.className = 'card channel-head-card';
-
- const title = document.createElement('strong');
- title.className = 'channel-head-title';
- title.textContent = String(channelData.channel.name || '').trim();
-
- const owner = document.createElement('p');
- owner.className = 'channel-head-meta';
- owner.textContent = `Владелец: ${channelData.channel.ownerName}`;
-
- const headActions = document.createElement('div');
- headActions.className = 'channel-head-actions';
- const aboutButton = document.createElement('button');
- aboutButton.type = 'button';
- aboutButton.className = 'secondary-btn small-btn';
- aboutButton.textContent = 'О канале';
- aboutButton.addEventListener('click', (event) => {
- animatePress(event.currentTarget);
- openAboutChannelModal(channelData.channel);
- });
- headActions.append(aboutButton);
-
- head.append(title);
- head.append(owner, headActions);
if (channelData.reverseChannelMissingWarning) {
const reverseWarning = document.createElement('p');
reverseWarning.className = 'channel-head-meta';
reverseWarning.textContent = channelData.reverseChannelMissingWarning;
- head.append(reverseWarning);
+ screen.append(reverseWarning);
}
const actionButton = document.createElement('button');
@@ -850,9 +825,9 @@ function renderBody(screen, navigate, routeKey, channelData, handlers) {
backButton.addEventListener('click', () => navigate('channels-list'));
if (channelData.isOwnChannel || !channelData.isSubscribed) {
- screen.append(head, actionButton, feed, backButton);
+ screen.append(actionButton, feed, backButton);
} else {
- screen.append(head, feed, backButton);
+ screen.append(feed, backButton);
}
applyPendingScroll(screen, routeKey);
@@ -893,6 +868,17 @@ export function render({ navigate, route }) {
statusBox.style.display = '';
};
+ const header = renderHeader({
+ title: '',
+ leftAction: { label: '<', onClick: () => navigateBack() },
+ rightActions: [{ label: 'Канал', onClick: () => {} }],
+ });
+ const channelHeaderButton = header.querySelector('.header-actions .icon-btn');
+ if (channelHeaderButton) {
+ channelHeaderButton.classList.add('channel-header-route-btn');
+ channelHeaderButton.disabled = true;
+ }
+
const rerender = () => {
const current = document.querySelector('section.channels-screen--channel');
if (!current) return;
@@ -990,12 +976,7 @@ export function render({ navigate, route }) {
rerender();
};
- screen.append(
- renderHeader({
- title: '',
- leftAction: { label: '<', onClick: () => navigateBack() },
- }),
- );
+ screen.append(header);
screen.append(statusBox);
const skeleton = renderSkeleton(screen);
@@ -1006,6 +987,15 @@ export function render({ navigate, route }) {
try {
const apiData = await loadFromApi(route, channelId);
activeSelector = apiData?.selector || null;
+ const channelRouteLabel = `${apiData?.channel?.ownerName || 'owner'}/${apiData?.channel?.name || 'channel'}`;
+ if (channelHeaderButton) {
+ channelHeaderButton.textContent = channelRouteLabel;
+ channelHeaderButton.disabled = false;
+ channelHeaderButton.onclick = (event) => {
+ animatePress(event.currentTarget);
+ openAboutChannelModal(apiData.channel);
+ };
+ }
skeleton.remove();
cleanupSeenTracking = renderBody(screen, navigate, routeKey, apiData, {
onToggleLike: async (messageRef, action) => {
diff --git a/shine-UI/styles/components.css b/shine-UI/styles/components.css
index 977def9..2562680 100644
--- a/shine-UI/styles/components.css
+++ b/shine-UI/styles/components.css
@@ -2341,6 +2341,14 @@ textarea.input {
backdrop-filter: blur(12px);
}
+.channel-header-route-btn {
+ max-width: 68vw;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ font-size: 13px;
+}
+
.thread-node-heading {
color: #f1dcab;
font-size: 15px;
@@ -2381,6 +2389,11 @@ textarea.input {
gap: 8px;
}
+.channels-screen--thread .thread-node-actions {
+ display: flex !important;
+ grid-template-columns: none !important;
+}
+
.thread-node-level {
--depth: 0;
margin-left: calc(var(--depth) * 12px);
@@ -2389,11 +2402,16 @@ textarea.input {
.thread-block {
gap: 8px;
border-radius: 15px;
- padding: 10px;
+ padding: 8px;
border: 1px solid rgba(151, 174, 221, 0.2);
background: linear-gradient(160deg, rgba(10, 21, 43, 0.72), rgba(8, 16, 31, 0.78));
}
+.channels-screen--thread .thread-node-card {
+ padding: 14px;
+ margin-bottom: 10px;
+}
+
.thread-block--ancestors > .section-title {
color: #b9cbef;
}