From 6f82d81f3955e321c916a76ebda8f88b90e822d2 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Mon, 19 Oct 2020 18:29:13 +0200 Subject: [PATCH 1/8] better session backup ui --- src/domain/session/SessionStatusViewModel.js | 22 ----- src/domain/session/SettingsViewModel.js | 87 ++++++++++++++++++- src/matrix/Session.js | 4 + src/matrix/e2ee/megolm/SessionBackup.js | 4 + src/matrix/ssss/index.js | 4 +- src/ui/web/css/themes/element/theme.css | 32 +++++-- src/ui/web/session/SessionStatusView.js | 7 ++ src/ui/web/session/SessionView.js | 2 +- .../settings/SessionBackupSettingsView.js | 68 +++++++++++++++ .../session/{ => settings}/SettingsView.js | 7 +- 10 files changed, 200 insertions(+), 37 deletions(-) create mode 100644 src/ui/web/session/settings/SessionBackupSettingsView.js rename src/ui/web/session/{ => settings}/SettingsView.js (84%) diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 9c1b6cde..32fd4f6c 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -138,26 +138,4 @@ export class SessionStatusViewModel extends ViewModel { this._reconnector.tryNow(); } } - - async enterPassphrase(passphrase) { - if (passphrase) { - try { - await this._session.enableSecretStorage("passphrase", passphrase); - } catch (err) { - console.error(err); - alert(`Could not set up secret storage with passphrase: ${err.message}`); - } - } - } - - async enterSecurityKey(securityKey) { - if (securityKey) { - try { - await this._session.enableSecretStorage("recoverykey", securityKey); - } catch (err) { - console.error(err); - alert(`Could not set up secret storage with securityKey: ${err.message}`); - } - } - } } diff --git a/src/domain/session/SettingsViewModel.js b/src/domain/session/SettingsViewModel.js index eeb0b302..aaf31e37 100644 --- a/src/domain/session/SettingsViewModel.js +++ b/src/domain/session/SettingsViewModel.js @@ -21,8 +21,10 @@ export class SettingsViewModel extends ViewModel { constructor(options) { super(options); this._updateService = options.updateService; - this._session = options.session; - this._closeUrl = this.urlCreator.urlUntilSegment("session"); + const session = options.session; + this._session = session; + this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session}))); + this._closeUrl = this.urlCreator.urlUntilSegment("session"); } get closeUrl() { @@ -52,7 +54,7 @@ export class SettingsViewModel extends ViewModel { if (this._updateService) { return `${this._updateService.version} (${this._updateService.buildHash})`; } - return "development version"; + return this.i18n`development version`; } checkForUpdate() { @@ -62,4 +64,83 @@ export class SettingsViewModel extends ViewModel { get showUpdateButton() { return !!this._updateService; } + + get sessionBackupViewModel() { + return this._sessionBackupViewModel; + } +} + + +class SessionBackupViewModel extends ViewModel { + constructor(options) { + super(options); + this._session = options.session; + this._showKeySetup = true; + this._error = null; + this._isBusy = false; + } + + get isBusy() { + return this._isBusy; + } + + get backupVersion() { + return this._session.sessionBackup?.version; + } + + get status() { + if (this._session.sessionBackup) { + return "enabled"; + } else { + return this._showKeySetup ? "setupKey" : "setupPhrase"; + } + } + + get error() { + return this._error?.message; + } + + showPhraseSetup() { + this._showKeySetup = false; + this.emitChange("showKeySetup"); + } + + showKeySetup() { + this._showKeySetup = true; + this.emitChange("showKeySetup"); + } + + async enterSecurityPhrase(passphrase) { + if (passphrase) { + try { + this._isBusy = true; + this.emitChange("isBusy"); + await this._session.enableSecretStorage("phrase", passphrase); + } catch (err) { + console.error(err); + this._error = err; + this.emitChange("error"); + } finally { + this._isBusy = false; + this.emitChange("isBusy"); + } + } + } + + async enterSecurityKey(securityKey) { + if (securityKey) { + try { + this._isBusy = true; + this.emitChange("isBusy"); + await this._session.enableSecretStorage("key", securityKey); + } catch (err) { + console.error(err); + this._error = err; + this.emitChange("error"); + } finally { + this._isBusy = false; + this.emitChange("isBusy"); + } + } + } } diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 9dbe9f17..80f20d0e 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -207,6 +207,10 @@ export class Session { this.needsSessionBackup.set(false); } + get sessionBackup() { + return this._sessionBackup; + } + // called after load async beforeFirstSync(isNewLogin) { if (this._olm) { diff --git a/src/matrix/e2ee/megolm/SessionBackup.js b/src/matrix/e2ee/megolm/SessionBackup.js index e6614601..a5095311 100644 --- a/src/matrix/e2ee/megolm/SessionBackup.js +++ b/src/matrix/e2ee/megolm/SessionBackup.js @@ -33,6 +33,10 @@ export class SessionBackup { return JSON.parse(sessionInfo); } + get version() { + return this._backupInfo.version; + } + dispose() { this._decryption.free(); } diff --git a/src/matrix/ssss/index.js b/src/matrix/ssss/index.js index d5bcfe97..25c85b8e 100644 --- a/src/matrix/ssss/index.js +++ b/src/matrix/ssss/index.js @@ -53,9 +53,9 @@ export async function keyFromCredential(type, credential, storage, cryptoDriver, throw new Error("Could not find a default secret storage key in account data"); } let key; - if (type === "passphrase") { + if (type === "phrase") { key = await keyFromPassphrase(keyDescription, credential, cryptoDriver); - } else if (type === "recoverykey") { + } else if (type === "key") { key = keyFromRecoveryKey(olm, keyDescription, credential); } else { throw new Error(`Invalid type: ${type}`); diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index 203dbc9e..fd6b73c4 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -306,14 +306,9 @@ a { } .SessionStatusView { - padding: 5px; - position: absolute; - top: 20px; - right: 20px; - background-color: #3D88FA; + padding: 8px 4px; + background-color: #03B381; color: white; - border-radius: 10px; - z-index: 2; } .middle-shown .SessionStatusView { @@ -548,7 +543,11 @@ ul.Timeline > li.messageStatus .message-container > p { } .SettingsBody { - padding: 12px 16px; + padding: 0px 16px; +} + +.Settings h3 { + margin: 16px 0 8px 0; } .Settings .row .label { @@ -583,3 +582,20 @@ ul.Timeline > li.messageStatus .message-container > p { .Settings .row .label { flex: 0 0 200px; } + +.Settings .error { + color: red; + font-weight: 600; +} + +button.link { + font-size: 1em; + border: none; + text-decoration: underline; + background: none; + cursor: pointer; + color: #03B381; + font-weight: 600; + margin: -12px; + padding: 12px; +} diff --git a/src/ui/web/session/SessionStatusView.js b/src/ui/web/session/SessionStatusView.js index a833c84e..9b9c10e2 100644 --- a/src/ui/web/session/SessionStatusView.js +++ b/src/ui/web/session/SessionStatusView.js @@ -32,3 +32,10 @@ export class SessionStatusView extends TemplateView { ]); } } + +export class SetupSecretStorageView extends TemplateView { + render(t, vm) { + return t.p([t.a({href: vm.setupSecretStorageUrl}, vm.i18n`Set up secret storage`), vm.i18n` to decrypt older messages.`]) + } +} + diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js index 6d3d7906..fbd3eb23 100644 --- a/src/ui/web/session/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -21,7 +21,7 @@ import {TemplateView} from "../general/TemplateView.js"; import {StaticView} from "../general/StaticView.js"; import {SessionStatusView} from "./SessionStatusView.js"; import {RoomGridView} from "./RoomGridView.js"; -import {SettingsView} from "./SettingsView.js"; +import {SettingsView} from "./settings/SettingsView.js"; export class SessionView extends TemplateView { render(t, vm) { diff --git a/src/ui/web/session/settings/SessionBackupSettingsView.js b/src/ui/web/session/settings/SessionBackupSettingsView.js new file mode 100644 index 00000000..ce4afe62 --- /dev/null +++ b/src/ui/web/session/settings/SessionBackupSettingsView.js @@ -0,0 +1,68 @@ +/* +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 "../../general/TemplateView.js"; + +export class SessionBackupSettingsView extends TemplateView { + render(t, vm) { + return t.mapView(vm => vm.status, status => { + switch (status) { + case "enabled": return new TemplateView(vm, renderEnabled) + case "setupKey": return new TemplateView(vm, renderEnableFromKey) + case "setupPhrase": return new TemplateView(vm, renderEnableFromPhrase) + } + }); + } +} + +function renderEnabled(t, vm) { + return t.p(vm.i18n`Session backup is enabled, using backup version ${vm.backupVersion}.`); +} + +function renderEnableFromKey(t, vm) { + return t.div([ + t.p(vm.i18n`Enter your security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`), + t.p([vm.i18n`Alternatively, you can `, t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`), vm.i18n` if you have one.`]), + renderError(t), + renderEnableFieldRow(t, vm.i18n`Security key`, key => vm.enterSecurityKey(key)) + ]); +} + +function renderEnableFromPhrase(t, vm) { + return t.div([ + t.p(vm.i18n`Enter your security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`), + t.p([vm.i18n`You can also `, t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`), vm.i18n`.`]), + renderError(t), + renderEnableFieldRow(t, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase)) + ]); +} + +function renderEnableFieldRow(t, label, callback) { + return t.div({className: `row`}, [ + t.div({className: "label"}, label), + t.div({className: "content"}, t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label, onChange: evt => callback(evt.target.value)})), + ]); +} + +function renderError(t) { + return t.if(vm => vm.error, t.createTemplate((t, vm) => { + return t.div([ + t.p({className: "error"}, vm => vm.i18n`Could not enable session backup: ${vm.error}.`), + t.p(vm.i18n`Try double checking that you did not mix up your security key, security phrase and login password as explained above.`) + ]) + })); +} + diff --git a/src/ui/web/session/SettingsView.js b/src/ui/web/session/settings/SettingsView.js similarity index 84% rename from src/ui/web/session/SettingsView.js rename to src/ui/web/session/settings/SettingsView.js index 73498530..82e41939 100644 --- a/src/ui/web/session/SettingsView.js +++ b/src/ui/web/session/settings/SettingsView.js @@ -14,7 +14,8 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {TemplateView} from "../general/TemplateView.js"; +import {TemplateView} from "../../general/TemplateView.js"; +import {SessionBackupSettingsView} from "./SessionBackupSettingsView.js" export class SettingsView extends TemplateView { render(t, vm) { @@ -39,9 +40,13 @@ export class SettingsView extends TemplateView { t.h2("Settings") ]), t.div({className: "SettingsBody"}, [ + t.h3("Session"), row(vm.i18n`User ID`, vm.userId), row(vm.i18n`Session ID`, vm.deviceId, "code"), row(vm.i18n`Session key`, vm.fingerprintKey, "code"), + t.h3("Session Backup"), + t.view(new SessionBackupSettingsView(vm.sessionBackupViewModel)), + t.h3("Application"), row(vm.i18n`Version`, version), ]) ]); From 6f09bd97b6ffb0ea7e0206094f8e29a6f156e867 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 12:10:41 +0200 Subject: [PATCH 2/8] move setting view models in own dir / file --- src/domain/session/SessionViewModel.js | 2 +- .../SessionBackupViewModel.js} | 59 +-------------- .../session/settings/SettingsViewModel.js | 74 +++++++++++++++++++ 3 files changed, 77 insertions(+), 58 deletions(-) rename src/domain/session/{SettingsViewModel.js => settings/SessionBackupViewModel.js} (60%) create mode 100644 src/domain/session/settings/SettingsViewModel.js diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index b9b6def1..081e9f16 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -19,7 +19,7 @@ import {LeftPanelViewModel} from "./leftpanel/LeftPanelViewModel.js"; import {RoomViewModel} from "./room/RoomViewModel.js"; import {SessionStatusViewModel} from "./SessionStatusViewModel.js"; import {RoomGridViewModel} from "./RoomGridViewModel.js"; -import {SettingsViewModel} from "./SettingsViewModel.js"; +import {SettingsViewModel} from "./settings/SettingsViewModel.js"; import {ViewModel} from "../ViewModel.js"; export class SessionViewModel extends ViewModel { diff --git a/src/domain/session/SettingsViewModel.js b/src/domain/session/settings/SessionBackupViewModel.js similarity index 60% rename from src/domain/session/SettingsViewModel.js rename to src/domain/session/settings/SessionBackupViewModel.js index aaf31e37..c617e504 100644 --- a/src/domain/session/SettingsViewModel.js +++ b/src/domain/session/settings/SessionBackupViewModel.js @@ -1,5 +1,4 @@ /* -Copyright 2020 Bruno Windels Copyright 2020 The Matrix.org Foundation C.I.C. Licensed under the Apache License, Version 2.0 (the "License"); @@ -15,63 +14,9 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {ViewModel} from "../ViewModel.js"; +import {ViewModel} from "../../ViewModel.js"; -export class SettingsViewModel extends ViewModel { - constructor(options) { - super(options); - this._updateService = options.updateService; - const session = options.session; - this._session = session; - this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session}))); - this._closeUrl = this.urlCreator.urlUntilSegment("session"); - } - - get closeUrl() { - return this._closeUrl; - } - - get fingerprintKey() { - const key = this._session.fingerprintKey; - const partLength = 4; - const partCount = Math.ceil(key.length / partLength); - let formattedKey = ""; - for (let i = 0; i < partCount; i += 1) { - formattedKey += (formattedKey.length ? " " : "") + key.slice(i * partLength, (i + 1) * partLength); - } - return formattedKey; - } - - get deviceId() { - return this._session.deviceId; - } - - get userId() { - return this._session.userId; - } - - get version() { - if (this._updateService) { - return `${this._updateService.version} (${this._updateService.buildHash})`; - } - return this.i18n`development version`; - } - - checkForUpdate() { - this._updateService?.checkForUpdate(); - } - - get showUpdateButton() { - return !!this._updateService; - } - - get sessionBackupViewModel() { - return this._sessionBackupViewModel; - } -} - - -class SessionBackupViewModel extends ViewModel { +export class SessionBackupViewModel extends ViewModel { constructor(options) { super(options); this._session = options.session; diff --git a/src/domain/session/settings/SettingsViewModel.js b/src/domain/session/settings/SettingsViewModel.js new file mode 100644 index 00000000..76931884 --- /dev/null +++ b/src/domain/session/settings/SettingsViewModel.js @@ -0,0 +1,74 @@ +/* +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 "../../ViewModel.js"; +import {SessionBackupViewModel} from "./SessionBackupViewModel.js"; + +function formatKey(key) { + const partLength = 4; + const partCount = Math.ceil(key.length / partLength); + let formattedKey = ""; + for (let i = 0; i < partCount; i += 1) { + formattedKey += (formattedKey.length ? " " : "") + key.slice(i * partLength, (i + 1) * partLength); + } + return formattedKey; +} + +export class SettingsViewModel extends ViewModel { + constructor(options) { + super(options); + this._updateService = options.updateService; + const session = options.session; + this._session = session; + this._sessionBackupViewModel = this.track(new SessionBackupViewModel(this.childOptions({session}))); + this._closeUrl = this.urlCreator.urlUntilSegment("session"); + } + + get closeUrl() { + return this._closeUrl; + } + + get fingerprintKey() { + return formatKey(this._session.fingerprintKey); + } + + get deviceId() { + return this._session.deviceId; + } + + get userId() { + return this._session.userId; + } + + get version() { + if (this._updateService) { + return `${this._updateService.version} (${this._updateService.buildHash})`; + } + return this.i18n`development version`; + } + + checkForUpdate() { + this._updateService?.checkForUpdate(); + } + + get showUpdateButton() { + return !!this._updateService; + } + + get sessionBackupViewModel() { + return this._sessionBackupViewModel; + } +} From 16e0f515dc7e5be495b87896d1596ddf54241316 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 12:10:56 +0200 Subject: [PATCH 3/8] make text not sprawl across screen on large monitors --- src/ui/web/css/themes/element/theme.css | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index fd6b73c4..22b5ebf7 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -550,6 +550,10 @@ ul.Timeline > li.messageStatus .message-container > p { margin: 16px 0 8px 0; } +.Settings p { + max-width: 700px; +} + .Settings .row .label { font-weight: 600; } From 98d7ffe87cf07a7dafe82a67526830849bf4c6c9 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 12:11:39 +0200 Subject: [PATCH 4/8] add button to set up field --- .../session/settings/SessionBackupSettingsView.js | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/ui/web/session/settings/SessionBackupSettingsView.js b/src/ui/web/session/settings/SessionBackupSettingsView.js index ce4afe62..c117f73d 100644 --- a/src/ui/web/session/settings/SessionBackupSettingsView.js +++ b/src/ui/web/session/settings/SessionBackupSettingsView.js @@ -37,7 +37,7 @@ function renderEnableFromKey(t, vm) { t.p(vm.i18n`Enter your security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`), t.p([vm.i18n`Alternatively, you can `, t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`), vm.i18n` if you have one.`]), renderError(t), - renderEnableFieldRow(t, vm.i18n`Security key`, key => vm.enterSecurityKey(key)) + renderEnableFieldRow(t, vm, vm.i18n`Security key`, key => vm.enterSecurityKey(key)) ]); } @@ -46,14 +46,19 @@ function renderEnableFromPhrase(t, vm) { t.p(vm.i18n`Enter your security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`), t.p([vm.i18n`You can also `, t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`), vm.i18n`.`]), renderError(t), - renderEnableFieldRow(t, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase)) + renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase)) ]); } -function renderEnableFieldRow(t, label, callback) { +function renderEnableFieldRow(t, vm, label, callback) { + const eventHandler = () => callback(input.value); + const input = t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label, onChange: eventHandler}); return t.div({className: `row`}, [ t.div({className: "label"}, label), - t.div({className: "content"}, t.input({type: "password", disabled: vm => vm.isBusy, placeholder: label, onChange: evt => callback(evt.target.value)})), + t.div({className: "content"}, [ + input, + t.button({disabled: vm => vm.isBusy, onClick: eventHandler}, vm.i18n`Set up`), + ]), ]); } From 446d30469b1ae1c6c9506a38b05d8d113a2db420 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 12:12:07 +0200 Subject: [PATCH 5/8] improve wording and cleanup --- .../web/session/settings/SessionBackupSettingsView.js | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/ui/web/session/settings/SessionBackupSettingsView.js b/src/ui/web/session/settings/SessionBackupSettingsView.js index c117f73d..162f2525 100644 --- a/src/ui/web/session/settings/SessionBackupSettingsView.js +++ b/src/ui/web/session/settings/SessionBackupSettingsView.js @@ -33,18 +33,20 @@ function renderEnabled(t, vm) { } function renderEnableFromKey(t, vm) { + const useASecurityPhrase = t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`); return t.div([ - t.p(vm.i18n`Enter your security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`), - t.p([vm.i18n`Alternatively, you can `, t.button({className: "link", onClick: () => vm.showPhraseSetup()}, vm.i18n`use a security phrase`), vm.i18n` if you have one.`]), + t.p(vm.i18n`Enter your secret storage security key below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security key is a code of 12 groups of 4 characters separated by a space that Element created for you when setting up security.`), + t.p([vm.i18n`Alternatively, you can `, useASecurityPhrase, vm.i18n` if you have one.`]), renderError(t), renderEnableFieldRow(t, vm, vm.i18n`Security key`, key => vm.enterSecurityKey(key)) ]); } function renderEnableFromPhrase(t, vm) { + const useASecurityKey = t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`); return t.div([ - t.p(vm.i18n`Enter your security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`), - t.p([vm.i18n`You can also `, t.button({className: "link", onClick: () => vm.showKeySetup()}, vm.i18n`use your security key`), vm.i18n`.`]), + t.p(vm.i18n`Enter your secret storage security phrase below to set up session backup, which will enable you to decrypt messages received before you logged into this session. The security phrase is a freeform secret phrase you optionally chose when setting up security in Element. It is different from your password to login, unless you chose to set them to the same value.`), + t.p([vm.i18n`You can also `, useASecurityKey, vm.i18n`.`]), renderError(t), renderEnableFieldRow(t, vm, vm.i18n`Security phrase`, phrase => vm.enterSecurityPhrase(phrase)) ]); From 601bdbb25d4ddcadd49840fe4f1bc438dc191847 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 14:42:17 +0200 Subject: [PATCH 6/8] make session backup banner dismissable --- src/domain/session/SessionStatusViewModel.js | 23 +++++++++++-- src/ui/web/css/status.css | 7 ---- .../web/css/themes/element/icons/dismiss.svg | 4 +++ src/ui/web/css/themes/element/theme.css | 34 ++++++++++++++++--- src/ui/web/session/SessionStatusView.js | 7 ++-- 5 files changed, 56 insertions(+), 19 deletions(-) create mode 100644 src/ui/web/css/themes/element/icons/dismiss.svg diff --git a/src/domain/session/SessionStatusViewModel.js b/src/domain/session/SessionStatusViewModel.js index 32fd4f6c..4486f654 100644 --- a/src/domain/session/SessionStatusViewModel.js +++ b/src/domain/session/SessionStatusViewModel.js @@ -36,6 +36,8 @@ export class SessionStatusViewModel extends ViewModel { this._reconnector = reconnector; this._status = this._calculateState(reconnector.connectionStatus.get(), sync.status.get()); this._session = session; + this._setupSessionBackupUrl = this.urlCreator.urlForSegment("settings"); + this._dismissSecretStorage = false; } start() { @@ -47,8 +49,12 @@ export class SessionStatusViewModel extends ViewModel { })); } + get setupSessionBackupUrl () { + return this._setupSessionBackupUrl; + } + get isShown() { - return this._session.needsSessionBackup.get() || this._status !== SessionStatus.Syncing; + return (this._session.needsSessionBackup.get() && !this._dismissSecretStorage) || this._status !== SessionStatus.Syncing; } get statusLabel() { @@ -65,7 +71,7 @@ export class SessionStatusViewModel extends ViewModel { return this.i18n`Sync failed because of ${this._sync.error}`; } if (this._session.needsSessionBackup.get()) { - return this.i18n`Set up secret storage to decrypt older messages.`; + return this.i18n`Set up session backup to decrypt older messages.`; } return ""; } @@ -130,7 +136,18 @@ export class SessionStatusViewModel extends ViewModel { get isSecretStorageShown() { // TODO: we need a model here where we can have multiple messages queued up and their buttons don't bleed into each other. - return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get(); + return this._status === SessionStatus.Syncing && this._session.needsSessionBackup.get() && !this._dismissSecretStorage; + } + + get canDismiss() { + return this.isSecretStorageShown; + } + + dismiss() { + if (this.isSecretStorageShown) { + this._dismissSecretStorage = true; + this.emitChange(); + } } connectNow() { diff --git a/src/ui/web/css/status.css b/src/ui/web/css/status.css index de891de1..3c4a436a 100644 --- a/src/ui/web/css/status.css +++ b/src/ui/web/css/status.css @@ -24,10 +24,3 @@ limitations under the License. word-break: break-all; word-break: break-word; } - -.SessionStatusView button { - border: none; - background: none; - color: currentcolor; - text-decoration: underline; -} diff --git a/src/ui/web/css/themes/element/icons/dismiss.svg b/src/ui/web/css/themes/element/icons/dismiss.svg new file mode 100644 index 00000000..1b9cea04 --- /dev/null +++ b/src/ui/web/css/themes/element/icons/dismiss.svg @@ -0,0 +1,4 @@ + + + + diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index 22b5ebf7..b880c83d 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -306,13 +306,34 @@ a { } .SessionStatusView { - padding: 8px 4px; + padding: 4px; + min-height: 22px; background-color: #03B381; color: white; + align-items: center; } -.middle-shown .SessionStatusView { - top: 72px; + +.SessionStatusView button.link { + color: currentcolor; +} + +.SessionStatusView > .end { + flex: 1; + display: flex; + justify-content: flex-end; + align-self: stretch; + align-items: stretch; +} + +.SessionStatusView .dismiss { + border: none; + background: none; + background-image: url('icons/dismiss.svg'); + background-position: center; + background-repeat: no-repeat; + width: 32px; + cursor: pointer; } .room-placeholder { @@ -598,8 +619,11 @@ button.link { text-decoration: underline; background: none; cursor: pointer; - color: #03B381; - font-weight: 600; margin: -12px; padding: 12px; } + +.Settings a, .Settings .link { + color: #03B381; + font-weight: 600; +} diff --git a/src/ui/web/session/SessionStatusView.js b/src/ui/web/session/SessionStatusView.js index 9b9c10e2..946264bc 100644 --- a/src/ui/web/session/SessionStatusView.js +++ b/src/ui/web/session/SessionStatusView.js @@ -25,10 +25,9 @@ export class SessionStatusView extends TemplateView { }}, [ spinner(t, {hidden: vm => !vm.isWaiting}), t.p(vm => vm.statusLabel), - t.if(vm => vm.isConnectNowShown, t.createTemplate(t => t.button({onClick: () => vm.connectNow()}, "Retry now"))), - t.if(vm => vm.isSecretStorageShown, t.createTemplate(t => t.button({onClick: () => vm.enterPassphrase(prompt("Passphrase"))}, "Enter passphrase"))), - t.if(vm => vm.isSecretStorageShown, t.createTemplate(t => t.button({onClick: () => vm.enterSecurityKey(prompt("Security key"))}, "Enter security key"))), - window.DEBUG ? t.button({id: "showlogs"}, "Show logs") : "" + t.if(vm => vm.isConnectNowShown, t.createTemplate(t => t.button({className: "link", onClick: () => vm.connectNow()}, "Retry now"))), + t.if(vm => vm.isSecretStorageShown, t.createTemplate(t => t.a({href: vm.setupSessionBackupUrl}, "Go to settings"))), + t.if(vm => vm.canDismiss, t.createTemplate(t => t.div({className: "end"}, t.button({className: "dismiss", onClick: () => vm.dismiss()})))), ]); } } From 1e1b254678fa228360ca5379852d596b47dfaa1b Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 15:26:22 +0200 Subject: [PATCH 7/8] adjust prop names --- src/domain/session/settings/SessionBackupViewModel.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/domain/session/settings/SessionBackupViewModel.js b/src/domain/session/settings/SessionBackupViewModel.js index c617e504..2fee7b82 100644 --- a/src/domain/session/settings/SessionBackupViewModel.js +++ b/src/domain/session/settings/SessionBackupViewModel.js @@ -47,12 +47,12 @@ export class SessionBackupViewModel extends ViewModel { showPhraseSetup() { this._showKeySetup = false; - this.emitChange("showKeySetup"); + this.emitChange("status"); } showKeySetup() { this._showKeySetup = true; - this.emitChange("showKeySetup"); + this.emitChange("status"); } async enterSecurityPhrase(passphrase) { @@ -67,7 +67,7 @@ export class SessionBackupViewModel extends ViewModel { this.emitChange("error"); } finally { this._isBusy = false; - this.emitChange("isBusy"); + this.emitChange(""); } } } @@ -84,7 +84,7 @@ export class SessionBackupViewModel extends ViewModel { this.emitChange("error"); } finally { this._isBusy = false; - this.emitChange("isBusy"); + this.emitChange(""); } } } From 4dd303e558d3a33810b5319b19e53bbd2a9c2cf8 Mon Sep 17 00:00:00 2001 From: Bruno Windels Date: Tue, 20 Oct 2020 15:29:36 +0200 Subject: [PATCH 8/8] remove unused code --- src/ui/web/session/SessionStatusView.js | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/ui/web/session/SessionStatusView.js b/src/ui/web/session/SessionStatusView.js index 946264bc..6a123ea9 100644 --- a/src/ui/web/session/SessionStatusView.js +++ b/src/ui/web/session/SessionStatusView.js @@ -31,10 +31,3 @@ export class SessionStatusView extends TemplateView { ]); } } - -export class SetupSecretStorageView extends TemplateView { - render(t, vm) { - return t.p([t.a({href: vm.setupSecretStorageUrl}, vm.i18n`Set up secret storage`), vm.i18n` to decrypt older messages.`]) - } -} -