UI: карточка автора в канале, профиль user и назад по истории

This commit is contained in:
AidarKC 2026-05-19 13:58:49 +03:00
parent db2d9a666b
commit 90d10086d7
10 changed files with 80 additions and 28 deletions

View File

@ -0,0 +1,25 @@
# Карточка автора в сообщении канала и стрелка «назад» по истории
- Краткое описание:
- В `channel-view` в карточке сообщения добавлена вложенная плитка автора (аватар, логин, номер сообщения, дата/время).
- Клик по плитке автора открывает профиль пользователя.
- Клик по области сообщения (вне плитки автора и вне action-кнопок) открывает тред, как кнопка `Тред`.
- Стрелка `назад` в `channel-view`, `channel-thread-view` и профиле переведена на реальную навигацию `history.back()`.
- Маршрут профиля переименован с `user-profile-view` на `user`.
- Что проверять:
- В канале у каждого сообщения сверху есть вложенная плитка автора.
- Клик по вложенной плитке открывает профиль автора.
- Клик по тексту/телу сообщения открывает тред.
- Кнопки `Лайк`, `Ответить`, `Тред`, `Отправить` работают отдельно и не конфликтуют с кликом по карточке.
- Стрелка `назад` возвращает на предыдущий экран по реальной истории переходов.
- При отсутствии истории стрелка `назад` не делает переход.
- Переходы на профиль работают по новому маршруту `user/{login}/...`.
- Ожидаемый результат:
- Навигация в каналах и тредах соответствует ожидаемому UX.
- Переходы в профиль и назад по истории работают стабильно.
- Старый маршрут `user-profile-view` больше не используется.
- Статус:
- `pending`

View File

@ -1,2 +1,2 @@
client.version=1.2.66
server.version=1.2.60
client.version=1.2.67
server.version=1.2.61

View File

