UI: переход на history-router без # и короткие ссылки тредов

This commit is contained in:
AidarKC 2026-05-19 10:15:15 +03:00
parent 3a0899bcfe
commit db2d9a666b
9 changed files with 61 additions and 28 deletions

View File

@ -0,0 +1,21 @@
# Переход на history-router без `#` в URL
- Краткое описание:
- UI переведён с hash-router на history API роутинг.
- Ссылки на треды переведены в формат без hash сообщения: `/m/{blockchainName}/{blockNumber}`.
- Навигация и шаринг-ссылки обновлены под `pathname`.
- Что проверять:
- Открытие UI с корня (`/`) и переход на стартовую страницу без тёмного экрана.
- Навигация между основными экранами (сообщения, каналы, профиль, настройки).
- Переход в канал, открытие треда, ответ/лайк, шаринг ссылки.
- Прямое открытие URL формата `/m/{blockchain}/{number}`.
- Поведение после refresh (F5) при настроенном серверном fallback на `index.html`.
- Ожидаемый результат:
- Приложение работает без `#` в адресе.
- Треды открываются и действия по сообщению (reply/like/share) работают корректно.
- Нет зависания на пустом/тёмном экране при входе.
- Статус:
- `pending`

View File

@ -1,2 +1,2 @@
client.version=1.2.65 client.version=1.2.66
server.version=1.2.59 server.version=1.2.60

View File

