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:
commit
454d2d3666
13 changed files with 198 additions and 46 deletions
63
src/domain/LogoutViewModel.js
Normal file
63
src/domain/LogoutViewModel.js
Normal 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.`;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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; }
|
||||
}
|
||||
|
|
|
@ -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":
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
|
|
|
@ -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
|
||||
|
|
53
src/platform/web/ui/LogoutView.js
Normal file
53
src/platform/web/ui/LogoutView.js
Normal 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;
|
||||
})
|
||||
]),
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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":
|
||||
|
|
|
@ -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"})
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…`))
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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`)),
|
||||
);
|
||||
|
|
Reference in a new issue