diff --git a/public/css/room-directory.css b/public/css/room-directory.css index fcc7ce4..a309147 100644 --- a/public/css/room-directory.css +++ b/public/css/room-directory.css @@ -222,12 +222,38 @@ margin-bottom: 0; } +.RoomDirectoryView_roomListError { + display: block; + width: 100%; + max-width: 1180px; + padding-left: 20px; + padding-right: 20px; + margin-top: 20px; + + line-height: 1.5; +} + +.RoomDirectoryView_codeBlock { + overflow: auto; + padding: 0.5em 0.8em; + + background-color: #f6f6f6; + border: 1px solid rgb(233, 233, 233); + border-radius: 4px; +} + @media (min-width: 750px) { .RoomDirectoryView_roomList { grid-template-columns: repeat(2, 1fr); + margin-top: 40px; padding-left: 40px; padding-right: 40px; + } + + .RoomDirectoryView_roomListError { margin-top: 40px; + padding-left: 40px; + padding-right: 40px; } } diff --git a/public/css/styles.css b/public/css/styles.css index 60a3129..4bacd4a 100644 --- a/public/css/styles.css +++ b/public/css/styles.css @@ -12,6 +12,32 @@ body { margin: 0; } +summary { + cursor: pointer; +} + +.PrimaryActionButton { + display: inline-block; + padding: 4px 16px; + + background-color: transparent; + /* Always make a pill shape */ + border-radius: 9999px; + border: 1px solid #2774c2; + + color: #2774c2; + line-height: 24px; + text-decoration: none; + + cursor: pointer; +} + +.PrimaryActionButton:hover, +.PrimaryActionButton:focus { + background-color: #2774c2; + color: #ffffff; +} + /* Based on .SessionView from Hydrogen */ .ArchiveRoomView { /* this takes into account whether or not the url bar is hidden on mobile @@ -321,28 +347,6 @@ body { margin-top: 16px; } -.ModalView_actionButton { - display: inline-block; - padding: 4px 16px; - - background-color: transparent; - /* Always make a pill shape */ - border-radius: 9999px; - border: 1px solid #2774c2; - - color: #2774c2; - line-height: 24px; - text-decoration: none; - - cursor: pointer; -} - -.ModalView_actionButton:hover, -.ModalView_actionButton:focus { - background-color: #2774c2; - color: #ffffff; -} - /* Developer options modal */ .DeveloperOptionsContentView_settingsFlag { diff --git a/server/routes/room-directory-routes.js b/server/routes/room-directory-routes.js index 494fe5a..eb86701 100644 --- a/server/routes/room-directory-routes.js +++ b/server/routes/room-directory-routes.js @@ -6,7 +6,6 @@ const urlJoin = require('url-join'); const express = require('express'); const asyncHandler = require('../lib/express-async-handler'); -const RethrownError = require('../lib/rethrown-error'); const fetchPublicRooms = require('../lib/matrix-utils/fetch-public-rooms'); const renderHydrogenVmRenderScriptToPageHtml = require('../hydrogen-render/render-hydrogen-vm-render-script-to-page-html'); @@ -39,9 +38,10 @@ router.get( // `/publicRooms` directly via the API (needs MSC). const limit = 9; - let rooms; + let rooms = []; let nextPaginationToken; let prevPaginationToken; + let roomFetchError; try { ({ rooms, nextPaginationToken, prevPaginationToken } = await fetchPublicRooms( matrixAccessToken, @@ -53,10 +53,7 @@ router.get( } )); } catch (err) { - throw new RethrownError( - `Unable to fetch rooms from room directory (using matrixServerUrl=${matrixServerUrl})\n homeserver=${homeserver}, searchTerm=${searchTerm}, paginationToken=${paginationToken}, limit=${limit}`, - err - ); + roomFetchError = err; } const hydrogenStylesUrl = urlJoin(basePath, '/hydrogen-styles.css'); @@ -68,9 +65,20 @@ router.get( path.resolve(__dirname, '../../shared/room-directory-vm-render-script.js'), { rooms, + roomFetchError: roomFetchError + ? { + message: roomFetchError.message, + stack: roomFetchError.stack, + } + : null, nextPaginationToken, prevPaginationToken, - searchTerm, + searchParameters: { + homeserver, + searchTerm, + paginationToken, + limit, + }, config: { basePath, matrixServerUrl, diff --git a/shared/room-directory-vm-render-script.js b/shared/room-directory-vm-render-script.js index 67a6a3f..b9df7a4 100644 --- a/shared/room-directory-vm-render-script.js +++ b/shared/room-directory-vm-render-script.js @@ -17,9 +17,10 @@ const RoomDirectoryViewModel = require('matrix-public-archive-shared/viewmodels/ const rooms = window.matrixPublicArchiveContext.rooms; assert(rooms); +const roomFetchError = window.matrixPublicArchiveContext.roomFetchError; const nextPaginationToken = window.matrixPublicArchiveContext.nextPaginationToken; const prevPaginationToken = window.matrixPublicArchiveContext.prevPaginationToken; -const searchTerm = window.matrixPublicArchiveContext.searchTerm; +const searchParameters = window.matrixPublicArchiveContext.searchParameters; const config = window.matrixPublicArchiveContext.config; assert(config); assert(config.matrixServerUrl); @@ -78,7 +79,8 @@ async function mountHydrogen() { homeserverName: config.matrixServerName, matrixPublicArchiveURLCreator, rooms, - searchTerm, + roomFetchError, + searchParameters, nextPaginationToken, prevPaginationToken, }); diff --git a/shared/viewmodels/RoomDirectoryViewModel.js b/shared/viewmodels/RoomDirectoryViewModel.js index c100cde..4c848a4 100644 --- a/shared/viewmodels/RoomDirectoryViewModel.js +++ b/shared/viewmodels/RoomDirectoryViewModel.js @@ -19,7 +19,8 @@ class RoomDirectoryViewModel extends ViewModel { homeserverName, matrixPublicArchiveURLCreator, rooms, - searchTerm, + roomFetchError, + searchParameters, nextPaginationToken, prevPaginationToken, } = options; @@ -28,6 +29,8 @@ class RoomDirectoryViewModel extends ViewModel { assert(matrixPublicArchiveURLCreator); assert(rooms); + this._roomFetchError = roomFetchError; + this._homeserverUrl = homeserverUrl; this._homeserverName = homeserverName; this._matrixPublicArchiveURLCreator = matrixPublicArchiveURLCreator; @@ -45,7 +48,9 @@ class RoomDirectoryViewModel extends ViewModel { }; }) ); - this._searchTerm = searchTerm; + + this._searchParameters = searchParameters; + this._searchTerm = searchParameters.searchTerm; this._addedHomeserversList = []; this._nextPaginationToken = nextPaginationToken; this._prevPaginationToken = prevPaginationToken; @@ -92,10 +97,18 @@ class RoomDirectoryViewModel extends ViewModel { return this._homeserverUrl; } + get homeserverName() { + return this._homeserverName; + } + get roomDirectoryUrl() { return this._matrixPublicArchiveURLCreator.roomDirectoryUrl(); } + get searchParameters() { + return this._searchParameters; + } + get searchTerm() { return this._searchTerm || ''; } @@ -181,6 +194,10 @@ class RoomDirectoryViewModel extends ViewModel { this.setHomeserverSelection(newHomeserver); } + get roomFetchError() { + return this._roomFetchError; + } + get nextPageUrl() { if (this._nextPaginationToken) { return this._matrixPublicArchiveURLCreator.roomDirectoryUrl({ diff --git a/shared/views/HomeserverSelectionModalContentView.js b/shared/views/HomeserverSelectionModalContentView.js index ad186f1..7e55cf1 100644 --- a/shared/views/HomeserverSelectionModalContentView.js +++ b/shared/views/HomeserverSelectionModalContentView.js @@ -24,7 +24,7 @@ class HomeserverSelectionModalContentView extends TemplateView { t.p(['Enter the name of a new server you want to explore.']), serverNameInput, t.footer({ className: 'ModalView_footerActionBar' }, [ - t.button({ className: 'ModalView_actionButton' }, 'Add'), + t.button({ className: 'PrimaryActionButton' }, 'Add'), ]), ]), ] diff --git a/shared/views/RoomDirectoryView.js b/shared/views/RoomDirectoryView.js index 6c6ae59..ea81607 100644 --- a/shared/views/RoomDirectoryView.js +++ b/shared/views/RoomDirectoryView.js @@ -168,6 +168,77 @@ class RoomDirectoryView extends TemplateView { [ t.header({ className: 'RoomDirectoryView_header' }, [headerForm]), t.main({ className: 'RoomDirectoryView_mainContent' }, [ + t.if( + (vm) => vm.roomFetchError, + (t, vm) => { + return t.section({ className: 'RoomDirectoryView_roomListError' }, [ + t.h3('❗ Unable to fetch rooms from room directory'), + t.p({}, [ + `This may be a temporary problem with the homeserver where the room directory lives (${vm.searchParameters.homeserver}) or the homeserver that the archive is pulling from (${vm.homeserverName}). You can try adjusting your search term or select a different homeserver to look at. If this problem persists, please open a `, + t.a( + { href: 'https://github.com/matrix-org/matrix-public-archive/issues/new' }, + 'bug report' + ), + ` with all of this whole section copy-pasted into the issue.`, + ]), + t.button( + { + className: 'PrimaryActionButton', + onClick: () => { + window.location.reload(); + }, + }, + 'Refresh page' + ), + t.p({}, `The exact error we ran into was:`), + t.pre( + { className: 'RoomDirectoryView_codeBlock' }, + t.code({}, vm.roomFetchError.stack) + ), + t.p({}, `The error occured with these search paramers:`), + t.pre( + { className: 'RoomDirectoryView_codeBlock' }, + t.code({}, JSON.stringify(vm.searchParameters, null, 2)) + ), + t.details({}, [ + t.summary({}, 'Why are we showing so many details?'), + t.p({}, [ + `We're showing as much detail as we know so you're not frustrated by a generic message with no feedback on how to move forward. This also makes it easier for you to write a `, + t.a( + { href: 'https://github.com/matrix-org/matrix-public-archive/issues/new' }, + 'bug report' + ), + ` with all the details necessary for us to triage it.`, + ]), + t.p({}, t.strong(`Isn't this a security risk?`)), + t.p({}, [ + `Not really. Usually, people are worried about returning details because it makes it easier for people to know how to prod and poke and get better feedback about what's going wrong to craft exploits. But the `, + t.a( + { href: 'https://github.com/matrix-org/matrix-public-archive' }, + 'Matrix Public Archive' + ), + ` is already open source so the details of the app are already public and you can run your own instance against the same homeservers that we are to find problems.`, + ]), + t.p({}, [ + `If you find any security vulnerabilities, please `, + t.a( + { href: 'https://matrix.org/security-disclosure-policy/' }, + 'responsibly disclose' + ), + ` them to us.`, + ]), + t.p({}, [ + `If you have ideas on how we can better present these errors, please `, + t.a( + { href: 'https://github.com/matrix-org/matrix-public-archive/issues' }, + 'create an issue' + ), + `.`, + ]), + ]), + ]); + } + ), t.view(roomList), t.div({ className: 'RoomDirectoryView_paginationButtonCombo' }, [ t.a(