@ -101,7 +101,7 @@ const routes = {
'messages-list': messagesList,
'contact-search-view': contactSearchView,
'chat-view': chatView,
'user-profile-view': userProfileView,
user: userProfileView,
'channels-list': channelsList,
'channel-view': channelView,
'channel-thread-view': channelThreadView,

View File

@ -11,6 +11,7 @@ import {
softHaptic,
} from '../services/channels-ux.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import { navigateBack } from '../router.js';
export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };
@ -197,17 +198,6 @@ async function resolveChannelDisplayNameFromServer(channelSelector) {
}
}
function buildBackRoute(selector) {
if (selector?.short?.ownerBlockchainName && selector?.short?.channelName) {
return [
'channel',
encodeRoutePart(selector.short.ownerBlockchainName),
encodeRoutePart(selector.short.channelName),
].join('/');
}
return 'channels-list';
}
function buildThreadRouteFromTarget(target, selector) {
if (!target) return '';
return [
@ -501,7 +491,6 @@ function renderSkeleton(screen) {
export function render({ navigate, route }) {
const selector = parseThreadSelector(route);
const backRoute = buildBackRoute(selector);
const channelDisplayName = resolveChannelDisplayName(selector?.channel);
const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;
@ -624,7 +613,7 @@ export function render({ navigate, route }) {
screen.append(
renderHeader({
title: 'Тред',
leftAction: { label: '<', onClick: () => navigate(backRoute) },
leftAction: { label: '<', onClick: () => navigateBack() },
}),
);
screen.append(channelIndicator, statusBox);

View File

@ -17,6 +17,7 @@ import {
softHaptic,
} from '../services/channels-ux.js';
import { openSpeechInputModal } from '../components/speech-input-modal.js';
import { navigateBack } from '../router.js';
export const pageMeta = { id: 'channel-view', title: 'Канал' };
const CHANNEL_TYPE_PERSONAL = 100;
@ -628,8 +629,9 @@ function renderPostCard(post, {
const card = document.createElement('article');
card.className = 'card stack channel-message-card';
const topRow = document.createElement('div');
topRow.className = 'channel-message-top';
const authorTile = document.createElement('button');
authorTile.type = 'button';
authorTile.className = 'channel-message-author-tile';
const avatar = document.createElement('div');
avatar.className = 'channel-message-avatar';
@ -654,13 +656,19 @@ function renderPostCard(post, {
title.append(loginEl, numberEl);
authorBlock.append(title, timestamp);
topRow.append(avatar, authorBlock);
authorTile.append(avatar, authorBlock);
authorTile.addEventListener('click', (event) => {
event.stopPropagation();
const cleanLogin = String(post.authorLogin || '').trim();
if (!cleanLogin) return;
navigate(`user/${encodeRoutePart(cleanLogin)}/channel-view`);
});
const body = document.createElement('p');
body.className = 'channel-message-body';
body.textContent = post.body;
card.append(topRow, body);
card.append(authorTile, body);
const refKey = messageRefKey(post.messageRef);
if (refKey) {
@ -688,6 +696,7 @@ function renderPostCard(post, {
`;
likeButton.disabled = isPending;
likeButton.addEventListener('click', async (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
if (isPending) return;
if (!isLiked) {
@ -710,6 +719,7 @@ function renderPostCard(post, {
<span class="channel-action-counter">${post.repliesCount || 0}</span>
`;
replyButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
openReplyModal({
navigate,
@ -726,6 +736,7 @@ function renderPostCard(post, {
<span class="channel-action-label">Тред</span>
`;
openThreadButton.addEventListener('click', (event) => {
event.stopPropagation();
animatePress(event.currentTarget);
const route = buildThreadRoute(post.messageRef, selector);
if (route) navigate(route);
@ -747,6 +758,10 @@ function renderPostCard(post, {
actions.append(openThreadButton, shareButton);
card.append(actions);
card.addEventListener('click', () => {
const route = buildThreadRoute(post.messageRef, selector);
if (route) navigate(route);
});
return card;
}
@ -978,7 +993,7 @@ export function render({ navigate, route }) {
screen.append(
renderHeader({
title: '',
leftAction: { label: '<', onClick: () => navigate('channels-list') },
leftAction: { label: '<', onClick: () => navigateBack() },
}),
);
screen.append(statusBox);

View File

@ -53,7 +53,7 @@ export function render({ navigate }) {
<div class="meta-muted">Профиль</div>
`;
row.addEventListener('click', () => {
navigate(`user-profile-view/${encodeURIComponent(login)}/contact-search-view`);
navigate(`user/${encodeURIComponent(login)}/contact-search-view`);
});
resultsList.append(row);
});

View File

@ -533,7 +533,7 @@ export function render({ navigate, route }) {
const cleanLogin = normalizeLogin(login);
if (!cleanLogin) return '';
if (normKey(cleanLogin) === normKey(state.session.login)) return 'profile-view';
return `user-profile-view/${encodeURIComponent(cleanLogin)}/${encodeURIComponent('network-view/keep-history')}`;
return `user/${encodeURIComponent(cleanLogin)}/${encodeURIComponent('network-view/keep-history')}`;
}
function helpText() {

View File

@ -7,7 +7,9 @@ import {
} from '../services/user-connections.js';
import { renderUserAvatar } from '../components/avatar-image.js';
export const pageMeta = { id: 'user-profile-view', title: 'Чужой профиль' };
import { navigateBack } from '../router.js';
export const pageMeta = { id: 'user', title: 'Чужой профиль' };
function escapeHtml(text) {
return String(text || '')
@ -147,7 +149,6 @@ function renderReadOnlyParams(card) {
export function render({ navigate, route }) {
const requestedLogin = String(route.params.login || '').trim();
const fromPage = String(route.params.fromPage || 'messages-list').trim() || 'messages-list';
const sessionLogin = String(state.session.login || '').trim();
const screen = document.createElement('section');
@ -163,7 +164,7 @@ export function render({ navigate, route }) {
screen.append(
renderHeader({
title: 'Профиль пользователя',
leftAction: { label: '←', onClick: () => navigate(fromPage) },
leftAction: { label: '←', onClick: () => navigateBack() },
rightActions: [{ label: 'Обновить', onClick: () => refresh() }],
}),
status,

View File

@ -122,7 +122,7 @@ export function getRoute() {
return { pageId, params: { sessionId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
}
if (pageId === 'user-profile-view') {
if (pageId === 'user') {
return {
pageId,
params: {
@ -162,6 +162,11 @@ export function navigate(path) {
window.dispatchEvent(new PopStateEvent('popstate'));
}
export function navigateBack() {
if (window.history.length <= 1) return;
window.history.back();
}
export function resolveToolbarActive(pageId) {
if (ROOT_PAGES.includes(pageId)) return pageId;
if (
@ -185,6 +190,6 @@ export function resolveToolbarActive(pageId) {
}
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
if (pageId === 'channel-view' || pageId === 'channel-thread-view' || pageId === 'add-channel-view' || pageId === 'add-personal-public-chat-view') return 'channels-list';
if (pageId === 'user-profile-view') return 'messages-list';
if (pageId === 'user') return 'messages-list';
return 'profile-view';
}

View File

@ -2171,6 +2171,23 @@ textarea.input {
gap: 12px;
}
.channel-message-author-tile {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
padding: 10px 12px;
border-radius: 8px;
border: 1px solid rgba(255, 255, 255, 0.12);
background: rgba(255, 255, 255, 0.04);
text-align: left;
cursor: pointer;
}
.channel-message-author-tile:hover {
background: rgba(255, 255, 255, 0.08);
}
.channel-message-avatar {
width: 44px;
height: 44px;