Abstract electron settings properly to avoid boilerplate-hell (#8798)

* Remove unused method `BasePlatform::screenCaptureErrorString`

* Improve platform typescripting

* Remove redundant awaits

* Abstract electron settings properly to avoid boilerplate-hell

* i18n

* Fix stray semi-colons

* Fix setting level order for Platform settings

(cherry picked from commit ba2ce5ecba)
This commit is contained in:
Michael Telatynski 2022-06-10 22:38:50 +01:00
parent ec9117dbd1
commit d6c162a5cf
No known key found for this signature in database
GPG key ID: 98BC6A2B829297FE
9 changed files with 122 additions and 220 deletions

View file

@ -46,6 +46,17 @@ export enum UpdateCheckStatus {
Ready = "READY",
}
export interface UpdateStatus {
/**
* The current phase of the manual update check.
*/
status: UpdateCheckStatus;
/**
* Detail string relating to the current status, typically for error details.
*/
detail?: string;
}
const UPDATE_DEFER_KEY = "mx_defer_update";
/**
@ -225,79 +236,21 @@ export default abstract class BasePlatform {
*/
public abstract getAppVersion(): Promise<string>;
/*
* If it's not expected that capturing the screen will work
* with getUserMedia, return a string explaining why not.
* Otherwise, return null.
*/
public screenCaptureErrorString(): string {
return "Not implemented";
}
/**
* Restarts the application, without necessarily reloading
* any application code
*/
public abstract reload(): void;
public supportsAutoLaunch(): boolean {
public supportsSetting(settingName?: string): boolean {
return false;
}
// XXX: Surely this should be a setting like any other?
public async getAutoLaunchEnabled(): Promise<boolean> {
return false;
public getSettingValue(settingName: string): Promise<any> {
return undefined;
}
public async setAutoLaunchEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
public supportsWarnBeforeExit(): boolean {
return false;
}
public async shouldWarnBeforeExit(): Promise<boolean> {
return false;
}
public async setWarnBeforeExit(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
public supportsAutoHideMenuBar(): boolean {
return false;
}
public async getAutoHideMenuBarEnabled(): Promise<boolean> {
return false;
}
public async setAutoHideMenuBarEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
public supportsMinimizeToTray(): boolean {
return false;
}
public async getMinimizeToTrayEnabled(): Promise<boolean> {
return false;
}
public async setMinimizeToTrayEnabled(enabled: boolean): Promise<void> {
throw new Error("Unimplemented");
}
public supportsTogglingHardwareAcceleration(): boolean {
return false;
}
public async getHardwareAccelerationEnabled(): Promise<boolean> {
return true;
}
public async setHardwareAccelerationEnabled(enabled: boolean): Promise<void> {
public setSettingValue(settingName: string, value: any): Promise<void> {
throw new Error("Unimplemented");
}

View file

@ -33,6 +33,7 @@ interface IProps {
// XXX: once design replaces all toggles make this the default
useCheckbox?: boolean;
disabled?: boolean;
hideIfCannotSet?: boolean;
onChange?(checked: boolean): void;
}
@ -76,6 +77,8 @@ export default class SettingsFlag extends React.Component<IProps, IState> {
public render() {
const canChange = SettingsStore.canSetValue(this.props.name, this.props.roomId, this.props.level);
if (!canChange && this.props.hideIfCannotSet) return null;
const label = this.props.label
? _t(this.props.label)
: SettingsStore.getDisplayName(this.props.name, this.props.level);

View file

@ -18,10 +18,8 @@ limitations under the License.
import React from 'react';
import { _t } from "../../../../../languageHandler";
import LabelledToggleSwitch from "../../../elements/LabelledToggleSwitch";
import SettingsStore from "../../../../../settings/SettingsStore";
import Field from "../../../elements/Field";
import PlatformPeg from "../../../../../PlatformPeg";
import { SettingLevel } from "../../../../../settings/SettingLevel";
import SettingsFlag from '../../../elements/SettingsFlag';
import AccessibleButton from "../../../elements/AccessibleButton";
@ -36,16 +34,6 @@ interface IProps {
}
interface IState {
autoLaunch: boolean;
autoLaunchSupported: boolean;
warnBeforeExit: boolean;
warnBeforeExitSupported: boolean;
alwaysShowMenuBarSupported: boolean;
alwaysShowMenuBar: boolean;
minimizeToTraySupported: boolean;
minimizeToTray: boolean;
togglingHardwareAccelerationSupported: boolean;
enableHardwareAcceleration: boolean;
autocompleteDelay: string;
readMarkerInViewThresholdMs: string;
readMarkerOutOfViewThresholdMs: string;
@ -112,16 +100,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
super(props);
this.state = {
autoLaunch: false,
autoLaunchSupported: false,
warnBeforeExit: true,
warnBeforeExitSupported: false,
alwaysShowMenuBar: true,
alwaysShowMenuBarSupported: false,
minimizeToTray: true,
minimizeToTraySupported: false,
enableHardwareAcceleration: true,
togglingHardwareAccelerationSupported: false,
autocompleteDelay:
SettingsStore.getValueAt(SettingLevel.DEVICE, 'autocompleteDelay').toString(10),
readMarkerInViewThresholdMs:
@ -131,74 +109,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
}
async componentDidMount() {
const platform = PlatformPeg.get();
const autoLaunchSupported = await platform.supportsAutoLaunch();
let autoLaunch = false;
if (autoLaunchSupported) {
autoLaunch = await platform.getAutoLaunchEnabled();
}
const warnBeforeExitSupported = await platform.supportsWarnBeforeExit();
let warnBeforeExit = false;
if (warnBeforeExitSupported) {
warnBeforeExit = await platform.shouldWarnBeforeExit();
}
const alwaysShowMenuBarSupported = await platform.supportsAutoHideMenuBar();
let alwaysShowMenuBar = true;
if (alwaysShowMenuBarSupported) {
alwaysShowMenuBar = !(await platform.getAutoHideMenuBarEnabled());
}
const minimizeToTraySupported = await platform.supportsMinimizeToTray();
let minimizeToTray = true;
if (minimizeToTraySupported) {
minimizeToTray = await platform.getMinimizeToTrayEnabled();
}
const togglingHardwareAccelerationSupported = platform.supportsTogglingHardwareAcceleration();
let enableHardwareAcceleration = true;
if (togglingHardwareAccelerationSupported) {
enableHardwareAcceleration = await platform.getHardwareAccelerationEnabled();
}
this.setState({
autoLaunch,
autoLaunchSupported,
warnBeforeExit,
warnBeforeExitSupported,
alwaysShowMenuBarSupported,
alwaysShowMenuBar,
minimizeToTraySupported,
minimizeToTray,
togglingHardwareAccelerationSupported,
enableHardwareAcceleration,
});
}
private onAutoLaunchChange = (checked: boolean) => {
PlatformPeg.get().setAutoLaunchEnabled(checked).then(() => this.setState({ autoLaunch: checked }));
};
private onWarnBeforeExitChange = (checked: boolean) => {
PlatformPeg.get().setWarnBeforeExit(checked).then(() => this.setState({ warnBeforeExit: checked }));
};
private onAlwaysShowMenuBarChange = (checked: boolean) => {
PlatformPeg.get().setAutoHideMenuBarEnabled(!checked).then(() => this.setState({ alwaysShowMenuBar: checked }));
};
private onMinimizeToTrayChange = (checked: boolean) => {
PlatformPeg.get().setMinimizeToTrayEnabled(checked).then(() => this.setState({ minimizeToTray: checked }));
};
private onHardwareAccelerationChange = (checked: boolean) => {
PlatformPeg.get().setHardwareAccelerationEnabled(checked).then(
() => this.setState({ enableHardwareAcceleration: checked }));
};
private onAutocompleteDelayChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ autocompleteDelay: e.target.value });
SettingsStore.setValue("autocompleteDelay", null, SettingLevel.DEVICE, e.target.value);
@ -232,49 +142,6 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
};
render() {
let autoLaunchOption = null;
if (this.state.autoLaunchSupported) {
autoLaunchOption = <LabelledToggleSwitch
value={this.state.autoLaunch}
onChange={this.onAutoLaunchChange}
label={_t('Start automatically after system login')} />;
}
let warnBeforeExitOption = null;
if (this.state.warnBeforeExitSupported) {
warnBeforeExitOption = <LabelledToggleSwitch
value={this.state.warnBeforeExit}
onChange={this.onWarnBeforeExitChange}
label={_t('Warn before quitting')} />;
}
let autoHideMenuOption = null;
if (this.state.alwaysShowMenuBarSupported) {
autoHideMenuOption = <LabelledToggleSwitch
value={this.state.alwaysShowMenuBar}
onChange={this.onAlwaysShowMenuBarChange}
label={_t('Always show the window menu bar')} />;
}
let minimizeToTrayOption = null;
if (this.state.minimizeToTraySupported) {
minimizeToTrayOption = <LabelledToggleSwitch
value={this.state.minimizeToTray}
onChange={this.onMinimizeToTrayChange}
label={_t('Show tray icon and minimise window to it on close')} />;
}
let hardwareAccelerationOption = null;
if (this.state.togglingHardwareAccelerationSupported) {
const appName = SdkConfig.get().brand;
hardwareAccelerationOption = <LabelledToggleSwitch
value={this.state.enableHardwareAcceleration}
onChange={this.onHardwareAccelerationChange}
label={_t('Enable hardware acceleration (restart %(appName)s to take effect)', {
appName,
})} />;
}
return (
<div className="mx_SettingsTab mx_PreferencesUserSettingsTab">
<div className="mx_SettingsTab_heading">{ _t("Preferences") }</div>
@ -331,11 +198,20 @@ export default class PreferencesUserSettingsTab extends React.Component<IProps,
<div className="mx_SettingsTab_section">
<span className="mx_SettingsTab_subheading">{ _t("General") }</span>
{ this.renderGroup(PreferencesUserSettingsTab.GENERAL_SETTINGS) }
{ minimizeToTrayOption }
{ hardwareAccelerationOption }
{ autoHideMenuOption }
{ autoLaunchOption }
{ warnBeforeExitOption }
<SettingsFlag name="Electron.showTrayIcon" level={SettingLevel.PLATFORM} hideIfCannotSet />
<SettingsFlag
name="Electron.enableHardwareAcceleration"
level={SettingLevel.PLATFORM}
hideIfCannotSet
label={_t('Enable hardware acceleration (restart %(appName)s to take effect)', {
appName: SdkConfig.get().brand,
})}
/>
<SettingsFlag name="Electron.alwaysShowMenuBar" level={SettingLevel.PLATFORM} hideIfCannotSet />
<SettingsFlag name="Electron.autoLaunch" level={SettingLevel.PLATFORM} hideIfCannotSet />
<SettingsFlag name="Electron.warnBeforeExit" level={SettingLevel.PLATFORM} hideIfCannotSet />
<Field
label={_t('Autocomplete delay (ms)')}
type='number'

View file

@ -16,18 +16,8 @@ limitations under the License.
import { ActionPayload } from "../payloads";
import { Action } from "../actions";
import { UpdateCheckStatus } from "../../BasePlatform";
import { UpdateStatus } from "../../BasePlatform";
export interface CheckUpdatesPayload extends ActionPayload {
export interface CheckUpdatesPayload extends ActionPayload, UpdateStatus {
action: Action.CheckUpdates;
/**
* The current phase of the manual update check.
*/
status: UpdateCheckStatus;
/**
* Detail string relating to the current status, typically for error details.
*/
detail?: string;
}

View file

@ -970,6 +970,11 @@
"Automatically send debug logs on any error": "Automatically send debug logs on any error",
"Automatically send debug logs on decryption errors": "Automatically send debug logs on decryption errors",
"Automatically send debug logs when key backup is not functioning": "Automatically send debug logs when key backup is not functioning",
"Start automatically after system login": "Start automatically after system login",
"Warn before quitting": "Warn before quitting",
"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",
"Enable hardware acceleration": "Enable hardware acceleration",
"Partial Support for Threads": "Partial Support for Threads",
"Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. <a>Learn more</a>.": "Your homeserver does not currently support threads, so this feature may be unreliable. Some threaded messages may not be reliably available. <a>Learn more</a>.",
"Do you want to enable threads anyway?": "Do you want to enable threads anyway?",
@ -1498,11 +1503,6 @@
"If this isn't what you want, please use a different tool to ignore users.": "If this isn't what you want, please use a different tool to ignore users.",
"Room ID or address of ban list": "Room ID or address of ban list",
"Subscribe": "Subscribe",
"Start automatically after system login": "Start automatically after system login",
"Warn before quitting": "Warn before quitting",
"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",
"Enable hardware acceleration (restart %(appName)s to take effect)": "Enable hardware acceleration (restart %(appName)s to take effect)",
"Preferences": "Preferences",
"Room list": "Room list",
"Keyboard shortcuts": "Keyboard shortcuts",
@ -1512,6 +1512,7 @@
"Code blocks": "Code blocks",
"Images, GIFs and videos": "Images, GIFs and videos",
"Timeline": "Timeline",
"Enable hardware acceleration (restart %(appName)s to take effect)": "Enable hardware acceleration (restart %(appName)s to take effect)",
"Autocomplete delay (ms)": "Autocomplete delay (ms)",
"Read Marker lifetime (ms)": "Read Marker lifetime (ms)",
"Read Marker off-screen lifetime (ms)": "Read Marker off-screen lifetime (ms)",

View file

@ -24,6 +24,7 @@ export enum SettingLevel {
ROOM_ACCOUNT = "room-account",
ACCOUNT = "account",
ROOM = "room",
PLATFORM = "platform",
CONFIG = "config",
DEFAULT = "default",
}

View file

@ -1008,4 +1008,32 @@ export const SETTINGS: {[setting: string]: ISetting} = {
supportedLevels: LEVELS_UI_FEATURE,
default: true,
},
// Electron-specific settings, they are stored by Electron and set/read over an IPC.
// We store them over there are they are necessary to know before the renderer process launches.
"Electron.autoLaunch": {
supportedLevels: [SettingLevel.PLATFORM],
displayName: _td("Start automatically after system login"),
default: false,
},
"Electron.warnBeforeExit": {
supportedLevels: [SettingLevel.PLATFORM],
displayName: _td("Warn before quitting"),
default: true,
},
"Electron.alwaysShowMenuBar": {
supportedLevels: [SettingLevel.PLATFORM],
displayName: _td("Always show the window menu bar"),
default: false,
},
"Electron.showTrayIcon": {
supportedLevels: [SettingLevel.PLATFORM],
displayName: _td("Show tray icon and minimise window to it on close"),
default: true,
},
"Electron.enableHardwareAcceleration": {
supportedLevels: [SettingLevel.PLATFORM],
displayName: _td("Enable hardware acceleration"),
default: true,
},
};

View file

@ -34,6 +34,7 @@ import { SettingLevel } from "./SettingLevel";
import SettingsHandler from "./handlers/SettingsHandler";
import { SettingUpdatedPayload } from "../dispatcher/payloads/SettingUpdatedPayload";
import { Action } from "../dispatcher/actions";
import PlatformSettingsHandler from "./handlers/PlatformSettingsHandler";
const defaultWatchManager = new WatchManager();
@ -61,6 +62,7 @@ const LEVEL_HANDLERS = {
),
[SettingLevel.ACCOUNT]: new LocalEchoWrapper(new AccountSettingsHandler(defaultWatchManager), SettingLevel.ACCOUNT),
[SettingLevel.ROOM]: new LocalEchoWrapper(new RoomSettingsHandler(defaultWatchManager), SettingLevel.ROOM),
[SettingLevel.PLATFORM]: new LocalEchoWrapper(new PlatformSettingsHandler(), SettingLevel.PLATFORM),
[SettingLevel.CONFIG]: new ConfigSettingsHandler(featureNames),
[SettingLevel.DEFAULT]: new DefaultSettingsHandler(defaultSettings, invertedDefaultSettings),
};
@ -75,6 +77,14 @@ export const LEVEL_ORDER = [
SettingLevel.DEFAULT,
];
function getLevelOrder(setting: ISetting): SettingLevel[] {
// Settings which support only a single setting level are inherently ordered
if (setting.supportedLevelsAreOrdered || setting.supportedLevels.length === 1) {
return setting.supportedLevels;
}
return LEVEL_ORDER;
}
export type CallbackFn = (
settingName: string,
roomId: string,
@ -316,7 +326,7 @@ export default class SettingsStore {
}
const setting = SETTINGS[settingName];
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
const levelOrder = getLevelOrder(setting);
return SettingsStore.getValueAt(levelOrder[0], settingName, roomId, false, excludeDefault);
}
@ -345,7 +355,7 @@ export default class SettingsStore {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
const levelOrder = getLevelOrder(setting);
if (!levelOrder.includes(SettingLevel.DEFAULT)) levelOrder.push(SettingLevel.DEFAULT); // always include default
const minIndex = levelOrder.indexOf(level);
@ -518,7 +528,7 @@ export default class SettingsStore {
throw new Error("Setting '" + settingName + "' does not appear to be a setting.");
}
const levelOrder = (setting.supportedLevelsAreOrdered ? setting.supportedLevels : LEVEL_ORDER);
const levelOrder = getLevelOrder(setting);
if (!levelOrder.includes(SettingLevel.DEFAULT)) levelOrder.push(SettingLevel.DEFAULT); // always include default
const handlers = SettingsStore.getHandlers(settingName);

View file

@ -0,0 +1,40 @@
/*
Copyright 2022 The Matrix.org Foundation C.I.C.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
import SettingsHandler from "./SettingsHandler";
import PlatformPeg from "../../PlatformPeg";
/**
* Gets and sets settings at the "platform" level for the current device.
* This handler does not make use of the roomId parameter.
*/
export default class PlatformSettingsHandler extends SettingsHandler {
public canSetValue(settingName: string, roomId: string): boolean {
return PlatformPeg.get().supportsSetting(settingName);
}
public getValue(settingName: string, roomId: string): any {
return PlatformPeg.get().getSettingValue(settingName);
}
public setValue(settingName: string, roomId: string, newValue: any): Promise<void> {
return PlatformPeg.get().setSettingValue(settingName, newValue);
}
public isSupported(): boolean {
return PlatformPeg.get().supportsSetting();
}
}