[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:
Devin Binnie
2025-02-27 15:51:49 -05:00
committed by GitHub
parent a4019ddd72
commit 5d7374971c
40 changed files with 2294 additions and 1669 deletions

View File

@@ -67,14 +67,14 @@ describe('focus', function desc() {
});
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) => {
ipcMain.emit(showWindow);
}, SHOW_SETTINGS_WINDOW);
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
await settingsWindow.waitForSelector('.SettingsModal');
await settingsWindow.close();
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
@@ -91,7 +91,7 @@ describe('focus', function desc() {
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 dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.ServerDropdownButton');

View File

@@ -42,9 +42,10 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
await settingsWindow.waitForSelector('#inputAutoStart', {state: expected ? 'attached' : 'detached'});
const existing = await settingsWindow.isVisible('#inputAutoStart');
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
await settingsWindow.waitForSelector('#CheckSetting_autostart', {state: expected ? 'attached' : 'detached'});
const existing = await settingsWindow.isVisible('#CheckSetting_autostart');
existing.should.equal(expected);
});
});
@@ -58,9 +59,10 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
await settingsWindow.waitForSelector('#inputShowTrayIcon', {state: expected ? 'attached' : 'detached'});
const existing = await settingsWindow.isVisible('#inputShowTrayIcon');
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
await settingsWindow.waitForSelector('#CheckSetting_showTrayIcon', {state: expected ? 'attached' : 'detached'});
const existing = await settingsWindow.isVisible('#CheckSetting_showTrayIcon');
existing.should.equal(expected);
});
@@ -72,17 +74,18 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
await settingsWindow.click('#inputShowTrayIcon');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
await settingsWindow.click('#CheckSetting_showTrayIcon button');
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'));
config0.showTrayIcon.should.true;
await settingsWindow.click('#inputShowTrayIcon');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
await settingsWindow.click('#CheckSetting_showTrayIcon button');
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Saving...")');
await settingsWindow.waitForSelector('.SettingsModal__saving :text("Changes saved")');
config0 = JSON.parse(fs.readFileSync(env.configFilePath, 'utf-8'));
config0.showTrayIcon.should.false;
@@ -97,18 +100,20 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
await settingsWindow.click('#inputShowTrayIcon');
await settingsWindow.click('input[value="dark"]');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
await settingsWindow.click('#CheckSetting_showTrayIcon button');
await settingsWindow.click('#RadioSetting_trayIconTheme_dark');
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'));
config0.trayIconTheme.should.equal('dark');
await settingsWindow.click('input[value="light"]');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
await settingsWindow.waitForSelector('.SettingsModal__saving', {state: 'detached'});
await settingsWindow.click('#RadioSetting_trayIconTheme_light');
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'));
config1.trayIconTheme.should.equal('light');
@@ -125,8 +130,9 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const existing = await settingsWindow.isVisible('#inputMinimizeToTray');
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
const existing = await settingsWindow.isVisible('#CheckSetting_minimizeToTray');
existing.should.equal(expected);
});
});
@@ -140,8 +146,9 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const existing = await settingsWindow.isVisible('#inputflashWindow');
await settingsWindow.waitForSelector('#settingCategoryButton-notifications');
await settingsWindow.click('#settingCategoryButton-notifications');
const existing = await settingsWindow.isVisible('#CheckSetting_flashWindow');
existing.should.equal(expected);
});
});
@@ -155,8 +162,9 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const existing = await settingsWindow.isVisible('#inputShowUnreadBadge');
await settingsWindow.waitForSelector('#settingCategoryButton-notifications');
await settingsWindow.click('#settingCategoryButton-notifications');
const existing = await settingsWindow.isVisible('#CheckSetting_showUnreadBadge');
existing.should.equal(expected);
});
});
@@ -169,16 +177,17 @@ describe('Settings', function desc() {
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const existing = await settingsWindow.isVisible('#inputSpellChecker');
await settingsWindow.waitForSelector('#settingCategoryButton-language');
await settingsWindow.click('#settingCategoryButton-language');
const existing = await settingsWindow.isVisible('#CheckSetting_useSpellChecker');
existing.should.equal(true);
const selected = await settingsWindow.isChecked('#inputSpellChecker');
const selected = await settingsWindow.isChecked('#checkSetting-useSpellChecker');
selected.should.equal(true);
await settingsWindow.click('#inputSpellChecker');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saved")');
await settingsWindow.click('#CheckSetting_useSpellChecker button');
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'));
config1.useSpellChecker.should.equal(false);
@@ -187,26 +196,28 @@ describe('Settings', function desc() {
describe('Enable GPU hardware acceleration', () => {
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) => {
ipcMain.emit(showWindow);
}, SHOW_SETTINGS_WINDOW);
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
await settingsWindow.waitForSelector('#settingCategoryButton-advanced');
await settingsWindow.click('#settingCategoryButton-advanced');
console.log('balls');
const selected = await settingsWindow.isChecked('#checkSetting-enableHardwareAcceleration');
selected.should.equal(true); // default is true
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
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'));
config0.enableHardwareAcceleration.should.equal(false);
await settingsWindow.click(ID_INPUT_ENABLE_HARDWARE_ACCELERATION);
await settingsWindow.waitForSelector('.appOptionsSaveIndicator :text("Saving...")');
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'));
config1.enableHardwareAcceleration.should.equal(true);
});
@@ -215,26 +226,27 @@ describe('Settings', function desc() {
if (process.platform !== 'darwin') {
describe('Enable automatic check for updates', () => {
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) => {
ipcMain.emit(showWindow);
}, SHOW_SETTINGS_WINDOW);
const settingsWindow = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('settings'),
});
await settingsWindow.waitForSelector('.settingsPage.container');
const selected = await settingsWindow.isChecked(ID_INPUT_ENABLE_AUTO_UPDATES);
await settingsWindow.waitForSelector('#settingCategoryButton-general');
await settingsWindow.click('#settingCategoryButton-general');
const selected = await settingsWindow.isChecked('#checkSetting-autoCheckForUpdates');
selected.should.equal(true); // default is true
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.updatesSaveIndicator :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'));
config0.autoCheckForUpdates.should.equal(false);
await settingsWindow.click(ID_INPUT_ENABLE_AUTO_UPDATES);
await settingsWindow.waitForSelector('.updatesSaveIndicator :text("Saving...")');
await settingsWindow.waitForSelector('.updatesSaveIndicator :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'));
config1.autoCheckForUpdates.should.equal(true);
});

