From a294ba2ad4f5cc2f848bdd63245105211189e6e0 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Tue, 4 Jul 2023 14:49:27 +0100 Subject: [PATCH] Conform more of the codebase to strictNullChecks + noImplicitAny (#11179) --- src/AddThreepid.ts | 4 +-- src/Lifecycle.ts | 8 ++--- src/Login.ts | 4 +-- src/MatrixClientPeg.ts | 32 +---------------- src/components/structures/InteractiveAuth.tsx | 17 +++++----- .../structures/auth/Registration.tsx | 27 +++++++-------- .../views/dialogs/DeactivateAccountDialog.tsx | 6 +++- .../views/dialogs/InteractiveAuthDialog.tsx | 7 ++-- src/components/views/dialogs/InviteDialog.tsx | 2 +- src/components/views/rooms/RoomPreviewBar.tsx | 5 ++- .../views/settings/devices/deleteDevices.tsx | 2 +- src/i18n/strings/en_EN.json | 1 + .../components/structures/MatrixChat-test.tsx | 6 +++- .../components/structures/auth/Login-test.tsx | 6 +++- .../dialogs/InteractiveAuthDialog-test.tsx | 3 +- .../views/rooms/RoomPreviewBar-test.tsx | 28 ++++++++------- .../RoomPreviewBar-test.tsx.snap | 34 +++++++++++++++++-- 17 files changed, 104 insertions(+), 88 deletions(-) diff --git a/src/AddThreepid.ts b/src/AddThreepid.ts index 9be733824c..d8cfb62777 100644 --- a/src/AddThreepid.ts +++ b/src/AddThreepid.ts @@ -236,7 +236,7 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog, { + const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { title: _t("Add Email Address"), matrixClient: this.matrixClient, authData: err.data, @@ -357,7 +357,7 @@ export default class AddThreepid { continueKind: "primary", }, }; - const { finished } = Modal.createDialog(InteractiveAuthDialog, { + const { finished } = Modal.createDialog(InteractiveAuthDialog<{}>, { title: _t("Add Phone Number"), matrixClient: this.matrixClient, authData: err.data, diff --git a/src/Lifecycle.ts b/src/Lifecycle.ts index 2931e6c5ef..5fb627ec70 100644 --- a/src/Lifecycle.ts +++ b/src/Lifecycle.ts @@ -327,7 +327,7 @@ function registerAsGuest(hsUrl: string, isUrl?: string, defaultDeviceDisplayName { userId: creds.user_id, deviceId: creds.device_id, - accessToken: creds.access_token, + accessToken: creds.access_token!, homeserverUrl: hsUrl, identityServerUrl: isUrl, guest: true, @@ -920,10 +920,8 @@ async function clearStorage(opts?: { deleteEverything?: boolean }): Promise { - const roomId = i.roomId; - delete i.roomId; // delete to avoid confusing the store - ThreepidInviteStore.instance.storeInvite(roomId, i); + pendingInvites.forEach(({ roomId, ...invite }) => { + ThreepidInviteStore.instance.storeInvite(roomId, invite); }); if (registrationTime) { diff --git a/src/Login.ts b/src/Login.ts index 27d3690ee9..42a5062210 100644 --- a/src/Login.ts +++ b/src/Login.ts @@ -19,7 +19,7 @@ limitations under the License. import { createClient } from "matrix-js-sdk/src/matrix"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; -import { DELEGATED_OIDC_COMPATIBILITY, ILoginFlow, ILoginParams, LoginFlow } from "matrix-js-sdk/src/@types/auth"; +import { DELEGATED_OIDC_COMPATIBILITY, ILoginFlow, LoginFlow, LoginRequest } from "matrix-js-sdk/src/@types/auth"; import { IMatrixClientCreds } from "./MatrixClientPeg"; import SecurityCustomisations from "./customisations/Security"; @@ -238,7 +238,7 @@ export async function sendLoginRequest( hsUrl: string, isUrl: string | undefined, loginType: string, - loginParams: ILoginParams, + loginParams: Omit, ): Promise { const client = createClient({ baseUrl: hsUrl, diff --git a/src/MatrixClientPeg.ts b/src/MatrixClientPeg.ts index 21820f443a..d45482e020 100644 --- a/src/MatrixClientPeg.ts +++ b/src/MatrixClientPeg.ts @@ -49,7 +49,7 @@ export interface IMatrixClientCreds { identityServerUrl?: string; userId: string; deviceId?: string; - accessToken?: string; + accessToken: string; guest?: boolean; pickleKey?: string; freshLogin?: boolean; @@ -79,8 +79,6 @@ export interface IMatrixClientPeg { assign(): Promise; start(): Promise; - getCredentials(): IMatrixClientCreds; - /** * If we've registered a user ID we set this to the ID of the * user we've just registered. If they then go & log in, we @@ -138,10 +136,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { private matrixClient: MatrixClient | null = null; private justRegisteredUserId: string | null = null; - // the credentials used to init the current client object. - // used if we tear it down & recreate it with a different store - private currentClientCreds: IMatrixClientCreds | null = null; - public get(): MatrixClient | null { return this.matrixClient; } @@ -195,7 +189,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { } public replaceUsingCreds(creds: IMatrixClientCreds): void { - this.currentClientCreds = creds; this.createClient(creds); } @@ -335,29 +328,6 @@ class MatrixClientPegClass implements IMatrixClientPeg { logger.log(`MatrixClientPeg: MatrixClient started`); } - public getCredentials(): IMatrixClientCreds { - if (!this.matrixClient) { - throw new Error("createClient must be called first"); - } - - let copiedCredentials: IMatrixClientCreds | null = this.currentClientCreds; - if (this.currentClientCreds?.userId !== this.matrixClient?.credentials?.userId) { - // cached credentials belong to a different user - don't use them - copiedCredentials = null; - } - return { - // Copy the cached credentials before overriding what we can. - ...(copiedCredentials ?? {}), - - homeserverUrl: this.matrixClient.baseUrl, - identityServerUrl: this.matrixClient.idBaseUrl, - userId: this.matrixClient.getSafeUserId(), - deviceId: this.matrixClient.getDeviceId() ?? undefined, - accessToken: this.matrixClient.getAccessToken() ?? undefined, - guest: this.matrixClient.isGuest(), - }; - } - public getHomeserverName(): string { const matches = /^@[^:]+:(.+)$/.exec(this.safeGet().getSafeUserId()); if (matches === null || matches.length < 1) { diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 46b8de426a..5c9d53ce92 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -25,20 +25,19 @@ import { } from "matrix-js-sdk/src/interactive-auth"; import { MatrixClient } from "matrix-js-sdk/src/client"; import { logger } from "matrix-js-sdk/src/logger"; -import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import getEntryComponentForLoginType, { IStageComponent } from "../views/auth/InteractiveAuthEntryComponents"; import Spinner from "../views/elements/Spinner"; export const ERROR_USER_CANCELLED = new Error("User cancelled auth session"); -type InteractiveAuthCallbackSuccess = ( +type InteractiveAuthCallbackSuccess = ( success: true, - response?: IAuthData, + response: T, extra?: { emailSid?: string; clientSecret?: string }, ) => void; type InteractiveAuthCallbackFailure = (success: false, response: IAuthData | Error) => void; -export type InteractiveAuthCallback = InteractiveAuthCallbackSuccess & InteractiveAuthCallbackFailure; +export type InteractiveAuthCallback = InteractiveAuthCallbackSuccess & InteractiveAuthCallbackFailure; export interface InteractiveAuthProps { // matrix client to use for UI auth requests @@ -62,7 +61,7 @@ export interface InteractiveAuthProps { continueText?: string; continueKind?: string; // callback - makeRequest(auth: IAuthDict | null): Promise>; + makeRequest(auth: IAuthDict | null): Promise; // callback called when the auth process has finished, // successfully or unsuccessfully. // @param {boolean} status True if the operation requiring @@ -75,7 +74,7 @@ export interface InteractiveAuthProps { // the auth session. // * clientSecret {string} The client secret used in auth // sessions with the ID server. - onAuthFinished: InteractiveAuthCallback; + onAuthFinished: InteractiveAuthCallback; // As js-sdk interactive-auth requestEmailToken?(email: string, secret: string, attempt: number, session: string): Promise<{ sid: string }>; // Called when the stage changes, or the stage's phase changes. First @@ -94,7 +93,7 @@ interface IState { } export default class InteractiveAuthComponent extends React.Component, IState> { - private readonly authLogic: InteractiveAuth; + private readonly authLogic: InteractiveAuth; private readonly intervalId: number | null = null; private readonly stageComponent = createRef(); @@ -108,7 +107,7 @@ export default class InteractiveAuthComponent extends React.Component({ authData: this.props.authData, doRequest: this.requestCallback, busyChanged: this.onBusyChanged, @@ -211,7 +210,7 @@ export default class InteractiveAuthComponent extends React.Component> => { + private requestCallback = (auth: IAuthDict | null, background: boolean): Promise => { // This wrapper just exists because the js-sdk passes a second // 'busy' param for backwards compat. This throws the tests off // so discard it here. diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 7526930152..5492b4f039 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -14,12 +14,13 @@ See the License for the specific language governing permissions and limitations under the License. */ -import { AuthType, createClient, IAuthDict, IAuthData, IInputs, MatrixError } from "matrix-js-sdk/src/matrix"; +import { AuthType, createClient, IAuthData, IAuthDict, IInputs, MatrixError } from "matrix-js-sdk/src/matrix"; import React, { Fragment, ReactNode } from "react"; import { IRegisterRequestParams, IRequestTokenResponse, MatrixClient } from "matrix-js-sdk/src/client"; import classNames from "classnames"; import { logger } from "matrix-js-sdk/src/logger"; import { ISSOFlow, SSOAction } from "matrix-js-sdk/src/@types/auth"; +import { RegisterResponse } from "matrix-js-sdk/src/@types/registration"; import { _t } from "../../../languageHandler"; import { adminContactStrings, messageForResourceLimitError, resourceLimitStrings } from "../../../utils/ErrorUtils"; @@ -305,7 +306,7 @@ export default class Registration extends React.Component { ); }; - private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { + private onUIAuthFinished: InteractiveAuthCallback = async (success, response): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); debuglog("Registration: ui authentication finished: ", { success, response }); @@ -329,8 +330,8 @@ export default class Registration extends React.Component {

{errorDetail}

); - } else if ((response as IAuthData).required_stages?.includes(AuthType.Msisdn)) { - const flows = (response as IAuthData).available_flows ?? []; + } else if ((response as IAuthData).flows?.some((flow) => flow.stages.includes(AuthType.Msisdn))) { + const flows = (response as IAuthData).flows ?? []; const msisdnAvailable = flows.some((flow) => flow.stages.includes(AuthType.Msisdn)); if (!msisdnAvailable) { errorText = _t("This server does not support authentication with a phone number."); @@ -349,15 +350,15 @@ export default class Registration extends React.Component { return; } - const userId = (response as IAuthData).user_id; - const accessToken = (response as IAuthData).access_token; + const userId = (response as RegisterResponse).user_id; + const accessToken = (response as RegisterResponse).access_token; if (!userId || !accessToken) throw new Error("Registration failed"); MatrixClientPeg.setJustRegisteredUserId(userId); const newState: Partial = { doingUIAuth: false, - registeredUsername: (response as IAuthData).user_id, + registeredUsername: userId, differentLoggedInUserId: undefined, completedNoSignin: false, // we're still busy until we get unmounted: don't show the registration form again @@ -370,10 +371,8 @@ export default class Registration extends React.Component { // starting the registration process. This isn't perfect since it's possible // the user had a separate guest session they didn't actually mean to replace. const [sessionOwner, sessionIsGuest] = await Lifecycle.getStoredSessionOwner(); - if (sessionOwner && !sessionIsGuest && sessionOwner !== (response as IAuthData).user_id) { - logger.log( - `Found a session for ${sessionOwner} but ${(response as IAuthData).user_id} has just registered.`, - ); + if (sessionOwner && !sessionIsGuest && sessionOwner !== userId) { + logger.log(`Found a session for ${sessionOwner} but ${userId} has just registered.`); newState.differentLoggedInUserId = sessionOwner; } @@ -390,7 +389,7 @@ export default class Registration extends React.Component { // as the client that started registration may be gone by the time we've verified the email, and only the client // that verified the email is guaranteed to exist, we'll always do the login in that client. const hasEmail = Boolean(this.state.formVals.email); - const hasAccessToken = Boolean((response as IAuthData).access_token); + const hasAccessToken = Boolean(accessToken); debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user if (!hasEmail && hasAccessToken && !newState.differentLoggedInUserId) { @@ -399,7 +398,7 @@ export default class Registration extends React.Component { await this.props.onLoggedIn( { userId, - deviceId: (response as IAuthData).device_id, + deviceId: (response as RegisterResponse).device_id!, homeserverUrl: this.state.matrixClient.getHomeserverUrl(), identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), accessToken, @@ -461,7 +460,7 @@ export default class Registration extends React.Component { }); }; - private makeRegisterRequest = (auth: IAuthDict | null): Promise => { + private makeRegisterRequest = (auth: IAuthDict | null): Promise => { if (!this.state.matrixClient) throw new Error("Matrix client has not yet been loaded"); const registerParams: IRegisterRequestParams = { diff --git a/src/components/views/dialogs/DeactivateAccountDialog.tsx b/src/components/views/dialogs/DeactivateAccountDialog.tsx index 0c3a9c8565..d59f42109d 100644 --- a/src/components/views/dialogs/DeactivateAccountDialog.tsx +++ b/src/components/views/dialogs/DeactivateAccountDialog.tsx @@ -18,6 +18,7 @@ limitations under the License. import React from "react"; import { AuthType, IAuthData } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; +import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { MatrixClientPeg } from "../../../MatrixClientPeg"; import { _t } from "../../../languageHandler"; @@ -109,7 +110,10 @@ export default class DeactivateAccountDialog extends React.Component { + private onUIAuthFinished: InteractiveAuthCallback>> = ( + success, + result, + ) => { if (success) return; // great! makeRequest() will be called too. if (result === ERROR_USER_CANCELLED) { diff --git a/src/components/views/dialogs/InteractiveAuthDialog.tsx b/src/components/views/dialogs/InteractiveAuthDialog.tsx index 393808ddf8..2ba3db0fb6 100644 --- a/src/components/views/dialogs/InteractiveAuthDialog.tsx +++ b/src/components/views/dialogs/InteractiveAuthDialog.tsx @@ -18,7 +18,8 @@ limitations under the License. import React from "react"; import { MatrixClient } from "matrix-js-sdk/src/client"; -import { AuthType, IAuthData } from "matrix-js-sdk/src/interactive-auth"; +import { AuthType } from "matrix-js-sdk/src/interactive-auth"; +import { UIAResponse } from "matrix-js-sdk/src/@types/uia"; import { _t } from "../../../languageHandler"; import AccessibleButton from "../elements/AccessibleButton"; @@ -70,7 +71,7 @@ export interface InteractiveAuthDialogProps // Default is defined in _getDefaultDialogAesthetics() aestheticsForStagePhases?: DialogAesthetics; - onFinished(success?: boolean, result?: IAuthData | Error | null): void; + onFinished(success?: boolean, result?: UIAResponse | Error | null): void; } interface IState { @@ -116,7 +117,7 @@ export default class InteractiveAuthDialog extends React.Component { + private onAuthFinished: InteractiveAuthCallback = (success, result): void => { if (success) { this.props.onFinished(true, result); } else { diff --git a/src/components/views/dialogs/InviteDialog.tsx b/src/components/views/dialogs/InviteDialog.tsx index 68a31a8bdb..7bb714c025 100644 --- a/src/components/views/dialogs/InviteDialog.tsx +++ b/src/components/views/dialogs/InviteDialog.tsx @@ -760,7 +760,7 @@ export default class InviteDialog extends React.PureComponent { this.props.invitedEmail, identityAccessToken!, ); + if (!("mxid" in result)) { + throw new UserFriendlyError("Unable to find user by email"); + } this.setState({ invitedEmailMxid: result.mxid }); } catch (err) { this.setState({ threePidFetchError: err as MatrixError }); diff --git a/src/components/views/settings/devices/deleteDevices.tsx b/src/components/views/settings/devices/deleteDevices.tsx index 4b7af3119e..3fa042864a 100644 --- a/src/components/views/settings/devices/deleteDevices.tsx +++ b/src/components/views/settings/devices/deleteDevices.tsx @@ -32,7 +32,7 @@ const makeDeleteRequest = export const deleteDevicesWithInteractiveAuth = async ( matrixClient: MatrixClient, deviceIds: string[], - onFinished: InteractiveAuthCallback, + onFinished: InteractiveAuthCallback, ): Promise => { if (!deviceIds.length) { return; diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index e73860a030..0fa5a5abd4 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -2076,6 +2076,7 @@ "Currently removing messages in %(count)s rooms|one": "Currently removing messages in %(count)s room", "%(spaceName)s menu": "%(spaceName)s menu", "Home options": "Home options", + "Unable to find user by email": "Unable to find user by email", "Joining space…": "Joining space…", "Joining room…": "Joining room…", "Joining…": "Joining…", diff --git a/test/components/structures/MatrixChat-test.tsx b/test/components/structures/MatrixChat-test.tsx index ecc8aec778..07dbba6ab6 100644 --- a/test/components/structures/MatrixChat-test.tsx +++ b/test/components/structures/MatrixChat-test.tsx @@ -418,7 +418,11 @@ describe("", () => { // this is used to create a temporary client during login jest.spyOn(MatrixJs, "createClient").mockReturnValue(loginClient); - loginClient.login.mockClear().mockResolvedValue({}); + loginClient.login.mockClear().mockResolvedValue({ + access_token: "TOKEN", + device_id: "IMADEVICE", + user_id: userId, + }); loginClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] }); loginClient.getProfileInfo.mockResolvedValue({ diff --git a/test/components/structures/auth/Login-test.tsx b/test/components/structures/auth/Login-test.tsx index dbb5bf0f90..148829eb2a 100644 --- a/test/components/structures/auth/Login-test.tsx +++ b/test/components/structures/auth/Login-test.tsx @@ -54,7 +54,11 @@ describe("Login", function () { disable_custom_urls: true, oidc_static_client_ids: oidcStaticClientsConfig, }); - mockClient.login.mockClear().mockResolvedValue({}); + mockClient.login.mockClear().mockResolvedValue({ + access_token: "TOKEN", + device_id: "IAMADEVICE", + user_id: "@user:server", + }); mockClient.loginFlows.mockClear().mockResolvedValue({ flows: [{ type: "m.login.password" }] }); mocked(createClient).mockImplementation((opts) => { mockClient.idBaseUrl = opts.idBaseUrl; diff --git a/test/components/views/dialogs/InteractiveAuthDialog-test.tsx b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx index 4482a2a3cd..1fce8783e8 100644 --- a/test/components/views/dialogs/InteractiveAuthDialog-test.tsx +++ b/test/components/views/dialogs/InteractiveAuthDialog-test.tsx @@ -19,6 +19,7 @@ import React from "react"; import { fireEvent, render, screen, act } from "@testing-library/react"; import userEvent from "@testing-library/user-event"; import { mocked } from "jest-mock"; +import { MatrixError } from "matrix-js-sdk/src/matrix"; import InteractiveAuthDialog from "../../../../src/components/views/dialogs/InteractiveAuthDialog"; import { clearAllModals, flushPromises, getMockClientWithEventEmitter, unmockClientPeg } from "../../../test-utils"; @@ -130,7 +131,7 @@ describe("InteractiveAuthDialog", function () { const successfulResult = { test: 1 }; const makeRequest = jest .fn() - .mockRejectedValueOnce({ httpStatus: 401, data: { flows: [{ stages: ["m.login.sso"] }] } }) + .mockRejectedValueOnce(new MatrixError({ data: { flows: [{ stages: ["m.login.sso"] }] } }, 401)) .mockResolvedValue(successfulResult); mockClient.credentials = { userId: "@user:id" }; diff --git a/test/components/views/rooms/RoomPreviewBar-test.tsx b/test/components/views/rooms/RoomPreviewBar-test.tsx index cc418bcb0a..d0132f7e9e 100644 --- a/test/components/views/rooms/RoomPreviewBar-test.tsx +++ b/test/components/views/rooms/RoomPreviewBar-test.tsx @@ -332,16 +332,18 @@ describe("", () => { { medium: "not-email", address: "address 2" }, ]; - const testJoinButton = (props: ComponentProps) => async () => { - const onJoinClick = jest.fn(); - const onRejectClick = jest.fn(); - const component = getComponent({ ...props, onJoinClick, onRejectClick }); - await new Promise(setImmediate); - expect(getPrimaryActionButton(component)).toBeTruthy(); - expect(getSecondaryActionButton(component)).toBeFalsy(); - fireEvent.click(getPrimaryActionButton(component)!); - expect(onJoinClick).toHaveBeenCalled(); - }; + const testJoinButton = + (props: ComponentProps, expectSecondaryButton = false) => + async () => { + const onJoinClick = jest.fn(); + const onRejectClick = jest.fn(); + const component = getComponent({ ...props, onJoinClick, onRejectClick }); + await new Promise(setImmediate); + expect(getPrimaryActionButton(component)).toBeTruthy(); + if (expectSecondaryButton) expect(getSecondaryActionButton(component)).toBeFalsy(); + fireEvent.click(getPrimaryActionButton(component)!); + expect(onJoinClick).toHaveBeenCalled(); + }; describe("when client fails to get 3PIDs", () => { beforeEach(() => { @@ -399,7 +401,7 @@ describe("", () => { }); it("renders email mismatch message when invite email mxid doesnt match", async () => { - MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue("not userid"); + MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue({ mxid: "not userid" }); const component = getComponent({ inviterName, invitedEmail }); await new Promise(setImmediate); @@ -413,12 +415,12 @@ describe("", () => { }); it("renders invite message when invite email mxid match", async () => { - MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue(userId); + MatrixClientPeg.safeGet().lookupThreePid = jest.fn().mockReturnValue({ mxid: userId }); const component = getComponent({ inviterName, invitedEmail }); await new Promise(setImmediate); expect(getMessage(component)).toMatchSnapshot(); - await testJoinButton({ inviterName, invitedEmail })(); + await testJoinButton({ inviterName, invitedEmail }, false)(); }); }); }); diff --git a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap index 80b5c89432..7c82a4040d 100644 --- a/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap +++ b/test/components/views/rooms/__snapshots__/RoomPreviewBar-test.tsx.snap @@ -116,10 +116,40 @@ exports[` with an invite with an invited email when client has class="mx_RoomPreviewBar_message" >

- This invite to RoomPreviewBar-test-room was sent to test@test.com + Do you want to join RoomPreviewBar-test-room?

- Share this email in Settings to receive invites directly in Element. + + + + +

+

+ + + @inviter:test.com + + invited you +

`;