[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:
@@ -67,14 +67,14 @@ describe('focus', function desc() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('Focus textbox tests', () => {
|
describe('Focus textbox tests', () => {
|
||||||
it('MM-T1315 should return focus to the message box when closing the settings window', async () => {
|
it('MM-T1315 should return focus to the message box when closing the settings modal', async () => {
|
||||||
this.app.evaluate(({ipcMain}, showWindow) => {
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
ipcMain.emit(showWindow);
|
ipcMain.emit(showWindow);
|
||||||
}, SHOW_SETTINGS_WINDOW);
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('.SettingsModal');
|
||||||
await settingsWindow.close();
|
await settingsWindow.close();
|
||||||
|
|
||||||
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
|
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
|
||||||
@@ -91,7 +91,7 @@ describe('focus', function desc() {
|
|||||||
textboxString.should.equal('Mattermost');
|
textboxString.should.equal('Mattermost');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T1316 should return focus to the message box when closing the settings window', async () => {
|
it('MM-T1316 should return focus to the message box when closing the Add Server modal', async () => {
|
||||||
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
const mainView = this.app.windows().find((window) => window.url().includes('index'));
|
||||||
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
|
||||||
await mainView.click('.ServerDropdownButton');
|
await mainView.click('.ServerDropdownButton');
|
||||||
|
@@ -42,9 +42,10 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
await settingsWindow.waitForSelector('#inputAutoStart', {state: expected ? 'attached' : 'detached'});
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
const existing = await settingsWindow.isVisible('#inputAutoStart');
|
await settingsWindow.waitForSelector('#CheckSetting_autostart', {state: expected ? 'attached' : 'detached'});
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_autostart');
|
||||||
existing.should.equal(expected);
|
existing.should.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -58,9 +59,10 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
await settingsWindow.waitForSelector('#inputShowTrayIcon', {state: expected ? 'attached' : 'detached'});
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
const existing = await settingsWindow.isVisible('#inputShowTrayIcon');
|
await settingsWindow.waitForSelector('#CheckSetting_showTrayIcon', {state: expected ? 'attached' : 'detached'});
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_showTrayIcon');
|
||||||
existing.should.equal(expected);
|
existing.should.equal(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -72,17 +74,18 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
await settingsWindow.click('#inputShowTrayIcon');
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.click('#CheckSetting_showTrayIcon button');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
|
|
||||||
let config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
let config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config0.showTrayIcon.should.true;
|
config0.showTrayIcon.should.true;
|
||||||
|
|
||||||
await settingsWindow.click('#inputShowTrayIcon');
|
await settingsWindow.click('#CheckSetting_showTrayIcon button');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
|
|
||||||
config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config0.showTrayIcon.should.false;
|
config0.showTrayIcon.should.false;
|
||||||
@@ -97,18 +100,20 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
await settingsWindow.click('#inputShowTrayIcon');
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
await settingsWindow.click('input[value="dark"]');
|
await settingsWindow.click('#CheckSetting_showTrayIcon button');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.click('#RadioSetting_trayIconTheme_dark');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
|
|
||||||
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config0.trayIconTheme.should.equal('dark');
|
config0.trayIconTheme.should.equal('dark');
|
||||||
|
|
||||||
await settingsWindow.click('input[value="light"]');
|
await settingsWindow.waitForSelector('.SettingsModal__saving', {state: 'detached'});
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.click('#RadioSetting_trayIconTheme_light');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
|
|
||||||
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config1.trayIconTheme.should.equal('light');
|
config1.trayIconTheme.should.equal('light');
|
||||||
@@ -125,8 +130,9 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
const existing = await settingsWindow.isVisible('#inputMinimizeToTray');
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_minimizeToTray');
|
||||||
existing.should.equal(expected);
|
existing.should.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -140,8 +146,9 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-notifications');
|
||||||
const existing = await settingsWindow.isVisible('#inputflashWindow');
|
await settingsWindow.click('#settingCategoryButton-notifications');
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_flashWindow');
|
||||||
existing.should.equal(expected);
|
existing.should.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -155,8 +162,9 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-notifications');
|
||||||
const existing = await settingsWindow.isVisible('#inputShowUnreadBadge');
|
await settingsWindow.click('#settingCategoryButton-notifications');
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_showUnreadBadge');
|
||||||
existing.should.equal(expected);
|
existing.should.equal(expected);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -169,16 +177,17 @@ describe('Settings', function desc() {
|
|||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-language');
|
||||||
const existing = await settingsWindow.isVisible('#inputSpellChecker');
|
await settingsWindow.click('#settingCategoryButton-language');
|
||||||
|
const existing = await settingsWindow.isVisible('#CheckSetting_useSpellChecker');
|
||||||
existing.should.equal(true);
|
existing.should.equal(true);
|
||||||
|
|
||||||
const selected = await settingsWindow.isChecked('#inputSpellChecker');
|
const selected = await settingsWindow.isChecked('#checkSetting-useSpellChecker');
|
||||||
selected.should.equal(true);
|
selected.should.equal(true);
|
||||||
|
|
||||||
await settingsWindow.click('#inputSpellChecker');
|
await settingsWindow.click('#CheckSetting_useSpellChecker button');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
|
|
||||||
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config1.useSpellChecker.should.equal(false);
|
config1.useSpellChecker.should.equal(false);
|
||||||
@@ -187,26 +196,28 @@ describe('Settings', function desc() {
|
|||||||
|
|
||||||
describe('Enable GPU hardware acceleration', () => {
|
describe('Enable GPU hardware acceleration', () => {
|
||||||
it('MM-T4398 should save selected option', async () => {
|
it('MM-T4398 should save selected option', async () => {
|
||||||
const ID_INPUT_ENABLE_HARDWARE_ACCELERATION = '#inputEnableHardwareAcceleration';
|
const ID_INPUT_ENABLE_HARDWARE_ACCELERATION = '#CheckSetting_enableHardwareAcceleration button';
|
||||||
this.app.evaluate(({ipcMain}, showWindow) => {
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
ipcMain.emit(showWindow);
|
ipcMain.emit(showWindow);
|
||||||
}, SHOW_SETTINGS_WINDOW);
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-advanced');
|
||||||
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
await settingsWindow.click('#settingCategoryButton-advanced');
|
||||||
|
console.log('balls');
|
||||||
|
const selected = await settingsWindow.isChecked('#checkSetting-enableHardwareAcceleration');
|
||||||
selected.should.equal(true); // default is true
|
selected.should.equal(true); // default is true
|
||||||
|
|
||||||
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config0.enableHardwareAcceleration.should.equal(false);
|
config0.enableHardwareAcceleration.should.equal(false);
|
||||||
|
|
||||||
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config1.enableHardwareAcceleration.should.equal(true);
|
config1.enableHardwareAcceleration.should.equal(true);
|
||||||
});
|
});
|
||||||
@@ -215,26 +226,27 @@ describe('Settings', function desc() {
|
|||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
describe('Enable automatic check for updates', () => {
|
describe('Enable automatic check for updates', () => {
|
||||||
it('MM-T4549 should save selected option', async () => {
|
it('MM-T4549 should save selected option', async () => {
|
||||||
const ID_INPUT_ENABLE_AUTO_UPDATES = '#inputAutoCheckForUpdates';
|
const ID_INPUT_ENABLE_AUTO_UPDATES = '#CheckSetting_autoCheckForUpdates button';
|
||||||
this.app.evaluate(({ipcMain}, showWindow) => {
|
this.app.evaluate(({ipcMain}, showWindow) => {
|
||||||
ipcMain.emit(showWindow);
|
ipcMain.emit(showWindow);
|
||||||
}, SHOW_SETTINGS_WINDOW);
|
}, SHOW_SETTINGS_WINDOW);
|
||||||
const settingsWindow = await this.app.waitForEvent('window', {
|
const settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-general');
|
||||||
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_AUTO_UPDATES);
|
await settingsWindow.click('#settingCategoryButton-general');
|
||||||
|
const selected = await settingsWindow.isChecked('#checkSetting-autoCheckForUpdates');
|
||||||
selected.should.equal(true); // default is true
|
selected.should.equal(true); // default is true
|
||||||
|
|
||||||
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
||||||
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config0.autoCheckForUpdates.should.equal(false);
|
config0.autoCheckForUpdates.should.equal(false);
|
||||||
|
|
||||||
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
|
||||||
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
|
||||||
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saved")');
|
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
|
||||||
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
const config1 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
|
||||||
config1.autoCheckForUpdates.should.equal(true);
|
config1.autoCheckForUpdates.should.equal(true);
|
||||||
});
|
});
|
||||||
|
@@ -29,9 +29,10 @@ describe('settings/keyboard_shortcuts', function desc() {
|
|||||||
settingsWindow = await this.app.waitForEvent('window', {
|
settingsWindow = await this.app.waitForEvent('window', {
|
||||||
predicate: (window) => window.url().includes('settings'),
|
predicate: (window) => window.url().includes('settings'),
|
||||||
});
|
});
|
||||||
await settingsWindow.waitForSelector('.settingsPage.container');
|
await settingsWindow.waitForSelector('#settingCategoryButton-language');
|
||||||
|
await settingsWindow.click('#settingCategoryButton-language');
|
||||||
|
|
||||||
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales');
|
||||||
await textbox.scrollIntoViewIfNeeded();
|
await textbox.scrollIntoViewIfNeeded();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -45,22 +46,22 @@ describe('settings/keyboard_shortcuts', function desc() {
|
|||||||
describe('MM-T1288 Manipulating Text', () => {
|
describe('MM-T1288 Manipulating Text', () => {
|
||||||
it('MM-T1288_1 should be able to select and deselect language in the settings window', async () => {
|
it('MM-T1288_1 should be able to select and deselect language in the settings window', async () => {
|
||||||
let textboxString;
|
let textboxString;
|
||||||
await settingsWindow.click('#inputSpellCheckerLocalesDropdown');
|
await settingsWindow.click('#selectSetting_spellCheckerLocales');
|
||||||
await settingsWindow.type('#inputSpellCheckerLocalesDropdown', 'Afrikaans');
|
await settingsWindow.type('#selectSetting_spellCheckerLocales', 'Afrikaans');
|
||||||
robot.keyTap('tab');
|
robot.keyTap('tab');
|
||||||
|
|
||||||
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
await settingsWindow.isVisible('.SettingsModal__saving');
|
||||||
|
|
||||||
textboxString = await settingsWindow.innerText('div.SettingsPage__spellCheckerLocalesDropdown__multi-value__label');
|
textboxString = await settingsWindow.innerText('.SpellCheckerSetting .SelectSetting__select__multi-value__label');
|
||||||
textboxString.should.equal('Afrikaans');
|
textboxString.should.equal('Afrikaans');
|
||||||
|
|
||||||
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
await settingsWindow.isVisible('.SettingsModal__saving');
|
||||||
|
|
||||||
await settingsWindow.click('[aria-label="Remove Afrikaans"]');
|
await settingsWindow.click('[aria-label="Remove Afrikaans"]');
|
||||||
|
|
||||||
await settingsWindow.isVisible('#appOptionsSaveIndicator');
|
await settingsWindow.isVisible('.SettingsModal__saving');
|
||||||
|
|
||||||
textboxString = await settingsWindow.inputValue('#inputSpellCheckerLocalesDropdown');
|
textboxString = await settingsWindow.inputValue('#selectSetting_spellCheckerLocales');
|
||||||
textboxString.should.equal('');
|
textboxString.should.equal('');
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -68,7 +69,7 @@ describe('settings/keyboard_shortcuts', function desc() {
|
|||||||
const textToCopy = 'Afrikaans';
|
const textToCopy = 'Afrikaans';
|
||||||
env.clipboard(textToCopy);
|
env.clipboard(textToCopy);
|
||||||
|
|
||||||
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales');
|
||||||
|
|
||||||
await textbox.selectText({force: true});
|
await textbox.selectText({force: true});
|
||||||
robot.keyTap('x', [env.cmdOrCtrl]);
|
robot.keyTap('x', [env.cmdOrCtrl]);
|
||||||
@@ -85,7 +86,7 @@ describe('settings/keyboard_shortcuts', function desc() {
|
|||||||
const textToCopy = 'Afrikaans';
|
const textToCopy = 'Afrikaans';
|
||||||
env.clipboard(textToCopy);
|
env.clipboard(textToCopy);
|
||||||
|
|
||||||
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
|
const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales');
|
||||||
|
|
||||||
await textbox.selectText({force: true});
|
await textbox.selectText({force: true});
|
||||||
robot.keyTap('c', [env.cmdOrCtrl]);
|
robot.keyTap('c', [env.cmdOrCtrl]);
|
||||||
|
41
i18n/en.json
41
i18n/en.json
@@ -148,8 +148,6 @@
|
|||||||
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray",
|
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray",
|
||||||
"modal.cancel": "Cancel",
|
"modal.cancel": "Cancel",
|
||||||
"modal.confirm": "Confirm",
|
"modal.confirm": "Confirm",
|
||||||
"renderer.components.autoSaveIndicator.saved": "Saved",
|
|
||||||
"renderer.components.autoSaveIndicator.saving": "Saving...",
|
|
||||||
"renderer.components.configureServer.cardtitle": "Enter your server details",
|
"renderer.components.configureServer.cardtitle": "Enter your server details",
|
||||||
"renderer.components.configureServer.connect.default": "Connect",
|
"renderer.components.configureServer.connect.default": "Connect",
|
||||||
"renderer.components.configureServer.connect.override": "Connect anyway",
|
"renderer.components.configureServer.connect.override": "Connect anyway",
|
||||||
@@ -212,20 +210,20 @@
|
|||||||
"renderer.components.saveButton.save": "Save",
|
"renderer.components.saveButton.save": "Save",
|
||||||
"renderer.components.saveButton.saving": "Saving",
|
"renderer.components.saveButton.saving": "Saving",
|
||||||
"renderer.components.serverDropdownButton.noServersConfigured": "No servers configured",
|
"renderer.components.serverDropdownButton.noServersConfigured": "No servers configured",
|
||||||
|
"renderer.components.settingsPage.advanced": "Advanced",
|
||||||
"renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.",
|
"renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.",
|
||||||
"renderer.components.settingsPage.appLanguage": "Set app language (beta)",
|
"renderer.components.settingsPage.appLanguage": "App Language",
|
||||||
"renderer.components.settingsPage.appLanguage.description": "Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.",
|
"renderer.components.settingsPage.appLanguage.description": "The language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.",
|
||||||
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Use system default",
|
"renderer.components.settingsPage.appLanguage.placeholder": "Use system default",
|
||||||
"renderer.components.settingsPage.appOptions": "App Options",
|
"renderer.components.settingsPage.bounceIcon.never": "Never",
|
||||||
"renderer.components.settingsPage.bounceIcon": "Bounce the Dock icon",
|
"renderer.components.settingsPage.bounceIcon.once": "Once",
|
||||||
"renderer.components.settingsPage.bounceIcon.description": "If enabled, the Dock icon bounces once or until the user opens the app when a new notification is received.",
|
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "Until I open the app",
|
||||||
"renderer.components.settingsPage.bounceIcon.once": "once",
|
"renderer.components.settingsPage.bounceIconType": "Bounce the Dock icon...",
|
||||||
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "until I open the app",
|
"renderer.components.settingsPage.changesSaved": "Changes saved",
|
||||||
"renderer.components.settingsPage.checkSpelling": "Check spelling",
|
"renderer.components.settingsPage.checkSpelling": "Check spelling",
|
||||||
"renderer.components.settingsPage.checkSpelling.description": "Highlight misspelled words in your messages based on your system language or language preference.",
|
"renderer.components.settingsPage.checkSpelling.description": "Highlight misspelled words in your messages based on your system language or language preference.",
|
||||||
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "Use an alternative dictionary URL",
|
"renderer.components.settingsPage.checkSpelling.editSpellcheckUrl": "Use an alternative dictionary URL",
|
||||||
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "Select preferred language(s)",
|
"renderer.components.settingsPage.checkSpelling.preferredLanguages": "Select preferred language(s)",
|
||||||
"renderer.components.settingsPage.checkSpelling.revertToDefault": "Revert to default",
|
|
||||||
"renderer.components.settingsPage.checkSpelling.specifyURL": "Specify the url where dictionary definitions can be retrieved",
|
"renderer.components.settingsPage.checkSpelling.specifyURL": "Specify the url where dictionary definitions can be retrieved",
|
||||||
"renderer.components.settingsPage.downloadLocation": "Download Location",
|
"renderer.components.settingsPage.downloadLocation": "Download Location",
|
||||||
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
|
||||||
@@ -237,15 +235,15 @@
|
|||||||
"renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.",
|
"renderer.components.settingsPage.flashWindow.description": "If enabled, the taskbar icon will flash for a few seconds when a new message is received.",
|
||||||
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.",
|
"renderer.components.settingsPage.flashWindow.description.linuxFunctionality": "This functionality may not work with all Linux window managers.",
|
||||||
"renderer.components.settingsPage.flashWindow.description.note": "NOTE: ",
|
"renderer.components.settingsPage.flashWindow.description.note": "NOTE: ",
|
||||||
"renderer.components.settingsPage.fullscreen": "Open app in fullscreen",
|
"renderer.components.settingsPage.fullscreen": "Open app in full screen",
|
||||||
"renderer.components.settingsPage.fullscreen.description": "If enabled, the {appName} application will always open in full screen",
|
"renderer.components.settingsPage.fullscreen.description": "If enabled, the {appName} application will always open in full screen",
|
||||||
"renderer.components.settingsPage.header": "Settings",
|
"renderer.components.settingsPage.general": "General",
|
||||||
|
"renderer.components.settingsPage.header": "Desktop App Settings",
|
||||||
|
"renderer.components.settingsPage.language": "Language",
|
||||||
"renderer.components.settingsPage.launchAppMinimized": "Launch app minimized",
|
"renderer.components.settingsPage.launchAppMinimized": "Launch app minimized",
|
||||||
"renderer.components.settingsPage.launchAppMinimized.description": "If enabled, the app will start in system tray, and will not show the window on launch.",
|
"renderer.components.settingsPage.launchAppMinimized.description": "If enabled, the app will start in system tray, and will not show the window on launch.",
|
||||||
"renderer.components.settingsPage.loadingConfig": "Loading configuration...",
|
|
||||||
"renderer.components.settingsPage.loggingLevel": "Logging level",
|
"renderer.components.settingsPage.loggingLevel": "Logging level",
|
||||||
"renderer.components.settingsPage.loggingLevel.description": "Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.",
|
"renderer.components.settingsPage.loggingLevel.description": "Logging is helpful for developers and support to isolate issues you may be encountering with the desktop app.",
|
||||||
"renderer.components.settingsPage.loggingLevel.description.subtitle": "Increasing the log level increases disk space usage and can impact performance. We recommend only increasing the log level if you are having issues.",
|
|
||||||
"renderer.components.settingsPage.loggingLevel.level.debug": "Debug (debug)",
|
"renderer.components.settingsPage.loggingLevel.level.debug": "Debug (debug)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.error": "Errors (error)",
|
"renderer.components.settingsPage.loggingLevel.level.error": "Errors (error)",
|
||||||
"renderer.components.settingsPage.loggingLevel.level.info": "Info (info)",
|
"renderer.components.settingsPage.loggingLevel.level.info": "Info (info)",
|
||||||
@@ -254,18 +252,25 @@
|
|||||||
"renderer.components.settingsPage.loggingLevel.level.warn": "Errors and Warnings (warn)",
|
"renderer.components.settingsPage.loggingLevel.level.warn": "Errors and Warnings (warn)",
|
||||||
"renderer.components.settingsPage.minimizeToTray": "Leave app running in notification area when application window is closed",
|
"renderer.components.settingsPage.minimizeToTray": "Leave app running in notification area when application window is closed",
|
||||||
"renderer.components.settingsPage.minimizeToTray.description": "If enabled, the app stays running in the notification area after app window is closed.",
|
"renderer.components.settingsPage.minimizeToTray.description": "If enabled, the app stays running in the notification area after app window is closed.",
|
||||||
"renderer.components.settingsPage.saving.error": "Can't save your changes. Please try again.",
|
"renderer.components.settingsPage.notifications": "Notifications",
|
||||||
|
"renderer.components.settingsPage.saving": "Saving...",
|
||||||
|
"renderer.components.settingsPage.servers": "Servers",
|
||||||
|
"renderer.components.settingsPage.serverSetting.addAServer": "Add a server",
|
||||||
|
"renderer.components.settingsPage.serverSetting.noServers": "No servers added",
|
||||||
|
"renderer.components.settingsPage.serverSetting.noServers.description": "Add a server to connect to your team's communication hub",
|
||||||
|
"renderer.components.settingsPage.serverSetting.title": "Servers",
|
||||||
"renderer.components.settingsPage.showUnreadBadge": "Show red badge on {taskbar} icon to indicate unread messages",
|
"renderer.components.settingsPage.showUnreadBadge": "Show red badge on {taskbar} icon to indicate unread messages",
|
||||||
"renderer.components.settingsPage.showUnreadBadge.description": "Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.",
|
"renderer.components.settingsPage.showUnreadBadge.description": "Regardless of this setting, mentions are always indicated with a red badge and item count on the {taskbar} icon.",
|
||||||
|
"renderer.components.settingsPage.showUnreadBadge.heading": "Unread Badge",
|
||||||
|
"renderer.components.settingsPage.spellChecker": "Spell Checker",
|
||||||
|
"renderer.components.settingsPage.spellCheckerSetting.language": "Spell Checker Languages",
|
||||||
"renderer.components.settingsPage.startAppOnLogin": "Start app on login",
|
"renderer.components.settingsPage.startAppOnLogin": "Start app on login",
|
||||||
"renderer.components.settingsPage.startAppOnLogin.description": "If enabled, the app starts automatically when you log in to your machine.",
|
"renderer.components.settingsPage.startAppOnLogin.description": "If enabled, the app starts automatically when you log in to your machine.",
|
||||||
"renderer.components.settingsPage.trayIcon.color": "Icon color: ",
|
"renderer.components.settingsPage.trayIcon.color": "Icon color: ",
|
||||||
"renderer.components.settingsPage.trayIcon.show": "Show icon in the notification area",
|
"renderer.components.settingsPage.trayIcon.show": "Show icon in the notification area",
|
||||||
"renderer.components.settingsPage.trayIcon.show.darwin": "Show {appName} icon in the menu bar",
|
|
||||||
"renderer.components.settingsPage.trayIcon.theme.dark": "Dark",
|
"renderer.components.settingsPage.trayIcon.theme.dark": "Dark",
|
||||||
"renderer.components.settingsPage.trayIcon.theme.light": "Light",
|
"renderer.components.settingsPage.trayIcon.theme.light": "Light",
|
||||||
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Use system default",
|
"renderer.components.settingsPage.trayIcon.theme.systemDefault": "Use system default",
|
||||||
"renderer.components.settingsPage.updates": "Updates",
|
|
||||||
"renderer.components.settingsPage.updates.automatic": "Automatically check for updates",
|
"renderer.components.settingsPage.updates.automatic": "Automatically check for updates",
|
||||||
"renderer.components.settingsPage.updates.automatic.description": "If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.",
|
"renderer.components.settingsPage.updates.automatic.description": "If enabled, updates to the Desktop App will download automatically and you will be notified when ready to install.",
|
||||||
"renderer.components.settingsPage.updates.checkNow": "Check for Updates Now",
|
"renderer.components.settingsPage.updates.checkNow": "Check for Updates Now",
|
||||||
|
@@ -19,6 +19,10 @@ import {
|
|||||||
UPDATE_SHORTCUT_MENU,
|
UPDATE_SHORTCUT_MENU,
|
||||||
UPDATE_TAB_ORDER,
|
UPDATE_TAB_ORDER,
|
||||||
VALIDATE_SERVER_URL,
|
VALIDATE_SERVER_URL,
|
||||||
|
GET_UNIQUE_SERVERS_WITH_PERMISSIONS,
|
||||||
|
ADD_SERVER,
|
||||||
|
EDIT_SERVER,
|
||||||
|
REMOVE_SERVER,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
@@ -33,7 +37,7 @@ import ModalManager from 'main/views/modalManager';
|
|||||||
import ViewManager from 'main/views/viewManager';
|
import ViewManager from 'main/views/viewManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import type {Server} from 'types/config';
|
import type {Server, UniqueServer} from 'types/config';
|
||||||
import type {Permissions, UniqueServerWithPermissions} from 'types/permissions';
|
import type {Permissions, UniqueServerWithPermissions} from 'types/permissions';
|
||||||
import type {URLValidationResult} from 'types/server';
|
import type {URLValidationResult} from 'types/server';
|
||||||
|
|
||||||
@@ -56,6 +60,11 @@ export class ServerViewState {
|
|||||||
ipcMain.handle(GET_LAST_ACTIVE, this.handleGetLastActive);
|
ipcMain.handle(GET_LAST_ACTIVE, this.handleGetLastActive);
|
||||||
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, this.handleGetOrderedViewsForServer);
|
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, this.handleGetOrderedViewsForServer);
|
||||||
ipcMain.on(UPDATE_TAB_ORDER, this.updateTabOrder);
|
ipcMain.on(UPDATE_TAB_ORDER, this.updateTabOrder);
|
||||||
|
|
||||||
|
ipcMain.handle(GET_UNIQUE_SERVERS_WITH_PERMISSIONS, this.getUniqueServersWithPermissions);
|
||||||
|
ipcMain.on(ADD_SERVER, this.handleAddServer);
|
||||||
|
ipcMain.on(EDIT_SERVER, this.handleEditServer);
|
||||||
|
ipcMain.on(REMOVE_SERVER, this.handleRemoveServer);
|
||||||
}
|
}
|
||||||
|
|
||||||
init = () => {
|
init = () => {
|
||||||
@@ -407,6 +416,51 @@ export class ServerViewState {
|
|||||||
const newView = filteredViews[nextIndex].view;
|
const newView = filteredViews[nextIndex].view;
|
||||||
ViewManager.showById(newView.id);
|
ViewManager.showById(newView.id);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private getUniqueServersWithPermissions = () => {
|
||||||
|
return ServerManager.getAllServers().
|
||||||
|
map((server) => ({
|
||||||
|
server: server.toUniqueServer(),
|
||||||
|
permissions: PermissionsManager.getForServer(server) ?? {},
|
||||||
|
}));
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleAddServer = (event: IpcMainEvent, server: Server) => {
|
||||||
|
log.debug('handleAddServer', server);
|
||||||
|
|
||||||
|
ServerManager.addServer(server);
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleEditServer = (event: IpcMainEvent, server: UniqueServer, permissions?: Permissions) => {
|
||||||
|
log.debug('handleEditServer', server, permissions);
|
||||||
|
|
||||||
|
if (!server.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!server.isPredefined) {
|
||||||
|
ServerManager.editServer(server.id, server);
|
||||||
|
}
|
||||||
|
if (permissions) {
|
||||||
|
const mattermostServer = ServerManager.getServer(server.id);
|
||||||
|
if (mattermostServer) {
|
||||||
|
PermissionsManager.setForServer(mattermostServer, permissions);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleRemoveServer = (event: IpcMainEvent, serverId: string) => {
|
||||||
|
log.debug('handleRemoveServer', serverId);
|
||||||
|
|
||||||
|
const remainingServers = ServerManager.getOrderedServers().filter((orderedServer) => serverId !== orderedServer.id);
|
||||||
|
if (this.currentServerId === serverId && remainingServers.length) {
|
||||||
|
this.currentServerId = remainingServers[0].id;
|
||||||
|
} else if (!remainingServers.length) {
|
||||||
|
delete this.currentServerId;
|
||||||
|
}
|
||||||
|
|
||||||
|
ServerManager.removeServer(serverId);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const serverViewState = new ServerViewState();
|
const serverViewState = new ServerViewState();
|
||||||
|
@@ -189,3 +189,8 @@ export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
|||||||
export const METRICS_SEND = 'metrics-send';
|
export const METRICS_SEND = 'metrics-send';
|
||||||
export const METRICS_RECEIVE = 'metrics-receive';
|
export const METRICS_RECEIVE = 'metrics-receive';
|
||||||
export const METRICS_REQUEST = 'metrics-request';
|
export const METRICS_REQUEST = 'metrics-request';
|
||||||
|
|
||||||
|
export const GET_UNIQUE_SERVERS_WITH_PERMISSIONS = 'get-unique-servers-with-permissions';
|
||||||
|
export const ADD_SERVER = 'add-server';
|
||||||
|
export const EDIT_SERVER = 'edit-server';
|
||||||
|
export const REMOVE_SERVER = 'remove-server';
|
||||||
|
@@ -38,16 +38,17 @@ function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerMo
|
|||||||
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible});
|
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible});
|
||||||
|
|
||||||
if (showWelcomeScreen) {
|
if (showWelcomeScreen) {
|
||||||
if (ModalManager.isModalDisplayed()) {
|
const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
|
||||||
|
if (welcomeScreen) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleWelcomeScreenModal();
|
handleWelcomeScreenModal();
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
|
const welcomeScreenTest = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
|
||||||
if (welcomeScreen?.view.webContents.isLoading()) {
|
if (welcomeScreenTest?.view.webContents.isLoading()) {
|
||||||
welcomeScreen?.view.webContents.once('did-finish-load', () => {
|
welcomeScreenTest?.view.webContents.once('did-finish-load', () => {
|
||||||
app.emit('e2e-app-loaded');
|
app.emit('e2e-app-loaded');
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
|
@@ -266,7 +266,7 @@ function flashFrame(flash: boolean) {
|
|||||||
MainWindow.get()?.flashFrame(flash);
|
MainWindow.get()?.flashFrame(flash);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin' && Config.notifications.bounceIcon) {
|
if (process.platform === 'darwin' && Config.notifications.bounceIcon && Config.notifications.bounceIconType) {
|
||||||
app.dock.bounce(Config.notifications.bounceIconType);
|
app.dock.bounce(Config.notifications.bounceIconType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -93,6 +93,10 @@ import {
|
|||||||
IS_DEVELOPER_MODE_ENABLED,
|
IS_DEVELOPER_MODE_ENABLED,
|
||||||
METRICS_REQUEST,
|
METRICS_REQUEST,
|
||||||
METRICS_RECEIVE,
|
METRICS_RECEIVE,
|
||||||
|
ADD_SERVER,
|
||||||
|
EDIT_SERVER,
|
||||||
|
REMOVE_SERVER,
|
||||||
|
GET_UNIQUE_SERVERS_WITH_PERMISSIONS,
|
||||||
LOAD_INCOMPATIBLE_SERVER,
|
LOAD_INCOMPATIBLE_SERVER,
|
||||||
OPEN_SERVER_UPGRADE_LINK,
|
OPEN_SERVER_UPGRADE_LINK,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
@@ -139,6 +143,10 @@ contextBridge.exposeInMainWorld('desktop', {
|
|||||||
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
|
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
|
||||||
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
|
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
|
||||||
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
|
validateServerURL: (url, currentId) => ipcRenderer.invoke(VALIDATE_SERVER_URL, url, currentId),
|
||||||
|
getUniqueServersWithPermissions: () => ipcRenderer.invoke(GET_UNIQUE_SERVERS_WITH_PERMISSIONS),
|
||||||
|
addServer: (server) => ipcRenderer.send(ADD_SERVER, server),
|
||||||
|
editServer: (server, permissions) => ipcRenderer.send(EDIT_SERVER, server, permissions),
|
||||||
|
removeServer: (serverId) => ipcRenderer.send(REMOVE_SERVER, serverId),
|
||||||
|
|
||||||
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
|
||||||
getVersion: () => ipcRenderer.invoke(GET_APP_INFO),
|
getVersion: () => ipcRenderer.invoke(GET_APP_INFO),
|
||||||
@@ -152,7 +160,10 @@ contextBridge.exposeInMainWorld('desktop', {
|
|||||||
getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION),
|
getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION),
|
||||||
|
|
||||||
onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()),
|
onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()),
|
||||||
onReloadConfiguration: (listener) => ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()),
|
onReloadConfiguration: (listener) => {
|
||||||
|
ipcRenderer.on(RELOAD_CONFIGURATION, () => listener());
|
||||||
|
return () => ipcRenderer.off(RELOAD_CONFIGURATION, listener);
|
||||||
|
},
|
||||||
onDarkModeChange: (listener) => ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => listener(darkMode)),
|
onDarkModeChange: (listener) => ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => listener(darkMode)),
|
||||||
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
|
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
|
||||||
onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)),
|
onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)),
|
||||||
@@ -248,18 +259,6 @@ contextBridge.exposeInMainWorld('desktop', {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// TODO: This is for modals only, should probably move this out for them
|
|
||||||
const createKeyDownListener = () => {
|
|
||||||
ipcRenderer.invoke(GET_MODAL_UNCLOSEABLE).then((uncloseable) => {
|
|
||||||
window.addEventListener('keydown', (e) => {
|
|
||||||
if (e.key === 'Escape' && !uncloseable) {
|
|
||||||
ipcRenderer.send(MODAL_CANCEL);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
createKeyDownListener();
|
|
||||||
|
|
||||||
ipcRenderer.on(METRICS_REQUEST, async (_, name) => {
|
ipcRenderer.on(METRICS_REQUEST, async (_, name) => {
|
||||||
const memory = await process.getProcessMemoryInfo();
|
const memory = await process.getProcessMemoryInfo();
|
||||||
ipcRenderer.send(METRICS_RECEIVE, name, {cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private});
|
ipcRenderer.send(METRICS_RECEIVE, name, {cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private});
|
||||||
|
@@ -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 [showState, setShowState] = useState<boolean>();
|
||||||
const backdropRef = useRef<HTMLDivElement>(null);
|
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(() => {
|
useEffect(() => {
|
||||||
setShowState(show ?? true);
|
setShowState(show ?? true);
|
||||||
}, [show]);
|
}, [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>) => {
|
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (autoCloseOnCancelButton) {
|
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
@@ -133,4 +133,9 @@
|
|||||||
--error-text: #da6c6e;
|
--error-text: #da6c6e;
|
||||||
--mention-highlight-bg: #0d6e6e;
|
--mention-highlight-bg: #0d6e6e;
|
||||||
--mention-highlight-link: #a4f4f4;
|
--mention-highlight-link: #a4f4f4;
|
||||||
|
|
||||||
|
/* Border variables */
|
||||||
|
--border-default: solid 1px rgba(var(--center-channel-color-rgb), 0.12);
|
||||||
|
--border-light: solid 1px rgba(var(--center-channel-color-rgb), 0.08);
|
||||||
|
--border-dark: solid 1px rgba(var(--center-channel-color-rgb), 0.16);
|
||||||
}
|
}
|
||||||
|
@@ -12,6 +12,10 @@
|
|||||||
|
|
||||||
.Modal_body {
|
.Modal_body {
|
||||||
margin-bottom: 12px;
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
.Modal__body {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,97 +0,0 @@
|
|||||||
@use 'sass:meta';
|
|
||||||
|
|
||||||
.darkMode {
|
|
||||||
@include meta.load-css('bootstrap-dark/src/bootstrap-dark.css');
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
> div.modal {
|
|
||||||
color: #fff;
|
|
||||||
|
|
||||||
.modal-header .modal-title, .modal-header .close {
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
.modal-content {
|
|
||||||
background-color: #191B1F;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__control {
|
|
||||||
background: #242a30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__menu {
|
|
||||||
background: #242a30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option {
|
|
||||||
background: #242a30;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsPage__spellCheckerLocalesDropdown .SettingsPage__spellCheckerLocalesDropdown__option:hover {
|
|
||||||
background: rgba(255, 255, 255, 0.16);
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsModal .modal-body {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--light) rgba(255, 255, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toggle___switch {
|
|
||||||
background: rgba(var(--center-channel-bg-rgb), 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toggle___switch.disabled {
|
|
||||||
background: rgba(var(--center-channel-bg-rgb), 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input {
|
|
||||||
color: var(--button-color);
|
|
||||||
background-color: unset;
|
|
||||||
|
|
||||||
&::placeholder {
|
|
||||||
color: rgba(var(--button-color-rgb), 0.56);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input_wrapper {
|
|
||||||
color: rgba(var(--button-color-rgb), 0.56);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input___info {
|
|
||||||
color: rgba(var(--button-color-rgb), 0.56);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input_fieldset {
|
|
||||||
background-color: #191B1F;
|
|
||||||
border: 1px solid rgba(#fff, 0.16);
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
border-color: rgba(#fff, 0.48);
|
|
||||||
}
|
|
||||||
|
|
||||||
&:focus-within {
|
|
||||||
border-color: #fff;
|
|
||||||
box-shadow: inset 0 0 0 1px #fff;
|
|
||||||
color: var(--button-color);
|
|
||||||
|
|
||||||
.Input_legend {
|
|
||||||
color: var(--button-color);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.Input_legend {
|
|
||||||
background-color: #191B1F;
|
|
||||||
color: rgba(var(--button-color-rgb), 0.64);
|
|
||||||
}
|
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
.Input_fieldset {
|
|
||||||
background: rgba(var(--button-color-rgb), 0.08);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@@ -1,12 +0,0 @@
|
|||||||
@import url("components/index.css");
|
|
||||||
@import url("fonts.css");
|
|
||||||
@import '~@mattermost/compass-icons/css/compass-icons.css';
|
|
||||||
|
|
||||||
body {
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #166de0;
|
|
||||||
border-color: #166de0;
|
|
||||||
}
|
|
@@ -1,65 +0,0 @@
|
|||||||
.CloseButton:hover span {
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.IndicatorContainer {
|
|
||||||
display: inline-block;
|
|
||||||
padding: 1em;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AutoSaveIndicator {
|
|
||||||
padding: 5px 15px;
|
|
||||||
margin: 0 0 0 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.AutoSaveIndicator.AutoSaveIndicator-Leave {
|
|
||||||
opacity: 0;
|
|
||||||
transition: opacity 1s cubic-bezier(0.19, 1, 0.22, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
.checkbox > label {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
|
||||||
overflow-x: hidden;
|
|
||||||
overflow-y: scroll;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#content {
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.btn-primary {
|
|
||||||
background-color: #166de0;
|
|
||||||
border-color: #166de0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.SettingsPage__spellCheckerLocalesDropdown {
|
|
||||||
margin-top: 8px;
|
|
||||||
margin-left: 16px;
|
|
||||||
width: 50%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsModal .modal-content {
|
|
||||||
padding: 16px;
|
|
||||||
max-height: calc(100vh - 50px);
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsModal .modal-body {
|
|
||||||
overflow-y: auto;
|
|
||||||
overflow-x: hidden;
|
|
||||||
scrollbar-width: thin;
|
|
||||||
scrollbar-color: var(--dark) rgba(255, 255, 255, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#settingsModal .modal-header {
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (min-width: 862px) {
|
|
||||||
#settingsModal {
|
|
||||||
max-width: 786px;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,15 +1,10 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
|
||||||
import 'renderer/css/index.css';
|
|
||||||
import 'renderer/css/settings.css';
|
|
||||||
import 'renderer/css/modals-dark.scss';
|
|
||||||
|
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom';
|
import ReactDOM from 'react-dom';
|
||||||
|
|
||||||
import SettingsPage from '../../components/SettingsPage';
|
import SettingsModal from '../../components/SettingsModal';
|
||||||
import IntlProvider from '../../intl_provider';
|
import IntlProvider from '../../intl_provider';
|
||||||
import setupDarkMode from '../darkMode';
|
import setupDarkMode from '../darkMode';
|
||||||
|
|
||||||
@@ -23,8 +18,7 @@ const start = async () => {
|
|||||||
ReactDOM.render(
|
ReactDOM.render(
|
||||||
(
|
(
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<SettingsPage
|
<SettingsModal
|
||||||
show={true}
|
|
||||||
onClose={onClose}
|
onClose={onClose}
|
||||||
/>
|
/>
|
||||||
</IntlProvider>
|
</IntlProvider>
|
||||||
|
@@ -41,7 +41,7 @@ export type ConfigV3 = {
|
|||||||
notifications: {
|
notifications: {
|
||||||
flashWindow: number;
|
flashWindow: number;
|
||||||
bounceIcon: boolean;
|
bounceIcon: boolean;
|
||||||
bounceIconType: 'critical' | 'informational';
|
bounceIconType: '' | 'critical' | 'informational';
|
||||||
};
|
};
|
||||||
showUnreadBadge: boolean;
|
showUnreadBadge: boolean;
|
||||||
useSpellChecker: boolean;
|
useSpellChecker: boolean;
|
||||||
|
@@ -1,12 +1,13 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import type {CombinedConfig} from './config';
|
import type {ReactNode, ComponentType, ComponentProps} from 'react';
|
||||||
|
|
||||||
|
import type {Config} from './config';
|
||||||
|
|
||||||
export type SaveQueueItem = {
|
export type SaveQueueItem = {
|
||||||
configType: 'updates' | 'appOptions';
|
key: keyof Config;
|
||||||
key: keyof CombinedConfig;
|
data: Config[keyof Config];
|
||||||
data: CombinedConfig[keyof CombinedConfig];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export type DeveloperSettings = {
|
export type DeveloperSettings = {
|
||||||
@@ -15,3 +16,16 @@ export type DeveloperSettings = {
|
|||||||
disableUserActivityMonitor?: boolean;
|
disableUserActivityMonitor?: boolean;
|
||||||
disableContextMenu?: boolean;
|
disableContextMenu?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type SettingsDefinition = Record<string, SettingCategory>;
|
||||||
|
export type SettingCategory = {
|
||||||
|
title: ReactNode;
|
||||||
|
icon: string;
|
||||||
|
settings: Setting[];
|
||||||
|
};
|
||||||
|
export type Setting = {
|
||||||
|
id: keyof Config;
|
||||||
|
component: ComponentType<any>;
|
||||||
|
condition?: boolean;
|
||||||
|
props?: ComponentProps<Setting['component']>;
|
||||||
|
};
|
||||||
|
@@ -3,8 +3,9 @@
|
|||||||
|
|
||||||
import type {ipcRenderer, Rectangle} from 'electron/renderer';
|
import type {ipcRenderer, Rectangle} from 'electron/renderer';
|
||||||
|
|
||||||
import type {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
|
import type {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer, Server} from './config';
|
||||||
import type {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
|
import type {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
|
||||||
|
import type {UniqueServerWithPermissions, Permissions} from './permissions';
|
||||||
import type {URLValidationResult} from './server';
|
import type {URLValidationResult} from './server';
|
||||||
import type {SaveQueueItem} from './settings';
|
import type {SaveQueueItem} from './settings';
|
||||||
|
|
||||||
@@ -55,6 +56,10 @@ declare global {
|
|||||||
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
|
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
|
||||||
onUpdateServers: (listener: () => void) => void;
|
onUpdateServers: (listener: () => void) => void;
|
||||||
validateServerURL: (url: string, currentId?: string) => Promise<URLValidationResult>;
|
validateServerURL: (url: string, currentId?: string) => Promise<URLValidationResult>;
|
||||||
|
getUniqueServersWithPermissions: () => Promise<UniqueServerWithPermissions[]>;
|
||||||
|
addServer: (server: Server) => void;
|
||||||
|
editServer: (server: UniqueServer, permissions?: Permissions) => void;
|
||||||
|
removeServer: (serverId: string) => void;
|
||||||
|
|
||||||
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
|
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
|
||||||
getVersion: () => Promise<{name: string; version: string}>;
|
getVersion: () => Promise<{name: string; version: string}>;
|
||||||
@@ -63,12 +68,12 @@ declare global {
|
|||||||
getFullScreenStatus: () => Promise<boolean>;
|
getFullScreenStatus: () => Promise<boolean>;
|
||||||
getAvailableSpellCheckerLanguages: () => Promise<string[]>;
|
getAvailableSpellCheckerLanguages: () => Promise<string[]>;
|
||||||
getAvailableLanguages: () => Promise<string[]>;
|
getAvailableLanguages: () => Promise<string[]>;
|
||||||
getLocalConfiguration: () => Promise<LocalConfiguration[keyof LocalConfiguration] | Partial<LocalConfiguration>>;
|
getLocalConfiguration: () => Promise<LocalConfiguration>;
|
||||||
getDownloadLocation: (downloadLocation?: string) => Promise<string>;
|
getDownloadLocation: (downloadLocation?: string) => Promise<string>;
|
||||||
getLanguageInformation: () => Promise<Language>;
|
getLanguageInformation: () => Promise<Language>;
|
||||||
|
|
||||||
onSynchronizeConfig: (listener: () => void) => void;
|
onSynchronizeConfig: (listener: () => void) => void;
|
||||||
onReloadConfiguration: (listener: () => void) => void;
|
onReloadConfiguration: (listener: () => void) => () => void;
|
||||||
onDarkModeChange: (listener: (darkMode: boolean) => void) => void;
|
onDarkModeChange: (listener: (darkMode: boolean) => void) => void;
|
||||||
onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
|
onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
|
||||||
onLoadSuccess: (listener: (viewId: string) => void) => void;
|
onLoadSuccess: (listener: (viewId: string) => void) => void;
|
||||||
|
Reference in New Issue
Block a user