diff --git a/playwright/e2e/crypto/event-shields.spec.ts b/playwright/e2e/crypto/event-shields.spec.ts index 077d9126fa..98142089fb 100644 --- a/playwright/e2e/crypto/event-shields.spec.ts +++ b/playwright/e2e/crypto/event-shields.spec.ts @@ -6,9 +6,12 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only Please see LICENSE files in the repository root for full details. */ +import { Page } from "@playwright/test"; + import { expect, test } from "../../element-web-test"; import { autoJoin, createSharedRoomWithUser, enableKeyBackup, logIntoElement, logOutOfElement, verify } from "./utils"; import { Bot } from "../../pages/bot"; +import { HomeserverInstance } from "../../plugins/homeserver"; test.describe("Cryptography", function () { test.use({ @@ -41,16 +44,14 @@ 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, - bootstrapCrossSigning: false, - }); - bobSecondDevice.setCredentials( - await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), - ); - await bobSecondDevice.prepareClient(); + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); await bob.sendEvent(testRoomId, null, "m.room.encrypted", { algorithm: "m.megolm.v1.aes-sha2", @@ -117,7 +118,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 +152,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 ({ @@ -204,14 +212,7 @@ test.describe("Cryptography", function () { test("should show the correct shield on edited e2e events", async ({ page, app, bot: bob, homeserver }) => { // bob has a second, not cross-signed, device - const bobSecondDevice = new Bot(page, homeserver, { - bootstrapSecretStorage: false, - bootstrapCrossSigning: false, - }); - bobSecondDevice.setCredentials( - await homeserver.loginUser(bob.credentials.userId, bob.credentials.password), - ); - await bobSecondDevice.prepareClient(); + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); // verify Bob await verify(app, bob); @@ -257,5 +258,51 @@ test.describe("Cryptography", function () { page.locator(".mx_EventTile", { hasText: "Hee!" }).locator(".mx_EventTile_e2eIcon_warning"), ).not.toBeVisible(); }); + + test("should show correct shields on events sent by devices which have since been deleted", async ({ + page, + app, + bot: bob, + homeserver, + }) => { + // Our app is blocked from syncing while Bob sends his messages. + await app.client.network.goOffline(); + + // Bob sends a message from his verified device + await bob.sendMessage(testRoomId, "test encrypted from verified"); + + // And one from a second, not cross-signed, device + const bobSecondDevice = await createSecondBotDevice(page, homeserver, bob); + await bobSecondDevice.waitForNextSync(); // make sure the client knows the room is encrypted + await bobSecondDevice.sendMessage(testRoomId, "test encrypted from unverified"); + + // ... and then logs out both devices. + await bob.evaluate((cli) => cli.logout(true)); + await bobSecondDevice.evaluate((cli) => cli.logout(true)); + + // Let our app start syncing again + await app.client.network.goOnline(); + + // Wait for the messages to arrive + const last = page.locator(".mx_EventTile_last"); + await expect(last).toContainText("test encrypted from unverified"); + const lastE2eIcon = last.locator(".mx_EventTile_e2eIcon"); + await expect(lastE2eIcon).toHaveClass(/mx_EventTile_e2eIcon_warning/); + await lastE2eIcon.focus(); + await expect(page.getByRole("tooltip")).toContainText("Encrypted by a device not verified by its owner."); + + const penultimate = page.locator(".mx_EventTile").filter({ hasText: "test encrypted from verified" }); + await expect(penultimate.locator(".mx_EventTile_e2eIcon")).not.toBeVisible(); + }); }); }); + +async function createSecondBotDevice(page: Page, homeserver: HomeserverInstance, bob: Bot) { + const bobSecondDevice = new Bot(page, homeserver, { + bootstrapSecretStorage: false, + bootstrapCrossSigning: false, + }); + bobSecondDevice.setCredentials(await homeserver.loginUser(bob.credentials.userId, bob.credentials.password)); + await bobSecondDevice.prepareClient(); + return bobSecondDevice; +} diff --git a/playwright/plugins/homeserver/synapse/index.ts b/playwright/plugins/homeserver/synapse/index.ts index becfffaa19..64c2465862 100644 --- a/playwright/plugins/homeserver/synapse/index.ts +++ b/playwright/plugins/homeserver/synapse/index.ts @@ -20,7 +20,7 @@ import { randB64Bytes } from "../../utils/rand"; // Docker tag to use for synapse docker image. // We target a specific digest as every now and then a Synapse update will break our CI. // This digest is updated by the playwright-image-updates.yaml workflow periodically. -const DOCKER_TAG = "develop@sha256:e69f01d085a69269c892dfa899cb274a593f0fbb4c518eac2b530319fa43c7cb"; +const DOCKER_TAG = "develop@sha256:117a94ee66e4049eb6f40d04cc70d4fc83f7022dacc9871448c141e7756492f9"; async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise> { const templateDir = path.join(__dirname, "templates", opts.template); 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/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/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/structures/MatrixChat.tsx b/src/components/structures/MatrixChat.tsx index cae0a549b7..8e0eaabe4f 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,19 +951,28 @@ 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; } + const isMobileRegistrationAllowed = + isMobileRegistration && SettingsStore.getValue("Registration.mobileRegistrationHelper"); const newState: Partial = { 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 (isMobileRegistrationAllowed && 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, @@ -978,10 +992,12 @@ export default class MatrixChat extends React.PureComponent { newState.register_id_sid = params.sid; } + newState.isMobileRegistration = isMobileRegistrationAllowed; + this.setStateForNewView(newState); ThemeController.isLogin = true; this.themeWatcher.recheck(); - this.notifyNewScreen("register"); + this.notifyNewScreen(isMobileRegistrationAllowed ? "mobile_register" : "register"); } // switch view to the given room @@ -1721,6 +1737,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 +2101,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/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/auth/Registration.tsx b/src/components/structures/auth/Registration.tsx index d48bdfdcba..2dc9125362 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, + }); + window.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,15 @@ export default class Registration extends React.Component { {regDoneText}

); + } else if (this.props.mobileRegister) { + body = ( + +

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

+ {errorText} + {serverDeadSection} + {this.renderRegisterComponent()} +
+ ); } else { body = ( @@ -746,7 +778,9 @@ export default class Registration extends React.Component { ); } - + if (this.props.mobileRegister) { + return
{body}
; + } return ( 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/auth/InteractiveAuthEntryComponents.tsx b/src/components/views/auth/InteractiveAuthEntryComponents.tsx index 05a8db0c54..44ccd3a30e 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.ComponentClass = ({ 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 +381,25 @@ 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/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(); diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json index 21addb3b98..1ceee5b51f 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", @@ -369,6 +370,8 @@ "email_resend_prompt": "Did not receive it? Resend 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:", @@ -1465,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\".", @@ -1851,6 +1853,11 @@ "other": "You can only pin up to %(count)s widgets" }, "menu": "Open menu", + "release_announcement": { + "close": "Ok", + "description": "Find all pinned messages here. Rollover any message and select “Pin” to add it.", + "title": "All new pinned messages" + }, "reply_thread": "Reply to a thread message", "title": "Pinned messages", "unpin_all": { diff --git a/src/settings/Settings.tsx b/src/settings/Settings.tsx index a82fdef1ba..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, @@ -876,6 +868,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, diff --git a/src/stores/ReleaseAnnouncementStore.ts b/src/stores/ReleaseAnnouncementStore.ts index 3c36af91ae..ba6a79fec0 100644 --- a/src/stores/ReleaseAnnouncementStore.ts +++ b/src/stores/ReleaseAnnouncementStore.ts @@ -17,7 +17,7 @@ import { Features } from "../settings/Settings"; /** * The features are shown in the array order. */ -const FEATURES = ["threadsActivityCentre"] as const; +const FEATURES = ["threadsActivityCentre", "pinningMessageList"] as const; /** * All the features that can be shown in the release announcements. */ 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/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 4e03a26f02..10caa3638a 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”. +

+
+ + +
+
+
+`; 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..8300f4920d 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,55 @@ exports[` has button to edit topic 1`] = ` data-orientation="horizontal" role="separator" /> + + +