View File

@@ -29,9 +29,10 @@ describe('settings/keyboard_shortcuts', function desc() {
settingsWindow = await this.app.waitForEvent('window', {
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();
});
@@ -45,22 +46,22 @@ describe('settings/keyboard_shortcuts', function desc() {
describe('MM-T1288 Manipulating Text', () => {
it('MM-T1288_1 should be able to select and deselect language in the settings window', async () => {
let textboxString;
await settingsWindow.click('#inputSpellCheckerLocalesDropdown');
await settingsWindow.type('#inputSpellCheckerLocalesDropdown', 'Afrikaans');
await settingsWindow.click('#selectSetting_spellCheckerLocales');
await settingsWindow.type('#selectSetting_spellCheckerLocales', 'Afrikaans');
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');
await settingsWindow.isVisible('#appOptionsSaveIndicator');
await settingsWindow.isVisible('.SettingsModal__saving');
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('');
});
@@ -68,7 +69,7 @@ describe('settings/keyboard_shortcuts', function desc() {
const textToCopy = 'Afrikaans';
env.clipboard(textToCopy);
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales');
await textbox.selectText({force: true});
robot.keyTap('x', [env.cmdOrCtrl]);
@@ -85,7 +86,7 @@ describe('settings/keyboard_shortcuts', function desc() {
const textToCopy = 'Afrikaans';
env.clipboard(textToCopy);
const textbox = await settingsWindow.waitForSelector('#inputSpellCheckerLocalesDropdown');
const textbox = await settingsWindow.waitForSelector('#selectSetting_spellCheckerLocales');
await textbox.selectText({force: true});
robot.keyTap('c', [env.cmdOrCtrl]);

View File

@@ -148,8 +148,6 @@
"main.windows.mainWindow.minimizeToTray.dialog.title": "Minimize to Tray",
"modal.cancel": "Cancel",
"modal.confirm": "Confirm",
"renderer.components.autoSaveIndicator.saved": "Saved",
"renderer.components.autoSaveIndicator.saving": "Saving...",
"renderer.components.configureServer.cardtitle": "Enter your server details",
"renderer.components.configureServer.connect.default": "Connect",
"renderer.components.configureServer.connect.override": "Connect anyway",
@@ -212,20 +210,20 @@
"renderer.components.saveButton.save": "Save",
"renderer.components.saveButton.saving": "Saving",
"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.appLanguage": "Set app language (beta)",
"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.useSystemDefault": "Use system default",
"renderer.components.settingsPage.appOptions": "App Options",
"renderer.components.settingsPage.bounceIcon": "Bounce the Dock icon",
"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.once": "once",
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "until I open the app",
"renderer.components.settingsPage.appLanguage": "App Language",
"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.placeholder": "Use system default",
"renderer.components.settingsPage.bounceIcon.never": "Never",
"renderer.components.settingsPage.bounceIcon.once": "Once",
"renderer.components.settingsPage.bounceIcon.untilOpenApp": "Until I open the app",
"renderer.components.settingsPage.bounceIconType": "Bounce the Dock icon...",
"renderer.components.settingsPage.changesSaved": "Changes saved",
"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.editSpellcheckUrl": "Use an alternative dictionary URL",
"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.downloadLocation": "Download Location",
"renderer.components.settingsPage.downloadLocation.description": "Specify the folder where files will download.",
@@ -239,13 +237,13 @@
"renderer.components.settingsPage.flashWindow.description.note": "NOTE: ",
"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.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.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.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.error": "Errors (error)",
"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.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.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.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.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.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.light": "Light",
"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.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",

View File

@@ -19,6 +19,10 @@ import {
UPDATE_SHORTCUT_MENU,
UPDATE_TAB_ORDER,
VALIDATE_SERVER_URL,
GET_UNIQUE_SERVERS_WITH_PERMISSIONS,
ADD_SERVER,
EDIT_SERVER,
REMOVE_SERVER,
} from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log';
@@ -33,7 +37,7 @@ import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager';
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 {URLValidationResult} from 'types/server';
@@ -56,6 +60,11 @@ export class ServerViewState {
ipcMain.handle(GET_LAST_ACTIVE, this.handleGetLastActive);
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, this.handleGetOrderedViewsForServer);
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 = () => {
@@ -407,6 +416,51 @@ export class ServerViewState {
const newView = filteredViews[nextIndex].view;
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();

View File

@@ -189,3 +189,8 @@ export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
export const METRICS_SEND = 'metrics-send';
export const METRICS_RECEIVE = 'metrics-receive';
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';

View File

@@ -38,16 +38,17 @@ function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerMo
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible});
if (showWelcomeScreen) {
if (ModalManager.isModalDisplayed()) {
const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
if (welcomeScreen) {
return;
}
handleWelcomeScreenModal();
if (process.env.NODE_ENV === 'test') {
const welcomeScreen = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
if (welcomeScreen?.view.webContents.isLoading()) {
welcomeScreen?.view.webContents.once('did-finish-load', () => {
const welcomeScreenTest = ModalManager.modalQueue.find((modal) => modal.key === 'welcomeScreen');
if (welcomeScreenTest?.view.webContents.isLoading()) {
welcomeScreenTest?.view.webContents.once('did-finish-load', () => {
app.emit('e2e-app-loaded');
});
} else {

View File

@@ -266,7 +266,7 @@ function flashFrame(flash: boolean) {
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);
}
}

View File

@@ -93,6 +93,10 @@ import {
IS_DEVELOPER_MODE_ENABLED,
METRICS_REQUEST,
METRICS_RECEIVE,
ADD_SERVER,
EDIT_SERVER,
REMOVE_SERVER,
GET_UNIQUE_SERVERS_WITH_PERMISSIONS,
LOAD_INCOMPATIBLE_SERVER,
OPEN_SERVER_UPGRADE_LINK,
} from 'common/communication';
@@ -139,6 +143,10 @@ contextBridge.exposeInMainWorld('desktop', {
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
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),
getVersion: () => ipcRenderer.invoke(GET_APP_INFO),
@@ -152,7 +160,10 @@ contextBridge.exposeInMainWorld('desktop', {
getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION),
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)),
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
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) => {
const memory = await process.getProcessMemoryInfo();
ipcRenderer.send(METRICS_RECEIVE, name, {cpu: process.getCPUUsage().percentCPUUsage, memory: memory.residentSet ?? memory.private});

View File

@@ -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;

View 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;

View File

@@ -78,6 +78,29 @@ export const Modal: React.FC<Props> = ({
const [showState, setShowState] = useState<boolean>();
const backdropRef = useRef<HTMLDivElement>(null);
const onClose = useCallback(async () => {
await onHide();
onExited();
}, [onExited]);
useEffect(() => {
const escListener = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
onClose();
}
};
async function createEscListener() {
const uncloseable = await window.desktop.modals.isModalUncloseable();
if (!uncloseable) {
window.addEventListener('keydown', escListener);
}
}
createEscListener();
return () => {
window.removeEventListener('keydown', escListener);
};
}, [onClose]);
useEffect(() => {
setShowState(show ?? true);
}, [show]);
@@ -91,11 +114,6 @@ export const Modal: React.FC<Props> = ({
});
};
const onClose = useCallback(async () => {
await onHide();
onExited();
}, [onExited]);
const handleCancelClick = async (event: React.MouseEvent<HTMLButtonElement, MouseEvent>) => {
event.preventDefault();
if (autoCloseOnCancelButton) {

View 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;
}
}
}

View File

@@ -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)
}
}
}
}

View File

@@ -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>
);
}

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -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>
</>}
</>
)}
/>
);
}

View File

@@ -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;
}
}
}
}

