diff --git a/.ts-eslintrc.js b/.ts-eslintrc.js index 1974e07b..cf1fc3bf 100644 --- a/.ts-eslintrc.js +++ b/.ts-eslintrc.js @@ -19,6 +19,7 @@ module.exports = { ], rules: { "@typescript-eslint/no-floating-promises": 2, - "@typescript-eslint/no-misused-promises": 2 + "@typescript-eslint/no-misused-promises": 2, + "semi": ["error", "always"] } }; diff --git a/src/domain/LogoutViewModel.ts b/src/domain/LogoutViewModel.ts index 3edfcad5..b0409edd 100644 --- a/src/domain/LogoutViewModel.ts +++ b/src/domain/LogoutViewModel.ts @@ -14,18 +14,18 @@ See the License for the specific language governing permissions and limitations under the License. */ -import {Options, ViewModel} from "./ViewModel"; +import {Options as BaseOptions, ViewModel} from "./ViewModel"; import {Client} from "../matrix/Client.js"; -type LogoutOptions = { sessionId: string; } & Options; +type Options = { sessionId: string; } & BaseOptions; -export class LogoutViewModel extends ViewModel { +export class LogoutViewModel extends ViewModel { private _sessionId: string; private _busy: boolean; private _showConfirm: boolean; private _error?: Error; - constructor(options: LogoutOptions) { + constructor(options: Options) { super(options); this._sessionId = options.sessionId; this._busy = false; diff --git a/src/domain/RootViewModel.js b/src/domain/RootViewModel.js index 2711cd2f..4094d864 100644 --- a/src/domain/RootViewModel.js +++ b/src/domain/RootViewModel.js @@ -17,7 +17,7 @@ limitations under the License. import {Client} from "../matrix/Client.js"; import {SessionViewModel} from "./session/SessionViewModel.js"; import {SessionLoadViewModel} from "./SessionLoadViewModel.js"; -import {LoginViewModel} from "./login/LoginViewModel.js"; +import {LoginViewModel} from "./login/LoginViewModel"; import {LogoutViewModel} from "./LogoutViewModel"; import {SessionPickerViewModel} from "./SessionPickerViewModel.js"; import {ViewModel} from "./ViewModel"; @@ -118,7 +118,7 @@ export class RootViewModel extends ViewModel { // but we also want the change of screen to go through the navigation // so we store the session container in a temporary variable that will be // consumed by _applyNavigation, triggered by the navigation change - // + // // Also, we should not call _setSection before the navigation is in the correct state, // as url creation (e.g. in RoomTileViewModel) // won't be using the correct navigation base path. diff --git a/src/domain/ViewModel.ts b/src/domain/ViewModel.ts index 0bc52f6e..00ae847b 100644 --- a/src/domain/ViewModel.ts +++ b/src/domain/ViewModel.ts @@ -58,11 +58,11 @@ export class ViewModel extends EventEmitter<{change return this._options[name]; } - observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void) { + observeNavigation(type: string, onChange: (value: string | true | undefined, type: string) => void): void { const segmentObservable = this.navigation.observe(type); const unsubscribe = segmentObservable.subscribe((value: string | true | undefined) => { onChange(value, type); - }) + }); this.track(unsubscribe); } @@ -100,10 +100,10 @@ export class ViewModel extends EventEmitter<{change // TODO: this will need to support binding // if any of the expr is a function, assume the function is a binding, and return a binding function ourselves - // + // // translated string should probably always be bindings, unless we're fine with a refresh when changing the language? // we probably are, if we're using routing with a url, we could just refresh. - i18n(parts: TemplateStringsArray, ...expr: any[]) { + i18n(parts: TemplateStringsArray, ...expr: any[]): string { // just concat for now let result = ""; for (let i = 0; i < parts.length; ++i) { diff --git a/src/domain/login/LoginViewModel.js b/src/domain/login/LoginViewModel.ts similarity index 66% rename from src/domain/login/LoginViewModel.js rename to src/domain/login/LoginViewModel.ts index bf77e624..aaeca54f 100644 --- a/src/domain/login/LoginViewModel.js +++ b/src/domain/login/LoginViewModel.ts @@ -15,101 +15,143 @@ limitations under the License. */ import {Client} from "../../matrix/Client.js"; -import {ViewModel} from "../ViewModel"; +import {Options as BaseOptions, ViewModel} from "../ViewModel"; import {PasswordLoginViewModel} from "./PasswordLoginViewModel.js"; import {StartSSOLoginViewModel} from "./StartSSOLoginViewModel.js"; import {CompleteSSOLoginViewModel} from "./CompleteSSOLoginViewModel.js"; import {LoadStatus} from "../../matrix/Client.js"; import {SessionLoadViewModel} from "../SessionLoadViewModel.js"; +import type {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod} from "../../matrix/login"; -export class LoginViewModel extends ViewModel { - constructor(options) { +type Options = { + defaultHomeserver: string; + ready: ReadyFn; + loginToken?: string; +} & BaseOptions; + +export class LoginViewModel extends ViewModel { + private _ready: ReadyFn; + private _loginToken?: string; + private _client: Client; + private _loginOptions?: LoginOptions; + private _passwordLoginViewModel?: PasswordLoginViewModel; + private _startSSOLoginViewModel?: StartSSOLoginViewModel; + private _completeSSOLoginViewModel?: CompleteSSOLoginViewModel; + private _loadViewModel?: SessionLoadViewModel; + private _loadViewModelSubscription?: () => void; + private _homeserver: string; + private _queriedHomeserver?: string; + private _abortHomeserverQueryTimeout?: () => void; + private _abortQueryOperation?: () => void; + + private _hideHomeserver: boolean = false; + private _isBusy: boolean = false; + private _errorMessage: string = ""; + + constructor(options: Readonly) { super(options); const {ready, defaultHomeserver, loginToken} = options; this._ready = ready; this._loginToken = loginToken; this._client = new Client(this.platform); - this._loginOptions = null; - this._passwordLoginViewModel = null; - this._startSSOLoginViewModel = null; - this._completeSSOLoginViewModel = null; - this._loadViewModel = null; - this._loadViewModelSubscription = null; this._homeserver = defaultHomeserver; - this._queriedHomeserver = null; - this._errorMessage = ""; - this._hideHomeserver = false; - this._isBusy = false; - this._abortHomeserverQueryTimeout = null; - this._abortQueryOperation = null; this._initViewModels(); } - get passwordLoginViewModel() { return this._passwordLoginViewModel; } - get startSSOLoginViewModel() { return this._startSSOLoginViewModel; } - get completeSSOLoginViewModel(){ return this._completeSSOLoginViewModel; } - get homeserver() { return this._homeserver; } - get resolvedHomeserver() { return this._loginOptions?.homeserver; } - get errorMessage() { return this._errorMessage; } - get showHomeserver() { return !this._hideHomeserver; } - get loadViewModel() {return this._loadViewModel; } - get isBusy() { return this._isBusy; } - get isFetchingLoginOptions() { return !!this._abortQueryOperation; } + get passwordLoginViewModel(): PasswordLoginViewModel { + return this._passwordLoginViewModel; + } - goBack() { + get startSSOLoginViewModel(): StartSSOLoginViewModel { + return this._startSSOLoginViewModel; + } + + get completeSSOLoginViewModel(): CompleteSSOLoginViewModel { + return this._completeSSOLoginViewModel; + } + + get homeserver(): string { + return this._homeserver; + } + + get resolvedHomeserver(): string | undefined { + return this._loginOptions?.homeserver; + } + + get errorMessage(): string { + return this._errorMessage; + } + + get showHomeserver(): boolean { + return !this._hideHomeserver; + } + + get loadViewModel(): SessionLoadViewModel { + return this._loadViewModel; + } + + get isBusy(): boolean { + return this._isBusy; + } + + get isFetchingLoginOptions(): boolean { + return !!this._abortQueryOperation; + } + + goBack(): void { this.navigation.push("session"); } - async _initViewModels() { + private _initViewModels(): void { if (this._loginToken) { this._hideHomeserver = true; this._completeSSOLoginViewModel = this.track(new CompleteSSOLoginViewModel( this.childOptions( { client: this._client, - attemptLogin: loginMethod => this.attemptLogin(loginMethod), + attemptLogin: (loginMethod: TokenLoginMethod) => this.attemptLogin(loginMethod), loginToken: this._loginToken }))); this.emitChange("completeSSOLoginViewModel"); } else { - await this.queryHomeserver(); + void this.queryHomeserver(); } } - _showPasswordLogin() { + private _showPasswordLogin(): void { this._passwordLoginViewModel = this.track(new PasswordLoginViewModel( this.childOptions({ loginOptions: this._loginOptions, - attemptLogin: loginMethod => this.attemptLogin(loginMethod) + attemptLogin: (loginMethod: PasswordLoginMethod) => this.attemptLogin(loginMethod) }))); this.emitChange("passwordLoginViewModel"); } - _showSSOLogin() { + private _showSSOLogin(): void { this._startSSOLoginViewModel = this.track( new StartSSOLoginViewModel(this.childOptions({loginOptions: this._loginOptions})) ); this.emitChange("startSSOLoginViewModel"); } - _showError(message) { + private _showError(message: string): void { this._errorMessage = message; this.emitChange("errorMessage"); } - _setBusy(status) { + private _setBusy(status: boolean): void { this._isBusy = status; this._passwordLoginViewModel?.setBusy(status); this._startSSOLoginViewModel?.setBusy(status); this.emitChange("isBusy"); } - async attemptLogin(loginMethod) { + async attemptLogin(loginMethod: ILoginMethod): Promise { this._setBusy(true); - this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); + void this._client.startWithLogin(loginMethod, {inspectAccountSetup: true}); const loadStatus = this._client.loadStatus; - const handle = loadStatus.waitFor(status => status !== LoadStatus.Login); + const handle = loadStatus.waitFor((status: LoadStatus) => status !== LoadStatus.Login); await handle.promise; this._setBusy(false); const status = loadStatus.get(); @@ -119,11 +161,11 @@ export class LoginViewModel extends ViewModel { this._hideHomeserver = true; this.emitChange("hideHomeserver"); this._disposeViewModels(); - this._createLoadViewModel(); + void this._createLoadViewModel(); return null; } - _createLoadViewModel() { + private _createLoadViewModel(): void { this._loadViewModelSubscription = this.disposeTracked(this._loadViewModelSubscription); this._loadViewModel = this.disposeTracked(this._loadViewModel); this._loadViewModel = this.track( @@ -139,7 +181,7 @@ export class LoginViewModel extends ViewModel { }) ) ); - this._loadViewModel.start(); + void this._loadViewModel.start(); this.emitChange("loadViewModel"); this._loadViewModelSubscription = this.track( this._loadViewModel.disposableOn("change", () => { @@ -151,22 +193,22 @@ export class LoginViewModel extends ViewModel { ); } - _disposeViewModels() { - this._startSSOLoginViewModel = this.disposeTracked(this._ssoLoginViewModel); + private _disposeViewModels(): void { + this._startSSOLoginViewModel = this.disposeTracked(this._startSSOLoginViewModel); this._passwordLoginViewModel = this.disposeTracked(this._passwordLoginViewModel); this._completeSSOLoginViewModel = this.disposeTracked(this._completeSSOLoginViewModel); this.emitChange("disposeViewModels"); } - async setHomeserver(newHomeserver) { + async setHomeserver(newHomeserver: string): Promise { this._homeserver = newHomeserver; // clear everything set by queryHomeserver - this._loginOptions = null; - this._queriedHomeserver = null; + this._loginOptions = undefined; + this._queriedHomeserver = undefined; this._showError(""); this._disposeViewModels(); this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); - this.emitChange(); // multiple fields changing + this.emitChange("loginViewModels"); // multiple fields changing // also clear the timeout if it is still running this.disposeTracked(this._abortHomeserverQueryTimeout); const timeout = this.clock.createTimeout(1000); @@ -181,10 +223,10 @@ export class LoginViewModel extends ViewModel { } } this._abortHomeserverQueryTimeout = this.disposeTracked(this._abortHomeserverQueryTimeout); - this.queryHomeserver(); + void this.queryHomeserver(); } - - async queryHomeserver() { + + async queryHomeserver(): Promise { // don't repeat a query we've just done if (this._homeserver === this._queriedHomeserver || this._homeserver === "") { return; @@ -210,7 +252,7 @@ export class LoginViewModel extends ViewModel { if (e.name === "AbortError") { return; //aborted, bail out } else { - this._loginOptions = null; + this._loginOptions = undefined; } } finally { this._abortQueryOperation = this.disposeTracked(this._abortQueryOperation); @@ -221,19 +263,29 @@ export class LoginViewModel extends ViewModel { if (this._loginOptions.password) { this._showPasswordLogin(); } if (!this._loginOptions.sso && !this._loginOptions.password) { this._showError("This homeserver supports neither SSO nor password based login flows"); - } + } } else { this._showError(`Could not query login methods supported by ${this.homeserver}`); } } - dispose() { + dispose(): void { super.dispose(); if (this._client) { // if we move away before we're done with initial sync // delete the session - this._client.deleteSession(); + void this._client.deleteSession(); } } } + +type ReadyFn = (client: Client) => void; + +// TODO: move to Client.js when its converted to typescript. +type LoginOptions = { + homeserver: string; + password?: (username: string, password: string) => PasswordLoginMethod; + sso?: SSOLoginHelper; + token?: (loginToken: string) => TokenLoginMethod; +}; diff --git a/src/matrix/Client.js b/src/matrix/Client.js index 21175a7f..44643cc1 100644 --- a/src/matrix/Client.js +++ b/src/matrix/Client.js @@ -100,6 +100,8 @@ export class Client { }); } + // TODO: When converted to typescript this should return the same type + // as this._loginOptions is in LoginViewModel.ts (LoginOptions). _parseLoginOptions(options, homeserver) { /* Take server response and return new object which has two props password and sso which @@ -136,7 +138,7 @@ export class Client { const request = this._platform.request; const hsApi = new HomeServerApi({homeserver, request}); const registration = new Registration(hsApi, { - username, + username, password, initialDeviceDisplayName, }, @@ -196,7 +198,7 @@ export class Client { sessionInfo.deviceId = dehydratedDevice.deviceId; } } - await this._platform.sessionInfoStorage.add(sessionInfo); + await this._platform.sessionInfoStorage.add(sessionInfo); // loading the session can only lead to // LoadStatus.Error in case of an error, // so separate try/catch @@ -266,7 +268,7 @@ export class Client { this._status.set(LoadStatus.SessionSetup); await log.wrap("createIdentity", log => this._session.createIdentity(log)); } - + this._sync = new Sync({hsApi: this._requestScheduler.hsApi, storage: this._storage, session: this._session, logger: this._platform.logger}); // notify sync and session when back online this._reconnectSubscription = this._reconnector.connectionStatus.subscribe(state => { @@ -311,7 +313,7 @@ export class Client { this._waitForFirstSyncHandle = this._sync.status.waitFor(s => { if (s === SyncStatus.Stopped) { // keep waiting if there is a ConnectionError - // as the reconnector above will call + // as the reconnector above will call // sync.start again to retry in this case return this._sync.error?.name !== "ConnectionError"; } diff --git a/src/matrix/login/index.ts b/src/matrix/login/index.ts new file mode 100644 index 00000000..ba133a26 --- /dev/null +++ b/src/matrix/login/index.ts @@ -0,0 +1,7 @@ +import {ILoginMethod} from "./LoginMethod"; +import {PasswordLoginMethod} from "./PasswordLoginMethod"; +import {SSOLoginHelper} from "./SSOLoginHelper"; +import {TokenLoginMethod} from "./TokenLoginMethod"; + + +export {PasswordLoginMethod, SSOLoginHelper, TokenLoginMethod, ILoginMethod}; \ No newline at end of file