Add support for client-side room alias hash `#` redirects to the correct URL (#111)
This helps when someone just pastes a room alias on the end of the domain, - `/#room-alias:server` -> `/r/room-alias:server` - `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27` Since these redirects happen on the client, we can't write any e2e tests. Those e2e tests do everything but run client-side JavaScript. Follow-up to https://github.com/matrix-org/matrix-public-archive/pull/107 Part of https://github.com/matrix-org/matrix-public-archive/issues/25
This commit is contained in:
parent
7a88ea0c19
commit
2b4ecb737a
|
@ -19,6 +19,7 @@ const generateViteConfigForEntryPoint = require('./generate-vite-config-for-entr
|
||||||
const entryPoints = [
|
const entryPoints = [
|
||||||
path.resolve(__dirname, '../public/js/entry-client-hydrogen.js'),
|
path.resolve(__dirname, '../public/js/entry-client-hydrogen.js'),
|
||||||
path.resolve(__dirname, '../public/js/entry-client-room-directory.js'),
|
path.resolve(__dirname, '../public/js/entry-client-room-directory.js'),
|
||||||
|
path.resolve(__dirname, '../public/js/entry-client-room-alias-hash-redirect.js'),
|
||||||
];
|
];
|
||||||
|
|
||||||
async function buildClientScripts(extraConfig = {}) {
|
async function buildClientScripts(extraConfig = {}) {
|
||||||
|
|
|
@ -155,6 +155,32 @@
|
||||||
color: initial;
|
color: initial;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.RoomDirectoryView_notificationToast {
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
left: 20px;
|
||||||
|
max-width: 450px;
|
||||||
|
margin-right: 20px;
|
||||||
|
padding: 20px;
|
||||||
|
|
||||||
|
background: hsl(207deg 36% 18% / 90%);
|
||||||
|
border-radius: 8px;
|
||||||
|
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDirectoryView_notificationToastTitle {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.RoomDirectoryView_notificationToastDescription {
|
||||||
|
margin-top: 1em;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.RoomDirectoryView_mainContent {
|
.RoomDirectoryView_mainContent {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
|
@ -0,0 +1,18 @@
|
||||||
|
import assert from 'matrix-public-archive-shared/lib/assert';
|
||||||
|
import MatrixPublicArchiveURLCreator from 'matrix-public-archive-shared/lib/url-creator';
|
||||||
|
import redirectIfRoomAliasInHash from 'matrix-public-archive-shared/lib/redirect-if-room-alias-in-hash';
|
||||||
|
|
||||||
|
const config = window.matrixPublicArchiveContext.config;
|
||||||
|
assert(config);
|
||||||
|
assert(config.basePath);
|
||||||
|
|
||||||
|
const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(config.basePath);
|
||||||
|
|
||||||
|
console.log(`Trying to redirect based on pageHash=${window.location.hash}`);
|
||||||
|
const isRedirecting = redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator);
|
||||||
|
|
||||||
|
// Show the message while we're trying to redirect or if we found nothing, remove the
|
||||||
|
// message
|
||||||
|
document.querySelector('.js-try-redirect-message').style.display = isRedirecting
|
||||||
|
? 'inline'
|
||||||
|
: 'none';
|
|
@ -0,0 +1,62 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
const assert = require('assert');
|
||||||
|
const urlJoin = require('url-join');
|
||||||
|
const safeJson = require('../lib/safe-json');
|
||||||
|
const sanitizeHtml = require('../lib/sanitize-html');
|
||||||
|
|
||||||
|
const config = require('../lib/config');
|
||||||
|
const basePath = config.get('basePath');
|
||||||
|
assert(basePath);
|
||||||
|
|
||||||
|
// Since everything after the hash (`#`) won't make it to the server, let's serve a 404
|
||||||
|
// page that will potentially redirect them to the correct place if they tried
|
||||||
|
// `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27`
|
||||||
|
function clientSideRoomAliasHashRedirectRoute(req, res) {
|
||||||
|
const cspNonce = res.locals.cspNonce;
|
||||||
|
const hydrogenStylesUrl = urlJoin(basePath, '/hydrogen-styles.css');
|
||||||
|
const stylesUrl = urlJoin(basePath, '/css/styles.css');
|
||||||
|
const jsBundleUrl = urlJoin(basePath, '/js/entry-client-room-alias-hash-redirect.es.js');
|
||||||
|
|
||||||
|
const context = {
|
||||||
|
config: {
|
||||||
|
basePath,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
const serializedContext = JSON.stringify(context);
|
||||||
|
|
||||||
|
const pageHtml = `
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Page not found - Matrix Public Archive</title>
|
||||||
|
<link href="${hydrogenStylesUrl}" rel="stylesheet" nonce="${cspNonce}">
|
||||||
|
<link href="${stylesUrl}" rel="stylesheet" nonce="${cspNonce}">
|
||||||
|
</head>
|
||||||
|
${/* We add the .hydrogen class here just to get normal body styles */ ''}
|
||||||
|
<body class="hydrogen">
|
||||||
|
<h1>
|
||||||
|
404: Page not found.
|
||||||
|
<span class="js-try-redirect-message" style="display: none">One sec while we try to redirect you to the right place.</span>
|
||||||
|
</h1>
|
||||||
|
<p>If there was a #room_alias:server hash in the URL, we tried redirecting you to the right place.</p>
|
||||||
|
<p>
|
||||||
|
Otherwise, you're simply in a place that does not exist.
|
||||||
|
You can ${sanitizeHtml(`<a href="${basePath}">go back to the homepage</a>.`)}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<script type="text/javascript" nonce="${cspNonce}">
|
||||||
|
window.matrixPublicArchiveContext = ${safeJson(serializedContext)}
|
||||||
|
</script>
|
||||||
|
<script type="text/javascript" src="${jsBundleUrl}" nonce="${cspNonce}"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
`;
|
||||||
|
|
||||||
|
res.status(404);
|
||||||
|
res.set('Content-Type', 'text/html');
|
||||||
|
res.send(pageHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = clientSideRoomAliasHashRedirectRoute;
|
|
@ -8,6 +8,7 @@ const { handleTracingMiddleware } = require('../tracing/tracing-middleware');
|
||||||
const getVersionTags = require('../lib/get-version-tags');
|
const getVersionTags = require('../lib/get-version-tags');
|
||||||
const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware');
|
const preventClickjackingMiddleware = require('./prevent-clickjacking-middleware');
|
||||||
const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware');
|
const contentSecurityPolicyMiddleware = require('./content-security-policy-middleware');
|
||||||
|
const clientSideRoomAliasHashRedirectRoute = require('./client-side-room-alias-hash-redirect-route');
|
||||||
const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-archive-url-if-bad-sigil-middleware');
|
const redirectToCorrectArchiveUrlIfBadSigil = require('./redirect-to-correct-archive-url-if-bad-sigil-middleware');
|
||||||
|
|
||||||
function installRoutes(app) {
|
function installRoutes(app) {
|
||||||
|
@ -62,6 +63,11 @@ function installRoutes(app) {
|
||||||
// For room aliases (/r) or room ID's (/roomid)
|
// For room aliases (/r) or room ID's (/roomid)
|
||||||
app.use('/:entityDescriptor(r|roomid)/:roomIdOrAliasDirty', require('./room-routes'));
|
app.use('/:entityDescriptor(r|roomid)/:roomIdOrAliasDirty', require('./room-routes'));
|
||||||
|
|
||||||
|
// Since everything after the hash (`#`) won't make it to the server, let's serve a 404
|
||||||
|
// page that will potentially redirect them to the correct place if they tried
|
||||||
|
// `/r/#room-alias:server/date/2022/10/27` -> `/r/room-alias:server/date/2022/10/27`
|
||||||
|
app.use('/:entityDescriptor(r|roomid)', clientSideRoomAliasHashRedirectRoute);
|
||||||
|
|
||||||
// Correct any honest mistakes: If someone accidentally put the sigil in the URL, then
|
// Correct any honest mistakes: If someone accidentally put the sigil in the URL, then
|
||||||
// redirect them to the correct URL without the sigil to the correct path above.
|
// redirect them to the correct URL without the sigil to the correct path above.
|
||||||
app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil);
|
app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil);
|
||||||
|
|
|
@ -0,0 +1,45 @@
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
// https://spec.matrix.org/v1.1/appendices/#room-aliases
|
||||||
|
// - `#room_alias:domain`
|
||||||
|
// - `#room-alias:server/date/2022/10/27`
|
||||||
|
const BASIC_ROOM_ALIAS_REGEX = /^(#(?:[^/:]+):(?:[^/]+))/;
|
||||||
|
|
||||||
|
// Returns `true` if redirecting, otherwise `false`
|
||||||
|
function redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator, redirectCallback) {
|
||||||
|
function handleHashChange() {
|
||||||
|
const pageHash = window.location.hash;
|
||||||
|
|
||||||
|
const match = pageHash.match(BASIC_ROOM_ALIAS_REGEX);
|
||||||
|
if (match) {
|
||||||
|
const roomAlias = match[0];
|
||||||
|
const newLocation = matrixPublicArchiveURLCreator.archiveUrlForRoom(roomAlias);
|
||||||
|
console.log(`Saw room alias in hash, redirecting to newLocation=${newLocation}`);
|
||||||
|
window.location = newLocation;
|
||||||
|
if (redirectCallback) {
|
||||||
|
redirectCallback();
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const eventHandler = {
|
||||||
|
handleEvent(e) {
|
||||||
|
if (e.type === 'hashchange') {
|
||||||
|
handleHashChange();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
window.addEventListener('hashchange', eventHandler);
|
||||||
|
|
||||||
|
// Handle the initial hash
|
||||||
|
if (window.location) {
|
||||||
|
return handleHashChange();
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = redirectIfRoomAliasInHash;
|
|
@ -11,6 +11,7 @@ const { Platform, Navigation, createRouter } = require('hydrogen-view-sdk');
|
||||||
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
|
const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator');
|
||||||
const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history');
|
const ArchiveHistory = require('matrix-public-archive-shared/lib/archive-history');
|
||||||
const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page');
|
const supressBlankAnchorsReloadingThePage = require('matrix-public-archive-shared/lib/supress-blank-anchors-reloading-the-page');
|
||||||
|
const redirectIfRoomAliasInHash = require('matrix-public-archive-shared/lib/redirect-if-room-alias-in-hash');
|
||||||
|
|
||||||
const RoomDirectoryView = require('matrix-public-archive-shared/views/RoomDirectoryView');
|
const RoomDirectoryView = require('matrix-public-archive-shared/views/RoomDirectoryView');
|
||||||
const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/RoomDirectoryViewModel');
|
const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/RoomDirectoryViewModel');
|
||||||
|
@ -31,6 +32,15 @@ const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(config.b
|
||||||
|
|
||||||
supressBlankAnchorsReloadingThePage();
|
supressBlankAnchorsReloadingThePage();
|
||||||
|
|
||||||
|
let roomDirectoryViewModel;
|
||||||
|
let isRedirecting = false;
|
||||||
|
isRedirecting = redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator, () => {
|
||||||
|
isRedirecting = true;
|
||||||
|
if (roomDirectoryViewModel) {
|
||||||
|
roomDirectoryViewModel.setPageRedirectingFromUrlHash(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
async function mountHydrogen() {
|
async function mountHydrogen() {
|
||||||
console.log('Mounting Hydrogen...');
|
console.log('Mounting Hydrogen...');
|
||||||
console.time('Completed mounting Hydrogen');
|
console.time('Completed mounting Hydrogen');
|
||||||
|
@ -69,7 +79,7 @@ async function mountHydrogen() {
|
||||||
// page don't say `undefined`.
|
// page don't say `undefined`.
|
||||||
urlRouter.attach();
|
urlRouter.attach();
|
||||||
|
|
||||||
const roomDirectoryViewModel = new RoomDirectoryViewModel({
|
roomDirectoryViewModel = new RoomDirectoryViewModel({
|
||||||
// Hydrogen options
|
// Hydrogen options
|
||||||
navigation: navigation,
|
navigation: navigation,
|
||||||
urlCreator: urlRouter,
|
urlCreator: urlRouter,
|
||||||
|
@ -84,6 +94,8 @@ async function mountHydrogen() {
|
||||||
nextPaginationToken,
|
nextPaginationToken,
|
||||||
prevPaginationToken,
|
prevPaginationToken,
|
||||||
});
|
});
|
||||||
|
// Update the model with the initial value
|
||||||
|
roomDirectoryViewModel.setPageRedirectingFromUrlHash(isRedirecting);
|
||||||
|
|
||||||
const view = new RoomDirectoryView(roomDirectoryViewModel);
|
const view = new RoomDirectoryView(roomDirectoryViewModel);
|
||||||
|
|
||||||
|
|
|
@ -35,6 +35,8 @@ class RoomDirectoryViewModel extends ViewModel {
|
||||||
this._homeserverName = homeserverName;
|
this._homeserverName = homeserverName;
|
||||||
this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator;
|
this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator;
|
||||||
|
|
||||||
|
this._isPageRedirectingFromUrlHash = false;
|
||||||
|
|
||||||
this._pageSearchParameters = pageSearchParameters;
|
this._pageSearchParameters = pageSearchParameters;
|
||||||
// Default to what the page started with
|
// Default to what the page started with
|
||||||
this._searchTerm = pageSearchParameters.searchTerm;
|
this._searchTerm = pageSearchParameters.searchTerm;
|
||||||
|
@ -106,6 +108,15 @@ class RoomDirectoryViewModel extends ViewModel {
|
||||||
this.homeserverSelectionModalViewModel.setOpen(shouldShowAddServerModal);
|
this.homeserverSelectionModalViewModel.setOpen(shouldShowAddServerModal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPageRedirectingFromUrlHash(newValue) {
|
||||||
|
this._isPageRedirectingFromUrlHash = newValue;
|
||||||
|
this.emitChange('isPageRedirectingFromUrlHash');
|
||||||
|
}
|
||||||
|
|
||||||
|
get isPageRedirectingFromUrlHash() {
|
||||||
|
return this._isPageRedirectingFromUrlHash;
|
||||||
|
}
|
||||||
|
|
||||||
get homeserverUrl() {
|
get homeserverUrl() {
|
||||||
return this._homeserverUrl;
|
return this._homeserverUrl;
|
||||||
}
|
}
|
||||||
|
@ -266,7 +277,7 @@ class RoomDirectoryViewModel extends ViewModel {
|
||||||
const deduplicatedHomeserverList = Object.keys(deduplicatedHomeserverMap);
|
const deduplicatedHomeserverList = Object.keys(deduplicatedHomeserverMap);
|
||||||
|
|
||||||
this._availableHomeserverList = deduplicatedHomeserverList;
|
this._availableHomeserverList = deduplicatedHomeserverList;
|
||||||
this.emit('availableHomeserverList');
|
this.emitChange('availableHomeserverList');
|
||||||
}
|
}
|
||||||
|
|
||||||
get availableHomeserverList() {
|
get availableHomeserverList() {
|
||||||
|
|
|
@ -288,6 +288,21 @@ class RoomDirectoryView extends TemplateView {
|
||||||
t.a({ className: 'RoomDirectoryView_paginationButton', href: vm.nextPageUrl }, 'Next'),
|
t.a({ className: 'RoomDirectoryView_paginationButton', href: vm.nextPageUrl }, 'Next'),
|
||||||
]),
|
]),
|
||||||
]),
|
]),
|
||||||
|
t.if(
|
||||||
|
(vm) => vm.isPageRedirectingFromUrlHash,
|
||||||
|
(t /*, vm*/) => {
|
||||||
|
return t.div({ className: 'RoomDirectoryView_notificationToast', role: 'alert' }, [
|
||||||
|
t.h5(
|
||||||
|
{ className: 'RoomDirectoryView_notificationToastTitle' },
|
||||||
|
'Found room alias in URL #hash'
|
||||||
|
),
|
||||||
|
t.p(
|
||||||
|
{ className: 'RoomDirectoryView_notificationToastDescription' },
|
||||||
|
'One sec while we try to redirect you to the right place.'
|
||||||
|
),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
),
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue