UI: переход на history-router без # и короткие ссылки тредов
This commit is contained in:
parent
3a0899bcfe
commit
db2d9a666b
@ -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`
|
||||||
@ -1,2 +1,2 @@
|
|||||||
client.version=1.2.65
|
client.version=1.2.66
|
||||||
server.version=1.2.59
|
server.version=1.2.60
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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('Для этого действия нужно войти');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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('Для этого действия нужно войти');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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');
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user