Skip to content

Commit ad8d27d

Browse files
authored
Fix some features not being configurable via features (matrix-org#10276)
1 parent 6746ce2 commit ad8d27d

File tree

12 files changed

+328
-60
lines changed

12 files changed

+328
-60
lines changed

src/MatrixClientPeg.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import { SlidingSyncManager } from "./SlidingSyncManager";
4040
import CryptoStoreTooNewDialog from "./components/views/dialogs/CryptoStoreTooNewDialog";
4141
import { _t } from "./languageHandler";
4242
import { SettingLevel } from "./settings/SettingLevel";
43+
import MatrixClientBackedController from "./settings/controllers/MatrixClientBackedController";
4344

4445
export interface IMatrixClientCreds {
4546
homeserverUrl: string;
@@ -237,6 +238,7 @@ class MatrixClientPegClass implements IMatrixClientPeg {
237238
// Connect the matrix client to the dispatcher and setting handlers
238239
MatrixActionCreators.start(this.matrixClient);
239240
MatrixClientBackedSettingsHandler.matrixClient = this.matrixClient;
241+
MatrixClientBackedController.matrixClient = this.matrixClient;
240242

241243
return opts;
242244
}

src/components/views/settings/tabs/user/LabsUserSettingsTab.tsx

Lines changed: 36 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -23,60 +23,70 @@ import { SettingLevel } from "../../../../../settings/SettingLevel";
2323
import SdkConfig from "../../../../../SdkConfig";
2424
import BetaCard from "../../../beta/BetaCard";
2525
import SettingsFlag from "../../../elements/SettingsFlag";
26-
import { MatrixClientPeg } from "../../../../../MatrixClientPeg";
27-
import { LabGroup, labGroupNames } from "../../../../../settings/Settings";
26+
import { defaultWatchManager, LabGroup, labGroupNames } from "../../../../../settings/Settings";
2827
import { EnhancedMap } from "../../../../../utils/maps";
28+
import { arrayHasDiff } from "../../../../../utils/arrays";
2929

30-
interface IState {
31-
showJumpToDate: boolean;
32-
showExploringPublicSpaces: boolean;
30+
interface State {
31+
labs: string[];
32+
betas: string[];
3333
}
3434

35-
export default class LabsUserSettingsTab extends React.Component<{}, IState> {
35+
export default class LabsUserSettingsTab extends React.Component<{}, State> {
36+
private readonly features = SettingsStore.getFeatureSettingNames();
37+
3638
public constructor(props: {}) {
3739
super(props);
3840

39-
const cli = MatrixClientPeg.get();
40-
41-
cli.doesServerSupportUnstableFeature("org.matrix.msc3030").then((showJumpToDate) => {
42-
this.setState({ showJumpToDate });
43-
});
41+
this.state = {
42+
betas: [],
43+
labs: [],
44+
};
45+
}
4446

45-
cli.doesServerSupportUnstableFeature("org.matrix.msc3827.stable").then((showExploringPublicSpaces) => {
46-
this.setState({ showExploringPublicSpaces });
47+
public componentDidMount(): void {
48+
this.features.forEach((feature) => {
49+
defaultWatchManager.watchSetting(feature, null, this.onChange);
4750
});
51+
this.onChange();
52+
}
4853

49-
this.state = {
50-
showJumpToDate: false,
51-
showExploringPublicSpaces: false,
52-
};
54+
public componentWillUnmount(): void {
55+
defaultWatchManager.unwatchSetting(this.onChange);
5356
}
5457

55-
public render(): React.ReactNode {
56-
const features = SettingsStore.getFeatureSettingNames();
57-
const [labs, betas] = features.reduce(
58+
private onChange = (): void => {
59+
const features = SettingsStore.getFeatureSettingNames().filter((f) => SettingsStore.isEnabled(f));
60+
const [_labs, betas] = features.reduce(
5861
(arr, f) => {
5962
arr[SettingsStore.getBetaInfo(f) ? 1 : 0].push(f);
6063
return arr;
6164
},
6265
[[], []] as [string[], string[]],
6366
);
6467

65-
let betaSection;
66-
if (betas.length) {
68+
const labs = SdkConfig.get("show_labs_settings") ? _labs : [];
69+
if (arrayHasDiff(labs, this.state.labs) || arrayHasDiff(betas, this.state.betas)) {
70+
this.setState({ labs, betas });
71+
}
72+
};
73+
74+
public render(): React.ReactNode {
75+
let betaSection: JSX.Element | undefined;
76+
if (this.state.betas.length) {
6777
betaSection = (
6878
<div data-testid="labs-beta-section" className="mx_SettingsTab_section">
69-
{betas.map((f) => (
79+
{this.state.betas.map((f) => (
7080
<BetaCard key={f} featureId={f} />
7181
))}
7282
</div>
7383
);
7484
}
7585

76-
let labsSections;
77-
if (SdkConfig.get("show_labs_settings")) {
86+
let labsSections: JSX.Element | undefined;
87+
if (this.state.labs.length) {
7888
const groups = new EnhancedMap<LabGroup, JSX.Element[]>();
79-
labs.forEach((f) => {
89+
this.state.labs.forEach((f) => {
8090
groups
8191
.getOrCreate(SettingsStore.getLabGroup(f), [])
8292
.push(<SettingsFlag level={SettingLevel.DEVICE} name={f} key={f} />);
@@ -101,30 +111,6 @@ export default class LabsUserSettingsTab extends React.Component<{}, IState> {
101111
/>,
102112
);
103113

104-
if (this.state.showJumpToDate) {
105-
groups
106-
.getOrCreate(LabGroup.Messaging, [])
107-
.push(
108-
<SettingsFlag
109-
key="feature_jump_to_date"
110-
name="feature_jump_to_date"
111-
level={SettingLevel.DEVICE}
112-
/>,
113-
);
114-
}
115-
116-
if (this.state.showExploringPublicSpaces) {
117-
groups
118-
.getOrCreate(LabGroup.Spaces, [])
119-
.push(
120-
<SettingsFlag
121-
key="feature_exploring_public_spaces"
122-
name="feature_exploring_public_spaces"
123-
level={SettingLevel.DEVICE}
124-
/>,
125-
);
126-
}
127-
128114
labsSections = (
129115
<>
130116
{sortBy(Array.from(groups.entries()), "0").map(([group, flags]) => (

src/settings/Settings.tsx

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ import SdkConfig from "../SdkConfig";
4444
import SlidingSyncController from "./controllers/SlidingSyncController";
4545
import { FontWatcher } from "./watchers/FontWatcher";
4646
import RustCryptoSdkController from "./controllers/RustCryptoSdkController";
47+
import ServerSupportUnstableFeatureController from "./controllers/ServerSupportUnstableFeatureController";
48+
import { WatchManager } from "./WatchManager";
49+
50+
export const defaultWatchManager = new WatchManager();
4751

4852
// These are just a bunch of helper arrays to avoid copy/pasting a bunch of times
4953
const LEVELS_ROOM_SETTINGS = [
@@ -218,9 +222,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
218222
},
219223
},
220224
"feature_exploring_public_spaces": {
225+
isFeature: true,
226+
labsGroup: LabGroup.Spaces,
221227
displayName: _td("Explore public spaces in the new search dialog"),
222228
supportedLevels: LEVELS_FEATURE,
223229
default: false,
230+
controller: new ServerSupportUnstableFeatureController("feature_exploring_public_spaces", defaultWatchManager, [
231+
"org.matrix.msc3827.stable",
232+
]),
224233
},
225234
"feature_msc3531_hide_messages_pending_moderation": {
226235
isFeature: true,
@@ -359,13 +368,14 @@ export const SETTINGS: { [setting: string]: ISetting } = {
359368
default: false,
360369
},
361370
"feature_jump_to_date": {
362-
// We purposely leave out `isFeature: true` so it doesn't show in Labs
363-
// by default. We will conditionally show it depending on whether we can
364-
// detect MSC3030 support (see LabUserSettingsTab.tsx).
365-
// labsGroup: LabGroup.Messaging,
371+
isFeature: true,
372+
labsGroup: LabGroup.Messaging,
366373
displayName: _td("Jump to date (adds /jumptodate and jump to date headers)"),
367374
supportedLevels: LEVELS_FEATURE,
368375
default: false,
376+
controller: new ServerSupportUnstableFeatureController("feature_jump_to_date", defaultWatchManager, [
377+
"org.matrix.msc3030",
378+
]),
369379
},
370380
"RoomList.backgroundImage": {
371381
supportedLevels: LEVELS_ACCOUNT_SETTINGS,
@@ -387,6 +397,7 @@ export const SETTINGS: { [setting: string]: ISetting } = {
387397
controller: new SlidingSyncController(),
388398
},
389399
"feature_sliding_sync_proxy_url": {
400+
// This is not a distinct feature, it is a setting for feature_sliding_sync above
390401
supportedLevels: LEVELS_DEVICE_ONLY_SETTINGS_WITH_CONFIG,
391402
default: "",
392403
},

src/settings/SettingsStore.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,9 +27,9 @@ import RoomSettingsHandler from "./handlers/RoomSettingsHandler";
2727
import ConfigSettingsHandler from "./handlers/ConfigSettingsHandler";
2828
import { _t } from "../languageHandler";
2929
import dis from "../dispatcher/dispatcher";
30-
import { IFeature, ISetting, LabGroup, SETTINGS } from "./Settings";
30+
import { IFeature, ISetting, LabGroup, SETTINGS, defaultWatchManager } from "./Settings";
3131
import LocalEchoWrapper from "./handlers/LocalEchoWrapper";
32-
import { CallbackFn as WatchCallbackFn, WatchManager } from "./WatchManager";
32+
import { CallbackFn as WatchCallbackFn } from "./WatchManager";
3333
import { SettingLevel } from "./SettingLevel";
3434
import SettingsHandler from "./handlers/SettingsHandler";
3535
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
@@ -39,8 +39,6 @@ import dispatcher from "../dispatcher/dispatcher";
3939
import { ActionPayload } from "../dispatcher/payloads";
4040
import { MatrixClientPeg } from "../MatrixClientPeg";
4141

42-
const defaultWatchManager = new WatchManager();
43-
4442
// Convert the settings to easier to manage objects for the handlers
4543
const defaultSettings: Record<string, any> = {};
4644
const invertedDefaultSettings: Record<string, boolean> = {};

src/settings/WatchManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export class WatchManager {
3939
public unwatchSetting(cb: CallbackFn): void {
4040
this.watchers.forEach((map) => {
4141
map.forEach((callbacks) => {
42-
let idx;
42+
let idx: number;
4343
while ((idx = callbacks.indexOf(cb)) !== -1) {
4444
callbacks.splice(idx, 1);
4545
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/client";
18+
19+
import SettingController from "./SettingController";
20+
21+
// Dev note: This whole class exists in the event someone logs out and back in - we want
22+
// to make sure the right MatrixClient is listening for changes.
23+
24+
/**
25+
* Represents the base class for settings controllers which need access to a MatrixClient.
26+
* This class performs no logic and should be overridden.
27+
*/
28+
export default abstract class MatrixClientBackedController extends SettingController {
29+
private static _matrixClient: MatrixClient;
30+
private static instances: MatrixClientBackedController[] = [];
31+
32+
public static set matrixClient(client: MatrixClient) {
33+
const oldClient = MatrixClientBackedController._matrixClient;
34+
MatrixClientBackedController._matrixClient = client;
35+
36+
for (const instance of MatrixClientBackedController.instances) {
37+
instance.initMatrixClient(oldClient, client);
38+
}
39+
}
40+
41+
protected constructor() {
42+
super();
43+
44+
MatrixClientBackedController.instances.push(this);
45+
}
46+
47+
public get client(): MatrixClient {
48+
return MatrixClientBackedController._matrixClient;
49+
}
50+
51+
protected abstract initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): void;
52+
}
Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
/*
2+
Copyright 2023 The Matrix.org Foundation C.I.C.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
17+
import { MatrixClient } from "matrix-js-sdk/src/matrix";
18+
19+
import { SettingLevel } from "../SettingLevel";
20+
import MatrixClientBackedController from "./MatrixClientBackedController";
21+
import { WatchManager } from "../WatchManager";
22+
import SettingsStore from "../SettingsStore";
23+
24+
/**
25+
* Disables a given setting if the server unstable feature it requires is not supported
26+
* When a setting gets disabled or enabled from this controller it notifies the given WatchManager
27+
*/
28+
export default class ServerSupportUnstableFeatureController extends MatrixClientBackedController {
29+
private enabled: boolean | undefined;
30+
31+
public constructor(
32+
private readonly settingName: string,
33+
private readonly watchers: WatchManager,
34+
private readonly unstableFeatures: string[],
35+
private readonly forcedValue: any = false,
36+
) {
37+
super();
38+
}
39+
40+
public get disabled(): boolean {
41+
return !this.enabled;
42+
}
43+
44+
public set disabled(v: boolean) {
45+
if (!v === this.enabled) return;
46+
this.enabled = !v;
47+
const level = SettingsStore.firstSupportedLevel(this.settingName);
48+
const settingValue = SettingsStore.getValue(this.settingName, null);
49+
this.watchers.notifyUpdate(this.settingName, null, level, settingValue);
50+
}
51+
52+
protected async initMatrixClient(oldClient: MatrixClient, newClient: MatrixClient): Promise<void> {
53+
this.disabled = true;
54+
let supported = true;
55+
for (const feature of this.unstableFeatures) {
56+
supported = await this.client.doesServerSupportUnstableFeature(feature);
57+
if (!supported) break;
58+
}
59+
this.disabled = !supported;
60+
}
61+
62+
public getValueOverride(
63+
level: SettingLevel,
64+
roomId: string,
65+
calculatedValue: any,
66+
calculatedAtLevel: SettingLevel | null,
67+
): any {
68+
if (this.settingDisabled) {
69+
return this.forcedValue;
70+
}
71+
return null; // no override
72+
}
73+
74+
public get settingDisabled(): boolean {
75+
return this.disabled;
76+
}
77+
}

test/MatrixClientPeg-test.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ limitations under the License.
1515
*/
1616

1717
import { logger } from "matrix-js-sdk/src/logger";
18+
import fetchMockJest from "fetch-mock-jest";
1819

1920
import { advanceDateAndTime, stubClient } from "./test-utils";
2021
import { IMatrixClientPeg, MatrixClientPeg as peg } from "../src/MatrixClientPeg";
@@ -68,6 +69,7 @@ describe("MatrixClientPeg", () => {
6869
// instantiate a MatrixClientPegClass instance, with a new MatrixClient
6970
const PegClass = Object.getPrototypeOf(peg).constructor;
7071
testPeg = new PegClass();
72+
fetchMockJest.get("http://example.com/_matrix/client/versions", {});
7173
testPeg.replaceUsingCreds({
7274
accessToken: "SEKRET",
7375
homeserverUrl: "http://example.com",

0 commit comments

Comments
 (0)