diff --git a/src/InvalidUrlView.js b/src/InvalidUrlView.js new file mode 100644 index 0000000..ecc66da --- /dev/null +++ b/src/InvalidUrlView.js @@ -0,0 +1,56 @@ +/* +Copyright 2021 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {TemplateView} from "./utils/TemplateView.js"; +import {LinkKind, IdentifierKind} from "./Link.js"; + +export class InvalidUrlView extends TemplateView { + render(t, vm) { + return t.div({ className: "DisclaimerView card" }, [ + t.h1("Invalid URL"), + t.p([ + 'The link you have entered is not valid. If you like, you can ', + t.a({ href: "#/" }, 'return to the home page.') + ]), + vm.validFixes.length ? this._renderValidFixes(t, vm.validFixes) : [], + ]); + } + + _describeRoom(identifierKind) { + return identifierKind === IdentifierKind.RoomAlias ? "room alias" : "room"; + } + + _describeLinkKind(linkKind, identifierKind) { + switch (linkKind) { + case LinkKind.Room: return `The ${this._describeRoom(identifierKind)} `; + case LinkKind.User: return "The user "; + case LinkKind.Group: return "The group "; + case LinkKind.Event: return `An event in ${this._describeRoom(identifierKind)} `; + } + } + + _renderValidFixes(t, validFixes) { + return t.p([ + 'Did you mean any of the following?', + t.ul(validFixes.map(fix => + t.li([ + this._describeLinkKind(fix.link.kind, fix.link.identifierKind), + t.a({ href: fix.url }, fix.link.identifier) + ]) + )) + ]); + } +} diff --git a/src/InvalidUrlViewModel.js b/src/InvalidUrlViewModel.js new file mode 100644 index 0000000..dc0bf56 --- /dev/null +++ b/src/InvalidUrlViewModel.js @@ -0,0 +1,25 @@ +/* +Copyright 2020 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +import {ViewModel} from "./utils/ViewModel.js"; +import {tryFixUrl} from "./Link.js"; + +export class InvalidUrlViewModel extends ViewModel { + constructor(options) { + super(options); + this.validFixes = tryFixUrl(options.fragment); + } +} diff --git a/src/Link.js b/src/Link.js index 079d7be..cb7653f 100644 --- a/src/Link.js +++ b/src/Link.js @@ -71,6 +71,23 @@ export const LinkKind = createEnum( "Event" ) +export function tryFixUrl(fragment) { + const attempts = []; + const afterHash = fragment.substring(fragment.startsWith("#/") ? 2 : 1); + attempts.push('#/@' + afterHash); + attempts.push('#/#' + afterHash); + attempts.push('#/!' + afterHash); + + const validAttempts = []; + for (const attempt of [...new Set(attempts)]) { + const link = Link.parse(attempt); + if (link) { + validAttempts.push({ url: attempt, link }); + } + } + return validAttempts; +} + export class Link { static validateIdentifier(identifier) { return !!( @@ -105,9 +122,10 @@ export class Link { webInstances = getWebInstanceMap(queryParams); } - if (linkStr.startsWith("#/")) { - linkStr = linkStr.substr(2); + if (!linkStr.startsWith("#/")) { + return null; } + linkStr = linkStr.substr(2); const [identifier, eventId] = linkStr.split("/"); diff --git a/src/RootView.js b/src/RootView.js index 5669f36..3dc2d23 100644 --- a/src/RootView.js +++ b/src/RootView.js @@ -19,10 +19,12 @@ import {OpenLinkView} from "./open/OpenLinkView.js"; import {CreateLinkView} from "./create/CreateLinkView.js"; import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js"; import {DisclaimerView} from "./disclaimer/DisclaimerView.js"; +import {InvalidUrlView} from "./InvalidUrlView.js"; export class RootView extends TemplateView { render(t, vm) { return t.div({className: "RootView"}, [ + t.mapView(vm => vm.invalidUrlViewModel, invalidVM => invalidVM ? new InvalidUrlView(invalidVM) : null), t.mapView(vm => vm.showDisclaimer, disclaimer => disclaimer ? new DisclaimerView() : null), t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), diff --git a/src/RootViewModel.js b/src/RootViewModel.js index 7c2d35e..39f6733 100644 --- a/src/RootViewModel.js +++ b/src/RootViewModel.js @@ -20,6 +20,7 @@ import {OpenLinkViewModel} from "./open/OpenLinkViewModel.js"; import {createClients} from "./open/clients/index.js"; import {CreateLinkViewModel} from "./create/CreateLinkViewModel.js"; import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js"; +import {InvalidUrlViewModel} from "./InvalidUrlViewModel.js"; import {Platform} from "./Platform.js"; export class RootViewModel extends ViewModel { @@ -29,26 +30,23 @@ export class RootViewModel extends ViewModel { this.openLinkViewModel = null; this.createLinkViewModel = null; this.loadServerPolicyViewModel = null; + this.invalidUrlViewModel = null; this.showDisclaimer = false; this.preferences.on("canClear", () => { this.emitChange(); }); } - _updateChildVMs(oldLink) { - if (this.link) { - this.createLinkViewModel = null; - if (!oldLink || !oldLink.equals(this.link)) { - this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ - link: this.link, - clients: createClients(), - })); - } - } else { + _updateChildVMs(newLink, oldLink) { + this.link = newLink; + if (!newLink) { this.openLinkViewModel = null; - this.createLinkViewModel = new CreateLinkViewModel(this.childOptions()); + } else if (!oldLink || !oldLink.equals(newLink)) { + this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ + link: newLink, + clients: createClients(), + })); } - this.emitChange(); } _hideLinks() { @@ -58,24 +56,35 @@ export class RootViewModel extends ViewModel { } updateHash(hash) { + // All view models except openLink are re-created anyway. Might as well + // clear them to avoid having to manually reset (n-1)/n view models in every case. + // That just doesn't scale well when we add new views. + const oldLink = this.link; + this.invalidUrlViewModel = null; this.showDisclaimer = false; + this.loadServerPolicyViewModel = null; + this.createLinkViewModel = null; + let newLink; if (hash.startsWith("#/policy/")) { const server = hash.substr(9); - this._hideLinks(); + this._updateChildVMs(null, oldLink); this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server})); this.loadServerPolicyViewModel.load(); - this.emitChange(); } else if (hash.startsWith("#/disclaimer/")) { - this._hideLinks(); - this.loadServerPolicyViewModel = null; + this._updateChildVMs(null, oldLink); this.showDisclaimer = true; - this.emitChange(); + } else if (hash === "" || hash === "#" || hash === "#/") { + this._updateChildVMs(null, oldLink); + this.createLinkViewModel = new CreateLinkViewModel(this.childOptions()); + } else if (newLink = Link.parse(hash)) { + this._updateChildVMs(newLink, oldLink); } else { - const oldLink = this.link; - this.loadServerPolicyViewModel = null; - this.link = Link.parse(hash); - this._updateChildVMs(oldLink); + this._updateChildVMs(null, oldLink); + this.invalidUrlViewModel = new InvalidUrlViewModel(this.childOptions({ + fragment: hash + })); } + this.emitChange(); } clearPreferences() {