You've already forked element-web
mirror of
https://github.com/element-hq/element-web.git
synced 2025-08-09 14:42:51 +03:00
Implement MSC4155: Invite filtering (#29603)
* Add settings for MSC4155 * copyright * Tweak to not use js-sdk * Update for latest MSC * Various tidyups * Move tab * i18n * update .snap * mvvm * lint * add header * Remove capability check * fix * Rewrite to use Settings * lint * lint * fix test * Tweaks * lint * revert copyright * update screenshot * cleanup
This commit is contained in:
Binary file not shown.
Before Width: | Height: | Size: 268 KiB After Width: | Height: | Size: 272 KiB |
29
src/@types/invite-rules.ts
Normal file
29
src/@types/invite-rules.ts
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
export const INVITE_RULES_ACCOUNT_DATA_TYPE = "org.matrix.msc4155.invite_permission_config";
|
||||||
|
|
||||||
|
export interface InviteConfigAccountData {
|
||||||
|
allowed_users?: string[];
|
||||||
|
blocked_users?: string[];
|
||||||
|
ignored_users?: string[];
|
||||||
|
allowed_servers?: string[];
|
||||||
|
blocked_servers?: string[];
|
||||||
|
ignored_servers?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Computed values based on MSC4155. Currently Element Web only supports
|
||||||
|
* blocking all invites.
|
||||||
|
*/
|
||||||
|
export interface ComputedInviteConfig extends Record<string, unknown> {
|
||||||
|
/**
|
||||||
|
* Are all invites blocked. This is only about blocking all invites,
|
||||||
|
* but this being false may still block invites through other rules.
|
||||||
|
*/
|
||||||
|
allBlocked: boolean;
|
||||||
|
}
|
4
src/@types/matrix-js-sdk.d.ts
vendored
4
src/@types/matrix-js-sdk.d.ts
vendored
@@ -15,6 +15,7 @@ import type { EmptyObject } from "matrix-js-sdk/src/matrix";
|
|||||||
import type { DeviceClientInformation } from "../utils/device/types.ts";
|
import type { DeviceClientInformation } from "../utils/device/types.ts";
|
||||||
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
|
import type { UserWidget } from "../utils/WidgetUtils-types.ts";
|
||||||
import { type MediaPreviewConfig } from "./media_preview.ts";
|
import { type MediaPreviewConfig } from "./media_preview.ts";
|
||||||
|
import { type INVITE_RULES_ACCOUNT_DATA_TYPE, type InviteConfigAccountData } from "./invite-rules.ts";
|
||||||
|
|
||||||
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
// Extend Matrix JS SDK types via Typescript declaration merging to support unspecced event fields and types
|
||||||
declare module "matrix-js-sdk/src/types" {
|
declare module "matrix-js-sdk/src/types" {
|
||||||
@@ -60,7 +61,6 @@ declare module "matrix-js-sdk/src/types" {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AccountDataEvents {
|
export interface AccountDataEvents {
|
||||||
// Analytics account data event
|
// Analytics account data event
|
||||||
"im.vector.analytics": {
|
"im.vector.analytics": {
|
||||||
@@ -89,6 +89,8 @@ declare module "matrix-js-sdk/src/types" {
|
|||||||
accepted: string[];
|
accepted: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// MSC4155: Invite filtering
|
||||||
|
[INVITE_RULES_ACCOUNT_DATA_TYPE]: InviteConfigAccountData;
|
||||||
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
|
"io.element.msc4278.media_preview_config": MediaPreviewConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -0,0 +1,47 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { type FC, useCallback, useState } from "react";
|
||||||
|
import { Root } from "@vector-im/compound-web";
|
||||||
|
import { logger } from "matrix-js-sdk/src/logger";
|
||||||
|
|
||||||
|
import { _t } from "../../../../../languageHandler";
|
||||||
|
import { useSettingValue } from "../../../../../hooks/useSettings";
|
||||||
|
import SettingsStore from "../../../../../settings/SettingsStore";
|
||||||
|
import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
|
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
||||||
|
|
||||||
|
export const InviteRulesAccountSetting: FC = () => {
|
||||||
|
const rules = useSettingValue("inviteRules");
|
||||||
|
const settingsDisabled = SettingsStore.disabledMessage("inviteRules");
|
||||||
|
const [busy, setBusy] = useState(false);
|
||||||
|
|
||||||
|
const onChange = useCallback(async (checked: boolean) => {
|
||||||
|
try {
|
||||||
|
setBusy(true);
|
||||||
|
await SettingsStore.setValue("inviteRules", null, SettingLevel.ACCOUNT, {
|
||||||
|
allBlocked: !checked,
|
||||||
|
});
|
||||||
|
} catch (ex) {
|
||||||
|
logger.error(`Unable to set invite rules`, ex);
|
||||||
|
} finally {
|
||||||
|
setBusy(false);
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
return (
|
||||||
|
<Root className="mx_MediaPreviewAccountSetting_Form">
|
||||||
|
<LabelledToggleSwitch
|
||||||
|
className="mx_MediaPreviewAccountSetting_ToggleSwitch"
|
||||||
|
label={_t("settings|invite_controls|default_label")}
|
||||||
|
value={!rules.allBlocked}
|
||||||
|
onChange={onChange}
|
||||||
|
tooltip={settingsDisabled}
|
||||||
|
disabled={!!settingsDisabled || busy}
|
||||||
|
/>
|
||||||
|
</Root>
|
||||||
|
);
|
||||||
|
};
|
@@ -33,6 +33,7 @@ import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
|
|||||||
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
import * as TimezoneHandler from "../../../../../TimezoneHandler";
|
||||||
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
import { type BooleanSettingKey } from "../../../../../settings/Settings.tsx";
|
||||||
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
import { MediaPreviewAccountSettings } from "./MediaPreviewAccountSettings.tsx";
|
||||||
|
import { InviteRulesAccountSetting } from "./InviteRulesAccountSettings.tsx";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
closeSettingsFn(success: boolean): void;
|
closeSettingsFn(success: boolean): void;
|
||||||
@@ -339,6 +340,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
|||||||
|
|
||||||
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
<SettingsSubsection heading={_t("common|moderation_and_safety")} legacy={false}>
|
||||||
<MediaPreviewAccountSettings />
|
<MediaPreviewAccountSettings />
|
||||||
|
<InviteRulesAccountSetting />
|
||||||
</SettingsSubsection>
|
</SettingsSubsection>
|
||||||
|
|
||||||
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
<SettingsSubsection heading={_t("settings|preferences|room_directory_heading")}>
|
||||||
|
@@ -2686,6 +2686,9 @@
|
|||||||
"inline_url_previews_room": "Enable URL previews by default for participants in this room",
|
"inline_url_previews_room": "Enable URL previews by default for participants in this room",
|
||||||
"inline_url_previews_room_account": "Enable URL previews for this room (only affects you)",
|
"inline_url_previews_room_account": "Enable URL previews for this room (only affects you)",
|
||||||
"insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message",
|
"insert_trailing_colon_mentions": "Insert a trailing colon after user mentions at the start of a message",
|
||||||
|
"invite_controls": {
|
||||||
|
"default_label": "Allow users to invite you to rooms"
|
||||||
|
},
|
||||||
"jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message",
|
"jump_to_bottom_on_send": "Jump to the bottom of the timeline when you send a message",
|
||||||
"key_backup": {
|
"key_backup": {
|
||||||
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
|
"backup_in_progress": "Your keys are being backed up (the first backup could take a few minutes).",
|
||||||
@@ -2752,6 +2755,7 @@
|
|||||||
"show_in_private": "In private rooms",
|
"show_in_private": "In private rooms",
|
||||||
"show_media": "Always show"
|
"show_media": "Always show"
|
||||||
},
|
},
|
||||||
|
"not_supported": "Your server does not implement this feature.",
|
||||||
"notifications": {
|
"notifications": {
|
||||||
"default_setting_description": "This setting will be applied by default to all your rooms.",
|
"default_setting_description": "This setting will be applied by default to all your rooms.",
|
||||||
"default_setting_section": "I want to be notified for (Default Setting)",
|
"default_setting_section": "I want to be notified for (Default Setting)",
|
||||||
|
@@ -47,6 +47,8 @@ import { type RecentEmojiData } from "../emojipicker/recent.ts";
|
|||||||
import { type Assignable } from "../@types/common.ts";
|
import { type Assignable } from "../@types/common.ts";
|
||||||
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
|
import { SortingAlgorithm } from "../stores/room-list-v3/skip-list/sorters/index.ts";
|
||||||
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
|
import MediaPreviewConfigController from "./controllers/MediaPreviewConfigController.ts";
|
||||||
|
import InviteRulesConfigController from "./controllers/InviteRulesConfigController.ts";
|
||||||
|
import { type ComputedInviteConfig } from "../@types/invite-rules.ts";
|
||||||
|
|
||||||
export const defaultWatchManager = new WatchManager();
|
export const defaultWatchManager = new WatchManager();
|
||||||
|
|
||||||
@@ -351,6 +353,7 @@ export interface Settings {
|
|||||||
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
"Electron.enableHardwareAcceleration": IBaseSetting<boolean>;
|
||||||
"Electron.enableContentProtection": IBaseSetting<boolean>;
|
"Electron.enableContentProtection": IBaseSetting<boolean>;
|
||||||
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
|
"mediaPreviewConfig": IBaseSetting<MediaPreviewConfig>;
|
||||||
|
"inviteRules": IBaseSetting<ComputedInviteConfig>;
|
||||||
"Developer.elementCallUrl": IBaseSetting<string>;
|
"Developer.elementCallUrl": IBaseSetting<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -434,6 +437,11 @@ export const SETTINGS: Settings = {
|
|||||||
supportedLevels: LEVELS_ROOM_SETTINGS,
|
supportedLevels: LEVELS_ROOM_SETTINGS,
|
||||||
default: MediaPreviewConfigController.default,
|
default: MediaPreviewConfigController.default,
|
||||||
},
|
},
|
||||||
|
"inviteRules": {
|
||||||
|
controller: new InviteRulesConfigController(),
|
||||||
|
supportedLevels: [SettingLevel.ACCOUNT],
|
||||||
|
default: InviteRulesConfigController.default,
|
||||||
|
},
|
||||||
"feature_report_to_moderators": {
|
"feature_report_to_moderators": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
labsGroup: LabGroup.Moderation,
|
labsGroup: LabGroup.Moderation,
|
||||||
|
88
src/settings/controllers/InviteRulesConfigController.ts
Normal file
88
src/settings/controllers/InviteRulesConfigController.ts
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { type MatrixClient, type IContent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import { type SettingLevel } from "../SettingLevel.ts";
|
||||||
|
import MatrixClientBackedController from "./MatrixClientBackedController.ts";
|
||||||
|
import {
|
||||||
|
type ComputedInviteConfig as ComputedInviteRules,
|
||||||
|
INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
type InviteConfigAccountData,
|
||||||
|
} from "../../@types/invite-rules.ts";
|
||||||
|
import { _t } from "../../languageHandler.tsx";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles invite filtering rules provided by MSC4155.
|
||||||
|
* This handler does not make use of the roomId parameter.
|
||||||
|
*/
|
||||||
|
export default class InviteRulesConfigController extends MatrixClientBackedController {
|
||||||
|
public static readonly default: ComputedInviteRules = {
|
||||||
|
allBlocked: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
private static getValidSettingData(content: IContent): ComputedInviteRules {
|
||||||
|
const expectedConfig = content as InviteConfigAccountData;
|
||||||
|
return {
|
||||||
|
allBlocked: !!expectedConfig.blocked_users?.includes("*"),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public initMatrixClient(newClient: MatrixClient): void {
|
||||||
|
newClient.doesServerSupportUnstableFeature("org.matrix.msc4155").then((result) => {
|
||||||
|
this.featureSupported = result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public featureSupported?: boolean;
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
this.featureSupported = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private getValue = (): ComputedInviteRules => {
|
||||||
|
const accountData =
|
||||||
|
this.client?.getAccountData(INVITE_RULES_ACCOUNT_DATA_TYPE)?.getContent<InviteConfigAccountData>() ?? {};
|
||||||
|
return InviteRulesConfigController.getValidSettingData(accountData);
|
||||||
|
};
|
||||||
|
|
||||||
|
public getValueOverride(_level: SettingLevel): ComputedInviteRules {
|
||||||
|
return this.getValue();
|
||||||
|
}
|
||||||
|
|
||||||
|
public get settingDisabled(): true | string {
|
||||||
|
return this.featureSupported ? true : _t("settings|not_supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
public async beforeChange(
|
||||||
|
_level: SettingLevel,
|
||||||
|
_roomId: string | null,
|
||||||
|
newValue: ComputedInviteRules,
|
||||||
|
): Promise<boolean> {
|
||||||
|
if (!this.client) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const existingContent = this.client
|
||||||
|
.getAccountData(INVITE_RULES_ACCOUNT_DATA_TYPE)
|
||||||
|
?.getContent<InviteConfigAccountData>();
|
||||||
|
const newContent: InviteConfigAccountData = {
|
||||||
|
...existingContent,
|
||||||
|
blocked_users: [...(existingContent?.blocked_users ?? [])],
|
||||||
|
};
|
||||||
|
if (newValue.allBlocked && !newContent.blocked_users!.includes("*")) {
|
||||||
|
newContent.blocked_users!.push("*");
|
||||||
|
} else if (!newValue.allBlocked && newContent.blocked_users?.includes("*")) {
|
||||||
|
newContent.blocked_users = newContent.blocked_users.filter((u) => u !== "*");
|
||||||
|
} else {
|
||||||
|
// No changes required.
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
await this.client.setAccountData(INVITE_RULES_ACCOUNT_DATA_TYPE, newContent);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
@@ -204,6 +204,9 @@ export default class AccountSettingsHandler extends MatrixClientBackedSettingsHa
|
|||||||
case "mediaPreviewConfig":
|
case "mediaPreviewConfig":
|
||||||
// Handled in MediaPreviewConfigController.
|
// Handled in MediaPreviewConfigController.
|
||||||
return;
|
return;
|
||||||
|
case "inviteRules":
|
||||||
|
// Handled in InviteRulesConfigController.
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
return this.setAccountData(DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
||||||
}
|
}
|
||||||
|
@@ -122,6 +122,9 @@ export default class RoomAccountSettingsHandler extends MatrixClientBackedSettin
|
|||||||
case "mediaPreviewConfig":
|
case "mediaPreviewConfig":
|
||||||
// Handled in MediaPreviewConfigController.
|
// Handled in MediaPreviewConfigController.
|
||||||
return;
|
return;
|
||||||
|
case "inviteRules":
|
||||||
|
// Handled in InviteRulesConfigController.
|
||||||
|
return;
|
||||||
default:
|
default:
|
||||||
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
return this.setRoomAccountData(roomId, DEFAULT_SETTINGS_EVENT_TYPE, settingName, newValue);
|
||||||
}
|
}
|
||||||
|
@@ -127,7 +127,7 @@ describe("<MatrixChat />", () => {
|
|||||||
setGuest: jest.fn(),
|
setGuest: jest.fn(),
|
||||||
setNotifTimelineSet: jest.fn(),
|
setNotifTimelineSet: jest.fn(),
|
||||||
getAccountData: jest.fn(),
|
getAccountData: jest.fn(),
|
||||||
doesServerSupportUnstableFeature: jest.fn(),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||||
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
|
getDevices: jest.fn().mockResolvedValue({ devices: [] }),
|
||||||
getProfileInfo: jest.fn().mockResolvedValue({
|
getProfileInfo: jest.fn().mockResolvedValue({
|
||||||
displayname: "Ernie",
|
displayname: "Ernie",
|
||||||
|
@@ -0,0 +1,76 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
import React from "react";
|
||||||
|
import { render } from "jest-matrix-react";
|
||||||
|
import userEvent from "@testing-library/user-event";
|
||||||
|
|
||||||
|
import { InviteRulesAccountSetting } from "../../../../../../../src/components/views/settings/tabs/user/InviteRulesAccountSettings";
|
||||||
|
import SettingsStore from "../../../../../../../src/settings/SettingsStore";
|
||||||
|
import { type ComputedInviteConfig } from "../../../../../../../src/@types/invite-rules";
|
||||||
|
import { SettingLevel } from "../../../../../../../src/settings/SettingLevel";
|
||||||
|
|
||||||
|
function mockSetting(mediaPreviews: ComputedInviteConfig, supported = true) {
|
||||||
|
jest.spyOn(SettingsStore, "getValue").mockImplementation((settingName) => {
|
||||||
|
if (settingName === "inviteRules") {
|
||||||
|
return mediaPreviews;
|
||||||
|
}
|
||||||
|
throw Error(`Unexpected setting ${settingName}`);
|
||||||
|
});
|
||||||
|
jest.spyOn(SettingsStore, "disabledMessage").mockImplementation((settingName) => {
|
||||||
|
if (settingName === "inviteRules") {
|
||||||
|
return supported ? undefined : "test-not-supported";
|
||||||
|
}
|
||||||
|
throw Error(`Unexpected setting ${settingName}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("InviteRulesAccountSetting", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("does not render if not supported", async () => {
|
||||||
|
mockSetting({ allBlocked: false }, false);
|
||||||
|
const { findByText, findByLabelText } = render(<InviteRulesAccountSetting />);
|
||||||
|
const input = await findByLabelText("Allow users to invite you to rooms");
|
||||||
|
await userEvent.hover(input);
|
||||||
|
const result = await findByText("test-not-supported");
|
||||||
|
expect(result).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
it("renders correct state when invites are not blocked", async () => {
|
||||||
|
mockSetting({ allBlocked: false }, true);
|
||||||
|
const { findByLabelText } = render(<InviteRulesAccountSetting />);
|
||||||
|
const result = await findByLabelText("Allow users to invite you to rooms");
|
||||||
|
expect(result).toBeChecked();
|
||||||
|
});
|
||||||
|
it("renders correct state when invites are blocked", async () => {
|
||||||
|
mockSetting({ allBlocked: true }, true);
|
||||||
|
const { findByLabelText } = render(<InviteRulesAccountSetting />);
|
||||||
|
const result = await findByLabelText("Allow users to invite you to rooms");
|
||||||
|
expect(result).not.toBeChecked();
|
||||||
|
});
|
||||||
|
it("handles disabling all invites", async () => {
|
||||||
|
mockSetting({ allBlocked: false }, true);
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockImplementation();
|
||||||
|
const { findByLabelText } = render(<InviteRulesAccountSetting />);
|
||||||
|
const result = await findByLabelText("Allow users to invite you to rooms");
|
||||||
|
await userEvent.click(result);
|
||||||
|
expect(SettingsStore.setValue).toHaveBeenCalledWith("inviteRules", null, SettingLevel.ACCOUNT, {
|
||||||
|
allBlocked: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it("handles enabling all invites", async () => {
|
||||||
|
mockSetting({ allBlocked: true }, true);
|
||||||
|
jest.spyOn(SettingsStore, "setValue").mockImplementation();
|
||||||
|
const { findByLabelText } = render(<InviteRulesAccountSetting />);
|
||||||
|
const result = await findByLabelText("Allow users to invite you to rooms");
|
||||||
|
await userEvent.click(result);
|
||||||
|
expect(SettingsStore.setValue).toHaveBeenCalledWith("inviteRules", null, SettingLevel.ACCOUNT, {
|
||||||
|
allBlocked: false,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1297,6 +1297,36 @@ exports[`PreferencesUserSettingsTab should render 1`] = `
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
<form
|
||||||
|
class="_root_19upo_16 mx_MediaPreviewAccountSetting_Form"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_SettingsFlag mx_MediaPreviewAccountSetting_ToggleSwitch"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
class="mx_SettingsFlag_label"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
id="mx_LabelledToggleSwitch_«re»"
|
||||||
|
>
|
||||||
|
Allow users to invite you to rooms
|
||||||
|
</div>
|
||||||
|
</span>
|
||||||
|
<div
|
||||||
|
aria-checked="true"
|
||||||
|
aria-disabled="true"
|
||||||
|
aria-label="Your server does not implement this feature."
|
||||||
|
aria-labelledby="mx_LabelledToggleSwitch_«re»"
|
||||||
|
class="mx_AccessibleButton mx_ToggleSwitch mx_ToggleSwitch_on"
|
||||||
|
role="switch"
|
||||||
|
tabindex="0"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
class="mx_ToggleSwitch_ball"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="_separator_7ckbw_8"
|
class="_separator_7ckbw_8"
|
||||||
|
@@ -0,0 +1,156 @@
|
|||||||
|
/*
|
||||||
|
Copyright 2025 New Vector Ltd.
|
||||||
|
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only OR GPL-3.0-only OR LicenseRef-Element-Commercial
|
||||||
|
Please see LICENSE files in the repository root for full details.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { MatrixEvent } from "matrix-js-sdk/src/matrix";
|
||||||
|
|
||||||
|
import MatrixClientBackedController from "../../../../src/settings/controllers/MatrixClientBackedController";
|
||||||
|
import InviteRulesConfigController from "../../../../src/settings/controllers/InviteRulesConfigController";
|
||||||
|
import { SettingLevel } from "../../../../src/settings/SettingLevel";
|
||||||
|
import { getMockClientWithEventEmitter, mockClientMethodsServer } from "../../../test-utils";
|
||||||
|
import { INVITE_RULES_ACCOUNT_DATA_TYPE, type InviteConfigAccountData } from "../../../../src/@types/invite-rules";
|
||||||
|
|
||||||
|
describe("InviteRulesConfigController", () => {
|
||||||
|
afterEach(() => {
|
||||||
|
jest.restoreAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets the default settings when none are specified.", () => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
|
||||||
|
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(null),
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = controller.getValueOverride(SettingLevel.ACCOUNT);
|
||||||
|
expect(value).toEqual(InviteRulesConfigController.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("gets the default settings when the setting is empty.", () => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
|
||||||
|
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest
|
||||||
|
.fn()
|
||||||
|
.mockReturnValue(new MatrixEvent({ type: INVITE_RULES_ACCOUNT_DATA_TYPE, content: {} })),
|
||||||
|
});
|
||||||
|
|
||||||
|
const value = controller.getValueOverride(SettingLevel.ACCOUNT);
|
||||||
|
expect(value).toEqual(InviteRulesConfigController.default);
|
||||||
|
});
|
||||||
|
|
||||||
|
it.each<InviteConfigAccountData>([{ blocked_users: ["foo_bar"] }, { blocked_users: [] }, {}])(
|
||||||
|
"calculates blockAll to be false",
|
||||||
|
(content: InviteConfigAccountData) => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
|
||||||
|
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT);
|
||||||
|
expect(globalValue.allBlocked).toEqual(false);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
it.each<InviteConfigAccountData>([
|
||||||
|
{ blocked_users: ["*"] },
|
||||||
|
{ blocked_users: ["*", "bob"] },
|
||||||
|
{ allowed_users: ["*"], blocked_users: ["*"] },
|
||||||
|
])("calculates blockAll to be true", (content: InviteConfigAccountData) => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
|
||||||
|
MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
content,
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
});
|
||||||
|
|
||||||
|
const globalValue = controller.getValueOverride(SettingLevel.ACCOUNT);
|
||||||
|
expect(globalValue.allBlocked).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the account data correctly for blockAll = true", async () => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
const client = (MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
content: {
|
||||||
|
existing_content: {},
|
||||||
|
allowed_servers: ["*"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setAccountData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(await controller.beforeChange(SettingLevel.ACCOUNT, null, { allBlocked: true })).toBe(true);
|
||||||
|
expect(client.setAccountData).toHaveBeenCalledWith(INVITE_RULES_ACCOUNT_DATA_TYPE, {
|
||||||
|
existing_content: {},
|
||||||
|
allowed_servers: ["*"],
|
||||||
|
blocked_users: ["*"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it("sets the account data correctly for blockAll = false", async () => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
const client = (MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
content: {
|
||||||
|
existing_content: {},
|
||||||
|
allowed_servers: ["*"],
|
||||||
|
blocked_users: ["extra_user", "*"],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setAccountData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(await controller.beforeChange(SettingLevel.ACCOUNT, null, { allBlocked: false })).toBe(true);
|
||||||
|
expect(client.setAccountData).toHaveBeenCalledWith(INVITE_RULES_ACCOUNT_DATA_TYPE, {
|
||||||
|
existing_content: {},
|
||||||
|
allowed_servers: ["*"],
|
||||||
|
blocked_users: ["extra_user"],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it.each([true, false])("ignores a no-op when allBlocked = %s", async (allBlocked) => {
|
||||||
|
const controller = new InviteRulesConfigController();
|
||||||
|
const client = (MatrixClientBackedController.matrixClient = getMockClientWithEventEmitter({
|
||||||
|
...mockClientMethodsServer(),
|
||||||
|
getAccountData: jest.fn().mockReturnValue(
|
||||||
|
new MatrixEvent({
|
||||||
|
type: INVITE_RULES_ACCOUNT_DATA_TYPE,
|
||||||
|
content: {
|
||||||
|
existing_content: {},
|
||||||
|
allowed_servers: ["*"],
|
||||||
|
blocked_users: allBlocked ? ["*"] : [],
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
setAccountData: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
expect(await controller.beforeChange(SettingLevel.ACCOUNT, null, { allBlocked })).toBe(false);
|
||||||
|
expect(client.setAccountData).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
Reference in New Issue
Block a user