[MM-40406] Add more singletons, refactor main.ts into pieces, add tests and some cleanup + tests for additional coverage (#1890)
* Refactor main.ts dependencies into singleton pattern * Split main.ts into testable pieces, some other refactoring for singleton pattern * Unit tests for main/app/app * Unit tests for main/app/config * Unit tests for main/app/initialize * Unit tests for main/app/intercom * Unit tests for main/app/utils * Add some more tests to get to 70% coverage * Fix for linux * Fix for alternate data dir paths * Fix E2E test
This commit is contained in:
@@ -72,7 +72,6 @@
|
|||||||
"scripts/check_build_config.js",
|
"scripts/check_build_config.js",
|
||||||
"LICENSE.txt",
|
"LICENSE.txt",
|
||||||
"src/utils/util.ts",
|
"src/utils/util.ts",
|
||||||
"src/main/main.ts",
|
|
||||||
"src/main/contextMenu.ts",
|
"src/main/contextMenu.ts",
|
||||||
"src/renderer/updater.tsx",
|
"src/renderer/updater.tsx",
|
||||||
"src/main/badge.ts",
|
"src/main/badge.ts",
|
||||||
|
@@ -57,7 +57,7 @@ describe('config', function desc() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('MM-T4402 should upgrade v0 config file', async () => {
|
it('MM-T4402 should upgrade v0 config file', async () => {
|
||||||
const Config = require('../../../src/common/config').default;
|
const Config = require('../../../src/common/config').Config;
|
||||||
const newConfig = new Config(env.configFilePath);
|
const newConfig = new Config(env.configFilePath);
|
||||||
const oldConfig = {
|
const oldConfig = {
|
||||||
url: env.mattermostURL,
|
url: env.mattermostURL,
|
||||||
|
@@ -72,10 +72,15 @@
|
|||||||
"jsx",
|
"jsx",
|
||||||
"json"
|
"json"
|
||||||
],
|
],
|
||||||
|
"collectCoverageFrom": [
|
||||||
|
"src/common/**/*.ts",
|
||||||
|
"src/main/**/*.ts"
|
||||||
|
],
|
||||||
"testMatch": ["**/src/**/*.test.js"],
|
"testMatch": ["**/src/**/*.test.js"],
|
||||||
"globals": {
|
"globals": {
|
||||||
"__HASH_VERSION__": "5.0.0"
|
"__HASH_VERSION__": "5.0.0"
|
||||||
}
|
},
|
||||||
|
"setupFiles": ["./src/jestSetup.js"]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/cli": "^7.14.5",
|
"@babel/cli": "^7.14.5",
|
||||||
|
@@ -100,3 +100,5 @@ export const GET_VIEW_WEBCONTENTS_ID = 'get-view-webcontents-id';
|
|||||||
|
|
||||||
export const GET_MODAL_UNCLOSEABLE = 'get-modal-uncloseable';
|
export const GET_MODAL_UNCLOSEABLE = 'get-modal-uncloseable';
|
||||||
export const MODAL_UNCLOSEABLE = 'modal-uncloseable';
|
export const MODAL_UNCLOSEABLE = 'modal-uncloseable';
|
||||||
|
|
||||||
|
export const UPDATE_PATHS = 'update-paths';
|
||||||
|
@@ -19,6 +19,26 @@ jest.mock('winreg-utf8', () => {
|
|||||||
value: `${key}-value-2`,
|
value: `${key}-value-2`,
|
||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
} else if (hive === 'mattermost-hive') {
|
||||||
|
if (key.endsWith('DefaultServerList')) {
|
||||||
|
fn(null, [
|
||||||
|
{
|
||||||
|
name: 'server-1',
|
||||||
|
value: 'http://server-1.com',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
} else {
|
||||||
|
fn(null, [
|
||||||
|
{
|
||||||
|
name: 'EnableServerManagement',
|
||||||
|
value: '0x1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'EnableAutoUpdater',
|
||||||
|
value: '0x1',
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
} else if (hive === 'really-bad-hive') {
|
} else if (hive === 'really-bad-hive') {
|
||||||
throw new Error('This is an error');
|
throw new Error('This is an error');
|
||||||
} else {
|
} else {
|
||||||
@@ -34,9 +54,33 @@ jest.mock('electron-log', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('common/config/RegistryConfig', () => {
|
describe('common/config/RegistryConfig', () => {
|
||||||
|
it('should initialize correctly', async () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
const registryConfig = new RegistryConfig();
|
||||||
|
const originalFn = registryConfig.getRegistryEntryValues;
|
||||||
|
registryConfig.getRegistryEntryValues = (hive, key, name) => originalFn('mattermost-hive', key, name);
|
||||||
|
await registryConfig.init();
|
||||||
|
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
expect(registryConfig.data.teams).toContainEqual({
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
order: 0,
|
||||||
|
});
|
||||||
|
expect(registryConfig.data.enableAutoUpdater).toBe(true);
|
||||||
|
expect(registryConfig.data.enableServerManagement).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
describe('getRegistryEntryValues', () => {
|
describe('getRegistryEntryValues', () => {
|
||||||
|
const registryConfig = new RegistryConfig();
|
||||||
|
|
||||||
it('should return correct values', () => {
|
it('should return correct values', () => {
|
||||||
const registryConfig = new RegistryConfig();
|
|
||||||
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key')).resolves.toStrictEqual([
|
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key')).resolves.toStrictEqual([
|
||||||
{
|
{
|
||||||
name: 'correct-key-name-1',
|
name: 'correct-key-name-1',
|
||||||
@@ -50,22 +94,18 @@ describe('common/config/RegistryConfig', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return correct value by name', () => {
|
it('should return correct value by name', () => {
|
||||||
const registryConfig = new RegistryConfig();
|
|
||||||
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'correct-key-name-1')).resolves.toBe('correct-key-value-1');
|
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'correct-key-name-1')).resolves.toBe('correct-key-value-1');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined with wrong name', () => {
|
it('should return undefined with wrong name', () => {
|
||||||
const registryConfig = new RegistryConfig();
|
|
||||||
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'wrong-key-name-1')).resolves.toBe(undefined);
|
expect(registryConfig.getRegistryEntryValues('correct-hive', 'correct-key', 'wrong-key-name-1')).resolves.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return undefined with bad hive', () => {
|
it('should return undefined with bad hive', () => {
|
||||||
const registryConfig = new RegistryConfig();
|
|
||||||
expect(registryConfig.getRegistryEntryValues('bad-hive', 'correct-key')).resolves.toBe(undefined);
|
expect(registryConfig.getRegistryEntryValues('bad-hive', 'correct-key')).resolves.toBe(undefined);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call reject when an error occurs', () => {
|
it('should call reject when an error occurs', () => {
|
||||||
const registryConfig = new RegistryConfig();
|
|
||||||
expect(registryConfig.getRegistryEntryValues('really-bad-hive', 'correct-key')).rejects.toThrow(new Error('This is an error'));
|
expect(registryConfig.getRegistryEntryValues('really-bad-hive', 'correct-key')).rejects.toThrow(new Error('This is an error'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,13 +1,17 @@
|
|||||||
// 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 Config from 'common/config';
|
import {Config} from 'common/config';
|
||||||
|
|
||||||
const configPath = '/fake/config/path';
|
const configPath = '/fake/config/path';
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
jest.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
name: 'Mattermost',
|
name: 'Mattermost',
|
||||||
|
getPath: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
},
|
},
|
||||||
nativeTheme: {
|
nativeTheme: {
|
||||||
shouldUseDarkColors: false,
|
shouldUseDarkColors: false,
|
||||||
@@ -122,7 +126,7 @@ describe('common/config', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reload', () => {
|
describe('reload', () => {
|
||||||
it('should emit update and synchronize events', () => {
|
it('should emit update event', () => {
|
||||||
const config = new Config(configPath);
|
const config = new Config(configPath);
|
||||||
config.loadDefaultConfigData = jest.fn();
|
config.loadDefaultConfigData = jest.fn();
|
||||||
config.loadBuildConfigData = jest.fn();
|
config.loadBuildConfigData = jest.fn();
|
||||||
@@ -135,7 +139,6 @@ describe('common/config', () => {
|
|||||||
|
|
||||||
config.reload();
|
config.reload();
|
||||||
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
|
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
|
||||||
expect(config.emit).toHaveBeenNthCalledWith(2, 'synchronize');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -170,7 +173,7 @@ describe('common/config', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('saveLocalConfigData', () => {
|
describe('saveLocalConfigData', () => {
|
||||||
it('should emit update and synchronize events on save', () => {
|
it('should emit update event on save', () => {
|
||||||
const config = new Config(configPath);
|
const config = new Config(configPath);
|
||||||
config.localConfigData = {test: 'test'};
|
config.localConfigData = {test: 'test'};
|
||||||
config.combinedData = {...config.localConfigData};
|
config.combinedData = {...config.localConfigData};
|
||||||
@@ -181,7 +184,6 @@ describe('common/config', () => {
|
|||||||
|
|
||||||
config.saveLocalConfigData();
|
config.saveLocalConfigData();
|
||||||
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
|
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
|
||||||
expect(config.emit).toHaveBeenNthCalledWith(2, 'synchronize');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should emit error when fs.writeSync throws an error', () => {
|
it('should emit error when fs.writeSync throws an error', () => {
|
||||||
|
@@ -20,8 +20,9 @@ import {
|
|||||||
TeamWithTabs,
|
TeamWithTabs,
|
||||||
} from 'types/config';
|
} from 'types/config';
|
||||||
|
|
||||||
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION} from 'common/communication';
|
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION, UPDATE_PATHS} from 'common/communication';
|
||||||
|
|
||||||
|
import {configPath} from 'main/constants';
|
||||||
import * as Validator from 'main/Validator';
|
import * as Validator from 'main/Validator';
|
||||||
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
@@ -35,7 +36,7 @@ import RegistryConfig, {REGISTRY_READ_EVENT} from './RegistryConfig';
|
|||||||
* Handles loading and merging all sources of configuration as well as saving user provided config
|
* Handles loading and merging all sources of configuration as well as saving user provided config
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default class Config extends EventEmitter {
|
export class Config extends EventEmitter {
|
||||||
configFilePath: string;
|
configFilePath: string;
|
||||||
|
|
||||||
registryConfig: RegistryConfig;
|
registryConfig: RegistryConfig;
|
||||||
@@ -108,7 +109,6 @@ export default class Config extends EventEmitter {
|
|||||||
this.regenerateCombinedConfigData();
|
this.regenerateCombinedConfigData();
|
||||||
|
|
||||||
this.emit('update', this.combinedData);
|
this.emit('update', this.combinedData);
|
||||||
this.emit('synchronize');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -186,7 +186,6 @@ export default class Config extends EventEmitter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
this.emit('update', this.combinedData);
|
this.emit('update', this.combinedData);
|
||||||
this.emit('synchronize');
|
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
this.emit('error', error);
|
this.emit('error', error);
|
||||||
@@ -266,6 +265,12 @@ export default class Config extends EventEmitter {
|
|||||||
get helpLink() {
|
get helpLink() {
|
||||||
return this.combinedData?.helpLink;
|
return this.combinedData?.helpLink;
|
||||||
}
|
}
|
||||||
|
get minimizeToTray() {
|
||||||
|
return this.combinedData?.minimizeToTray;
|
||||||
|
}
|
||||||
|
get lastActiveTeam() {
|
||||||
|
return this.combinedData?.lastActiveTeam;
|
||||||
|
}
|
||||||
|
|
||||||
// initialization/processing methods
|
// initialization/processing methods
|
||||||
|
|
||||||
@@ -540,3 +545,13 @@ export default class Config extends EventEmitter {
|
|||||||
this.emit('darkModeChange', this.combinedData.darkMode);
|
this.emit('darkModeChange', this.combinedData.darkMode);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const config = new Config(configPath);
|
||||||
|
export default config;
|
||||||
|
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
config.configFilePath = configPath;
|
||||||
|
if (config.combinedData) {
|
||||||
|
config.reload();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -2,13 +2,11 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
|
||||||
import electron from 'electron';
|
import {screen} from 'electron';
|
||||||
|
|
||||||
import {DEVELOPMENT, PRODUCTION} from './constants';
|
import {DEVELOPMENT, PRODUCTION} from './constants';
|
||||||
|
|
||||||
function getDisplayBoundaries() {
|
function getDisplayBoundaries() {
|
||||||
const {screen} = electron;
|
|
||||||
|
|
||||||
const displays = screen.getAllDisplays();
|
const displays = screen.getAllDisplays();
|
||||||
|
|
||||||
return displays.map((display) => {
|
return displays.map((display) => {
|
||||||
|
13
src/jestSetup.js
Normal file
13
src/jestSetup.js
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
jest.mock('main/constants', () => ({
|
||||||
|
configPath: 'configPath',
|
||||||
|
allowedProtocolFile: 'allowedProtocolFile',
|
||||||
|
appVersionJson: 'appVersionJson',
|
||||||
|
certificateStorePath: 'certificateStorePath',
|
||||||
|
trustedOriginsStoreFile: 'trustedOriginsStoreFile',
|
||||||
|
boundsInfoPath: 'boundsInfoPath',
|
||||||
|
|
||||||
|
updatePaths: jest.fn(),
|
||||||
|
}));
|
35
src/main/AppVersionManager.test.js
Normal file
35
src/main/AppVersionManager.test.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import * as Validator from 'main/Validator';
|
||||||
|
|
||||||
|
import {AppVersionManager} from './AppVersionManager';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
readFileSync: jest.fn(),
|
||||||
|
writeFile: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/Validator', () => ({
|
||||||
|
validateAppState: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/AppVersionManager', () => {
|
||||||
|
it('should wipe out JSON file when validation fails', () => {
|
||||||
|
fs.readFileSync.mockReturnValue('some bad JSON');
|
||||||
|
Validator.validateAppState.mockReturnValue(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-unused-vars
|
||||||
|
const appVersionManager = new AppVersionManager('somefilename.txt');
|
||||||
|
|
||||||
|
expect(fs.writeFile).toBeCalledWith('somefilename.txt', '{}', expect.any(Function));
|
||||||
|
});
|
||||||
|
});
|
@@ -2,22 +2,31 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||||
|
|
||||||
|
import {ipcMain} from 'electron';
|
||||||
|
|
||||||
import {AppState} from 'types/appState';
|
import {AppState} from 'types/appState';
|
||||||
|
|
||||||
import JsonFileManager from '../common/JsonFileManager';
|
import {UPDATE_PATHS} from 'common/communication';
|
||||||
|
import JsonFileManager from 'common/JsonFileManager';
|
||||||
|
|
||||||
|
import {appVersionJson} from 'main/constants';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
|
|
||||||
export default class AppVersionManager extends JsonFileManager<AppState> {
|
export class AppVersionManager extends JsonFileManager<AppState> {
|
||||||
constructor(file: string) {
|
constructor(file: string) {
|
||||||
super(file);
|
super(file);
|
||||||
|
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
init = () => {
|
||||||
// ensure data loaded from file is valid
|
// ensure data loaded from file is valid
|
||||||
const validatedJSON = Validator.validateAppState(this.json);
|
const validatedJSON = Validator.validateAppState(this.json);
|
||||||
if (!validatedJSON) {
|
if (!validatedJSON) {
|
||||||
this.setJson({});
|
this.setJson({});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set lastAppVersion(version) {
|
set lastAppVersion(version) {
|
||||||
this.setValue('lastAppVersion', version);
|
this.setValue('lastAppVersion', version);
|
||||||
}
|
}
|
||||||
@@ -46,3 +55,10 @@ export default class AppVersionManager extends JsonFileManager<AppState> {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let appVersionManager = new AppVersionManager(appVersionJson);
|
||||||
|
export default appVersionManager;
|
||||||
|
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
appVersionManager = new AppVersionManager(appVersionJson);
|
||||||
|
});
|
||||||
|
51
src/main/AutoLauncher.test.js
Normal file
51
src/main/AutoLauncher.test.js
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import AutoLaunch from 'auto-launch';
|
||||||
|
|
||||||
|
import {AutoLauncher} from './AutoLauncher';
|
||||||
|
|
||||||
|
jest.mock('auto-launch', () => jest.fn());
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
name: 'Mattermost',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-is-dev', () => false);
|
||||||
|
jest.mock('electron-log', () => ({}));
|
||||||
|
|
||||||
|
describe('main/AutoLauncher', () => {
|
||||||
|
let autoLauncher;
|
||||||
|
const isEnabled = jest.fn();
|
||||||
|
const enable = jest.fn();
|
||||||
|
const disable = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
AutoLaunch.mockImplementation(() => ({
|
||||||
|
isEnabled,
|
||||||
|
enable,
|
||||||
|
disable,
|
||||||
|
}));
|
||||||
|
autoLauncher = new AutoLauncher();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should toggle enabled/disabled', async () => {
|
||||||
|
isEnabled.mockReturnValue(true);
|
||||||
|
await autoLauncher.enable();
|
||||||
|
expect(enable).not.toBeCalled();
|
||||||
|
|
||||||
|
isEnabled.mockReturnValue(false);
|
||||||
|
await autoLauncher.enable();
|
||||||
|
expect(enable).toBeCalled();
|
||||||
|
|
||||||
|
isEnabled.mockReturnValue(false);
|
||||||
|
await autoLauncher.disable();
|
||||||
|
expect(disable).not.toBeCalled();
|
||||||
|
|
||||||
|
isEnabled.mockReturnValue(true);
|
||||||
|
await autoLauncher.disable();
|
||||||
|
expect(disable).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
@@ -7,7 +7,7 @@ import {app} from 'electron';
|
|||||||
import isDev from 'electron-is-dev';
|
import isDev from 'electron-is-dev';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
export default class AutoLauncher {
|
export class AutoLauncher {
|
||||||
appLauncher: AutoLaunch;
|
appLauncher: AutoLaunch;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -58,3 +58,6 @@ export default class AutoLauncher {
|
|||||||
return Promise.resolve(null);
|
return Promise.resolve(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const autoLauncher = new AutoLauncher();
|
||||||
|
export default autoLauncher;
|
||||||
|
@@ -8,7 +8,7 @@ import path from 'path';
|
|||||||
|
|
||||||
import {app, dialog} from 'electron';
|
import {app, dialog} from 'electron';
|
||||||
|
|
||||||
import CriticalErrorHandler from './CriticalErrorHandler';
|
import {CriticalErrorHandler} from './CriticalErrorHandler';
|
||||||
|
|
||||||
jest.mock('path', () => ({
|
jest.mock('path', () => ({
|
||||||
join: jest.fn().mockImplementation((...args) => args.join('/')),
|
join: jest.fn().mockImplementation((...args) => args.join('/')),
|
||||||
|
@@ -38,7 +38,7 @@ function openDetachedExternal(url: string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CriticalErrorHandler {
|
export class CriticalErrorHandler {
|
||||||
mainWindow?: BrowserWindow;
|
mainWindow?: BrowserWindow;
|
||||||
|
|
||||||
setMainWindow(mainWindow: BrowserWindow) {
|
setMainWindow(mainWindow: BrowserWindow) {
|
||||||
@@ -114,3 +114,6 @@ export default class CriticalErrorHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const criticalErrorHandler = new CriticalErrorHandler();
|
||||||
|
export default criticalErrorHandler;
|
||||||
|
|
||||||
|
12
src/main/ParseArgs.test.js
Normal file
12
src/main/ParseArgs.test.js
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import parse from 'main/ParseArgs';
|
||||||
|
|
||||||
|
describe('main/ParseArgs', () => {
|
||||||
|
it('should remove arguments following a deeplink', () => {
|
||||||
|
const args = parse(['mattermost', '--disableDevMode', 'mattermost://server-1.com', '--version']);
|
||||||
|
expect(args.disableDevMode).toBe(true);
|
||||||
|
expect(args.version).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
@@ -1,7 +1,7 @@
|
|||||||
// 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 UserActivityMonitor from './UserActivityMonitor';
|
import {UserActivityMonitor} from './UserActivityMonitor';
|
||||||
|
|
||||||
describe('UserActivityMonitor', () => {
|
describe('UserActivityMonitor', () => {
|
||||||
describe('updateIdleTime', () => {
|
describe('updateIdleTime', () => {
|
||||||
|
@@ -3,15 +3,13 @@
|
|||||||
|
|
||||||
import {EventEmitter} from 'events';
|
import {EventEmitter} from 'events';
|
||||||
|
|
||||||
import electron from 'electron';
|
import {app, powerMonitor} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
const {app} = electron;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Monitors system idle time, listens for system events and fires status updates as needed
|
* Monitors system idle time, listens for system events and fires status updates as needed
|
||||||
*/
|
*/
|
||||||
export default class UserActivityMonitor extends EventEmitter {
|
export class UserActivityMonitor extends EventEmitter {
|
||||||
isActive: boolean;
|
isActive: boolean;
|
||||||
idleTime: number;
|
idleTime: number;
|
||||||
lastSetActive?: number;
|
lastSetActive?: number;
|
||||||
@@ -70,7 +68,7 @@ export default class UserActivityMonitor extends EventEmitter {
|
|||||||
// Node typings don't map Timeout to number, but then clearInterval requires a number?
|
// Node typings don't map Timeout to number, but then clearInterval requires a number?
|
||||||
this.systemIdleTimeIntervalID = setInterval(() => {
|
this.systemIdleTimeIntervalID = setInterval(() => {
|
||||||
try {
|
try {
|
||||||
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
|
this.updateIdleTime(powerMonitor.getSystemIdleTime());
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
log.error('Error getting system idle time:', err);
|
log.error('Error getting system idle time:', err);
|
||||||
}
|
}
|
||||||
@@ -138,3 +136,6 @@ export default class UserActivityMonitor extends EventEmitter {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const userActivityMonitor = new UserActivityMonitor();
|
||||||
|
export default userActivityMonitor;
|
||||||
|
@@ -5,17 +5,14 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import path from 'path';
|
import {dialog, shell} from 'electron';
|
||||||
|
|
||||||
import {app, dialog, shell} from 'electron';
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {protocols} from '../../electron-builder.json';
|
import {protocols} from '../../electron-builder.json';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
import WindowManager from './windows/windowManager';
|
import WindowManager from './windows/windowManager';
|
||||||
|
import {allowedProtocolFile} from './constants';
|
||||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
|
||||||
|
|
||||||
export class AllowProtocolDialog {
|
export class AllowProtocolDialog {
|
||||||
allowedProtocols: string[];
|
allowedProtocols: string[];
|
||||||
|
156
src/main/app/app.test.js
Normal file
156
src/main/app/app.test.js
Normal file
@@ -0,0 +1,156 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app, dialog} from 'electron';
|
||||||
|
|
||||||
|
import CertificateStore from 'main/certificateStore';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {handleAppWillFinishLaunching, handleAppCertificateError, certificateErrorCallbacks} from 'main/app/app';
|
||||||
|
import {getDeeplinkingURL, openDeepLink} from 'main/app/utils';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
on: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
isReady: jest.fn(),
|
||||||
|
},
|
||||||
|
dialog: {
|
||||||
|
showMessageBox: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/app/utils', () => ({
|
||||||
|
getDeeplinkingURL: jest.fn(),
|
||||||
|
openDeepLink: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/certificateStore', () => ({
|
||||||
|
isExplicitlyUntrusted: jest.fn(),
|
||||||
|
isTrusted: jest.fn(),
|
||||||
|
isExisting: jest.fn(),
|
||||||
|
add: jest.fn(),
|
||||||
|
save: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/tray/tray', () => ({}));
|
||||||
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
|
getMainWindow: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/app/app', () => {
|
||||||
|
describe('handleAppWillFinishLaunching', () => {
|
||||||
|
const deepLinkURL = 'mattermost://server-1.com';
|
||||||
|
const testURL = 'http://server-1.com';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
app.on.mockImplementation((event, cb) => {
|
||||||
|
if (event === 'open-url') {
|
||||||
|
cb({preventDefault: jest.fn()}, deepLinkURL);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
getDeeplinkingURL.mockReturnValue(testURL);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open deep link if app is ready', () => {
|
||||||
|
app.isReady.mockReturnValue(true);
|
||||||
|
handleAppWillFinishLaunching();
|
||||||
|
expect(openDeepLink).toHaveBeenCalledWith(testURL);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should wait until app is ready to open deep link', () => {
|
||||||
|
let callback;
|
||||||
|
app.once.mockImplementation((event, cb) => {
|
||||||
|
if (event === 'ready') {
|
||||||
|
callback = cb;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
app.isReady.mockReturnValue(false);
|
||||||
|
handleAppWillFinishLaunching();
|
||||||
|
expect(openDeepLink).not.toHaveBeenCalled();
|
||||||
|
callback({preventDefault: jest.fn()}, deepLinkURL);
|
||||||
|
expect(openDeepLink).toHaveBeenCalledWith(testURL);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleAppCertificateError', () => {
|
||||||
|
const testURL = 'http://server-1.com';
|
||||||
|
const callback = jest.fn();
|
||||||
|
const event = {preventDefault: jest.fn()};
|
||||||
|
const webContents = {loadURL: jest.fn()};
|
||||||
|
const mainWindow = {};
|
||||||
|
const promise = Promise.resolve({});
|
||||||
|
const certificate = {};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
WindowManager.getMainWindow.mockReturnValue(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
certificateErrorCallbacks.clear();
|
||||||
|
dialog.showMessageBox.mockReturnValue(promise);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not trust if explicitly untrusted by CertificateStore', () => {
|
||||||
|
CertificateStore.isExplicitlyUntrusted.mockReturnValue(true);
|
||||||
|
handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trust if trusted by CertificateStore', () => {
|
||||||
|
CertificateStore.isExplicitlyUntrusted.mockReturnValue(false);
|
||||||
|
CertificateStore.isTrusted.mockReturnValue(true);
|
||||||
|
handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(event.preventDefault).toHaveBeenCalled();
|
||||||
|
expect(callback).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show additional dialogs if certificate error has already been logged', () => {
|
||||||
|
certificateErrorCallbacks.set('http://server-1.com:error-1', callback);
|
||||||
|
handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(dialog.showMessageBox).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set callback if one is not already set', () => {
|
||||||
|
handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(certificateErrorCallbacks.has('http://server-1.com:error-1')).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove callback and not add certificate if user selects Cancel', async () => {
|
||||||
|
dialog.showMessageBox.mockResolvedValue({response: 1});
|
||||||
|
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
expect(certificateErrorCallbacks.has('http://server-1.com:error-1')).toBe(false);
|
||||||
|
expect(CertificateStore.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove callback and add certificate if user selects More Details and Trust', async () => {
|
||||||
|
dialog.showMessageBox.mockResolvedValue({response: 0});
|
||||||
|
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(callback).toHaveBeenCalledWith(true);
|
||||||
|
expect(certificateErrorCallbacks.has('http://server-1.com:error-1')).toBe(false);
|
||||||
|
expect(CertificateStore.add).toHaveBeenCalledWith('http://server-1.com', certificate);
|
||||||
|
expect(CertificateStore.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should explicitly untrust if user selects More Details and then cancel with the checkbox checked', async () => {
|
||||||
|
dialog.showMessageBox.mockResolvedValueOnce({response: 0}).mockResolvedValueOnce({response: 1, checkboxChecked: true});
|
||||||
|
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
expect(certificateErrorCallbacks.has('http://server-1.com:error-1')).toBe(false);
|
||||||
|
expect(CertificateStore.add).toHaveBeenCalledWith('http://server-1.com', certificate, true);
|
||||||
|
expect(CertificateStore.save).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
145
src/main/app/app.ts
Normal file
145
src/main/app/app.ts
Normal file
@@ -0,0 +1,145 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app, BrowserWindow, Event, dialog, WebContents, Certificate} from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
|
import CertificateStore from 'main/certificateStore';
|
||||||
|
import {destroyTray} from 'main/tray/tray';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {getDeeplinkingURL, openDeepLink, resizeScreen} from './utils';
|
||||||
|
|
||||||
|
export const certificateErrorCallbacks = new Map();
|
||||||
|
|
||||||
|
//
|
||||||
|
// app event handlers
|
||||||
|
//
|
||||||
|
|
||||||
|
// activate first app instance, subsequent instances will quit themselves
|
||||||
|
export function handleAppSecondInstance(event: Event, argv: string[]) {
|
||||||
|
// Protocol handler for win32
|
||||||
|
// argv: An array of the second instance’s (command line / deep linked) arguments
|
||||||
|
const deeplinkingUrl = getDeeplinkingURL(argv);
|
||||||
|
WindowManager.showMainWindow(deeplinkingUrl);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppWindowAllClosed() {
|
||||||
|
// On OS X it is common for applications and their menu bar
|
||||||
|
// to stay active until the user quits explicitly with Cmd + Q
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppBrowserWindowCreated(event: Event, newWindow: BrowserWindow) {
|
||||||
|
// Screen cannot be required before app is ready
|
||||||
|
resizeScreen(newWindow);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppWillFinishLaunching() {
|
||||||
|
// Protocol handler for osx
|
||||||
|
app.on('open-url', (event, url) => {
|
||||||
|
log.info(`Handling deeplinking url: ${url}`);
|
||||||
|
event.preventDefault();
|
||||||
|
const deeplinkingUrl = getDeeplinkingURL([url]);
|
||||||
|
if (deeplinkingUrl) {
|
||||||
|
if (app.isReady() && deeplinkingUrl) {
|
||||||
|
openDeepLink(deeplinkingUrl);
|
||||||
|
} else {
|
||||||
|
app.once('ready', () => openDeepLink(deeplinkingUrl));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppBeforeQuit() {
|
||||||
|
// Make sure tray icon gets removed if the user exits via CTRL-Q
|
||||||
|
destroyTray();
|
||||||
|
global.willAppQuit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleAppCertificateError(event: Event, webContents: WebContents, url: string, error: string, certificate: Certificate, callback: (isTrusted: boolean) => void) {
|
||||||
|
const parsedURL = urlUtils.parseURL(url);
|
||||||
|
if (!parsedURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const origin = parsedURL.origin;
|
||||||
|
if (CertificateStore.isExplicitlyUntrusted(origin)) {
|
||||||
|
event.preventDefault();
|
||||||
|
log.warn(`Ignoring previously untrusted certificate for ${origin}`);
|
||||||
|
callback(false);
|
||||||
|
} else if (CertificateStore.isTrusted(origin, certificate)) {
|
||||||
|
event.preventDefault();
|
||||||
|
callback(true);
|
||||||
|
} else {
|
||||||
|
// update the callback
|
||||||
|
const errorID = `${origin}:${error}`;
|
||||||
|
|
||||||
|
// if we are already showing that error, don't add more dialogs
|
||||||
|
if (certificateErrorCallbacks.has(errorID)) {
|
||||||
|
log.warn(`Ignoring already shown dialog for ${errorID}`);
|
||||||
|
certificateErrorCallbacks.set(errorID, callback);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const extraDetail = CertificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
|
||||||
|
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
|
||||||
|
|
||||||
|
certificateErrorCallbacks.set(errorID, callback);
|
||||||
|
|
||||||
|
// TODO: should we move this to window manager or provide a handler for dialogs?
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let result = await dialog.showMessageBox(mainWindow, {
|
||||||
|
title: 'Certificate Error',
|
||||||
|
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
|
||||||
|
type: 'error',
|
||||||
|
detail,
|
||||||
|
buttons: ['More Details', 'Cancel Connection'],
|
||||||
|
cancelId: 1,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.response === 0) {
|
||||||
|
result = await dialog.showMessageBox(mainWindow, {
|
||||||
|
title: 'Certificate Not Trusted',
|
||||||
|
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
|
||||||
|
detail: extraDetail,
|
||||||
|
type: 'error',
|
||||||
|
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
|
||||||
|
cancelId: 1,
|
||||||
|
checkboxChecked: false,
|
||||||
|
checkboxLabel: "Don't ask again",
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
result = {response: result.response, checkboxChecked: false};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.response === 0) {
|
||||||
|
CertificateStore.add(origin, certificate);
|
||||||
|
CertificateStore.save();
|
||||||
|
certificateErrorCallbacks.get(errorID)(true);
|
||||||
|
webContents.loadURL(url);
|
||||||
|
} else {
|
||||||
|
if (result.checkboxChecked) {
|
||||||
|
CertificateStore.add(origin, certificate, true);
|
||||||
|
CertificateStore.save();
|
||||||
|
}
|
||||||
|
certificateErrorCallbacks.get(errorID)(false);
|
||||||
|
}
|
||||||
|
} catch (dialogError) {
|
||||||
|
log.error(`There was an error with the Certificate Error dialog: ${dialogError}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
certificateErrorCallbacks.delete(errorID);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppGPUProcessCrashed(event: Event, killed: boolean) {
|
||||||
|
log.error(`The GPU process has crashed (killed = ${killed})`);
|
||||||
|
}
|
108
src/main/app/config.test.js
Normal file
108
src/main/app/config.test.js
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app} from 'electron';
|
||||||
|
|
||||||
|
import {RELOAD_CONFIGURATION} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
|
|
||||||
|
import {handleConfigUpdate} from 'main/app/config';
|
||||||
|
import {handleNewServerModal} from 'main/app/intercom';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import AutoLauncher from 'main/AutoLauncher';
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
isReady: jest.fn(),
|
||||||
|
setPath: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
emit: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/app/utils', () => ({
|
||||||
|
handleUpdateMenuEvent: jest.fn(),
|
||||||
|
updateSpellCheckerLocales: jest.fn(),
|
||||||
|
updateServerInfos: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/app/intercom', () => ({
|
||||||
|
handleNewServerModal: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/AutoLauncher', () => ({
|
||||||
|
enable: jest.fn(),
|
||||||
|
disable: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/badge', () => ({
|
||||||
|
setUnreadBadgeSetting: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/tray/tray', () => ({}));
|
||||||
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
|
handleUpdateConfig: jest.fn(),
|
||||||
|
sendToRenderer: jest.fn(),
|
||||||
|
initializeCurrentServerName: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/app/config', () => {
|
||||||
|
describe('handleConfigUpdate', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
AutoLauncher.enable.mockResolvedValue({});
|
||||||
|
AutoLauncher.disable.mockResolvedValue({});
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.registryConfigData;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reload renderer config only when app is ready', () => {
|
||||||
|
handleConfigUpdate({});
|
||||||
|
expect(WindowManager.sendToRenderer).not.toBeCalled();
|
||||||
|
|
||||||
|
app.isReady.mockReturnValue(true);
|
||||||
|
handleConfigUpdate({});
|
||||||
|
expect(WindowManager.sendToRenderer).toBeCalledWith(RELOAD_CONFIGURATION);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set download path if applicable', () => {
|
||||||
|
handleConfigUpdate({downloadLocation: '/a/download/location'});
|
||||||
|
expect(app.setPath).toHaveBeenCalledWith('downloads', '/a/download/location');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should enable/disable auto launch on windows/linux', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
handleConfigUpdate({});
|
||||||
|
expect(AutoLauncher.disable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
handleConfigUpdate({autostart: true});
|
||||||
|
expect(AutoLauncher.enable).toHaveBeenCalled();
|
||||||
|
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should recheck teams after config update if registry data is pulled in', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'win32',
|
||||||
|
});
|
||||||
|
Config.registryConfigData = {};
|
||||||
|
|
||||||
|
handleConfigUpdate({teams: []});
|
||||||
|
expect(handleNewServerModal).toHaveBeenCalled();
|
||||||
|
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
75
src/main/app/config.ts
Normal file
75
src/main/app/config.ts
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app, ipcMain} from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import {CombinedConfig} from 'types/config';
|
||||||
|
|
||||||
|
import {DARK_MODE_CHANGE, EMIT_CONFIGURATION, RELOAD_CONFIGURATION} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
|
|
||||||
|
import AutoLauncher from 'main/AutoLauncher';
|
||||||
|
import {setUnreadBadgeSetting} from 'main/badge';
|
||||||
|
import {refreshTrayImages} from 'main/tray/tray';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {handleNewServerModal} from './intercom';
|
||||||
|
import {handleUpdateMenuEvent, updateServerInfos, updateSpellCheckerLocales} from './utils';
|
||||||
|
|
||||||
|
let didCheckForAddServerModal = false;
|
||||||
|
|
||||||
|
//
|
||||||
|
// config event handlers
|
||||||
|
//
|
||||||
|
|
||||||
|
export function handleConfigUpdate(newConfig: CombinedConfig) {
|
||||||
|
if (!newConfig) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
WindowManager.handleUpdateConfig();
|
||||||
|
if (app.isReady()) {
|
||||||
|
WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge);
|
||||||
|
updateSpellCheckerLocales();
|
||||||
|
|
||||||
|
if (newConfig.downloadLocation) {
|
||||||
|
try {
|
||||||
|
app.setPath('downloads', newConfig.downloadLocation);
|
||||||
|
} catch (e) {
|
||||||
|
log.error(`There was a problem trying to set the default download path: ${e}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||||
|
const autoStartTask = newConfig.autostart ? AutoLauncher.enable() : AutoLauncher.disable();
|
||||||
|
autoStartTask.then(() => {
|
||||||
|
log.info('config.autostart has been configured:', newConfig.autostart);
|
||||||
|
}).catch((err) => {
|
||||||
|
log.error('error:', err);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.platform === 'win32' && !didCheckForAddServerModal && typeof Config.registryConfigData !== 'undefined') {
|
||||||
|
didCheckForAddServerModal = true;
|
||||||
|
updateServerInfos(newConfig.teams);
|
||||||
|
WindowManager.initializeCurrentServerName();
|
||||||
|
if (newConfig.teams.length === 0) {
|
||||||
|
handleNewServerModal();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
handleUpdateMenuEvent();
|
||||||
|
ipcMain.emit(EMIT_CONFIGURATION, true, newConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleDarkModeChange(darkMode: boolean) {
|
||||||
|
refreshTrayImages(Config.trayIconTheme);
|
||||||
|
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
|
||||||
|
WindowManager.updateLoadingScreenDarkMode(darkMode);
|
||||||
|
|
||||||
|
ipcMain.emit(EMIT_CONFIGURATION, true, Config.data);
|
||||||
|
}
|
17
src/main/app/index.ts
Normal file
17
src/main/app/index.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* istanbul ignore file */
|
||||||
|
|
||||||
|
import {initialize} from './initialize';
|
||||||
|
|
||||||
|
if (process.env.NODE_ENV !== 'production' && module.hot) {
|
||||||
|
module.hot.accept();
|
||||||
|
}
|
||||||
|
|
||||||
|
// attempt to initialize the application
|
||||||
|
try {
|
||||||
|
initialize();
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`App initialization failed: ${error.toString()}`);
|
||||||
|
}
|
287
src/main/app/initialize.test.js
Normal file
287
src/main/app/initialize.test.js
Normal file
@@ -0,0 +1,287 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {app, session} from 'electron';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
|
import parseArgs from 'main/ParseArgs';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {initialize} from './initialize';
|
||||||
|
import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils';
|
||||||
|
|
||||||
|
jest.mock('fs', () => ({
|
||||||
|
unlinkSync: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('path', () => {
|
||||||
|
const original = jest.requireActual('path');
|
||||||
|
return {
|
||||||
|
...original,
|
||||||
|
resolve: jest.fn(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
on: jest.fn(),
|
||||||
|
exit: jest.fn(),
|
||||||
|
getPath: jest.fn(),
|
||||||
|
setPath: jest.fn(),
|
||||||
|
disableHardwareAcceleration: jest.fn(),
|
||||||
|
enableSandbox: jest.fn(),
|
||||||
|
requestSingleInstanceLock: jest.fn(),
|
||||||
|
setAsDefaultProtocolClient: jest.fn(),
|
||||||
|
setAppUserModelId: jest.fn(),
|
||||||
|
getVersion: jest.fn(),
|
||||||
|
whenReady: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
handle: jest.fn(),
|
||||||
|
emit: jest.fn(),
|
||||||
|
},
|
||||||
|
session: {
|
||||||
|
defaultSession: {
|
||||||
|
setSpellCheckerDictionaryDownloadURL: jest.fn(),
|
||||||
|
setPermissionRequestHandler: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron-devtools-installer', () => {
|
||||||
|
return () => ({
|
||||||
|
REACT_DEVELOPER_TOOLS: 'react-developer-tools',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const isDev = false;
|
||||||
|
jest.mock('electron-is-dev', () => isDev);
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
warn: jest.fn(),
|
||||||
|
error: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('../../../electron-builder.json', () => ([
|
||||||
|
{
|
||||||
|
name: 'Mattermost',
|
||||||
|
schemes: [
|
||||||
|
'mattermost',
|
||||||
|
],
|
||||||
|
},
|
||||||
|
]));
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({
|
||||||
|
once: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
init: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/utils/url', () => ({
|
||||||
|
isTrustedURL: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/allowProtocolDialog', () => ({
|
||||||
|
init: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/app/app', () => ({}));
|
||||||
|
jest.mock('main/app/config', () => ({
|
||||||
|
handleConfigUpdate: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/app/intercom', () => ({}));
|
||||||
|
jest.mock('main/app/utils', () => ({
|
||||||
|
clearAppCache: jest.fn(),
|
||||||
|
getDeeplinkingURL: jest.fn(),
|
||||||
|
handleUpdateMenuEvent: jest.fn(),
|
||||||
|
shouldShowTrayIcon: jest.fn(),
|
||||||
|
updateServerInfos: jest.fn(),
|
||||||
|
updateSpellCheckerLocales: jest.fn(),
|
||||||
|
wasUpdated: jest.fn(),
|
||||||
|
initCookieManager: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/AppVersionManager', () => ({}));
|
||||||
|
jest.mock('main/authManager', () => ({}));
|
||||||
|
jest.mock('main/AutoLauncher', () => ({
|
||||||
|
upgradeAutoLaunch: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/badge', () => ({
|
||||||
|
setupBadge: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/certificateManager', () => ({}));
|
||||||
|
jest.mock('main/CriticalErrorHandler', () => ({
|
||||||
|
processUncaughtExceptionHandler: jest.fn(),
|
||||||
|
setMainWindow: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/notifications', () => ({
|
||||||
|
displayDownloadCompleted: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/ParseArgs', () => jest.fn());
|
||||||
|
jest.mock('main/tray/tray', () => ({
|
||||||
|
refreshTrayImages: jest.fn(),
|
||||||
|
setupTray: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/trustedOrigins', () => ({
|
||||||
|
load: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/UserActivityMonitor', () => ({
|
||||||
|
on: jest.fn(),
|
||||||
|
startMonitoring: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
|
getMainWindow: jest.fn(),
|
||||||
|
showMainWindow: jest.fn(),
|
||||||
|
sendToMattermostViews: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/app/initialize', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
parseArgs.mockReturnValue({});
|
||||||
|
Config.once.mockImplementation((event, cb) => {
|
||||||
|
if (event === 'update') {
|
||||||
|
cb();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Config.data = {};
|
||||||
|
Config.teams = [];
|
||||||
|
app.whenReady.mockResolvedValue();
|
||||||
|
app.requestSingleInstanceLock.mockReturnValue(true);
|
||||||
|
app.getPath.mockImplementation((p) => `/basedir/${p}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
delete Config.data;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initialize without errors', async () => {
|
||||||
|
await initialize();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeArgs', () => {
|
||||||
|
it('should set datadir when specified', async () => {
|
||||||
|
path.resolve.mockImplementation((p) => `/basedir${p}`);
|
||||||
|
parseArgs.mockReturnValue({
|
||||||
|
dataDir: '/some/dir',
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(app.setPath).toHaveBeenCalledWith('userData', '/basedir/some/dir');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show version and exit when specified', async () => {
|
||||||
|
jest.spyOn(process.stdout, 'write').mockImplementation(() => {});
|
||||||
|
const exitSpy = jest.spyOn(process, 'exit').mockImplementation(() => {});
|
||||||
|
parseArgs.mockReturnValue({
|
||||||
|
version: true,
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(exitSpy).toHaveBeenCalledWith(0);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeConfig', () => {
|
||||||
|
it('should disable hardware acceleration when specified', async () => {
|
||||||
|
Config.enableHardwareAcceleration = false;
|
||||||
|
await initialize();
|
||||||
|
expect(app.disableHardwareAcceleration).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeBeforeAppReady', () => {
|
||||||
|
it('should exit the app when single instance lock fails', () => {
|
||||||
|
app.requestSingleInstanceLock.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('initializeAfterAppReady', () => {
|
||||||
|
it('should set spell checker URL if applicable', async () => {
|
||||||
|
Config.spellCheckerURL = 'http://server-1.com';
|
||||||
|
await initialize();
|
||||||
|
expect(session.defaultSession.setSpellCheckerDictionaryDownloadURL).toHaveBeenCalledWith('http://server-1.com/');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear app cache if last version opened was older', async () => {
|
||||||
|
wasUpdated.mockReturnValue(true);
|
||||||
|
await initialize();
|
||||||
|
expect(clearAppCache).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform deeplink on win32', async () => {
|
||||||
|
getDeeplinkingURL.mockReturnValue('mattermost://server-1.com');
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'argv', {
|
||||||
|
value: ['mattermost', 'mattermost://server-1.com'],
|
||||||
|
});
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'win32',
|
||||||
|
});
|
||||||
|
|
||||||
|
await initialize();
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(WindowManager.showMainWindow).toHaveBeenCalledWith('mattermost://server-1.com');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should setup save dialog correctly', async () => {
|
||||||
|
const item = {
|
||||||
|
getFilename: () => 'filename.txt',
|
||||||
|
on: jest.fn(),
|
||||||
|
setSaveDialogOptions: jest.fn(),
|
||||||
|
};
|
||||||
|
Config.downloadLocation = '/some/dir';
|
||||||
|
path.resolve.mockImplementation((base, p) => `${base}/${p}`);
|
||||||
|
session.defaultSession.on.mockImplementation((event, cb) => {
|
||||||
|
if (event === 'will-download') {
|
||||||
|
cb(null, item, {id: 0});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await initialize();
|
||||||
|
expect(item.setSaveDialogOptions).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
|
title: 'filename.txt',
|
||||||
|
defaultPath: '/some/dir/filename.txt',
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should allow permission requests for supported types from trusted URLs', async () => {
|
||||||
|
let callback = jest.fn();
|
||||||
|
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
||||||
|
cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback);
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
|
callback = jest.fn();
|
||||||
|
WindowManager.getMainWindow.mockReturnValue({webContents: {id: 1}});
|
||||||
|
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
||||||
|
cb({id: 1, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(callback).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
|
urlUtils.isTrustedURL.mockImplementation((url) => url === 'http://server-1.com');
|
||||||
|
|
||||||
|
callback = jest.fn();
|
||||||
|
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
||||||
|
cb({id: 2, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(callback).toHaveBeenCalledWith(true);
|
||||||
|
|
||||||
|
callback = jest.fn();
|
||||||
|
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
||||||
|
cb({id: 2, getURL: () => 'http://server-2.com'}, 'openExternal', callback);
|
||||||
|
});
|
||||||
|
await initialize();
|
||||||
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
381
src/main/app/initialize.ts
Normal file
381
src/main/app/initialize.ts
Normal file
@@ -0,0 +1,381 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {app, ipcMain, session} from 'electron';
|
||||||
|
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
||||||
|
import isDev from 'electron-is-dev';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import {
|
||||||
|
SWITCH_SERVER,
|
||||||
|
FOCUS_BROWSERVIEW,
|
||||||
|
QUIT,
|
||||||
|
DOUBLE_CLICK_ON_WINDOW,
|
||||||
|
SHOW_NEW_SERVER_MODAL,
|
||||||
|
WINDOW_CLOSE,
|
||||||
|
WINDOW_MAXIMIZE,
|
||||||
|
WINDOW_MINIMIZE,
|
||||||
|
WINDOW_RESTORE,
|
||||||
|
NOTIFY_MENTION,
|
||||||
|
GET_DOWNLOAD_LOCATION,
|
||||||
|
SHOW_SETTINGS_WINDOW,
|
||||||
|
RELOAD_CONFIGURATION,
|
||||||
|
SWITCH_TAB,
|
||||||
|
CLOSE_TAB,
|
||||||
|
OPEN_TAB,
|
||||||
|
SHOW_EDIT_SERVER_MODAL,
|
||||||
|
SHOW_REMOVE_SERVER_MODAL,
|
||||||
|
UPDATE_SHORTCUT_MENU,
|
||||||
|
UPDATE_LAST_ACTIVE,
|
||||||
|
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
|
||||||
|
USER_ACTIVITY_UPDATE,
|
||||||
|
} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
|
import AllowProtocolDialog from 'main/allowProtocolDialog';
|
||||||
|
import AppVersionManager from 'main/AppVersionManager';
|
||||||
|
import AuthManager from 'main/authManager';
|
||||||
|
import AutoLauncher from 'main/AutoLauncher';
|
||||||
|
import {setupBadge} from 'main/badge';
|
||||||
|
import CertificateManager from 'main/certificateManager';
|
||||||
|
import {updatePaths} from 'main/constants';
|
||||||
|
import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
||||||
|
import {displayDownloadCompleted} from 'main/notifications';
|
||||||
|
import parseArgs from 'main/ParseArgs';
|
||||||
|
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||||
|
import {refreshTrayImages, setupTray} from 'main/tray/tray';
|
||||||
|
import UserActivityMonitor from 'main/UserActivityMonitor';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {protocols} from '../../../electron-builder.json';
|
||||||
|
|
||||||
|
import {
|
||||||
|
handleAppBeforeQuit,
|
||||||
|
handleAppBrowserWindowCreated,
|
||||||
|
handleAppCertificateError,
|
||||||
|
handleAppGPUProcessCrashed,
|
||||||
|
handleAppSecondInstance,
|
||||||
|
handleAppWillFinishLaunching,
|
||||||
|
handleAppWindowAllClosed,
|
||||||
|
} from './app';
|
||||||
|
import {handleConfigUpdate, handleDarkModeChange} from './config';
|
||||||
|
import {
|
||||||
|
handleAppVersion,
|
||||||
|
handleCloseTab,
|
||||||
|
handleEditServerModal,
|
||||||
|
handleMentionNotification,
|
||||||
|
handleNewServerModal,
|
||||||
|
handleOpenAppMenu,
|
||||||
|
handleOpenTab,
|
||||||
|
handleQuit,
|
||||||
|
handleReloadConfig,
|
||||||
|
handleRemoveServerModal,
|
||||||
|
handleSelectDownload,
|
||||||
|
handleSwitchServer,
|
||||||
|
handleSwitchTab,
|
||||||
|
handleUpdateLastActive,
|
||||||
|
} from './intercom';
|
||||||
|
import {
|
||||||
|
clearAppCache,
|
||||||
|
getDeeplinkingURL,
|
||||||
|
handleUpdateMenuEvent,
|
||||||
|
shouldShowTrayIcon,
|
||||||
|
updateServerInfos,
|
||||||
|
updateSpellCheckerLocales,
|
||||||
|
wasUpdated,
|
||||||
|
initCookieManager,
|
||||||
|
} from './utils';
|
||||||
|
|
||||||
|
export const mainProtocol = protocols?.[0]?.schemes?.[0];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Main entry point for the application, ensures that everything initializes in the proper order
|
||||||
|
*/
|
||||||
|
export async function initialize() {
|
||||||
|
process.on('uncaughtException', CriticalErrorHandler.processUncaughtExceptionHandler.bind(CriticalErrorHandler));
|
||||||
|
global.willAppQuit = false;
|
||||||
|
|
||||||
|
// initialization that can run before the app is ready
|
||||||
|
initializeArgs();
|
||||||
|
await initializeConfig();
|
||||||
|
initializeAppEventListeners();
|
||||||
|
initializeBeforeAppReady();
|
||||||
|
|
||||||
|
// wait for registry config data to load and app ready event
|
||||||
|
await Promise.all([
|
||||||
|
app.whenReady(),
|
||||||
|
]);
|
||||||
|
|
||||||
|
// no need to continue initializing if app is quitting
|
||||||
|
if (global.willAppQuit) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// initialization that should run once the app is ready
|
||||||
|
initializeInterCommunicationEventListeners();
|
||||||
|
initializeAfterAppReady();
|
||||||
|
}
|
||||||
|
|
||||||
|
//
|
||||||
|
// initialization sub functions
|
||||||
|
//
|
||||||
|
|
||||||
|
function initializeArgs() {
|
||||||
|
global.args = parseArgs(process.argv.slice(1));
|
||||||
|
|
||||||
|
// output the application version via cli when requested (-v or --version)
|
||||||
|
if (global.args.version) {
|
||||||
|
process.stdout.write(`v.${app.getVersion()}\n`);
|
||||||
|
process.exit(0); // eslint-disable-line no-process-exit
|
||||||
|
}
|
||||||
|
|
||||||
|
global.isDev = isDev && !global.args.disableDevMode; // this doesn't seem to be right and isn't used as the single source of truth
|
||||||
|
|
||||||
|
if (global.args.dataDir) {
|
||||||
|
app.setPath('userData', path.resolve(global.args.dataDir));
|
||||||
|
updatePaths(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function initializeConfig() {
|
||||||
|
return new Promise<void>((resolve) => {
|
||||||
|
Config.once('update', (configData) => {
|
||||||
|
Config.on('update', handleConfigUpdate);
|
||||||
|
Config.on('darkModeChange', handleDarkModeChange);
|
||||||
|
Config.on('error', (error) => {
|
||||||
|
log.error(error);
|
||||||
|
});
|
||||||
|
handleConfigUpdate(configData);
|
||||||
|
|
||||||
|
// can only call this before the app is ready
|
||||||
|
if (Config.enableHardwareAcceleration === false) {
|
||||||
|
app.disableHardwareAcceleration();
|
||||||
|
}
|
||||||
|
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
Config.init();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAppEventListeners() {
|
||||||
|
app.on('second-instance', handleAppSecondInstance);
|
||||||
|
app.on('window-all-closed', handleAppWindowAllClosed);
|
||||||
|
app.on('browser-window-created', handleAppBrowserWindowCreated);
|
||||||
|
app.on('activate', () => WindowManager.showMainWindow());
|
||||||
|
app.on('before-quit', handleAppBeforeQuit);
|
||||||
|
app.on('certificate-error', handleAppCertificateError);
|
||||||
|
app.on('select-client-certificate', CertificateManager.handleSelectCertificate);
|
||||||
|
app.on('gpu-process-crashed', handleAppGPUProcessCrashed);
|
||||||
|
app.on('login', AuthManager.handleAppLogin);
|
||||||
|
app.on('will-finish-launching', handleAppWillFinishLaunching);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeBeforeAppReady() {
|
||||||
|
if (!Config.data) {
|
||||||
|
log.error('No config loaded');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (process.env.NODE_ENV !== 'test') {
|
||||||
|
app.enableSandbox();
|
||||||
|
}
|
||||||
|
TrustedOriginsStore.load();
|
||||||
|
|
||||||
|
// prevent using a different working directory, which happens on windows running after installation.
|
||||||
|
const expectedPath = path.dirname(process.execPath);
|
||||||
|
if (process.cwd() !== expectedPath && !isDev) {
|
||||||
|
log.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
|
||||||
|
process.chdir(expectedPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshTrayImages(Config.trayIconTheme);
|
||||||
|
|
||||||
|
// If there is already an instance, quit this one
|
||||||
|
const gotTheLock = app.requestSingleInstanceLock();
|
||||||
|
if (!gotTheLock) {
|
||||||
|
app.exit();
|
||||||
|
global.willAppQuit = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
AllowProtocolDialog.init();
|
||||||
|
|
||||||
|
if (isDev && process.env.NODE_ENV !== 'test') {
|
||||||
|
log.info('In development mode, deeplinking is disabled');
|
||||||
|
} else if (mainProtocol) {
|
||||||
|
app.setAsDefaultProtocolClient(mainProtocol);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeInterCommunicationEventListeners() {
|
||||||
|
ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig);
|
||||||
|
ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
|
||||||
|
ipcMain.handle('get-app-version', handleAppVersion);
|
||||||
|
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent);
|
||||||
|
ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView);
|
||||||
|
ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive);
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
ipcMain.on('open-app-menu', handleOpenAppMenu);
|
||||||
|
}
|
||||||
|
|
||||||
|
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
||||||
|
ipcMain.on(SWITCH_TAB, handleSwitchTab);
|
||||||
|
ipcMain.on(CLOSE_TAB, handleCloseTab);
|
||||||
|
ipcMain.on(OPEN_TAB, handleOpenTab);
|
||||||
|
|
||||||
|
ipcMain.on(QUIT, handleQuit);
|
||||||
|
|
||||||
|
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick);
|
||||||
|
|
||||||
|
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
|
||||||
|
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
|
||||||
|
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
|
||||||
|
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
|
||||||
|
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
|
||||||
|
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
|
||||||
|
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
|
||||||
|
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
|
||||||
|
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
|
||||||
|
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
|
||||||
|
}
|
||||||
|
|
||||||
|
function initializeAfterAppReady() {
|
||||||
|
updateServerInfos(Config.teams);
|
||||||
|
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||||
|
const defaultSession = session.defaultSession;
|
||||||
|
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
|
||||||
|
if (Config.spellCheckerURL) {
|
||||||
|
log.error(`There was an error while trying to load the dictionary definitions for ${lang} fromfully the specified url. Please review you have access to the needed files. Url used was ${Config.spellCheckerURL}`);
|
||||||
|
} else {
|
||||||
|
log.warn(`There was an error while trying to download the dictionary definitions for ${lang}, spellchecking might not work properly.`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Config.spellCheckerURL) {
|
||||||
|
const spellCheckerURL = Config.spellCheckerURL.endsWith('/') ? Config.spellCheckerURL : `${Config.spellCheckerURL}/`;
|
||||||
|
log.info(`Configuring spellchecker using download URL: ${spellCheckerURL}`);
|
||||||
|
defaultSession.setSpellCheckerDictionaryDownloadURL(spellCheckerURL);
|
||||||
|
|
||||||
|
defaultSession.on('spellcheck-dictionary-download-success', (event, lang) => {
|
||||||
|
log.info(`Dictionary definitions downloaded successfully for ${lang}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
updateSpellCheckerLocales();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wasUpdated(AppVersionManager.lastAppVersion)) {
|
||||||
|
clearAppCache();
|
||||||
|
}
|
||||||
|
AppVersionManager.lastAppVersion = app.getVersion();
|
||||||
|
|
||||||
|
if (!global.isDev) {
|
||||||
|
AutoLauncher.upgradeAutoLaunch();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (global.isDev) {
|
||||||
|
installExtension(REACT_DEVELOPER_TOOLS).
|
||||||
|
then((name) => log.info(`Added Extension: ${name}`)).
|
||||||
|
catch((err) => log.error('An error occurred: ', err));
|
||||||
|
}
|
||||||
|
|
||||||
|
let deeplinkingURL;
|
||||||
|
|
||||||
|
// Protocol handler for win32
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
const args = process.argv.slice(1);
|
||||||
|
if (Array.isArray(args) && args.length > 0) {
|
||||||
|
deeplinkingURL = getDeeplinkingURL(args);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initCookieManager(defaultSession);
|
||||||
|
|
||||||
|
WindowManager.showMainWindow(deeplinkingURL);
|
||||||
|
|
||||||
|
CriticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
|
||||||
|
|
||||||
|
// listen for status updates and pass on to renderer
|
||||||
|
UserActivityMonitor.on('status', (status) => {
|
||||||
|
WindowManager.sendToMattermostViews(USER_ACTIVITY_UPDATE, status);
|
||||||
|
});
|
||||||
|
|
||||||
|
// start monitoring user activity (needs to be started after the app is ready)
|
||||||
|
UserActivityMonitor.startMonitoring();
|
||||||
|
|
||||||
|
if (shouldShowTrayIcon()) {
|
||||||
|
setupTray(Config.trayIconTheme);
|
||||||
|
}
|
||||||
|
setupBadge();
|
||||||
|
|
||||||
|
defaultSession.on('will-download', (event, item, webContents) => {
|
||||||
|
const filename = item.getFilename();
|
||||||
|
const fileElements = filename.split('.');
|
||||||
|
const filters = [];
|
||||||
|
if (fileElements.length > 1) {
|
||||||
|
filters.push({
|
||||||
|
name: 'All files',
|
||||||
|
extensions: ['*'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
item.setSaveDialogOptions({
|
||||||
|
title: filename,
|
||||||
|
defaultPath: path.resolve(Config.downloadLocation, filename),
|
||||||
|
filters,
|
||||||
|
});
|
||||||
|
|
||||||
|
item.on('done', (doneEvent, state) => {
|
||||||
|
if (state === 'completed') {
|
||||||
|
displayDownloadCompleted(filename, item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || '');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
handleUpdateMenuEvent();
|
||||||
|
|
||||||
|
ipcMain.emit('update-dict');
|
||||||
|
|
||||||
|
// supported permission types
|
||||||
|
const supportedPermissionTypes = [
|
||||||
|
'media',
|
||||||
|
'geolocation',
|
||||||
|
'notifications',
|
||||||
|
'fullscreen',
|
||||||
|
'openExternal',
|
||||||
|
];
|
||||||
|
|
||||||
|
// handle permission requests
|
||||||
|
// - approve if a supported permission type and the request comes from the renderer or one of the defined servers
|
||||||
|
defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
||||||
|
// is the requested permission type supported?
|
||||||
|
if (!supportedPermissionTypes.includes(permission)) {
|
||||||
|
callback(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is the request coming from the renderer?
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (mainWindow && webContents.id === mainWindow.webContents.id) {
|
||||||
|
callback(true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const requestingURL = webContents.getURL();
|
||||||
|
|
||||||
|
// is the requesting url trusted?
|
||||||
|
callback(urlUtils.isTrustedURL(requestingURL, Config.teams));
|
||||||
|
});
|
||||||
|
|
||||||
|
// only check for non-Windows, as with Windows we have to wait for GPO teams
|
||||||
|
if (process.platform !== 'win32' || typeof Config.registryConfigData !== 'undefined') {
|
||||||
|
if (Config.teams.length === 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
handleNewServerModal();
|
||||||
|
}, 200);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
238
src/main/app/intercom.test.js
Normal file
238
src/main/app/intercom.test.js
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||||
|
|
||||||
|
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
||||||
|
import ModalManager from 'main/views/modalManager';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {
|
||||||
|
handleOpenTab,
|
||||||
|
handleCloseTab,
|
||||||
|
handleNewServerModal,
|
||||||
|
handleEditServerModal,
|
||||||
|
handleRemoveServerModal,
|
||||||
|
} from './intercom';
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({
|
||||||
|
set: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('common/tabs/TabView', () => ({
|
||||||
|
getDefaultTeamWithTabsFromTeam: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/notifications', () => ({}));
|
||||||
|
jest.mock('main/utils', () => ({
|
||||||
|
getLocalPreload: jest.fn(),
|
||||||
|
getLocalURLString: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/views/modalManager', () => ({
|
||||||
|
addModal: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
|
getMainWindow: jest.fn(),
|
||||||
|
switchServer: jest.fn(),
|
||||||
|
switchTab: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('./app', () => ({}));
|
||||||
|
jest.mock('./utils', () => ({
|
||||||
|
updateServerInfos: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: 'tab-1',
|
||||||
|
order: 0,
|
||||||
|
isOpen: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tab-2',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'tab-3',
|
||||||
|
order: 1,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const teams = [
|
||||||
|
{
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
describe('main/app/intercom', () => {
|
||||||
|
describe('handleCloseTab', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
Config.teams = JSON.parse(JSON.stringify(teams));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should close the specified tab and switch to the next open tab', () => {
|
||||||
|
handleCloseTab(null, 'server-1', 'tab-3');
|
||||||
|
expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-2');
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-3').isOpen).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleOpenTab', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
Config.teams = JSON.parse(JSON.stringify(teams));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open the specified tab', () => {
|
||||||
|
handleOpenTab(null, 'server-1', 'tab-1');
|
||||||
|
expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-1');
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-1').isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleNewServerModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
|
WindowManager.getMainWindow.mockReturnValue({});
|
||||||
|
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
Config.teams = JSON.parse(JSON.stringify(teams));
|
||||||
|
|
||||||
|
getDefaultTeamWithTabsFromTeam.mockImplementation((team) => ({
|
||||||
|
...team,
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add new team to the config', async () => {
|
||||||
|
const promise = Promise.resolve({
|
||||||
|
name: 'new-team',
|
||||||
|
url: 'http://new-team.com',
|
||||||
|
});
|
||||||
|
ModalManager.addModal.mockReturnValue(promise);
|
||||||
|
|
||||||
|
handleNewServerModal();
|
||||||
|
await promise;
|
||||||
|
expect(Config.teams).toContainEqual(expect.objectContaining({
|
||||||
|
name: 'new-team',
|
||||||
|
url: 'http://new-team.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
expect(WindowManager.switchServer).toBeCalledWith('new-team', true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleEditServerModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
|
WindowManager.getMainWindow.mockReturnValue({});
|
||||||
|
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
Config.teams = JSON.parse(JSON.stringify(teams));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should do nothing when the server cannot be found', () => {
|
||||||
|
handleEditServerModal(null, 'bad-server');
|
||||||
|
expect(ModalManager.addModal).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should edit the existing team', async () => {
|
||||||
|
const promise = Promise.resolve({
|
||||||
|
name: 'new-team',
|
||||||
|
url: 'http://new-team.com',
|
||||||
|
});
|
||||||
|
ModalManager.addModal.mockReturnValue(promise);
|
||||||
|
|
||||||
|
handleEditServerModal(null, 'server-1');
|
||||||
|
await promise;
|
||||||
|
expect(Config.teams).not.toContainEqual(expect.objectContaining({
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
expect(Config.teams).toContainEqual(expect.objectContaining({
|
||||||
|
name: 'new-team',
|
||||||
|
url: 'http://new-team.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('handleRemoveServerModal', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
|
WindowManager.getMainWindow.mockReturnValue({});
|
||||||
|
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
Config.teams = JSON.parse(JSON.stringify(teams));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should remove the existing team', async () => {
|
||||||
|
const promise = Promise.resolve(true);
|
||||||
|
ModalManager.addModal.mockReturnValue(promise);
|
||||||
|
|
||||||
|
handleRemoveServerModal(null, 'server-1');
|
||||||
|
await promise;
|
||||||
|
expect(Config.teams).not.toContainEqual(expect.objectContaining({
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not remove the existing team when clicking Cancel', async () => {
|
||||||
|
const promise = Promise.resolve(false);
|
||||||
|
ModalManager.addModal.mockReturnValue(promise);
|
||||||
|
|
||||||
|
expect(Config.teams).toContainEqual(expect.objectContaining({
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
|
||||||
|
handleRemoveServerModal(null, 'server-1');
|
||||||
|
await promise;
|
||||||
|
expect(Config.teams).toContainEqual(expect.objectContaining({
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
215
src/main/app/intercom.ts
Normal file
215
src/main/app/intercom.ts
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import {Team} from 'types/config';
|
||||||
|
import {MentionData} from 'types/notification';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||||
|
|
||||||
|
import {displayMention} from 'main/notifications';
|
||||||
|
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
||||||
|
import ModalManager from 'main/views/modalManager';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {handleAppBeforeQuit} from './app';
|
||||||
|
import {updateServerInfos} from './utils';
|
||||||
|
|
||||||
|
export function handleReloadConfig() {
|
||||||
|
Config.reload();
|
||||||
|
WindowManager.handleUpdateConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleAppVersion() {
|
||||||
|
return {
|
||||||
|
name: app.getName(),
|
||||||
|
version: app.getVersion(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
|
||||||
|
log.error(`Exiting App. Reason: ${reason}`);
|
||||||
|
log.info(`Stacktrace:\n${stack}`);
|
||||||
|
handleAppBeforeQuit();
|
||||||
|
app.quit();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSwitchServer(event: IpcMainEvent, serverName: string) {
|
||||||
|
WindowManager.switchServer(serverName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||||
|
WindowManager.switchTab(serverName, tabName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||||
|
const teams = Config.teams;
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (team.name === serverName) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name === tabName) {
|
||||||
|
tab.isOpen = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name;
|
||||||
|
WindowManager.switchTab(serverName, nextTab);
|
||||||
|
Config.set('teams', teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||||
|
const teams = Config.teams;
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (team.name === serverName) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name === tabName) {
|
||||||
|
tab.isOpen = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WindowManager.switchTab(serverName, tabName);
|
||||||
|
Config.set('teams', teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleNewServerModal() {
|
||||||
|
const html = getLocalURLString('newServer.html');
|
||||||
|
|
||||||
|
const modalPreload = getLocalPreload('modalPreload.js');
|
||||||
|
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modalPromise = ModalManager.addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow, Config.teams.length === 0);
|
||||||
|
if (modalPromise) {
|
||||||
|
modalPromise.then((data) => {
|
||||||
|
const teams = Config.teams;
|
||||||
|
const order = teams.length;
|
||||||
|
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order});
|
||||||
|
teams.push(newTeam);
|
||||||
|
Config.set('teams', teams);
|
||||||
|
updateServerInfos([newTeam]);
|
||||||
|
WindowManager.switchServer(newTeam.name, true);
|
||||||
|
}).catch((e) => {
|
||||||
|
// e is undefined for user cancellation
|
||||||
|
if (e) {
|
||||||
|
log.error(`there was an error in the new server modal: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn('There is already a new server modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleEditServerModal(e: IpcMainEvent, name: string) {
|
||||||
|
const html = getLocalURLString('editServer.html');
|
||||||
|
|
||||||
|
const modalPreload = getLocalPreload('modalPreload.js');
|
||||||
|
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const serverIndex = Config.teams.findIndex((team) => team.name === name);
|
||||||
|
if (serverIndex < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modalPromise = ModalManager.addModal<Team, Team>('editServer', html, modalPreload, Config.teams[serverIndex], mainWindow);
|
||||||
|
if (modalPromise) {
|
||||||
|
modalPromise.then((data) => {
|
||||||
|
const teams = Config.teams;
|
||||||
|
teams[serverIndex].name = data.name;
|
||||||
|
teams[serverIndex].url = data.url;
|
||||||
|
Config.set('teams', teams);
|
||||||
|
}).catch((e) => {
|
||||||
|
// e is undefined for user cancellation
|
||||||
|
if (e) {
|
||||||
|
log.error(`there was an error in the edit server modal: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn('There is already an edit server modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleRemoveServerModal(e: IpcMainEvent, name: string) {
|
||||||
|
const html = getLocalURLString('removeServer.html');
|
||||||
|
|
||||||
|
const modalPreload = getLocalPreload('modalPreload.js');
|
||||||
|
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const modalPromise = ModalManager.addModal<string, boolean>('removeServer', html, modalPreload, name, mainWindow);
|
||||||
|
if (modalPromise) {
|
||||||
|
modalPromise.then((remove) => {
|
||||||
|
if (remove) {
|
||||||
|
const teams = Config.teams;
|
||||||
|
const removedTeam = teams.findIndex((team) => team.name === name);
|
||||||
|
if (removedTeam < 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const removedOrder = teams[removedTeam].order;
|
||||||
|
teams.splice(removedTeam, 1);
|
||||||
|
teams.forEach((value) => {
|
||||||
|
if (value.order > removedOrder) {
|
||||||
|
value.order--;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Config.set('teams', teams);
|
||||||
|
}
|
||||||
|
}).catch((e) => {
|
||||||
|
// e is undefined for user cancellation
|
||||||
|
if (e) {
|
||||||
|
log.error(`there was an error in the edit server modal: ${e}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
log.warn('There is already an edit server modal');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, data: MentionData) {
|
||||||
|
displayMention(title, body, channel, teamId, url, silent, event.sender, data);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleOpenAppMenu() {
|
||||||
|
const windowMenu = Menu.getApplicationMenu();
|
||||||
|
if (!windowMenu) {
|
||||||
|
log.error('No application menu found');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
windowMenu.popup({
|
||||||
|
window: WindowManager.getMainWindow(),
|
||||||
|
x: 18,
|
||||||
|
y: 18,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
|
||||||
|
const message = 'Specify the folder where files will download';
|
||||||
|
const result = await dialog.showOpenDialog({defaultPath: startFrom || Config.downloadLocation,
|
||||||
|
message,
|
||||||
|
properties:
|
||||||
|
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
|
||||||
|
return result.filePaths[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewName: string) {
|
||||||
|
const teams = Config.teams;
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (team.name === serverName) {
|
||||||
|
const viewOrder = team?.tabs.find((tab) => tab.name === viewName)?.order || 0;
|
||||||
|
team.lastActiveTab = viewOrder;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
Config.set('teams', teams);
|
||||||
|
Config.set('lastActiveTeam', teams.find((team) => team.name === serverName)?.order || 0);
|
||||||
|
}
|
||||||
|
|
192
src/main/app/utils.test.js
Normal file
192
src/main/app/utils.test.js
Normal file
@@ -0,0 +1,192 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
import {TAB_MESSAGING, TAB_FOCALBOARD, TAB_PLAYBOOKS} from 'common/tabs/TabView';
|
||||||
|
import Utils from 'common/utils/util';
|
||||||
|
|
||||||
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
|
|
||||||
|
import {getDeeplinkingURL, updateServerInfos, resizeScreen} from './utils';
|
||||||
|
|
||||||
|
jest.mock('electron-log', () => ({
|
||||||
|
info: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({
|
||||||
|
set: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('common/utils/util', () => ({
|
||||||
|
isVersionGreaterThanOrEqualTo: jest.fn(),
|
||||||
|
getDisplayBoundaries: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/menus/app', () => ({}));
|
||||||
|
jest.mock('main/menus/tray', () => ({}));
|
||||||
|
jest.mock('main/server/serverInfo', () => ({
|
||||||
|
ServerInfo: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/tray/tray', () => ({}));
|
||||||
|
jest.mock('main/windows/windowManager', () => ({}));
|
||||||
|
|
||||||
|
jest.mock('./initialize', () => ({
|
||||||
|
mainProtocol: 'mattermost',
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('main/app/utils', () => {
|
||||||
|
describe('updateServerInfos', () => {
|
||||||
|
const tabs = [
|
||||||
|
{
|
||||||
|
name: TAB_MESSAGING,
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: TAB_FOCALBOARD,
|
||||||
|
order: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: TAB_PLAYBOOKS,
|
||||||
|
order: 1,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const teams = [
|
||||||
|
{
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
tabs,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0');
|
||||||
|
Config.set.mockImplementation((name, value) => {
|
||||||
|
Config[name] = value;
|
||||||
|
});
|
||||||
|
const teamsCopy = JSON.parse(JSON.stringify(teams));
|
||||||
|
Config.teams = teamsCopy;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
delete Config.teams;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open all tabs', async () => {
|
||||||
|
ServerInfo.mockReturnValue({promise: {
|
||||||
|
name: 'server-1',
|
||||||
|
serverVersion: '6.0.0',
|
||||||
|
hasPlaybooks: true,
|
||||||
|
hasFocalboard: true,
|
||||||
|
}});
|
||||||
|
|
||||||
|
updateServerInfos(Config.teams);
|
||||||
|
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
|
||||||
|
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBe(true);
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open only playbooks', async () => {
|
||||||
|
ServerInfo.mockReturnValue({promise: {
|
||||||
|
name: 'server-1',
|
||||||
|
serverVersion: '6.0.0',
|
||||||
|
hasPlaybooks: true,
|
||||||
|
hasFocalboard: false,
|
||||||
|
}});
|
||||||
|
|
||||||
|
updateServerInfos(Config.teams);
|
||||||
|
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
|
||||||
|
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBe(true);
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should open none when server version is too old', async () => {
|
||||||
|
ServerInfo.mockReturnValue({promise: {
|
||||||
|
name: 'server-1',
|
||||||
|
serverVersion: '5.0.0',
|
||||||
|
hasPlaybooks: true,
|
||||||
|
hasFocalboard: true,
|
||||||
|
}});
|
||||||
|
|
||||||
|
updateServerInfos(Config.teams);
|
||||||
|
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
|
||||||
|
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBeUndefined();
|
||||||
|
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBeUndefined();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getDeeplinkingURL', () => {
|
||||||
|
it('should return undefined if deeplinking URL is not last argument', () => {
|
||||||
|
expect(getDeeplinkingURL(['mattermost', 'mattermost://server-1.com', '--oops'])).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return undefined if deeplinking URL is not valid', () => {
|
||||||
|
expect(getDeeplinkingURL(['mattermost', 'mattermost://,a<lolbad'])).toBeUndefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return url if deeplinking URL is valid', () => {
|
||||||
|
expect(getDeeplinkingURL(['mattermost', 'mattermost://server-1.com'])).toBe('mattermost://server-1.com');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('resizeScreen', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Utils.getDisplayBoundaries.mockReturnValue([{
|
||||||
|
minX: 400,
|
||||||
|
minY: 300,
|
||||||
|
maxX: 2320,
|
||||||
|
maxY: 1380,
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
}]);
|
||||||
|
});
|
||||||
|
it('should keep the same position if it is within a display', () => {
|
||||||
|
const browserWindow = {
|
||||||
|
getPosition: () => [500, 400],
|
||||||
|
getSize: () => [1280, 720],
|
||||||
|
setPosition: jest.fn(),
|
||||||
|
center: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
resizeScreen(browserWindow);
|
||||||
|
expect(browserWindow.setPosition).toHaveBeenCalledWith(500, 400);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep the same position if it is halfway within a display', () => {
|
||||||
|
let browserWindow = {
|
||||||
|
getPosition: () => [1680, 400],
|
||||||
|
getSize: () => [1280, 720],
|
||||||
|
setPosition: jest.fn(),
|
||||||
|
center: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
resizeScreen(browserWindow);
|
||||||
|
expect(browserWindow.setPosition).toHaveBeenCalledWith(1680, 400);
|
||||||
|
|
||||||
|
browserWindow = {
|
||||||
|
getPosition: () => [500, 1020],
|
||||||
|
getSize: () => [1280, 720],
|
||||||
|
setPosition: jest.fn(),
|
||||||
|
center: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
resizeScreen(browserWindow);
|
||||||
|
expect(browserWindow.setPosition).toHaveBeenCalledWith(500, 1020);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should center if it is outside a display', () => {
|
||||||
|
const browserWindow = {
|
||||||
|
getPosition: () => [2400, 2000],
|
||||||
|
getSize: () => [1280, 720],
|
||||||
|
setPosition: jest.fn(),
|
||||||
|
center: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
};
|
||||||
|
resizeScreen(browserWindow);
|
||||||
|
expect(browserWindow.setPosition).not.toHaveBeenCalled();
|
||||||
|
expect(browserWindow.center).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
178
src/main/app/utils.ts
Normal file
178
src/main/app/utils.ts
Normal file
@@ -0,0 +1,178 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {app, BrowserWindow, Menu, Rectangle, Session, session} from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import {TeamWithTabs} from 'types/config';
|
||||||
|
import {RemoteInfo} from 'types/server';
|
||||||
|
import {Boundaries} from 'types/utils';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
|
import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
|
||||||
|
import urlUtils from 'common/utils/url';
|
||||||
|
import Utils from 'common/utils/util';
|
||||||
|
|
||||||
|
import {createMenu as createAppMenu} from 'main/menus/app';
|
||||||
|
import {createMenu as createTrayMenu} from 'main/menus/tray';
|
||||||
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
|
import {setTrayMenu} from 'main/tray/tray';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
|
import {mainProtocol} from './initialize';
|
||||||
|
|
||||||
|
export function openDeepLink(deeplinkingUrl: string) {
|
||||||
|
try {
|
||||||
|
WindowManager.showMainWindow(deeplinkingUrl);
|
||||||
|
} catch (err) {
|
||||||
|
log.error(`There was an error opening the deeplinking url: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateSpellCheckerLocales() {
|
||||||
|
if (Config.data?.spellCheckerLocales.length && app.isReady()) {
|
||||||
|
session.defaultSession.setSpellCheckerLanguages(Config.data?.spellCheckerLocales);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateServerInfos(teams: TeamWithTabs[]) {
|
||||||
|
const serverInfos: Array<Promise<RemoteInfo | string | undefined>> = [];
|
||||||
|
teams.forEach((team) => {
|
||||||
|
const serverInfo = new ServerInfo(new MattermostServer(team.name, team.url));
|
||||||
|
serverInfos.push(serverInfo.promise);
|
||||||
|
});
|
||||||
|
Promise.all(serverInfos).then((data: Array<RemoteInfo | string | undefined>) => {
|
||||||
|
const teams = Config.teams;
|
||||||
|
teams.forEach((team) => openExtraTabs(data, team));
|
||||||
|
Config.set('teams', teams);
|
||||||
|
}).catch((reason: any) => {
|
||||||
|
log.error('Error getting server infos', reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function openExtraTabs(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
|
||||||
|
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
|
||||||
|
if (remoteInfo) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name !== TAB_MESSAGING && remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0')) {
|
||||||
|
if (tab.name === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && tab.isOpen !== false) {
|
||||||
|
log.info(`opening ${team.name}___${tab.name} on hasPlaybooks`);
|
||||||
|
tab.isOpen = true;
|
||||||
|
}
|
||||||
|
if (tab.name === TAB_FOCALBOARD && remoteInfo.hasFocalboard && tab.isOpen !== false) {
|
||||||
|
log.info(`opening ${team.name}___${tab.name} on hasFocalboard`);
|
||||||
|
tab.isOpen = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function handleUpdateMenuEvent() {
|
||||||
|
const aMenu = createAppMenu(Config);
|
||||||
|
Menu.setApplicationMenu(aMenu);
|
||||||
|
aMenu.addListener('menu-will-close', WindowManager.focusBrowserView);
|
||||||
|
|
||||||
|
// set up context menu for tray icon
|
||||||
|
if (shouldShowTrayIcon()) {
|
||||||
|
const tMenu = createTrayMenu(Config.data!);
|
||||||
|
setTrayMenu(tMenu);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getDeeplinkingURL(args: string[]) {
|
||||||
|
if (Array.isArray(args) && args.length) {
|
||||||
|
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
|
||||||
|
const url = args[args.length - 1];
|
||||||
|
if (url && mainProtocol && url.startsWith(mainProtocol) && urlUtils.isValidURI(url)) {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function shouldShowTrayIcon() {
|
||||||
|
return Config.showTrayIcon || process.platform === 'win32';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function wasUpdated(lastAppVersion?: string) {
|
||||||
|
return lastAppVersion !== app.getVersion();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function clearAppCache() {
|
||||||
|
// TODO: clear cache on browserviews, not in the renderer.
|
||||||
|
const mainWindow = WindowManager.getMainWindow();
|
||||||
|
if (mainWindow) {
|
||||||
|
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
||||||
|
} else {
|
||||||
|
//Wait for mainWindow
|
||||||
|
setTimeout(clearAppCache, 100);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isWithinDisplay(state: Rectangle, display: Boundaries) {
|
||||||
|
const startsWithinDisplay = !(state.x > display.maxX || state.y > display.maxY || state.x < display.minX || state.y < display.minY);
|
||||||
|
if (!startsWithinDisplay) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// is half the screen within the display?
|
||||||
|
const midX = state.x + (state.width / 2);
|
||||||
|
const midY = state.y + (state.height / 2);
|
||||||
|
return !(midX > display.maxX || midY > display.maxY);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getValidWindowPosition(state: Rectangle) {
|
||||||
|
// Check if the previous position is out of the viewable area
|
||||||
|
// (e.g. because the screen has been plugged off)
|
||||||
|
const boundaries = Utils.getDisplayBoundaries();
|
||||||
|
const display = boundaries.find((boundary) => {
|
||||||
|
return isWithinDisplay(state, boundary);
|
||||||
|
});
|
||||||
|
|
||||||
|
if (typeof display === 'undefined') {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
return {x: state.x, y: state.y};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resizeScreen(browserWindow: BrowserWindow) {
|
||||||
|
function handle() {
|
||||||
|
const position = browserWindow.getPosition();
|
||||||
|
const size = browserWindow.getSize();
|
||||||
|
const validPosition = getValidWindowPosition({
|
||||||
|
x: position[0],
|
||||||
|
y: position[1],
|
||||||
|
width: size[0],
|
||||||
|
height: size[1],
|
||||||
|
});
|
||||||
|
if (typeof validPosition.x !== 'undefined' || typeof validPosition.y !== 'undefined') {
|
||||||
|
browserWindow.setPosition(validPosition.x || 0, validPosition.y || 0);
|
||||||
|
} else {
|
||||||
|
browserWindow.center();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
browserWindow.on('restore', handle);
|
||||||
|
handle();
|
||||||
|
}
|
||||||
|
|
||||||
|
function flushCookiesStore(session: Session) {
|
||||||
|
session.cookies.flushStore().catch((err) => {
|
||||||
|
log.error(`There was a problem flushing cookies:\n${err}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function initCookieManager(session: Session) {
|
||||||
|
// Somehow cookies are not immediately saved to disk.
|
||||||
|
// So manually flush cookie store to disk on closing the app.
|
||||||
|
// https://github.com/electron/electron/issues/8416
|
||||||
|
app.on('before-quit', () => {
|
||||||
|
flushCookiesStore(session);
|
||||||
|
});
|
||||||
|
|
||||||
|
app.on('browser-window-blur', () => {
|
||||||
|
flushCookiesStore(session);
|
||||||
|
});
|
||||||
|
}
|
@@ -25,10 +25,27 @@ jest.mock('common/utils/url', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getPath: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('electron-log', () => ({
|
jest.mock('electron-log', () => ({
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/trustedOrigins', () => ({
|
||||||
|
addPermission: jest.fn(),
|
||||||
|
checkPermission: (url) => {
|
||||||
|
return url.toString() === 'http://haspermissionurl.com/';
|
||||||
|
},
|
||||||
|
save: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getMainWindow: jest.fn().mockImplementation(() => ({})),
|
getMainWindow: jest.fn().mockImplementation(() => ({})),
|
||||||
}));
|
}));
|
||||||
@@ -90,17 +107,9 @@ const config = {
|
|||||||
}],
|
}],
|
||||||
};
|
};
|
||||||
|
|
||||||
const trustedOriginsStore = {
|
|
||||||
addPermission: jest.fn(),
|
|
||||||
checkPermission: (url) => {
|
|
||||||
return url.toString() === 'http://haspermissionurl.com/';
|
|
||||||
},
|
|
||||||
save: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('main/authManager', () => {
|
describe('main/authManager', () => {
|
||||||
describe('handleAppLogin', () => {
|
describe('handleAppLogin', () => {
|
||||||
const authManager = new AuthManager(config, trustedOriginsStore);
|
const authManager = new AuthManager(config);
|
||||||
authManager.popLoginModal = jest.fn();
|
authManager.popLoginModal = jest.fn();
|
||||||
authManager.popPermissionModal = jest.fn();
|
authManager.popPermissionModal = jest.fn();
|
||||||
|
|
||||||
@@ -148,7 +157,7 @@ describe('main/authManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('popLoginModal', () => {
|
describe('popLoginModal', () => {
|
||||||
const authManager = new AuthManager(config, trustedOriginsStore);
|
const authManager = new AuthManager(config);
|
||||||
|
|
||||||
it('should not pop modal when no main window exists', () => {
|
it('should not pop modal when no main window exists', () => {
|
||||||
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
||||||
@@ -216,7 +225,7 @@ describe('main/authManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('popPermissionModal', () => {
|
describe('popPermissionModal', () => {
|
||||||
const authManager = new AuthManager(config, trustedOriginsStore);
|
const authManager = new AuthManager(config);
|
||||||
|
|
||||||
it('should not pop modal when no main window exists', () => {
|
it('should not pop modal when no main window exists', () => {
|
||||||
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
||||||
@@ -261,7 +270,7 @@ describe('main/authManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleLoginCredentialsEvent', () => {
|
describe('handleLoginCredentialsEvent', () => {
|
||||||
const authManager = new AuthManager(config, trustedOriginsStore);
|
const authManager = new AuthManager(config);
|
||||||
const callback = jest.fn();
|
const callback = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@@ -3,10 +3,10 @@
|
|||||||
import {AuthenticationResponseDetails, AuthInfo, WebContents} from 'electron';
|
import {AuthenticationResponseDetails, AuthInfo, WebContents} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {CombinedConfig} from 'types/config';
|
|
||||||
import {PermissionType} from 'types/trustedOrigin';
|
import {PermissionType} from 'types/trustedOrigin';
|
||||||
import {LoginModalData} from 'types/auth';
|
import {LoginModalData} from 'types/auth';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
@@ -25,33 +25,25 @@ type LoginModalResult = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export class AuthManager {
|
export class AuthManager {
|
||||||
config: CombinedConfig;
|
|
||||||
trustedOriginsStore: TrustedOriginsStore;
|
|
||||||
loginCallbackMap: Map<string, ((username?: string, password?: string) => void) | undefined>;
|
loginCallbackMap: Map<string, ((username?: string, password?: string) => void) | undefined>;
|
||||||
|
|
||||||
constructor(config: CombinedConfig, trustedOriginsStore: TrustedOriginsStore) {
|
constructor() {
|
||||||
this.config = config;
|
|
||||||
this.trustedOriginsStore = trustedOriginsStore;
|
|
||||||
this.loginCallbackMap = new Map();
|
this.loginCallbackMap = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
handleConfigUpdate = (newConfig: CombinedConfig) => {
|
|
||||||
this.config = newConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => {
|
handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const parsedURL = urlUtils.parseURL(request.url);
|
const parsedURL = urlUtils.parseURL(request.url);
|
||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const server = urlUtils.getView(parsedURL, this.config.teams);
|
const server = urlUtils.getView(parsedURL, Config.teams);
|
||||||
if (!server) {
|
if (!server) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.loginCallbackMap.set(request.url, callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
this.loginCallbackMap.set(request.url, callback); // if callback is undefined set it to null instead so we know we have set it up with no value
|
||||||
if (urlUtils.isTrustedURL(request.url, this.config.teams) || urlUtils.isCustomLoginURL(parsedURL, server, this.config.teams) || this.trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
if (urlUtils.isTrustedURL(request.url, Config.teams) || urlUtils.isCustomLoginURL(parsedURL, server, Config.teams) || TrustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
||||||
this.popLoginModal(request, authInfo);
|
this.popLoginModal(request, authInfo);
|
||||||
} else {
|
} else {
|
||||||
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);
|
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);
|
||||||
@@ -114,7 +106,10 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handlePermissionGranted(url: string, permission: PermissionType) {
|
handlePermissionGranted(url: string, permission: PermissionType) {
|
||||||
this.trustedOriginsStore.addPermission(url, permission);
|
TrustedOriginsStore.addPermission(url, permission);
|
||||||
this.trustedOriginsStore.save();
|
TrustedOriginsStore.save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const authManager = new AuthManager();
|
||||||
|
export default authManager;
|
||||||
|
@@ -73,3 +73,6 @@ export class CertificateManager {
|
|||||||
this.certificateRequestCallbackMap.delete(server);
|
this.certificateRequestCallbackMap.delete(server);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const certificateManager = new CertificateManager();
|
||||||
|
export default certificateManager;
|
||||||
|
@@ -6,13 +6,27 @@ import fs from 'fs';
|
|||||||
|
|
||||||
import {validateCertificateStore} from './Validator';
|
import {validateCertificateStore} from './Validator';
|
||||||
|
|
||||||
import CertificateStore from './certificateStore';
|
import {CertificateStore} from './certificateStore';
|
||||||
|
|
||||||
|
jest.mock('path', () => ({
|
||||||
|
resolve: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getPath: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./Validator', () => ({
|
jest.mock('./Validator', () => ({
|
||||||
validateCertificateStore: jest.fn(),
|
validateCertificateStore: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('fs', () => ({
|
jest.mock('fs', () => ({
|
||||||
|
existsSync: jest.fn(),
|
||||||
readFileSync: jest.fn(),
|
readFileSync: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
@@ -5,13 +5,15 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import {Certificate} from 'electron';
|
import {Certificate, ipcMain} from 'electron';
|
||||||
|
|
||||||
import {ComparableCertificate} from 'types/certificate';
|
import {ComparableCertificate} from 'types/certificate';
|
||||||
|
|
||||||
|
import {UPDATE_PATHS} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
|
import {certificateStorePath} from './constants';
|
||||||
|
|
||||||
function comparableCertificate(certificate: Certificate, dontTrust = false): ComparableCertificate {
|
function comparableCertificate(certificate: Certificate, dontTrust = false): ComparableCertificate {
|
||||||
return {
|
return {
|
||||||
@@ -31,7 +33,7 @@ function areEqual(certificate0: ComparableCertificate, certificate1: ComparableC
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default class CertificateStore {
|
export class CertificateStore {
|
||||||
storeFile: string;
|
storeFile: string;
|
||||||
data: Record<string, ComparableCertificate>;
|
data: Record<string, ComparableCertificate>;
|
||||||
|
|
||||||
@@ -78,3 +80,10 @@ export default class CertificateStore {
|
|||||||
return dontTrust === undefined ? false : dontTrust;
|
return dontTrust === undefined ? false : dontTrust;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let certificateStore = new CertificateStore(certificateStorePath);
|
||||||
|
export default certificateStore;
|
||||||
|
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
certificateStore = new CertificateStore(certificateStorePath);
|
||||||
|
});
|
||||||
|
36
src/main/constants.ts
Normal file
36
src/main/constants.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
/* istanbul ignore file */
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
|
import {app, ipcMain} from 'electron';
|
||||||
|
|
||||||
|
import {UPDATE_PATHS} from 'common/communication';
|
||||||
|
|
||||||
|
let userDataPath;
|
||||||
|
|
||||||
|
export let configPath = '';
|
||||||
|
export let allowedProtocolFile = '';
|
||||||
|
export let appVersionJson = '';
|
||||||
|
export let certificateStorePath = '';
|
||||||
|
export let trustedOriginsStoreFile = '';
|
||||||
|
export let boundsInfoPath = '';
|
||||||
|
|
||||||
|
export function updatePaths(emit = false) {
|
||||||
|
userDataPath = app.getPath('userData');
|
||||||
|
|
||||||
|
configPath = `${userDataPath}/config.json`;
|
||||||
|
allowedProtocolFile = path.resolve(userDataPath, 'allowedProtocols.json');
|
||||||
|
appVersionJson = path.join(userDataPath, 'app-state.json');
|
||||||
|
certificateStorePath = path.resolve(userDataPath, 'certificate.json');
|
||||||
|
trustedOriginsStoreFile = path.resolve(userDataPath, 'trustedOrigins.json');
|
||||||
|
boundsInfoPath = path.join(userDataPath, 'bounds-info.json');
|
||||||
|
|
||||||
|
if (emit) {
|
||||||
|
ipcMain.emit(UPDATE_PATHS);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
updatePaths();
|
@@ -1,24 +0,0 @@
|
|||||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
import {app, Session} from 'electron';
|
|
||||||
import log from 'electron-log';
|
|
||||||
|
|
||||||
function flushCookiesStore(session: Session) {
|
|
||||||
session.cookies.flushStore().catch((err) => {
|
|
||||||
log.error(`There was a problem flushing cookies:\n${err}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function initCookieManager(session: Session) {
|
|
||||||
// Somehow cookies are not immediately saved to disk.
|
|
||||||
// So manually flush cookie store to disk on closing the app.
|
|
||||||
// https://github.com/electron/electron/issues/8416
|
|
||||||
app.on('before-quit', () => {
|
|
||||||
flushCookiesStore(session);
|
|
||||||
});
|
|
||||||
|
|
||||||
app.on('browser-window-blur', () => {
|
|
||||||
flushCookiesStore(session);
|
|
||||||
});
|
|
||||||
}
|
|
983
src/main/main.ts
983
src/main/main.ts
@@ -1,983 +0,0 @@
|
|||||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
|
||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
|
||||||
// See LICENSE.txt for license information.
|
|
||||||
|
|
||||||
/* eslint-disable max-lines */
|
|
||||||
import fs from 'fs';
|
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
|
|
||||||
import electron, {BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, Rectangle} from 'electron';
|
|
||||||
import isDev from 'electron-is-dev';
|
|
||||||
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
|
|
||||||
import log from 'electron-log';
|
|
||||||
import 'airbnb-js-shims/target/es2015';
|
|
||||||
|
|
||||||
import {CombinedConfig, Team, TeamWithTabs} from 'types/config';
|
|
||||||
import {MentionData} from 'types/notification';
|
|
||||||
import {RemoteInfo} from 'types/server';
|
|
||||||
import {Boundaries} from 'types/utils';
|
|
||||||
|
|
||||||
import {
|
|
||||||
SWITCH_SERVER,
|
|
||||||
FOCUS_BROWSERVIEW,
|
|
||||||
QUIT,
|
|
||||||
DARK_MODE_CHANGE,
|
|
||||||
DOUBLE_CLICK_ON_WINDOW,
|
|
||||||
SHOW_NEW_SERVER_MODAL,
|
|
||||||
WINDOW_CLOSE,
|
|
||||||
WINDOW_MAXIMIZE,
|
|
||||||
WINDOW_MINIMIZE,
|
|
||||||
WINDOW_RESTORE,
|
|
||||||
NOTIFY_MENTION,
|
|
||||||
GET_DOWNLOAD_LOCATION,
|
|
||||||
SHOW_SETTINGS_WINDOW,
|
|
||||||
RELOAD_CONFIGURATION,
|
|
||||||
USER_ACTIVITY_UPDATE,
|
|
||||||
EMIT_CONFIGURATION,
|
|
||||||
SWITCH_TAB,
|
|
||||||
CLOSE_TAB,
|
|
||||||
OPEN_TAB,
|
|
||||||
SHOW_EDIT_SERVER_MODAL,
|
|
||||||
SHOW_REMOVE_SERVER_MODAL,
|
|
||||||
UPDATE_SHORTCUT_MENU,
|
|
||||||
UPDATE_LAST_ACTIVE,
|
|
||||||
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
|
|
||||||
} from 'common/communication';
|
|
||||||
import Config from 'common/config';
|
|
||||||
import {MattermostServer} from 'common/servers/MattermostServer';
|
|
||||||
import {getDefaultTeamWithTabsFromTeam, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
|
|
||||||
import Utils from 'common/utils/util';
|
|
||||||
|
|
||||||
import urlUtils from 'common/utils/url';
|
|
||||||
|
|
||||||
import {protocols} from '../../electron-builder.json';
|
|
||||||
|
|
||||||
import AutoLauncher from './AutoLauncher';
|
|
||||||
import CriticalErrorHandler from './CriticalErrorHandler';
|
|
||||||
import CertificateStore from './certificateStore';
|
|
||||||
import TrustedOriginsStore from './trustedOrigins';
|
|
||||||
import {createMenu as createAppMenu} from './menus/app';
|
|
||||||
import {createMenu as createTrayMenu} from './menus/tray';
|
|
||||||
import allowProtocolDialog from './allowProtocolDialog';
|
|
||||||
import AppVersionManager from './AppVersionManager';
|
|
||||||
import initCookieManager from './cookieManager';
|
|
||||||
import UserActivityMonitor from './UserActivityMonitor';
|
|
||||||
import WindowManager from './windows/windowManager';
|
|
||||||
import {displayMention, displayDownloadCompleted} from './notifications';
|
|
||||||
|
|
||||||
import parseArgs from './ParseArgs';
|
|
||||||
import modalManager from './views/modalManager';
|
|
||||||
import {getLocalURLString, getLocalPreload} from './utils';
|
|
||||||
import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tray';
|
|
||||||
import {AuthManager} from './authManager';
|
|
||||||
import {CertificateManager} from './certificateManager';
|
|
||||||
import {setupBadge, setUnreadBadgeSetting} from './badge';
|
|
||||||
import {ServerInfo} from './server/serverInfo';
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production' && module.hot) {
|
|
||||||
module.hot.accept();
|
|
||||||
}
|
|
||||||
|
|
||||||
// pull out required electron components like this
|
|
||||||
// as not all components can be referenced before the app is ready
|
|
||||||
const {
|
|
||||||
app,
|
|
||||||
Menu,
|
|
||||||
ipcMain,
|
|
||||||
dialog,
|
|
||||||
session,
|
|
||||||
} = electron;
|
|
||||||
const criticalErrorHandler = new CriticalErrorHandler();
|
|
||||||
const userActivityMonitor = new UserActivityMonitor();
|
|
||||||
const autoLauncher = new AutoLauncher();
|
|
||||||
const certificateErrorCallbacks = new Map();
|
|
||||||
|
|
||||||
// Keep a global reference of the window object, if you don't, the window will
|
|
||||||
// be closed automatically when the JavaScript object is garbage collected.
|
|
||||||
let certificateStore: CertificateStore;
|
|
||||||
let trustedOriginsStore;
|
|
||||||
let scheme: string;
|
|
||||||
let appVersion = null;
|
|
||||||
let config: Config;
|
|
||||||
let authManager: AuthManager;
|
|
||||||
let certificateManager: CertificateManager;
|
|
||||||
let didCheckForAddServerModal = false;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Main entry point for the application, ensures that everything initializes in the proper order
|
|
||||||
*/
|
|
||||||
async function initialize() {
|
|
||||||
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
|
|
||||||
global.willAppQuit = false;
|
|
||||||
|
|
||||||
// initialization that can run before the app is ready
|
|
||||||
initializeArgs();
|
|
||||||
await initializeConfig();
|
|
||||||
initializeAppEventListeners();
|
|
||||||
initializeBeforeAppReady();
|
|
||||||
|
|
||||||
// wait for registry config data to load and app ready event
|
|
||||||
await Promise.all([
|
|
||||||
app.whenReady(),
|
|
||||||
]);
|
|
||||||
|
|
||||||
// no need to continue initializing if app is quitting
|
|
||||||
if (global.willAppQuit) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// initialization that should run once the app is ready
|
|
||||||
initializeInterCommunicationEventListeners();
|
|
||||||
initializeAfterAppReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
// attempt to initialize the application
|
|
||||||
try {
|
|
||||||
initialize();
|
|
||||||
} catch (error) {
|
|
||||||
throw new Error(`App initialization failed: ${error.toString()}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// initialization sub functions
|
|
||||||
//
|
|
||||||
|
|
||||||
function initializeArgs() {
|
|
||||||
global.args = parseArgs(process.argv.slice(1));
|
|
||||||
|
|
||||||
// output the application version via cli when requested (-v or --version)
|
|
||||||
if (global.args.version) {
|
|
||||||
process.stdout.write(`v.${app.getVersion()}\n`);
|
|
||||||
process.exit(0); // eslint-disable-line no-process-exit
|
|
||||||
}
|
|
||||||
|
|
||||||
global.isDev = isDev && !global.args.disableDevMode; // this doesn't seem to be right and isn't used as the single source of truth
|
|
||||||
|
|
||||||
if (global.args.dataDir) {
|
|
||||||
app.setPath('userData', path.resolve(global.args.dataDir));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function initializeConfig() {
|
|
||||||
const loadConfig = new Promise<void>((resolve) => {
|
|
||||||
config = new Config(app.getPath('userData') + '/config.json');
|
|
||||||
config.once('update', (configData) => {
|
|
||||||
config.on('update', handleConfigUpdate);
|
|
||||||
config.on('synchronize', handleConfigSynchronize);
|
|
||||||
config.on('darkModeChange', handleDarkModeChange);
|
|
||||||
config.on('error', (error) => {
|
|
||||||
log.error(error);
|
|
||||||
});
|
|
||||||
handleConfigUpdate(configData);
|
|
||||||
|
|
||||||
// can only call this before the app is ready
|
|
||||||
if (config.enableHardwareAcceleration === false) {
|
|
||||||
app.disableHardwareAcceleration();
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve();
|
|
||||||
});
|
|
||||||
config.init();
|
|
||||||
});
|
|
||||||
|
|
||||||
return loadConfig;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeAppEventListeners() {
|
|
||||||
app.on('second-instance', handleAppSecondInstance);
|
|
||||||
app.on('window-all-closed', handleAppWindowAllClosed);
|
|
||||||
app.on('browser-window-created', handleAppBrowserWindowCreated);
|
|
||||||
app.on('activate', handleAppActivate);
|
|
||||||
app.on('before-quit', handleAppBeforeQuit);
|
|
||||||
app.on('certificate-error', handleAppCertificateError);
|
|
||||||
app.on('select-client-certificate', handleSelectCertificate);
|
|
||||||
app.on('gpu-process-crashed', handleAppGPUProcessCrashed);
|
|
||||||
app.on('login', handleAppLogin);
|
|
||||||
app.on('will-finish-launching', handleAppWillFinishLaunching);
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeBeforeAppReady() {
|
|
||||||
if (!config || !config.data) {
|
|
||||||
log.error('No config loaded');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (process.env.NODE_ENV !== 'test') {
|
|
||||||
app.enableSandbox();
|
|
||||||
}
|
|
||||||
certificateStore = new CertificateStore(path.resolve(app.getPath('userData'), 'certificate.json'));
|
|
||||||
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
|
|
||||||
trustedOriginsStore.load();
|
|
||||||
|
|
||||||
// prevent using a different working directory, which happens on windows running after installation.
|
|
||||||
const expectedPath = path.dirname(process.execPath);
|
|
||||||
if (process.cwd() !== expectedPath && !isDev) {
|
|
||||||
log.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
|
|
||||||
process.chdir(expectedPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
refreshTrayImages(config.trayIconTheme);
|
|
||||||
|
|
||||||
// If there is already an instance, quit this one
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock();
|
|
||||||
if (!gotTheLock) {
|
|
||||||
app.exit();
|
|
||||||
global.willAppQuit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
allowProtocolDialog.init();
|
|
||||||
|
|
||||||
authManager = new AuthManager(config.data, trustedOriginsStore);
|
|
||||||
certificateManager = new CertificateManager();
|
|
||||||
|
|
||||||
if (isDev && process.env.NODE_ENV !== 'test') {
|
|
||||||
log.info('In development mode, deeplinking is disabled');
|
|
||||||
} else if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
|
|
||||||
scheme = protocols[0].schemes[0];
|
|
||||||
app.setAsDefaultProtocolClient(scheme);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeInterCommunicationEventListeners() {
|
|
||||||
ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig);
|
|
||||||
ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
|
|
||||||
ipcMain.handle('get-app-version', handleAppVersion);
|
|
||||||
ipcMain.on('update-menu', handleUpdateMenuEvent);
|
|
||||||
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateShortcutMenuEvent);
|
|
||||||
ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView);
|
|
||||||
ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive);
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
ipcMain.on('open-app-menu', handleOpenAppMenu);
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
|
||||||
ipcMain.on(SWITCH_TAB, handleSwitchTab);
|
|
||||||
ipcMain.on(CLOSE_TAB, handleCloseTab);
|
|
||||||
ipcMain.on(OPEN_TAB, handleOpenTab);
|
|
||||||
|
|
||||||
ipcMain.on(QUIT, handleQuit);
|
|
||||||
|
|
||||||
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick);
|
|
||||||
|
|
||||||
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
|
|
||||||
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
|
|
||||||
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
|
|
||||||
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
|
|
||||||
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
|
|
||||||
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
|
|
||||||
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
|
|
||||||
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
|
|
||||||
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, handleGetAvailableSpellCheckerLanguages);
|
|
||||||
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// config event handlers
|
|
||||||
//
|
|
||||||
|
|
||||||
function handleConfigUpdate(newConfig: CombinedConfig) {
|
|
||||||
if (!newConfig) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
|
||||||
const autoStartTask = config.autostart ? autoLauncher.enable() : autoLauncher.disable();
|
|
||||||
autoStartTask.then(() => {
|
|
||||||
log.info('config.autostart has been configured:', newConfig.autostart);
|
|
||||||
}).catch((err) => {
|
|
||||||
log.error('error:', err);
|
|
||||||
});
|
|
||||||
WindowManager.setConfig(newConfig);
|
|
||||||
if (authManager) {
|
|
||||||
authManager.handleConfigUpdate(newConfig);
|
|
||||||
}
|
|
||||||
setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge);
|
|
||||||
updateSpellCheckerLocales();
|
|
||||||
}
|
|
||||||
|
|
||||||
ipcMain.emit('update-menu', true, config);
|
|
||||||
ipcMain.emit(EMIT_CONFIGURATION, true, newConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleConfigSynchronize() {
|
|
||||||
if (!config.data) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: send this to server manager
|
|
||||||
WindowManager.setConfig(config.data);
|
|
||||||
setUnreadBadgeSetting(config.data.showUnreadBadge);
|
|
||||||
if (config.data.downloadLocation) {
|
|
||||||
try {
|
|
||||||
app.setPath('downloads', config.data.downloadLocation);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(`There was a problem trying to set the default download path: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (app.isReady()) {
|
|
||||||
WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (process.platform === 'win32' && !didCheckForAddServerModal && typeof config.registryConfigData !== 'undefined') {
|
|
||||||
didCheckForAddServerModal = true;
|
|
||||||
updateServerInfos(config.teams);
|
|
||||||
WindowManager.initializeCurrentServerName();
|
|
||||||
if (config.teams.length === 0) {
|
|
||||||
handleNewServerModal();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleReloadConfig() {
|
|
||||||
config.reload();
|
|
||||||
WindowManager.setConfig(config.data!);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppVersion() {
|
|
||||||
return {
|
|
||||||
name: app.getName(),
|
|
||||||
version: app.getVersion(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDarkModeChange(darkMode: boolean) {
|
|
||||||
refreshTrayImages(config.trayIconTheme);
|
|
||||||
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
|
|
||||||
WindowManager.updateLoadingScreenDarkMode(darkMode);
|
|
||||||
|
|
||||||
ipcMain.emit(EMIT_CONFIGURATION, true, config.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// app event handlers
|
|
||||||
//
|
|
||||||
|
|
||||||
// activate first app instance, subsequent instances will quit themselves
|
|
||||||
function handleAppSecondInstance(event: Event, argv: string[]) {
|
|
||||||
// Protocol handler for win32
|
|
||||||
// argv: An array of the second instance’s (command line / deep linked) arguments
|
|
||||||
const deeplinkingUrl = getDeeplinkingURL(argv);
|
|
||||||
WindowManager.showMainWindow(deeplinkingUrl);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppWindowAllClosed() {
|
|
||||||
// On OS X it is common for applications and their menu bar
|
|
||||||
// to stay active until the user quits explicitly with Cmd + Q
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppBrowserWindowCreated(event: Event, newWindow: BrowserWindow) {
|
|
||||||
// Screen cannot be required before app is ready
|
|
||||||
resizeScreen(newWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppActivate() {
|
|
||||||
WindowManager.showMainWindow();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppBeforeQuit() {
|
|
||||||
// Make sure tray icon gets removed if the user exits via CTRL-Q
|
|
||||||
destroyTray();
|
|
||||||
global.willAppQuit = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
|
|
||||||
log.error(`Exiting App. Reason: ${reason}`);
|
|
||||||
log.info(`Stacktrace:\n${stack}`);
|
|
||||||
handleAppBeforeQuit();
|
|
||||||
app.quit();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSelectCertificate(event: electron.Event, webContents: electron.WebContents, url: string, list: electron.Certificate[], callback: (certificate?: electron.Certificate | undefined) => void) {
|
|
||||||
certificateManager.handleSelectCertificate(event, webContents, url, list, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppCertificateError(event: electron.Event, webContents: electron.WebContents, url: string, error: string, certificate: electron.Certificate, callback: (isTrusted: boolean) => void) {
|
|
||||||
const parsedURL = new URL(url);
|
|
||||||
if (!parsedURL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const origin = parsedURL.origin;
|
|
||||||
if (certificateStore.isExplicitlyUntrusted(origin)) {
|
|
||||||
event.preventDefault();
|
|
||||||
log.warn(`Ignoring previously untrusted certificate for ${origin}`);
|
|
||||||
callback(false);
|
|
||||||
} else if (certificateStore.isTrusted(origin, certificate)) {
|
|
||||||
event.preventDefault();
|
|
||||||
callback(true);
|
|
||||||
} else {
|
|
||||||
// update the callback
|
|
||||||
const errorID = `${origin}:${error}`;
|
|
||||||
|
|
||||||
// if we are already showing that error, don't add more dialogs
|
|
||||||
if (certificateErrorCallbacks.has(errorID)) {
|
|
||||||
log.warn(`Ignoring already shown dialog for ${errorID}`);
|
|
||||||
certificateErrorCallbacks.set(errorID, callback);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const extraDetail = certificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
|
|
||||||
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
|
|
||||||
|
|
||||||
certificateErrorCallbacks.set(errorID, callback);
|
|
||||||
|
|
||||||
// TODO: should we move this to window manager or provide a handler for dialogs?
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (!mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dialog.showMessageBox(mainWindow, {
|
|
||||||
title: 'Certificate Error',
|
|
||||||
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
|
|
||||||
type: 'error',
|
|
||||||
detail,
|
|
||||||
buttons: ['More Details', 'Cancel Connection'],
|
|
||||||
cancelId: 1,
|
|
||||||
}).then(
|
|
||||||
({response}) => {
|
|
||||||
if (response === 0) {
|
|
||||||
return dialog.showMessageBox(mainWindow, {
|
|
||||||
title: 'Certificate Not Trusted',
|
|
||||||
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
|
|
||||||
detail: extraDetail,
|
|
||||||
type: 'error',
|
|
||||||
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
|
|
||||||
cancelId: 1,
|
|
||||||
checkboxChecked: false,
|
|
||||||
checkboxLabel: "Don't ask again",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
return {response, checkboxChecked: false};
|
|
||||||
}).then(
|
|
||||||
({response: responseTwo, checkboxChecked}) => {
|
|
||||||
if (responseTwo === 0) {
|
|
||||||
certificateStore.add(origin, certificate);
|
|
||||||
certificateStore.save();
|
|
||||||
certificateErrorCallbacks.get(errorID)(true);
|
|
||||||
certificateErrorCallbacks.delete(errorID);
|
|
||||||
webContents.loadURL(url);
|
|
||||||
} else {
|
|
||||||
if (checkboxChecked) {
|
|
||||||
certificateStore.add(origin, certificate, true);
|
|
||||||
certificateStore.save();
|
|
||||||
}
|
|
||||||
certificateErrorCallbacks.get(errorID)(false);
|
|
||||||
certificateErrorCallbacks.delete(errorID);
|
|
||||||
}
|
|
||||||
}).catch(
|
|
||||||
(dialogError) => {
|
|
||||||
log.error(`There was an error with the Certificate Error dialog: ${dialogError}`);
|
|
||||||
certificateErrorCallbacks.delete(errorID);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppLogin(event: electron.Event, webContents: electron.WebContents, request: electron.AuthenticationResponseDetails, authInfo: electron.AuthInfo, callback: (username?: string | undefined, password?: string | undefined) => void) {
|
|
||||||
authManager.handleAppLogin(event, webContents, request, authInfo, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppGPUProcessCrashed(event: electron.Event, killed: boolean) {
|
|
||||||
log.error(`The GPU process has crashed (killed = ${killed})`);
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDeepLink(deeplinkingUrl: string) {
|
|
||||||
try {
|
|
||||||
WindowManager.showMainWindow(deeplinkingUrl);
|
|
||||||
} catch (err) {
|
|
||||||
log.error(`There was an error opening the deeplinking url: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAppWillFinishLaunching() {
|
|
||||||
// Protocol handler for osx
|
|
||||||
app.on('open-url', (event, url) => {
|
|
||||||
log.info(`Handling deeplinking url: ${url}`);
|
|
||||||
event.preventDefault();
|
|
||||||
const deeplinkingUrl = getDeeplinkingURL([url]);
|
|
||||||
if (deeplinkingUrl) {
|
|
||||||
if (app.isReady() && deeplinkingUrl) {
|
|
||||||
openDeepLink(deeplinkingUrl);
|
|
||||||
} else {
|
|
||||||
app.once('ready', () => openDeepLink(deeplinkingUrl));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSwitchServer(event: IpcMainEvent, serverName: string) {
|
|
||||||
WindowManager.switchServer(serverName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
|
||||||
WindowManager.switchTab(serverName, tabName);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
|
||||||
const teams = config.teams;
|
|
||||||
teams.forEach((team) => {
|
|
||||||
if (team.name === serverName) {
|
|
||||||
team.tabs.forEach((tab) => {
|
|
||||||
if (tab.name === tabName) {
|
|
||||||
tab.isOpen = false;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name;
|
|
||||||
WindowManager.switchTab(serverName, nextTab);
|
|
||||||
config.set('teams', teams);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
|
||||||
const teams = config.teams;
|
|
||||||
teams.forEach((team) => {
|
|
||||||
if (team.name === serverName) {
|
|
||||||
team.tabs.forEach((tab) => {
|
|
||||||
if (tab.name === tabName) {
|
|
||||||
tab.isOpen = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
WindowManager.switchTab(serverName, tabName);
|
|
||||||
config.set('teams', teams);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleNewServerModal() {
|
|
||||||
const html = getLocalURLString('newServer.html');
|
|
||||||
|
|
||||||
const modalPreload = getLocalPreload('modalPreload.js');
|
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (!mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modalPromise = modalManager.addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow, config.teams.length === 0);
|
|
||||||
if (modalPromise) {
|
|
||||||
modalPromise.then((data) => {
|
|
||||||
const teams = config.teams;
|
|
||||||
const order = teams.length;
|
|
||||||
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order});
|
|
||||||
teams.push(newTeam);
|
|
||||||
config.set('teams', teams);
|
|
||||||
updateServerInfos([newTeam]);
|
|
||||||
WindowManager.switchServer(newTeam.name, true);
|
|
||||||
}).catch((e) => {
|
|
||||||
// e is undefined for user cancellation
|
|
||||||
if (e) {
|
|
||||||
log.error(`there was an error in the new server modal: ${e}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.warn('There is already a new server modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditServerModal(e: IpcMainEvent, name: string) {
|
|
||||||
const html = getLocalURLString('editServer.html');
|
|
||||||
|
|
||||||
const modalPreload = getLocalPreload('modalPreload.js');
|
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (!mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const serverIndex = config.teams.findIndex((team) => team.name === name);
|
|
||||||
if (serverIndex < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modalPromise = modalManager.addModal<Team, Team>('editServer', html, modalPreload, config.teams[serverIndex], mainWindow);
|
|
||||||
if (modalPromise) {
|
|
||||||
modalPromise.then((data) => {
|
|
||||||
const teams = config.teams;
|
|
||||||
teams[serverIndex].name = data.name;
|
|
||||||
teams[serverIndex].url = data.url;
|
|
||||||
config.set('teams', teams);
|
|
||||||
}).catch((e) => {
|
|
||||||
// e is undefined for user cancellation
|
|
||||||
if (e) {
|
|
||||||
log.error(`there was an error in the edit server modal: ${e}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.warn('There is already an edit server modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRemoveServerModal(e: IpcMainEvent, name: string) {
|
|
||||||
const html = getLocalURLString('removeServer.html');
|
|
||||||
|
|
||||||
const modalPreload = getLocalPreload('modalPreload.js');
|
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (!mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const modalPromise = modalManager.addModal<string, boolean>('removeServer', html, modalPreload, name, mainWindow);
|
|
||||||
if (modalPromise) {
|
|
||||||
modalPromise.then((remove) => {
|
|
||||||
if (remove) {
|
|
||||||
const teams = config.teams;
|
|
||||||
const removedTeam = teams.findIndex((team) => team.name === name);
|
|
||||||
if (removedTeam < 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const removedOrder = teams[removedTeam].order;
|
|
||||||
teams.splice(removedTeam, 1);
|
|
||||||
teams.forEach((value) => {
|
|
||||||
if (value.order > removedOrder) {
|
|
||||||
value.order--;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
config.set('teams', teams);
|
|
||||||
}
|
|
||||||
}).catch((e) => {
|
|
||||||
// e is undefined for user cancellation
|
|
||||||
if (e) {
|
|
||||||
log.error(`there was an error in the edit server modal: ${e}`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
log.warn('There is already an edit server modal');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateSpellCheckerLocales() {
|
|
||||||
if (config.data?.spellCheckerLocales.length && app.isReady()) {
|
|
||||||
session.defaultSession.setSpellCheckerLanguages(config.data?.spellCheckerLocales);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function initializeAfterAppReady() {
|
|
||||||
updateServerInfos(config.teams);
|
|
||||||
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
|
||||||
const defaultSession = session.defaultSession;
|
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
|
|
||||||
if (config.spellCheckerURL) {
|
|
||||||
log.error(`There was an error while trying to load the dictionary definitions for ${lang} fromfully the specified url. Please review you have access to the needed files. Url used was ${config.spellCheckerURL}`);
|
|
||||||
} else {
|
|
||||||
log.warn(`There was an error while trying to download the dictionary definitions for ${lang}, spellchecking might not work properly.`);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (config.spellCheckerURL) {
|
|
||||||
const spellCheckerURL = config.spellCheckerURL.endsWith('/') ? config.spellCheckerURL : `${config.spellCheckerURL}/`;
|
|
||||||
log.info(`Configuring spellchecker using download URL: ${spellCheckerURL}`);
|
|
||||||
defaultSession.setSpellCheckerDictionaryDownloadURL(spellCheckerURL);
|
|
||||||
|
|
||||||
defaultSession.on('spellcheck-dictionary-download-success', (event, lang) => {
|
|
||||||
log.info(`Dictionary definitions downloaded successfully for ${lang}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
updateSpellCheckerLocales();
|
|
||||||
}
|
|
||||||
|
|
||||||
const appVersionJson = path.join(app.getPath('userData'), 'app-state.json');
|
|
||||||
appVersion = new AppVersionManager(appVersionJson);
|
|
||||||
if (wasUpdated(appVersion.lastAppVersion)) {
|
|
||||||
clearAppCache();
|
|
||||||
}
|
|
||||||
appVersion.lastAppVersion = app.getVersion();
|
|
||||||
|
|
||||||
if (!global.isDev) {
|
|
||||||
autoLauncher.upgradeAutoLaunch();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (global.isDev) {
|
|
||||||
installExtension(REACT_DEVELOPER_TOOLS).
|
|
||||||
then((name) => log.info(`Added Extension: ${name}`)).
|
|
||||||
catch((err) => log.error('An error occurred: ', err));
|
|
||||||
}
|
|
||||||
|
|
||||||
// Workaround for MM-22193
|
|
||||||
// From this post: https://github.com/electron/electron/issues/19468#issuecomment-549593139
|
|
||||||
// Electron 6 has a bug that affects users on Windows 10 using dark mode, causing the app to hang
|
|
||||||
// This workaround deletes a file that stops that from happening
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const appUserDataPath = app.getPath('userData');
|
|
||||||
const devToolsExtensionsPath = path.join(appUserDataPath, 'DevTools Extensions');
|
|
||||||
try {
|
|
||||||
fs.unlinkSync(devToolsExtensionsPath);
|
|
||||||
} catch (_) {
|
|
||||||
// don't complain if the file doesn't exist
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let deeplinkingURL;
|
|
||||||
|
|
||||||
// Protocol handler for win32
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
const args = process.argv.slice(1);
|
|
||||||
if (Array.isArray(args) && args.length > 0) {
|
|
||||||
deeplinkingURL = getDeeplinkingURL(args);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
initCookieManager(defaultSession);
|
|
||||||
|
|
||||||
WindowManager.showMainWindow(deeplinkingURL);
|
|
||||||
|
|
||||||
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
|
|
||||||
|
|
||||||
// listen for status updates and pass on to renderer
|
|
||||||
userActivityMonitor.on('status', (status) => {
|
|
||||||
WindowManager.sendToMattermostViews(USER_ACTIVITY_UPDATE, status);
|
|
||||||
});
|
|
||||||
|
|
||||||
// start monitoring user activity (needs to be started after the app is ready)
|
|
||||||
userActivityMonitor.startMonitoring();
|
|
||||||
|
|
||||||
if (shouldShowTrayIcon()) {
|
|
||||||
setupTray(config.trayIconTheme);
|
|
||||||
}
|
|
||||||
setupBadge();
|
|
||||||
|
|
||||||
defaultSession.on('will-download', (event, item, webContents) => {
|
|
||||||
const filename = item.getFilename();
|
|
||||||
const fileElements = filename.split('.');
|
|
||||||
const filters = [];
|
|
||||||
if (fileElements.length > 1) {
|
|
||||||
filters.push({
|
|
||||||
name: 'All files',
|
|
||||||
extensions: ['*'],
|
|
||||||
});
|
|
||||||
}
|
|
||||||
item.setSaveDialogOptions({
|
|
||||||
title: filename,
|
|
||||||
defaultPath: path.resolve(config.downloadLocation, filename),
|
|
||||||
filters,
|
|
||||||
});
|
|
||||||
|
|
||||||
item.on('done', (doneEvent, state) => {
|
|
||||||
if (state === 'completed') {
|
|
||||||
displayDownloadCompleted(filename, item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || '');
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
ipcMain.emit('update-menu', true, config);
|
|
||||||
|
|
||||||
ipcMain.emit('update-dict');
|
|
||||||
|
|
||||||
// supported permission types
|
|
||||||
const supportedPermissionTypes = [
|
|
||||||
'media',
|
|
||||||
'geolocation',
|
|
||||||
'notifications',
|
|
||||||
'fullscreen',
|
|
||||||
'openExternal',
|
|
||||||
];
|
|
||||||
|
|
||||||
// handle permission requests
|
|
||||||
// - approve if a supported permission type and the request comes from the renderer or one of the defined servers
|
|
||||||
defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
|
|
||||||
// is the requested permission type supported?
|
|
||||||
if (!supportedPermissionTypes.includes(permission)) {
|
|
||||||
callback(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// is the request coming from the renderer?
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (mainWindow && webContents.id === mainWindow.webContents.id) {
|
|
||||||
callback(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestingURL = webContents.getURL();
|
|
||||||
|
|
||||||
// is the requesting url trusted?
|
|
||||||
callback(urlUtils.isTrustedURL(requestingURL, config.teams));
|
|
||||||
});
|
|
||||||
|
|
||||||
// only check for non-Windows, as with Windows we have to wait for GPO teams
|
|
||||||
if (process.platform !== 'win32' || typeof config.registryConfigData !== 'undefined') {
|
|
||||||
if (config.teams.length === 0) {
|
|
||||||
setTimeout(() => {
|
|
||||||
handleNewServerModal();
|
|
||||||
}, 200);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// ipc communication event handlers
|
|
||||||
//
|
|
||||||
|
|
||||||
function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, data: MentionData) {
|
|
||||||
displayMention(title, body, channel, teamId, url, silent, event.sender, data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateServerInfos(teams: TeamWithTabs[]) {
|
|
||||||
const serverInfos: Array<Promise<RemoteInfo | string | undefined>> = [];
|
|
||||||
teams.forEach((team) => {
|
|
||||||
const serverInfo = new ServerInfo(new MattermostServer(team.name, team.url));
|
|
||||||
serverInfos.push(serverInfo.promise);
|
|
||||||
});
|
|
||||||
Promise.all(serverInfos).then((data: Array<RemoteInfo | string | undefined>) => {
|
|
||||||
const teams = config.teams;
|
|
||||||
teams.forEach((team) => openExtraTabs(data, team));
|
|
||||||
config.set('teams', teams);
|
|
||||||
}).catch((reason: any) => {
|
|
||||||
log.error('Error getting server infos', reason);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function openExtraTabs(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
|
|
||||||
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
|
|
||||||
if (remoteInfo) {
|
|
||||||
team.tabs.forEach((tab) => {
|
|
||||||
if (tab.name !== TAB_MESSAGING && remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0')) {
|
|
||||||
if (tab.name === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && tab.isOpen !== false) {
|
|
||||||
log.info(`opening ${team.name}___${tab.name} on hasPlaybooks`);
|
|
||||||
tab.isOpen = true;
|
|
||||||
}
|
|
||||||
if (tab.name === TAB_FOCALBOARD && remoteInfo.hasFocalboard && tab.isOpen !== false) {
|
|
||||||
log.info(`opening ${team.name}___${tab.name} on hasFocalboard`);
|
|
||||||
tab.isOpen = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleOpenAppMenu() {
|
|
||||||
const windowMenu = Menu.getApplicationMenu();
|
|
||||||
if (!windowMenu) {
|
|
||||||
log.error('No application menu found');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
windowMenu.popup({
|
|
||||||
window: WindowManager.getMainWindow(),
|
|
||||||
x: 18,
|
|
||||||
y: 18,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCloseAppMenu() {
|
|
||||||
WindowManager.focusBrowserView();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateMenuEvent(event: IpcMainEvent, menuConfig: Config) {
|
|
||||||
const aMenu = createAppMenu(menuConfig);
|
|
||||||
Menu.setApplicationMenu(aMenu);
|
|
||||||
aMenu.addListener('menu-will-close', handleCloseAppMenu);
|
|
||||||
|
|
||||||
// set up context menu for tray icon
|
|
||||||
if (shouldShowTrayIcon()) {
|
|
||||||
const tMenu = createTrayMenu(menuConfig.data!);
|
|
||||||
setTrayMenu(tMenu);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleUpdateShortcutMenuEvent(event: IpcMainEvent) {
|
|
||||||
handleUpdateMenuEvent(event, config);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
|
|
||||||
const message = 'Specify the folder where files will download';
|
|
||||||
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation,
|
|
||||||
message,
|
|
||||||
properties:
|
|
||||||
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
|
|
||||||
return result.filePaths[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
//
|
|
||||||
// helper functions
|
|
||||||
//
|
|
||||||
|
|
||||||
function getDeeplinkingURL(args: string[]) {
|
|
||||||
if (Array.isArray(args) && args.length) {
|
|
||||||
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
|
|
||||||
const url = args[args.length - 1];
|
|
||||||
if (url && scheme && url.startsWith(scheme) && urlUtils.isValidURI(url)) {
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function shouldShowTrayIcon() {
|
|
||||||
return config.showTrayIcon || process.platform === 'win32';
|
|
||||||
}
|
|
||||||
|
|
||||||
function wasUpdated(lastAppVersion?: string) {
|
|
||||||
return lastAppVersion !== app.getVersion();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAppCache() {
|
|
||||||
// TODO: clear cache on browserviews, not in the renderer.
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
|
||||||
if (mainWindow) {
|
|
||||||
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
|
||||||
} else {
|
|
||||||
//Wait for mainWindow
|
|
||||||
setTimeout(clearAppCache, 100);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWithinDisplay(state: Rectangle, display: Boundaries) {
|
|
||||||
const startsWithinDisplay = !(state.x > display.maxX || state.y > display.maxY || state.x < display.minX || state.y < display.minY);
|
|
||||||
if (!startsWithinDisplay) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// is half the screen within the display?
|
|
||||||
const midX = state.x + (state.width / 2);
|
|
||||||
const midY = state.y + (state.height / 2);
|
|
||||||
return !(midX > display.maxX || midY > display.maxY);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getValidWindowPosition(state: Rectangle) {
|
|
||||||
// Check if the previous position is out of the viewable area
|
|
||||||
// (e.g. because the screen has been plugged off)
|
|
||||||
const boundaries = Utils.getDisplayBoundaries();
|
|
||||||
const display = boundaries.find((boundary) => {
|
|
||||||
return isWithinDisplay(state, boundary);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (typeof display === 'undefined') {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return {x: state.x, y: state.y};
|
|
||||||
}
|
|
||||||
|
|
||||||
function resizeScreen(browserWindow: BrowserWindow) {
|
|
||||||
function handle() {
|
|
||||||
const position = browserWindow.getPosition();
|
|
||||||
const size = browserWindow.getSize();
|
|
||||||
const validPosition = getValidWindowPosition({
|
|
||||||
x: position[0],
|
|
||||||
y: position[1],
|
|
||||||
width: size[0],
|
|
||||||
height: size[1],
|
|
||||||
});
|
|
||||||
if (typeof validPosition.x !== 'undefined' || typeof validPosition.y !== 'undefined') {
|
|
||||||
browserWindow.setPosition(validPosition.x || 0, validPosition.y || 0);
|
|
||||||
} else {
|
|
||||||
browserWindow.center();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
browserWindow.on('restore', handle);
|
|
||||||
handle();
|
|
||||||
}
|
|
||||||
function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewName: string) {
|
|
||||||
const teams = config.teams;
|
|
||||||
teams.forEach((team) => {
|
|
||||||
if (team.name === serverName) {
|
|
||||||
const viewOrder = team?.tabs.find((tab) => tab.name === viewName)?.order || 0;
|
|
||||||
team.lastActiveTab = viewOrder;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
config.set('teams', teams);
|
|
||||||
config.set('lastActiveTeam', teams.find((team) => team.name === serverName)?.order || 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleGetAvailableSpellCheckerLanguages() {
|
|
||||||
return session.defaultSession.availableSpellCheckerLanguages;
|
|
||||||
}
|
|
@@ -6,7 +6,7 @@
|
|||||||
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
|
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
|
||||||
|
|
||||||
import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import {Config} from 'common/config';
|
||||||
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
|
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
@@ -6,7 +6,7 @@
|
|||||||
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
|
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
|
||||||
import {CombinedConfig} from 'types/config';
|
import {CombinedConfig} from 'types/config';
|
||||||
|
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
export function createTemplate(config: CombinedConfig) {
|
export function createTemplate(config: CombinedConfig) {
|
||||||
const teams = config.teams;
|
const teams = config.teams;
|
||||||
|
@@ -16,6 +16,7 @@ let trayIcon: Tray;
|
|||||||
let lastStatus = 'normal';
|
let lastStatus = 'normal';
|
||||||
let lastMessage = app.name;
|
let lastMessage = app.name;
|
||||||
|
|
||||||
|
/* istanbul ignore next */
|
||||||
export function refreshTrayImages(trayIconTheme: string) {
|
export function refreshTrayImages(trayIconTheme: string) {
|
||||||
const winTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
const winTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||||
|
|
||||||
|
@@ -2,9 +2,22 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
import {TrustedOriginsStore} from 'main/trustedOrigins';
|
||||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||||
|
|
||||||
|
jest.mock('path', () => ({
|
||||||
|
resolve: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
|
jest.mock('electron', () => ({
|
||||||
|
app: {
|
||||||
|
getPath: jest.fn(),
|
||||||
|
},
|
||||||
|
ipcMain: {
|
||||||
|
on: jest.fn(),
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('electron-log', () => ({
|
jest.mock('electron-log', () => ({
|
||||||
error: jest.fn(),
|
error: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
@@ -5,14 +5,18 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import {ipcMain} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {TrustedOrigin, PermissionType} from 'types/trustedOrigin';
|
import {TrustedOrigin, PermissionType} from 'types/trustedOrigin';
|
||||||
|
|
||||||
|
import {UPDATE_PATHS} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
import * as Validator from './Validator';
|
import * as Validator from './Validator';
|
||||||
export default class TrustedOriginsStore {
|
import {trustedOriginsStoreFile} from './constants';
|
||||||
|
|
||||||
|
export class TrustedOriginsStore {
|
||||||
storeFile: string;
|
storeFile: string;
|
||||||
data?: Map<string, TrustedOrigin>;
|
data?: Map<string, TrustedOrigin>;
|
||||||
|
|
||||||
@@ -108,3 +112,13 @@ export default class TrustedOriginsStore {
|
|||||||
return urlPermissions ? urlPermissions[permission] : undefined;
|
return urlPermissions ? urlPermissions[permission] : undefined;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const trustedOriginsStore = new TrustedOriginsStore(trustedOriginsStoreFile);
|
||||||
|
export default trustedOriginsStore;
|
||||||
|
|
||||||
|
ipcMain.on(UPDATE_PATHS, () => {
|
||||||
|
trustedOriginsStore.storeFile = trustedOriginsStoreFile;
|
||||||
|
if (trustedOriginsStore.data) {
|
||||||
|
trustedOriginsStore.load();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
@@ -20,6 +20,7 @@ jest.mock('electron', () => ({
|
|||||||
},
|
},
|
||||||
ipcMain: {
|
ipcMain: {
|
||||||
emit: jest.fn(),
|
emit: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -63,7 +64,7 @@ jest.mock('./webContentEvents', () => ({}));
|
|||||||
|
|
||||||
describe('main/views/viewManager', () => {
|
describe('main/views/viewManager', () => {
|
||||||
describe('loadView', () => {
|
describe('loadView', () => {
|
||||||
const viewManager = new ViewManager({}, {});
|
const viewManager = new ViewManager({});
|
||||||
const onceFn = jest.fn();
|
const onceFn = jest.fn();
|
||||||
const loadFn = jest.fn();
|
const loadFn = jest.fn();
|
||||||
|
|
||||||
@@ -108,7 +109,7 @@ describe('main/views/viewManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reloadViewIfNeeded', () => {
|
describe('reloadViewIfNeeded', () => {
|
||||||
const viewManager = new ViewManager({}, {});
|
const viewManager = new ViewManager({});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
@@ -168,7 +169,7 @@ describe('main/views/viewManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reloadConfiguration', () => {
|
describe('reloadConfiguration', () => {
|
||||||
const viewManager = new ViewManager({}, {});
|
const viewManager = new ViewManager({});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewManager.loadView = jest.fn();
|
viewManager.loadView = jest.fn();
|
||||||
@@ -379,7 +380,8 @@ describe('main/views/viewManager', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
}];
|
}];
|
||||||
const viewManager = new ViewManager({teams}, {});
|
const viewManager = new ViewManager({});
|
||||||
|
viewManager.configServers = teams.concat();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewManager.showByName = jest.fn();
|
viewManager.showByName = jest.fn();
|
||||||
@@ -499,7 +501,7 @@ describe('main/views/viewManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('showByName', () => {
|
describe('showByName', () => {
|
||||||
const viewManager = new ViewManager({}, {});
|
const viewManager = new ViewManager({});
|
||||||
const baseView = {
|
const baseView = {
|
||||||
isReady: jest.fn(),
|
isReady: jest.fn(),
|
||||||
show: jest.fn(),
|
show: jest.fn(),
|
||||||
@@ -591,7 +593,7 @@ describe('main/views/viewManager', () => {
|
|||||||
setTopBrowserView: jest.fn(),
|
setTopBrowserView: jest.fn(),
|
||||||
addBrowserView: jest.fn(),
|
addBrowserView: jest.fn(),
|
||||||
};
|
};
|
||||||
const viewManager = new ViewManager({}, window);
|
const viewManager = new ViewManager(window);
|
||||||
const loadingScreen = {webContents: {send: jest.fn()}};
|
const loadingScreen = {webContents: {send: jest.fn()}};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -623,7 +625,7 @@ describe('main/views/viewManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('handleDeepLink', () => {
|
describe('handleDeepLink', () => {
|
||||||
const viewManager = new ViewManager({}, {});
|
const viewManager = new ViewManager({});
|
||||||
const baseView = {
|
const baseView = {
|
||||||
resetLoadingStatus: jest.fn(),
|
resetLoadingStatus: jest.fn(),
|
||||||
load: jest.fn(),
|
load: jest.fn(),
|
||||||
|
@@ -4,7 +4,7 @@ import log from 'electron-log';
|
|||||||
import {BrowserView, BrowserWindow, dialog, ipcMain} from 'electron';
|
import {BrowserView, BrowserWindow, dialog, ipcMain} from 'electron';
|
||||||
import {BrowserViewConstructorOptions} from 'electron/main';
|
import {BrowserViewConstructorOptions} from 'electron/main';
|
||||||
|
|
||||||
import {CombinedConfig, Tab, TeamWithTabs} from 'types/config';
|
import {Tab, TeamWithTabs} from 'types/config';
|
||||||
|
|
||||||
import {SECOND} from 'common/utils/constants';
|
import {SECOND} from 'common/utils/constants';
|
||||||
import {
|
import {
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
BROWSER_HISTORY_PUSH,
|
BROWSER_HISTORY_PUSH,
|
||||||
UPDATE_LAST_ACTIVE,
|
UPDATE_LAST_ACTIVE,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
import {MattermostServer} from 'common/servers/MattermostServer';
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
@@ -47,10 +48,10 @@ export class ViewManager {
|
|||||||
mainWindow: BrowserWindow;
|
mainWindow: BrowserWindow;
|
||||||
loadingScreen?: BrowserView;
|
loadingScreen?: BrowserView;
|
||||||
|
|
||||||
constructor(config: CombinedConfig, mainWindow: BrowserWindow) {
|
constructor(mainWindow: BrowserWindow) {
|
||||||
this.configServers = config.teams;
|
this.configServers = Config.teams.concat();
|
||||||
this.lastActiveServer = config.lastActiveTeam;
|
this.lastActiveServer = Config.lastActiveTeam;
|
||||||
this.viewOptions = {webPreferences: {spellcheck: config.useSpellChecker}};
|
this.viewOptions = {webPreferences: {spellcheck: Config.useSpellChecker}};
|
||||||
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
this.closedViews = new Map();
|
this.closedViews = new Map();
|
||||||
|
@@ -8,6 +8,7 @@ import path from 'path';
|
|||||||
import {BrowserWindow, screen, app, globalShortcut} from 'electron';
|
import {BrowserWindow, screen, app, globalShortcut} from 'electron';
|
||||||
|
|
||||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants';
|
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
@@ -37,6 +38,11 @@ jest.mock('electron', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({}));
|
||||||
|
jest.mock('common/utils/util', () => ({
|
||||||
|
isVersionGreaterThanOrEqualTo: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('electron-log', () => ({}));
|
jest.mock('electron-log', () => ({}));
|
||||||
|
|
||||||
jest.mock('global', () => ({
|
jest.mock('global', () => ({
|
||||||
@@ -101,7 +107,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set window size using bounds read from file', () => {
|
it('should set window size using bounds read from file', () => {
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
x: 400,
|
x: 400,
|
||||||
y: 300,
|
y: 300,
|
||||||
@@ -114,7 +120,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
|
|
||||||
it('should set default window size when failing to read bounds from file', () => {
|
it('should set default window size when failing to read bounds from file', () => {
|
||||||
fs.readFileSync.mockImplementation(() => 'just a bunch of garbage');
|
fs.readFileSync.mockImplementation(() => 'just a bunch of garbage');
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
width: DEFAULT_WINDOW_WIDTH,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: DEFAULT_WINDOW_HEIGHT,
|
height: DEFAULT_WINDOW_HEIGHT,
|
||||||
@@ -124,7 +130,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
it('should set default window size when bounds are outside the normal screen', () => {
|
it('should set default window size when bounds are outside the normal screen', () => {
|
||||||
fs.readFileSync.mockImplementation(() => '{"x":-400,"y":-300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":-400,"y":-300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
||||||
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
width: DEFAULT_WINDOW_WIDTH,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: DEFAULT_WINDOW_HEIGHT,
|
height: DEFAULT_WINDOW_HEIGHT,
|
||||||
@@ -136,7 +142,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: 'linux',
|
value: 'linux',
|
||||||
});
|
});
|
||||||
createMainWindow({}, {linuxAppIcon: 'linux-icon.png'});
|
createMainWindow({linuxAppIcon: 'linux-icon.png'});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -156,7 +162,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
expect(window.webContents.zoomLevel).toStrictEqual(0);
|
expect(window.webContents.zoomLevel).toStrictEqual(0);
|
||||||
expect(window.maximize).toBeCalled();
|
expect(window.maximize).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -191,7 +197,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -212,7 +218,9 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({minimizeToTray: true}, {});
|
Config.minimizeToTray = true;
|
||||||
|
createMainWindow({});
|
||||||
|
Config.minimizeToTray = false;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -233,7 +241,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -254,7 +262,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -282,7 +290,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -309,7 +317,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -331,7 +339,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
createMainWindow({});
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
|
@@ -3,19 +3,20 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
import path from 'path';
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
import {app, BrowserWindow, BrowserWindowConstructorOptions, globalShortcut, ipcMain, screen} from 'electron';
|
import {app, BrowserWindow, BrowserWindowConstructorOptions, globalShortcut, ipcMain, screen} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {CombinedConfig} from 'types/config';
|
|
||||||
import {SavedWindowState} from 'types/mainWindow';
|
import {SavedWindowState} from 'types/mainWindow';
|
||||||
|
|
||||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, OPEN_TEAMS_DROPDOWN} from 'common/communication';
|
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, OPEN_TEAMS_DROPDOWN} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants';
|
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
|
|
||||||
|
import {boundsInfoPath} from 'main/constants';
|
||||||
|
|
||||||
import * as Validator from '../Validator';
|
import * as Validator from '../Validator';
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
import {getLocalPreload, getLocalURLString} from '../utils';
|
||||||
@@ -42,10 +43,9 @@ function isFramelessWindow() {
|
|||||||
return os.platform() === 'darwin' || (os.platform() === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'));
|
return os.platform() === 'darwin' || (os.platform() === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'));
|
||||||
}
|
}
|
||||||
|
|
||||||
function createMainWindow(config: CombinedConfig, options: {linuxAppIcon: string}) {
|
function createMainWindow(options: {linuxAppIcon: string}) {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const preload = getLocalPreload('mainWindow.js');
|
const preload = getLocalPreload('mainWindow.js');
|
||||||
const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json');
|
|
||||||
let savedWindowState;
|
let savedWindowState;
|
||||||
try {
|
try {
|
||||||
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||||
@@ -64,7 +64,7 @@ function createMainWindow(config: CombinedConfig, options: {linuxAppIcon: string
|
|||||||
|
|
||||||
const {maximized: windowIsMaximized} = savedWindowState;
|
const {maximized: windowIsMaximized} = savedWindowState;
|
||||||
|
|
||||||
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
|
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
||||||
|
|
||||||
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, savedWindowState, {
|
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, savedWindowState, {
|
||||||
title: app.name,
|
title: app.name,
|
||||||
@@ -144,7 +144,7 @@ function createMainWindow(config: CombinedConfig, options: {linuxAppIcon: string
|
|||||||
hideWindow(mainWindow);
|
hideWindow(mainWindow);
|
||||||
break;
|
break;
|
||||||
case 'linux':
|
case 'linux':
|
||||||
if (config.minimizeToTray) {
|
if (Config.minimizeToTray) {
|
||||||
hideWindow(mainWindow);
|
hideWindow(mainWindow);
|
||||||
} else {
|
} else {
|
||||||
mainWindow.minimize();
|
mainWindow.minimize();
|
||||||
|
@@ -3,14 +3,15 @@
|
|||||||
|
|
||||||
import {BrowserWindow} from 'electron';
|
import {BrowserWindow} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import {CombinedConfig} from 'types/config';
|
|
||||||
|
import Config from 'common/config';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
import {getLocalPreload, getLocalURLString} from '../utils';
|
||||||
|
|
||||||
export function createSettingsWindow(mainWindow: BrowserWindow, config: CombinedConfig, withDevTools: boolean) {
|
export function createSettingsWindow(mainWindow: BrowserWindow, withDevTools: boolean) {
|
||||||
const preload = getLocalPreload('mainWindow.js');
|
const preload = getLocalPreload('mainWindow.js');
|
||||||
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
|
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
||||||
const settingsWindow = new BrowserWindow({
|
const settingsWindow = new BrowserWindow({
|
||||||
parent: mainWindow,
|
parent: mainWindow,
|
||||||
title: 'Desktop App Settings',
|
title: 'Desktop App Settings',
|
||||||
|
@@ -6,6 +6,7 @@
|
|||||||
|
|
||||||
import {app, systemPreferences} from 'electron';
|
import {app, systemPreferences} from 'electron';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
@@ -44,6 +45,8 @@ jest.mock('electron-log', () => ({
|
|||||||
info: jest.fn(),
|
info: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({}));
|
||||||
|
|
||||||
jest.mock('common/utils/url', () => ({
|
jest.mock('common/utils/url', () => ({
|
||||||
isTeamUrl: jest.fn(),
|
isTeamUrl: jest.fn(),
|
||||||
isAdminUrl: jest.fn(),
|
isAdminUrl: jest.fn(),
|
||||||
@@ -67,7 +70,7 @@ jest.mock('./settingsWindow', () => ({
|
|||||||
jest.mock('./mainWindow', () => jest.fn());
|
jest.mock('./mainWindow', () => jest.fn());
|
||||||
|
|
||||||
describe('main/windows/windowManager', () => {
|
describe('main/windows/windowManager', () => {
|
||||||
describe('setConfig', () => {
|
describe('handleUpdateConfig', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -76,15 +79,14 @@ describe('main/windows/windowManager', () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should reload config on set', () => {
|
it('should reload config', () => {
|
||||||
windowManager.setConfig({some: 'config item'});
|
windowManager.handleUpdateConfig();
|
||||||
expect(windowManager.viewManager.reloadConfiguration).toHaveBeenCalled();
|
expect(windowManager.viewManager.reloadConfiguration).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showSettingsWindow', () => {
|
describe('showSettingsWindow', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.config = {};
|
|
||||||
windowManager.showMainWindow = jest.fn();
|
windowManager.showMainWindow = jest.fn();
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -119,7 +121,6 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('showMainWindow', () => {
|
describe('showMainWindow', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.config = {};
|
|
||||||
windowManager.viewManager = {
|
windowManager.viewManager = {
|
||||||
handleDeepLink: jest.fn(),
|
handleDeepLink: jest.fn(),
|
||||||
updateMainWindow: jest.fn(),
|
updateMainWindow: jest.fn(),
|
||||||
@@ -327,9 +328,13 @@ describe('main/windows/windowManager', () => {
|
|||||||
flashFrame: jest.fn(),
|
flashFrame: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Config.notifications = {};
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
delete windowManager.config;
|
Config.notifications = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('linux/windows - should not flash frame when config item is not set', () => {
|
it('linux/windows - should not flash frame when config item is not set', () => {
|
||||||
@@ -346,10 +351,8 @@ describe('main/windows/windowManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('linux/windows - should flash frame when config item is set', () => {
|
it('linux/windows - should flash frame when config item is set', () => {
|
||||||
windowManager.config = {
|
Config.notifications = {
|
||||||
notifications: {
|
flashWindow: true,
|
||||||
flashWindow: true,
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const originalPlatform = process.platform;
|
const originalPlatform = process.platform;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
@@ -376,11 +379,9 @@ describe('main/windows/windowManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('mac - should bounce icon when config item is set', () => {
|
it('mac - should bounce icon when config item is set', () => {
|
||||||
windowManager.config = {
|
Config.notifications = {
|
||||||
notifications: {
|
bounceIcon: true,
|
||||||
bounceIcon: true,
|
bounceIconType: 'critical',
|
||||||
bounceIconType: 'critical',
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
const originalPlatform = process.platform;
|
const originalPlatform = process.platform;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
@@ -469,8 +470,15 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('switchServer', () => {
|
describe('switchServer', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.config = {
|
windowManager.viewManager = {
|
||||||
teams: [
|
showByName: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
jest.useFakeTimers();
|
||||||
|
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`);
|
||||||
|
|
||||||
|
Config.teams = [
|
||||||
{
|
{
|
||||||
name: 'server-1',
|
name: 'server-1',
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -513,26 +521,20 @@ describe('main/windows/windowManager', () => {
|
|||||||
],
|
],
|
||||||
lastActiveTab: 2,
|
lastActiveTab: 2,
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
|
||||||
windowManager.viewManager = {
|
|
||||||
showByName: jest.fn(),
|
|
||||||
};
|
|
||||||
const map = windowManager.config.teams.reduce((arr, item) => {
|
|
||||||
item.tabs.forEach((tab) => {
|
|
||||||
arr.push([`${item.name}_${tab.name}`, {}]);
|
|
||||||
});
|
|
||||||
return arr;
|
|
||||||
}, []);
|
|
||||||
windowManager.viewManager.views = new Map(map);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
const map = Config.teams.reduce((arr, item) => {
|
||||||
jest.useFakeTimers();
|
item.tabs.forEach((tab) => {
|
||||||
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`);
|
arr.push([`${item.name}_${tab.name}`, {}]);
|
||||||
|
});
|
||||||
|
return arr;
|
||||||
|
}, []);
|
||||||
|
windowManager.viewManager.views = new Map(map);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
Config.teams = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if cannot find the server', () => {
|
it('should do nothing if cannot find the server', () => {
|
||||||
@@ -625,8 +627,13 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('selectTab', () => {
|
describe('selectTab', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.config = {
|
windowManager.viewManager = {
|
||||||
teams: [
|
getCurrentView: jest.fn(),
|
||||||
|
};
|
||||||
|
windowManager.switchTab = jest.fn();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Config.teams = [
|
||||||
{
|
{
|
||||||
name: 'server-1',
|
name: 'server-1',
|
||||||
order: 1,
|
order: 1,
|
||||||
@@ -648,15 +655,12 @@ describe('main/windows/windowManager', () => {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
],
|
];
|
||||||
};
|
});
|
||||||
windowManager.viewManager = {
|
|
||||||
getCurrentView: jest.fn(),
|
|
||||||
};
|
|
||||||
windowManager.switchTab = jest.fn();
|
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
Config.teams = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should select next server when open', () => {
|
it('should select next server when open', () => {
|
||||||
@@ -696,7 +700,6 @@ describe('main/windows/windowManager', () => {
|
|||||||
type: 'tab-2',
|
type: 'tab-2',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
windowManager.selectTab((order) => order + 1);
|
windowManager.selectTab((order) => order + 1);
|
||||||
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3');
|
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3');
|
||||||
});
|
});
|
||||||
@@ -704,32 +707,6 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('handleBrowserHistoryPush', () => {
|
describe('handleBrowserHistoryPush', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.config = {
|
|
||||||
teams: [
|
|
||||||
{
|
|
||||||
name: 'server-1',
|
|
||||||
url: 'http://server-1.com',
|
|
||||||
order: 0,
|
|
||||||
tabs: [
|
|
||||||
{
|
|
||||||
name: 'tab-messaging',
|
|
||||||
order: 0,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'other_type_1',
|
|
||||||
order: 2,
|
|
||||||
isOpen: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'other_type_2',
|
|
||||||
order: 1,
|
|
||||||
isOpen: false,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
const view1 = {
|
const view1 = {
|
||||||
name: 'server-1_tab-messaging',
|
name: 'server-1_tab-messaging',
|
||||||
isLoggedIn: true,
|
isLoggedIn: true,
|
||||||
@@ -783,8 +760,36 @@ describe('main/windows/windowManager', () => {
|
|||||||
showByName: jest.fn(),
|
showByName: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
Config.teams = [
|
||||||
|
{
|
||||||
|
name: 'server-1',
|
||||||
|
url: 'http://server-1.com',
|
||||||
|
order: 0,
|
||||||
|
tabs: [
|
||||||
|
{
|
||||||
|
name: 'tab-messaging',
|
||||||
|
order: 0,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'other_type_1',
|
||||||
|
order: 2,
|
||||||
|
isOpen: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'other_type_2',
|
||||||
|
order: 1,
|
||||||
|
isOpen: false,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
|
Config.teams = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open closed view if pushing to it', () => {
|
it('should open closed view if pushing to it', () => {
|
||||||
|
@@ -6,8 +6,6 @@ import path from 'path';
|
|||||||
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {CombinedConfig} from 'types/config';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
MAXIMIZE_CHANGE,
|
MAXIMIZE_CHANGE,
|
||||||
HISTORY,
|
HISTORY,
|
||||||
@@ -24,7 +22,7 @@ import {
|
|||||||
APP_LOGGED_OUT,
|
APP_LOGGED_OUT,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
import Config from 'common/config';
|
||||||
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {getAdjustedWindowBoundaries} from '../utils';
|
import {getAdjustedWindowBoundaries} from '../utils';
|
||||||
@@ -44,7 +42,6 @@ export class WindowManager {
|
|||||||
|
|
||||||
mainWindow?: BrowserWindow;
|
mainWindow?: BrowserWindow;
|
||||||
settingsWindow?: BrowserWindow;
|
settingsWindow?: BrowserWindow;
|
||||||
config?: CombinedConfig;
|
|
||||||
viewManager?: ViewManager;
|
viewManager?: ViewManager;
|
||||||
teamDropdown?: TeamDropdownView;
|
teamDropdown?: TeamDropdownView;
|
||||||
currentServerName?: string;
|
currentServerName?: string;
|
||||||
@@ -64,12 +61,9 @@ export class WindowManager {
|
|||||||
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
|
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
|
||||||
}
|
}
|
||||||
|
|
||||||
setConfig = (data: CombinedConfig) => {
|
handleUpdateConfig = () => {
|
||||||
if (data) {
|
if (this.viewManager) {
|
||||||
this.config = data;
|
this.viewManager.reloadConfiguration(Config.teams || []);
|
||||||
}
|
|
||||||
if (this.viewManager && this.config) {
|
|
||||||
this.viewManager.reloadConfiguration(this.config.teams || []);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -82,10 +76,7 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
|
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
|
||||||
|
|
||||||
if (!this.config) {
|
this.settingsWindow = createSettingsWindow(this.mainWindow!, withDevTools);
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.settingsWindow = createSettingsWindow(this.mainWindow!, this.config, withDevTools);
|
|
||||||
this.settingsWindow.on('closed', () => {
|
this.settingsWindow.on('closed', () => {
|
||||||
delete this.settingsWindow;
|
delete this.settingsWindow;
|
||||||
});
|
});
|
||||||
@@ -100,10 +91,7 @@ export class WindowManager {
|
|||||||
this.mainWindow.show();
|
this.mainWindow.show();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (!this.config) {
|
this.mainWindow = createMainWindow({
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.mainWindow = createMainWindow(this.config, {
|
|
||||||
linuxAppIcon: path.join(this.assetsDir, 'linux', 'app_icon.png'),
|
linuxAppIcon: path.join(this.assetsDir, 'linux', 'app_icon.png'),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -119,9 +107,8 @@ export class WindowManager {
|
|||||||
delete this.mainWindow;
|
delete this.mainWindow;
|
||||||
});
|
});
|
||||||
this.mainWindow.on('unresponsive', () => {
|
this.mainWindow.on('unresponsive', () => {
|
||||||
const criticalErrorHandler = new CriticalErrorHandler();
|
CriticalErrorHandler.setMainWindow(this.mainWindow!);
|
||||||
criticalErrorHandler.setMainWindow(this.mainWindow!);
|
CriticalErrorHandler.windowUnresponsiveHandler();
|
||||||
criticalErrorHandler.windowUnresponsiveHandler();
|
|
||||||
});
|
});
|
||||||
this.mainWindow.on('maximize', this.handleMaximizeMainWindow);
|
this.mainWindow.on('maximize', this.handleMaximizeMainWindow);
|
||||||
this.mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow);
|
this.mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow);
|
||||||
@@ -138,7 +125,7 @@ export class WindowManager {
|
|||||||
this.viewManager.updateMainWindow(this.mainWindow);
|
this.viewManager.updateMainWindow(this.mainWindow);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.teamDropdown = new TeamDropdownView(this.mainWindow, this.config.teams, this.config.darkMode, this.config.enableServerManagement);
|
this.teamDropdown = new TeamDropdownView(this.mainWindow, Config.teams, Config.darkMode, Config.enableServerManagement);
|
||||||
}
|
}
|
||||||
this.initializeViewManager();
|
this.initializeViewManager();
|
||||||
|
|
||||||
@@ -251,7 +238,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
flashFrame = (flash: boolean) => {
|
flashFrame = (flash: boolean) => {
|
||||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
if (this.config?.notifications.flashWindow) {
|
if (Config.notifications.flashWindow) {
|
||||||
this.mainWindow?.flashFrame(flash);
|
this.mainWindow?.flashFrame(flash);
|
||||||
if (this.settingsWindow) {
|
if (this.settingsWindow) {
|
||||||
// main might be hidden behind the settings
|
// main might be hidden behind the settings
|
||||||
@@ -259,8 +246,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (process.platform === 'darwin' && this.config?.notifications.bounceIcon) {
|
if (process.platform === 'darwin' && Config.notifications.bounceIcon) {
|
||||||
app.dock.bounce(this.config?.notifications.bounceIconType);
|
app.dock.bounce(Config.notifications.bounceIconType);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -358,8 +345,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeViewManager = () => {
|
initializeViewManager = () => {
|
||||||
if (!this.viewManager && this.config && this.mainWindow) {
|
if (!this.viewManager && Config && this.mainWindow) {
|
||||||
this.viewManager = new ViewManager(this.config, this.mainWindow);
|
this.viewManager = new ViewManager(this.mainWindow);
|
||||||
this.viewManager.load();
|
this.viewManager.load();
|
||||||
this.viewManager.showInitial();
|
this.viewManager.showInitial();
|
||||||
this.initializeCurrentServerName();
|
this.initializeCurrentServerName();
|
||||||
@@ -367,14 +354,14 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeCurrentServerName = () => {
|
initializeCurrentServerName = () => {
|
||||||
if (this.config && !this.currentServerName) {
|
if (!this.currentServerName) {
|
||||||
this.currentServerName = (this.config.teams.find((team) => team.order === this.config?.lastActiveTeam) || this.config.teams.find((team) => team.order === 0))?.name;
|
this.currentServerName = (Config.teams.find((team) => team.order === Config.lastActiveTeam) || Config.teams.find((team) => team.order === 0))?.name;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
switchServer = (serverName: string, waitForViewToExist = false) => {
|
switchServer = (serverName: string, waitForViewToExist = false) => {
|
||||||
this.showMainWindow();
|
this.showMainWindow();
|
||||||
const server = this.config?.teams.find((team) => team.name === serverName);
|
const server = Config.teams.find((team) => team.name === serverName);
|
||||||
if (!server) {
|
if (!server) {
|
||||||
log.error('Cannot find server in config');
|
log.error('Cannot find server in config');
|
||||||
return;
|
return;
|
||||||
@@ -428,7 +415,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
handleLoadingScreenDataRequest = () => {
|
handleLoadingScreenDataRequest = () => {
|
||||||
return {
|
return {
|
||||||
darkMode: this.config?.darkMode || false,
|
darkMode: Config.darkMode || false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -529,7 +516,7 @@ export class WindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentTeamTabs = this.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
const currentTeamTabs = Config.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||||
const filteredTabs = currentTeamTabs?.filter((tab) => tab.isOpen);
|
const filteredTabs = currentTeamTabs?.filter((tab) => tab.isOpen);
|
||||||
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
||||||
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
||||||
@@ -549,12 +536,12 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
handleGetDarkMode = () => {
|
handleGetDarkMode = () => {
|
||||||
return this.config?.darkMode;
|
return Config.darkMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => {
|
handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => {
|
||||||
const currentView = this.viewManager?.views.get(viewName);
|
const currentView = this.viewManager?.views.get(viewName);
|
||||||
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${pathName}`, this.config!.teams)?.name || viewName;
|
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${pathName}`, Config.teams)?.name || viewName;
|
||||||
if (this.viewManager?.closedViews.has(redirectedViewName)) {
|
if (this.viewManager?.closedViews.has(redirectedViewName)) {
|
||||||
this.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${pathName}`);
|
this.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${pathName}`);
|
||||||
}
|
}
|
||||||
|
@@ -16,7 +16,7 @@ const base = require('./webpack.config.base');
|
|||||||
|
|
||||||
module.exports = merge(base, {
|
module.exports = merge(base, {
|
||||||
entry: {
|
entry: {
|
||||||
index: './src/main/main.ts',
|
index: './src/main/app/index.ts',
|
||||||
mainWindow: './src/main/preload/mainWindow.js',
|
mainWindow: './src/main/preload/mainWindow.js',
|
||||||
dropdown: './src/main/preload/dropdown.js',
|
dropdown: './src/main/preload/dropdown.js',
|
||||||
preload: './src/main/preload/mattermost.js',
|
preload: './src/main/preload/mattermost.js',
|
||||||
|
Reference in New Issue
Block a user