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 = ` + + ${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; }