Files
mattermostest/src/renderer/components/SettingsPage.tsx
Devin Binnie fc13f877f2 [MM-30713] Stop Linux and Windows app from minimizing/hiding without user warning (#1988)
* [MM-30713] Stop Linux app from minimizing/hiding without user warning

* Added same behaviour for Windows

* Update messages

* Change wording

* Fix for accidentally disabled setting
2022-03-01 12:35:27 -05:00

812 lines
32 KiB
TypeScript

// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import 'renderer/css/settings.css';
import React from 'react';
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button} from 'react-bootstrap';
import ReactSelect, {ActionMeta, OptionsType} from 'react-select';
import {debounce} from 'underscore';
import {CombinedConfig, LocalConfiguration} from 'types/config';
import {DeepPartial} from 'types/utils';
import {localeTranslations} from 'common/utils/constants';
import {
GET_LOCAL_CONFIGURATION,
UPDATE_CONFIGURATION,
DOUBLE_CLICK_ON_WINDOW,
GET_DOWNLOAD_LOCATION,
RELOAD_CONFIGURATION,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
} from 'common/communication';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
const CONFIG_TYPE_SERVERS = 'servers';
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
type State = DeepPartial<CombinedConfig> & {
ready: boolean;
maximized?: boolean;
savingState: SavingStateItems;
userOpenedDownloadDialog: boolean;
allowSaveSpellCheckerURL: boolean;
availableLanguages: Array<{label: string; value: string}>;
}
type SavingStateItems = {
appOptions: SavingState;
servers: SavingState;
};
type SaveQueueItem = {
configType: ConfigType;
key: keyof CombinedConfig;
data: CombinedConfig[keyof CombinedConfig];
}
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
trayIconThemeRef: React.RefObject<HTMLDivElement>;
downloadLocationRef: React.RefObject<HTMLInputElement>;
showTrayIconRef: React.RefObject<HTMLInputElement>;
autostartRef: React.RefObject<HTMLInputElement>;
hideOnStartRef: React.RefObject<HTMLInputElement>;
minimizeToTrayRef: React.RefObject<HTMLInputElement>;
flashWindowRef: React.RefObject<HTMLInputElement>;
bounceIconRef: React.RefObject<HTMLInputElement>;
showUnreadBadgeRef: React.RefObject<HTMLInputElement>;
useSpellCheckerRef: React.RefObject<HTMLInputElement>;
spellCheckerURLRef: React.RefObject<HTMLInputElement>;
enableHardwareAccelerationRef: React.RefObject<HTMLInputElement>;
saveQueue: SaveQueueItem[];
selectedSpellCheckerLocales: Array<{label: string; value: string}>;
constructor(props: Record<string, never>) {
super(props);
this.state = {
ready: false,
savingState: {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
},
userOpenedDownloadDialog: false,
allowSaveSpellCheckerURL: false,
availableLanguages: [],
};
this.getConfig();
this.trayIconThemeRef = React.createRef();
this.downloadLocationRef = React.createRef();
this.showTrayIconRef = React.createRef();
this.autostartRef = React.createRef();
this.hideOnStartRef = React.createRef();
this.minimizeToTrayRef = React.createRef();
this.flashWindowRef = React.createRef();
this.bounceIconRef = React.createRef();
this.showUnreadBadgeRef = React.createRef();
this.useSpellCheckerRef = React.createRef();
this.enableHardwareAccelerationRef = React.createRef();
this.spellCheckerURLRef = React.createRef();
this.saveQueue = [];
this.selectedSpellCheckerLocales = [];
}
componentDidMount() {
window.ipcRenderer.on(RELOAD_CONFIGURATION, () => {
this.updateSaveState();
this.getConfig();
});
window.ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES).then((languages: string[]) => {
const availableLanguages = languages.filter((language) => localeTranslations[language]).map((language) => ({label: localeTranslations[language], value: language}));
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
this.setState({availableLanguages});
});
}
getConfig = () => {
window.ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config) as Omit<State, 'ready'>});
});
}
convertConfigDataToState = (configData: Partial<LocalConfiguration>, currentState: Partial<State> = {}) => {
const newState = Object.assign({} as State, configData);
newState.savingState = currentState.savingState || {
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
};
this.selectedSpellCheckerLocales = configData.spellCheckerLocales?.map((language: string) => ({label: localeTranslations[language] || language, value: language})) || [];
return newState;
}
saveSetting = (configType: ConfigType, {key, data}: {key: keyof CombinedConfig; data: CombinedConfig[keyof CombinedConfig]}) => {
this.saveQueue.push({
configType,
key,
data,
});
this.updateSaveState();
this.processSaveQueue();
}
processSaveQueue = debounce(() => {
window.ipcRenderer.send(UPDATE_CONFIGURATION, this.saveQueue.splice(0, this.saveQueue.length));
}, 500);
updateSaveState = () => {
let queuedUpdateCounts = {
[CONFIG_TYPE_SERVERS]: 0,
[CONFIG_TYPE_APP_OPTIONS]: 0,
};
queuedUpdateCounts = this.saveQueue.reduce((updateCounts, {configType}) => {
updateCounts[configType]++;
return updateCounts;
}, queuedUpdateCounts);
const savingState = Object.assign({}, this.state.savingState);
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
if (count > 0) {
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVING;
} else if (count === 0 && savingState[configType as keyof SavingStateItems] === SavingState.SAVING_STATE_SAVING) {
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVED;
this.resetSaveState(configType as keyof SavingStateItems);
}
});
this.setState({savingState});
}
resetSaveState = debounce((configType: keyof SavingStateItems) => {
if (this.state.savingState[configType] !== SavingState.SAVING_STATE_SAVING) {
const savingState = Object.assign({}, this.state.savingState);
savingState[configType] = SavingState.SAVING_STATE_DONE;
this.setState({savingState});
}
}, 2000);
handleChangeShowTrayIcon = () => {
const shouldShowTrayIcon = this.showTrayIconRef.current?.checked;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
this.setState({
showTrayIcon: shouldShowTrayIcon,
});
if (window.process.platform === 'darwin' && !shouldShowTrayIcon) {
this.setState({
minimizeToTray: false,
});
}
}
handleChangeTrayIconTheme = (theme: string) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
this.setState({
trayIconTheme: theme,
});
}
handleChangeAutoStart = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: this.autostartRef.current?.checked});
this.setState({
autostart: this.autostartRef.current?.checked,
});
}
handleChangeHideOnStart = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'hideOnStart', data: this.hideOnStartRef.current?.checked});
this.setState({
hideOnStart: this.hideOnStartRef.current?.checked,
});
}
handleChangeMinimizeToTray = () => {
const shouldMinimizeToTray = (process.platform === 'win32' || this.state.showTrayIcon) && this.minimizeToTrayRef.current?.checked;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
this.setState({
minimizeToTray: shouldMinimizeToTray,
});
}
handleFlashWindow = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current?.checked ? 2 : 0,
},
});
this.setState({
notifications: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current?.checked ? 2 : 0,
},
});
}
handleBounceIcon = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
bounceIcon: this.bounceIconRef.current?.checked,
},
});
this.setState({
notifications: {
...this.state.notifications,
bounceIcon: this.bounceIconRef.current?.checked,
},
});
}
handleBounceIconType = (event: React.ChangeEvent<HTMLInputElement>) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
...this.state.notifications,
bounceIconType: event.target.value,
},
});
this.setState({
notifications: {
...this.state.notifications,
bounceIconType: event.target.value as 'critical' | 'informational',
},
});
}
handleShowUnreadBadge = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: this.showUnreadBadgeRef.current?.checked});
this.setState({
showUnreadBadge: this.showUnreadBadgeRef.current?.checked,
});
}
handleChangeUseSpellChecker = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: this.useSpellCheckerRef.current?.checked});
this.setState({
useSpellChecker: this.useSpellCheckerRef.current?.checked,
});
}
handleChangeSpellCheckerLocales = (value: OptionsType<{label: string; value: string}>, actionMeta: ActionMeta<{label: string; value: string}>) => {
switch (actionMeta.action) {
case 'select-option':
this.selectedSpellCheckerLocales = [...value];
break;
case 'remove-value':
this.selectedSpellCheckerLocales = this.selectedSpellCheckerLocales.filter((language) => language.value !== actionMeta.removedValue.value);
}
const values = this.selectedSpellCheckerLocales.map((language) => language.value);
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'spellCheckerLocales', data: values});
this.setState({
spellCheckerLocales: values,
});
}
handleChangeEnableHardwareAcceleration = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: this.enableHardwareAccelerationRef.current?.checked});
this.setState({
enableHardwareAcceleration: this.enableHardwareAccelerationRef.current?.checked,
});
}
saveDownloadLocation = (location: string) => {
if (!location) {
return;
}
this.setState({
downloadLocation: location,
});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
}
handleChangeDownloadLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
this.saveDownloadLocation(e.target.value);
}
selectDownloadLocation = () => {
if (!this.state.userOpenedDownloadDialog) {
window.ipcRenderer.invoke(GET_DOWNLOAD_LOCATION, this.state.downloadLocation).then((result) => this.saveDownloadLocation(result));
this.setState({userOpenedDownloadDialog: true});
}
this.setState({userOpenedDownloadDialog: false});
}
saveSpellCheckerURL = (): void => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'spellCheckerURL', data: this.state.spellCheckerURL});
}
resetSpellCheckerURL = (): void => {
this.setState({spellCheckerURL: undefined, allowSaveSpellCheckerURL: false});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'spellCheckerURL', data: null});
}
handleChangeSpellCheckerURL= (e: React.ChangeEvent<HTMLInputElement>): void => {
const dictionaryURL = e.target.value;
let allowSaveSpellCheckerURL;
try {
// eslint-disable-next-line no-new
new URL(dictionaryURL);
allowSaveSpellCheckerURL = true;
} catch {
allowSaveSpellCheckerURL = false;
}
this.setState({
spellCheckerURL: dictionaryURL,
allowSaveSpellCheckerURL,
});
}
handleDoubleClick = () => {
window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
}
render() {
const settingsPage = {
close: {
textDecoration: 'none',
position: 'absolute',
right: '0',
top: '5px',
fontSize: '35px',
fontWeight: 'normal',
color: '#bbb',
},
heading: {
textAlign: 'center' as const,
fontSize: '24px',
margin: '0',
padding: '1em 0',
},
sectionHeading: {
fontSize: '20px',
margin: '0',
padding: '1em 0',
display: 'inline-block',
},
sectionHeadingLink: {
marginTop: '24px',
display: 'inline-block',
fontSize: '15px',
},
footer: {
padding: '0.4em 0',
},
downloadLocationInput: {
marginRight: '3px',
marginTop: '8px',
width: '320px',
height: '34px',
padding: '0 12px',
borderRadius: '4px',
border: '1px solid #ccc',
fontWeight: 500,
},
downloadLocationButton: {
marginBottom: '4px',
},
container: {
paddingBottom: '40px',
},
};
const options = [];
// MacOS has an option in the Dock, to set the app to autostart, so we choose to not support this option for OSX
if (window.process.platform === 'win32' || window.process.platform === 'linux') {
options.push(
<FormCheck>
<FormCheck.Input
type='checkbox'
key='inputAutoStart'
id='inputAutoStart'
ref={this.autostartRef}
checked={this.state.autostart}
onChange={this.handleChangeAutoStart}
/>
{'Start app on login'}
<FormText>
{'If enabled, the app starts automatically when you log in to your machine.'}
</FormText>
</FormCheck>);
options.push(
<FormCheck>
<FormCheck.Input
type='checkbox'
key='inputHideOnStart'
id='inputHideOnStart'
ref={this.hideOnStartRef}
checked={this.state.hideOnStart}
onChange={this.handleChangeHideOnStart}
/>
{'Launch app minimized'}
<FormText>
{'If enabled, the app will start in system tray, and will not show the window on launch.'}
</FormText>
</FormCheck>);
}
options.push(
<>
<FormCheck>
<FormCheck.Input
type='checkbox'
key='inputSpellChecker'
id='inputSpellChecker'
ref={this.useSpellCheckerRef}
checked={this.state.useSpellChecker}
onChange={this.handleChangeUseSpellChecker}
/>
{'Check spelling'}
<FormText>
{'Highlight misspelled words in your messages based on your system language or language preference. '}
{'Setting takes effect after restarting the app.'}
</FormText>
</FormCheck>
{this.state.useSpellChecker &&
<ReactSelect
className='SettingsPage__spellCheckerLocalesDropdown'
classNamePrefix='SettingsPage__spellCheckerLocalesDropdown'
options={this.state.availableLanguages}
isMulti={true}
isClearable={false}
onChange={this.handleChangeSpellCheckerLocales}
value={this.selectedSpellCheckerLocales}
placeholder={'Select preferred language(s)'}
/>
}
</>,
);
if (process.platform !== 'darwin') {
if (this.state.spellCheckerURL === null || typeof this.state.spellCheckerURL === 'undefined') {
options.push(
<Button
id='editSpellcheckerURL'
key='editSpellcheckerURL'
onClick={() => this.setState({spellCheckerURL: '', allowSaveSpellCheckerURL: false})}
variant='link'
>{'Use an alternative dictionary URL'}</Button>,
);
} else {
options.push(
<div
style={settingsPage.container}
key='containerInputSpellchekerURL'
>
<input
disabled={!this.state.useSpellChecker}
style={settingsPage.downloadLocationInput}
key='inputSpellCheckerURL'
id='inputSpellCheckerURL'
ref={this.spellCheckerURLRef}
onChange={this.handleChangeSpellCheckerURL}
value={this.state.spellCheckerURL}
/>
<Button
disabled={!this.state.allowSaveSpellCheckerURL}
key='saveSpellCheckerURL'
style={settingsPage.downloadLocationButton}
id='saveSpellCheckerURL'
onClick={this.saveSpellCheckerURL}
>
<span>{'Save'}</span>
</Button>
<FormText>
{'Specify the url where dictionary definitions can be retrieved'}
</FormText>
<Button
id='revertSpellcheckerURL'
key='revertSpellcheckerURL'
onClick={this.resetSpellCheckerURL}
variant='link'
>{'Revert to default'}</Button>
</div>);
}
}
if (window.process.platform === 'darwin' || window.process.platform === 'win32') {
const TASKBAR = window.process.platform === 'win32' ? 'taskbar' : 'Dock';
options.push(
<FormCheck
key='showunreadbadge'
>
<FormCheck.Input
type='checkbox'
key='inputShowUnreadBadge'
id='inputShowUnreadBadge'
ref={this.showUnreadBadgeRef}
checked={this.state.showUnreadBadge}
onChange={this.handleShowUnreadBadge}
/>
{`Show red badge on ${TASKBAR} icon to indicate unread messages`}
<FormText>
{`Regardless of this setting, mentions are always indicated with a red badge and item count on the ${TASKBAR} icon.`}
</FormText>
</FormCheck>);
}
if (window.process.platform === 'win32' || window.process.platform === 'linux') {
options.push(
<FormCheck>
<FormCheck.Input
type='checkbox'
key='flashWindow'
id='inputflashWindow'
ref={this.flashWindowRef}
checked={!this.state.notifications || this.state.notifications.flashWindow === 2}
onChange={this.handleFlashWindow}
/>
{'Flash app window and taskbar icon when a new message is received'}
<FormText>
{'If enabled, app window and taskbar icon flash for a few seconds when a new message is received.'}
{window.process.platform === 'linux' && (
<>
<br/>
<em><strong>{'NOTE: '}</strong>{'This functionality may not work with all Linux window managers.'}</em>
</>
)}
</FormText>
</FormCheck>);
}
if (window.process.platform === 'darwin') {
options.push(
<FormGroup
key='OptionsForm'
>
<FormCheck
type='checkbox'
inline={true}
key='bounceIcon'
id='inputBounceIcon'
ref={this.bounceIconRef}
checked={this.state.notifications ? this.state.notifications.bounceIcon : false}
onChange={this.handleBounceIcon}
style={{marginRight: '10px'}}
label='Bounce the Dock icon'
/>
<FormCheck
type='radio'
inline={true}
name='bounceIconType'
value='informational'
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
defaultChecked={
!this.state.notifications ||
!this.state.notifications.bounceIconType ||
this.state.notifications.bounceIconType === 'informational'
}
onChange={this.handleBounceIconType}
label='once'
/>
{' '}
<FormCheck
type='radio'
inline={true}
name='bounceIconType'
value='critical'
disabled={!this.state.notifications || !this.state.notifications.bounceIcon}
defaultChecked={this.state.notifications && this.state.notifications.bounceIconType === 'critical'}
onChange={this.handleBounceIconType}
label={'until I open the app'}
/>
<FormText
style={{marginLeft: '20px'}}
>
{'If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.'}
</FormText>
</FormGroup>,
);
}
if (window.process.platform === 'darwin' || window.process.platform === 'linux') {
options.push(
<FormCheck
key='inputShowTrayIcon'
>
<FormCheck.Input
type='checkbox'
id='inputShowTrayIcon'
ref={this.showTrayIconRef}
checked={this.state.showTrayIcon}
onChange={this.handleChangeShowTrayIcon}
/>
{window.process.platform === 'darwin' ? `Show ${this.state.appName} icon in the menu bar` : 'Show icon in the notification area'}
<FormText>
{'Setting takes effect after restarting the app.'}
</FormText>
</FormCheck>);
}
if (window.process.platform === 'linux' || window.process.platform === 'win32') {
options.push(
<FormGroup
key='trayIconTheme'
ref={this.trayIconThemeRef}
style={{marginLeft: '20px'}}
>
{'Icon theme: '}
{window.process.platform === 'win32' &&
<>
<FormCheck
type='radio'
inline={true}
name='trayIconTheme'
value='use_system'
defaultChecked={this.state.trayIconTheme === 'use_system' || !this.state.trayIconTheme}
onChange={() => this.handleChangeTrayIconTheme('use_system')}
label={'Use system default'}
/>
{' '}
</>
}
<FormCheck
type='radio'
inline={true}
name='trayIconTheme'
value='light'
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
onChange={() => this.handleChangeTrayIconTheme('light')}
label={'Light'}
/>
{' '}
<FormCheck
type='radio'
inline={true}
name='trayIconTheme'
value='dark'
defaultChecked={this.state.trayIconTheme === 'dark'}
onChange={() => this.handleChangeTrayIconTheme('dark')}
label={'Dark'}
/>
</FormGroup>,
);
}
if (window.process.platform === 'linux' || window.process.platform === 'win32') {
options.push(
<FormCheck
key='inputMinimizeToTray'
>
<FormCheck.Input
type='checkbox'
id='inputMinimizeToTray'
ref={this.minimizeToTrayRef}
disabled={process.platform !== 'win32' && !this.state.showTrayIcon}
checked={this.state.minimizeToTray}
onChange={this.handleChangeMinimizeToTray}
/>
{'Leave app running in notification area when application window is closed'}
<FormText>
{'If enabled, the app stays running in the notification area after app window is closed.'}
{this.state.showTrayIcon ? ' Setting takes effect after restarting the app.' : ''}
</FormText>
</FormCheck>);
}
options.push(
<FormCheck
key='inputEnableHardwareAcceleration'
>
<FormCheck.Input
type='checkbox'
id='inputEnableHardwareAcceleration'
ref={this.enableHardwareAccelerationRef}
checked={this.state.enableHardwareAcceleration}
onChange={this.handleChangeEnableHardwareAcceleration}
/>
{'Use GPU hardware acceleration'}
<FormText>
{'If enabled, Mattermost UI is rendered more efficiently but can lead to decreased stability for some systems.'}
{' Setting takes effect after restarting the app.'}
</FormText>
</FormCheck>,
);
options.push(
<div
style={settingsPage.container}
key='containerDownloadLocation'
>
<hr/>
<div>{'Download Location'}</div>
<input
disabled={true}
style={settingsPage.downloadLocationInput}
key='inputDownloadLocation'
id='inputDownloadLocation'
ref={this.downloadLocationRef}
onChange={this.handleChangeDownloadLocation}
value={this.state.downloadLocation}
/>
<Button
style={settingsPage.downloadLocationButton}
id='saveDownloadLocation'
onClick={this.selectDownloadLocation}
>
<span>{'Change'}</span>
</Button>
<FormText>
{'Specify the folder where files will download.'}
</FormText>
</div>,
);
let optionsRow = null;
if (options.length > 0) {
optionsRow = (
<Row>
<Col md={12}>
<h2 style={settingsPage.sectionHeading}>{'App Options'}</h2>
<div className='IndicatorContainer'>
<AutoSaveIndicator
id='appOptionsSaveIndicator'
savingState={this.state.savingState.appOptions}
errorMessage={'Can\'t save your changes. Please try again.'}
/>
</div>
{ options.map((opt) => (
<FormGroup key={opt.key}>
{opt}
</FormGroup>
)) }
</Col>
</Row>
);
}
let waitForIpc;
if (this.state.ready) {
waitForIpc = optionsRow;
} else {
waitForIpc = (<p>{'Loading configuration...'}</p>);
}
return (
<div
className='container-fluid'
style={{
height: '100%',
}}
>
<div
style={{
overflowY: 'auto',
height: '100%',
margin: '0 -15px',
}}
>
<div style={{position: 'relative'}}>
<h1 style={settingsPage.heading}>{'Settings'}</h1>
<hr/>
</div>
<Container
className='settingsPage'
>
{waitForIpc}
</Container>
</div>
</div>
);
}
}