Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework UrlPreviewSettings to use MatrixClient.CryptoApi.isEncryptionEnabledInRoom #28463

Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
204 changes: 120 additions & 84 deletions src/components/views/room_settings/UrlPreviewSettings.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,107 +9,143 @@ 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, { ReactNode } from "react";
import React, { ReactNode, JSX } from "react";
import { Room } from "matrix-js-sdk/src/matrix";

import { _t, _td } from "../../../languageHandler";
import { _t } from "../../../languageHandler";
import SettingsStore from "../../../settings/SettingsStore";
import dis from "../../../dispatcher/dispatcher";
import { MatrixClientPeg } from "../../../MatrixClientPeg";
import { Action } from "../../../dispatcher/actions";
import { SettingLevel } from "../../../settings/SettingLevel";
import SettingsFlag from "../elements/SettingsFlag";
import SettingsFieldset from "../settings/SettingsFieldset";
import AccessibleButton, { ButtonEvent } from "../elements/AccessibleButton";
import { useIsEncrypted } from "../../../hooks/useIsEncrypted.ts";
import { useMatrixClientContext } from "../../../contexts/MatrixClientContext.tsx";
import { useSettingValueAt } from "../../../hooks/useSettings.ts";

interface IProps {
/**
* The URL preview settings for a room
*/
interface UrlPreviewSettingsProps {
/**
* The room.
*/
room: Room;
}

export default class UrlPreviewSettings extends React.Component<IProps> {
private onClickUserSettings = (e: ButtonEvent): void => {
e.preventDefault();
e.stopPropagation();
dis.fire(Action.ViewUserSettings);
};

public render(): ReactNode {
const roomId = this.props.room.roomId;
const isEncrypted = MatrixClientPeg.safeGet().isRoomEncrypted(roomId);

let previewsForAccount: ReactNode | undefined;
let previewsForRoom: ReactNode | undefined;

if (!isEncrypted) {
// Only show account setting state and room state setting state in non-e2ee rooms where they apply
const accountEnabled = SettingsStore.getValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");
if (accountEnabled) {
previewsForAccount = _t(
"room_settings|general|user_url_previews_default_on",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
{sub}
</AccessibleButton>
),
},
);
} else {
previewsForAccount = _t(
"room_settings|general|user_url_previews_default_off",
{},
{
a: (sub) => (
<AccessibleButton kind="link_inline" onClick={this.onClickUserSettings}>
{sub}
</AccessibleButton>
),
},
);
}

if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
previewsForRoom = (
export function UrlPreviewSettings({ room }: UrlPreviewSettingsProps): JSX.Element {
const { roomId } = room;
const matrixClient = useMatrixClientContext();
const isEncrypted = useIsEncrypted(matrixClient, room);
const isLoading = isEncrypted === null;

return (
<SettingsFieldset
legend={_t("room_settings|general|url_previews_section")}
description={!isLoading && <Description isEncrypted={isEncrypted} />}
>
{isLoading ? (
_t("common|loading")
florianduros marked this conversation as resolved.
Show resolved Hide resolved
) : (
<>
<PreviewsForRoom isEncrypted={isEncrypted} roomId={roomId} />
<SettingsFlag
name="urlPreviewsEnabled"
level={SettingLevel.ROOM}
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
level={SettingLevel.ROOM_DEVICE}
roomId={roomId}
isExplicit={true}
/>
);
} else {
let str = _td("room_settings|general|default_url_previews_on");
if (!SettingsStore.getValueAt(SettingLevel.ROOM, "urlPreviewsEnabled", roomId, /*explicit=*/ true)) {
str = _td("room_settings|general|default_url_previews_off");
}
previewsForRoom = <div>{_t(str)}</div>;
}
} else {
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
}

const previewsForRoomAccount = // in an e2ee room we use a special key to enforce per-room opt-in
(
<SettingsFlag
name={isEncrypted ? "urlPreviewsEnabled_e2ee" : "urlPreviewsEnabled"}
level={SettingLevel.ROOM_DEVICE}
roomId={roomId}
/>
);

const description = (
<>
<p>{_t("room_settings|general|url_preview_explainer")}</p>
<p>{previewsForAccount}</p>
</>
);
</>
)}
</SettingsFieldset>
);
}

/**
* Click handler for the user settings link
* @param e
*/
function onClickUserSettings(e: ButtonEvent): void {
e.preventDefault();
e.stopPropagation();
dis.fire(Action.ViewUserSettings);
}

/**
* The description for the URL preview settings
*/
interface DescriptionProps {
/**
* Whether the room is encrypted
*/
isEncrypted: boolean;
}

