From a6dec86374a1d544a352deea86ca7e523ca0757e Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Sep 2024 12:12:04 +0100 Subject: [PATCH 01/13] Add mobile registration and dispatch event for mobile postmessage dance --- res/css/_components.pcss | 1 + .../structures/auth/_MobileRegistration.pcss | 10 +++ src/components/structures/MatrixChat.tsx | 20 +++++- .../structures/auth/Registration.tsx | 63 ++++++++++++++----- src/settings/Settings.tsx | 4 ++ 5 files changed, 79 insertions(+), 19 deletions(-) create mode 100644 res/css/structures/auth/_MobileRegistration.pcss diff --git a/res/css/_components.pcss b/res/css/_components.pcss index bfcab19879..dadd9aadb3 100644 --- a/res/css/_components.pcss +++ b/res/css/_components.pcss @@ -94,6 +94,7 @@ @import "./structures/auth/_ConfirmSessionLockTheftView.pcss"; @import "./structures/auth/_Login.pcss"; @import "./structures/auth/_LoginSplashView.pcss"; +@import "./structures/auth/_MobileRegistration.pcss"; @import "./structures/auth/_Registration.pcss"; @import "./structures/auth/_SessionLockStolenView.pcss"; @import "./structures/auth/_SetupEncryptionBody.pcss"; diff --git a/res/css/structures/auth/_MobileRegistration.pcss b/res/css/structures/auth/_MobileRegistration.pcss new file mode 100644 index 0000000000..d50ff8dc1f --- /dev/null +++ b/res/css/structures/auth/_MobileRegistration.pcss @@ -0,0 +1,10 @@ +/* +Copyright 2024 New Vector Ltd. + +SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only +Please see LICENSE files in the repository root for full details. +*/ + +.mx_MobileRegister_body { + padding: 32px; +} diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index cae0a549b7..a4119382a1 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -140,7 +140,7 @@ import { cleanUpDraftsIfRequired } from "../../DraftCleaner"; // legacy export export { default as Views } from "../../Views"; -const AUTH_SCREENS = ["register", "login", "forgot_password", "start_sso", "start_cas", "welcome"]; +const AUTH_SCREENS = ["register", "mobile_register", "login", "forgot_password", "start_sso", "start_cas", "welcome"]; // Actions that are redirected through the onboarding process prior to being // re-dispatched. NOTE: some actions are non-trivial and would require @@ -189,6 +189,7 @@ interface IState { register_session_id?: string; // eslint-disable-next-line camelcase register_id_sid?: string; + isMobileRegistration?: boolean; // When showing Modal dialogs we need to set aria-hidden on the root app element // and disable it when there are no dialogs hideToSRUsers: boolean; @@ -243,6 +244,7 @@ export default class MatrixChat extends React.PureComponent { currentUserId: null, hideToSRUsers: false, + isMobileRegistration: false, syncError: null, // If the current syncing status is ERROR, the error object, otherwise null. resizeNotifier: new ResizeNotifier(), @@ -650,6 +652,9 @@ export default class MatrixChat extends React.PureComponent { case "require_registration": startAnyRegistrationFlow(payload as any); break; + case "start_mobile_registration": + this.startRegistration(payload.params || {}, true); + break; case "start_registration": if (Lifecycle.isSoftLogout()) { this.onSoftLogout(); @@ -946,7 +951,7 @@ export default class MatrixChat extends React.PureComponent { }); } - private async startRegistration(params: { [key: string]: string }): Promise { + private async startRegistration(params: { [key: string]: string }, isMobileRegistration?: boolean): Promise { if (!SettingsStore.getValue(UIFeature.Registration)) { this.showScreen("welcome"); return; @@ -976,12 +981,15 @@ export default class MatrixChat extends React.PureComponent { newState.register_client_secret = params.client_secret; newState.register_session_id = params.session_id; newState.register_id_sid = params.sid; + newState.register_id_sid = params.sid; } + newState.isMobileRegistration = isMobileRegistration; //&& SettingsStore.getValue("Registration.mobileRegistrationHelper"); + this.setStateForNewView(newState); ThemeController.isLogin = true; this.themeWatcher.recheck(); - this.notifyNewScreen("register"); + this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); } // switch view to the given room @@ -1721,6 +1729,11 @@ export default class MatrixChat extends React.PureComponent { params: params, }); PerformanceMonitor.instance.start(PerformanceEntryNames.REGISTER); + } else if (screen === "mobile_register") { + dis.dispatch({ + action: "start_mobile_registration", + params: params, + }); } else if (screen === "login") { dis.dispatch({ action: "start_login", @@ -2080,6 +2093,7 @@ export default class MatrixChat extends React.PureComponent { onServerConfigChange={this.onServerConfigChange} defaultDeviceDisplayName={this.props.defaultDeviceDisplayName} fragmentAfterLogin={fragmentAfterLogin} + mobileRegister={this.state.isMobileRegistration} {...this.getServerProperties()} /> ); diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index d48bdfdcba..0d8c62604a 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -53,6 +53,13 @@ const debuglog = (...args: any[]): void => { } }; +export interface MobileRegistrationResponse { + user_id: string; + home_server: string; + access_token: string; + device_id: string; +} + interface IProps { serverConfig: ValidatedServerConfig; defaultDeviceDisplayName?: string; @@ -62,7 +69,7 @@ interface IProps { sessionId?: string; idSid?: string; fragmentAfterLogin?: string; - + mobileRegister?: boolean; // Called when the user has logged in. Params: // - object with userId, deviceId, homeserverUrl, identityServerUrl, accessToken // - The user's password, if available and applicable (may be cached in memory @@ -410,18 +417,33 @@ export default class Registration extends React.Component { debuglog("Registration: ui auth finished:", { hasEmail, hasAccessToken }); // don’t log in if we found a session for a different user if (hasAccessToken && !newState.differentLoggedInUserId) { - await this.props.onLoggedIn( - { - userId, - deviceId: (response as RegisterResponse).device_id!, - homeserverUrl: this.state.matrixClient.getHomeserverUrl(), - identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), - accessToken, - }, - this.state.formVals.password!, - ); + if (this.props.mobileRegister) { + const mobileResponse: MobileRegistrationResponse = { + user_id: userId, + home_server: this.state.matrixClient.getHomeserverUrl(), + access_token: accessToken, + device_id: (response as RegisterResponse).device_id!, + }; + const event = new CustomEvent("mobileregistrationresponse", { + detail: mobileResponse, + }); + document.dispatchEvent(event); + newState.busy = false; + newState.completedNoSignin = true; + } else { + await this.props.onLoggedIn( + { + userId, + deviceId: (response as RegisterResponse).device_id!, + homeserverUrl: this.state.matrixClient.getHomeserverUrl(), + identityServerUrl: this.state.matrixClient.getIdentityServerUrl(), + accessToken, + }, + this.state.formVals.password!, + ); - this.setupPushers(); + this.setupPushers(); + } } else { newState.busy = false; newState.completedNoSignin = true; @@ -558,7 +580,7 @@ export default class Registration extends React.Component { ); } else if (this.state.matrixClient && this.state.flows.length) { let ssoSection: JSX.Element | undefined; - if (this.state.ssoFlow) { + if (!this.props.mobileRegister && this.state.ssoFlow) { let continueWithSection; const providers = this.state.ssoFlow.identity_providers || []; // when there is only a single (or 0) providers we show a wide button with `Continue with X` text @@ -591,7 +613,6 @@ export default class Registration extends React.Component { ); } - return ( {ssoSection} @@ -660,7 +681,9 @@ export default class Registration extends React.Component { let body; if (this.state.completedNoSignin) { let regDoneText; - if (this.state.differentLoggedInUserId) { + if (this.props.mobileRegister) { + regDoneText = undefined; + } else if (this.state.differentLoggedInUserId) { regDoneText = (

