diff --git a/build/build-client-scripts.js b/build/build-client-scripts.js index 0f50cbd..26091b1 100644 --- a/build/build-client-scripts.js +++ b/build/build-client-scripts.js @@ -19,6 +19,7 @@ const generateViteConfigForEntryPoint = require('./generate-vite-config-for-entr const entryPoints = [ 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-alias-hash-redirect.js'), ]; async function buildClientScripts(extraConfig = {}) { diff --git a/public/css/room-directory.css b/public/css/room-directory.css index a309147..0d11a4d 100644 --- a/public/css/room-directory.css +++ b/public/css/room-directory.css @@ -155,6 +155,32 @@ 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 { display: flex; flex-direction: column; diff --git a/public/js/entry-client-room-alias-hash-redirect.js b/public/js/entry-client-room-alias-hash-redirect.js new file mode 100644 index 0000000..ba62b42 --- /dev/null +++ b/public/js/entry-client-room-alias-hash-redirect.js @@ -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'; diff --git a/server/routes/client-side-room-alias-hash-redirect-route.js b/server/routes/client-side-room-alias-hash-redirect-route.js new file mode 100644 index 0000000..0e20f3d --- /dev/null +++ b/server/routes/client-side-room-alias-hash-redirect-route.js @@ -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 = ` + + +
+ +If there was a #room_alias:server hash in the URL, we tried redirecting you to the right place.
++ Otherwise, you're simply in a place that does not exist. + You can ${sanitizeHtml(`go back to the homepage.`)} +
+ + + + + + `; + + res.status(404); + res.set('Content-Type', 'text/html'); + res.send(pageHtml); +} + +module.exports = clientSideRoomAliasHashRedirectRoute; diff --git a/server/routes/install-routes.js b/server/routes/install-routes.js index be221ab..472aee6 100644 --- a/server/routes/install-routes.js +++ b/server/routes/install-routes.js @@ -8,6 +8,7 @@ const { handleTracingMiddleware } = require('../tracing/tracing-middleware'); const getVersionTags = require('../lib/get-version-tags'); const preventClickjackingMiddleware = require('./prevent-clickjacking-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'); function installRoutes(app) { @@ -62,6 +63,11 @@ function installRoutes(app) { // For room aliases (/r) or room ID's (/roomid) 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 // redirect them to the correct URL without the sigil to the correct path above. app.use('/:roomIdOrAliasDirty', redirectToCorrectArchiveUrlIfBadSigil); diff --git a/shared/lib/redirect-if-room-alias-in-hash.js b/shared/lib/redirect-if-room-alias-in-hash.js new file mode 100644 index 0000000..b68f7e5 --- /dev/null +++ b/shared/lib/redirect-if-room-alias-in-hash.js @@ -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; diff --git a/shared/room-directory-vm-render-script.js b/shared/room-directory-vm-render-script.js index 1e65c6b..e5e4e04 100644 --- a/shared/room-directory-vm-render-script.js +++ b/shared/room-directory-vm-render-script.js @@ -11,6 +11,7 @@ const { Platform, Navigation, createRouter } = require('hydrogen-view-sdk'); const MatrixPublicArchiveURLCreator = require('matrix-public-archive-shared/lib/url-creator'); 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 redirectIfRoomAliasInHash = require('matrix-public-archive-shared/lib/redirect-if-room-alias-in-hash'); const RoomDirectoryView = require('matrix-public-archive-shared/views/RoomDirectoryView'); const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/RoomDirectoryViewModel'); @@ -31,6 +32,15 @@ const matrixPublicArchiveURLCreator = new MatrixPublicArchiveURLCreator(config.b supressBlankAnchorsReloadingThePage(); +let roomDirectoryViewModel; +let isRedirecting = false; +isRedirecting = redirectIfRoomAliasInHash(matrixPublicArchiveURLCreator, () => { + isRedirecting = true; + if (roomDirectoryViewModel) { + roomDirectoryViewModel.setPageRedirectingFromUrlHash(true); + } +}); + async function mountHydrogen() { console.log('Mounting Hydrogen...'); console.time('Completed mounting Hydrogen'); @@ -69,7 +79,7 @@ async function mountHydrogen() { // page don't say `undefined`. urlRouter.attach(); - const roomDirectoryViewModel = new RoomDirectoryViewModel({ + roomDirectoryViewModel = new RoomDirectoryViewModel({ // Hydrogen options navigation: navigation, urlCreator: urlRouter, @@ -84,6 +94,8 @@ async function mountHydrogen() { nextPaginationToken, prevPaginationToken, }); + // Update the model with the initial value + roomDirectoryViewModel.setPageRedirectingFromUrlHash(isRedirecting); const view = new RoomDirectoryView(roomDirectoryViewModel); diff --git a/shared/viewmodels/RoomDirectoryViewModel.js b/shared/viewmodels/RoomDirectoryViewModel.js index 5820053..7afdf41 100644 --- a/shared/viewmodels/RoomDirectoryViewModel.js +++ b/shared/viewmodels/RoomDirectoryViewModel.js @@ -35,6 +35,8 @@ class RoomDirectoryViewModel extends ViewModel { this._homeserverName = homeserverName; this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator; + this._isPageRedirectingFromUrlHash = false; + this._pageSearchParameters = pageSearchParameters; // Default to what the page started with this._searchTerm = pageSearchParameters.searchTerm; @@ -106,6 +108,15 @@ class RoomDirectoryViewModel extends ViewModel { this.homeserverSelectionModalViewModel.setOpen(shouldShowAddServerModal); } + setPageRedirectingFromUrlHash(newValue) { + this._isPageRedirectingFromUrlHash = newValue; + this.emitChange('isPageRedirectingFromUrlHash'); + } + + get isPageRedirectingFromUrlHash() { + return this._isPageRedirectingFromUrlHash; + } + get homeserverUrl() { return this._homeserverUrl; } @@ -266,7 +277,7 @@ class RoomDirectoryViewModel extends ViewModel { const deduplicatedHomeserverList = Object.keys(deduplicatedHomeserverMap); this._availableHomeserverList = deduplicatedHomeserverList; - this.emit('availableHomeserverList'); + this.emitChange('availableHomeserverList'); } get availableHomeserverList() { diff --git a/shared/views/RoomDirectoryView.js b/shared/views/RoomDirectoryView.js index 70aefcc..193b84b 100644 --- a/shared/views/RoomDirectoryView.js +++ b/shared/views/RoomDirectoryView.js @@ -288,6 +288,21 @@ class RoomDirectoryView extends TemplateView { 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.' + ), + ]); + } + ), ] ); }