Merge pull request #228 from matrix-org/danilafe/invalid-url

Display an error when a junk URL is entered, and provide suggestions.
This commit is contained in:
Bruno Windels 2021-09-06 13:28:43 +02:00 committed by GitHub
commit a460b52e03
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 133 additions and 23 deletions

56
src/InvalidUrlView.js Normal file
View file

@ -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)
])
))
]);
}
}

View file

@ -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);
}
}

View file

@ -71,6 +71,23 @@ export const LinkKind = createEnum(
"Event" "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 { export class Link {
static validateIdentifier(identifier) { static validateIdentifier(identifier) {
return !!( return !!(
@ -105,9 +122,10 @@ export class Link {
webInstances = getWebInstanceMap(queryParams); webInstances = getWebInstanceMap(queryParams);
} }
if (linkStr.startsWith("#/")) { if (!linkStr.startsWith("#/")) {
linkStr = linkStr.substr(2); return null;
} }
linkStr = linkStr.substr(2);
const [identifier, eventId] = linkStr.split("/"); const [identifier, eventId] = linkStr.split("/");

View file

@ -19,10 +19,12 @@ import {OpenLinkView} from "./open/OpenLinkView.js";
import {CreateLinkView} from "./create/CreateLinkView.js"; import {CreateLinkView} from "./create/CreateLinkView.js";
import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js"; import {LoadServerPolicyView} from "./policy/LoadServerPolicyView.js";
import {DisclaimerView} from "./disclaimer/DisclaimerView.js"; import {DisclaimerView} from "./disclaimer/DisclaimerView.js";
import {InvalidUrlView} from "./InvalidUrlView.js";
export class RootView extends TemplateView { export class RootView extends TemplateView {
render(t, vm) { render(t, vm) {
return t.div({className: "RootView"}, [ 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.showDisclaimer, disclaimer => disclaimer ? new DisclaimerView() : null),
t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null), t.mapView(vm => vm.openLinkViewModel, vm => vm ? new OpenLinkView(vm) : null),
t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null), t.mapView(vm => vm.createLinkViewModel, vm => vm ? new CreateLinkView(vm) : null),

View file

@ -20,6 +20,7 @@ import {OpenLinkViewModel} from "./open/OpenLinkViewModel.js";
import {createClients} from "./open/clients/index.js"; import {createClients} from "./open/clients/index.js";
import {CreateLinkViewModel} from "./create/CreateLinkViewModel.js"; import {CreateLinkViewModel} from "./create/CreateLinkViewModel.js";
import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js"; import {LoadServerPolicyViewModel} from "./policy/LoadServerPolicyViewModel.js";
import {InvalidUrlViewModel} from "./InvalidUrlViewModel.js";
import {Platform} from "./Platform.js"; import {Platform} from "./Platform.js";
export class RootViewModel extends ViewModel { export class RootViewModel extends ViewModel {
@ -29,26 +30,23 @@ export class RootViewModel extends ViewModel {
this.openLinkViewModel = null; this.openLinkViewModel = null;
this.createLinkViewModel = null; this.createLinkViewModel = null;
this.loadServerPolicyViewModel = null; this.loadServerPolicyViewModel = null;
this.invalidUrlViewModel = null;
this.showDisclaimer = false; this.showDisclaimer = false;
this.preferences.on("canClear", () => { this.preferences.on("canClear", () => {
this.emitChange(); this.emitChange();
}); });
} }
_updateChildVMs(oldLink) { _updateChildVMs(newLink, oldLink) {
if (this.link) { this.link = newLink;
this.createLinkViewModel = null; if (!newLink) {
if (!oldLink || !oldLink.equals(this.link)) { this.openLinkViewModel = null;
} else if (!oldLink || !oldLink.equals(newLink)) {
this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({
link: this.link, link: newLink,
clients: createClients(), clients: createClients(),
})); }));
} }
} else {
this.openLinkViewModel = null;
this.createLinkViewModel = new CreateLinkViewModel(this.childOptions());
}
this.emitChange();
} }
_hideLinks() { _hideLinks() {
@ -58,24 +56,35 @@ export class RootViewModel extends ViewModel {
} }
updateHash(hash) { 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.showDisclaimer = false;
this.loadServerPolicyViewModel = null;
this.createLinkViewModel = null;
let newLink;
if (hash.startsWith("#/policy/")) { if (hash.startsWith("#/policy/")) {
const server = hash.substr(9); const server = hash.substr(9);
this._hideLinks(); this._updateChildVMs(null, oldLink);
this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server})); this.loadServerPolicyViewModel = new LoadServerPolicyViewModel(this.childOptions({server}));
this.loadServerPolicyViewModel.load(); this.loadServerPolicyViewModel.load();
this.emitChange();
} else if (hash.startsWith("#/disclaimer/")) { } else if (hash.startsWith("#/disclaimer/")) {
this._hideLinks(); this._updateChildVMs(null, oldLink);
this.loadServerPolicyViewModel = null;
this.showDisclaimer = true; 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 { } else {
const oldLink = this.link; this._updateChildVMs(null, oldLink);
this.loadServerPolicyViewModel = null; this.invalidUrlViewModel = new InvalidUrlViewModel(this.childOptions({
this.link = Link.parse(hash); fragment: hash
this._updateChildVMs(oldLink); }));
} }
this.emitChange();
} }
clearPreferences() { clearPreferences() {