View File

@@ -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>
);
}

View File

@@ -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);
}
}

View File

@@ -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>
);
}

View File

@@ -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;
}
}
}

View File

@@ -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>
</>
);
}

View File

@@ -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);
}

View File

@@ -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>
);
}

View File

@@ -0,0 +1,7 @@
.UpdatesSetting__button {
margin-top: 8px;
}
.UpdatesSetting__subLabel > span {
display: block;
}

View File

@@ -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>
}
/>
</>
);
}

View 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.'
/>
&nbsp;
<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.'
/>
&nbsp;
<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}}
/>
&nbsp;
<FormattedMessage
id='renderer.components.settingsPage.afterRestart'
defaultMessage='Setting takes effect after restarting the app.'
/>
</>
),
},
},
],
},
};
};
export default definition;

View 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

View File

@@ -133,4 +133,9 @@
--error-text: #da6c6e;
--mention-highlight-bg: #0d6e6e;
--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);
}

View File

@@ -12,6 +12,10 @@
.Modal_body {
margin-bottom: 12px;
.Modal__body {
display: block;
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}

View File

@@ -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;
}
}

View File

@@ -1,15 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// 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 ReactDOM from 'react-dom';
import SettingsPage from '../../components/SettingsPage';
import SettingsModal from '../../components/SettingsModal';
import IntlProvider from '../../intl_provider';
import setupDarkMode from '../darkMode';
@@ -23,8 +18,7 @@ const start = async () => {
ReactDOM.render(
(
<IntlProvider>
<SettingsPage
show={true}
<SettingsModal
onClose={onClose}
/>
</IntlProvider>

View File

@@ -41,7 +41,7 @@ export type ConfigV3 = {
notifications: {
flashWindow: number;
bounceIcon: boolean;
bounceIconType: 'critical' | 'informational';
bounceIconType: '' | 'critical' | 'informational';
};
showUnreadBadge: boolean;
useSpellChecker: boolean;

View File

@@ -1,12 +1,13 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// 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 = {
configType: 'updates' | 'appOptions';
key: keyof CombinedConfig;
data: CombinedConfig[keyof CombinedConfig];
key: keyof Config;
data: Config[keyof Config];
};
export type DeveloperSettings = {
@@ -15,3 +16,16 @@ export type DeveloperSettings = {
disableUserActivityMonitor?: 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']>;
};

View File

@@ -3,8 +3,9 @@
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 {UniqueServerWithPermissions, Permissions} from './permissions';
import type {URLValidationResult} from './server';
import type {SaveQueueItem} from './settings';
@@ -55,6 +56,10 @@ declare global {
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
onUpdateServers: (listener: () => void) => void;
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>;
getVersion: () => Promise<{name: string; version: string}>;
@@ -63,12 +68,12 @@ declare global {
getFullScreenStatus: () => Promise<boolean>;
getAvailableSpellCheckerLanguages: () => Promise<string[]>;
getAvailableLanguages: () => Promise<string[]>;
getLocalConfiguration: () => Promise<LocalConfiguration[keyof LocalConfiguration] | Partial<LocalConfiguration>>;
getLocalConfiguration: () => Promise<LocalConfiguration>;
getDownloadLocation: (downloadLocation?: string) => Promise<string>;
getLanguageInformation: () => Promise<Language>;
onSynchronizeConfig: (listener: () => void) => void;
onReloadConfiguration: (listener: () => void) => void;
onReloadConfiguration: (listener: () => void) => () => void;
onDarkModeChange: (listener: (darkMode: boolean) => void) => void;
onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
onLoadSuccess: (listener: (viewId: string) => void) => void;