[MM-59552] Remove unsafe-inline
from internal CSP, replace with nonce and rework some dynamic styling (#3120)
* Remove unsafe-inline * Fix dynamic dark mode loading * Include nonce generator for CSP for styles * Add nonce provider for react-select * Fix test
This commit is contained in:
parent
e310fa705f
commit
c9f671d82a
3
package-lock.json
generated
3
package-lock.json
generated
|
@ -10,6 +10,7 @@
|
||||||
"hasInstallScript": true,
|
"hasInstallScript": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "11.11.4",
|
||||||
"@mattermost/compass-icons": "0.1.45",
|
"@mattermost/compass-icons": "0.1.45",
|
||||||
"auto-launch": "5.0.6",
|
"auto-launch": "5.0.6",
|
||||||
"bootstrap": "4.6.1",
|
"bootstrap": "4.6.1",
|
||||||
|
@ -88,7 +89,7 @@
|
||||||
},
|
},
|
||||||
"api-types": {
|
"api-types": {
|
||||||
"name": "@mattermost/desktop-api",
|
"name": "@mattermost/desktop-api",
|
||||||
"version": "5.9.0-1",
|
"version": "5.10.0-1",
|
||||||
"dev": true,
|
"dev": true,
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"peerDependencies": {
|
"peerDependencies": {
|
||||||
|
|
|
@ -159,6 +159,7 @@
|
||||||
"webpack-merge": "5.8.0"
|
"webpack-merge": "5.8.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@emotion/react": "11.11.4",
|
||||||
"@mattermost/compass-icons": "0.1.45",
|
"@mattermost/compass-icons": "0.1.45",
|
||||||
"auto-launch": "5.0.6",
|
"auto-launch": "5.0.6",
|
||||||
"bootstrap": "4.6.1",
|
"bootstrap": "4.6.1",
|
||||||
|
|
|
@ -191,3 +191,5 @@ export const GET_MEDIA_ACCESS_STATUS = 'get-media-access-status';
|
||||||
|
|
||||||
// Legacy code remove signal
|
// Legacy code remove signal
|
||||||
export const LEGACY_OFF = 'legacy-off';
|
export const LEGACY_OFF = 'legacy-off';
|
||||||
|
|
||||||
|
export const GET_NONCE = 'get-nonce';
|
||||||
|
|
|
@ -64,6 +64,9 @@ jest.mock('electron', () => ({
|
||||||
},
|
},
|
||||||
session: {
|
session: {
|
||||||
defaultSession: {
|
defaultSession: {
|
||||||
|
webRequest: {
|
||||||
|
onHeadersReceived: jest.fn(),
|
||||||
|
},
|
||||||
setSpellCheckerDictionaryDownloadURL: jest.fn(),
|
setSpellCheckerDictionaryDownloadURL: jest.fn(),
|
||||||
setPermissionRequestHandler: jest.fn(),
|
setPermissionRequestHandler: jest.fn(),
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
|
|
|
@ -45,6 +45,7 @@ import {configPath, updatePaths} from 'main/constants';
|
||||||
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import i18nManager from 'main/i18nManager';
|
import i18nManager from 'main/i18nManager';
|
||||||
|
import NonceManager from 'main/nonceManager';
|
||||||
import {getDoNotDisturb} from 'main/notifications';
|
import {getDoNotDisturb} from 'main/notifications';
|
||||||
import parseArgs from 'main/ParseArgs';
|
import parseArgs from 'main/ParseArgs';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
|
@ -313,6 +314,20 @@ async function initializeAfterAppReady() {
|
||||||
|
|
||||||
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||||
const defaultSession = session.defaultSession;
|
const defaultSession = session.defaultSession;
|
||||||
|
defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
||||||
|
const url = parseURL(details.url);
|
||||||
|
if (url?.protocol === 'mattermost-desktop:' && url?.pathname.endsWith('html')) {
|
||||||
|
callback({
|
||||||
|
responseHeaders: {
|
||||||
|
...details.responseHeaders,
|
||||||
|
'Content-Security-Policy': [`default-src 'self'; style-src 'self' 'nonce-${NonceManager.create(details.url)}'; media-src data:; img-src 'self' data:`],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
downloadsManager.webRequestOnHeadersReceivedHandler(details, callback);
|
||||||
|
});
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
|
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
|
||||||
|
|
34
src/main/nonceManager.ts
Normal file
34
src/main/nonceManager.ts
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {ipcMain} from 'electron';
|
||||||
|
import type {IpcMainInvokeEvent} from 'electron';
|
||||||
|
import {v4 as uuid} from 'uuid';
|
||||||
|
|
||||||
|
import {GET_NONCE} from 'common/communication';
|
||||||
|
|
||||||
|
export class NonceManager {
|
||||||
|
private nonces: Map<string, string>;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.nonces = new Map();
|
||||||
|
|
||||||
|
ipcMain.handle(GET_NONCE, this.handleGetNonce);
|
||||||
|
}
|
||||||
|
|
||||||
|
create = (url: string) => {
|
||||||
|
const nonce = uuid();
|
||||||
|
this.nonces.set(url, nonce);
|
||||||
|
return nonce;
|
||||||
|
};
|
||||||
|
|
||||||
|
private handleGetNonce = (event: IpcMainInvokeEvent) => {
|
||||||
|
const url = event.sender.getURL();
|
||||||
|
const nonce = this.nonces.get(url);
|
||||||
|
this.nonces.delete(url);
|
||||||
|
return nonce;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const nonceManager = new NonceManager();
|
||||||
|
export default nonceManager;
|
|
@ -91,6 +91,7 @@ import {
|
||||||
OPEN_WINDOWS_MICROPHONE_PREFERENCES,
|
OPEN_WINDOWS_MICROPHONE_PREFERENCES,
|
||||||
GET_MEDIA_ACCESS_STATUS,
|
GET_MEDIA_ACCESS_STATUS,
|
||||||
VIEW_FINISHED_RESIZING,
|
VIEW_FINISHED_RESIZING,
|
||||||
|
GET_NONCE,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
|
@ -124,6 +125,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
||||||
goBack: () => ipcRenderer.send(HISTORY, -1),
|
goBack: () => ipcRenderer.send(HISTORY, -1),
|
||||||
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
|
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
|
||||||
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
|
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
|
||||||
|
getNonce: () => ipcRenderer.invoke(GET_NONCE),
|
||||||
|
|
||||||
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
|
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
|
||||||
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
|
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
|
||||||
|
|
|
@ -66,7 +66,6 @@ export class DownloadsDropdownView {
|
||||||
}});
|
}});
|
||||||
|
|
||||||
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
this.view.webContents.loadURL('mattermost-desktop://renderer/downloadsDropdown.html');
|
||||||
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
|
|
||||||
MainWindow.get()?.addBrowserView(this.view);
|
MainWindow.get()?.addBrowserView(this.view);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -6,6 +6,9 @@
|
||||||
|
|
||||||
import 'renderer/css/settings.css';
|
import 'renderer/css/settings.css';
|
||||||
|
|
||||||
|
import createCache from '@emotion/cache';
|
||||||
|
import {CacheProvider} from '@emotion/react';
|
||||||
|
import type {EmotionCache} from '@emotion/react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl, Modal} from 'react-bootstrap';
|
import {FormCheck, Col, FormGroup, FormText, Container, Row, Button, FormControl, Modal} from 'react-bootstrap';
|
||||||
import type {IntlShape} from 'react-intl';
|
import type {IntlShape} from 'react-intl';
|
||||||
|
@ -39,6 +42,7 @@ type State = DeepPartial<CombinedConfig> & {
|
||||||
availableLanguages: Array<{label: string; value: string}>;
|
availableLanguages: Array<{label: string; value: string}>;
|
||||||
availableSpellcheckerLanguages: Array<{label: string; value: string}>;
|
availableSpellcheckerLanguages: Array<{label: string; value: string}>;
|
||||||
canUpgrade?: boolean;
|
canUpgrade?: boolean;
|
||||||
|
cache?: EmotionCache;
|
||||||
}
|
}
|
||||||
|
|
||||||
type SavingStateItems = {
|
type SavingStateItems = {
|
||||||
|
@ -127,6 +131,13 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
|
availableLanguages.sort((a, b) => a.label.localeCompare(b.label));
|
||||||
this.setState({availableLanguages});
|
this.setState({availableLanguages});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
window.desktop.getNonce().then((nonce) => {
|
||||||
|
this.setState({cache: createCache({
|
||||||
|
key: 'react-select-cache',
|
||||||
|
nonce,
|
||||||
|
})});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
getConfig = () => {
|
getConfig = () => {
|
||||||
|
@ -426,6 +437,10 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
render() {
|
render() {
|
||||||
const {intl} = this.props;
|
const {intl} = this.props;
|
||||||
|
|
||||||
|
if (!this.state.cache) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const settingsPage = {
|
const settingsPage = {
|
||||||
close: {
|
close: {
|
||||||
textDecoration: 'none',
|
textDecoration: 'none',
|
||||||
|
@ -581,6 +596,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
</FormText>
|
</FormText>
|
||||||
</FormCheck>
|
</FormCheck>
|
||||||
{this.state.useSpellChecker &&
|
{this.state.useSpellChecker &&
|
||||||
|
<CacheProvider value={this.state.cache}>
|
||||||
<ReactSelect
|
<ReactSelect
|
||||||
inputId='inputSpellCheckerLocalesDropdown'
|
inputId='inputSpellCheckerLocalesDropdown'
|
||||||
className='SettingsPage__spellCheckerLocalesDropdown'
|
className='SettingsPage__spellCheckerLocalesDropdown'
|
||||||
|
@ -597,6 +613,7 @@ class SettingsPage extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
</CacheProvider>
|
||||||
}
|
}
|
||||||
</>,
|
</>,
|
||||||
);
|
);
|
||||||
|
|
|
@ -32,6 +32,10 @@ type Props = {
|
||||||
intl: IntlShape;
|
intl: IntlShape;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
type State = {
|
||||||
|
nonce?: string;
|
||||||
|
}
|
||||||
|
|
||||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||||
if (style?.transform) {
|
if (style?.transform) {
|
||||||
const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`;
|
const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`;
|
||||||
|
@ -43,7 +47,12 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||||
return style;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
class TabBar extends React.PureComponent<Props> {
|
class TabBar extends React.PureComponent<Props, State> {
|
||||||
|
constructor(props: Props) {
|
||||||
|
super(props);
|
||||||
|
this.state = {};
|
||||||
|
}
|
||||||
|
|
||||||
onCloseTab = (id: string) => {
|
onCloseTab = (id: string) => {
|
||||||
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
|
@ -51,7 +60,19 @@ class TabBar extends React.PureComponent<Props> {
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
componentDidMount(): void {
|
||||||
|
window.desktop.getNonce().then((nonce) => {
|
||||||
|
this.setState({
|
||||||
|
nonce,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.state.nonce) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const tabs = this.props.tabs.map((tab, index) => {
|
const tabs = this.props.tabs.map((tab, index) => {
|
||||||
const sessionExpired = this.props.sessionsExpired[tab.id!];
|
const sessionExpired = this.props.sessionsExpired[tab.id!];
|
||||||
const hasUnreads = this.props.unreadCounts[tab.id!];
|
const hasUnreads = this.props.unreadCounts[tab.id!];
|
||||||
|
@ -145,7 +166,10 @@ class TabBar extends React.PureComponent<Props> {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DragDropContext onDragEnd={this.props.onDrop}>
|
<DragDropContext
|
||||||
|
nonce={this.state.nonce}
|
||||||
|
onDragEnd={this.props.onDrop}
|
||||||
|
>
|
||||||
<Droppable
|
<Droppable
|
||||||
isDropDisabled={this.props.tabsDisabled}
|
isDropDisabled={this.props.tabsDisabled}
|
||||||
droppableId='tabBar'
|
droppableId='tabBar'
|
||||||
|
|
|
@ -1,36 +0,0 @@
|
||||||
@import '~bootstrap-dark/src/bootstrap-dark.css';
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toggle .Toggle___switch {
|
|
||||||
background: rgba(var(--center-channel-bg-rgb), 0.24);
|
|
||||||
}
|
|
||||||
|
|
||||||
.Toggle .Toggle___switch.disabled {
|
|
||||||
background: rgba(var(--center-channel-bg-rgb), 0.08);
|
|
||||||
}
|
|
38
src/renderer/css/modals-dark.scss
Normal file
38
src/renderer/css/modals-dark.scss
Normal file
|
@ -0,0 +1,38 @@
|
||||||
|
@use 'sass:meta';
|
||||||
|
|
||||||
|
.darkMode {
|
||||||
|
@include meta.load-css('bootstrap-dark/src/bootstrap-dark.css');
|
||||||
|
color: #fff;
|
||||||
|
|
||||||
|
.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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ type State = {
|
||||||
hasGPOServers?: boolean;
|
hasGPOServers?: boolean;
|
||||||
isAnyDragging: boolean;
|
isAnyDragging: boolean;
|
||||||
windowBounds?: Electron.Rectangle;
|
windowBounds?: Electron.Rectangle;
|
||||||
|
nonce?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||||
|
@ -141,6 +142,9 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||||
window.desktop.serverDropdown.requestInfo();
|
window.desktop.serverDropdown.requestInfo();
|
||||||
window.addEventListener('click', this.closeMenu);
|
window.addEventListener('click', this.closeMenu);
|
||||||
window.addEventListener('keydown', this.handleKeyboardShortcuts);
|
window.addEventListener('keydown', this.handleKeyboardShortcuts);
|
||||||
|
window.desktop.getNonce().then((nonce) => {
|
||||||
|
this.setState({nonce});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentDidUpdate() {
|
componentDidUpdate() {
|
||||||
|
@ -231,6 +235,10 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||||
};
|
};
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
|
if (!this.state.nonce) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IntlProvider>
|
<IntlProvider>
|
||||||
<div
|
<div
|
||||||
|
@ -256,6 +264,7 @@ class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||||
</div>
|
</div>
|
||||||
<hr className='ServerDropdown__divider'/>
|
<hr className='ServerDropdown__divider'/>
|
||||||
<DragDropContext
|
<DragDropContext
|
||||||
|
nonce={this.state.nonce}
|
||||||
onDragStart={this.onDragStart}
|
onDragStart={this.onDragStart}
|
||||||
onDragEnd={this.onDragEnd}
|
onDragEnd={this.onDragEnd}
|
||||||
>
|
>
|
||||||
|
|
|
@ -2,7 +2,6 @@
|
||||||
<html>
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; style-src 'self' 'unsafe-inline'; media-src data:; img-src 'self' data:">
|
|
||||||
<title><%= htmlWebpackPlugin.options.title %></title>
|
<title><%= htmlWebpackPlugin.options.title %></title>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
|
@ -1,14 +1,14 @@
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import darkStyles from 'renderer/css/lazy/modals-dark.lazy.css';
|
import 'renderer/css/modals-dark.scss';
|
||||||
|
|
||||||
export default function addDarkModeListener() {
|
export default function addDarkModeListener() {
|
||||||
const setDarkMode = (darkMode: boolean) => {
|
const setDarkMode = (darkMode: boolean) => {
|
||||||
if (darkMode) {
|
if (darkMode) {
|
||||||
darkStyles.use();
|
document.body.classList.add('darkMode');
|
||||||
} else {
|
} else {
|
||||||
darkStyles.unuse();
|
document.body.classList.remove('darkMode');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
window.desktop.onDarkModeChange(setDarkMode);
|
window.desktop.onDarkModeChange(setDarkMode);
|
||||||
|
|
|
@ -44,6 +44,7 @@ declare global {
|
||||||
goBack: () => void;
|
goBack: () => void;
|
||||||
checkForUpdates: () => void;
|
checkForUpdates: () => void;
|
||||||
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
||||||
|
getNonce: () => Promise<string | undefined>;
|
||||||
|
|
||||||
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
||||||
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
||||||
|
|
|
@ -152,7 +152,14 @@ module.exports = merge(base, {
|
||||||
use: [
|
use: [
|
||||||
MiniCssExtractPlugin.loader,
|
MiniCssExtractPlugin.loader,
|
||||||
'css-loader',
|
'css-loader',
|
||||||
'sass-loader',
|
{
|
||||||
|
loader: 'sass-loader',
|
||||||
|
options: {
|
||||||
|
sassOptions: {
|
||||||
|
includePaths: [path.resolve(__dirname, 'node_modules')],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}, {
|
}, {
|
||||||
test: /\.mp3$/,
|
test: /\.mp3$/,
|
||||||
|
|
Loading…
Reference in a new issue