From b57fbd8b6d213ece500847ed54d95ba43c5f90e1 Mon Sep 17 00:00:00 2001 From: Danila Fedorin Date: Mon, 30 Aug 2021 14:14:31 -0700 Subject: [PATCH] Add suggestions for invalid URLs --- src/InvalidUrlView.js | 22 +++++++++++++++++++++- src/InvalidUrlViewModel.js | 25 +++++++++++++++++++++++++ src/Link.js | 18 ++++++++++++++++++ src/RootView.js | 2 +- src/RootViewModel.js | 9 ++++++--- 5 files changed, 71 insertions(+), 5 deletions(-) create mode 100644 src/InvalidUrlViewModel.js diff --git a/src/InvalidUrlView.js b/src/InvalidUrlView.js index e6a58e7..a13bcd2 100644 --- a/src/InvalidUrlView.js +++ b/src/InvalidUrlView.js @@ -15,15 +15,35 @@ limitations under the License. */ import {TemplateView} from "./utils/TemplateView.js"; +import {LinkKind} from "./Link.js"; export class InvalidUrlView extends TemplateView { - render(t) { + 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) : [], + ]); + } + + _describeLinkKind(kind) { + switch (kind) { + case LinkKind.Room: return "The room "; + case LinkKind.User: return "The user "; + case LinkKind.Group: return "The group "; + case LinkKind.Event: return "An event in room "; + } + } + + _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), 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 53459ae..76bfa8a 100644 --- a/src/Link.js +++ b/src/Link.js @@ -71,6 +71,24 @@ export const LinkKind = createEnum( "Event" ) +export function tryFixUrl(fragment) { + const attempts = []; + if (fragment.startsWith('#') && !fragment.startsWith('#/')) { + if (!fragment.match(/^#[@$!]/)) { + attempts.push('#/' + fragment); // #room => #/#room + } + attempts.push('#/' + fragment.substring(1)); // #@room => #/@room + } + const validAttempts = []; + for (const attempt of attempts) { + const link = Link.parse(attempt); + if (link) { + validAttempts.push({ url: attempt, link }); + } + } + return validAttempts; +} + export class Link { static validateIdentifier(identifier) { return !!( diff --git a/src/RootView.js b/src/RootView.js index 218ad57..3dc2d23 100644 --- a/src/RootView.js +++ b/src/RootView.js @@ -24,7 +24,7 @@ import {InvalidUrlView} from "./InvalidUrlView.js"; export class RootView extends TemplateView { render(t, vm) { return t.div({className: "RootView"}, [ - t.mapView(vm => vm.invalidUrl, invalid => invalid ? new InvalidUrlView() : null), + 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 b9cde3f..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,8 +30,8 @@ export class RootViewModel extends ViewModel { this.openLinkViewModel = null; this.createLinkViewModel = null; this.loadServerPolicyViewModel = null; + this.invalidUrlViewModel = null; this.showDisclaimer = false; - this.invalidUrl = false; this.preferences.on("canClear", () => { this.emitChange(); }); @@ -59,7 +60,7 @@ export class RootViewModel extends ViewModel { // 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.invalidUrl = false; + this.invalidUrlViewModel = null; this.showDisclaimer = false; this.loadServerPolicyViewModel = null; this.createLinkViewModel = null; @@ -79,7 +80,9 @@ export class RootViewModel extends ViewModel { this._updateChildVMs(newLink, oldLink); } else { this._updateChildVMs(null, oldLink); - this.invalidUrl = true; + this.invalidUrlViewModel = new InvalidUrlViewModel(this.childOptions({ + fragment: hash + })); } this.emitChange(); }