diff --git a/res/css/views/dialogs/_DevtoolsDialog.scss b/res/css/views/dialogs/_DevtoolsDialog.scss
index 35cb6bc7ab..7b4449eee0 100644
--- a/res/css/views/dialogs/_DevtoolsDialog.scss
+++ b/res/css/views/dialogs/_DevtoolsDialog.scss
@@ -223,3 +223,54 @@ limitations under the License.
content: ":";
}
}
+
+.mx_DevTools_SettingsExplorer {
+ table {
+ width: 100%;
+ table-layout: fixed;
+ border-collapse: collapse;
+
+ th {
+ // Colour choice: first one autocomplete gave me.
+ border-bottom: 1px solid $accent-color;
+ text-align: left;
+ }
+
+ td, th {
+ width: 360px; // "feels right" number
+
+ text-overflow: ellipsis;
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ td+td, th+th {
+ width: auto;
+ }
+
+ tr:hover {
+ // Colour choice: first one autocomplete gave me.
+ background-color: $accent-color-50pct;
+ }
+ }
+
+ .mx_DevTools_SettingsExplorer_mutable {
+ background-color: $accent-color;
+ }
+
+ .mx_DevTools_SettingsExplorer_immutable {
+ background-color: $warning-color;
+ }
+
+ .mx_DevTools_SettingsExplorer_edit {
+ float: right;
+ margin-right: 16px;
+ }
+
+ .mx_DevTools_SettingsExplorer_warning {
+ border: 2px solid $warning-color;
+ border-radius: 4px;
+ padding: 4px;
+ margin-bottom: 8px;
+ }
+}
diff --git a/src/components/views/dialogs/DevtoolsDialog.js b/src/components/views/dialogs/DevtoolsDialog.js
index 7d4e3b462f..5b1ed86adb 100644
--- a/src/components/views/dialogs/DevtoolsDialog.js
+++ b/src/components/views/dialogs/DevtoolsDialog.js
@@ -34,6 +34,10 @@ import {
} from "matrix-js-sdk/src/crypto/verification/request/VerificationRequest";
import WidgetStore from "../../../stores/WidgetStore";
import {UPDATE_EVENT} from "../../../stores/AsyncStore";
+import {SETTINGS} from "../../../settings/Settings";
+import SettingsStore, {LEVEL_ORDER} from "../../../settings/SettingsStore";
+import Modal from "../../../Modal";
+import ErrorDialog from "./ErrorDialog";
class GenericEditor extends React.PureComponent {
// static propTypes = {onBack: PropTypes.func.isRequired};
@@ -794,6 +798,286 @@ class WidgetExplorer extends React.Component {
}
}
+class SettingsExplorer extends React.Component {
+ static getLabel() {
+ return _t("Settings Explorer");
+ }
+
+ constructor(props) {
+ super(props);
+
+ this.state = {
+ query: '',
+ editSetting: null, // set to a setting ID when editing
+ viewSetting: null, // set to a setting ID when exploring in detail
+
+ explicitValues: null, // stringified JSON for edit view
+ explicitRoomValues: null, // stringified JSON for edit view
+ };
+ }
+
+ onQueryChange = (ev) => {
+ this.setState({query: ev.target.value});
+ };
+
+ onExplValuesEdit = (ev) => {
+ this.setState({explicitValues: ev.target.value});
+ };
+
+ onExplRoomValuesEdit = (ev) => {
+ this.setState({explicitRoomValues: ev.target.value});
+ };
+
+ onBack = () => {
+ if (this.state.editSetting) {
+ this.setState({editSetting: null});
+ } else if (this.state.viewSetting) {
+ this.setState({viewSetting: null});
+ } else {
+ this.props.onBack();
+ }
+ };
+
+ onViewClick = (ev, settingId) => {
+ ev.preventDefault();
+ this.setState({viewSetting: settingId});
+ };
+
+ onEditClick = (ev, settingId) => {
+ ev.preventDefault();
+ this.setState({
+ editSetting: settingId,
+ explicitValues: this.renderExplicitSettingValues(settingId, null),
+ explicitRoomValues: this.renderExplicitSettingValues(settingId, this.props.room.roomId),
+ });
+ };
+
+ onSaveClick = async () => {
+ try {
+ const settingId = this.state.editSetting;
+ const parsedExplicit = JSON.parse(this.state.explicitValues);
+ const parsedExplicitRoom = JSON.parse(this.state.explicitRoomValues);
+ for (const level of Object.keys(parsedExplicit)) {
+ console.log(`[Devtools] Setting value of ${settingId} at ${level} from user input`);
+ try {
+ const val = parsedExplicit[level];
+ await SettingsStore.setValue(settingId, null, level, val);
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+ const roomId = this.props.room.roomId;
+ for (const level of Object.keys(parsedExplicit)) {
+ console.log(`[Devtools] Setting value of ${settingId} at ${level} in ${roomId} from user input`);
+ try {
+ const val = parsedExplicitRoom[level];
+ await SettingsStore.setValue(settingId, roomId, level, val);
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+ this.setState({
+ viewSetting: settingId,
+ editSetting: null,
+ });
+ } catch (e) {
+ Modal.createTrackedDialog('Devtools - Failed to save settings', '', ErrorDialog, {
+ title: _t("Failed to save settings"),
+ description: e.message,
+ });
+ }
+ };
+
+ renderSettingValue(val) {
+ // Note: we don't .toString() a string because we want JSON.stringify to inject quotes for us
+ const toStringTypes = ['boolean', 'number'];
+ if (toStringTypes.includes(typeof(val))) {
+ return val.toString();
+ } else {
+ return JSON.stringify(val);
+ }
+ }
+
+ renderExplicitSettingValues(setting, roomId) {
+ const vals = {};
+ for (const level of LEVEL_ORDER) {
+ try {
+ vals[level] = SettingsStore.getValueAt(level, setting, roomId, true, true);
+ if (vals[level] === undefined) {
+ vals[level] = null;
+ }
+ } catch (e) {
+ console.warn(e);
+ }
+ }
+ return JSON.stringify(vals, null, 4);
+ }
+
+ renderCanEditLevel(roomId, level) {
+ let canEdit = SettingsStore.canSetValue(this.state.editSetting, roomId, level);
+ const className = canEdit ? 'mx_DevTools_SettingsExplorer_mutable' : 'mx_DevTools_SettingsExplorer_immutable';
+ return
{canEdit.toString()}
;
+ }
+
+ render() {
+ const room = this.props.room;
+
+ if (!this.state.viewSetting && !this.state.editSetting) {
+ // view all settings
+ const allSettings = Object.keys(SETTINGS)
+ .filter(n => this.state.query ? n.toLowerCase().includes(this.state.query.toLowerCase()) : true);
+ return (
+
+ );
+ } else if (this.state.editSetting) {
+ return (
+
+
+
{_t("Setting:")} {this.state.editSetting}
+
+
+ {_t("Caution:")} {_t(
+ "This UI does NOT check the types of the values. Use at your own risk.",
+ )}
+
+
+
+ {_t("Setting definition:")}
+
{JSON.stringify(SETTINGS[this.state.editSetting], null, 4)}
+
+
+
+
+
+
+ {_t("Level")}
+ {_t("Settable at global")}
+ {_t("Settable at room")}
+
+
+
+ {LEVEL_ORDER.map(lvl => (
+
+ {lvl}
+ {this.renderCanEditLevel(null, lvl)}
+ {this.renderCanEditLevel(room.roomId, lvl)}
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {_t("Save setting values")}
+ {_t("Back")}
+
+
+ );
+ } else if (this.state.viewSetting) {
+ return (
+
+
+
{_t("Setting:")} {this.state.viewSetting}
+
+
+ {_t("Setting definition:")}
+
{JSON.stringify(SETTINGS[this.state.viewSetting], null, 4)}
+
+
+
+ {_t("Value:")}
+ {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting))}
+
+
+
+ {_t("Value in this room:")}
+ {this.renderSettingValue(SettingsStore.getValue(this.state.viewSetting, room.roomId))}
+
+
+
+ {_t("Values at explicit levels:")}
+
{this.renderExplicitSettingValues(this.state.viewSetting, null)}
+
+
+
+ {_t("Values at explicit levels in this room:")}
+
{this.renderExplicitSettingValues(this.state.viewSetting, room.roomId)}
+
+
+
+
+ this.onEditClick(e, this.state.viewSetting)}>{_t("Edit Values")}
+ {_t("Back")}
+
+
+ );
+ }
+ }
+}
+
const Entries = [
SendCustomEvent,
RoomStateExplorer,
@@ -802,6 +1086,7 @@ const Entries = [
ServersInRoomList,
VerificationExplorer,
WidgetExplorer,
+ SettingsExplorer,
];
export default class DevtoolsDialog extends React.PureComponent {
diff --git a/src/i18n/strings/en_EN.json b/src/i18n/strings/en_EN.json
index 5bbbdf60b5..54f0f66eb4 100644
--- a/src/i18n/strings/en_EN.json
+++ b/src/i18n/strings/en_EN.json
@@ -2068,6 +2068,26 @@
"Verification Requests": "Verification Requests",
"Active Widgets": "Active Widgets",
"There was an error finding this widget.": "There was an error finding this widget.",
+ "Settings Explorer": "Settings Explorer",
+ "Failed to save settings": "Failed to save settings",
+ "Setting ID": "Setting ID",
+ "Value": "Value",
+ "Value in this room": "Value in this room",
+ "Setting:": "Setting:",
+ "Caution:": "Caution:",
+ "This UI does NOT check the types of the values. Use at your own risk.": "This UI does NOT check the types of the values. Use at your own risk.",
+ "Setting definition:": "Setting definition:",
+ "Level": "Level",
+ "Settable at global": "Settable at global",
+ "Settable at room": "Settable at room",
+ "Values at explicit levels": "Values at explicit levels",
+ "Values at explicit levels in this room": "Values at explicit levels in this room",
+ "Save setting values": "Save setting values",
+ "Value:": "Value:",
+ "Value in this room:": "Value in this room:",
+ "Values at explicit levels:": "Values at explicit levels:",
+ "Values at explicit levels in this room:": "Values at explicit levels in this room:",
+ "Edit Values": "Edit Values",
"Toolbox": "Toolbox",
"Developer Tools": "Developer Tools",
"There was an error updating your community. The server is unable to process your request.": "There was an error updating your community. The server is unable to process your request.",
diff --git a/src/settings/SettingsStore.ts b/src/settings/SettingsStore.ts
index 6dc2a76ae8..c2675bd8f8 100644
--- a/src/settings/SettingsStore.ts
+++ b/src/settings/SettingsStore.ts
@@ -61,7 +61,7 @@ for (const key of Object.keys(LEVEL_HANDLERS)) {
LEVEL_HANDLERS[key] = new LocalEchoWrapper(LEVEL_HANDLERS[key]);
}
-const LEVEL_ORDER = [
+export const LEVEL_ORDER = [
SettingLevel.DEVICE,
SettingLevel.ROOM_DEVICE,
SettingLevel.ROOM_ACCOUNT,