Show all labs even if incompatible, with appropriate tooltip explaining requirements (#10369)
Co-authored-by: Richard van der Hoff <1389908+richvdh@users.noreply.github.com>pull/28788/head^2
parent
209b65243a
commit
e3930fb8b0
|
@ -284,7 +284,7 @@ export default class MessageContextMenu extends React.Component<IProps, IState>
|
||||||
this.closeMenu();
|
this.closeMenu();
|
||||||
};
|
};
|
||||||
|
|
||||||
private onShareClick = (e: React.MouseEvent): void => {
|
private onShareClick = (e: ButtonEvent): void => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
Modal.createDialog(ShareDialog, {
|
Modal.createDialog(ShareDialog, {
|
||||||
target: this.props.mxEvent,
|
target: this.props.mxEvent,
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { _t } from "../../../languageHandler";
|
||||||
import ToggleSwitch from "./ToggleSwitch";
|
import ToggleSwitch from "./ToggleSwitch";
|
||||||
import StyledCheckbox from "./StyledCheckbox";
|
import StyledCheckbox from "./StyledCheckbox";
|
||||||
import { SettingLevel } from "../../../settings/SettingLevel";
|
import { SettingLevel } from "../../../settings/SettingLevel";
|
||||||
|
import { defaultWatchManager } from "../../../settings/Settings";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
// The setting must be a boolean
|
// The setting must be a boolean
|
||||||
|
@ -32,14 +33,14 @@ interface IProps {
|
||||||
isExplicit?: boolean;
|
isExplicit?: boolean;
|
||||||
// XXX: once design replaces all toggles make this the default
|
// XXX: once design replaces all toggles make this the default
|
||||||
useCheckbox?: boolean;
|
useCheckbox?: boolean;
|
||||||
disabled?: boolean;
|
|
||||||
disabledDescription?: string;
|
|
||||||
hideIfCannotSet?: boolean;
|
hideIfCannotSet?: boolean;
|
||||||
onChange?(checked: boolean): void;
|
onChange?(checked: boolean): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
value: boolean;
|
value: boolean;
|
||||||
|
/** true if `SettingsStore.isEnabled` returned false. */
|
||||||
|
disabled: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class SettingsFlag extends React.Component<IProps, IState> {
|
export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
|
@ -47,19 +48,43 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
value: SettingsStore.getValueAt(
|
value: this.getSettingValue(),
|
||||||
this.props.level,
|
disabled: this.isSettingDisabled(),
|
||||||
this.props.name,
|
|
||||||
this.props.roomId,
|
|
||||||
this.props.isExplicit,
|
|
||||||
),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public componentDidMount(): void {
|
||||||
|
defaultWatchManager.watchSetting(this.props.name, this.props.roomId ?? null, this.onSettingChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
public componentWillUnmount(): void {
|
||||||
|
defaultWatchManager.unwatchSetting(this.onSettingChange);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getSettingValue(): boolean {
|
||||||
|
return SettingsStore.getValueAt(
|
||||||
|
this.props.level,
|
||||||
|
this.props.name,
|
||||||
|
this.props.roomId ?? null,
|
||||||
|
this.props.isExplicit,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private isSettingDisabled(): boolean {
|
||||||
|
return !SettingsStore.isEnabled(this.props.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onSettingChange = (): void => {
|
||||||
|
this.setState({
|
||||||
|
value: this.getSettingValue(),
|
||||||
|
disabled: this.isSettingDisabled(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
private onChange = async (checked: boolean): Promise<void> => {
|
private onChange = async (checked: boolean): Promise<void> => {
|
||||||
await this.save(checked);
|
await this.save(checked);
|
||||||
this.setState({ value: checked });
|
this.setState({ value: checked });
|
||||||
if (this.props.onChange) this.props.onChange(checked);
|
this.props.onChange?.(checked);
|
||||||
};
|
};
|
||||||
|
|
||||||
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private checkBoxOnChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
|
@ -86,19 +111,11 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
|
: SettingsStore.getDisplayName(this.props.name, this.props.level)) ?? undefined;
|
||||||
const description = SettingsStore.getDescription(this.props.name);
|
const description = SettingsStore.getDescription(this.props.name);
|
||||||
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
const shouldWarn = SettingsStore.shouldHaveWarning(this.props.name);
|
||||||
|
const disabled = this.state.disabled || !canChange;
|
||||||
let disabledDescription: JSX.Element | null = null;
|
|
||||||
if (this.props.disabled && this.props.disabledDescription) {
|
|
||||||
disabledDescription = <div className="mx_SettingsFlag_microcopy">{this.props.disabledDescription}</div>;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.props.useCheckbox) {
|
if (this.props.useCheckbox) {
|
||||||
return (
|
return (
|
||||||
<StyledCheckbox
|
<StyledCheckbox checked={this.state.value} onChange={this.checkBoxOnChange} disabled={disabled}>
|
||||||
checked={this.state.value}
|
|
||||||
onChange={this.checkBoxOnChange}
|
|
||||||
disabled={this.props.disabled || !canChange}
|
|
||||||
>
|
|
||||||
{label}
|
{label}
|
||||||
</StyledCheckbox>
|
</StyledCheckbox>
|
||||||
);
|
);
|
||||||
|
@ -117,18 +134,18 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
|
||||||
w: (sub) => (
|
w: (sub) => (
|
||||||
<span className="mx_SettingsTab_microcopy_warning">{sub}</span>
|
<span className="mx_SettingsTab_microcopy_warning">{sub}</span>
|
||||||
),
|
),
|
||||||
description: description,
|
description,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
: description}
|
: description}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{disabledDescription}
|
|
||||||
</label>
|
</label>
|
||||||
<ToggleSwitch
|
<ToggleSwitch
|
||||||
checked={this.state.value}
|
checked={this.state.value}
|
||||||
onChange={this.onChange}
|
onChange={this.onChange}
|
||||||
disabled={this.props.disabled || !canChange}
|
disabled={disabled}
|
||||||
|
tooltip={disabled ? SettingsStore.disabledMessage(this.props.name) : undefined}
|
||||||
title={label}
|
title={label}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -23,41 +23,18 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import BetaCard from "../../../beta/BetaCard";
|
import BetaCard from "../../../beta/BetaCard";
|
||||||
import SettingsFlag from "../../../elements/SettingsFlag";
|
import SettingsFlag from "../../../elements/SettingsFlag";
|
||||||
import { defaultWatchManager, LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
|
||||||
import { EnhancedMap } from "../../../../../utils/maps";
|
import { EnhancedMap } from "../../../../../utils/maps";
|
||||||
import { arrayHasDiff } from "../../../../../utils/arrays";
|
|
||||||
|
|
||||||
interface State {
|
export default class LabsUserSettingsTab extends React.Component<{}> {
|
||||||
labs: string[];
|
private readonly labs: string[];
|
||||||
betas: string[];
|
private readonly betas: string[];
|
||||||
}
|
|
||||||
|
|
||||||
export default class LabsUserSettingsTab extends React.Component<{}, State> {
|
|
||||||
private readonly features = SettingsStore.getFeatureSettingNames();
|
|
||||||
|
|
||||||
public constructor(props: {}) {
|
public constructor(props: {}) {
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
const features = SettingsStore.getFeatureSettingNames();
|
||||||
betas: [],
|
const [labs, betas] = features.reduce(
|
||||||
labs: [],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentDidMount(): void {
|
|
||||||
this.features.forEach((feature) => {
|
|
||||||
defaultWatchManager.watchSetting(feature, null, this.onChange);
|
|
||||||
});
|
|
||||||
this.onChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
public componentWillUnmount(): void {
|
|
||||||
defaultWatchManager.unwatchSetting(this.onChange);
|
|
||||||
}
|
|
||||||
|
|
||||||
private onChange = (): void => {
|
|
||||||
const features = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.isEnabled(f));
|
|
||||||
const [_labs, betas] = features.reduce(
|
|
||||||
(arr, f) => {
|
(arr, f) => {
|
||||||
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
|
||||||
return arr;
|
return arr;
|
||||||
|
@ -65,18 +42,20 @@ export default class LabsUserSettingsTab extends React.Component<{}, State> {
|
||||||
[[], []] as [string[], string[]],
|
[[], []] as [string[], string[]],
|
||||||
);
|
);
|
||||||
|
|
||||||
const labs = SdkConfig.get("show_labs_settings") ? _labs : [];
|
this.labs = labs;
|
||||||
if (arrayHasDiff(labs, this.state.labs) || arrayHasDiff(betas, this.state.betas)) {
|
this.betas = betas;
|
||||||
this.setState({ labs, betas });
|
|
||||||
|
if (!SdkConfig.get("show_labs_settings")) {
|
||||||
|
this.labs = [];
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
|
||||||
|
|
||||||
public render(): React.ReactNode {
|
public render(): React.ReactNode {
|
||||||
let betaSection: JSX.Element | undefined;
|
let betaSection: JSX.Element | undefined;
|
||||||
if (this.state.betas.length) {
|
if (this.betas.length) {
|
||||||
betaSection = (
|
betaSection = (
|
||||||
<div data-testid="labs-beta-section" className="mx_SettingsTab_section">
|
<div data-testid="labs-beta-section" className="mx_SettingsTab_section">
|
||||||
{this.state.betas.map((f) => (
|
{this.betas.map((f) => (
|
||||||
<BetaCard key={f} featureId={f} />
|
<BetaCard key={f} featureId={f} />
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
|
@ -84,9 +63,9 @@ export default class LabsUserSettingsTab extends React.Component<{}, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
let labsSections: JSX.Element | undefined;
|
let labsSections: JSX.Element | undefined;
|
||||||
if (this.state.labs.length) {
|
if (this.labs.length) {
|
||||||
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
|
||||||
this.state.labs.forEach((f) => {
|
this.labs.forEach((f) => {
|
||||||
groups
|
groups
|
||||||
.getOrCreate(SettingsStore.getLabGroup(f), [])
|
.getOrCreate(SettingsStore.getLabGroup(f), [])
|
||||||
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);
|
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);
|
||||||
|
|
|
@ -29,7 +29,6 @@ import { UserTab } from "../../../dialogs/UserTab";
|
||||||
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
|
import { OpenToTabPayload } from "../../../../../dispatcher/payloads/OpenToTabPayload";
|
||||||
import { Action } from "../../../../../dispatcher/actions";
|
import { Action } from "../../../../../dispatcher/actions";
|
||||||
import SdkConfig from "../../../../../SdkConfig";
|
import SdkConfig from "../../../../../SdkConfig";
|
||||||
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
|
|
||||||
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
|
import { showUserOnboardingPage } from "../../../user-onboarding/UserOnboardingPage";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
|
@ -37,7 +36,6 @@ interface IProps {
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IState {
|
interface IState {
|
||||||
disablingReadReceiptsSupported: boolean;
|
|
||||||
autocompleteDelay: string;
|
autocompleteDelay: string;
|
||||||
readMarkerInViewThresholdMs: string;
|
readMarkerInViewThresholdMs: string;
|
||||||
readMarkerOutOfViewThresholdMs: string;
|
readMarkerOutOfViewThresholdMs: string;
|
||||||
|
@ -50,10 +48,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
|
|
||||||
private static KEYBINDINGS_SETTINGS = ["ctrlFForSearch"];
|
private static KEYBINDINGS_SETTINGS = ["ctrlFForSearch"];
|
||||||
|
|
||||||
private static PRESENCE_SETTINGS = [
|
private static PRESENCE_SETTINGS = ["sendReadReceipts", "sendTypingNotifications"];
|
||||||
"sendTypingNotifications",
|
|
||||||
// sendReadReceipts - handled specially due to server needing support
|
|
||||||
];
|
|
||||||
|
|
||||||
private static COMPOSER_SETTINGS = [
|
private static COMPOSER_SETTINGS = [
|
||||||
"MessageComposerInput.autoReplaceEmoji",
|
"MessageComposerInput.autoReplaceEmoji",
|
||||||
|
@ -101,7 +96,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
super(props);
|
super(props);
|
||||||
|
|
||||||
this.state = {
|
this.state = {
|
||||||
disablingReadReceiptsSupported: false,
|
|
||||||
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
|
autocompleteDelay: SettingsStore.getValueAt(SettingLevel.DEVICE, "autocompleteDelay").toString(10),
|
||||||
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
|
readMarkerInViewThresholdMs: SettingsStore.getValueAt(
|
||||||
SettingLevel.DEVICE,
|
SettingLevel.DEVICE,
|
||||||
|
@ -114,16 +108,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async componentDidMount(): Promise<void> {
|
|
||||||
const cli = MatrixClientPeg.get();
|
|
||||||
|
|
||||||
this.setState({
|
|
||||||
disablingReadReceiptsSupported:
|
|
||||||
(await cli.doesServerSupportUnstableFeature("org.matrix.msc2285.stable")) ||
|
|
||||||
(await cli.isVersionSupported("v1.4")),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>): void => {
|
||||||
this.setState({ autocompleteDelay: e.target.value });
|
this.setState({ autocompleteDelay: e.target.value });
|
||||||
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
|
||||||
|
@ -140,10 +124,7 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
};
|
};
|
||||||
|
|
||||||
private renderGroup(settingIds: string[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
|
private renderGroup(settingIds: string[], level = SettingLevel.ACCOUNT): React.ReactNodeArray {
|
||||||
return settingIds.map((i) => {
|
return settingIds.map((i) => <SettingsFlag key={i} name={i} level={level} />);
|
||||||
const disabled = !SettingsStore.isEnabled(i);
|
|
||||||
return <SettingsFlag key={i} name={i} level={level} disabled={disabled} />;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private onKeyboardShortcutsClicked = (): void => {
|
private onKeyboardShortcutsClicked = (): void => {
|
||||||
|
@ -205,14 +186,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
|
||||||
<span className="mx_SettingsTab_subsectionText">
|
<span className="mx_SettingsTab_subsectionText">
|
||||||
{_t("Share your activity and status with others.")}
|
{_t("Share your activity and status with others.")}
|
||||||
</span>
|
</span>
|
||||||
<SettingsFlag
|
|
||||||
disabled={
|
|
||||||
!this.state.disablingReadReceiptsSupported && SettingsStore.getValue("sendReadReceipts") // Make sure the feature can always be enabled
|
|
||||||
}
|
|
||||||
disabledDescription={_t("Your server doesn't support disabling sending read receipts.")}
|
|
||||||
name="sendReadReceipts"
|
|
||||||
level={SettingLevel.ACCOUNT}
|
|
||||||
/>
|
|
||||||
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
|
{this.renderGroup(PreferencesUserSettingsTab.PRESENCE_SETTINGS)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -939,6 +939,7 @@
|
||||||
"Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.",
|
"Yes, the chat timeline is displayed alongside the video.": "Yes, the chat timeline is displayed alongside the video.",
|
||||||
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
"Thank you for trying the beta, please go into as much detail as you can so we can improve it.": "Thank you for trying the beta, please go into as much detail as you can so we can improve it.",
|
||||||
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
|
"Explore public spaces in the new search dialog": "Explore public spaces in the new search dialog",
|
||||||
|
"Requires your server to support the stable version of MSC3827": "Requires your server to support the stable version of MSC3827",
|
||||||
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
"Let moderators hide messages pending moderation.": "Let moderators hide messages pending moderation.",
|
||||||
"Report to moderators": "Report to moderators",
|
"Report to moderators": "Report to moderators",
|
||||||
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
"In rooms that support moderation, the “Report” button will let you report abuse to room moderators.": "In rooms that support moderation, the “Report” button will let you report abuse to room moderators.",
|
||||||
|
@ -962,7 +963,9 @@
|
||||||
"Polls history": "Polls history",
|
"Polls history": "Polls history",
|
||||||
"View a list of polls in a room. (Under active development)": "View a list of polls in a room. (Under active development)",
|
"View a list of polls in a room. (Under active development)": "View a list of polls in a room. (Under active development)",
|
||||||
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
"Jump to date (adds /jumptodate and jump to date headers)": "Jump to date (adds /jumptodate and jump to date headers)",
|
||||||
|
"Requires your server to support MSC3030": "Requires your server to support MSC3030",
|
||||||
"Send read receipts": "Send read receipts",
|
"Send read receipts": "Send read receipts",
|
||||||
|
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
|
||||||
"Sliding Sync mode": "Sliding Sync mode",
|
"Sliding Sync mode": "Sliding Sync mode",
|
||||||
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
|
"Under active development, cannot be disabled.": "Under active development, cannot be disabled.",
|
||||||
"Element Call video rooms": "Element Call video rooms",
|
"Element Call video rooms": "Element Call video rooms",
|
||||||
|
@ -979,7 +982,6 @@
|
||||||
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
|
"Have greater visibility and control over all your sessions.": "Have greater visibility and control over all your sessions.",
|
||||||
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.",
|
"Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.": "Our new sessions manager provides better visibility of all your sessions, and greater control over them including the ability to remotely toggle push notifications.",
|
||||||
"Rust cryptography implementation": "Rust cryptography implementation",
|
"Rust cryptography implementation": "Rust cryptography implementation",
|
||||||
"Under active development. Can currently only be enabled via config.json": "Under active development. Can currently only be enabled via config.json",
|
|
||||||
"Font size": "Font size",
|
"Font size": "Font size",
|
||||||
"Use custom size": "Use custom size",
|
"Use custom size": "Use custom size",
|
||||||
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
"Enable Emoji suggestions while typing": "Enable Emoji suggestions while typing",
|
||||||
|
@ -1056,6 +1058,8 @@
|
||||||
"Always show the window menu bar": "Always show the window menu bar",
|
"Always show the window menu bar": "Always show the window menu bar",
|
||||||
"Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close",
|
"Show tray icon and minimise window to it on close": "Show tray icon and minimise window to it on close",
|
||||||
"Enable hardware acceleration": "Enable hardware acceleration",
|
"Enable hardware acceleration": "Enable hardware acceleration",
|
||||||
|
"Can currently only be enabled via config.json": "Can currently only be enabled via config.json",
|
||||||
|
"Log out and back in to disable": "Log out and back in to disable",
|
||||||
"Collecting app version information": "Collecting app version information",
|
"Collecting app version information": "Collecting app version information",
|
||||||
"Collecting logs": "Collecting logs",
|
"Collecting logs": "Collecting logs",
|
||||||
"Uploading logs": "Uploading logs",
|
"Uploading logs": "Uploading logs",
|
||||||
|
@ -1621,7 +1625,6 @@
|
||||||
"Displaying time": "Displaying time",
|
"Displaying time": "Displaying time",
|
||||||
"Presence": "Presence",
|
"Presence": "Presence",
|
||||||
"Share your activity and status with others.": "Share your activity and status with others.",
|
"Share your activity and status with others.": "Share your activity and status with others.",
|
||||||
"Your server doesn't support disabling sending read receipts.": "Your server doesn't support disabling sending read receipts.",
|
|
||||||
"Composer": "Composer",
|
"Composer": "Composer",
|
||||||
"Code blocks": "Code blocks",
|
"Code blocks": "Code blocks",
|
||||||
"Images, GIFs and videos": "Images, GIFs and videos",
|
"Images, GIFs and videos": "Images, GIFs and videos",
|
||||||
|
|
|
@ -227,9 +227,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
displayName: _td("Explore public spaces in the new search dialog"),
|
displayName: _td("Explore public spaces in the new search dialog"),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new ServerSupportUnstableFeatureController("feature_exploring_public_spaces", defaultWatchManager, [
|
controller: new ServerSupportUnstableFeatureController(
|
||||||
"org.matrix.msc3827.stable",
|
"feature_exploring_public_spaces",
|
||||||
]),
|
defaultWatchManager,
|
||||||
|
["org.matrix.msc3827.stable"],
|
||||||
|
undefined,
|
||||||
|
_td("Requires your server to support the stable version of MSC3827"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"feature_msc3531_hide_messages_pending_moderation": {
|
"feature_msc3531_hide_messages_pending_moderation": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
|
@ -373,9 +377,13 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
displayName: _td("Jump to date (adds /jumptodate and jump to date headers)"),
|
displayName: _td("Jump to date (adds /jumptodate and jump to date headers)"),
|
||||||
supportedLevels: LEVELS_FEATURE,
|
supportedLevels: LEVELS_FEATURE,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new ServerSupportUnstableFeatureController("feature_jump_to_date", defaultWatchManager, [
|
controller: new ServerSupportUnstableFeatureController(
|
||||||
"org.matrix.msc3030",
|
"feature_jump_to_date",
|
||||||
]),
|
defaultWatchManager,
|
||||||
|
["org.matrix.msc3030"],
|
||||||
|
undefined,
|
||||||
|
_td("Requires your server to support MSC3030"),
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"RoomList.backgroundImage": {
|
"RoomList.backgroundImage": {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
|
@ -385,6 +393,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
|
||||||
displayName: _td("Send read receipts"),
|
displayName: _td("Send read receipts"),
|
||||||
default: true,
|
default: true,
|
||||||
|
controller: new ServerSupportUnstableFeatureController(
|
||||||
|
"sendReadReceipts",
|
||||||
|
defaultWatchManager,
|
||||||
|
["org.matrix.msc2285.stable"],
|
||||||
|
"v1.4",
|
||||||
|
_td("Your server doesn't support disabling sending read receipts."),
|
||||||
|
true,
|
||||||
|
),
|
||||||
},
|
},
|
||||||
"feature_sliding_sync": {
|
"feature_sliding_sync": {
|
||||||
isFeature: true,
|
isFeature: true,
|
||||||
|
@ -482,7 +498,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
|
||||||
labsGroup: LabGroup.Developer,
|
labsGroup: LabGroup.Developer,
|
||||||
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
|
||||||
displayName: _td("Rust cryptography implementation"),
|
displayName: _td("Rust cryptography implementation"),
|
||||||
description: _td("Under active development. Can currently only be enabled via config.json"),
|
description: _td("Under active development."),
|
||||||
// shouldWarn: true,
|
// shouldWarn: true,
|
||||||
default: false,
|
default: false,
|
||||||
controller: new RustCryptoSdkController(),
|
controller: new RustCryptoSdkController(),
|
||||||
|
|
|
@ -325,7 +325,9 @@ export default class SettingsStore {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Determines if a setting is enabled.
|
* Determines if a setting is enabled.
|
||||||
* If a setting is disabled then it should be hidden from the user.
|
* If a setting is disabled then it should normally be hidden from the user to de-clutter the user interface.
|
||||||
|
* This rule is intentionally ignored for labs flags to unveil what features are available with
|
||||||
|
* the right server support.
|
||||||
* @param {string} settingName The setting to look up.
|
* @param {string} settingName The setting to look up.
|
||||||
* @return {boolean} True if the setting is enabled.
|
* @return {boolean} True if the setting is enabled.
|
||||||
*/
|
*/
|
||||||
|
@ -334,6 +336,18 @@ export default class SettingsStore {
|
||||||
return !SETTINGS[settingName].controller?.settingDisabled ?? true;
|
return !SETTINGS[settingName].controller?.settingDisabled ?? true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Retrieves the reason a setting is disabled if one is assigned.
|
||||||
|
* If a setting is not disabled, or no reason is given by the `SettingController`,
|
||||||
|
* this will return undefined.
|
||||||
|
* @param {string} settingName The setting to look up.
|
||||||
|
* @return {string} The reason the setting is disabled.
|
||||||
|
*/
|
||||||
|
public static disabledMessage(settingName: string): string | undefined {
|
||||||
|
const disabled = SETTINGS[settingName].controller?.settingDisabled;
|
||||||
|
return typeof disabled === "string" ? disabled : undefined;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the value of a setting. The room ID is optional if the setting is not to
|
* Gets the value of a setting. The room ID is optional if the setting is not to
|
||||||
* be applied to any particular room, otherwise it should be supplied.
|
* be applied to any particular room, otherwise it should be supplied.
|
||||||
|
|
|
@ -14,12 +14,13 @@ See the License for the specific language governing permissions and
|
||||||
limitations under the License.
|
limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
import SettingController from "./SettingController";
|
import SettingController from "./SettingController";
|
||||||
|
|
||||||
export default class RustCryptoSdkController extends SettingController {
|
export default class RustCryptoSdkController extends SettingController {
|
||||||
public get settingDisabled(): boolean {
|
public get settingDisabled(): boolean | string {
|
||||||
// Currently this can only be changed via config.json. In future, we'll allow the user to *enable* this setting
|
// Currently this can only be changed via config.json. In future, we'll allow the user to *enable* this setting
|
||||||
// via labs, which will migrate their existing device to the rust-sdk implementation.
|
// via labs, which will migrate their existing device to the rust-sdk implementation.
|
||||||
return true;
|
return _t("Can currently only be enabled via config.json");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -32,6 +32,8 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||||
private readonly settingName: string,
|
private readonly settingName: string,
|
||||||
private readonly watchers: WatchManager,
|
private readonly watchers: WatchManager,
|
||||||
private readonly unstableFeatures: string[],
|
private readonly unstableFeatures: string[],
|
||||||
|
private readonly stableVersion?: string,
|
||||||
|
private readonly disabledMessage?: string,
|
||||||
private readonly forcedValue: any = false,
|
private readonly forcedValue: any = false,
|
||||||
) {
|
) {
|
||||||
super();
|
super();
|
||||||
|
@ -53,10 +55,16 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||||
protected async initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): Promise<void> {
|
protected async initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): Promise<void> {
|
||||||
this.disabled = true;
|
this.disabled = true;
|
||||||
let supported = true;
|
let supported = true;
|
||||||
|
|
||||||
|
if (this.stableVersion && (await this.client.isVersionSupported(this.stableVersion))) {
|
||||||
|
this.disabled = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
for (const feature of this.unstableFeatures) {
|
for (const feature of this.unstableFeatures) {
|
||||||
supported = await this.client.doesServerSupportUnstableFeature(feature);
|
supported = await this.client.doesServerSupportUnstableFeature(feature);
|
||||||
if (!supported) break;
|
if (!supported) break;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.disabled = !supported;
|
this.disabled = !supported;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -72,7 +80,10 @@ export default class ServerSupportUnstableFeatureController extends MatrixClient
|
||||||
return null; // no override
|
return null; // no override
|
||||||
}
|
}
|
||||||
|
|
||||||
public get settingDisabled(): boolean {
|
public get settingDisabled(): boolean | string {
|
||||||
return this.disabled;
|
if (this.disabled) {
|
||||||
|
return this.disabledMessage ?? true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -69,8 +69,9 @@ export default abstract class SettingController {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets whether the setting has been disabled due to this controller.
|
* Gets whether the setting has been disabled due to this controller.
|
||||||
|
* Can also return a string with the reason the setting is disabled.
|
||||||
*/
|
*/
|
||||||
public get settingDisabled(): boolean {
|
public get settingDisabled(): boolean | string {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,6 +20,7 @@ import { SettingLevel } from "../SettingLevel";
|
||||||
import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
|
import { SlidingSyncOptionsDialog } from "../../components/views/dialogs/SlidingSyncOptionsDialog";
|
||||||
import Modal from "../../Modal";
|
import Modal from "../../Modal";
|
||||||
import SettingsStore from "../SettingsStore";
|
import SettingsStore from "../SettingsStore";
|
||||||
|
import { _t } from "../../languageHandler";
|
||||||
|
|
||||||
export default class SlidingSyncController extends SettingController {
|
export default class SlidingSyncController extends SettingController {
|
||||||
public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
|
public async beforeChange(level: SettingLevel, roomId: string, newValue: any): Promise<boolean> {
|
||||||
|
@ -32,8 +33,12 @@ export default class SlidingSyncController extends SettingController {
|
||||||
PlatformPeg.get()?.reload();
|
PlatformPeg.get()?.reload();
|
||||||
}
|
}
|
||||||
|
|
||||||
public get settingDisabled(): boolean {
|
public get settingDisabled(): boolean | string {
|
||||||
// Cannot be disabled once enabled, user has been warned and must log out and back in.
|
// Cannot be disabled once enabled, user has been warned and must log out and back in.
|
||||||
return SettingsStore.getValue("feature_sliding_sync");
|
if (SettingsStore.getValue("feature_sliding_sync")) {
|
||||||
|
return _t("Log out and back in to disable");
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,7 +15,7 @@ limitations under the License.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import React from "react";
|
import React from "react";
|
||||||
import { render } from "@testing-library/react";
|
import { render, waitFor } from "@testing-library/react";
|
||||||
import { defer } from "matrix-js-sdk/src/utils";
|
import { defer } from "matrix-js-sdk/src/utils";
|
||||||
|
|
||||||
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
|
import LabsUserSettingsTab from "../../../../../../src/components/views/settings/tabs/user/LabsUserSettingsTab";
|
||||||
|
@ -28,7 +28,7 @@ import {
|
||||||
import SdkConfig from "../../../../../../src/SdkConfig";
|
import SdkConfig from "../../../../../../src/SdkConfig";
|
||||||
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
|
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
|
||||||
|
|
||||||
describe("<SecurityUserSettingsTab />", () => {
|
describe("<LabsUserSettingsTab />", () => {
|
||||||
const sdkConfigSpy = jest.spyOn(SdkConfig, "get");
|
const sdkConfigSpy = jest.spyOn(SdkConfig, "get");
|
||||||
|
|
||||||
const defaultProps = {
|
const defaultProps = {
|
||||||
|
@ -70,10 +70,10 @@ describe("<SecurityUserSettingsTab />", () => {
|
||||||
const { container } = render(getComponent());
|
const { container } = render(getComponent());
|
||||||
|
|
||||||
const labsSections = container.getElementsByClassName("mx_SettingsTab_section");
|
const labsSections = container.getElementsByClassName("mx_SettingsTab_section");
|
||||||
expect(labsSections.length).toEqual(11);
|
expect(labsSections).toHaveLength(12);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("renders a labs flag which requires unstable support once support is confirmed", async () => {
|
it("allow setting a labs flag which requires unstable support once support is confirmed", async () => {
|
||||||
// enable labs
|
// enable labs
|
||||||
sdkConfigSpy.mockImplementation((configName) => configName === "show_labs_settings");
|
sdkConfigSpy.mockImplementation((configName) => configName === "show_labs_settings");
|
||||||
|
|
||||||
|
@ -83,10 +83,20 @@ describe("<SecurityUserSettingsTab />", () => {
|
||||||
});
|
});
|
||||||
MatrixClientBackedController.matrixClient = cli;
|
MatrixClientBackedController.matrixClient = cli;
|
||||||
|
|
||||||
const { queryByText, findByText } = render(getComponent());
|
const { queryByText } = render(getComponent());
|
||||||
|
|
||||||
expect(queryByText("Explore public spaces in the new search dialog")).toBeFalsy();
|
expect(
|
||||||
|
queryByText("Explore public spaces in the new search dialog")!
|
||||||
|
.closest(".mx_SettingsFlag")!
|
||||||
|
.querySelector(".mx_AccessibleButton"),
|
||||||
|
).toHaveAttribute("aria-disabled", "true");
|
||||||
deferred.resolve(true);
|
deferred.resolve(true);
|
||||||
await expect(findByText("Explore public spaces in the new search dialog")).resolves.toBeDefined();
|
await waitFor(() => {
|
||||||
|
expect(
|
||||||
|
queryByText("Explore public spaces in the new search dialog")!
|
||||||
|
.closest(".mx_SettingsFlag")!
|
||||||
|
.querySelector(".mx_AccessibleButton"),
|
||||||
|
).toHaveAttribute("aria-disabled", "false");
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
|
@ -22,6 +22,7 @@ import { MatrixClientPeg } from "../../../../../../src/MatrixClientPeg";
|
||||||
import { mockPlatformPeg, stubClient } from "../../../../../test-utils";
|
import { mockPlatformPeg, stubClient } from "../../../../../test-utils";
|
||||||
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
import SettingsStore from "../../../../../../src/settings/SettingsStore";
|
||||||
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
import { SettingLevel } from "../../../../../../src/settings/SettingLevel";
|
||||||
|
import MatrixClientBackedController from "../../../../../../src/settings/controllers/MatrixClientBackedController";
|
||||||
|
|
||||||
describe("PreferencesUserSettingsTab", () => {
|
describe("PreferencesUserSettingsTab", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -36,6 +37,7 @@ describe("PreferencesUserSettingsTab", () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
stubClient();
|
stubClient();
|
||||||
jest.spyOn(SettingsStore, "setValue");
|
jest.spyOn(SettingsStore, "setValue");
|
||||||
|
jest.spyOn(SettingsStore, "canSetValue").mockReturnValue(true);
|
||||||
jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as MediaQueryList);
|
jest.spyOn(window, "matchMedia").mockReturnValue({ matches: false } as MediaQueryList);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -47,10 +49,12 @@ describe("PreferencesUserSettingsTab", () => {
|
||||||
|
|
||||||
const mockIsVersionSupported = (val: boolean) => {
|
const mockIsVersionSupported = (val: boolean) => {
|
||||||
const client = MatrixClientPeg.get();
|
const client = MatrixClientPeg.get();
|
||||||
|
jest.spyOn(client, "doesServerSupportUnstableFeature").mockResolvedValue(false);
|
||||||
jest.spyOn(client, "isVersionSupported").mockImplementation(async (version: string) => {
|
jest.spyOn(client, "isVersionSupported").mockImplementation(async (version: string) => {
|
||||||
if (version === "v1.4") return val;
|
if (version === "v1.4") return val;
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
MatrixClientBackedController.matrixClient = client;
|
||||||
};
|
};
|
||||||
|
|
||||||
const mockGetValue = (val: boolean) => {
|
const mockGetValue = (val: boolean) => {
|
||||||
|
@ -98,13 +102,12 @@ describe("PreferencesUserSettingsTab", () => {
|
||||||
mockIsVersionSupported(false);
|
mockIsVersionSupported(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
it("can be enabled", async () => {
|
it("is forcibly enabled", async () => {
|
||||||
mockGetValue(false);
|
|
||||||
const toggle = getToggle();
|
const toggle = getToggle();
|
||||||
|
await waitFor(() => {
|
||||||
await waitFor(() => expect(toggle).toHaveAttribute("aria-disabled", "false"));
|
expect(toggle).toHaveAttribute("aria-checked", "true");
|
||||||
fireEvent.click(toggle);
|
expect(toggle).toHaveAttribute("aria-disabled", "true");
|
||||||
expectSetValueToHaveBeenCalled("sendReadReceipts", null, SettingLevel.ACCOUNT, true);
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("cannot be disabled", async () => {
|
it("cannot be disabled", async () => {
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||||
|
|
||||||
exports[`<SecurityUserSettingsTab /> renders settings marked as beta as beta cards 1`] = `
|
exports[`<LabsUserSettingsTab /> renders settings marked as beta as beta cards 1`] = `
|
||||||
<div
|
<div
|
||||||
class="mx_SettingsTab_section"
|
class="mx_SettingsTab_section"
|
||||||
data-testid="labs-beta-section"
|
data-testid="labs-beta-section"
|
||||||
|
|
|
@ -56,6 +56,8 @@ describe("ServerSupportUnstableFeatureController", () => {
|
||||||
setting,
|
setting,
|
||||||
watchers,
|
watchers,
|
||||||
["feature"],
|
["feature"],
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
"other_value",
|
"other_value",
|
||||||
);
|
);
|
||||||
await prepareSetting(cli, controller);
|
await prepareSetting(cli, controller);
|
||||||
|
|
|
@ -126,6 +126,7 @@ export const mockClientMethodsServer = (): Partial<Record<MethodLikeKeys<MatrixC
|
||||||
getCapabilities: jest.fn().mockReturnValue({}),
|
getCapabilities: jest.fn().mockReturnValue({}),
|
||||||
getClientWellKnown: jest.fn().mockReturnValue({}),
|
getClientWellKnown: jest.fn().mockReturnValue({}),
|
||||||
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
doesServerSupportUnstableFeature: jest.fn().mockResolvedValue(false),
|
||||||
|
isVersionSupported: jest.fn().mockResolvedValue(false),
|
||||||
getVersions: jest.fn().mockResolvedValue({}),
|
getVersions: jest.fn().mockResolvedValue({}),
|
||||||
isFallbackICEServerAllowed: jest.fn(),
|
isFallbackICEServerAllowed: jest.fn(),
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue