feat(ui): короткий роут m для тредов и восстановление заголовка канала

This commit is contained in:
AidarKC 2026-05-19 01:05:25 +03:00
parent d13c60fca1
commit 3a0899bcfe
5 changed files with 111 additions and 44 deletions

View File

@ -0,0 +1,19 @@
# Короткая ссылка на сообщение `#/m/{blockchainName}/{blockNumber}`
Статус: `pending`
## Краткое описание
Добавлен короткий роут сообщения `#/m/{blockchainName}/{blockNumber}` (поддерживает и вариант с hash).
Переходы в тред из канала и из треда теперь формируются через `#/m/...`, а не через длинный путь канала.
## Что проверять
1. Открыть сообщение в канале и перейти в тред — адрес должен быть формата `#/m/...`.
2. Скопировать ссылку на тред сообщения и открыть в новой вкладке.
3. Для ответа (reply) нажать `🧵 В тред` и убедиться, что тред открывается без ошибок `BAD_FIELDS`/`Не удалось определить hash`.
4. Проверить шапку треда: UI должен попытаться восстановить красивый заголовок канала (`owner/channel`).
5. Проверить, что старый маршрут `#/channel-thread-view/...` тоже продолжает работать.
## Ожидаемый результат
- Короткий роут работает стабильно для постов и ответов.
- Тред открывается даже если в URL нет hash (опциональный случай).
- Ошибка про невозможность определить hash для открытия треда не воспроизводится.

View File

@ -1,2 +1,2 @@
client.version=1.2.64 client.version=1.2.65
server.version=1.2.58 server.version=1.2.59

View File

