diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 3316bacc..a5ba1da3 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -125,7 +125,10 @@ export class RootViewModel extends ViewModel { _showSession(sessionContainer) { this._setSection(() => { - this._sessionViewModel = new SessionViewModel(this.childOptions({sessionContainer})); + this._sessionViewModel = new SessionViewModel(this.childOptions({ + sessionContainer, + updateService: this.getOption("updateService") + })); this._sessionViewModel.start(); }); } diff --git a/src/domain/ViewModel.js b/src/domain/ViewModel.js index 8c847247..a8b58921 100644 --- a/src/domain/ViewModel.js +++ b/src/domain/ViewModel.js @@ -34,6 +34,11 @@ export class ViewModel extends EventEmitter { return Object.assign({navigation, urlCreator, clock}, explicitOptions); } + // makes it easier to pass through dependencies of a sub-view model + getOption(name) { + return this._options[name]; + } + track(disposable) { if (!this.disposables) { this.disposables = new Disposables(); diff --git a/src/domain/session/SessionViewModel.js b/src/domain/session/SessionViewModel.js index de110fa1..b9b6def1 100644 --- a/src/domain/session/SessionViewModel.js +++ b/src/domain/session/SessionViewModel.js @@ -19,6 +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 {ViewModel} from "../ViewModel.js"; export class SessionViewModel extends ViewModel { @@ -34,6 +35,7 @@ export class SessionViewModel extends ViewModel { this._leftPanelViewModel = this.track(new LeftPanelViewModel(this.childOptions({ rooms: this._sessionContainer.session.rooms }))); + this._settingsViewModel = null; this._currentRoomViewModel = null; this._gridViewModel = null; this._setupNavigation(); @@ -53,12 +55,18 @@ export class SessionViewModel extends ViewModel { // this gives us the active room this.track(currentRoomId.subscribe(roomId => { if (!this._gridViewModel) { - this._openRoom(roomId); + this._updateRoom(roomId); } })); - if (currentRoomId.get() && !this._gridViewModel) { - this._openRoom(currentRoomId.get()); + if (!this._gridViewModel) { + this._updateRoom(currentRoomId.get()); } + + const settings = this.navigation.observe("settings"); + this.track(settings.subscribe(settingsOpen => { + this._updateSettings(settingsOpen); + })); + this._updateSettings(settings.get()); } get id() { @@ -74,6 +82,8 @@ export class SessionViewModel extends ViewModel { return this._currentRoomViewModel.id; } else if (this._gridViewModel) { return "roomgrid"; + } else if (this._settingsViewModel) { + return "settings"; } return "placeholder"; } @@ -90,6 +100,10 @@ export class SessionViewModel extends ViewModel { return this._sessionStatusViewModel; } + get settingsViewModel() { + return this._settingsViewModel; + } + get roomList() { return this._roomList; } @@ -148,7 +162,7 @@ export class SessionViewModel extends ViewModel { return roomVM; } - _openRoom(roomId) { + _updateRoom(roomId) { if (!roomId) { if (this._currentRoomViewModel) { this._currentRoomViewModel = this.disposeTracked(this._currentRoomViewModel); @@ -167,4 +181,17 @@ export class SessionViewModel extends ViewModel { } this.emitChange("currentRoom"); } + + _updateSettings(settingsOpen) { + if (this._settingsViewModel) { + this._settingsViewModel = this.disposeTracked(this._settingsViewModel); + } + if (settingsOpen) { + this._settingsViewModel = this.track(new SettingsViewModel(this.childOptions({ + updateService: this.getOption("updateService"), + session: this._sessionContainer.session + }))); + } + this.emitChange("activeSection"); + } } diff --git a/src/domain/session/SettingsViewModel.js b/src/domain/session/SettingsViewModel.js new file mode 100644 index 00000000..eeb0b302 --- /dev/null +++ b/src/domain/session/SettingsViewModel.js @@ -0,0 +1,65 @@ +/* +Copyright 2020 Bruno Windels +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"; + +export class SettingsViewModel extends ViewModel { + constructor(options) { + super(options); + this._updateService = options.updateService; + this._session = options.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 "development version"; + } + + checkForUpdate() { + this._updateService?.checkForUpdate(); + } + + get showUpdateButton() { + return !!this._updateService; + } +} diff --git a/src/domain/session/leftpanel/LeftPanelViewModel.js b/src/domain/session/leftpanel/LeftPanelViewModel.js index 9d57d6bd..de70245a 100644 --- a/src/domain/session/leftpanel/LeftPanelViewModel.js +++ b/src/domain/session/leftpanel/LeftPanelViewModel.js @@ -45,12 +45,17 @@ export class LeftPanelViewModel extends ViewModel { this._currentTileVM = null; this._setupNavigation(); this._closeUrl = this.urlCreator.urlForSegment("session"); + this._settingsUrl = this.urlCreator.urlForSegment("settings"); } get closeUrl() { return this._closeUrl; } + get settingsUrl() { + return this._settingsUrl; + } + _setupNavigation() { const roomObservable = this.navigation.observe("room"); this.track(roomObservable.subscribe(roomId => this._open(roomId))); diff --git a/src/matrix/Session.js b/src/matrix/Session.js index 708efd29..9dbe9f17 100644 --- a/src/matrix/Session.js +++ b/src/matrix/Session.js @@ -77,6 +77,18 @@ export class Session { this.needsSessionBackup = new ObservableValue(false); } + get fingerprintKey() { + return this._e2eeAccount?.identityKeys.ed25519; + } + + get deviceId() { + return this._sessionInfo.deviceId; + } + + get userId() { + return this._sessionInfo.userId; + } + // called once this._e2eeAccount is assigned _setupEncryption() { console.log("loaded e2ee account with keys", this._e2eeAccount.identityKeys); diff --git a/src/ui/web/css/themes/element/icons/settings.svg b/src/ui/web/css/themes/element/icons/settings.svg new file mode 100644 index 00000000..9d1fbc78 --- /dev/null +++ b/src/ui/web/css/themes/element/icons/settings.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/ui/web/css/themes/element/theme.css b/src/ui/web/css/themes/element/theme.css index 8b634fe8..c21f99b5 100644 --- a/src/ui/web/css/themes/element/theme.css +++ b/src/ui/web/css/themes/element/theme.css @@ -132,12 +132,17 @@ a.button-action { background-repeat: no-repeat; border: none; border-radius: 100%; + display: block; } .button-utility.grid { background-image: url('icons/enable-grid.svg'); } +.button-utility.settings { + background-image: url('icons/settings.svg'); +} + .button-utility.grid.on { background-image: url('icons/disable-grid.svg'); } @@ -541,3 +546,25 @@ ul.Timeline > li.messageStatus .message-container > p { .GapView > :not(:first-child) { margin-left: 12px; } + +.Settings { + flex: 1; +} + +.Settings .row .label { + font-weight: 600; +} + +.Settings .row.key .content { + font-family: monospace; +} + +.Settings .row { + margin: 12px; + display: flex; + flex-wrap: wrap; +} + +.Settings .row .label { + flex: 0 0 200px; +} diff --git a/src/ui/web/dom/ServiceWorkerHandler.js b/src/ui/web/dom/ServiceWorkerHandler.js index 432a6ce8..c3c6d4b0 100644 --- a/src/ui/web/dom/ServiceWorkerHandler.js +++ b/src/ui/web/dom/ServiceWorkerHandler.js @@ -152,6 +152,14 @@ export class ServiceWorkerHandler { this._registration.update(); } + get version() { + return window.HYDROGEN_VERSION; + } + + get buildHash() { + return window.HYDROGEN_GLOBAL_HASH; + } + async preventConcurrentSessionAccess(sessionId) { return this._sendAndWaitForReply("closeSession", {sessionId}); } diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js index 0be4e818..aded917b 100644 --- a/src/ui/web/session/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -38,6 +38,8 @@ export class SessionView extends TemplateView { return new RoomGridView(vm.roomGridViewModel); case "placeholder": return new StaticView(t => t.div({className: "room-placeholder"}, t.h2(vm.i18n`Choose a room on the left side.`))); + case "settings": + return new SettingsView(vm.settingsViewModel); default: //room id return new RoomView(vm.currentRoomViewModel); } @@ -46,3 +48,35 @@ export class SessionView extends TemplateView { ]); } } + +class SettingsView extends TemplateView { + render(t, vm) { + let version = vm.version; + if (vm.showUpdateButton) { + version = t.span([ + vm.version, + t.button({onClick: () => vm.checkForUpdate()}, vm.i18n`Check for updates`) + ]); + } + + const row = (label, content, extraClass = "") => { + return t.div({className: `row ${extraClass}`}, [ + t.div({className: "label"}, label), + t.div({className: "content"}, content), + ]); + }; + + return t.div({className: "Settings"}, [ + t.div({className: "header"}, [ + t.a({className: "button-utility close-room", href: vm.closeUrl, title: vm.i18n`Close room`}), + t.h2("Settings") + ]), + t.div([ + row(vm.i18n`User ID`, vm.userId), + row(vm.i18n`Session ID`, vm.deviceId), + row(vm.i18n`Session key`, vm.fingerprintKey, "key"), + row(vm.i18n`Version`, version), + ]) + ]); + } +} diff --git a/src/ui/web/session/leftpanel/LeftPanelView.js b/src/ui/web/session/leftpanel/LeftPanelView.js index 7c3d939b..a681b326 100644 --- a/src/ui/web/session/leftpanel/LeftPanelView.js +++ b/src/ui/web/session/leftpanel/LeftPanelView.js @@ -57,7 +57,7 @@ export class LeftPanelView extends TemplateView { vm.i18n`Enable grid layout`; }; const utilitiesRow = t.div({className: "utilities"}, [ - t.a({className: "button-utility close-session", href: vm.closeUrl}), + t.a({className: "button-utility close-session", href: vm.closeUrl, "aria-label": vm.i18n`Back to account list`, title: vm.i18n`Back to account list`}), t.view(new FilterField({ i18n: vm.i18n, label: vm.i18n`Filter rooms…`, @@ -75,7 +75,8 @@ export class LeftPanelView extends TemplateView { }, title: gridButtonLabel, "aria-label": gridButtonLabel - }) + }), + t.a({className: "button-utility settings", href: vm.settingsUrl, "aria-label": vm.i18n`Settings`, title: vm.i18n`Settings`}), ]); return t.div({className: "LeftPanel"}, [