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/layout.css b/src/ui/web/css/layout.css index aaf196b8..2ee584b0 100644 --- a/src/ui/web/css/layout.css +++ b/src/ui/web/css/layout.css @@ -51,17 +51,18 @@ html { width: 100vw; } +/* hide back button in middle section by default */ +.middle .close-middle { display: none; } /* mobile layout */ @media screen and (max-width: 800px) { /* show back button */ - .RoomHeader .close-room { display: block !important; } + .middle .close-middle { display: block !important; } /* hide grid button */ .LeftPanel .grid { display: none !important; } - div.RoomView, div.room-placeholder, div.RoomGridView { display: none; } + div.middle, div.room-placeholder { display: none; } div.LeftPanel {flex-grow: 1;} - div.room-shown div.RoomGridView { display: flex; } - div.room-shown div.RoomView { display: flex; } - div.room-shown div.LeftPanel { display: none; } + div.middle-shown div.middle { display: flex; } + div.middle-shown div.LeftPanel { display: none; } div.right-shown div.TimelinePanel { display: none; } } @@ -70,7 +71,7 @@ html { min-width: 0; } -.room-placeholder, .RoomView, .RoomGridView { +.room-placeholder, .middle { flex: 1 0 0; min-width: 0; } @@ -94,7 +95,7 @@ html { flex: 1 0 0; } -.RoomHeader { +.middle-header { display: flex; } diff --git a/src/ui/web/css/room.css b/src/ui/web/css/room.css index a5f8fe6d..23fab8c1 100644 --- a/src/ui/web/css/room.css +++ b/src/ui/web/css/room.css @@ -15,23 +15,19 @@ See the License for the specific language governing permissions and limitations under the License. */ -.RoomHeader { +.middle-header { align-items: center; } -.RoomHeader h2 { +.middle-header h2 { flex: 1; } -.RoomHeader button { +.middle-header button { display: block; } -.RoomHeader .close-room { - display: none; -} - -.RoomHeader .room-description { +.middle-header .room-description { flex: 1; min-width: 0; } @@ -47,7 +43,7 @@ limitations under the License. min-width: 0; } -.RoomHeader h2 { +.middle-header h2 { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; 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..203dbc9e 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'); } @@ -311,7 +316,7 @@ a { z-index: 2; } -.room-shown .SessionStatusView { +.middle-shown .SessionStatusView { top: 72px; } @@ -382,7 +387,7 @@ a { border-radius: 12px; } -.RoomHeader { +.middle-header { box-sizing: border-box; height: 58px; /* 12 + 36 + 12 to align with filter field + margin */ background: white; @@ -390,19 +395,19 @@ a { border-bottom: 1px solid rgba(245, 245, 245, 0.90); } -.RoomHeader h2 { +.middle-header h2 { font-size: 1.8rem; font-weight: 600; } -.RoomHeader > :not(:last-child) { +.middle-header > :not(:last-child) { /* use margin-right because the first item, - .close-room might be hidden and then we don't + .close-middle might be hidden and then we don't want a margin-left on the second item*/ margin-right: 8px; } -.close-room, .close-session { +.close-middle, .close-session { background-image: url('icons/chevron-left.svg'); background-position-x: 10px; } @@ -541,3 +546,40 @@ ul.Timeline > li.messageStatus .message-container > p { .GapView > :not(:first-child) { margin-left: 12px; } + +.SettingsBody { + padding: 12px 16px; +} + +.Settings .row .label { + font-weight: 600; +} + +.Settings .row .label, .Settings .row .content { + margin-top: 4px; + margin-bottom: 4px; +} + +.Settings .row .content { + margin-left: 4px; +} + +.Settings .row.code .content { + font-family: monospace; +} + +.Settings .row .content button { + display: inline-block; + margin: 0 8px; +} + +.Settings .row { + margin: 4px 0px; + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.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/RoomGridView.js b/src/ui/web/session/RoomGridView.js index 88e3e9ab..f2a73068 100644 --- a/src/ui/web/session/RoomGridView.js +++ b/src/ui/web/session/RoomGridView.js @@ -1,5 +1,5 @@ /* -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. @@ -42,6 +42,6 @@ export class RoomGridView extends TemplateView { }))); } children.push(t.div({className: vm => `focus-ring tile${vm.focusIndex}`})); - return t.div({className: "RoomGridView layout3x2"}, children); + return t.div({className: "RoomGridView middle layout3x2"}, children); } } diff --git a/src/ui/web/session/SessionView.js b/src/ui/web/session/SessionView.js index 0be4e818..6d3d7906 100644 --- a/src/ui/web/session/SessionView.js +++ b/src/ui/web/session/SessionView.js @@ -1,5 +1,6 @@ /* 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. @@ -20,13 +21,14 @@ 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"; export class SessionView extends TemplateView { render(t, vm) { return t.div({ className: { "SessionView": true, - "room-shown": vm => vm.activeSection !== "placeholder" + "middle-shown": vm => vm.activeSection !== "placeholder" }, }, [ t.view(new SessionStatusView(vm.sessionStatusViewModel)), @@ -38,6 +40,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); } diff --git a/src/ui/web/session/SettingsView.js b/src/ui/web/session/SettingsView.js new file mode 100644 index 00000000..73498530 --- /dev/null +++ b/src/ui/web/session/SettingsView.js @@ -0,0 +1,49 @@ +/* +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 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.main({className: "Settings middle"}, [ + t.div({className: "middle-header"}, [ + t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close settings`}), + t.h2("Settings") + ]), + t.div({className: "SettingsBody"}, [ + row(vm.i18n`User ID`, vm.userId), + row(vm.i18n`Session ID`, vm.deviceId, "code"), + row(vm.i18n`Session key`, vm.fingerprintKey, "code"), + 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"}, [ diff --git a/src/ui/web/session/room/RoomView.js b/src/ui/web/session/room/RoomView.js index 3fe3f9c3..65f464d9 100644 --- a/src/ui/web/session/room/RoomView.js +++ b/src/ui/web/session/room/RoomView.js @@ -23,10 +23,10 @@ import {renderAvatar} from "../../common.js"; export class RoomView extends TemplateView { render(t, vm) { - return t.div({className: "RoomView"}, [ + return t.main({className: "RoomView middle"}, [ t.div({className: "TimelinePanel"}, [ - t.div({className: "RoomHeader"}, [ - t.a({className: "button-utility close-room", href: vm.closeUrl, title: vm.i18n`Close room`}), + t.div({className: "RoomHeader middle-header"}, [ + t.a({className: "button-utility close-middle", href: vm.closeUrl, title: vm.i18n`Close room`}), renderAvatar(t, vm, 32), t.div({className: "room-description"}, [ t.h2(vm => vm.name),