UI: карточка автора в канале, профиль user и назад по истории
This commit is contained in:
parent
db2d9a666b
commit
90d10086d7
@ -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`
|
||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.66
|
client.version=1.2.67
|
||||||
server.version=1.2.60
|
server.version=1.2.61
|
||||||
|
|||||||
@ -101,7 +101,7 @@ const routes = {
|
|||||||
'messages-list': messagesList,
|
'messages-list': messagesList,
|
||||||
'contact-search-view': contactSearchView,
|
'contact-search-view': contactSearchView,
|
||||||
'chat-view': chatView,
|
'chat-view': chatView,
|
||||||
'user-profile-view': userProfileView,
|
user: userProfileView,
|
||||||
'channels-list': channelsList,
|
'channels-list': channelsList,
|
||||||
'channel-view': channelView,
|
'channel-view': channelView,
|
||||||
'channel-thread-view': channelThreadView,
|
'channel-thread-view': channelThreadView,
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import {
|
|||||||
softHaptic,
|
softHaptic,
|
||||||
} from '../services/channels-ux.js';
|
} from '../services/channels-ux.js';
|
||||||
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
||||||
|
import { navigateBack } from '../router.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'channel-thread-view', title: 'Тред' };
|
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) {
|
function buildThreadRouteFromTarget(target, selector) {
|
||||||
if (!target) return '';
|
if (!target) return '';
|
||||||
return [
|
return [
|
||||||
@ -501,7 +491,6 @@ function renderSkeleton(screen) {
|
|||||||
|
|
||||||
export function render({ navigate, route }) {
|
export function render({ navigate, route }) {
|
||||||
const selector = parseThreadSelector(route);
|
const selector = parseThreadSelector(route);
|
||||||
const backRoute = buildBackRoute(selector);
|
|
||||||
const channelDisplayName = resolveChannelDisplayName(selector?.channel);
|
const channelDisplayName = resolveChannelDisplayName(selector?.channel);
|
||||||
const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;
|
const routeKey = `${selector?.message?.blockchainName || ''}:${selector?.message?.blockNumber || ''}:${selector?.message?.blockHash || ''}`;
|
||||||
|
|
||||||
@ -624,7 +613,7 @@ export function render({ navigate, route }) {
|
|||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'Тред',
|
title: 'Тред',
|
||||||
leftAction: { label: '<', onClick: () => navigate(backRoute) },
|
leftAction: { label: '<', onClick: () => navigateBack() },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
screen.append(channelIndicator, statusBox);
|
screen.append(channelIndicator, statusBox);
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import {
|
|||||||
softHaptic,
|
softHaptic,
|
||||||
} from '../services/channels-ux.js';
|
} from '../services/channels-ux.js';
|
||||||
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
import { openSpeechInputModal } from '../components/speech-input-modal.js';
|
||||||
|
import { navigateBack } from '../router.js';
|
||||||
|
|
||||||
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
export const pageMeta = { id: 'channel-view', title: 'Канал' };
|
||||||
const CHANNEL_TYPE_PERSONAL = 100;
|
const CHANNEL_TYPE_PERSONAL = 100;
|
||||||
@ -628,8 +629,9 @@ function renderPostCard(post, {
|
|||||||
const card = document.createElement('article');
|
const card = document.createElement('article');
|
||||||
card.className = 'card stack channel-message-card';
|
card.className = 'card stack channel-message-card';
|
||||||
|
|
||||||
const topRow = document.createElement('div');
|
const authorTile = document.createElement('button');
|
||||||
topRow.className = 'channel-message-top';
|
authorTile.type = 'button';
|
||||||
|
authorTile.className = 'channel-message-author-tile';
|
||||||
|
|
||||||
const avatar = document.createElement('div');
|
const avatar = document.createElement('div');
|
||||||
avatar.className = 'channel-message-avatar';
|
avatar.className = 'channel-message-avatar';
|
||||||
@ -654,13 +656,19 @@ function renderPostCard(post, {
|
|||||||
|
|
||||||
title.append(loginEl, numberEl);
|
title.append(loginEl, numberEl);
|
||||||
authorBlock.append(title, timestamp);
|
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');
|
const body = document.createElement('p');
|
||||||
body.className = 'channel-message-body';
|
body.className = 'channel-message-body';
|
||||||
body.textContent = post.body;
|
body.textContent = post.body;
|
||||||
|
|
||||||
card.append(topRow, body);
|
card.append(authorTile, body);
|
||||||
|
|
||||||
const refKey = messageRefKey(post.messageRef);
|
const refKey = messageRefKey(post.messageRef);
|
||||||
if (refKey) {
|
if (refKey) {
|
||||||
@ -688,6 +696,7 @@ function renderPostCard(post, {
|
|||||||
`;
|
`;
|
||||||
likeButton.disabled = isPending;
|
likeButton.disabled = isPending;
|
||||||
likeButton.addEventListener('click', async (event) => {
|
likeButton.addEventListener('click', async (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
if (isPending) return;
|
if (isPending) return;
|
||||||
if (!isLiked) {
|
if (!isLiked) {
|
||||||
@ -710,6 +719,7 @@ function renderPostCard(post, {
|
|||||||
<span class="channel-action-counter">${post.repliesCount || 0}</span>
|
<span class="channel-action-counter">${post.repliesCount || 0}</span>
|
||||||
`;
|
`;
|
||||||
replyButton.addEventListener('click', (event) => {
|
replyButton.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
openReplyModal({
|
openReplyModal({
|
||||||
navigate,
|
navigate,
|
||||||
@ -726,6 +736,7 @@ function renderPostCard(post, {
|
|||||||
<span class="channel-action-label">Тред</span>
|
<span class="channel-action-label">Тред</span>
|
||||||
`;
|
`;
|
||||||
openThreadButton.addEventListener('click', (event) => {
|
openThreadButton.addEventListener('click', (event) => {
|
||||||
|
event.stopPropagation();
|
||||||
animatePress(event.currentTarget);
|
animatePress(event.currentTarget);
|
||||||
const route = buildThreadRoute(post.messageRef, selector);
|
const route = buildThreadRoute(post.messageRef, selector);
|
||||||
if (route) navigate(route);
|
if (route) navigate(route);
|
||||||
@ -747,6 +758,10 @@ function renderPostCard(post, {
|
|||||||
|
|
||||||
actions.append(openThreadButton, shareButton);
|
actions.append(openThreadButton, shareButton);
|
||||||
card.append(actions);
|
card.append(actions);
|
||||||
|
card.addEventListener('click', () => {
|
||||||
|
const route = buildThreadRoute(post.messageRef, selector);
|
||||||
|
if (route) navigate(route);
|
||||||
|
});
|
||||||
return card;
|
return card;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -978,7 +993,7 @@ export function render({ navigate, route }) {
|
|||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: '',
|
title: '',
|
||||||
leftAction: { label: '<', onClick: () => navigate('channels-list') },
|
leftAction: { label: '<', onClick: () => navigateBack() },
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
screen.append(statusBox);
|
screen.append(statusBox);
|
||||||
|
|||||||
@ -53,7 +53,7 @@ export function render({ navigate }) {
|
|||||||
<div class="meta-muted">Профиль</div>
|
<div class="meta-muted">Профиль</div>
|
||||||
`;
|
`;
|
||||||
row.addEventListener('click', () => {
|
row.addEventListener('click', () => {
|
||||||
navigate(`user-profile-view/${encodeURIComponent(login)}/contact-search-view`);
|
navigate(`user/${encodeURIComponent(login)}/contact-search-view`);
|
||||||
});
|
});
|
||||||
resultsList.append(row);
|
resultsList.append(row);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -533,7 +533,7 @@ export function render({ navigate, route }) {
|
|||||||
const cleanLogin = normalizeLogin(login);
|
const cleanLogin = normalizeLogin(login);
|
||||||
if (!cleanLogin) return '';
|
if (!cleanLogin) return '';
|
||||||
if (normKey(cleanLogin) === normKey(state.session.login)) return 'profile-view';
|
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() {
|
function helpText() {
|
||||||
|
|||||||
@ -7,7 +7,9 @@ import {
|
|||||||
} from '../services/user-connections.js';
|
} from '../services/user-connections.js';
|
||||||
import { renderUserAvatar } from '../components/avatar-image.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) {
|
function escapeHtml(text) {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
@ -147,7 +149,6 @@ function renderReadOnlyParams(card) {
|
|||||||
|
|
||||||
export function render({ navigate, route }) {
|
export function render({ navigate, route }) {
|
||||||
const requestedLogin = String(route.params.login || '').trim();
|
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 sessionLogin = String(state.session.login || '').trim();
|
||||||
|
|
||||||
const screen = document.createElement('section');
|
const screen = document.createElement('section');
|
||||||
@ -163,7 +164,7 @@ export function render({ navigate, route }) {
|
|||||||
screen.append(
|
screen.append(
|
||||||
renderHeader({
|
renderHeader({
|
||||||
title: 'Профиль пользователя',
|
title: 'Профиль пользователя',
|
||||||
leftAction: { label: '←', onClick: () => navigate(fromPage) },
|
leftAction: { label: '←', onClick: () => navigateBack() },
|
||||||
rightActions: [{ label: 'Обновить', onClick: () => refresh() }],
|
rightActions: [{ label: 'Обновить', onClick: () => refresh() }],
|
||||||
}),
|
}),
|
||||||
status,
|
status,
|
||||||
|
|||||||
@ -122,7 +122,7 @@ export function getRoute() {
|
|||||||
return { pageId, params: { sessionId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
|
return { pageId, params: { sessionId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pageId === 'user-profile-view') {
|
if (pageId === 'user') {
|
||||||
return {
|
return {
|
||||||
pageId,
|
pageId,
|
||||||
params: {
|
params: {
|
||||||
@ -162,6 +162,11 @@ export function navigate(path) {
|
|||||||
window.dispatchEvent(new PopStateEvent('popstate'));
|
window.dispatchEvent(new PopStateEvent('popstate'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function navigateBack() {
|
||||||
|
if (window.history.length <= 1) return;
|
||||||
|
window.history.back();
|
||||||
|
}
|
||||||
|
|
||||||
export function resolveToolbarActive(pageId) {
|
export function resolveToolbarActive(pageId) {
|
||||||
if (ROOT_PAGES.includes(pageId)) return pageId;
|
if (ROOT_PAGES.includes(pageId)) return pageId;
|
||||||
if (
|
if (
|
||||||
@ -185,6 +190,6 @@ export function resolveToolbarActive(pageId) {
|
|||||||
}
|
}
|
||||||
if (pageId === 'chat-view' || pageId === 'contact-search-view') return 'messages-list';
|
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 === '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';
|
return 'profile-view';
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2171,6 +2171,23 @@ textarea.input {
|
|||||||
gap: 12px;
|
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 {
|
.channel-message-avatar {
|
||||||
width: 44px;
|
width: 44px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user