@ -289,7 +289,7 @@ function consumeCallPushActionFromUrlIfAny() {
params.delete('callPushAction'); params.delete('callPushAction');
params.delete('callPushPayload'); params.delete('callPushPayload');
const nextQuery = params.toString(); const nextQuery = params.toString();
const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ''}${window.location.hash || ''}`; const nextUrl = `${window.location.pathname}${nextQuery ? `?${nextQuery}` : ''}`;
window.history.replaceState({}, '', nextUrl); window.history.replaceState({}, '', nextUrl);
} catch { } catch {
// ignore URL parsing errors // ignore URL parsing errors
@ -634,7 +634,7 @@ function renderPageFailureFallback(pageId, error) {
stack: error?.stack || '', stack: error?.stack || '',
context: { context: {
pageId, pageId,
routeHash: window.location.hash || '', routeHash: window.location.pathname || '',
}, },
}); });
@ -1031,13 +1031,14 @@ async function init() {
startPeriodicUiVersionCheck(); startPeriodicUiVersionCheck();
await ensureSessionRuntimeStarted(); await ensureSessionRuntimeStarted();
if (!window.location.hash) { if (!window.location.pathname || window.location.pathname === '/' || window.location.pathname === '/index.html') {
navigate(state.session.isAuthorized ? 'messages-list' : 'start-view'); navigate(state.session.isAuthorized ? 'messages-list' : 'start-view');
renderApp();
} else { } else {
renderApp(); renderApp();
} }
window.addEventListener('hashchange', renderApp); window.addEventListener('popstate', renderApp);
document.addEventListener('visibilitychange', () => { document.addEventListener('visibilitychange', () => {
if (document.visibilityState !== 'visible') return; if (document.visibilityState !== 'visible') return;
void checkConnectionHealth(); void checkConnectionHealth();

View File

@ -81,7 +81,8 @@ function messageRefKey(messageRef) {
function buildAbsoluteRouteUrl(routePath = '') { function buildAbsoluteRouteUrl(routePath = '') {
const cleanRoute = String(routePath || '').replace(/^#?\/?/, ''); const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.hash = `#/${cleanRoute}`; url.pathname = `/${cleanRoute}`;
url.hash = '';
return url.toString(); return url.toString();
} }
@ -213,7 +214,6 @@ function buildThreadRouteFromTarget(target, selector) {
'm', 'm',
encodeRoutePart(target.blockchainName), encodeRoutePart(target.blockchainName),
target.blockNumber, target.blockNumber,
normalizeRouteHash(target.blockHash),
].join('/'); ].join('/');
} }
@ -525,7 +525,7 @@ export function render({ navigate, route }) {
const next = render({ navigate, route }); const next = render({ navigate, route });
current.replaceWith(next); current.replaceWith(next);
} catch (error) { } catch (error) {
logThreadRuntimeError('rerender', error, { routeHash: window.location.hash }); logThreadRuntimeError('rerender', error, { routePath: window.location.pathname });
} }
}; };
@ -543,7 +543,7 @@ export function render({ navigate, route }) {
const login = state.session.login; const login = state.session.login;
const storagePwd = state.session.storagePwdInMemory; const storagePwd = state.session.storagePwdInMemory;
if (!login || !storagePwd) { if (!login || !storagePwd) {
state.authReturnHash = window.location.hash || '#/channels-list'; state.authReturnHash = window.location.pathname || '/channels-list';
navigate('login-view'); navigate('login-view');
throw new Error('Для этого действия нужно войти'); throw new Error('Для этого действия нужно войти');
} }

View File

@ -110,7 +110,8 @@ function blockRefToMessageKey(blockRef, fallbackBch = '') {
function buildAbsoluteRouteUrl(routePath = '') { function buildAbsoluteRouteUrl(routePath = '') {
const cleanRoute = String(routePath || '').replace(/^#?\/?/, ''); const cleanRoute = String(routePath || '').replace(/^#?\/?/, '');
const url = new URL(window.location.href); const url = new URL(window.location.href);
url.hash = `#/${cleanRoute}`; url.pathname = `/${cleanRoute}`;
url.hash = '';
return url.toString(); return url.toString();
} }
@ -150,7 +151,6 @@ function buildThreadRoute(messageRef, selector) {
'm', 'm',
encodeRoutePart(messageRef.blockchainName), encodeRoutePart(messageRef.blockchainName),
messageRef.blockNumber, messageRef.blockNumber,
normalizeRouteHash(messageRef.blockHash),
].join('/'); ].join('/');
} }
@ -890,7 +890,7 @@ export function render({ navigate, route }) {
const login = state.session.login; const login = state.session.login;
const storagePwd = state.session.storagePwdInMemory; const storagePwd = state.session.storagePwdInMemory;
if (!login || !storagePwd) { if (!login || !storagePwd) {
state.authReturnHash = window.location.hash || '#/channels-list'; state.authReturnHash = window.location.pathname || '/channels-list';
navigate('login-view'); navigate('login-view');
throw new Error('Для этого действия нужно войти'); throw new Error('Для этого действия нужно войти');
} }

View File

@ -1167,9 +1167,9 @@ export function render({ navigate, route }) {
const rerenderList = () => { const rerenderList = () => {
try { try {
const expectedHash = `#/channels-list/${listState.activeTab}`; const expectedPath = `/channels-list/${listState.activeTab}`;
if (window.location.hash !== expectedHash) { if (window.location.pathname !== expectedPath) {
window.history.replaceState({}, '', expectedHash); window.history.replaceState({}, '', expectedPath);
} }
} catch { } catch {
// ignore history errors // ignore history errors

View File

@ -184,7 +184,8 @@ function openDeveloperAvatarUploadModal({ walletLogin, storagePwd, gateway } = {
async function forceUiUpdateNow() { async function forceUiUpdateNow() {
try { try {
window.location.hash = '#/settings-view'; window.history.replaceState({}, '', '/settings-view');
window.dispatchEvent(new PopStateEvent('popstate'));
} catch {} } catch {}
if (!('serviceWorker' in navigator)) { if (!('serviceWorker' in navigator)) {
window.location.reload(); window.location.reload();

View File

@ -132,8 +132,8 @@ export function render({ navigate }) {
: `Ключи сохранены. Регистрация завершена для @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`); : `Ключи сохранены. Регистрация завершена для @${state.registrationDraft.login}. Далее откройте вкладку «Каналы».`);
const nextHash = String(state.authReturnHash || '').trim(); const nextHash = String(state.authReturnHash || '').trim();
state.authReturnHash = ''; state.authReturnHash = '';
if (nextHash.startsWith('#/')) { if (nextHash.startsWith('/')) {
navigate(nextHash.slice(2)); navigate(nextHash.slice(1));
} else { } else {
navigate('profile-view'); navigate('profile-view');
} }

View File

@ -14,7 +14,12 @@ export const PRE_AUTH_PAGES = [
]; ];
export function getRoute() { export function getRoute() {
const raw = window.location.hash.replace(/^#\/?/, ''); const currentPath = String(window.location.pathname || '').trim();
const raw = currentPath
.replace(/^\/+/, '')
.replace(/^index\.html$/i, '')
.replace(/^index\.html\//i, '')
.replace(/\/+$/, '');
if (!raw) { if (!raw) {
return { pageId: '', params: {} }; return { pageId: '', params: {} };
} }
@ -52,8 +57,8 @@ export function getRoute() {
if (pageId === 'channel') { if (pageId === 'channel') {
// Короткий формат: // Короткий формат:
// #/channel/{ownerBlockchainName}/{channelName} // /channel/{ownerBlockchainName}/{channelName}
// #/channel/{ownerBlockchainName}/{channelName}/{messageBlockNumber} // /channel/{ownerBlockchainName}/{channelName}/{messageBlockNumber}
const ownerBlockchainName = decodePart(segments[1] || ''); const ownerBlockchainName = decodePart(segments[1] || '');
const channelName = decodePart(segments[2] || ''); const channelName = decodePart(segments[2] || '');
const messageBlockNumber = segments[3] || ''; const messageBlockNumber = segments[3] || '';
@ -91,10 +96,10 @@ export function getRoute() {
params: { params: {
messageBlockchainName: decodePart(segments[1]), messageBlockchainName: decodePart(segments[1]),
messageBlockNumber: segments[2] || '', messageBlockNumber: segments[2] || '',
messageBlockHash: segments[3] || '', messageBlockHash: '',
channelOwnerBlockchainName: decodePart(segments[4]), channelOwnerBlockchainName: decodePart(segments[3]),
channelRootBlockNumber: segments[5] || '', channelRootBlockNumber: segments[4] || '',
channelRootBlockHash: segments[6] || '', channelRootBlockHash: segments[5] || '',
}, },
}; };
} }
@ -105,7 +110,7 @@ export function getRoute() {
params: { params: {
messageBlockchainName: decodePart(segments[1]), messageBlockchainName: decodePart(segments[1]),
messageBlockNumber: segments[2] || '', messageBlockNumber: segments[2] || '',
messageBlockHash: segments[3] || '', messageBlockHash: '',
channelOwnerBlockchainName: '', channelOwnerBlockchainName: '',
channelRootBlockNumber: '', channelRootBlockNumber: '',
channelRootBlockHash: '', channelRootBlockHash: '',
@ -149,7 +154,12 @@ export function getRoute() {
} }
export function navigate(path) { export function navigate(path) {
window.location.hash = `#/${path}`; const cleanPath = String(path || '').replace(/^\/+/, '');
const nextPath = cleanPath ? `/${cleanPath}` : '/';
if (window.location.pathname !== nextPath) {
window.history.pushState({}, '', nextPath);
}
window.dispatchEvent(new PopStateEvent('popstate'));
} }
export function resolveToolbarActive(pageId) { export function resolveToolbarActive(pageId) {