diff --git a/src/main/app/config.ts b/src/main/app/config.ts index 1fc8b0fe..d06e7dd3 100644 --- a/src/main/app/config.ts +++ b/src/main/app/config.ts @@ -11,7 +11,6 @@ import {setUnreadBadgeSetting} from 'main/badge'; import Tray from 'main/tray/tray'; import LoadingScreen from 'main/views/loadingScreen'; import MainWindow from 'main/windows/mainWindow'; -import SettingsWindow from 'main/windows/settingsWindow'; import type {CombinedConfig, Config as ConfigType} from 'types/config'; @@ -73,7 +72,7 @@ export function handleConfigUpdate(newConfig: CombinedConfig) { if (app.isReady()) { MainWindow.sendToRenderer(RELOAD_CONFIGURATION); - SettingsWindow.sendToRenderer(RELOAD_CONFIGURATION); + ipcMain.emit(EMIT_CONFIGURATION, true, Config.data); } setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge); @@ -113,7 +112,6 @@ export function handleDarkModeChange(darkMode: boolean) { Tray.refreshImages(Config.trayIconTheme); MainWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode); - SettingsWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode); LoadingScreen.setDarkMode(darkMode); ipcMain.emit(EMIT_CONFIGURATION, true, Config.data); diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index 73081e6e..ce0bdc74 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -166,9 +166,6 @@ jest.mock('main/views/viewManager', () => ({ getViewByWebContentsId: jest.fn(), handleDeepLink: jest.fn(), })); -jest.mock('main/windows/settingsWindow', () => ({ - show: jest.fn(), -})); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), show: jest.fn(), diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index 45fb7fa7..ba35243b 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -31,6 +31,7 @@ import { DOUBLE_CLICK_ON_WINDOW, TOGGLE_SECURE_INPUT, GET_APP_INFO, + SHOW_SETTINGS_WINDOW, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -79,6 +80,7 @@ import { handleQuit, handlePingDomain, handleToggleSecureInput, + handleShowSettingsModal, } from './intercom'; import { clearAppCache, @@ -281,6 +283,10 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(DOUBLE_CLICK_ON_WINDOW, handleDoubleClick); ipcMain.on(TOGGLE_SECURE_INPUT, handleToggleSecureInput); + + if (process.env.NODE_ENV === 'test') { + ipcMain.on(SHOW_SETTINGS_WINDOW, handleShowSettingsModal); + } } async function initializeAfterAppReady() { diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index d07cd101..381b43d8 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -160,3 +160,18 @@ export function handleToggleSecureInput(event: IpcMainEvent, secureInput: boolea log.debug('handleToggleSecureInput', secureInput); app.setSecureKeyboardEntryEnabled(secureInput); } + +export function handleShowSettingsModal() { + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + + ModalManager.addModal( + 'settingsModal', + getLocalURLString('settings.html'), + getLocalPreload('internalAPI.js'), + null, + mainWindow, + ); +} diff --git a/src/main/menus/app.test.js b/src/main/menus/app.test.js index 0d318156..9e934880 100644 --- a/src/main/menus/app.test.js +++ b/src/main/menus/app.test.js @@ -79,10 +79,8 @@ jest.mock('main/downloadsManager', () => ({ })); jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/mainWindow', () => ({ - sendToRenderer: jest.fn(), - on: jest.fn(), + get: jest.fn(), })); -jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('common/views/View', () => ({ getViewDisplayName: (name) => name, })); @@ -93,6 +91,9 @@ jest.mock('main/AutoLauncher', () => ({ jest.mock('main/windows/callsWidgetWindow', () => ({ isOpen: jest.fn(), })); +jest.mock('main/views/modalManager', () => ({ + addModal: jest.fn(), +})); describe('main/menus/app', () => { const config = { diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index 6d0e1e68..a097f30f 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -18,9 +18,11 @@ import type {UpdateManager} from 'main/autoUpdater'; import Diagnostics from 'main/diagnostics'; import downloadsManager from 'main/downloadsManager'; import {localizeMessage} from 'main/i18nManager'; +import {getLocalPreload, getLocalURLString} from 'main/utils'; +import ModalManager from 'main/views/modalManager'; import ViewManager from 'main/views/viewManager'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; -import SettingsWindow from 'main/windows/settingsWindow'; +import MainWindow from 'main/windows/mainWindow'; export function createTemplate(config: Config, updateManager: UpdateManager) { const separatorItem: MenuItemConstructorOptions = { @@ -48,7 +50,18 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: settingsLabel, accelerator: 'CmdOrCtrl+,', click() { - SettingsWindow.show(); + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + + ModalManager.addModal( + 'settingsModal', + getLocalURLString('settings.html'), + getLocalPreload('internalAPI.js'), + null, + mainWindow, + ); }, }); diff --git a/src/main/menus/tray.test.js b/src/main/menus/tray.test.js index a58559cc..25e461ae 100644 --- a/src/main/menus/tray.test.js +++ b/src/main/menus/tray.test.js @@ -17,7 +17,13 @@ jest.mock('common/servers/serverManager', () => ({ jest.mock('app/serverViewState', () => ({ switchServer: jest.fn(), })); -jest.mock('main/windows/settingsWindow', () => ({})); +jest.mock('main/windows/mainWindow', () => ({ + sendToRenderer: jest.fn(), + on: jest.fn(), +})); +jest.mock('main/views/modalManager', () => ({ + addModal: jest.fn(), +})); describe('main/menus/tray', () => { it('should show the first 9 servers (using order)', () => { diff --git a/src/main/menus/tray.ts b/src/main/menus/tray.ts index 5b9b8df6..383fe9fc 100644 --- a/src/main/menus/tray.ts +++ b/src/main/menus/tray.ts @@ -9,7 +9,9 @@ import {Menu} from 'electron'; import ServerViewState from 'app/serverViewState'; import ServerManager from 'common/servers/serverManager'; import {localizeMessage} from 'main/i18nManager'; -import SettingsWindow from 'main/windows/settingsWindow'; +import {getLocalPreload, getLocalURLString} from 'main/utils'; +import ModalManager from 'main/views/modalManager'; +import MainWindow from 'main/windows/mainWindow'; export function createTemplate() { const servers = ServerManager.getOrderedServers(); @@ -26,7 +28,18 @@ export function createTemplate() { }, { label: process.platform === 'darwin' ? localizeMessage('main.menus.tray.preferences', 'Preferences...') : localizeMessage('main.menus.tray.settings', 'Settings'), click: () => { - SettingsWindow.show(); + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + + ModalManager.addModal( + 'settingsModal', + getLocalURLString('settings.html'), + getLocalPreload('internalAPI.js'), + null, + mainWindow, + ); }, }, { type: 'separator', diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts index 455d2db2..b7a710be 100644 --- a/src/main/tray/tray.ts +++ b/src/main/tray/tray.ts @@ -10,7 +10,6 @@ import {UPDATE_APPSTATE_TOTALS} from 'common/communication'; import {Logger} from 'common/log'; import {localizeMessage} from 'main/i18nManager'; import MainWindow from 'main/windows/mainWindow'; -import SettingsWindow from 'main/windows/settingsWindow'; const assetsDir = path.resolve(app.getAppPath(), 'assets'); const log = new Logger('Tray'); @@ -144,12 +143,7 @@ export class TrayIcon { mainWindow.show(); } - const settingsWindow = SettingsWindow.get(); - if (settingsWindow) { - settingsWindow.focus(); - } else { - mainWindow.focus(); - } + mainWindow.focus(); }; private onAppStateUpdate = (anyExpired: boolean, anyMentions: number, anyUnreads: boolean) => { diff --git a/src/main/views/modalManager.ts b/src/main/views/modalManager.ts index b55398db..590fe25d 100644 --- a/src/main/views/modalManager.ts +++ b/src/main/views/modalManager.ts @@ -15,6 +15,7 @@ import { DARK_MODE_CHANGE, GET_MODAL_UNCLOSEABLE, MAIN_WINDOW_RESIZED, + RELOAD_CONFIGURATION, } from 'common/communication'; import {Logger} from 'common/log'; import {getAdjustedWindowBoundaries} from 'main/utils'; @@ -152,6 +153,7 @@ export class ModalManager { } this.modalQueue.forEach((modal) => { + modal.view.webContents.send(RELOAD_CONFIGURATION); modal.view.webContents.send(DARK_MODE_CHANGE, config.darkMode); }); }; diff --git a/src/main/windows/settingsWindow.ts b/src/main/windows/settingsWindow.ts deleted file mode 100644 index bb6d16b8..00000000 --- a/src/main/windows/settingsWindow.ts +++ /dev/null @@ -1,83 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {BrowserWindow, ipcMain} from 'electron'; - -import {SHOW_SETTINGS_WINDOW} from 'common/communication'; -import Config from 'common/config'; -import {Logger} from 'common/log'; - -import MainWindow from './mainWindow'; - -import ContextMenu from '../contextMenu'; -import {getLocalPreload, getLocalURLString} from '../utils'; - -const log = new Logger('SettingsWindow'); - -export class SettingsWindow { - private win?: BrowserWindow; - - constructor() { - ipcMain.on(SHOW_SETTINGS_WINDOW, this.show); - } - - show = () => { - if (this.win) { - this.win.show(); - } else { - this.create(); - } - }; - - get = () => { - return this.win; - }; - - sendToRenderer = (channel: string, ...args: any[]) => { - this.win?.webContents.send(channel, ...args); - }; - - private create = () => { - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - - const preload = getLocalPreload('internalAPI.js'); - const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker); - this.win = new BrowserWindow({ - title: 'Desktop App Settings', - fullscreen: false, - webPreferences: { - preload, - spellcheck, - }}); - - const contextMenu = new ContextMenu({}, this.win); - contextMenu.reload(); - - const localURL = getLocalURLString('settings.html'); - this.win.setMenuBarVisibility(false); - this.win.loadURL(localURL).catch( - (reason) => { - log.error('failed to load', reason); - }); - this.win.show(); - - if (Boolean(process.env.MM_DEBUG_SETTINGS) || false) { - this.win.webContents.openDevTools({mode: 'detach'}); - } - - this.win.on('closed', () => { - delete this.win; - - // For some reason, on macOS, the app will hard crash when the settings window is closed - // It seems to be related to calling view.focus() and there's no log output unfortunately - // Adding this arbitrary delay seems to get rid of it (it happens very frequently) - setTimeout(() => MainWindow.get()?.focus(), 10); - }); - }; -} - -const settingsWindow = new SettingsWindow(); -export default settingsWindow; diff --git a/src/renderer/components/SettingsPage.tsx b/src/renderer/components/SettingsPage.tsx index b0450deb..ff0daa9c 100644 --- a/src/renderer/components/SettingsPage.tsx +++ b/src/renderer/components/SettingsPage.tsx @@ -7,7 +7,7 @@ import 'renderer/css/settings.css'; import React from 'react'; -import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl} from 'react-bootstrap'; +import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl, Modal} from 'react-bootstrap'; import type {IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl} from 'react-intl'; import type {ActionMeta, MultiValue} from 'react-select'; @@ -25,6 +25,8 @@ const CONFIG_TYPE_UPDATES = 'updates'; const CONFIG_TYPE_APP_OPTIONS = 'appOptions'; type Props = { + show: boolean; + onClose: () => void; intl: IntlShape; } @@ -1228,35 +1230,36 @@ class SettingsPage extends React.PureComponent { } return ( -
-
-
-

- -

-
-
- + + + + + + +
- {waitForIpc} - -
-
+ + {waitForIpc} + +
+ + ); } } diff --git a/src/renderer/css/lazy/modals-dark.lazy.css b/src/renderer/css/lazy/modals-dark.lazy.css index 870d0c89..32085b26 100644 --- a/src/renderer/css/lazy/modals-dark.lazy.css +++ b/src/renderer/css/lazy/modals-dark.lazy.css @@ -2,4 +2,27 @@ body { background-color: transparent; +} + +.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); } \ No newline at end of file diff --git a/src/renderer/css/lazy/settings-dark.lazy.css b/src/renderer/css/lazy/settings-dark.lazy.css deleted file mode 100644 index 9a50cbfa..00000000 --- a/src/renderer/css/lazy/settings-dark.lazy.css +++ /dev/null @@ -1,21 +0,0 @@ -@import '~bootstrap-dark/src/bootstrap-dark.css'; - -.ServerListItem:hover { - background: #242a30; -} - -.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); -} diff --git a/src/renderer/css/settings.css b/src/renderer/css/settings.css index 5b6d86d6..6e5b8c93 100644 --- a/src/renderer/css/settings.css +++ b/src/renderer/css/settings.css @@ -41,3 +41,25 @@ body { 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; + } +} diff --git a/src/renderer/settings.tsx b/src/renderer/modals/settings/settings.tsx similarity index 58% rename from src/renderer/settings.tsx rename to src/renderer/modals/settings/settings.tsx index 7da49b01..2925bef5 100644 --- a/src/renderer/settings.tsx +++ b/src/renderer/modals/settings/settings.tsx @@ -1,4 +1,3 @@ -// Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. @@ -9,27 +8,24 @@ import 'renderer/css/settings.css'; import React from 'react'; import ReactDOM from 'react-dom'; -import darkStyles from 'renderer/css/lazy/settings-dark.lazy.css'; +import SettingsPage from '../../components/SettingsPage'; +import IntlProvider from '../../intl_provider'; +import setupDarkMode from '../darkMode'; -import SettingsPage from './components/SettingsPage'; -import IntlProvider from './intl_provider'; +setupDarkMode(); -const setDarkMode = (darkMode: boolean) => { - if (darkMode) { - darkStyles.use(); - } else { - darkStyles.unuse(); - } +const onClose = () => { + window.desktop.modals.finishModal(); }; -window.desktop.onDarkModeChange((darkMode) => setDarkMode(darkMode)); -window.desktop.getDarkMode().then(setDarkMode); - const start = async () => { ReactDOM.render( ( - + ) , diff --git a/webpack.config.renderer.js b/webpack.config.renderer.js index 97b565ac..6f16a2b1 100644 --- a/webpack.config.renderer.js +++ b/webpack.config.renderer.js @@ -13,7 +13,7 @@ const base = require('./webpack.config.base'); module.exports = merge(base, { entry: { index: './src/renderer/index.tsx', - settings: './src/renderer/settings.tsx', + settings: './src/renderer/modals/settings/settings.tsx', dropdown: './src/renderer/dropdown.tsx', downloadsDropdownMenu: './src/renderer/downloadsDropdownMenu.tsx', downloadsDropdown: './src/renderer/downloadsDropdown.tsx',