From c68e00f7a2d9f52721823eff10fb466e49048e12 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Fri, 4 Dec 2020 12:34:31 +0100 Subject: [PATCH] implement homeserver consent stage --- css/main.css | 38 ++++++++++ src/Preferences.js | 15 +++- src/RootViewModel.js | 2 - src/open/OpenLinkView.js | 26 +++++-- src/open/OpenLinkViewModel.js | 38 +++++++--- src/open/ServerConsentView.js | 118 +++++++++++++++++++++++++++++ src/open/ServerConsentViewModel.js | 54 +++++++++++++ 7 files changed, 268 insertions(+), 23 deletions(-) create mode 100644 src/open/ServerConsentView.js create mode 100644 src/open/ServerConsentViewModel.js diff --git a/css/main.css b/css/main.css index fc89f50..2e9220f 100644 --- a/css/main.css +++ b/css/main.css @@ -61,6 +61,11 @@ textarea { font-style: normal; } +button, input { + font-size: inherit; + font-weight: inherit; +} + .RootView { margin: 0 auto; max-width: 480px; @@ -174,6 +179,39 @@ input[type='text'].large { box-sizing: border-box; } +.ServerConsentView .actions { + margin-top: 24px; + display: flex; + align-items: center; +} + +.ServerConsentView input[type=submit] { + flex: 1; + margin-left: 12px; +} + +.ServerOptions div { + margin: 8px 0; +} + +.ServerOptions label { + display: flex; + align-items: center; +} + +.ServerOptions label > span, +.ServerOptions label > .line { + margin-left: 8px; +} + +.ServerOptions label > .line { + flex: 1; + border: none; + border-bottom: 1px solid var(--grey); + padding: 4px 0; +} + + .LoadServerPolicyView { display: flex; } diff --git a/src/Preferences.js b/src/Preferences.js index d75edc2..8293371 100644 --- a/src/Preferences.js +++ b/src/Preferences.js @@ -22,7 +22,7 @@ export class Preferences { this.clientId = null; // used to differentiate web from native if a client supports both this.platform = null; - this.homeservers = []; + this.homeservers = null; const prefsStr = localStorage.getItem("preferred_client"); if (prefsStr) { @@ -30,6 +30,10 @@ export class Preferences { this.clientId = id; this.platform = Platform[platform]; } + const serversStr = localStorage.getItem("consented_servers"); + if (serversStr) { + this.homeservers = JSON.parse(serversStr); + } } setClient(id, platform) { @@ -40,16 +44,19 @@ export class Preferences { } setHomeservers(homeservers) { - + this.homeservers = homeservers; + this._localStorage.setItem("consented_servers", JSON.stringify(homeservers)); } clear() { this._localStorage.removeItem("preferred_client"); + this._localStorage.removeItem("consented_servers"); this.clientId = null; this.platform = null; + this.homeservers = null; } get canClear() { - return !!this.clientId || !!this.platform; + return !!this.clientId || !!this.platform || !!this.homeservers; } -} \ No newline at end of file +} diff --git a/src/RootViewModel.js b/src/RootViewModel.js index fba00ab..19deca7 100644 --- a/src/RootViewModel.js +++ b/src/RootViewModel.js @@ -37,10 +37,8 @@ export class RootViewModel extends ViewModel { if (!oldLink || !oldLink.equals(this.link)) { this.openLinkViewModel = new OpenLinkViewModel(this.childOptions({ link: this.link, - consentedServers: this.link.servers, clients: createClients() })); - this.openLinkViewModel.load(); } } else { this.openLinkViewModel = null; diff --git a/src/open/OpenLinkView.js b/src/open/OpenLinkView.js index c67e2ff..9f2cca4 100644 --- a/src/open/OpenLinkView.js +++ b/src/open/OpenLinkView.js @@ -17,17 +17,29 @@ limitations under the License. import {TemplateView} from "../utils/TemplateView.js"; import {ClientListView} from "./ClientListView.js"; import {PreviewView} from "../preview/PreviewView.js"; +import {ServerConsentView} from "./ServerConsentView.js"; export class OpenLinkView extends TemplateView { render(t, vm) { return t.div({className: "OpenLinkView card"}, [ - t.view(new PreviewView(vm.previewViewModel)), - t.p({className: {accept: true, hidden: vm => vm.clientsViewModel}}, t.button({ - className: "primary fullwidth", - onClick: () => vm.showClients() - }, vm => vm.showClientsLabel)), - t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null), - t.p({className: {hidden: vm => !vm.previewDomain}}, ["Preview provided by ", vm => vm.previewDomain]), + t.mapView(vm => vm.previewViewModel, previewVM => previewVM ? + new ShowLinkView(vm) : + new ServerConsentView(vm.serverConsentViewModel) + ), ]); } } + +class ShowLinkView extends TemplateView { + render(t, vm) { + return t.div([ + t.view(new PreviewView(vm.previewViewModel)), + t.p({className: {accept: true, hidden: vm => vm.clientsViewModel}}, t.button({ + className: "primary fullwidth", + onClick: () => vm.showClients() + }, vm => vm.showClientsLabel)), + t.mapView(vm => vm.clientsViewModel, childVM => childVM ? new ClientListView(childVM) : null), + t.p({className: {hidden: vm => !vm.previewDomain}}, ["Preview provided by ", vm => vm.previewDomain]), + ]); + } +} diff --git a/src/open/OpenLinkViewModel.js b/src/open/OpenLinkViewModel.js index 99707bb..a6a6320 100644 --- a/src/open/OpenLinkViewModel.js +++ b/src/open/OpenLinkViewModel.js @@ -18,31 +18,49 @@ import {ViewModel} from "../utils/ViewModel.js"; import {ClientListViewModel} from "./ClientListViewModel.js"; import {ClientViewModel} from "./ClientViewModel.js"; import {PreviewViewModel} from "../preview/PreviewViewModel.js"; +import {ServerConsentViewModel} from "./ServerConsentViewModel.js"; import {getLabelForLinkKind} from "../Link.js"; export class OpenLinkViewModel extends ViewModel { constructor(options) { super(options); - const {clients, link, consentedServers} = options; + const {clients, link} = options; this._link = link; this._clients = clients; - this.previewViewModel = new PreviewViewModel(this.childOptions({link, consentedServers})); + this.serverConsentViewModel = null; + this.previewViewModel = null; + this.clientsViewModel = null; this.previewLoading = false; - const preferredClient = this.preferences.clientId ? clients.find(c => c.id === this.preferences.clientId) : null; - this.clientsViewModel = preferredClient ? new ClientListViewModel(this.childOptions({ - clients, - link, - client: preferredClient, - })) : null; + if (this.preferences.homeservers === null) { + this.serverConsentViewModel = new ServerConsentViewModel(this.childOptions({ + servers: this._link.servers, + done: () => { + this.serverConsentViewModel = null; + this._showLink(); + } + })); + } else { + this._showLink(); + } } - async load() { + async _showLink() { + const preferredClient = this.preferences.clientId ? this._clients.find(c => c.id === this.preferences.clientId) : null; + this.clientsViewModel = preferredClient ? new ClientListViewModel(this.childOptions({ + clients: this._clients, + link: this._link, + client: preferredClient, + })) : null; + this.previewViewModel = new PreviewViewModel(this.childOptions({ + link: this._link, + consentedServers: this.preferences.homeservers + })); this.previewLoading = true; this.emitChange(); await this.previewViewModel.load(); this.previewLoading = false; this.emitChange(); - } + } get previewDomain() { return this.previewViewModel.previewDomain; diff --git a/src/open/ServerConsentView.js b/src/open/ServerConsentView.js new file mode 100644 index 0000000..cb87d7d --- /dev/null +++ b/src/open/ServerConsentView.js @@ -0,0 +1,118 @@ +/* +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 {TemplateView} from "../utils/TemplateView.js"; +import {ClientListView} from "./ClientListView.js"; +import {PreviewView} from "../preview/PreviewView.js"; + +export class ServerConsentView extends TemplateView { + render(t, vm) { + const useAnotherServer = t.button({ + className: "text", + onClick: () => vm.setShowServers()}, "use another server"); + const continueWithoutPreview = t.button({ + className: "text", + onClick: () => vm.continueWithoutConsent() + }, "continue without a preview"); + return t.div({className: "ServerConsentView"}, [ + t.p([ + "View this link using ", + t.strong(vm => vm.selectedServer || "…"), + t.span({className: {hidden: vm => !vm.selectedServer}}, [ + " (", + t.a({ + href: vm => `#/policy/${vm.selectedServer}`, + target: "_blank", + }, "privacy policy"), + ") ", + ]), + t.span({className: {hidden: vm => vm.showSelectServer}}, [ + " to preview content, or your can ", + useAnotherServer, + ]), + " or ", + continueWithoutPreview, + "." + ]), + t.form({action: "#", onSubmit: evt => this._onSubmit(evt)}, [ + t.mapView(vm => vm.showSelectServer, show => show ? new ServerOptions(vm) : null), + t.div({className: "actions"}, [ + t.label([t.input({type: "checkbox", name: "persist"}), "Ask every time"]), + t.input({type: "submit", value: "Continue", className: "primary fullwidth"}) + ]) + ]) + ]); + } + + _onSubmit(evt) { + evt.preventDefault(); + this.value.continueWithSelection(); + } +} + +class ServerOptions extends TemplateView { + render(t, vm) { + const options = vm.servers.map(server => { + return t.div(t.label([t.input({type: "radio", name: "selectedServer", value: server}), t.span(server)])) + }); + options.push(t.div({className: "other"}, t.label([ + t.input({type: "radio", name: "selectedServer", value: "other"}), + t.input({ + type: "text", + className: "line", + placeholder: "Other", + name: "otherServer", + pattern: "((?:[0-9a-zA-Z][0-9a-zA-Z-]{1,61}\\.)*)(xn--[a-z0-9]+|[a-z]+)", + onClick: evt => this._onClickOther(evt), + }) + ]))); + return t.div({ + className: "ServerOptions", + onChange: evt => this._onChange(evt), + }, options); + } + + _onClickOther(evt) { + const textField = evt.target; + const radio = Array.from(textField.form.elements.selectedServer).find(r => r.value === "other"); + if (!radio.checked) { + radio.checked = true; + this._onChangeServerRadio(radio); + } + } + + _onChange(evt) { + let {name, value} = evt.target; + if (name === "selectedServer") { + this._onChangeServerRadio(evt.target); + + } else if (name === "otherServer") { + this.value.selectServer(value); + } + } + + _onChangeServerRadio(radio) { + let {value, form} = radio; + const {otherServer} = form.elements; + if (value === "other") { + otherServer.required = true; + value = otherServer.value; + } else { + otherServer.required = false; + } + this.value.selectServer(value); + } +} diff --git a/src/open/ServerConsentViewModel.js b/src/open/ServerConsentViewModel.js new file mode 100644 index 0000000..d11b1fc --- /dev/null +++ b/src/open/ServerConsentViewModel.js @@ -0,0 +1,54 @@ +/* +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 {ClientListViewModel} from "./ClientListViewModel.js"; +import {ClientViewModel} from "./ClientViewModel.js"; +import {PreviewViewModel} from "../preview/PreviewViewModel.js"; +import {getLabelForLinkKind} from "../Link.js"; + +export class ServerConsentViewModel extends ViewModel { + constructor(options) { + super(options); + this.servers = options.servers; + this.done = options.done; + this.selectedServer = this.servers[0]; + this.showSelectServer = false; + } + + setShowServers() { + this.showSelectServer = true; + this.emitChange(); + } + + selectServer(server) { + this.selectedServer = server; + this.emitChange(); + } + + continueWithSelection() { + // keep previously consented servers + const homeservers = this.preferences.homeservers || []; + homeservers.unshift(this.selectedServer); + this.preferences.setHomeservers(homeservers); + this.done(); + } + + continueWithoutConsent() { + this.preferences.setHomeservers([]); + this.done(); + } +}