function Description({ isEncrypted }: DescriptionProps): JSX.Element {
const urlPreviewsEnabled = useSettingValueAt(SettingLevel.ACCOUNT, "urlPreviewsEnabled");

let previewsForAccount: ReactNode | undefined;
if (isEncrypted) {
previewsForAccount = _t("room_settings|general|url_preview_encryption_warning");
} else {
const button = {
a: (sub: string) => (
<AccessibleButton kind="link_inline" onClick={onClickUserSettings}>
{sub}
</AccessibleButton>
),
};

return (
<SettingsFieldset legend={_t("room_settings|general|url_previews_section")} description={description}>
{previewsForRoom}
{previewsForRoomAccount}
</SettingsFieldset>
previewsForAccount = urlPreviewsEnabled
? _t("room_settings|general|user_url_previews_default_on", {}, button)
: _t("room_settings|general|user_url_previews_default_off", {}, button);
}

return (
<>
<p>{_t("room_settings|general|url_preview_explainer")}</p>
<p>{previewsForAccount}</p>
</>
);
}

/**
* The description for the URL preview settings
*/
interface PreviewsForRoomProps {
/**
* Whether the room is encrypted
*/
isEncrypted: boolean;
/**
* The room ID
*/
roomId: string;
}

function PreviewsForRoom({ isEncrypted, roomId }: PreviewsForRoomProps): JSX.Element | null {
const urlPreviewsEnabled = useSettingValueAt(
SettingLevel.ACCOUNT,
"urlPreviewsEnabled",
roomId,
/*explicit=*/ true,
);
if (isEncrypted) return null;

let previewsForRoom: ReactNode;
if (SettingsStore.canSetValue("urlPreviewsEnabled", roomId, SettingLevel.ROOM)) {
previewsForRoom = (
<SettingsFlag name="urlPreviewsEnabled" level={SettingLevel.ROOM} roomId={roomId} isExplicit={true} />
);
} else {
previewsForRoom = (
<div>
{urlPreviewsEnabled
? _t("room_settings|general|default_url_previews_on")
: _t("room_settings|general|default_url_previews_off")}
</div>
);
}

return previewsForRoom;
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ import dis from "../../../../../dispatcher/dispatcher";
import MatrixClientContext from "../../../../../contexts/MatrixClientContext";
import SettingsStore from "../../../../../settings/SettingsStore";
import { UIFeature } from "../../../../../settings/UIFeature";
import UrlPreviewSettings from "../../../room_settings/UrlPreviewSettings";
import AliasSettings from "../../../room_settings/AliasSettings";
import PosthogTrackers from "../../../../../PosthogTrackers";
import { SettingsSubsection } from "../../shared/SettingsSubsection";
import SettingsTab from "../SettingsTab";
import { SettingsSection } from "../../shared/SettingsSection";
import { UrlPreviewSettings } from "../../../room_settings/UrlPreviewSettings";

interface IProps {
room: Room;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* 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.
*/

import React from "react";
import { MatrixClient, Room } from "matrix-js-sdk/src/matrix";
import { render, screen } from "jest-matrix-react";
import { waitFor } from "@testing-library/dom";

import { createTestClient, mkStubRoom, withClientContextRenderOptions } from "../../../../test-utils";
import { UrlPreviewSettings } from "../../../../../src/components/views/room_settings/UrlPreviewSettings.tsx";
import SettingsStore from "../../../../../src/settings/SettingsStore.ts";
import dis from "../../../../../src/dispatcher/dispatcher.ts";
import { Action } from "../../../../../src/dispatcher/actions.ts";

describe("UrlPreviewSettings", () => {
let client: MatrixClient;
let room: Room;

beforeEach(() => {
client = createTestClient();
room = mkStubRoom("roomId", "room", client);
});

afterEach(() => {
jest.restoreAllMocks();
});

function renderComponent() {
return render(<UrlPreviewSettings room={room} />, withClientContextRenderOptions(client));
}

it("should display the correct preview when the setting is in a loading state", () => {
jest.spyOn(client, "getCrypto").mockReturnValue(undefined);
const { asFragment } = renderComponent();
expect(screen.getByText("URL Previews")).toBeInTheDocument();

expect(asFragment()).toMatchSnapshot();
});

it("should display the correct preview when the room is encrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(true);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);

const { asFragment } = renderComponent();
await waitFor(() => {
expect(
screen.getByText(
"In encrypted rooms, like this one, URL previews are disabled by default to ensure that your homeserver (where the previews are generated) cannot gather information about links you see in this room.",
),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});

it("should display the correct preview when the room is unencrypted and the url preview is enabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(true);
jest.spyOn(dis, "fire").mockReturnValue(undefined);

const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "enabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are enabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();

screen.getByRole("button", { name: "enabled" }).click();
expect(dis.fire).toHaveBeenCalledWith(Action.ViewUserSettings);
});

it("should display the correct preview when the room is unencrypted and the url preview is disabled", async () => {
jest.spyOn(client.getCrypto()!, "isEncryptionEnabledInRoom").mockResolvedValue(false);
jest.spyOn(SettingsStore, "getValueAt").mockReturnValue(false);

const { asFragment } = renderComponent();
await waitFor(() => {
expect(screen.getByRole("button", { name: "disabled" })).toBeInTheDocument();
expect(
screen.getByText("URL previews are disabled by default for participants in this room."),
).toBeInTheDocument();
});
expect(asFragment()).toMatchSnapshot();
});
});
Loading
Loading