[MM-61717] Refresh Settings Modal without Bootstrap (#3337)
* [MM-61717] Refresh Settings Modal without Bootstrap * Fix i18n * Couple small bug fixes * E2E test updates * Fix linux tests * PR feedback * PR feedback * PR feedback * Fix the border opacity and height * PR feedback * PR feedback 2
This commit is contained in:
@@ -1,56 +0,0 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {Alert} from 'react-bootstrap';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
import {useIntl} from 'react-intl';
|
||||
|
||||
const baseClassName = 'AutoSaveIndicator';
|
||||
const leaveClassName = `${baseClassName}-Leave`;
|
||||
|
||||
export enum SavingState {
|
||||
SAVING_STATE_SAVING = 'saving',
|
||||
SAVING_STATE_SAVED = 'saved',
|
||||
SAVING_STATE_ERROR = 'error',
|
||||
SAVING_STATE_DONE = 'done',
|
||||
}
|
||||
|
||||
function getClassNameAndMessage(intl: IntlShape, savingState: SavingState, errorMessage?: React.ReactNode) {
|
||||
switch (savingState) {
|
||||
case SavingState.SAVING_STATE_SAVING:
|
||||
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saving', defaultMessage: 'Saving...'})};
|
||||
case SavingState.SAVING_STATE_SAVED:
|
||||
return {className: baseClassName, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
|
||||
case SavingState.SAVING_STATE_ERROR:
|
||||
return {className: `${baseClassName}`, message: errorMessage};
|
||||
case SavingState.SAVING_STATE_DONE:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: intl.formatMessage({id: 'renderer.components.autoSaveIndicator.saved', defaultMessage: 'Saved'})};
|
||||
default:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
|
||||
}
|
||||
}
|
||||
|
||||
type Props = {
|
||||
id?: string;
|
||||
savingState: SavingState;
|
||||
errorMessage?: React.ReactNode;
|
||||
};
|
||||
|
||||
const AutoSaveIndicator: React.FC<Props> = (props: Props) => {
|
||||
const intl = useIntl();
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(intl, savingState, errorMessage);
|
||||
return (
|
||||
<Alert
|
||||
className={className}
|
||||
{...rest}
|
||||
variant={savingState === 'error' ? 'danger' : 'info'}
|
||||
>
|
||||
{message}
|
||||
</Alert>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoSaveIndicator;
|
116
src/renderer/components/Images/server-small.tsx
Normal file
116
src/renderer/components/Images/server-small.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
|
||||
const ServerSmallImage = () => (
|
||||
<svg
|
||||
width='85'
|
||||
height='75'
|
||||
viewBox='0 0 85 75'
|
||||
fill='none'
|
||||
xmlns='http://www.w3.org/2000/svg'
|
||||
>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='0.441162'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 4.39417H4.45312V19.2177H80.5472V4.39417Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 15.2646C22.8902 15.2646 24.2175 13.9373 24.2175 12.2999C24.2175 10.6626 22.8902 9.33521 21.2528 9.33521C19.6154 9.33521 18.2881 10.6626 18.2881 12.2999C18.2881 13.9373 19.6154 15.2646 21.2528 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 15.2646C31.7837 15.2646 33.1111 13.9373 33.1111 12.2999C33.1111 10.6626 31.7837 9.33521 30.1463 9.33521C28.509 9.33521 27.1816 10.6626 27.1816 12.2999C27.1816 13.9373 28.509 15.2646 30.1463 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 15.2646C13.9956 15.2646 15.323 13.9373 15.323 12.2999C15.323 10.6626 13.9956 9.33521 12.3583 9.33521C10.7209 9.33521 9.39355 10.6626 9.39355 12.2999C9.39355 13.9373 10.7209 15.2646 12.3583 15.2646Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='26.1353'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 30.0883H4.45312V44.9118H80.5472V30.0883Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 40.9588C22.8902 40.9588 24.2175 39.6315 24.2175 37.9941C24.2175 36.3568 22.8902 35.0294 21.2528 35.0294C19.6154 35.0294 18.2881 36.3568 18.2881 37.9941C18.2881 39.6315 19.6154 40.9588 21.2528 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 40.9588C31.7837 40.9588 33.1111 39.6315 33.1111 37.9941C33.1111 36.3568 31.7837 35.0294 30.1463 35.0294C28.509 35.0294 27.1816 36.3568 27.1816 37.9941C27.1816 39.6315 28.509 40.9588 30.1463 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 40.9588C13.9956 40.9588 15.323 39.6315 15.323 37.9941C15.323 36.3568 13.9956 35.0294 12.3583 35.0294C10.7209 35.0294 9.39355 36.3568 9.39355 37.9941C9.39355 39.6315 10.7209 40.9588 12.3583 40.9588Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<rect
|
||||
x='0.5'
|
||||
y='51.8295'
|
||||
width='84'
|
||||
height='22.7294'
|
||||
rx='2.96471'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.12'
|
||||
/>
|
||||
<path
|
||||
opacity='0.48'
|
||||
d='M80.5472 55.7823H4.45312V70.6059H80.5472V55.7823Z'
|
||||
stroke='var(--center-channel-color)'
|
||||
strokeOpacity='0.75'
|
||||
strokeWidth='0.988235'
|
||||
strokeLinecap='round'
|
||||
/>
|
||||
<path
|
||||
d='M21.2528 66.6529C22.8902 66.6529 24.2175 65.3256 24.2175 63.6882C24.2175 62.0509 22.8902 60.7235 21.2528 60.7235C19.6154 60.7235 18.2881 62.0509 18.2881 63.6882C18.2881 65.3256 19.6154 66.6529 21.2528 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
d='M30.1463 66.6529C31.7837 66.6529 33.1111 65.3256 33.1111 63.6882C33.1111 62.0509 31.7837 60.7235 30.1463 60.7235C28.509 60.7235 27.1816 62.0509 27.1816 63.6882C27.1816 65.3256 28.509 66.6529 30.1463 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
<path
|
||||
opacity='0.5'
|
||||
d='M12.3583 66.6529C13.9956 66.6529 15.323 65.3256 15.323 63.6882C15.323 62.0509 13.9956 60.7235 12.3583 60.7235C10.7209 60.7235 9.39355 62.0509 9.39355 63.6882C9.39355 65.3256 10.7209 66.6529 12.3583 66.6529Z'
|
||||
fill='var(--center-channel-color)'
|
||||
fillOpacity='0.56'
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
|
||||
export default ServerSmallImage;
|
@@ -78,6 +78,29 @@ export const Modal: React.FC<Props> = ({
|
||||
const [showState, setShowState] = useState<boolean>();
|
||||
const backdropRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const onClose = useCallback(async () => {
|
||||
await onHide();
|
||||
onExited();
|
||||
}, [onExited]);
|
||||
|
||||
useEffect(() => {
|
||||
const escListener = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') {
|
||||
onClose();
|
||||
}
|
||||
};
|
||||
async function createEscListener() {
|
||||
const uncloseable = await window.desktop.modals.isModalUncloseable();
|
||||
if (!uncloseable) {
|
||||
window.addEventListener('keydown', escListener);
|
||||
}
|
||||
}
|
||||
createEscListener();
|
||||
return () => {
|
||||
window.removeEventListener('keydown', escListener);
|
||||
};
|
||||
}, [onClose]);
|
||||
|
||||
useEffect(() => {
|
||||
setShowState(show ?? true);
|
||||
}, [show]);
|
||||
@@ -91,11 +114,6 @@ export const Modal: React.FC<Props> = ({
|
||||
});
|
||||
};
|
||||
|
||||
const onClose = useCallback(async () => {
|
||||
await onHide();
|
||||
onExited();
|
||||
}, [onExited]);
|
||||
|
||||
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||
event.preventDefault();
|
||||
if (autoCloseOnCancelButton) {
|
||||
|
106
src/renderer/components/SettingsModal/SettingsModal.scss
Normal file
106
src/renderer/components/SettingsModal/SettingsModal.scss
Normal file
@@ -0,0 +1,106 @@
|
||||
@use '../../css/css_variables';
|
||||
|
||||
.SettingsModal {
|
||||
&.Modal_dialog {
|
||||
width: 832px;
|
||||
max-width: 832px;
|
||||
}
|
||||
|
||||
> .Modal_content {
|
||||
border: var(--border-light);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> .Modal_body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
|
||||
> .Modal__body {
|
||||
display: flex;
|
||||
flex: 1;
|
||||
min-height: 475px;
|
||||
|
||||
.SettingsModal__sidebar {
|
||||
flex: 0 0 auto;
|
||||
width: 232px;
|
||||
padding: 24px;
|
||||
box-sizing: border-box;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
|
||||
.SettingsModal__category {
|
||||
border: none;
|
||||
width: 100%;
|
||||
padding: 8px 12px;
|
||||
border-radius: var(--radius-s);
|
||||
background: none;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
& + .SettingsModal__category {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
&.selected {
|
||||
background-color: rgba(var(--button-bg-rgb), 0.08);
|
||||
color: var(--button-bg);
|
||||
}
|
||||
|
||||
&:hover:not(.selected) {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.04);
|
||||
color: rgba(var(--center-channel-color-rgb), 0.8);
|
||||
}
|
||||
|
||||
> span {
|
||||
font-weight: 600;
|
||||
line-height: 14px;
|
||||
font-size: 14px;
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
> i {
|
||||
font-size: 18px;
|
||||
line-height: 18px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SettingsModal__content {
|
||||
padding: 28px 32px;
|
||||
border-left: var(--border-light);
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
> div + div {
|
||||
margin-top: 24px;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SettingsModal__saving {
|
||||
padding: 8px 16px;
|
||||
margin-right: 14px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
display: flex;
|
||||
|
||||
> span {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
> i {
|
||||
font-size: 14.4px;
|
||||
line-height: 14.4px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,72 @@
|
||||
.CheckSetting {
|
||||
.CheckSetting__heading {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.CheckSetting__content {
|
||||
display: flex;
|
||||
|
||||
.CheckSetting__checkbox {
|
||||
margin-right: 10px;
|
||||
background: none;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
|
||||
border-radius: var(--radius-xs);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin-top: 2px;
|
||||
|
||||
input {
|
||||
appearance: none;
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
> i {
|
||||
display: block;
|
||||
font-size: 0;
|
||||
line-height: 16px;
|
||||
position: relative;
|
||||
right: -1px;
|
||||
top: 2px;
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: var(--radius-xs);
|
||||
transition: all ease-in-out 0.175s;
|
||||
background: none;
|
||||
color: none;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
&.checked {
|
||||
border: 1px solid var(--button-bg);
|
||||
|
||||
> i {
|
||||
right: 7px;
|
||||
top: -2px;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
font-size: 14.4px;
|
||||
background: var(--button-bg);
|
||||
color: var(--button-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.CheckSetting__label {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
|
||||
.CheckSetting__sublabel {
|
||||
margin-top: 4px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.76)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,63 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import './CheckSetting.scss';
|
||||
|
||||
export default function CheckSetting({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
heading,
|
||||
subLabel,
|
||||
...props
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: boolean) => void;
|
||||
label: React.ReactNode;
|
||||
value: boolean;
|
||||
heading?: React.ReactNode;
|
||||
subLabel?: React.ReactNode;
|
||||
}) {
|
||||
const [value, setValue] = useState(props.value);
|
||||
const save = () => {
|
||||
onSave(id, !value);
|
||||
setValue(!value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
id={`CheckSetting_${id}`}
|
||||
className='CheckSetting'
|
||||
>
|
||||
{heading && <div className='CheckSetting__heading'>{heading}</div>}
|
||||
<div className='CheckSetting__content'>
|
||||
<button
|
||||
className={classNames('CheckSetting__checkbox', {checked: value})}
|
||||
onClick={save}
|
||||
role='checkbox'
|
||||
aria-checked={value}
|
||||
aria-labelledby={`checkSetting-${id}`}
|
||||
>
|
||||
<input
|
||||
id={`checkSetting-${id}`}
|
||||
defaultChecked={value}
|
||||
type='checkbox'
|
||||
tabIndex={-1}
|
||||
disabled={true}
|
||||
/>
|
||||
<i className='icon-check'/>
|
||||
</button>
|
||||
<label
|
||||
htmlFor={`checkSetting-${id}`}
|
||||
className='CheckSetting__label'
|
||||
>
|
||||
{label}
|
||||
{subLabel && <div className='CheckSetting__sublabel'>{subLabel}</div>}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,39 @@
|
||||
.DownloadSetting {
|
||||
padding-bottom: 24px;
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.DownloadSetting__content {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
|
||||
.Input_container.disabled {
|
||||
margin-top: 0;
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.DownloadSetting__heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.DownloadSetting__label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
}
|
||||
|
||||
div + .DownloadSetting {
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import './DownloadSetting.scss';
|
||||
import Input, {SIZE} from 'renderer/components/Input';
|
||||
|
||||
export default function DownloadSetting({
|
||||
id,
|
||||
onSave,
|
||||
...props
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: string) => void;
|
||||
label: React.ReactNode;
|
||||
value: string;
|
||||
}) {
|
||||
const [value, setValue] = useState(props.value);
|
||||
|
||||
const selectDownloadLocation = async () => {
|
||||
const newDownloadLocation = await window.desktop.getDownloadLocation(props.value);
|
||||
if (!newDownloadLocation) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(id, newDownloadLocation);
|
||||
setValue(newDownloadLocation);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='DownloadSetting'>
|
||||
<h3 className='DownloadSetting__heading'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation'
|
||||
defaultMessage='Download Location'
|
||||
/>
|
||||
</h3>
|
||||
<div className='DownloadSetting__label'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.downloadLocation.description'
|
||||
defaultMessage='Specify the folder where files will download.'
|
||||
/>
|
||||
</div>
|
||||
<div className='DownloadSetting__content'>
|
||||
<Input
|
||||
disabled={true}
|
||||
value={value}
|
||||
inputSize={SIZE.MEDIUM}
|
||||
/>
|
||||
<button
|
||||
className='DownloadSetting__changeButton btn btn-tertiary'
|
||||
id='saveDownloadLocation'
|
||||
onClick={selectDownloadLocation}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='label.change'
|
||||
defaultMessage='Change'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import type {Config} from 'types/config';
|
||||
|
||||
import CheckSetting from './CheckSetting';
|
||||
import RadioSetting from './RadioSetting';
|
||||
|
||||
export default function NotificationSetting({
|
||||
onSave,
|
||||
value,
|
||||
}: {
|
||||
onSave: (key: 'notifications', value: Config['notifications']) => void;
|
||||
value: Config['notifications'];
|
||||
}) {
|
||||
if (window.process.platform === 'darwin') {
|
||||
return (
|
||||
<RadioSetting
|
||||
id='notifications.bounceIconType'
|
||||
onSave={(k, v) => onSave('notifications', {
|
||||
...value,
|
||||
bounceIcon: Boolean(v),
|
||||
bounceIconType: v,
|
||||
})}
|
||||
value={value.bounceIconType}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIconType'
|
||||
defaultMessage='Bounce the Dock icon...'
|
||||
/>
|
||||
)}
|
||||
options={[
|
||||
{
|
||||
value: 'informational',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.once'
|
||||
defaultMessage='Once'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'critical',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.untilOpenApp'
|
||||
defaultMessage='Until I open the app'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: '',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.bounceIcon.never'
|
||||
defaultMessage='Never'
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<CheckSetting
|
||||
id='flashWindow'
|
||||
onSave={(k, v) => onSave('notifications', {...value, [k]: v ? 2 : 0})}
|
||||
value={value.flashWindow === 2}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow'
|
||||
defaultMessage='Flash taskbar icon when a new message is received'
|
||||
/>
|
||||
)}
|
||||
subLabel={(
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description'
|
||||
defaultMessage='If enabled, the taskbar icon will flash for a few seconds when a new message is received.'
|
||||
/>
|
||||
{window.process.platform === 'linux' &&
|
||||
<>
|
||||
<br/>
|
||||
<em>
|
||||
<strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.note'
|
||||
defaultMessage='NOTE: '
|
||||
/>
|
||||
</strong>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.flashWindow.description.linuxFunctionality'
|
||||
defaultMessage='This functionality may not work with all Linux window managers.'
|
||||
/>
|
||||
</em>
|
||||
</>}
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
.RadioSetting {
|
||||
.RadioSetting__heading {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.RadioSetting__content {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
/* Create a custom radio button */
|
||||
.RadioSetting__radio {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
padding-left: 28px;
|
||||
cursor: pointer;
|
||||
font-size: 16px;
|
||||
user-select: none;
|
||||
border: none;
|
||||
background: none;
|
||||
color: var(--center-channel-color);
|
||||
text-align: left;
|
||||
|
||||
+ .RadioSetting__radio {
|
||||
margin-top: 12px;
|
||||
}
|
||||
|
||||
input[type="radio"] {
|
||||
display: none;
|
||||
cursor: pointer;
|
||||
|
||||
+ .RadioSetting__label::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.24);
|
||||
border-radius: 50%;
|
||||
transition: border-color ease-in 0.175s;
|
||||
}
|
||||
|
||||
+ .RadioSetting__label::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
left: 9px;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
width: 0;
|
||||
height: 0;
|
||||
border-radius: 50%;
|
||||
background-color: var(--button-bg);
|
||||
transition: width ease-in-out 0.175s, height ease-in-out 0.175s, left ease-in-out 0.175s;
|
||||
}
|
||||
|
||||
&:checked {
|
||||
+ .RadioSetting__label::before {
|
||||
border-color: var(--button-bg);
|
||||
}
|
||||
|
||||
+ .RadioSetting__label::after {
|
||||
width: 8px;
|
||||
height: 8px;
|
||||
left: 5px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.RadioSetting__label {
|
||||
display: block;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
cursor: inherit;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,62 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useState} from 'react';
|
||||
|
||||
import './RadioSetting.scss';
|
||||
|
||||
export default function RadioSetting<T extends string>({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
options,
|
||||
...props
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: T) => void;
|
||||
label: React.ReactNode;
|
||||
value: T;
|
||||
options: Array<{value: T; label: React.ReactNode}>;
|
||||
}) {
|
||||
const [value, setValue] = useState(props.value);
|
||||
|
||||
const save = (value: T) => {
|
||||
onSave(id, value);
|
||||
setValue(value);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='RadioSetting'>
|
||||
<div className='RadioSetting__heading'>{label}</div>
|
||||
<div
|
||||
className='RadioSetting__content'
|
||||
role='radiogroup'
|
||||
>
|
||||
{options.map((option, index) => (
|
||||
<button
|
||||
id={`RadioSetting_${id}_${option.value}`}
|
||||
className='RadioSetting__radio'
|
||||
key={`${index}`}
|
||||
onClick={() => save(option.value)}
|
||||
role='radio'
|
||||
aria-checked={value === option.value}
|
||||
>
|
||||
<input
|
||||
type='radio'
|
||||
value={option.value}
|
||||
name={id}
|
||||
checked={value === option.value}
|
||||
readOnly={true}
|
||||
/>
|
||||
<label
|
||||
htmlFor={`RadioSetting_${id}_${option.value}`}
|
||||
className='RadioSetting__label'
|
||||
>
|
||||
{option.label}
|
||||
</label>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,93 @@
|
||||
.SelectSetting {
|
||||
&.SelectSetting-bottomBorder {
|
||||
border-bottom: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
padding-bottom: 24px;
|
||||
}
|
||||
|
||||
.SelectSetting__select {
|
||||
margin-top: 16px;
|
||||
width: 50%;
|
||||
|
||||
.SelectSetting__select__single-value {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
.SelectSetting__select__control {
|
||||
background: var(--center-channel-bg);
|
||||
border: 1px solid rgba(var(--center-channel-color-rgb), 0.16);
|
||||
border-radius: var(--radius-s);
|
||||
box-sizing: border-box;
|
||||
|
||||
&:hover {
|
||||
border-color: rgba(var(--center-channel-color-rgb), 0.48);
|
||||
}
|
||||
|
||||
&:focus-within {
|
||||
border-color: var(--button-bg);
|
||||
border-width: 1px;
|
||||
}
|
||||
}
|
||||
|
||||
.SelectSetting__select__menu {
|
||||
background: var(--center-channel-bg);
|
||||
}
|
||||
|
||||
.SelectSetting__select__menu-portal {
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.SelectSetting__select__option {
|
||||
background: var(--center-channel-bg);
|
||||
color: var(--center-channel-color);
|
||||
|
||||
&:hover {
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.16);
|
||||
}
|
||||
}
|
||||
|
||||
.SelectSetting__select__indicator {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
|
||||
.SelectSetting__select__indicator-separator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.SelectSetting__select__multi-value {
|
||||
border-radius: var(--radius-l);
|
||||
border: none;
|
||||
background-color: rgba(var(--center-channel-color-rgb), 0.08);
|
||||
|
||||
.SelectSetting__select__multi-value__label {
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
line-height: 15px;
|
||||
color: var(--center-channel-color);
|
||||
padding: 4.5px;
|
||||
padding-left: 10px;
|
||||
}
|
||||
|
||||
.SelectSetting__select__multi-value__remove {
|
||||
padding: 0;
|
||||
margin: 4.5px 10px 4.5px 1.5px;
|
||||
border-radius: 50%;
|
||||
background: rgba(var(--center-channel-color-rgb), 0.32);
|
||||
|
||||
&:hover {
|
||||
color: inherit;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.SelectSetting__heading {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.SelectSetting__label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
}
|
@@ -0,0 +1,100 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useState} from 'react';
|
||||
import ReactSelect from 'react-select';
|
||||
import type {ActionMeta, MultiValue, PropsValue, SingleValue} from 'react-select';
|
||||
|
||||
import './SelectSetting.scss';
|
||||
|
||||
type Option = {value: string; label: string};
|
||||
|
||||
type IsMulti = {
|
||||
isMulti: true;
|
||||
value: string[];
|
||||
onSave: (key: string, value: string[]) => void;
|
||||
} | {
|
||||
isMulti: false;
|
||||
value: string;
|
||||
onSave: (key: string, value: string) => void;
|
||||
}
|
||||
type Props = IsMulti & {
|
||||
id: string;
|
||||
label: React.ReactNode;
|
||||
options: Option[];
|
||||
subLabel?: React.ReactNode;
|
||||
placeholder?: React.ReactNode;
|
||||
bottomBorder?: boolean;
|
||||
};
|
||||
|
||||
function valueToOption(value: string | string[], options: Option[]): PropsValue<Option> {
|
||||
if (Array.isArray(value)) {
|
||||
return value.map((v) => options.find((o) => o.value === v)!);
|
||||
}
|
||||
|
||||
return options.find((o) => o.value === value)!;
|
||||
}
|
||||
|
||||
export default function SelectSetting({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
options,
|
||||
isMulti,
|
||||
subLabel,
|
||||
bottomBorder,
|
||||
value: propValue,
|
||||
placeholder,
|
||||
}: Props) {
|
||||
const [value, setValue] = useState<PropsValue<Option>>(valueToOption(propValue, options));
|
||||
|
||||
const save = (newValue: PropsValue<Option>, actionMeta: ActionMeta<Option>) => {
|
||||
if (isMulti) {
|
||||
let values = [...(value as MultiValue<Option>).map((v) => v.value)];
|
||||
switch (actionMeta.action) {
|
||||
case 'select-option':
|
||||
values = [...(newValue as MultiValue<Option>).map((v) => v.value)];
|
||||
break;
|
||||
case 'remove-value':
|
||||
values = values.filter((v) => v !== actionMeta.removedValue.value);
|
||||
break;
|
||||
case 'clear':
|
||||
values = [];
|
||||
break;
|
||||
}
|
||||
|
||||
onSave(id, values);
|
||||
} else {
|
||||
const singleValue = newValue as SingleValue<Option>;
|
||||
if (!singleValue) {
|
||||
return;
|
||||
}
|
||||
onSave(id, singleValue.value);
|
||||
}
|
||||
|
||||
setValue(newValue);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className={classNames('SelectSetting', {'SelectSetting-bottomBorder': bottomBorder})}>
|
||||
<h3 className='SelectSetting__heading'>
|
||||
{label}
|
||||
</h3>
|
||||
{subLabel && <div className='SelectSetting__label'>
|
||||
{subLabel}
|
||||
</div>}
|
||||
<ReactSelect
|
||||
inputId={`selectSetting_${id}`}
|
||||
className='SelectSetting__select'
|
||||
classNamePrefix='SelectSetting__select'
|
||||
options={options}
|
||||
onChange={save}
|
||||
isMulti={isMulti}
|
||||
value={value}
|
||||
menuPosition='fixed'
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
.ServerSetting {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
|
||||
.ServerSetting__heading {
|
||||
display: flex;
|
||||
padding-bottom: 20px;
|
||||
align-items: center;
|
||||
border-bottom: 1px solid var(--center-channel-color-8, rgba(63, 67, 80, 0.08));
|
||||
|
||||
h3 {
|
||||
margin-right: auto;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
line-height: 24px;
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__server {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px;
|
||||
border-bottom: 1px solid var(--center-channel-color-8, rgba(63, 67, 80, 0.08));
|
||||
|
||||
i.icon-server-variant {
|
||||
font-size: 18px;
|
||||
|
||||
&::before {
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__serverName {
|
||||
margin-left: 12px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.ServerSetting__serverUrl {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 18px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.56);
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.btn.btn-tertiary.btn-danger:not(:hover) {
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
||||
.ServerSetting__noServers {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
height: 100%;
|
||||
|
||||
span {
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64)
|
||||
}
|
||||
|
||||
.ServerSetting__noServersTitle {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
font-weight: 600;
|
||||
margin-top: 24px;
|
||||
}
|
||||
|
||||
.ServerSetting__noServersDescription {
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
max-width: 283px;
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,179 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {useCallback, useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import ServerSmallImage from 'renderer/components/Images/server-small';
|
||||
import NewServerModal from 'renderer/components/NewServerModal';
|
||||
import RemoveServerModal from 'renderer/components/RemoveServerModal';
|
||||
|
||||
import type {Server, UniqueServer} from 'types/config';
|
||||
import type {Permissions, UniqueServerWithPermissions} from 'types/permissions';
|
||||
|
||||
import './ServerSetting.scss';
|
||||
|
||||
enum Modal {
|
||||
ADD = 1,
|
||||
EDIT,
|
||||
REMOVE,
|
||||
}
|
||||
|
||||
export default function ServerSetting() {
|
||||
const [servers, setServers] = useState<UniqueServerWithPermissions[]>([]);
|
||||
const [currentServer, setCurrentServer] = useState<UniqueServerWithPermissions>();
|
||||
const [modal, setModal] = useState<Modal>();
|
||||
|
||||
const reloadServers = useCallback(() => {
|
||||
window.desktop.getUniqueServersWithPermissions().then(setServers);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const off = window.desktop.onReloadConfiguration(reloadServers);
|
||||
reloadServers();
|
||||
|
||||
return () => off();
|
||||
}, []);
|
||||
|
||||
const closeModal = () => {
|
||||
setCurrentServer(undefined);
|
||||
setModal(undefined);
|
||||
};
|
||||
|
||||
const addServer = (server: Server) => {
|
||||
window.desktop.addServer(server);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const editServer = (server: UniqueServer, permissions?: Permissions) => {
|
||||
window.desktop.editServer(server, permissions);
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const removeServer = () => {
|
||||
if (currentServer?.server.id) {
|
||||
window.desktop.removeServer(currentServer.server.id);
|
||||
}
|
||||
closeModal();
|
||||
};
|
||||
|
||||
const showAddServerModal = () => {
|
||||
setModal(Modal.ADD);
|
||||
};
|
||||
|
||||
const showEditServerModal = (server: UniqueServerWithPermissions) => () => {
|
||||
setCurrentServer(server);
|
||||
setModal(Modal.EDIT);
|
||||
};
|
||||
|
||||
const showRemoveServerModal = (server: UniqueServerWithPermissions) => () => {
|
||||
setCurrentServer(server);
|
||||
setModal(Modal.REMOVE);
|
||||
};
|
||||
|
||||
let openModal;
|
||||
switch (modal) {
|
||||
case Modal.ADD:
|
||||
openModal = (
|
||||
<NewServerModal
|
||||
onClose={closeModal}
|
||||
onSave={addServer}
|
||||
show={true}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case Modal.EDIT:
|
||||
openModal = (
|
||||
<NewServerModal
|
||||
onClose={closeModal}
|
||||
onSave={editServer}
|
||||
editMode={true}
|
||||
show={true}
|
||||
server={currentServer?.server}
|
||||
permissions={currentServer?.permissions}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
case Modal.REMOVE:
|
||||
openModal = (
|
||||
<RemoveServerModal
|
||||
show={true}
|
||||
onHide={closeModal}
|
||||
onCancel={closeModal}
|
||||
onAccept={removeServer}
|
||||
/>
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className='ServerSetting'>
|
||||
<div className='ServerSetting__heading'>
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.title'
|
||||
defaultMessage='Servers'
|
||||
/>
|
||||
</h3>
|
||||
<button
|
||||
onClick={showAddServerModal}
|
||||
className='ServerSetting__addServer btn btn-sm btn-tertiary'
|
||||
>
|
||||
<i className='icon icon-plus'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.addAServer'
|
||||
defaultMessage='Add a server'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
{servers.length === 0 && (
|
||||
<div className='ServerSetting__noServers'>
|
||||
<ServerSmallImage/>
|
||||
<div className='ServerSetting__noServersTitle'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.noServers'
|
||||
defaultMessage='No servers added'
|
||||
/>
|
||||
</div>
|
||||
<div className='ServerSetting__noServersDescription'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.serverSetting.noServers.description'
|
||||
defaultMessage="Add a server to connect to your team's communication hub"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className='ServerSetting__serverList'>
|
||||
{(servers.map((server, index) => (
|
||||
<div
|
||||
key={`${index}`}
|
||||
className='ServerSetting__server'
|
||||
>
|
||||
<i className='icon icon-server-variant'/>
|
||||
<div className='ServerSetting__serverName'>
|
||||
{server.server.name}
|
||||
</div>
|
||||
<div className='ServerSetting__serverUrl'>
|
||||
{server.server.url}
|
||||
</div>
|
||||
<button
|
||||
onClick={showEditServerModal(server)}
|
||||
className='ServerSetting__editServer btn btn-icon btn-sm'
|
||||
>
|
||||
<i className='icon icon-pencil-outline'/>
|
||||
</button>
|
||||
<button
|
||||
onClick={showRemoveServerModal(server)}
|
||||
className='ServerSetting__removeServer btn btn-icon btn-sm btn-tertiary btn-transparent btn-danger'
|
||||
>
|
||||
<i className='icon icon-trash-can-outline'/>
|
||||
</button>
|
||||
</div>
|
||||
)))}
|
||||
</div>
|
||||
{openModal}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
@@ -0,0 +1,57 @@
|
||||
.SpellCheckerSetting {
|
||||
> .SelectSetting {
|
||||
margin-top: 24px;
|
||||
|
||||
.SelectSetting__heading {
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
|
||||
.SelectSetting__select {
|
||||
width: 100%;
|
||||
}
|
||||
};
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
font-size: 16px;
|
||||
line-height: 24px;
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative {
|
||||
.SpellCheckerSetting__alternative__content {
|
||||
display: flex;
|
||||
margin-top: 16px;
|
||||
|
||||
.Input_container {
|
||||
margin-top: 0;
|
||||
|
||||
input {
|
||||
height: 34px;
|
||||
font-size: 14px;
|
||||
line-height: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
button {
|
||||
margin-left: 8px;
|
||||
}
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative__heading {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.SpellCheckerSetting__alternative__label {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 16px;
|
||||
color: rgba(var(--center-channel-color-rgb), 0.64);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
div + .SpellCheckerSetting {
|
||||
padding-top: 24px;
|
||||
border-top: 1px solid rgba(var(--center-channel-color-rgb), 0.08);
|
||||
}
|
@@ -0,0 +1,141 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import Input, {SIZE} from 'renderer/components/Input';
|
||||
|
||||
import CheckSetting from './CheckSetting';
|
||||
import SelectSetting from './SelectSetting';
|
||||
|
||||
import './SpellCheckerSetting.scss';
|
||||
|
||||
type Option = {value: string; label: string};
|
||||
|
||||
export default function SpellCheckerSetting({
|
||||
id,
|
||||
onSave,
|
||||
label,
|
||||
options,
|
||||
subLabel,
|
||||
heading,
|
||||
value: propValue,
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: string | boolean | string[]) => void;
|
||||
label: React.ReactNode;
|
||||
options: Option[];
|
||||
subLabel?: React.ReactNode;
|
||||
heading?: React.ReactNode;
|
||||
value: boolean;
|
||||
}) {
|
||||
const [spellCheckerLocales, setSpellCheckerLocales] = useState<string[]>();
|
||||
|
||||
const [spellCheckerURL, setSpellCheckerURL] = useState<string>();
|
||||
const [editingURL, setEditingURL] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Unfortunately we need to sidestep the props for this one as it is a very special case
|
||||
window.desktop.getLocalConfiguration().then((config) => {
|
||||
setSpellCheckerLocales(config.spellCheckerLocales);
|
||||
setSpellCheckerURL(config.spellCheckerURL);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const saveSpellCheckerLocales = (key: string, newValue: string[]) => {
|
||||
onSave('spellCheckerLocales', newValue);
|
||||
setSpellCheckerLocales(newValue);
|
||||
};
|
||||
|
||||
const editURL = () => {
|
||||
if (editingURL) {
|
||||
onSave('spellCheckerURL', spellCheckerURL ?? '');
|
||||
}
|
||||
setEditingURL(!editingURL);
|
||||
};
|
||||
|
||||
if (!spellCheckerLocales) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='SpellCheckerSetting'>
|
||||
<CheckSetting
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
label={label}
|
||||
subLabel={subLabel}
|
||||
value={propValue}
|
||||
heading={heading}
|
||||
/>
|
||||
{propValue &&
|
||||
<SelectSetting
|
||||
id='spellCheckerLocales'
|
||||
onSave={saveSpellCheckerLocales}
|
||||
label={(
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.spellCheckerSetting.language'
|
||||
defaultMessage='Spell Checker Languages'
|
||||
/>
|
||||
)}
|
||||
options={options}
|
||||
value={spellCheckerLocales}
|
||||
isMulti={true}
|
||||
placeholder={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.preferredLanguages'
|
||||
defaultMessage='Select preferred language(s)'
|
||||
/>
|
||||
}
|
||||
/>
|
||||
}
|
||||
{propValue &&
|
||||
<div className='SpellCheckerSetting__alternative'>
|
||||
<h4 className='SpellCheckerSetting__alternative__heading'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.editSpellcheckUrl'
|
||||
defaultMessage='Use an alternative dictionary URL'
|
||||
/>
|
||||
</h4>
|
||||
<div className='SpellCheckerSetting__alternative__label'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.specifyURL'
|
||||
defaultMessage='Specify the url where dictionary definitions can be retrieved'
|
||||
/>
|
||||
</div>
|
||||
<div className='SpellCheckerSetting__alternative__content'>
|
||||
<Input
|
||||
disabled={!editingURL}
|
||||
value={spellCheckerURL}
|
||||
inputSize={SIZE.MEDIUM}
|
||||
onChange={(e) => setSpellCheckerURL(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className={classNames('DownloadSetting__changeButton btn', {
|
||||
'btn-primary': editingURL,
|
||||
'btn-tertiary': !editingURL,
|
||||
})}
|
||||
id='saveDownloadLocation'
|
||||
onClick={editURL}
|
||||
>
|
||||
{editingURL &&
|
||||
<FormattedMessage
|
||||
id='label.save'
|
||||
defaultMessage='Save'
|
||||
/>
|
||||
}
|
||||
{!editingURL &&
|
||||
<FormattedMessage
|
||||
id='label.change'
|
||||
defaultMessage='Change'
|
||||
/>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
@@ -0,0 +1,7 @@
|
||||
.UpdatesSetting__button {
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.UpdatesSetting__subLabel > span {
|
||||
display: block;
|
||||
}
|
@@ -0,0 +1,53 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
|
||||
import CheckSetting from './CheckSetting';
|
||||
|
||||
import './UpdatesSetting.scss';
|
||||
|
||||
export default function UpdatesSetting({
|
||||
id,
|
||||
onSave,
|
||||
value,
|
||||
}: {
|
||||
id: string;
|
||||
onSave: (key: string, value: boolean) => void;
|
||||
value: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<CheckSetting
|
||||
id={id}
|
||||
onSave={onSave}
|
||||
value={value}
|
||||
label={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic'
|
||||
defaultMessage='Automatically check for updates'
|
||||
/>
|
||||
}
|
||||
subLabel={
|
||||
<div className='UpdatesSetting__subLabel'>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.automatic.description'
|
||||
defaultMessage='If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.'
|
||||
/>
|
||||
<button
|
||||
className='UpdatesSetting__button btn btn-primary'
|
||||
id='checkForUpdatesNow'
|
||||
onClick={window.desktop.checkForUpdates}
|
||||
>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.updates.checkNow'
|
||||
defaultMessage='Check for Updates Now'
|
||||
/>
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
449
src/renderer/components/SettingsModal/definition.tsx
Normal file
449
src/renderer/components/SettingsModal/definition.tsx
Normal file
@@ -0,0 +1,449 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import {FormattedMessage} from 'react-intl';
|
||||
import type {IntlShape} from 'react-intl';
|
||||
|
||||
import {localeTranslations} from 'common/utils/constants';
|
||||
|
||||
import type {SettingsDefinition} from 'types/settings';
|
||||
|
||||
import CheckSetting from './components/CheckSetting';
|
||||
import DownloadSetting from './components/DownloadSetting';
|
||||
import NotificationSetting from './components/NotificationSetting';
|
||||
import RadioSetting from './components/RadioSetting';
|
||||
import SelectSetting from './components/SelectSetting';
|
||||
import ServerSetting from './components/ServerSetting';
|
||||
import SpellCheckerSetting from './components/SpellCheckerSetting';
|
||||
import UpdatesSetting from './components/UpdatesSetting';
|
||||
|
||||
const getLanguages = async (func: () => Promise<string[]>) => {
|
||||
return (await func()).filter((language) => localeTranslations[language]).
|
||||
map((language) => ({label: localeTranslations[language], value: language})).
|
||||
sort((a, b) => a.label.localeCompare(b.label));
|
||||
};
|
||||
|
||||
const definition: (intl: IntlShape) => Promise<SettingsDefinition> = async (intl: IntlShape) => {
|
||||
return {
|
||||
general: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.general'
|
||||
defaultMessage='General'
|
||||
/>
|
||||
),
|
||||
icon: 'settings-outline',
|
||||
settings: [
|
||||
{
|
||||
id: 'autoCheckForUpdates',
|
||||
component: UpdatesSetting,
|
||||
condition: (await window.desktop.getLocalConfiguration()).canUpgrade,
|
||||
},
|
||||
{
|
||||
id: 'downloadLocation',
|
||||
component: DownloadSetting,
|
||||
},
|
||||
{
|
||||
id: 'autostart',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'win32' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin'
|
||||
defaultMessage='Start app on login'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.startAppOnLogin.description'
|
||||
defaultMessage='If enabled, the app starts automatically when you log in to your machine.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'hideOnStart',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'win32' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized'
|
||||
defaultMessage='Launch app minimized'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.launchAppMinimized.description'
|
||||
defaultMessage='If enabled, the app will start in system tray, and will not show the window on launch.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'showTrayIcon',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'darwin' || window.process.platform === 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.show'
|
||||
defaultMessage='Show icon in the notification area'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'trayIconTheme',
|
||||
component: RadioSetting,
|
||||
condition: (window.process.platform === 'linux' || window.process.platform === 'win32') && (await window.desktop.getLocalConfiguration()).showTrayIcon,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.color'
|
||||
defaultMessage='Icon color'
|
||||
/>
|
||||
),
|
||||
options: [
|
||||
{
|
||||
value: 'use_system',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.systemDefault'
|
||||
defaultMessage='Use system default'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'light',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.light'
|
||||
defaultMessage='Light'
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
value: 'dark',
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.trayIcon.theme.dark'
|
||||
defaultMessage='Dark'
|
||||
/>
|
||||
),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'minimizeToTray',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'linux' || window.process.platform === 'win32',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray'
|
||||
defaultMessage='Leave app running in notification area when application window is closed'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.minimizeToTray.description'
|
||||
defaultMessage='If enabled, the app stays running in the notification area after app window is closed.'
|
||||
/>
|
||||
<br/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'startInFullscreen',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform !== 'linux',
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen'
|
||||
defaultMessage='Open app in full screen'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.fullscreen.description'
|
||||
defaultMessage='If enabled, the {appName} application will always open in full screen'
|
||||
values={{appName: (await window.desktop.getVersion()).name}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
notifications: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.notifications'
|
||||
defaultMessage='Notifications'
|
||||
/>
|
||||
),
|
||||
icon: 'bell-outline',
|
||||
settings: [
|
||||
{
|
||||
id: 'showUnreadBadge',
|
||||
component: CheckSetting,
|
||||
condition: window.process.platform === 'darwin' || window.process.platform === 'win32',
|
||||
props: {
|
||||
heading: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge.heading'
|
||||
defaultMessage='Unread Badge'
|
||||
/>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge'
|
||||
defaultMessage='Show red badge on {taskbar} icon to indicate unread messages'
|
||||
values={{taskbar: window.process.platform === 'win32' ? 'taskbar' : 'Dock'}}
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.showUnreadBadge.description'
|
||||
defaultMessage='Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.'
|
||||
values={{taskbar: window.process.platform === 'win32' ? 'taskbar' : 'Dock'}}
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'notifications',
|
||||
component: NotificationSetting,
|
||||
},
|
||||
],
|
||||
},
|
||||
language: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.language'
|
||||
defaultMessage='Language'
|
||||
/>
|
||||
),
|
||||
icon: 'globe',
|
||||
settings: [
|
||||
{
|
||||
id: 'appLanguage',
|
||||
component: SelectSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage'
|
||||
defaultMessage='App Language'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage.description'
|
||||
defaultMessage='The language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.'
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
placeholder: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.appLanguage.placeholder'
|
||||
defaultMessage='Use system default'
|
||||
/>
|
||||
),
|
||||
options: await getLanguages(window.desktop.getAvailableLanguages),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'useSpellChecker',
|
||||
component: SpellCheckerSetting,
|
||||
props: {
|
||||
heading: (
|
||||
<h3>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.spellChecker'
|
||||
defaultMessage='Spell Checker'
|
||||
/>
|
||||
</h3>
|
||||
),
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling'
|
||||
defaultMessage='Check spelling'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.checkSpelling.description'
|
||||
defaultMessage='Highlight misspelled words in your messages based on your system language or language preference.'
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
options: await getLanguages(window.desktop.getAvailableSpellCheckerLanguages),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
servers: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.servers'
|
||||
defaultMessage='Servers'
|
||||
/>
|
||||
),
|
||||
icon: 'server-variant',
|
||||
settings: [
|
||||
{
|
||||
id: 'teams',
|
||||
component: ServerSetting,
|
||||
},
|
||||
],
|
||||
},
|
||||
advanced: {
|
||||
title: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.advanced'
|
||||
defaultMessage='Advanced'
|
||||
/>
|
||||
),
|
||||
icon: 'tune',
|
||||
settings: [
|
||||
{
|
||||
id: 'logLevel',
|
||||
component: SelectSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel'
|
||||
defaultMessage='Logging level'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.loggingLevel.description'
|
||||
defaultMessage='Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.'
|
||||
/>
|
||||
),
|
||||
bottomBorder: true,
|
||||
options: [
|
||||
{
|
||||
value: 'error',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.error',
|
||||
defaultMessage: 'Errors (error)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'warn',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.warn',
|
||||
defaultMessage: 'Errors and Warnings (warn)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'info',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.info',
|
||||
defaultMessage: 'Info (info)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'verbose',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.verbose',
|
||||
defaultMessage: 'Verbose (verbose)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'debug',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.debug',
|
||||
defaultMessage: 'Debug (debug)',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: 'silly',
|
||||
label: intl.formatMessage({
|
||||
id: 'renderer.components.settingsPage.loggingLevel.level.silly',
|
||||
defaultMessage: 'Finest (silly)',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enableMetrics',
|
||||
component: CheckSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics'
|
||||
defaultMessage='Send anonymous usage data to your configured servers'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableMetrics.description'
|
||||
defaultMessage='Sends usage data about the application and its performance to your configured servers that accept it.'
|
||||
/>
|
||||
),
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'enableHardwareAcceleration',
|
||||
component: CheckSetting,
|
||||
props: {
|
||||
label: (
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration'
|
||||
defaultMessage='Use GPU hardware acceleration'
|
||||
/>
|
||||
),
|
||||
subLabel: (
|
||||
<>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.enableHardwareAcceleration.description'
|
||||
defaultMessage='If enabled, {appName} UI is rendered more efficiently but can lead to decreased stability for some systems.'
|
||||
values={{appName: (await window.desktop.getVersion()).name}}
|
||||
/>
|
||||
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.afterRestart'
|
||||
defaultMessage='Setting takes effect after restarting the app.'
|
||||
/>
|
||||
</>
|
||||
),
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default definition;
|
190
src/renderer/components/SettingsModal/index.tsx
Normal file
190
src/renderer/components/SettingsModal/index.tsx
Normal file
@@ -0,0 +1,190 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import createCache from '@emotion/cache';
|
||||
import type {EmotionCache} from '@emotion/react';
|
||||
import {CacheProvider} from '@emotion/react';
|
||||
import classNames from 'classnames';
|
||||
import React, {useEffect, useRef, useState, useCallback} from 'react';
|
||||
import {FormattedMessage, useIntl} from 'react-intl';
|
||||
|
||||
import {Modal} from 'renderer/components/Modal';
|
||||
|
||||
import type {Config, LocalConfiguration} from 'types/config';
|
||||
import type {SaveQueueItem, SettingsDefinition} from 'types/settings';
|
||||
|
||||
import generateDefinition from './definition';
|
||||
|
||||
import './SettingsModal.scss';
|
||||
|
||||
enum SavingState {
|
||||
SAVING = 1,
|
||||
SAVED,
|
||||
DONE
|
||||
}
|
||||
|
||||
export default function SettingsModal({
|
||||
onClose,
|
||||
}: {
|
||||
onClose: () => void;
|
||||
}) {
|
||||
const intl = useIntl();
|
||||
|
||||
const saveQueue = useRef<SaveQueueItem[]>([]);
|
||||
const saveDebounce = useRef<boolean>(false);
|
||||
const resetDebounce = useRef<boolean>(false);
|
||||
|
||||
const [savingState, setSavingState] = useState<SavingState>(SavingState.DONE);
|
||||
const [selectedCategory, setSelectedCategory] = useState<string>();
|
||||
const [config, setConfig] = useState<LocalConfiguration>();
|
||||
const [definition, setDefinition] = useState<SettingsDefinition>();
|
||||
const [cache, setCache] = useState<EmotionCache>();
|
||||
|
||||
const getConfig = useCallback(() => {
|
||||
window.desktop.getLocalConfiguration().then((result) => {
|
||||
setConfig(result);
|
||||
});
|
||||
}, []);
|
||||
|
||||
const getDefinition = useCallback(() => {
|
||||
return generateDefinition(intl).then((result) => {
|
||||
setDefinition(result);
|
||||
return result;
|
||||
});
|
||||
}, [intl, selectedCategory]);
|
||||
|
||||
const setSavingStateToDone = useCallback(() => {
|
||||
resetDebounce.current = false;
|
||||
if (savingState !== SavingState.SAVING) {
|
||||
setSavingState(SavingState.DONE);
|
||||
}
|
||||
}, [savingState]);
|
||||
|
||||
const resetSaveState = useCallback(() => {
|
||||
if (resetDebounce.current) {
|
||||
return;
|
||||
}
|
||||
resetDebounce.current = true;
|
||||
setTimeout(setSavingStateToDone, 2000);
|
||||
}, [setSavingStateToDone]);
|
||||
|
||||
const updateConfiguration = useCallback(() => {
|
||||
if (saveQueue.current.length === 0) {
|
||||
setSavingState(SavingState.SAVED);
|
||||
resetSaveState();
|
||||
}
|
||||
getConfig();
|
||||
getDefinition();
|
||||
}, [getConfig, resetSaveState]);
|
||||
|
||||
const sendSave = useCallback(() => {
|
||||
saveDebounce.current = false;
|
||||
window.desktop.updateConfiguration(saveQueue.current.splice(0, saveQueue.current.length));
|
||||
}, []);
|
||||
|
||||
const processSaveQueue = useCallback(() => {
|
||||
if (saveDebounce.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveDebounce.current = true;
|
||||
setTimeout(sendSave, 500);
|
||||
}, [sendSave]);
|
||||
|
||||
const save = useCallback((key: keyof Config, data: Config[keyof Config]) => {
|
||||
saveQueue.current.push({
|
||||
key,
|
||||
data,
|
||||
});
|
||||
setSavingState(SavingState.SAVING);
|
||||
processSaveQueue();
|
||||
}, [processSaveQueue]);
|
||||
|
||||
useEffect(() => {
|
||||
window.desktop.getNonce().then((nonce) => {
|
||||
setCache(createCache({
|
||||
key: 'react-select-cache',
|
||||
nonce,
|
||||
}));
|
||||
});
|
||||
|
||||
window.desktop.onReloadConfiguration(updateConfiguration);
|
||||
|
||||
getDefinition().then((definition) => {
|
||||
setSelectedCategory(Object.keys(definition)[0]);
|
||||
});
|
||||
getConfig();
|
||||
}, []);
|
||||
|
||||
let savingText;
|
||||
if (savingState === SavingState.SAVING) {
|
||||
savingText = (
|
||||
<div className='SettingsModal__saving'>
|
||||
<i className='icon-spinner'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.saving'
|
||||
defaultMessage='Saving...'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
} else if (savingState === SavingState.SAVED) {
|
||||
savingText = (
|
||||
<div className='SettingsModal__saving'>
|
||||
<i className='icon-check'/>
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.changesSaved'
|
||||
defaultMessage='Changes saved'
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!cache) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<CacheProvider value={cache}>
|
||||
<Modal
|
||||
id='settingsModal'
|
||||
className='SettingsModal'
|
||||
show={Boolean(config && definition && selectedCategory)}
|
||||
onExited={onClose}
|
||||
modalHeaderText={
|
||||
<FormattedMessage
|
||||
id='renderer.components.settingsPage.header'
|
||||
defaultMessage='Desktop App Settings'
|
||||
/>
|
||||
}
|
||||
headerContent={savingText}
|
||||
bodyDivider={true}
|
||||
bodyPadding={false}
|
||||
>
|
||||
<div className='SettingsModal__sidebar'>
|
||||
{definition && Object.entries(definition).map(([id, category]) => (
|
||||
<button
|
||||
id={`settingCategoryButton-${id}`}
|
||||
key={id}
|
||||
className={classNames('SettingsModal__category', {selected: id === selectedCategory})}
|
||||
onClick={() => setSelectedCategory(id)}
|
||||
>
|
||||
<i className={`icon icon-${category.icon}`}/>
|
||||
{category.title}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
<div className='SettingsModal__content'>
|
||||
{(config && definition && selectedCategory) && definition[selectedCategory].settings.map((setting) => (setting.condition ?? true) && (
|
||||
<setting.component
|
||||
key={setting.id}
|
||||
id={setting.id}
|
||||
onSave={save}
|
||||
value={config[setting.id]}
|
||||
{...setting.props}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
</Modal>
|
||||
</CacheProvider>
|
||||
);
|
||||
}
|
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user