@ -148,6 +148,54 @@ function resolveChannelDisplayName(channelSelector) {
return `${found.channel?.ownerLogin || 'неизвестно'}/${found.channel?.channelName || 'канал'}`; return `${found.channel?.ownerLogin || 'неизвестно'}/${found.channel?.channelName || 'канал'}`;
} }
function extractChannelContextFromThreadPayload(payload) {
const focusInfo = payload?.focus?.channelInfo;
if (focusInfo?.ownerBlockchainName && focusInfo?.channelRoot?.blockNumber != null) {
return {
ownerBlockchainName: String(focusInfo.ownerBlockchainName || '').trim(),
channelRootBlockNumber: Number(focusInfo.channelRoot.blockNumber),
channelRootBlockHash: '0',
};
}
const ancestors = Array.isArray(payload?.ancestors) ? payload.ancestors : [];
for (let i = ancestors.length - 1; i >= 0; i -= 1) {
const info = ancestors[i]?.channelInfo;
if (info?.ownerBlockchainName && info?.channelRoot?.blockNumber != null) {
return {
ownerBlockchainName: String(info.ownerBlockchainName || '').trim(),
channelRootBlockNumber: Number(info.channelRoot.blockNumber),
channelRootBlockHash: '0',
};
}
}
return null;
}
async function resolveChannelDisplayNameFromServer(channelSelector) {
const ownerBch = String(channelSelector?.ownerBlockchainName || '').trim();
const rootNo = Number(channelSelector?.channelRootBlockNumber);
if (!ownerBch || !Number.isFinite(rootNo) || rootNo < 0) return '';
const ownerLogin = extractLoginFromBlockchainName(ownerBch);
if (!ownerLogin) return '';
try {
const feed = await authService.listSubscriptionsFeed(ownerLogin, 1000);
const rows = Array.isArray(feed?.ownedChannels) ? feed.ownedChannels : [];
const row = rows.find((item) => (
String(item?.channel?.ownerBlockchainName || '').trim().toLowerCase() === ownerBch.toLowerCase()
&& Number(item?.channel?.channelRoot?.blockNumber) === rootNo
));
if (!row?.channel?.channelName) return '';
channelSelector.channelRootBlockHash = normalizeRouteHash(row?.channel?.channelRoot?.blockHash);
return `${row.channel.ownerLogin || ownerLogin}/${row.channel.channelName}`;
} catch {
return '';
}
}
function buildBackRoute(selector) { function buildBackRoute(selector) {
if (selector?.short?.ownerBlockchainName && selector?.short?.channelName) { if (selector?.short?.ownerBlockchainName && selector?.short?.channelName) {
return [ return [
@ -161,25 +209,12 @@ function buildBackRoute(selector) {
function buildThreadRouteFromTarget(target, selector) { function buildThreadRouteFromTarget(target, selector) {
if (!target) return ''; if (!target) return '';
const ownerBch = String(selector?.channel?.ownerBlockchainName || '').trim(); return [
const rootNo = Number(selector?.channel?.channelRootBlockNumber); 'm',
const rootHash = normalizeRouteHash(selector?.channel?.channelRootBlockHash);
const base = [
'channel-thread-view',
encodeRoutePart(target.blockchainName), encodeRoutePart(target.blockchainName),
target.blockNumber, target.blockNumber,
normalizeRouteHash(target.blockHash), normalizeRouteHash(target.blockHash),
]; ].join('/');
if (ownerBch && Number.isFinite(rootNo) && rootNo >= 0) {
base.push(
encodeRoutePart(ownerBch),
String(rootNo),
normalizeRouteHash(rootHash),
);
}
return base.join('/');
} }
function buildTargetFromNode(node) { function buildTargetFromNode(node) {
@ -666,21 +701,10 @@ export function render({ navigate, route }) {
channelRootBlockHash: rootHash, channelRootBlockHash: rootHash,
}; };
let resolvedHash = normalizeMessageHash(resolvedMessage?.blockHash);
if (!resolvedHash) {
const channelPayload = await authService.getChannelMessages(selector.channel, 400, 'asc', state.session.login);
const messages = Array.isArray(channelPayload?.messages) ? channelPayload.messages : [];
const foundMessage = messages.find((item) => Number(item?.messageRef?.blockNumber) === Number(resolvedMessage.blockNumber));
const foundHash = normalizeMessageHash(foundMessage?.messageRef?.blockHash);
if (!foundHash) {
throw new Error('Не удалось определить hash сообщения для открытия треда.');
}
resolvedHash = foundHash;
}
resolvedMessage = { resolvedMessage = {
blockchainName: ownerBch, blockchainName: ownerBch,
blockNumber: resolvedMessage.blockNumber, blockNumber: resolvedMessage.blockNumber,
blockHash: resolvedHash, blockHash: normalizeMessageHash(resolvedMessage?.blockHash),
}; };
} }
@ -691,6 +715,29 @@ export function render({ navigate, route }) {
const focus = payload?.focus || null; const focus = payload?.focus || null;
const descendants = Array.isArray(payload?.descendants) ? payload.descendants : []; const descendants = Array.isArray(payload?.descendants) ? payload.descendants : [];
const focusHash = normalizeMessageHash(focus?.messageRef?.blockHash);
if (focusHash && selector?.message) {
selector.message.blockHash = focusHash;
}
if ((!selector?.channel?.ownerBlockchainName || selector?.channel?.channelRootBlockNumber == null) && payload) {
const context = extractChannelContextFromThreadPayload(payload);
if (context) {
selector.channel = {
ownerBlockchainName: context.ownerBlockchainName,
channelRootBlockNumber: context.channelRootBlockNumber,
channelRootBlockHash: normalizeRouteHash(context.channelRootBlockHash),
};
}
}
let resolvedChannelLabel = resolveChannelDisplayName(selector?.channel);
if (!resolvedChannelLabel && selector?.channel?.ownerBlockchainName && selector?.channel?.channelRootBlockNumber != null) {
resolvedChannelLabel = await resolveChannelDisplayNameFromServer(selector.channel);
}
const fallbackChannel = String(selector?.channel?.ownerBlockchainName || '').trim() || 'неизвестно';
channelIndicator.textContent = `Канал: ${resolvedChannelLabel || fallbackChannel}`;
let seq = 0; let seq = 0;
const nextNumber = () => { const nextNumber = () => {
seq += 1; seq += 1;

View File

@ -146,24 +146,11 @@ function buildSelectorFromRoute(route, channelId) {
function buildThreadRoute(messageRef, selector) { function buildThreadRoute(messageRef, selector) {
if (!messageRef || !selector) return ''; if (!messageRef || !selector) return '';
const ownerBlockchainName = String(selector.ownerBlockchainName || '').trim();
const channelName = String(selector.channelName || '').trim();
if (ownerBlockchainName && channelName) {
return [
'channel',
encodeRoutePart(ownerBlockchainName),
encodeRoutePart(channelName),
messageRef.blockNumber,
].join('/');
}
return [ return [
'channel-thread-view', 'm',
encodeRoutePart(messageRef.blockchainName), encodeRoutePart(messageRef.blockchainName),
messageRef.blockNumber, messageRef.blockNumber,
normalizeRouteHash(messageRef.blockHash), normalizeRouteHash(messageRef.blockHash),
encodeRoutePart(selector.ownerBlockchainName),
selector.channelRootBlockNumber,
normalizeRouteHash(selector.channelRootBlockHash),
].join('/'); ].join('/');
} }

View File

@ -99,6 +99,20 @@ export function getRoute() {
}; };
} }
if (pageId === 'm') {
return {
pageId: 'channel-thread-view',
params: {
messageBlockchainName: decodePart(segments[1]),
messageBlockNumber: segments[2] || '',
messageBlockHash: segments[3] || '',
channelOwnerBlockchainName: '',
channelRootBlockNumber: '',
channelRootBlockHash: '',
},
};
}
if (pageId === 'device-session-view') { if (pageId === 'device-session-view') {
return { pageId, params: { sessionId: dynamicId ? decodeURIComponent(dynamicId) : '' } }; return { pageId, params: { sessionId: dynamicId ? decodeURIComponent(dynamicId) : '' } };
} }