@@ -717,6 +740,8 @@ export default class Registration extends React.Component { {regDoneText}

); + } else if (this.props.mobileRegister) { + body = this.renderRegisterComponent(); } else { body = ( @@ -746,7 +771,13 @@ export default class Registration extends React.Component { ); } - + if (this.props.mobileRegister) { + return ( + +
{body}
+
+ ); + } return ( diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a82fdef1ba..5733c6e353 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -876,6 +876,10 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevels: LEVELS_ACCOUNT_SETTINGS, default: null, }, + "Registration.mobileRegistrationHelper": { + supportedLevels: [SettingLevel.CONFIG], + default: false, + }, "autocompleteDelay": { supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG, default: 200, From 0244aaeb2f1a5485ea30b0d10c34b524a00bcb9e Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Sep 2024 12:18:32 +0100 Subject: [PATCH 02/13] use window.dispatchEvent --- src/components/structures/auth/Registration.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index 0d8c62604a..f2a04d00b4 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -427,7 +427,7 @@ export default class Registration extends React.Component { const event = new CustomEvent("mobileregistrationresponse", { detail: mobileResponse, }); - document.dispatchEvent(event); + window.dispatchEvent(event); newState.busy = false; newState.completedNoSignin = true; } else { From b5058286e0e57b8e23bf75eb883f749c21f0d7a2 Mon Sep 17 00:00:00 2001 From: Hubert Chathi Date: Fri, 13 Sep 2024 07:29:25 -0400 Subject: [PATCH 03/13] update test to work with newer Rust crypto (#32) --- playwright/e2e/crypto/event-shields.spec.ts | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 077d9126fa..3a85d06333 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -41,7 +41,12 @@ test.describe("Cryptography", function () { }); }); - test("should show the correct shield on e2e events", async ({ page, app, bot: bob, homeserver }) => { + test("should show the correct shield on e2e events", async ({ + page, + app, + bot: bob, + homeserver, + }, workerInfo) => { // Bob has a second, not cross-signed, device const bobSecondDevice = new Bot(page, homeserver, { bootstrapSecretStorage: false, @@ -117,7 +122,10 @@ test.describe("Cryptography", function () { await lastTileE2eIcon.focus(); await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); - /* Should show a grey padlock for a message from an unknown device */ + /* In legacy crypto: should show a grey padlock for a message from a deleted device. + * In rust crypto: should show a red padlock for a message from an unverified device. + * Rust crypto remembers the verification state of the sending device, so it will know that the device was + * unverified, even if it gets deleted. */ // bob deletes his second device await bobSecondDevice.evaluate((cli) => cli.logout(true)); @@ -148,7 +156,11 @@ test.describe("Cryptography", function () { await expect(last).toContainText("test encrypted from unverified"); await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); await lastE2eIcon.focus(); - await expect(page.getByRole("tooltip")).toContainText("Encrypted by an unknown or deleted device."); + await expect(page.getByRole("tooltip")).toContainText( + workerInfo.project.name === "Legacy Crypto" + ? "Encrypted by an unknown or deleted device." + : "Encrypted by a device not verified by its owner.", + ); }); test("Should show a grey padlock for a key restored from backup", async ({ From be59791db1605e136814d37d29dc9f6b68dcb440 Mon Sep 17 00:00:00 2001 From: Michael Telatynski <7t3chguy@gmail.com> Date: Fri, 13 Sep 2024 12:49:19 +0100 Subject: [PATCH 04/13] Add support for `org.matrix.cross_signing_reset` UIA stage flow (#34) * Soften UIA fallback postMessage check to work cross-origin Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Do the same for the SSO UIA flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add support for `org.matrix.cross_signing_reset` UIA stage flow Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Check against MessageEvent::source instead Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * i18n Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Add tests Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> * Remove protected method Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --------- Signed-off-by: Michael Telatynski <7t3chguy@gmail.com> --- src/components/structures/InteractiveAuth.tsx | 5 +- .../auth/InteractiveAuthEntryComponents.tsx | 59 +++++++++++++++++-- src/i18n/strings/en_EN.json | 2 + .../InteractiveAuthEntryComponents-test.tsx | 48 ++++++++++++++- ...teractiveAuthEntryComponents-test.tsx.snap | 50 ++++++++++++++++ 5 files changed, 155 insertions(+), 9 deletions(-) diff --git a/src/components/structures/InteractiveAuth.tsx b/src/components/structures/InteractiveAuth.tsx index 86cf6af665..91e52a1905 100644 --- a/src/components/structures/InteractiveAuth.tsx +++ b/src/components/structures/InteractiveAuth.tsx @@ -20,6 +20,7 @@ import { logger } from "matrix-js-sdk/src/logger"; import getEntryComponentForLoginType, { ContinueKind, + CustomAuthType, IStageComponent, } from "../views/auth/InteractiveAuthEntryComponents"; import Spinner from "../views/elements/Spinner"; @@ -75,11 +76,11 @@ export interface InteractiveAuthProps { // Called when the stage changes, or the stage's phase changes. First // argument is the stage, second is the phase. Some stages do not have // phases and will be counted as 0 (numeric). - onStagePhaseChange?(stage: AuthType | null, phase: number): void; + onStagePhaseChange?(stage: AuthType | CustomAuthType | null, phase: number): void; } interface IState { - authStage?: AuthType; + authStage?: CustomAuthType | AuthType; stageState?: IStageStatus; busy: boolean; errorText?: string; diff --git a/src/components/views/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index a0946564aa..7a15ee2095 100644 --- a/src/components/views/auth/InteractiveAuthEntryComponents.tsx +++ b/src/components/views/auth/InteractiveAuthEntryComponents.tsx @@ -11,6 +11,8 @@ import { MatrixClient } from "matrix-js-sdk/src/matrix"; import { AuthType, AuthDict, IInputs, IStageStatus } from "matrix-js-sdk/src/interactive-auth"; import { logger } from "matrix-js-sdk/src/logger"; import React, { ChangeEvent, createRef, FormEvent, Fragment } from "react"; +import { Button, Text } from "@vector-im/compound-web"; +import PopOutIcon from "@vector-im/compound-design-tokens/assets/web/icons/pop-out"; import EmailPromptIcon from "../../../../res/img/element-icons/email-prompt.svg"; import { _t } from "../../../languageHandler"; @@ -21,6 +23,7 @@ import AccessibleButton, { AccessibleButtonKind, ButtonEvent } from "../elements import Field from "../elements/Field"; import Spinner from "../elements/Spinner"; import CaptchaForm from "./CaptchaForm"; +import { Flex } from "../../utils/Flex"; /* This file contains a collection of components which are used by the * InteractiveAuth to prompt the user to enter the information needed @@ -905,11 +908,11 @@ export class SSOAuthEntry extends React.Component { - private popupWindow: Window | null; - private fallbackButton = createRef(); +export class FallbackAuthEntry extends React.Component { + protected popupWindow: Window | null; + protected fallbackButton = createRef(); - public constructor(props: IAuthEntryProps) { + public constructor(props: IAuthEntryProps & T) { super(props); // we have to make the user click a button, as browsers will block @@ -967,6 +970,50 @@ export class FallbackAuthEntry extends React.Component { } } +export enum CustomAuthType { + // Workaround for MAS requiring non-UIA authentication for resetting cross-signing. + MasCrossSigningReset = "org.matrix.cross_signing_reset", +} + +export class MasUnlockCrossSigningAuthEntry extends FallbackAuthEntry<{ + stageParams?: { + url?: string; + }; +}> { + public static LOGIN_TYPE = CustomAuthType.MasCrossSigningReset; + + private onGoToAccountClick = (): void => { + if (!this.props.stageParams?.url) return; + this.popupWindow = window.open(this.props.stageParams.url, "_blank"); + }; + + private onRetryClick = (): void => { + this.props.submitAuthDict({}); + }; + + public render(): React.ReactNode { + return ( +
+ {_t("auth|uia|mas_cross_signing_reset_description")} + + + + +
+ ); + } +} + export interface IStageComponentProps extends IAuthEntryProps { stageParams?: Record; inputs?: IInputs; @@ -983,8 +1030,10 @@ export interface IStageComponent extends React.ComponentClassResend it", "email_resent": "Resent!", "fallback_button": "Start authentication", + "mas_cross_signing_reset_cta": "Go to your account", + "mas_cross_signing_reset_description": "Reset your identity through your account provider and then come back and click “Retry”.", "msisdn": "A text message has been sent to %(msisdn)s", "msisdn_token_incorrect": "Token incorrect", "msisdn_token_prompt": "Please enter the code it contains:", diff --git a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx index 62c02b0d58..1cbf799af7 100644 --- a/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx +++ b/test/components/views/auth/InteractiveAuthEntryComponents-test.tsx @@ -7,11 +7,14 @@ */ import React from "react"; -import { render, screen, waitFor, act } from "@testing-library/react"; +import { render, screen, waitFor, act, fireEvent } from "@testing-library/react"; import { AuthType } from "matrix-js-sdk/src/interactive-auth"; import userEvent from "@testing-library/user-event"; -import { EmailIdentityAuthEntry } from "../../../../src/components/views/auth/InteractiveAuthEntryComponents"; +import { + EmailIdentityAuthEntry, + MasUnlockCrossSigningAuthEntry, +} from "../../../../src/components/views/auth/InteractiveAuthEntryComponents"; import { createTestClient } from "../../../test-utils"; describe("", () => { @@ -55,3 +58,44 @@ describe("", () => { await waitFor(() => expect(screen.queryByRole("button", { name: "Resend" })).toBeInTheDocument()); }); }); + +describe("", () => { + const renderAuth = (props = {}) => { + const matrixClient = createTestClient(); + + return render( + , + ); + }; + + test("should render", () => { + const { container } = renderAuth(); + expect(container).toMatchSnapshot(); + }); + + test("should open idp in new tab on click", async () => { + const spy = jest.spyOn(global.window, "open"); + renderAuth(); + + fireEvent.click(screen.getByRole("button", { name: "Go to your account" })); + expect(spy).toHaveBeenCalledWith("https://example.com", "_blank"); + }); + + test("should retry uia request on click", async () => { + const submitAuthDict = jest.fn(); + renderAuth({ submitAuthDict }); + + fireEvent.click(screen.getByRole("button", { name: "Retry" })); + expect(submitAuthDict).toHaveBeenCalledWith({}); + }); +}); diff --git a/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap b/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap index 65f86a35d2..16e5b3abc2 100644 --- a/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap +++ b/test/components/views/auth/__snapshots__/InteractiveAuthEntryComponents-test.tsx.snap @@ -32,3 +32,53 @@ exports[` should render 1`] = ` `; + +exports[` should render 1`] = ` +
+
+

+ Reset your identity through your account provider and then come back and click “Retry”. +

+
+ + +
+
+
+`; From 9426fec8c7bb2bd5f5045603a0016428e89f1ac1 Mon Sep 17 00:00:00 2001 From: David Baker Date: Fri, 13 Sep 2024 14:15:10 +0100 Subject: [PATCH 05/13] Fix timeout type (#40) In the user profile timezone code, which was failing a ts check. --- src/hooks/useUserTimezone.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/useUserTimezone.ts b/src/hooks/useUserTimezone.ts index 11198be1fa..686679bb90 100644 --- a/src/hooks/useUserTimezone.ts +++ b/src/hooks/useUserTimezone.ts @@ -29,7 +29,7 @@ import { MatrixClient, MatrixError } from "matrix-js-sdk/src/matrix"; */ export const useUserTimezone = (cli: MatrixClient, userId: string): { timezone: string; friendly: string } | null => { const [timezone, setTimezone] = useState(); - const [updateInterval, setUpdateInterval] = useState(); + const [updateInterval, setUpdateInterval] = useState>(); const [friendly, setFriendly] = useState(); const [supported, setSupported] = useState(); From a89f61a3578c770361ee21f86de177bf7ef266cf Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Sep 2024 14:48:48 +0100 Subject: [PATCH 06/13] Add error text and title with server name --- src/components/structures/auth/Registration.tsx | 15 +++++++++------ src/i18n/strings/en_EN.json | 1 + 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/components/structures/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index f2a04d00b4..2dc9125362 100644 --- a/src/components/structures/auth/Registration.tsx +++ b/src/components/structures/auth/Registration.tsx @@ -741,7 +741,14 @@ export default class Registration extends React.Component { ); } else if (this.props.mobileRegister) { - body = this.renderRegisterComponent(); + body = ( + +

{_t("auth|mobile_create_account_title", { hsName: this.props.serverConfig.hsName })}

+ {errorText} + {serverDeadSection} + {this.renderRegisterComponent()} +
+ ); } else { body = ( @@ -772,11 +779,7 @@ export default class Registration extends React.Component { ); } if (this.props.mobileRegister) { - return ( - -
{body}
-
- ); + return
{body}
; } return ( diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21addb3b98..2eacd48d87 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -229,6 +229,7 @@ }, "misconfigured_body": "Ask your %(brand)s admin to check your config for incorrect or duplicate entries.", "misconfigured_title": "Your %(brand)s is misconfigured", + "mobile_create_account_title": "You're about to create an account on %(hsName)s", "msisdn_field_description": "Other users can invite you to rooms using your contact details", "msisdn_field_label": "Phone", "msisdn_field_number_invalid": "That phone number doesn't look quite right, please check and try again", From 0716434aa1cac6bf5434bbac0f55e137e6060d21 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Sep 2024 16:59:11 +0100 Subject: [PATCH 07/13] Allow hs_url as param on mobile_register --- src/components/structures/MatrixChat.tsx | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index a4119382a1..0de3498003 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -961,9 +961,16 @@ export default class MatrixChat extends React.PureComponent { view: Views.REGISTER, }; - // Only honour params if they are all present, otherwise we reset - // HS and IS URLs when switching to registration. - if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) { + if (isMobileRegistration && params.hs_url) { + try { + const config = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(params.hs_url); + newState.serverConfig = config; + } catch (err) { + logger.warn("Failed to load hs_url param:", params.hs_url); + } + } else if (params.client_secret && params.session_id && params.hs_url && params.is_url && params.sid) { + // Only honour params if they are all present, otherwise we reset + // HS and IS URLs when switching to registration. newState.serverConfig = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls( params.hs_url, params.is_url, From 62d66f9d918c4314c9dde8fb10ad2c926b4e0e02 Mon Sep 17 00:00:00 2001 From: David Langley Date: Fri, 13 Sep 2024 16:59:50 +0100 Subject: [PATCH 08/13] Remove accidental paste --- src/components/structures/MatrixChat.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index 0de3498003..b54c1bd65f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -988,7 +988,6 @@ export default class MatrixChat extends React.PureComponent { newState.register_client_secret = params.client_secret; newState.register_session_id = params.session_id; newState.register_id_sid = params.sid; - newState.register_id_sid = params.sid; } newState.isMobileRegistration = isMobileRegistration; //&& SettingsStore.getValue("Registration.mobileRegistrationHelper"); From 20a4f0af995a45304450814ef9217761871bbe45 Mon Sep 17 00:00:00 2001 From: David Langley Date: Mon, 16 Sep 2024 15:16:24 +0100 Subject: [PATCH 09/13] Enforce config setting --- src/components/structures/MatrixChat.tsx | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/components/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index b54c1bd65f..8e0eaabe4f 100644 --- a/src/components/structures/MatrixChat.tsx +++ b/src/components/structures/MatrixChat.tsx @@ -956,12 +956,14 @@ export default class MatrixChat extends React.PureComponent { this.showScreen("welcome"); return; } + const isMobileRegistrationAllowed = + isMobileRegistration && SettingsStore.getValue("Registration.mobileRegistrationHelper"); const newState: Partial = { view: Views.REGISTER, }; - if (isMobileRegistration && params.hs_url) { + if (isMobileRegistrationAllowed && params.hs_url) { try { const config = await AutoDiscoveryUtils.validateServerConfigWithStaticUrls(params.hs_url); newState.serverConfig = config; @@ -990,12 +992,12 @@ export default class MatrixChat extends React.PureComponent { newState.register_id_sid = params.sid; } - newState.isMobileRegistration = isMobileRegistration; //&& SettingsStore.getValue("Registration.mobileRegistrationHelper"); + newState.isMobileRegistration = isMobileRegistrationAllowed; this.setStateForNewView(newState); ThemeController.isLogin = true; this.themeWatcher.recheck(); - this.notifyNewScreen(isMobileRegistration ? "mobile_register" : "register"); + this.notifyNewScreen(isMobileRegistrationAllowed ? "mobile_register" : "register"); } // switch view to the given room From 59852773ad41ea3be479ba9f6bfbdad1dd159d69 Mon Sep 17 00:00:00 2001 From: Florian Duros Date: Mon, 16 Sep 2024 16:51:59 +0200 Subject: [PATCH 10/13] Unlabs feature pinning (#22) --- src/TextForEvent.tsx | 1 - src/components/structures/RightPanel.tsx | 3 +- src/components/structures/RoomView.tsx | 10 +- .../structures/grouper/MainGrouper.tsx | 3 +- .../views/context_menus/RoomContextMenu.tsx | 6 +- .../right_panel/LegacyRoomHeaderButtons.tsx | 22 ++- .../views/right_panel/RoomSummaryCard.tsx | 25 ++-- .../tabs/room/RolesRoomSettingsTab.tsx | 4 +- src/i18n/strings/en_EN.json | 1 - src/settings/Settings.tsx | 8 -- src/utils/PinningUtils.ts | 2 - test/TextForEvent-test.ts | 5 - .../context_menus/MessageContextMenu-test.tsx | 16 --- .../right_panel/RoomSummaryCard-test.tsx | 12 +- .../RoomSummaryCard-test.tsx.snap | 132 ++++++++++++++++++ .../views/rooms/PinnedEventTile-test.tsx | 3 - test/submit-rageshake-test.ts | 4 +- test/utils/PinningUtils-test.ts | 8 -- 18 files changed, 166 insertions(+), 99 deletions(-) diff --git a/src/TextForEvent.tsx b/src/TextForEvent.tsx index 1ee7088945..1ffae62aea 100644 --- a/src/TextForEvent.tsx +++ b/src/TextForEvent.tsx @@ -561,7 +561,6 @@ const onPinnedMessagesClick = (): void => { }; function textForPinnedEvent(event: MatrixEvent, client: MatrixClient, allowJSX: boolean): (() => Renderable) | null { - if (!SettingsStore.getValue("feature_pinning")) return null; const senderName = getSenderName(event); const roomId = event.getRoomId()!; diff --git a/src/components/structures/RightPanel.tsx b/src/components/structures/RightPanel.tsx index 1cfc57e4fb..d4014f6aa0 100644 --- a/src/components/structures/RightPanel.tsx +++ b/src/components/structures/RightPanel.tsx @@ -17,7 +17,6 @@ import RightPanelStore from "../../stores/right-panel/RightPanelStore"; import MatrixClientContext from "../../contexts/MatrixClientContext"; import RoomSummaryCard from "../views/right_panel/RoomSummaryCard"; import WidgetCard from "../views/right_panel/WidgetCard"; -import SettingsStore from "../../settings/SettingsStore"; import MemberList from "../views/rooms/MemberList"; import UserInfo from "../views/right_panel/UserInfo"; import ThirdPartyMemberInfo from "../views/rooms/ThirdPartyMemberInfo"; @@ -220,7 +219,7 @@ export default class RightPanel extends React.Component { break; case RightPanelPhases.PinnedMessages: - if (!!this.props.room && SettingsStore.getValue("feature_pinning")) { + if (!!this.props.room) { card = ( { ); - const isPinningEnabled = SettingsStore.getValue("feature_pinning"); - let pinnedMessageBanner; - if (isPinningEnabled) { - pinnedMessageBanner = ( - - ); - } + const pinnedMessageBanner = ( + + ); let messageComposer; const showComposer = diff --git a/src/components/structures/grouper/MainGrouper.tsx b/src/components/structures/grouper/MainGrouper.tsx index 2a7baab3a2..72fd3fcdf0 100644 --- a/src/components/structures/grouper/MainGrouper.tsx +++ b/src/components/structures/grouper/MainGrouper.tsx @@ -18,7 +18,6 @@ import DateSeparator from "../../views/messages/DateSeparator"; import HistoryTile from "../../views/rooms/HistoryTile"; import EventListSummary from "../../views/elements/EventListSummary"; import { SeparatorKind } from "../../views/messages/TimelineSeparator"; -import SettingsStore from "../../../settings/SettingsStore"; const groupedStateEvents = [ EventType.RoomMember, @@ -91,7 +90,7 @@ export class MainGrouper extends BaseGrouper { return; } - if (ev.getType() === EventType.RoomPinnedEvents && !SettingsStore.getValue("feature_pinning")) { + if (ev.getType() === EventType.RoomPinnedEvents) { // If pinned messages are disabled, don't show the summary return; } diff --git a/src/components/views/context_menus/RoomContextMenu.tsx b/src/components/views/context_menus/RoomContextMenu.tsx index bc10e88074..694aba0ef5 100644 --- a/src/components/views/context_menus/RoomContextMenu.tsx +++ b/src/components/views/context_menus/RoomContextMenu.tsx @@ -26,7 +26,6 @@ import { EchoChamber } from "../../../stores/local-echo/EchoChamber"; import { RoomNotifState } from "../../../RoomNotifs"; import Modal from "../../../Modal"; import ExportDialog from "../dialogs/ExportDialog"; -import { useFeatureEnabled } from "../../../hooks/useSettings"; import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePhases"; import { RoomSettingsTab } from "../dialogs/RoomSettingsDialog"; import { useEventEmitterState } from "../../../hooks/useEventEmitter"; @@ -261,11 +260,10 @@ const RoomContextMenu: React.FC = ({ room, onFinished, ...props }) => { ); } - const pinningEnabled = useFeatureEnabled("feature_pinning"); - const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length; + const pinCount = usePinnedEvents(room).length; let pinsOption: JSX.Element | undefined; - if (pinningEnabled && !isVideoRoom) { + if (!isVideoRoom) { pinsOption = ( { diff --git a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx index 1a5d0d1dcc..85b173c40c 100644 --- a/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx +++ b/src/components/views/right_panel/LegacyRoomHeaderButtons.tsx @@ -21,7 +21,6 @@ import { RightPanelPhases } from "../../../stores/right-panel/RightPanelStorePha import { ActionPayload } from "../../../dispatcher/payloads"; import RightPanelStore from "../../../stores/right-panel/RightPanelStore"; import { showThreadPanel } from "../../../dispatcher/dispatch-actions/threads"; -import SettingsStore from "../../../settings/SettingsStore"; import { RoomNotificationStateStore, UPDATE_STATUS_INDICATOR, @@ -245,17 +244,16 @@ export default class LegacyRoomHeaderButtons extends HeaderButtons { const rightPanelPhaseButtons: Map = new Map(); - if (SettingsStore.getValue("feature_pinning")) { - rightPanelPhaseButtons.set( - RightPanelPhases.PinnedMessages, - , - ); - } + rightPanelPhaseButtons.set( + RightPanelPhases.PinnedMessages, + , + ); + rightPanelPhaseButtons.set( RightPanelPhases.Timeline, = ({ ); - const pinningEnabled = useFeatureEnabled("feature_pinning"); - const pinCount = usePinnedEvents(pinningEnabled ? room : undefined)?.length; + const pinCount = usePinnedEvents(room).length; const roomTags = useEventEmitterState(RoomListStore.instance, LISTS_UPDATE_EVENT, () => RoomListStore.instance.getTagsForRoom(room), @@ -382,17 +380,16 @@ const RoomSummaryCard: React.FC = ({ {!isVideoRoom && ( <> - {pinningEnabled && ( - - - {pinCount} - - - )} + + + {pinCount} + + + )} diff --git a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx index eebd0b194f..ec8a2b8718 100644 --- a/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx +++ b/src/components/views/settings/tabs/room/RolesRoomSettingsTab.tsx @@ -267,15 +267,13 @@ export default class RolesRoomSettingsTab extends React.Component { [EventType.RoomServerAcl]: _td("room_settings|permissions|m.room.server_acl"), [EventType.Reaction]: _td("room_settings|permissions|m.reaction"), [EventType.RoomRedaction]: _td("room_settings|permissions|m.room.redaction"), + [EventType.RoomPinnedEvents]: _td("room_settings|permissions|m.room.pinned_events"), // TODO: Enable support for m.widget event type (https://github.com/vector-im/element-web/issues/13111) "im.vector.modular.widgets": isSpaceRoom ? null : _td("room_settings|permissions|m.widget"), [VoiceBroadcastInfoEventType]: _td("room_settings|permissions|io.element.voice_broadcast_info"), }; - if (SettingsStore.getValue("feature_pinning")) { - plEventsToLabels[EventType.RoomPinnedEvents] = _td("room_settings|permissions|m.room.pinned_events"); - } // MSC3401: Native Group VoIP signaling if (SettingsStore.getValue("feature_group_calls")) { plEventsToLabels[ElementCall.CALL_EVENT_TYPE.name] = _td("room_settings|permissions|m.call"); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 8580b6c13b..cc051ac0cf 100644 --- a/src/i18n/strings/en_EN.json +++ b/src/i18n/strings/en_EN.json @@ -1468,7 +1468,6 @@ "notifications": "Enable the notifications panel in the room header", "oidc_native_flow": "OIDC native authentication", "oidc_native_flow_description": "⚠ WARNING: Experimental. Use OIDC native authentication when supported by the server.", - "pinning": "Message Pinning", "release_announcement": "Release announcement", "render_reaction_images": "Render custom images in reactions", "render_reaction_images_description": "Sometimes referred to as \"custom emojis\".", diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index 5733c6e353..2fadb53dde 100644 --- a/src/settings/Settings.tsx +++ b/src/settings/Settings.tsx @@ -275,14 +275,6 @@ export const SETTINGS: { [setting: string]: ISetting } = { supportedLevelsAreOrdered: true, default: false, }, - "feature_pinning": { - isFeature: true, - labsGroup: LabGroup.Messaging, - displayName: _td("labs|pinning"), - supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG_PRIORITISED, - supportedLevelsAreOrdered: true, - default: true, - }, "feature_wysiwyg_composer": { isFeature: true, labsGroup: LabGroup.Messaging, diff --git a/src/utils/PinningUtils.ts b/src/utils/PinningUtils.ts index dfc71b134a..806e59b014 100644 --- a/src/utils/PinningUtils.ts +++ b/src/utils/PinningUtils.ts @@ -9,7 +9,6 @@ Please see LICENSE files in the repository root for full details. import { MatrixEvent, EventType, M_POLL_START, MatrixClient, EventTimeline, Room } from "matrix-js-sdk/src/matrix"; import { isContentActionable } from "./EventUtils"; -import SettingsStore from "../settings/SettingsStore"; import { ReadPinsEventId } from "../components/views/right_panel/types"; export default class PinningUtils { @@ -70,7 +69,6 @@ export default class PinningUtils { * @private */ private static canPinOrUnpin(matrixClient: MatrixClient, mxEvent: MatrixEvent): boolean { - if (!SettingsStore.getValue("feature_pinning")) return false; if (!isContentActionable(mxEvent)) return false; const room = matrixClient.getRoom(mxEvent.getRoomId()); diff --git a/test/TextForEvent-test.ts b/test/TextForEvent-test.ts index e709e66cff..66f4345a8d 100644 --- a/test/TextForEvent-test.ts +++ b/test/TextForEvent-test.ts @@ -65,11 +65,6 @@ describe("TextForEvent", () => { }); describe("TextForPinnedEvent", () => { - beforeAll(() => { - // enable feature_pinning setting - (SettingsStore.getValue as jest.Mock).mockImplementation((feature) => feature === "feature_pinning"); - }); - it("mentions message when a single message was pinned, with no previously pinned messages", () => { const event = mockPinnedEvent(["message-1"]); const plainText = textForEvent(event, mockClient); diff --git a/test/components/views/context_menus/MessageContextMenu-test.tsx b/test/components/views/context_menus/MessageContextMenu-test.tsx index fe0d5037b9..b2763c1085 100644 --- a/test/components/views/context_menus/MessageContextMenu-test.tsx +++ b/test/components/views/context_menus/MessageContextMenu-test.tsx @@ -116,22 +116,6 @@ describe("MessageContextMenu", () => { expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy(); }); - it("does not show pin option when pinning feature is disabled", () => { - const eventContent = createMessageEventContent("hello"); - const pinnableEvent = new MatrixEvent({ - type: EventType.RoomMessage, - content: eventContent, - room_id: roomId, - }); - - // disable pinning feature - jest.spyOn(SettingsStore, "getValue").mockReturnValue(false); - - createMenu(pinnableEvent, { rightClick: true }, {}, undefined, room); - - expect(screen.queryByRole("menuitem", { name: "Pin" })).toBeFalsy(); - }); - it("shows pin option when pinning feature is enabled", () => { const eventContent = createMessageEventContent("hello"); const pinnableEvent = new MatrixEvent({ diff --git a/test/components/views/right_panel/RoomSummaryCard-test.tsx b/test/components/views/right_panel/RoomSummaryCard-test.tsx index 9a5b12c717..2e230c6352 100644 --- a/test/components/views/right_panel/RoomSummaryCard-test.tsx +++ b/test/components/views/right_panel/RoomSummaryCard-test.tsx @@ -259,8 +259,7 @@ describe("", () => { }); describe("pinning", () => { - it("renders pins options when pinning feature is enabled", () => { - mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_pinning"); + it("renders pins options", () => { const { getByText } = getComponent(); expect(getByText("Pinned messages")).toBeInTheDocument(); @@ -291,9 +290,7 @@ describe("", () => { describe("video rooms", () => { it("does not render irrelevant options for element video room", () => { jest.spyOn(room, "isElementVideoRoom").mockReturnValue(true); - mocked(settingsHooks.useFeatureEnabled).mockImplementation( - (feature) => feature === "feature_video_rooms" || feature === "feature_pinning", - ); + mocked(settingsHooks.useFeatureEnabled).mockImplementation((feature) => feature === "feature_video_rooms"); const { queryByText } = getComponent(); // options not rendered @@ -305,10 +302,7 @@ describe("", () => { it("does not render irrelevant options for element call room", () => { jest.spyOn(room, "isCallRoom").mockReturnValue(true); mocked(settingsHooks.useFeatureEnabled).mockImplementation( - (feature) => - feature === "feature_element_call_video_rooms" || - feature === "feature_video_rooms" || - feature === "feature_pinning", + (feature) => feature === "feature_element_call_video_rooms" || feature === "feature_video_rooms", ); const { queryByText } = getComponent(); diff --git a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap index f04669e877..26a7b88eda 100644 --- a/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap +++ b/test/components/views/right_panel/__snapshots__/RoomSummaryCard-test.tsx.snap @@ -186,6 +186,50 @@ exports[` has button to edit topic 1`] = ` data-orientation="horizontal" role="separator" /> + + + + Pinned messages + + + + 0 + + + + + + Pinned messages + + + + 0 + + + + + + Pinned messages + + + + 0 + + +