Skip to content

Commit

Permalink
Merge branch 'develop' into florianduros/rip-out-legacy-crypto/1-remo…
Browse files Browse the repository at this point in the history
…ve-deprecated-call-client

# Conflicts:
#	test/unit-tests/components/structures/MatrixChat-test.tsx
  • Loading branch information
florianduros committed Oct 18, 2024
2 parents 511a70b + 1bb482f commit 8904b28
Show file tree
Hide file tree
Showing 15 changed files with 381 additions and 133 deletions.
3 changes: 3 additions & 0 deletions playwright/e2e/accessibility/keyboard-navigation.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,9 @@ test.describe("Landmark navigation tests", () => {
});

test("without any rooms", async ({ page, homeserver, app, user }) => {
// sometimes the space button doesn't appear right away
await expect(page.locator(".mx_SpaceButton_active")).toBeVisible();

/**
* Without any rooms, there is no tile in the roomlist to be focused.
* So the next landmark in the list should be focused instead.
Expand Down
2 changes: 1 addition & 1 deletion playwright/e2e/login/login.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ test.describe("Login", () => {
const h1 = await page.getByRole("heading", { name: "Verify this device", level: 1 });
await expect(h1).toBeVisible();

expect(h1.locator(".mx_CompleteSecurity_skip")).not.toBeVisible();
await expect(h1.locator(".mx_CompleteSecurity_skip")).toHaveCount(0);
});
});
});
Expand Down
47 changes: 47 additions & 0 deletions playwright/e2e/settings/account-user-settings-tab.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -140,4 +140,51 @@ test.describe("Account user settings tab", () => {
await expect(page.locator(".mx_UserMenu .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
await expect(page.locator(".mx_RoomView_wrapper .mx_BaseAvatar").getByText("A")).toBeVisible(); // Alice
});

// ported to a playwright test because the jest test was very flakey for no obvious reason
test("should display an error if the code is incorrect when adding a phone number", async ({ uut, page }) => {
const dummyUrl = "https://nowhere.dummy/_matrix/client/unstable/add_threepid/msisdn/submit_token";

await page.route(
`**/_matrix/client/v3/account/3pid/msisdn/requestToken`,
async (route) => {
await route.fulfill({
json: {
success: true,
sid: "1",
msisdn: "447700900000",
intl_fmt: "+44 7700 900000",
submit_url: dummyUrl,
},
});
},
{ times: 1 },
);

await page.route(
dummyUrl,
async (route) => {
await route.fulfill({
status: 400,
json: {
errcode: "M_THREEPID_AUTH_FAILED",
error: "That code is definitely wrong",
},
});
},
{ times: 1 },
);

const phoneSection = page.getByTestId("mx_AccountPhoneNumbers");
await phoneSection.getByRole("textbox", { name: "Phone Number" }).fill("07700900000");
await phoneSection.getByRole("button", { name: "Add" }).click();

await phoneSection
.getByRole("textbox", { name: "Verification code" })
.fill("A small eurasian field mouse dancing the paso doble");

await phoneSection.getByRole("button", { name: "Continue" }).click();

await expect(page.getByRole("heading", { name: "Unable to verify phone number." })).toBeVisible();
});
});
2 changes: 1 addition & 1 deletion playwright/plugins/homeserver/synapse/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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:fd6ba2d8471a0807e1bccef4124b22d17f0058f2cf9285066fdd94d8c631964a";
const DOCKER_TAG = "develop@sha256:47c62aa9507a24820190eef547861c0d278cc83fe90329c46b9f4329eed88ef4";

async function cfgDirFromTemplate(opts: StartHomeserverOpts): Promise<Omit<HomeserverConfig, "dockerUrl">> {
const templateDir = path.join(__dirname, "templates", opts.template);
Expand Down
2 changes: 1 addition & 1 deletion src/SlashCommands.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -727,7 +727,7 @@ export const Commands = [
isEnabled: (cli) => !isCurrentLocalRoom(cli),
runFn: function (cli, roomId) {
try {
cli.forceDiscardSession(roomId);
cli.getCrypto()?.forceDiscardSession(roomId);
} catch (e) {
return reject(e instanceof Error ? e.message : e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,92 +7,82 @@ SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only
Please see LICENSE files in the repository root for full details.
*/

import React from "react";
import { KeyBackupInfo } from "matrix-js-sdk/src/crypto-api";
import React, { JSX, useEffect, useState } from "react";

import { MatrixClientPeg } from "../../../../MatrixClientPeg";
import dis from "../../../../dispatcher/dispatcher";
import { _t } from "../../../../languageHandler";
import Modal from "../../../../Modal";
import RestoreKeyBackupDialog from "../../../../components/views/dialogs/security/RestoreKeyBackupDialog";
import { Action } from "../../../../dispatcher/actions";
import DialogButtons from "../../../../components/views/elements/DialogButtons";
import BaseDialog from "../../../../components/views/dialogs/BaseDialog";
import { useMatrixClientContext } from "../../../../contexts/MatrixClientContext.tsx";

interface IProps {
newVersionInfo: KeyBackupInfo;
/**
* Properties for {@link NewRecoveryMethodDialog}.
*/
interface NewRecoveryMethodDialogProps {
/**
* Callback when the dialog is dismissed.
*/
onFinished(): void;
}

export default class NewRecoveryMethodDialog extends React.PureComponent<IProps> {
private onOkClick = (): void => {
this.props.onFinished();
};
// Export as default instead of a named export so that it can be dynamically imported with `Modal.createDialogAsync`

private onGoToSettingsClick = (): void => {
this.props.onFinished();
dis.fire(Action.ViewUserSettings);
};
/**
* Dialog to inform the user that a new recovery method has been detected.
*/
export default function NewRecoveryMethodDialog({ onFinished }: NewRecoveryMethodDialogProps): JSX.Element {
const matrixClient = useMatrixClientContext();
const [isKeyBackupEnabled, setIsKeyBackupEnabled] = useState(false);
useEffect(() => {
const checkBackupEnabled = async (): Promise<void> => {
const crypto = matrixClient.getCrypto();
setIsKeyBackupEnabled(Boolean(crypto && (await crypto.getActiveSessionBackupVersion()) !== null));
};

private onSetupClick = async (): Promise<void> => {
Modal.createDialog(
RestoreKeyBackupDialog,
{
onFinished: this.props.onFinished,
},
undefined,
/* priority = */ false,
/* static = */ true,
);
};
checkBackupEnabled();
}, [matrixClient]);

public render(): React.ReactNode {
const title = (
<span className="mx_KeyBackupFailedDialog_title">
{_t("encryption|new_recovery_method_detected|title")}
</span>
);

const newMethodDetected = <p>{_t("encryption|new_recovery_method_detected|description_1")}</p>;

const hackWarning = (
<strong className="warning">{_t("encryption|new_recovery_method_detected|warning")}</strong>
);

let content: JSX.Element | undefined;
if (MatrixClientPeg.safeGet().getKeyBackupEnabled()) {
content = (
<div>
{newMethodDetected}
<p>{_t("encryption|new_recovery_method_detected|description_2")}</p>
{hackWarning}
<DialogButtons
primaryButton={_t("action|ok")}
onPrimaryButtonClick={this.onOkClick}
cancelButton={_t("common|go_to_settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
);
function onClick(): void {
if (isKeyBackupEnabled) {
onFinished();
} else {
content = (
<div>
{newMethodDetected}
{hackWarning}
<DialogButtons
primaryButton={_t("common|setup_secure_messages")}
onPrimaryButtonClick={this.onSetupClick}
cancelButton={_t("common|go_to_settings")}
onCancel={this.onGoToSettingsClick}
/>
</div>
Modal.createDialog(
RestoreKeyBackupDialog,
{
onFinished,
},
undefined,
false,
true,
);
}

return (
<BaseDialog className="mx_KeyBackupFailedDialog" onFinished={this.props.onFinished} title={title}>
{content}
</BaseDialog>
);
}

return (
<BaseDialog
className="mx_KeyBackupFailedDialog"
onFinished={onFinished}
title={
<span className="mx_KeyBackupFailedDialog_title">
{_t("encryption|new_recovery_method_detected|title")}
</span>
}
>
<p>{_t("encryption|new_recovery_method_detected|description_1")}</p>
{isKeyBackupEnabled && <p>{_t("encryption|new_recovery_method_detected|description_2")}</p>}
<strong className="warning">{_t("encryption|new_recovery_method_detected|warning")}</strong>
<DialogButtons
primaryButton={_t("common|setup_secure_messages")}
onPrimaryButtonClick={onClick}
cancelButton={_t("common|go_to_settings")}
onCancel={() => {
onFinished();
dis.fire(Action.ViewUserSettings);
}}
/>
</BaseDialog>
);
}
17 changes: 11 additions & 6 deletions src/components/structures/GenericDropdownMenu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,10 @@ type IProps<T> = WithKeyFunction<T> & {
AdditionalOptions?: FunctionComponent<AdditionalOptionsProps>;
};

function calculateKey<T>(value: T, toKey: ((key: T) => Key) | undefined): Key {
return toKey ? toKey(value) : (value as Key);
}

export function GenericDropdownMenu<T>({
value,
onChange,
Expand All @@ -119,23 +123,24 @@ export function GenericDropdownMenu<T>({
}: IProps<T>): JSX.Element {
const [menuDisplayed, button, openMenu, closeMenu] = useContextMenu<HTMLElement>();

const valueKey = calculateKey(value, toKey);
const selected: GenericDropdownMenuItem<T> | undefined = options
.flatMap((it) => (isGenericDropdownMenuGroup(it) ? [it, ...it.options] : [it]))
.find((option) => (toKey ? toKey(option.key) === toKey(value) : option.key === value));
.find((option) => calculateKey(option.key, toKey) === valueKey);
let contextMenuOptions: JSX.Element;
if (options && isGenericDropdownMenuGroupArray(options)) {
contextMenuOptions = (
<>
{options.map((group) => (
<GenericDropdownMenuGroup
key={toKey?.(group.key) ?? (group.key as Key)}
key={calculateKey(group.key, toKey)}
label={group.label}
description={group.description}
adornment={group.adornment}
>
{group.options.map((option) => (
<GenericDropdownMenuOption
key={toKey?.(option.key) ?? (option.key as Key)}
key={calculateKey(option.key, toKey)}
label={option.label}
description={option.description}
onClick={(ev: ButtonEvent) => {
Expand All @@ -144,7 +149,7 @@ export function GenericDropdownMenu<T>({
onClose?.(ev);
}}
adornment={option.adornment}
isSelected={option === selected}
isSelected={calculateKey(option.key, toKey) === valueKey}
/>
))}
</GenericDropdownMenuGroup>
Expand All @@ -156,7 +161,7 @@ export function GenericDropdownMenu<T>({
<>
{options.map((option) => (
<GenericDropdownMenuOption
key={toKey?.(option.key) ?? (option.key as Key)}
key={calculateKey(option.key, toKey)}
label={option.label}
description={option.description}
onClick={(ev: ButtonEvent) => {
Expand All @@ -165,7 +170,7 @@ export function GenericDropdownMenu<T>({
onClose?.(ev);
}}
adornment={option.adornment}
isSelected={option === selected}
isSelected={calculateKey(option.key, toKey) === valueKey}
/>
))}
</>
Expand Down
7 changes: 5 additions & 2 deletions src/components/structures/MatrixChat.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -1631,8 +1631,12 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
cli.on(CryptoEvent.KeyBackupFailed, async (errcode): Promise<void> => {
let haveNewVersion: boolean | undefined;
let newVersionInfo: KeyBackupInfo | null = null;
const keyBackupEnabled = Boolean(
cli.getCrypto() && (await cli.getCrypto()?.getActiveSessionBackupVersion()) !== null,
);

// if key backup is still enabled, there must be a new backup in place
if (cli.getKeyBackupEnabled()) {
if (keyBackupEnabled) {
haveNewVersion = true;
} else {
// otherwise check the server to see if there's a new one
Expand All @@ -1650,7 +1654,6 @@ export default class MatrixChat extends React.PureComponent<IProps, IState> {
import(
"../../async-components/views/dialogs/security/NewRecoveryMethodDialog"
) as unknown as Promise<typeof NewRecoveryMethodDialog>,
{ newVersionInfo: newVersionInfo! },
);
} else {
Modal.createDialogAsync(
Expand Down
1 change: 1 addition & 0 deletions src/stores/right-panel/RightPanelStore.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ export default class RightPanelStore extends ReadyWatchingStore {
if (this.currentCard.phase !== RightPanelPhases.RoomSummary) {
this.setCard({ phase: RightPanelPhases.RoomSummary, state: { focusRoomSearch: true } });
}
this.show(null);
}
}
}
Expand Down
4 changes: 4 additions & 0 deletions src/stores/right-panel/RightPanelStoreIPanelState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@ export interface IRightPanelCardStateStored {
initialEventId?: string;
isInitialEventHighlighted?: boolean;
initialEventScrollIntoView?: boolean;
// room summary card
focusRoomSearch?: boolean;
}

export interface IRightPanelCard {
Expand Down Expand Up @@ -85,6 +87,7 @@ export function convertCardToStore(panelState: IRightPanelCard): IRightPanelCard
memberInfoEventId: !!state?.memberInfoEvent?.getId() ? state.memberInfoEvent.getId() : undefined,
initialEventId: !!state?.initialEvent?.getId() ? state.initialEvent.getId() : undefined,
memberId: !!state?.member?.userId ? state.member.userId : undefined,
focusRoomSearch: state.focusRoomSearch,
};

return { state: stateStored, phase: panelState.phase };
Expand All @@ -105,6 +108,7 @@ function convertStoreToCard(panelStateStore: IRightPanelCardStored, room: Room):
: undefined,
initialEvent: !!stateStored?.initialEventId ? room.findEventById(stateStored.initialEventId) : undefined,
member: (!!stateStored?.memberId && room.getMember(stateStored.memberId)) || undefined,
focusRoomSearch: stateStored?.focusRoomSearch,
};

return { state: state, phase: panelStateStore.phase };
Expand Down
2 changes: 1 addition & 1 deletion test/test-utils/test-utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,7 @@ export function createTestClient(): MatrixClient {
getVerificationRequestsToDeviceInProgress: jest.fn().mockReturnValue([]),
setDeviceIsolationMode: jest.fn(),
prepareToEncrypt: jest.fn(),
getActiveSessionBackupVersion: jest.fn().mockResolvedValue(null),
}),

getPushActionsForEvent: jest.fn(),
Expand Down Expand Up @@ -163,7 +164,6 @@ export function createTestClient(): MatrixClient {
});
}),
mxcUrlToHttp: jest.fn().mockImplementation((mxc: string) => `http://this.is.a.url/${mxc.substring(6)}`),
scheduleAllGroupSessionsForBackup: jest.fn().mockResolvedValue(undefined),
setAccountData: jest.fn(),
setRoomAccountData: jest.fn(),
setRoomTopic: jest.fn(),
Expand Down
Loading

0 comments on commit 8904b28

Please sign in to comment.