Merge pull request #643 from vector-im/bwindels/separate-logout-view

Show logout in separate view so it's clear something is happening
This commit is contained in:
Bruno Windels 2022-01-17 16:40:49 +01:00 committed by GitHub
commit 454d2d3666
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 198 additions and 46 deletions

View file

@ -0,0 +1,63 @@
/*
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 {ViewModel} from "./ViewModel.js";
import {Client} from "../matrix/Client.js";
export class LogoutViewModel extends ViewModel {
constructor(options) {
super(options);
this._sessionId = options.sessionId;
this._busy = false;
this._showConfirm = true;
this._error = undefined;
}
get showConfirm() {
return this._showConfirm;
}
get busy() {
return this._busy;
}
get cancelUrl() {
return this.urlCreator.urlForSegment("session", true);
}
async logout() {
this._busy = true;
this._showConfirm = false;
this.emitChange("busy");
try {
const client = new Client(this.platform);
await client.startLogout(this._sessionId);
this.navigation.push("session", true);
} catch (err) {
this._error = err;
this._busy = false;
this.emitChange("busy");
}
}
get status() {
if (this._error) {
return this.i18n`Could not log out of device: ${this._error.message}`;
} else {
return this.i18n`Logging out… Please don't close the app.`;
}
}
}

View file

@ -18,6 +18,7 @@ import {Client} from "../matrix/Client.js";
import {SessionViewModel} from "./session/SessionViewModel.js";
import {SessionLoadViewModel} from "./SessionLoadViewModel.js";
import {LoginViewModel} from "./login/LoginViewModel.js";
import {LogoutViewModel} from "./LogoutViewModel.js";
import {SessionPickerViewModel} from "./SessionPickerViewModel.js";
import {ViewModel} from "./ViewModel.js";
@ -28,6 +29,7 @@ export class RootViewModel extends ViewModel {
this._sessionPickerViewModel = null;
this._sessionLoadViewModel = null;
this._loginViewModel = null;
this._logoutViewModel = null;
this._sessionViewModel = null;
this._pendingClient = null;
}
@ -40,13 +42,18 @@ export class RootViewModel extends ViewModel {
}
async _applyNavigation(shouldRestoreLastUrl) {
const isLogin = this.navigation.path.get("login")
const isLogin = this.navigation.path.get("login");
const logoutSessionId = this.navigation.path.get("logout")?.value;
const sessionId = this.navigation.path.get("session")?.value;
const loginToken = this.navigation.path.get("sso")?.value;
if (isLogin) {
if (this.activeSection !== "login") {
this._showLogin();
}
} else if (logoutSessionId) {
if (this.activeSection !== "logout") {
this._showLogout(logoutSessionId);
}
} else if (sessionId === true) {
if (this.activeSection !== "picker") {
this._showPicker();
@ -123,6 +130,12 @@ export class RootViewModel extends ViewModel {
});
}
_showLogout(sessionId) {
this._setSection(() => {
this._logoutViewModel = new LogoutViewModel(this.childOptions({sessionId}));
});
}
_showSession(client) {
this._setSection(() => {
this._sessionViewModel = new SessionViewModel(this.childOptions({client}));
@ -149,6 +162,8 @@ export class RootViewModel extends ViewModel {
return "session";
} else if (this._loginViewModel) {
return "login";
} else if (this._logoutViewModel) {
return "logout";
} else if (this._sessionPickerViewModel) {
return "picker";
} else if (this._sessionLoadViewModel) {
@ -164,12 +179,14 @@ export class RootViewModel extends ViewModel {
this._sessionPickerViewModel = this.disposeTracked(this._sessionPickerViewModel);
this._sessionLoadViewModel = this.disposeTracked(this._sessionLoadViewModel);
this._loginViewModel = this.disposeTracked(this._loginViewModel);
this._logoutViewModel = this.disposeTracked(this._logoutViewModel);
this._sessionViewModel = this.disposeTracked(this._sessionViewModel);
// now set it again
setter();
this._sessionPickerViewModel && this.track(this._sessionPickerViewModel);
this._sessionLoadViewModel && this.track(this._sessionLoadViewModel);
this._loginViewModel && this.track(this._loginViewModel);
this._logoutViewModel && this.track(this._logoutViewModel);
this._sessionViewModel && this.track(this._sessionViewModel);
this.emitChange("activeSection");
}
@ -177,6 +194,7 @@ export class RootViewModel extends ViewModel {
get error() { return this._error; }
get sessionViewModel() { return this._sessionViewModel; }
get loginViewModel() { return this._loginViewModel; }
get logoutViewModel() { return this._logoutViewModel; }
get sessionPickerViewModel() { return this._sessionPickerViewModel; }
get sessionLoadViewModel() { return this._sessionLoadViewModel; }
}

View file

@ -30,7 +30,7 @@ function allowsChild(parent, child) {
switch (parent?.type) {
case undefined:
// allowed root segments
return type === "login" || type === "session" || type === "sso";
return type === "login" || type === "session" || type === "sso" || type === "logout";
case "session":
return type === "room" || type === "rooms" || type === "settings";
case "rooms":

View file

@ -50,7 +50,6 @@ export class SettingsViewModel extends ViewModel {
this.minSentImageSizeLimit = 400;
this.maxSentImageSizeLimit = 4000;
this.pushNotifications = new PushNotificationStatus();
this._isLoggingOut = false;
}
get _session() {
@ -58,14 +57,9 @@ export class SettingsViewModel extends ViewModel {
}
async logout() {
this._isLoggingOut = true;
await this._client.logout();
this.emitChange("isLoggingOut");
this.navigation.push("session", true);
this.navigation.push("logout", this._client.sessionId);
}
get isLoggingOut() { return this._isLoggingOut; }
setSentImageSizeLimit(size) {
if (size > this.maxSentImageSizeLimit || size < this.minSentImageSizeLimit) {
this.sentImageSizeLimit = null;

View file

@ -386,10 +386,21 @@ export class Client {
return !this._reconnector;
}
logout() {
startLogout(sessionId) {
return this._platform.logger.run("logout", async log => {
this._sessionId = sessionId;
log.set("id", this._sessionId);
const sessionInfo = await this._platform.sessionInfoStorage.get(this._sessionId);
if (!sessionInfo) {
throw new Error(`Could not find session for id ${this._sessionId}`);
}
try {
await this._session?.logout(log);
const hsApi = new HomeServerApi({
homeserver: sessionInfo.homeServer,
accessToken: sessionInfo.accessToken,
request: this._platform.request
});
await hsApi.logout({log}).response();
} catch (err) {}
await this.deleteSession(log);
});

View file

@ -109,11 +109,6 @@ export class Session {
return this._sessionInfo.userId;
}
/** @internal call Client.logout instead */
async logout(log = undefined) {
await this._hsApi.logout({log}).response();
}
// called once this._e2eeAccount is assigned
_setupEncryption() {
// TODO: this should all go in a wrapper in e2ee/ that is bootstrapped by passing in the account

View file

@ -0,0 +1,53 @@
/*
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, InlineTemplateView} from "./general/TemplateView";
import {spinner} from "./common.js";
export class LogoutView extends TemplateView {
render(t, vm) {
const confirmView = new InlineTemplateView(vm, t => {
return t.div([
t.p("Are you sure you want to log out?"),
t.div({ className: "button-row" }, [
t.a({
className: "button-action",
type: "submit",
href: vm.cancelUrl,
}, ["Cancel"]),
t.button({
className: "button-action primary destructive",
type: "submit",
onClick: () => vm.logout(),
}, vm.i18n`Log out`)
]),
]);
});
const progressView = new InlineTemplateView(vm, t => {
return t.p({className: "status", hidden: vm => !vm.showStatus}, [
spinner(t, {hidden: vm => !vm.busy}), t.span(vm => vm.status)
]);
});
return t.div({className: "LogoutScreen"}, [
t.div({className: "content"}, [
t.mapView(vm => vm.showConfirm, showConfirm => {
return showConfirm ? confirmView : progressView;
})
]),
]);
}
}

View file

@ -15,7 +15,8 @@ limitations under the License.
*/
import {SessionView} from "./session/SessionView.js";
import {LoginView} from "./login/LoginView.js";
import {LoginView} from "./login/LoginView";
import {LogoutView} from "./LogoutView.js";
import {SessionLoadView} from "./login/SessionLoadView.js";
import {SessionPickerView} from "./login/SessionPickerView.js";
import {TemplateView} from "./general/TemplateView";
@ -36,6 +37,8 @@ export class RootView extends TemplateView {
return new SessionView(vm.sessionViewModel);
case "login":
return new LoginView(vm.loginViewModel);
case "logout":
return new LogoutView(vm.logoutViewModel);
case "picker":
return new SessionPickerView(vm.sessionPickerViewModel);
case "redirecting":

View file

@ -20,15 +20,16 @@ export function spinner(t, extraClasses = undefined) {
if (container === undefined) {
container = document.querySelector(".hydrogen");
}
const classes = Object.assign({"spinner": true}, extraClasses);
if (container?.classList.contains("legacy")) {
return t.div({className: "spinner"}, [
return t.div({className: classes}, [
t.div(),
t.div(),
t.div(),
t.div(),
]);
} else {
return t.svg({className: Object.assign({"spinner": true}, extraClasses), viewBox:"0 0 100 100"},
return t.svg({className: classes, viewBox:"0 0 100 100"},
t.circle({cx:"50%", cy:"50%", r:"45%", pathLength:"100"})
);
}

View file

@ -1058,3 +1058,20 @@ button.RoomDetailsView_row::after {
.LazyListParent {
overflow-y: auto;
}
.LogoutScreen {
height: 100vh;
}
.LogoutScreen .content {
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.LogoutScreen .status {
display: flex;
gap: 12px;
}

View file

@ -29,7 +29,6 @@ function objHasFns(obj: ClassNames<unknown>): obj is { [className: string]: bool
return false;
}
export type RenderFn<T> = (t: Builder<T>, vm: T) => ViewNode;
type EventHandler = ((event: Event) => void);
type AttributeStaticValue = string | boolean;
@ -52,20 +51,13 @@ export type Builder<T> = TemplateBuilder<T> & { [tagName in typeof TAG_NAMES[str
- add subviews inside the template
*/
// TODO: should we rename this to BoundView or something? As opposed to StaticView ...
export class TemplateView<T extends IObservableValue> extends BaseUpdateView<T> {
private _render?: RenderFn<T>;
export abstract class TemplateView<T extends IObservableValue> extends BaseUpdateView<T> {
private _eventListeners?: { node: Element, name: string, fn: EventHandler, useCapture: boolean }[] = undefined;
private _bindings?: (() => void)[] = undefined;
private _root?: ViewNode = undefined;
// public because used by TemplateBuilder
_subViews?: IView[] = undefined;
constructor(value: T, render?: RenderFn<T>) {
super(value);
// TODO: can avoid this if we have a separate class for inline templates vs class template views
this._render = render;
}
_attach(): void {
if (this._eventListeners) {
for (let {node, name, fn, useCapture} of this._eventListeners) {
@ -82,16 +74,12 @@ export class TemplateView<T extends IObservableValue> extends BaseUpdateView<T>
}
}
abstract render(t: Builder<T>, value: T): ViewNode;
mount(options?: IMountArgs): ViewNode {
const builder = new TemplateBuilder(this) as Builder<T>;
try {
if (this._render) {
this._root = this._render(builder, this._value);
} else if (this["render"]) { // overriden in subclass
this._root = this["render"](builder, this._value);
} else {
throw new Error("no render function passed in, or overriden in subclass");
}
this._root = this.render(builder, this._value);
} finally {
builder.close();
}
@ -344,7 +332,7 @@ export class TemplateBuilder<T extends IObservableValue> {
// on mappedValue, use `if` or `mapView`
map<R>(mapFn: (value: T) => R, renderFn: (mapped: R, t: Builder<T>, vm: T) => ViewNode): ViewNode {
return this.mapView(mapFn, mappedValue => {
return new TemplateView(this._value, (t, vm) => {
return new InlineTemplateView(this._value, (t, vm) => {
const rootNode = renderFn(mappedValue, t, vm);
if (!rootNode) {
// TODO: this will confuse mapView which assumes that
@ -366,7 +354,7 @@ export class TemplateBuilder<T extends IObservableValue> {
// creates a conditional subtemplate
// use mapView if you need to map to a different view class
if(predicate: (value: T) => boolean, renderFn: (t: Builder<T>, vm: T) => ViewNode) {
return this.ifView(predicate, vm => new TemplateView(vm, renderFn));
return this.ifView(predicate, vm => new InlineTemplateView(vm, renderFn));
}
/** You probably are looking for something else, like map or mapView.
@ -398,3 +386,16 @@ for (const [ns, tags] of Object.entries(TAG_NAMES)) {
};
}
}
export class InlineTemplateView<T> extends TemplateView<T> {
private _render: RenderFn<T>;
constructor(value: T, render: RenderFn<T>) {
super(value);
this._render = render;
}
override render(t: Builder<T>, value: T): ViewNode {
return this._render(t, value);
}
}

View file

@ -14,16 +14,16 @@ See the License for the specific language governing permissions and
limitations under the License.
*/
import {TemplateView} from "../../general/TemplateView";
import {TemplateView, InlineTemplateView} from "../../general/TemplateView";
import {StaticView} from "../../general/StaticView.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)
case "Enabled": return new InlineTemplateView(vm, renderEnabled)
case "SetupKey": return new InlineTemplateView(vm, renderEnableFromKey)
case "SetupPhrase": return new InlineTemplateView(vm, renderEnableFromPhrase)
case "Pending": return new StaticView(vm, t => t.p(vm.i18n`Waiting to go online…`))
}
});

View file

@ -42,11 +42,7 @@ export class SettingsView extends TemplateView {
row(t, vm.i18n`Session ID`, vm.deviceId, "code"),
row(t, vm.i18n`Session key`, vm.fingerprintKey, "code"),
row(t, "", t.button({
onClick: () => {
if (confirm(vm.i18n`Are you sure you want to log out?`)) {
vm.logout();
}
},
onClick: () => vm.logout(),
disabled: vm => vm.isLoggingOut
}, vm.i18n`Log out`)),
);