[MM-60308] Add a set of "Developer Mode" settings that allow the user to turn off systems or force the app to behave a certain way (#3144)
* Add developer mode manager, implement browser-only mode * Add indicator when developer mode is enabled * Add switch to disable notification storage * Add setting to disable the user activity monitor * Add switchOff method for easily creating switches to disable/enable functionality, added setting to disable context menu * Add setting to force legacy API * Add force new API to remove any support for legacy mode, fix i18n * Fix lint * Use one call to `push`
This commit is contained in:
parent
61cf759b23
commit
42a0bc4759
|
@ -81,6 +81,12 @@
|
||||||
"main.menus.app.view": "&View",
|
"main.menus.app.view": "&View",
|
||||||
"main.menus.app.view.actualSize": "Actual Size",
|
"main.menus.app.view.actualSize": "Actual Size",
|
||||||
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
|
"main.menus.app.view.clearCacheAndReload": "Clear Cache and Reload",
|
||||||
|
"main.menus.app.view.developerModeBrowserOnly": "Browser Only Mode",
|
||||||
|
"main.menus.app.view.developerModeDisableContextMenu": "Disable Context Menu",
|
||||||
|
"main.menus.app.view.developerModeDisableNotificationStorage": "Disable Notification Storage",
|
||||||
|
"main.menus.app.view.developerModeDisableUserActivityMonitor": "Disable User Activity Monitor",
|
||||||
|
"main.menus.app.view.developerModeForceLegacyAPI": "Force Legacy API",
|
||||||
|
"main.menus.app.view.developerModeForceNewAPI": "Force New API",
|
||||||
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
|
"main.menus.app.view.devToolsAppWrapper": "Developer Tools for Application Wrapper",
|
||||||
"main.menus.app.view.devToolsCurrentCallWidget": "Developer Tools for Call Widget",
|
"main.menus.app.view.devToolsCurrentCallWidget": "Developer Tools for Call Widget",
|
||||||
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
|
"main.menus.app.view.devToolsCurrentServer": "Developer Tools for Current Server",
|
||||||
|
@ -151,6 +157,7 @@
|
||||||
"renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
"renderer.components.configureServer.url.urlNotMatched": "The server URL provided does not match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
"renderer.components.configureServer.url.urlUpdated": "The server URL provided has been updated to match the configured Site URL on your Mattermost server. Server version: {serverVersion}",
|
||||||
"renderer.components.configureServer.url.validating": "Validating...",
|
"renderer.components.configureServer.url.validating": "Validating...",
|
||||||
|
"renderer.components.developerModeIndicator.tooltip": "Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.",
|
||||||
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
|
"renderer.components.errorView.cannotConnectToAppName": "Cannot connect to {appName}",
|
||||||
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
|
"renderer.components.errorView.havingTroubleConnecting": "We're having trouble connecting to {appName}. We'll continue to try and establish a connection.",
|
||||||
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
|
"renderer.components.errorView.refreshThenVerify": "If refreshing this page (Ctrl+R or Command+R) does not work please verify that:",
|
||||||
|
|
|
@ -193,3 +193,7 @@ export const GET_MEDIA_ACCESS_STATUS = 'get-media-access-status';
|
||||||
export const LEGACY_OFF = 'legacy-off';
|
export const LEGACY_OFF = 'legacy-off';
|
||||||
|
|
||||||
export const GET_NONCE = 'get-nonce';
|
export const GET_NONCE = 'get-nonce';
|
||||||
|
|
||||||
|
export const DEVELOPER_MODE_UPDATED = 'developer-mode-updated';
|
||||||
|
export const IS_DEVELOPER_MODE_ENABLED = 'is-developer-mode-enabled';
|
||||||
|
export const GET_DEVELOPER_MODE_SETTING = 'get-developer-mode-setting';
|
||||||
|
|
|
@ -82,6 +82,7 @@ export class UserActivityMonitor extends EventEmitter {
|
||||||
*/
|
*/
|
||||||
stopMonitoring() {
|
stopMonitoring() {
|
||||||
clearInterval(this.systemIdleTimeIntervalID);
|
clearInterval(this.systemIdleTimeIntervalID);
|
||||||
|
this.systemIdleTimeIntervalID = -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -29,6 +29,7 @@ import {
|
||||||
TOGGLE_SECURE_INPUT,
|
TOGGLE_SECURE_INPUT,
|
||||||
GET_APP_INFO,
|
GET_APP_INFO,
|
||||||
SHOW_SETTINGS_WINDOW,
|
SHOW_SETTINGS_WINDOW,
|
||||||
|
DEVELOPER_MODE_UPDATED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -43,6 +44,7 @@ import {setupBadge} from 'main/badge';
|
||||||
import CertificateManager from 'main/certificateManager';
|
import CertificateManager from 'main/certificateManager';
|
||||||
import {configPath, updatePaths} from 'main/constants';
|
import {configPath, updatePaths} from 'main/constants';
|
||||||
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
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 NonceManager from 'main/nonceManager';
|
||||||
|
@ -405,14 +407,16 @@ async function initializeAfterAppReady() {
|
||||||
// Call this to initiate a permissions check for DND state
|
// Call this to initiate a permissions check for DND state
|
||||||
getDoNotDisturb();
|
getDoNotDisturb();
|
||||||
|
|
||||||
|
DeveloperMode.switchOff('disableUserActivityMonitor', () => {
|
||||||
// listen for status updates and pass on to renderer
|
// listen for status updates and pass on to renderer
|
||||||
UserActivityMonitor.on('status', (status) => {
|
UserActivityMonitor.on('status', onUserActivityStatus);
|
||||||
log.debug('UserActivityMonitor.on(status)', status);
|
|
||||||
ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent);
|
|
||||||
});
|
|
||||||
|
|
||||||
// start monitoring user activity (needs to be started after the app is ready)
|
// start monitoring user activity (needs to be started after the app is ready)
|
||||||
UserActivityMonitor.startMonitoring();
|
UserActivityMonitor.startMonitoring();
|
||||||
|
}, () => {
|
||||||
|
UserActivityMonitor.off('status', onUserActivityStatus);
|
||||||
|
UserActivityMonitor.stopMonitoring();
|
||||||
|
});
|
||||||
|
|
||||||
if (shouldShowTrayIcon()) {
|
if (shouldShowTrayIcon()) {
|
||||||
Tray.init(Config.trayIconTheme);
|
Tray.init(Config.trayIconTheme);
|
||||||
|
@ -430,6 +434,7 @@ async function initializeAfterAppReady() {
|
||||||
}
|
}
|
||||||
|
|
||||||
handleUpdateMenuEvent();
|
handleUpdateMenuEvent();
|
||||||
|
DeveloperMode.on(DEVELOPER_MODE_UPDATED, handleUpdateMenuEvent);
|
||||||
|
|
||||||
ipcMain.emit('update-dict');
|
ipcMain.emit('update-dict');
|
||||||
|
|
||||||
|
@ -445,6 +450,15 @@ async function initializeAfterAppReady() {
|
||||||
handleMainWindowIsShown();
|
handleMainWindowIsShown();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function onUserActivityStatus(status: {
|
||||||
|
userIsActive: boolean;
|
||||||
|
idleTime: number;
|
||||||
|
isSystemEvent: boolean;
|
||||||
|
}) {
|
||||||
|
log.debug('UserActivityMonitor.on(status)', status);
|
||||||
|
ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status.userIsActive, status.idleTime, status.isSystemEvent);
|
||||||
|
}
|
||||||
|
|
||||||
function handleStartDownload() {
|
function handleStartDownload() {
|
||||||
if (updateManager) {
|
if (updateManager) {
|
||||||
updateManager.handleDownload();
|
updateManager.handleDownload();
|
||||||
|
|
|
@ -20,6 +20,7 @@ export let boundsInfoPath = '';
|
||||||
export let migrationInfoPath = '';
|
export let migrationInfoPath = '';
|
||||||
export let downloadsJson = '';
|
export let downloadsJson = '';
|
||||||
export let permissionsJson = '';
|
export let permissionsJson = '';
|
||||||
|
export let developerModeJson = '';
|
||||||
|
|
||||||
export function updatePaths(emit = false) {
|
export function updatePaths(emit = false) {
|
||||||
userDataPath = app.getPath('userData');
|
userDataPath = app.getPath('userData');
|
||||||
|
@ -33,6 +34,7 @@ export function updatePaths(emit = false) {
|
||||||
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
|
migrationInfoPath = path.resolve(userDataPath, 'migration-info.json');
|
||||||
downloadsJson = path.resolve(userDataPath, 'downloads.json');
|
downloadsJson = path.resolve(userDataPath, 'downloads.json');
|
||||||
permissionsJson = path.resolve(userDataPath, 'permissions.json');
|
permissionsJson = path.resolve(userDataPath, 'permissions.json');
|
||||||
|
developerModeJson = path.resolve(userDataPath, 'developerMode.json');
|
||||||
|
|
||||||
if (emit) {
|
if (emit) {
|
||||||
ipcMain.emit(UPDATE_PATHS);
|
ipcMain.emit(UPDATE_PATHS);
|
||||||
|
|
34
src/main/developerMode.test.js
Normal file
34
src/main/developerMode.test.js
Normal file
|
@ -0,0 +1,34 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {DeveloperMode} from './developerMode';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
writeFile: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/developerMode', () => {
|
||||||
|
it('should toggle values correctly', () => {
|
||||||
|
const developerMode = new DeveloperMode('file.json');
|
||||||
|
|
||||||
|
// Should be false unless developer mode is enabled
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(false);
|
||||||
|
|
||||||
|
developerMode.enabled = () => true;
|
||||||
|
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(true);
|
||||||
|
|
||||||
|
developerMode.toggle('setting1');
|
||||||
|
expect(developerMode.get('setting1')).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
68
src/main/developerMode.ts
Normal file
68
src/main/developerMode.ts
Normal file
|
@ -0,0 +1,68 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {ipcMain} from 'electron';
|
||||||
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
|
import {DEVELOPER_MODE_UPDATED, IS_DEVELOPER_MODE_ENABLED, UPDATE_PATHS, GET_DEVELOPER_MODE_SETTING} from 'common/communication';
|
||||||
|
import JsonFileManager from 'common/JsonFileManager';
|
||||||
|
import {developerModeJson} from 'main/constants';
|
||||||
|
|
||||||
|
import type {DeveloperSettings} from 'types/settings';
|
||||||
|
|
||||||
|
export class DeveloperMode extends EventEmitter {
|
||||||
|
private json: JsonFileManager<DeveloperSettings>;
|
||||||
|
|
||||||
|
constructor(file: string) {
|
||||||
|
super();
|
||||||
|
this.json = new JsonFileManager(file);
|
||||||
|
|
||||||
|
ipcMain.handle(IS_DEVELOPER_MODE_ENABLED, this.enabled);
|
||||||
|
ipcMain.handle(GET_DEVELOPER_MODE_SETTING, (_, setting) => this.get(setting));
|
||||||
|
}
|
||||||
|
|
||||||
|
enabled = () => process.env.MM_DESKTOP_DEVELOPER_MODE === 'true';
|
||||||
|
|
||||||
|
toggle = (setting: keyof DeveloperSettings) => {
|
||||||
|
if (!this.enabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.json.setValue(setting, !this.json.getValue(setting));
|
||||||
|
this.emit(DEVELOPER_MODE_UPDATED, {[setting]: this.json.getValue(setting)});
|
||||||
|
};
|
||||||
|
|
||||||
|
get = (setting: keyof DeveloperSettings) => {
|
||||||
|
if (!this.enabled()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.json.getValue(setting);
|
||||||
|
};
|
||||||
|
|
||||||
|
switchOff = (
|
||||||
|
setting: keyof DeveloperSettings,
|
||||||
|
onStart: () => void,
|
||||||
|
onStop: () => void,
|
||||||
|
) => {
|
||||||
|
if (!this.get(setting)) {
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.on(DEVELOPER_MODE_UPDATED, (settings: DeveloperSettings) => {
|
||||||
|
if (typeof settings[setting] !== 'undefined') {
|
||||||
|
if (settings[setting]) {
|
||||||
|
onStop();
|
||||||
|
} else {
|
||||||
|
onStart();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let developerMode = new DeveloperMode(developerModeJson);
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
developerMode = new DeveloperMode(developerModeJson);
|
||||||
|
});
|
||||||
|
export default developerMode;
|
|
@ -3,7 +3,7 @@
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import type {MenuItemConstructorOptions, MenuItem, WebContents} from 'electron';
|
import type {MenuItemConstructorOptions, MenuItem, BrowserWindow} from 'electron';
|
||||||
import {app, ipcMain, Menu, session, shell, clipboard} from 'electron';
|
import {app, ipcMain, Menu, session, shell, clipboard} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
@ -15,6 +15,7 @@ import {t} from 'common/utils/util';
|
||||||
import {getViewDisplayName} from 'common/views/View';
|
import {getViewDisplayName} from 'common/views/View';
|
||||||
import type {ViewType} from 'common/views/View';
|
import type {ViewType} from 'common/views/View';
|
||||||
import type {UpdateManager} from 'main/autoUpdater';
|
import type {UpdateManager} from 'main/autoUpdater';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import Diagnostics from 'main/diagnostics';
|
import Diagnostics from 'main/diagnostics';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
@ -139,7 +140,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
}],
|
}],
|
||||||
});
|
});
|
||||||
|
|
||||||
const devToolsSubMenu = [
|
const devToolsSubMenu: Electron.MenuItemConstructorOptions[] = [
|
||||||
{
|
{
|
||||||
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
|
label: localizeMessage('main.menus.app.view.devToolsAppWrapper', 'Developer Tools for Application Wrapper'),
|
||||||
accelerator: (() => {
|
accelerator: (() => {
|
||||||
|
@ -148,13 +149,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
}
|
}
|
||||||
return 'Ctrl+Shift+I';
|
return 'Ctrl+Shift+I';
|
||||||
})(),
|
})(),
|
||||||
click(item: Electron.MenuItem, focusedWindow?: WebContents) {
|
click(item: Electron.MenuItem, focusedWindow?: BrowserWindow) {
|
||||||
if (focusedWindow) {
|
if (focusedWindow) {
|
||||||
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
||||||
if (focusedWindow.isDevToolsOpened()) {
|
if (focusedWindow.webContents.isDevToolsOpened()) {
|
||||||
focusedWindow.closeDevTools();
|
focusedWindow.webContents.closeDevTools();
|
||||||
} else {
|
} else {
|
||||||
focusedWindow.openDevTools({mode: 'detach'});
|
focusedWindow.webContents.openDevTools({mode: 'detach'});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -176,6 +177,60 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (DeveloperMode.enabled()) {
|
||||||
|
devToolsSubMenu.push(...[
|
||||||
|
separatorItem,
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeBrowserOnly', 'Browser Only Mode'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('browserOnly'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('browserOnly');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableNotificationStorage', 'Disable Notification Storage'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableNotificationStorage'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableNotificationStorage');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableUserActivityMonitor', 'Disable User Activity Monitor'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableUserActivityMonitor'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableUserActivityMonitor');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeDisableContextMenu', 'Disable Context Menu'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('disableContextMenu'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('disableContextMenu');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeForceLegacyAPI', 'Force Legacy API'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('forceLegacyAPI'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('forceLegacyAPI');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: localizeMessage('main.menus.app.view.developerModeForceNewAPI', 'Force New API'),
|
||||||
|
type: 'checkbox' as const,
|
||||||
|
checked: DeveloperMode.get('forceNewAPI'),
|
||||||
|
click() {
|
||||||
|
DeveloperMode.toggle('forceNewAPI');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
const viewSubMenu = [{
|
const viewSubMenu = [{
|
||||||
label: localizeMessage('main.menus.app.view.find', 'Find..'),
|
label: localizeMessage('main.menus.app.view.find', 'Find..'),
|
||||||
accelerator: 'CmdOrCtrl+F',
|
accelerator: 'CmdOrCtrl+F',
|
||||||
|
|
|
@ -106,7 +106,9 @@ jest.mock('../windows/mainWindow', () => ({
|
||||||
show: jest.fn(),
|
show: jest.fn(),
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/developerMode', () => ({
|
||||||
|
switchOff: (_: string, onStart: () => void) => onStart(),
|
||||||
|
}));
|
||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
|
@ -8,6 +8,7 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state
|
||||||
import {PLAY_SOUND, NOTIFICATION_CLICKED, BROWSER_HISTORY_PUSH, OPEN_NOTIFICATION_PREFERENCES} from 'common/communication';
|
import {PLAY_SOUND, NOTIFICATION_CLICKED, BROWSER_HISTORY_PUSH, OPEN_NOTIFICATION_PREFERENCES} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
|
|
||||||
import getLinuxDoNotDisturb from './dnd-linux';
|
import getLinuxDoNotDisturb from './dnd-linux';
|
||||||
import getWindowsDoNotDisturb from './dnd-windows';
|
import getWindowsDoNotDisturb from './dnd-windows';
|
||||||
|
@ -22,13 +23,23 @@ import MainWindow from '../windows/mainWindow';
|
||||||
const log = new Logger('Notifications');
|
const log = new Logger('Notifications');
|
||||||
|
|
||||||
class NotificationManager {
|
class NotificationManager {
|
||||||
private mentionsPerChannel: Map<string, Mention> = new Map();
|
private mentionsPerChannel?: Map<string, Mention>;
|
||||||
private allActiveNotifications: Map<string, Notification> = new Map();
|
private allActiveNotifications?: Map<string, Notification>;
|
||||||
private upgradeNotification?: NewVersionNotification;
|
private upgradeNotification?: NewVersionNotification;
|
||||||
private restartToUpgradeNotification?: UpgradeNotification;
|
private restartToUpgradeNotification?: UpgradeNotification;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
ipcMain.on(OPEN_NOTIFICATION_PREFERENCES, this.openNotificationPreferences);
|
ipcMain.on(OPEN_NOTIFICATION_PREFERENCES, this.openNotificationPreferences);
|
||||||
|
|
||||||
|
DeveloperMode.switchOff('disableNotificationStorage', () => {
|
||||||
|
this.mentionsPerChannel = new Map();
|
||||||
|
this.allActiveNotifications = new Map();
|
||||||
|
}, () => {
|
||||||
|
this.mentionsPerChannel?.clear();
|
||||||
|
delete this.mentionsPerChannel;
|
||||||
|
this.allActiveNotifications?.clear();
|
||||||
|
delete this.allActiveNotifications;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) {
|
public async displayMention(title: string, body: string, channelId: string, teamId: string, url: string, silent: boolean, webcontents: Electron.WebContents, soundName: string) {
|
||||||
|
@ -68,12 +79,12 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const mention = new Mention(options, channelId, teamId);
|
const mention = new Mention(options, channelId, teamId);
|
||||||
this.allActiveNotifications.set(mention.uId, mention);
|
this.allActiveNotifications?.set(mention.uId, mention);
|
||||||
|
|
||||||
mention.on('click', () => {
|
mention.on('click', () => {
|
||||||
log.debug('notification click', serverName, mention.uId);
|
log.debug('notification click', serverName, mention.uId);
|
||||||
|
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
|
|
||||||
// Show the window after navigation has finished to avoid the focus handler
|
// Show the window after navigation has finished to avoid the focus handler
|
||||||
// being called before the current channel has updated
|
// being called before the current channel has updated
|
||||||
|
@ -87,7 +98,7 @@ class NotificationManager {
|
||||||
});
|
});
|
||||||
|
|
||||||
mention.on('close', () => {
|
mention.on('close', () => {
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
return new Promise((resolve) => {
|
return new Promise((resolve) => {
|
||||||
|
@ -107,12 +118,12 @@ class NotificationManager {
|
||||||
// On Windows, manually dismiss notifications from the same channel and only show the latest one
|
// On Windows, manually dismiss notifications from the same channel and only show the latest one
|
||||||
if (process.platform === 'win32') {
|
if (process.platform === 'win32') {
|
||||||
const mentionKey = `${mention.teamId}:${mention.channelId}`;
|
const mentionKey = `${mention.teamId}:${mention.channelId}`;
|
||||||
if (this.mentionsPerChannel.has(mentionKey)) {
|
if (this.mentionsPerChannel?.has(mentionKey)) {
|
||||||
log.debug(`close ${mentionKey}`);
|
log.debug(`close ${mentionKey}`);
|
||||||
this.mentionsPerChannel.get(mentionKey)?.close();
|
this.mentionsPerChannel?.get(mentionKey)?.close();
|
||||||
this.mentionsPerChannel.delete(mentionKey);
|
this.mentionsPerChannel?.delete(mentionKey);
|
||||||
}
|
}
|
||||||
this.mentionsPerChannel.set(mentionKey, mention);
|
this.mentionsPerChannel?.set(mentionKey, mention);
|
||||||
}
|
}
|
||||||
const notificationSound = mention.getNotificationSound();
|
const notificationSound = mention.getNotificationSound();
|
||||||
if (notificationSound) {
|
if (notificationSound) {
|
||||||
|
@ -127,7 +138,7 @@ class NotificationManager {
|
||||||
|
|
||||||
mention.on('failed', (_, error) => {
|
mention.on('failed', (_, error) => {
|
||||||
failed = true;
|
failed = true;
|
||||||
this.allActiveNotifications.delete(mention.uId);
|
this.allActiveNotifications?.delete(mention.uId);
|
||||||
clearTimeout(timeout);
|
clearTimeout(timeout);
|
||||||
|
|
||||||
// Special case for Windows - means that notifications are disabled at the OS level
|
// Special case for Windows - means that notifications are disabled at the OS level
|
||||||
|
@ -156,7 +167,7 @@ class NotificationManager {
|
||||||
}
|
}
|
||||||
|
|
||||||
const download = new DownloadNotification(fileName, serverName);
|
const download = new DownloadNotification(fileName, serverName);
|
||||||
this.allActiveNotifications.set(download.uId, download);
|
this.allActiveNotifications?.set(download.uId, download);
|
||||||
|
|
||||||
download.on('show', () => {
|
download.on('show', () => {
|
||||||
flashFrame(true);
|
flashFrame(true);
|
||||||
|
@ -164,15 +175,15 @@ class NotificationManager {
|
||||||
|
|
||||||
download.on('click', () => {
|
download.on('click', () => {
|
||||||
shell.showItemInFolder(path.normalize());
|
shell.showItemInFolder(path.normalize());
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
download.on('close', () => {
|
download.on('close', () => {
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
|
|
||||||
download.on('failed', () => {
|
download.on('failed', () => {
|
||||||
this.allActiveNotifications.delete(download.uId);
|
this.allActiveNotifications?.delete(download.uId);
|
||||||
});
|
});
|
||||||
download.show();
|
download.show();
|
||||||
}
|
}
|
||||||
|
|
|
@ -43,10 +43,19 @@ import {
|
||||||
UNREADS_AND_MENTIONS,
|
UNREADS_AND_MENTIONS,
|
||||||
LEGACY_OFF,
|
LEGACY_OFF,
|
||||||
TAB_LOGIN_CHANGED,
|
TAB_LOGIN_CHANGED,
|
||||||
|
GET_DEVELOPER_MODE_SETTING,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
import type {ExternalAPI} from 'types/externalAPI';
|
import type {ExternalAPI} from 'types/externalAPI';
|
||||||
|
|
||||||
|
let legacyEnabled = false;
|
||||||
|
let legacyOff: () => void;
|
||||||
|
|
||||||
|
ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceLegacyAPI').then((force) => {
|
||||||
|
if (force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const createListener: ExternalAPI['createListener'] = (channel: string, listener: (...args: never[]) => void) => {
|
const createListener: ExternalAPI['createListener'] = (channel: string, listener: (...args: never[]) => void) => {
|
||||||
const listenerWithEvent = (_: IpcRendererEvent, ...args: unknown[]) =>
|
const listenerWithEvent = (_: IpcRendererEvent, ...args: unknown[]) =>
|
||||||
listener(...args as never[]);
|
listener(...args as never[]);
|
||||||
|
@ -63,7 +72,7 @@ const desktopAPI: DesktopAPI = {
|
||||||
getAppInfo: () => {
|
getAppInfo: () => {
|
||||||
// Using this signal as the sign to disable the legacy code, since it is run before the app is rendered
|
// Using this signal as the sign to disable the legacy code, since it is run before the app is rendered
|
||||||
if (legacyEnabled) {
|
if (legacyEnabled) {
|
||||||
legacyOff();
|
legacyOff?.();
|
||||||
}
|
}
|
||||||
|
|
||||||
return ipcRenderer.invoke(GET_APP_INFO);
|
return ipcRenderer.invoke(GET_APP_INFO);
|
||||||
|
@ -126,6 +135,7 @@ const desktopAPI: DesktopAPI = {
|
||||||
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
unregister: (channel) => ipcRenderer.removeAllListeners(channel),
|
||||||
};
|
};
|
||||||
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
contextBridge.exposeInMainWorld('desktopAPI', desktopAPI);
|
||||||
|
});
|
||||||
|
|
||||||
// Specific info for the testing environment
|
// Specific info for the testing environment
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
|
@ -181,6 +191,11 @@ setInterval(() => {
|
||||||
webFrame.clearCache();
|
webFrame.clearCache();
|
||||||
}, CLEAR_CACHE_INTERVAL);
|
}, CLEAR_CACHE_INTERVAL);
|
||||||
|
|
||||||
|
ipcRenderer.invoke(GET_DEVELOPER_MODE_SETTING, 'forceNewAPI').then((force) => {
|
||||||
|
if (force) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
/****************************************************************************
|
/****************************************************************************
|
||||||
* LEGACY CODE BELOW
|
* LEGACY CODE BELOW
|
||||||
* All of this code is deprecated and should be removed eventually
|
* All of this code is deprecated and should be removed eventually
|
||||||
|
@ -474,13 +489,13 @@ ipcRenderer.on(USER_ACTIVITY_UPDATE, (event, userIsActive, isSystemEvent) => {
|
||||||
* Legacy functionality that needs to be disabled with the new API
|
* Legacy functionality that needs to be disabled with the new API
|
||||||
*/
|
*/
|
||||||
|
|
||||||
let legacyEnabled = true;
|
legacyEnabled = true;
|
||||||
ipcRenderer.on(IS_UNREAD, checkUnread);
|
ipcRenderer.on(IS_UNREAD, checkUnread);
|
||||||
const unreadInterval = setInterval(getUnreadCount, 1000);
|
const unreadInterval = setInterval(getUnreadCount, 1000);
|
||||||
window.addEventListener('storage', onStorageChanged);
|
window.addEventListener('storage', onStorageChanged);
|
||||||
window.addEventListener('load', onLoad);
|
window.addEventListener('load', onLoad);
|
||||||
|
|
||||||
function legacyOff() {
|
legacyOff = () => {
|
||||||
ipcRenderer.send(LEGACY_OFF);
|
ipcRenderer.send(LEGACY_OFF);
|
||||||
ipcRenderer.off(IS_UNREAD, checkUnread);
|
ipcRenderer.off(IS_UNREAD, checkUnread);
|
||||||
clearInterval(unreadInterval);
|
clearInterval(unreadInterval);
|
||||||
|
@ -489,4 +504,5 @@ function legacyOff() {
|
||||||
|
|
||||||
legacyEnabled = false;
|
legacyEnabled = false;
|
||||||
console.log('New API preload initialized');
|
console.log('New API preload initialized');
|
||||||
}
|
};
|
||||||
|
});
|
||||||
|
|
|
@ -92,6 +92,7 @@ import {
|
||||||
GET_MEDIA_ACCESS_STATUS,
|
GET_MEDIA_ACCESS_STATUS,
|
||||||
VIEW_FINISHED_RESIZING,
|
VIEW_FINISHED_RESIZING,
|
||||||
GET_NONCE,
|
GET_NONCE,
|
||||||
|
IS_DEVELOPER_MODE_ENABLED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
console.log('Preload initialized');
|
console.log('Preload initialized');
|
||||||
|
@ -126,6 +127,7 @@ contextBridge.exposeInMainWorld('desktop', {
|
||||||
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),
|
getNonce: () => ipcRenderer.invoke(GET_NONCE),
|
||||||
|
isDeveloperModeEnabled: () => ipcRenderer.invoke(IS_DEVELOPER_MODE_ENABLED),
|
||||||
|
|
||||||
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),
|
||||||
|
|
|
@ -88,12 +88,16 @@ export function getLocalPreload(file: string) {
|
||||||
return path.join(app.getAppPath(), file);
|
return path.join(app.getAppPath(), file);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function composeUserAgent() {
|
export function composeUserAgent(browserMode?: boolean) {
|
||||||
const baseUserAgent = app.userAgentFallback.split(' ');
|
const baseUserAgent = app.userAgentFallback.split(' ');
|
||||||
|
|
||||||
// filter out the Mattermost tag that gets added earlier on
|
// filter out the Mattermost tag that gets added earlier on
|
||||||
const filteredUserAgent = baseUserAgent.filter((ua) => !ua.startsWith('Mattermost'));
|
const filteredUserAgent = baseUserAgent.filter((ua) => !ua.startsWith('Mattermost'));
|
||||||
|
|
||||||
|
if (browserMode) {
|
||||||
|
return filteredUserAgent.join(' ');
|
||||||
|
}
|
||||||
|
|
||||||
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`;
|
return `${filteredUserAgent.join(' ')} Mattermost/${app.getVersion()}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -58,6 +58,9 @@ jest.mock('../utils', () => ({
|
||||||
composeUserAgent: () => 'Mattermost/5.0.0',
|
composeUserAgent: () => 'Mattermost/5.0.0',
|
||||||
shouldHaveBackBar: jest.fn(),
|
shouldHaveBackBar: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/developerMode', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
|
||||||
const view = new MessagingView(server, true);
|
const view = new MessagingView(server, true);
|
||||||
|
|
|
@ -24,6 +24,7 @@ import ServerManager from 'common/servers/serverManager';
|
||||||
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
|
||||||
import {isInternalURL, parseURL} from 'common/utils/url';
|
import {isInternalURL, parseURL} from 'common/utils/url';
|
||||||
import type {MattermostView} from 'common/views/View';
|
import type {MattermostView} from 'common/views/View';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import {getServerAPI} from 'main/server/serverAPI';
|
import {getServerAPI} from 'main/server/serverAPI';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
@ -52,7 +53,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
private atRoot: boolean;
|
private atRoot: boolean;
|
||||||
private options: BrowserViewConstructorOptions;
|
private options: BrowserViewConstructorOptions;
|
||||||
private removeLoading?: NodeJS.Timeout;
|
private removeLoading?: NodeJS.Timeout;
|
||||||
private contextMenu: ContextMenu;
|
private contextMenu?: ContextMenu;
|
||||||
private status?: Status;
|
private status?: Status;
|
||||||
private retryLoad?: NodeJS.Timeout;
|
private retryLoad?: NodeJS.Timeout;
|
||||||
private maxRetries: number;
|
private maxRetries: number;
|
||||||
|
@ -65,7 +66,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
const preload = getLocalPreload('externalAPI.js');
|
const preload = getLocalPreload('externalAPI.js');
|
||||||
this.options = Object.assign({}, options);
|
this.options = Object.assign({}, options);
|
||||||
this.options.webPreferences = {
|
this.options.webPreferences = {
|
||||||
preload,
|
preload: DeveloperMode.get('browserOnly') ? undefined : preload,
|
||||||
additionalArguments: [
|
additionalArguments: [
|
||||||
`version=${app.getVersion()}`,
|
`version=${app.getVersion()}`,
|
||||||
`appName=${app.name}`,
|
`appName=${app.name}`,
|
||||||
|
@ -99,7 +100,10 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
|
|
||||||
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
|
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
|
||||||
|
|
||||||
|
if (!DeveloperMode.get('disableContextMenu')) {
|
||||||
this.contextMenu = new ContextMenu({}, this.browserView);
|
this.contextMenu = new ContextMenu({}, this.browserView);
|
||||||
|
}
|
||||||
|
|
||||||
this.maxRetries = MAX_SERVER_RETRIES;
|
this.maxRetries = MAX_SERVER_RETRIES;
|
||||||
|
|
||||||
this.altPressStatus = false;
|
this.altPressStatus = false;
|
||||||
|
@ -192,7 +196,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
loadURL = this.view.url.toString();
|
loadURL = this.view.url.toString();
|
||||||
}
|
}
|
||||||
this.log.verbose(`Loading ${loadURL}`);
|
this.log.verbose(`Loading ${loadURL}`);
|
||||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||||
if (err.code && err.code.startsWith('ERR_CERT')) {
|
if (err.code && err.code.startsWith('ERR_CERT')) {
|
||||||
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
|
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
|
||||||
|
@ -427,7 +431,7 @@ export class MattermostBrowserView extends EventEmitter {
|
||||||
if (!this.browserView || !this.browserView.webContents) {
|
if (!this.browserView || !this.browserView.webContents) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent(DeveloperMode.get('browserOnly'))});
|
||||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||||
if (this.maxRetries-- > 0) {
|
if (this.maxRetries-- > 0) {
|
||||||
this.loadRetry(loadURL, err);
|
this.loadRetry(loadURL, err);
|
||||||
|
|
|
@ -34,6 +34,7 @@ import {
|
||||||
LEGACY_OFF,
|
LEGACY_OFF,
|
||||||
UNREADS_AND_MENTIONS,
|
UNREADS_AND_MENTIONS,
|
||||||
TAB_LOGIN_CHANGED,
|
TAB_LOGIN_CHANGED,
|
||||||
|
DEVELOPER_MODE_UPDATED,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {Logger} from 'common/log';
|
import {Logger} from 'common/log';
|
||||||
|
@ -45,10 +46,13 @@ import Utils from 'common/utils/util';
|
||||||
import type {MattermostView} from 'common/views/View';
|
import type {MattermostView} from 'common/views/View';
|
||||||
import {TAB_MESSAGING} from 'common/views/View';
|
import {TAB_MESSAGING} from 'common/views/View';
|
||||||
import {flushCookiesStore} from 'main/app/utils';
|
import {flushCookiesStore} from 'main/app/utils';
|
||||||
|
import DeveloperMode from 'main/developerMode';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
import PermissionsManager from 'main/permissionsManager';
|
import PermissionsManager from 'main/permissionsManager';
|
||||||
import MainWindow from 'main/windows/mainWindow';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
|
import type {DeveloperSettings} from 'types/settings';
|
||||||
|
|
||||||
import LoadingScreen from './loadingScreen';
|
import LoadingScreen from './loadingScreen';
|
||||||
import {MattermostBrowserView} from './MattermostBrowserView';
|
import {MattermostBrowserView} from './MattermostBrowserView';
|
||||||
import modalManager from './modalManager';
|
import modalManager from './modalManager';
|
||||||
|
@ -91,6 +95,7 @@ export class ViewManager {
|
||||||
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId));
|
||||||
|
|
||||||
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
|
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
|
||||||
|
DeveloperMode.on(DEVELOPER_MODE_UPDATED, this.handleDeveloperModeUpdated);
|
||||||
}
|
}
|
||||||
|
|
||||||
private init = () => {
|
private init = () => {
|
||||||
|
@ -99,6 +104,17 @@ export class ViewManager {
|
||||||
this.showInitial();
|
this.showInitial();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private handleDeveloperModeUpdated = (json: DeveloperSettings) => {
|
||||||
|
log.debug('handleDeveloperModeUpdated', json);
|
||||||
|
|
||||||
|
if (['browserOnly', 'disableContextMenu', 'forceLegacyAPI', 'forceNewAPI'].some((key) => Object.hasOwn(json, key))) {
|
||||||
|
this.views.forEach((view) => view.destroy());
|
||||||
|
this.views = new Map();
|
||||||
|
this.closedViews = new Map();
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
getView = (viewId: string) => {
|
getView = (viewId: string) => {
|
||||||
return this.views.get(viewId);
|
return this.views.get(viewId);
|
||||||
};
|
};
|
||||||
|
|
36
src/renderer/components/DeveloperModeIndicator.tsx
Normal file
36
src/renderer/components/DeveloperModeIndicator.tsx
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import React from 'react';
|
||||||
|
// eslint-disable-next-line no-restricted-imports
|
||||||
|
import {OverlayTrigger, Tooltip} from 'react-bootstrap';
|
||||||
|
import {FormattedMessage} from 'react-intl';
|
||||||
|
|
||||||
|
import 'renderer/css/components/DeveloperModeIndicator.scss';
|
||||||
|
|
||||||
|
export default function DeveloperModeIndicator({developerMode, darkMode}: {developerMode: boolean; darkMode: boolean}) {
|
||||||
|
if (!developerMode) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger
|
||||||
|
placement='left'
|
||||||
|
overlay={
|
||||||
|
<Tooltip id='DeveloperModeIndicator__tooltip'>
|
||||||
|
<FormattedMessage
|
||||||
|
id='renderer.components.developerModeIndicator.tooltip'
|
||||||
|
defaultMessage='Developer mode is enabled. You should only have this enabled if a Mattermost developer has instructed you to.'
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div className={classNames('DeveloperModeIndicator', {darkMode})}>
|
||||||
|
<i className='icon-flask-outline'/>
|
||||||
|
<span className='DeveloperModeIndicator__badge'/>
|
||||||
|
</div>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
|
@ -12,6 +12,7 @@ import {injectIntl} from 'react-intl';
|
||||||
import type {UniqueView, UniqueServer} from 'types/config';
|
import type {UniqueView, UniqueServer} from 'types/config';
|
||||||
import type {DownloadedItems} from 'types/downloads';
|
import type {DownloadedItems} from 'types/downloads';
|
||||||
|
|
||||||
|
import DeveloperModeIndicator from './DeveloperModeIndicator';
|
||||||
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
|
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
|
||||||
import ErrorView from './ErrorView';
|
import ErrorView from './ErrorView';
|
||||||
import ExtraBar from './ExtraBar';
|
import ExtraBar from './ExtraBar';
|
||||||
|
@ -55,6 +56,7 @@ type State = {
|
||||||
showDownloadsBadge: boolean;
|
showDownloadsBadge: boolean;
|
||||||
hasDownloads: boolean;
|
hasDownloads: boolean;
|
||||||
threeDotsIsFocused: boolean;
|
threeDotsIsFocused: boolean;
|
||||||
|
developerMode: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TabViewStatus = {
|
type TabViewStatus = {
|
||||||
|
@ -88,6 +90,7 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
showDownloadsBadge: false,
|
showDownloadsBadge: false,
|
||||||
hasDownloads: false,
|
hasDownloads: false,
|
||||||
threeDotsIsFocused: false,
|
threeDotsIsFocused: false,
|
||||||
|
developerMode: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -263,6 +266,10 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
}
|
}
|
||||||
|
|
||||||
window.addEventListener('click', this.handleCloseDropdowns);
|
window.addEventListener('click', this.handleCloseDropdowns);
|
||||||
|
|
||||||
|
window.desktop.isDeveloperModeEnabled().then((developerMode) => {
|
||||||
|
this.setState({developerMode});
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
componentWillUnmount() {
|
componentWillUnmount() {
|
||||||
|
@ -471,6 +478,10 @@ class MainPage extends React.PureComponent<Props, State> {
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{tabsRow}
|
{tabsRow}
|
||||||
|
<DeveloperModeIndicator
|
||||||
|
darkMode={this.props.darkMode}
|
||||||
|
developerMode={this.state.developerMode}
|
||||||
|
/>
|
||||||
{downloadsDropdownButton}
|
{downloadsDropdownButton}
|
||||||
{window.process.platform !== 'darwin' && this.state.fullScreen &&
|
{window.process.platform !== 'darwin' && this.state.fullScreen &&
|
||||||
<span className='title-bar-btns'>
|
<span className='title-bar-btns'>
|
||||||
|
|
54
src/renderer/css/components/DeveloperModeIndicator.scss
Normal file
54
src/renderer/css/components/DeveloperModeIndicator.scss
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
.DeveloperModeIndicator {
|
||||||
|
align-items: center;
|
||||||
|
background: transparent;
|
||||||
|
border-radius: 4px;
|
||||||
|
border: none;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
margin: 4px;
|
||||||
|
position: relative;
|
||||||
|
width: 32px;
|
||||||
|
|
||||||
|
i {
|
||||||
|
color: rgba(63, 67, 80, 0.56);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 21px;
|
||||||
|
line-height: 21px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
i {
|
||||||
|
color: rgba(63, 67, 80, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.DeveloperModeIndicator__badge {
|
||||||
|
background: rgba(210, 75, 78, 1);
|
||||||
|
border-radius: 10px;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
position: absolute;
|
||||||
|
top: 5px;
|
||||||
|
right: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.darkMode {
|
||||||
|
i {
|
||||||
|
color: rgba(221, 223, 228, 0.56);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover, &:focus {
|
||||||
|
i {
|
||||||
|
color: rgba(221, 223, 228, 0.78);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#DeveloperModeIndicator__tooltip {
|
||||||
|
> .tooltip-inner {
|
||||||
|
max-width: none;
|
||||||
|
}
|
||||||
|
}
|
|
@ -8,3 +8,12 @@ export type SaveQueueItem = {
|
||||||
key: keyof CombinedConfig;
|
key: keyof CombinedConfig;
|
||||||
data: CombinedConfig[keyof CombinedConfig];
|
data: CombinedConfig[keyof CombinedConfig];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type DeveloperSettings = {
|
||||||
|
browserOnly?: boolean;
|
||||||
|
disableNotificationStorage?: boolean;
|
||||||
|
disableUserActivityMonitor?: boolean;
|
||||||
|
disableContextMenu?: boolean;
|
||||||
|
forceLegacyAPI?: boolean;
|
||||||
|
forceNewAPI?: boolean;
|
||||||
|
};
|
||||||
|
|
|
@ -45,6 +45,7 @@ declare global {
|
||||||
checkForUpdates: () => void;
|
checkForUpdates: () => void;
|
||||||
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
|
||||||
getNonce: () => Promise<string | undefined>;
|
getNonce: () => Promise<string | undefined>;
|
||||||
|
isDeveloperModeEnabled: () => Promise<boolean>;
|
||||||
|
|
||||||
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
updateServerOrder: (serverOrder: string[]) => Promise<void>;
|
||||||
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
|
||||||
|
|
Loading…
Reference in a new issue