Refactor config, move ipc calls to app module, some cleanup (#2669)

This commit is contained in:
Devin Binnie
2023-04-06 11:24:57 -04:00
committed by GitHub
parent 88eb2e2c70
commit 741087cb55
29 changed files with 551 additions and 638 deletions

View File

@@ -4,7 +4,7 @@
import Joi from 'joi'; import Joi from 'joi';
import {Args} from 'types/args'; import {Args} from 'types/args';
import {AnyConfig, ConfigV0, ConfigV1, ConfigV2, ConfigV3, TeamWithTabs} from 'types/config'; import {AnyConfig, ConfigV0, ConfigV1, ConfigV2, ConfigV3, ConfigServer} from 'types/config';
import {DownloadedItems} from 'types/downloads'; import {DownloadedItems} from 'types/downloads';
import {SavedWindowState} from 'types/mainWindow'; import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState'; import {AppState} from 'types/appState';
@@ -213,7 +213,7 @@ function cleanTeam<T extends {name: string; url: string}>(team: T) {
}; };
} }
function cleanTeamWithTabs(team: TeamWithTabs) { function cleanTeamWithTabs(team: ConfigServer) {
return { return {
...cleanTeam(team), ...cleanTeam(team),
tabs: team.tabs.map((tab) => { tabs: team.tabs.map((tab) => {

View File

@@ -67,7 +67,6 @@ describe('common/config/RegistryConfig', () => {
expect(registryConfig.data.teams).toContainEqual({ expect(registryConfig.data.teams).toContainEqual({
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
order: 0,
}); });
expect(registryConfig.data.enableAutoUpdater).toBe(true); expect(registryConfig.data.enableAutoUpdater).toBe(true);
expect(registryConfig.data.enableServerManagement).toBe(true); expect(registryConfig.data.enableServerManagement).toBe(true);

View File

@@ -6,7 +6,7 @@ import {EventEmitter} from 'events';
import WindowsRegistry from 'winreg'; import WindowsRegistry from 'winreg';
import WindowsRegistryUTF8 from 'winreg-utf8'; import WindowsRegistryUTF8 from 'winreg-utf8';
import {RegistryConfig as RegistryConfigType, FullTeam} from 'types/config'; import {RegistryConfig as RegistryConfigType, Team} from 'types/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@@ -78,12 +78,11 @@ export default class RegistryConfig extends EventEmitter {
*/ */
async getServersListFromRegistry() { async getServersListFromRegistry() {
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`); const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
return defaultServers.flat(2).reduce((servers: FullTeam[], server, index) => { return defaultServers.flat(2).reduce((servers: Team[], server) => {
if (server) { if (server) {
servers.push({ servers.push({
name: (server as WindowsRegistry.RegistryItem).name, name: (server as WindowsRegistry.RegistryItem).name,
url: (server as WindowsRegistry.RegistryItem).value, url: (server as WindowsRegistry.RegistryItem).value,
order: index,
}); });
} }
return servers; return servers;

View File

@@ -1,22 +1,19 @@
// 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 fs from 'fs';
import {Config} from 'common/config'; import {Config} from 'common/config';
const configPath = '/fake/config/path'; const configPath = '/fake/config/path';
const appName = 'app-name';
const appPath = '/my/app/path';
jest.mock('electron', () => ({ jest.mock('fs', () => ({
app: { readFileSync: jest.fn(),
name: 'Mattermost', writeFileSync: jest.fn(),
getPath: jest.fn(), existsSync: jest.fn(),
getAppPath: () => '/path/to/app', mkdirSync: jest.fn(),
},
ipcMain: {
on: jest.fn(),
},
nativeTheme: {
shouldUseDarkColors: false,
},
})); }));
jest.mock('common/Validator', () => ({ jest.mock('common/Validator', () => ({
@@ -28,7 +25,7 @@ jest.mock('common/Validator', () => ({
})); }));
jest.mock('common/tabs/TabView', () => ({ jest.mock('common/tabs/TabView', () => ({
getDefaultTeamWithTabsFromTeam: (value) => ({ getDefaultConfigTeamFromTeam: (value) => ({
...value, ...value,
tabs: [ tabs: [
{ {
@@ -99,15 +96,18 @@ jest.mock('common/config/RegistryConfig', () => {
describe('common/config', () => { describe('common/config', () => {
it('should load buildConfig', () => { it('should load buildConfig', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs); expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs);
}); });
describe('loadRegistry', () => { describe('loadRegistry', () => {
it('should load the registry items and reload the config', () => { it('should load the registry items and reload the config', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn(); config.reload = jest.fn();
config.loadRegistry({teams: [registryTeam]}); config.init(configPath, appName, appPath);
config.onLoadRegistry({teams: [registryTeam]});
expect(config.reload).toHaveBeenCalled(); expect(config.reload).toHaveBeenCalled();
expect(config.predefinedTeams).toContainEqual({ expect(config.predefinedTeams).toContainEqual({
...registryTeam, ...registryTeam,
@@ -125,7 +125,8 @@ describe('common/config', () => {
describe('reload', () => { describe('reload', () => {
it('should emit update event', () => { it('should emit update event', () => {
const config = new Config(configPath); const config = new Config();
config.init(configPath, appName, appPath);
config.loadDefaultConfigData = jest.fn(); config.loadDefaultConfigData = jest.fn();
config.loadBuildConfigData = jest.fn(); config.loadBuildConfigData = jest.fn();
config.loadLocalConfigFile = jest.fn(); config.loadLocalConfigFile = jest.fn();
@@ -134,6 +135,7 @@ describe('common/config', () => {
config.combinedData = {test: 'test'}; config.combinedData = {test: 'test'};
}); });
config.emit = jest.fn(); config.emit = jest.fn();
fs.existsSync.mockReturnValue(true);
config.reload(); config.reload();
expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'}); expect(config.emit).toHaveBeenNthCalledWith(1, 'update', {test: 'test'});
@@ -142,7 +144,9 @@ describe('common/config', () => {
describe('set', () => { describe('set', () => {
it('should set an arbitrary value and save to local config data', () => { it('should set an arbitrary value and save to local config data', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {}; config.localConfigData = {};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
@@ -155,24 +159,46 @@ describe('common/config', () => {
expect(config.saveLocalConfigData).toHaveBeenCalled(); expect(config.saveLocalConfigData).toHaveBeenCalled();
}); });
it('should set teams without including predefined', () => { it('should not allow teams to be set using this method', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {teams: [team]};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData};
});
config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}]);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team);
});
});
describe('setServers', () => {
it('should set only local servers', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {}; config.localConfigData = {};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
}); });
config.saveLocalConfigData = jest.fn(); config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}, team]); config.setServers([{...buildTeamWithTabs, name: 'build-team-2'}, team], 0);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); expect(config.localConfigData.teams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team); expect(config.localConfigData.lastActiveTeam).toBe(0);
expect(config.predefinedTeams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); expect(config.regenerateCombinedConfigData).toHaveBeenCalled();
expect(config.saveLocalConfigData).toHaveBeenCalled();
}); });
}); });
describe('saveLocalConfigData', () => { describe('saveLocalConfigData', () => {
it('should emit update event on save', () => { it('should emit update event on save', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'}; config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -185,7 +211,9 @@ describe('common/config', () => {
}); });
it('should emit error when fs.writeSync throws an error', () => { it('should emit error when fs.writeSync throws an error', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'}; config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -198,7 +226,9 @@ describe('common/config', () => {
}); });
it('should emit error when writeFile throws an error', () => { it('should emit error when writeFile throws an error', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'}; config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation(() => { config.writeFile = jest.fn().mockImplementation(() => {
@@ -212,7 +242,9 @@ describe('common/config', () => {
it('should retry when file is locked', () => { it('should retry when file is locked', () => {
const testFunc = jest.fn(); const testFunc = jest.fn();
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {test: 'test'}; config.localConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => { config.writeFile = jest.fn().mockImplementation((configFilePath, data, callback) => {
@@ -228,37 +260,41 @@ describe('common/config', () => {
describe('loadLocalConfigFile', () => { describe('loadLocalConfigFile', () => {
it('should use defaults if readFileSync fails', () => { it('should use defaults if readFileSync fails', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {test: 'test'}; config.defaultConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.readFileSync = jest.fn().mockImplementation(() => { fs.existsSync.mockReturnValue(true);
fs.readFileSync.mockImplementation(() => {
throw new Error('Error message'); throw new Error('Error message');
}); });
config.writeFileSync = jest.fn(); config.writeFile = jest.fn();
const configData = config.loadLocalConfigFile(); const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({test: 'test'}); expect(configData).toStrictEqual({test: 'test'});
}); });
it('should use defaults if validation fails', () => { it('should use defaults if validation fails', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {test: 'test'}; config.defaultConfigData = {test: 'test'};
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
config.readFileSync = jest.fn().mockImplementation(() => { fs.existsSync.mockReturnValue(true);
return {version: -1}; fs.readFileSync.mockReturnValue('{"version": -1}');
}); config.writeFile = jest.fn();
config.writeFileSync = jest.fn();
const configData = config.loadLocalConfigFile(); const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({test: 'test'}); expect(configData).toStrictEqual({test: 'test'});
}); });
it('should return config data if valid', () => { it('should return config data if valid', () => {
const config = new Config(configPath); const config = new Config();
config.readFileSync = jest.fn().mockImplementation(() => { config.init(configPath, appName, appPath);
return {version: 3}; fs.existsSync.mockReturnValue(true);
}); fs.readFileSync.mockReturnValue('{"version": 3}');
config.writeFileSync = jest.fn(); config.writeFile = jest.fn();
const configData = config.loadLocalConfigFile(); const configData = config.loadLocalConfigFile();
expect(configData).toStrictEqual({version: 3}); expect(configData).toStrictEqual({version: 3});
@@ -267,7 +303,9 @@ describe('common/config', () => {
describe('checkForConfigUpdates', () => { describe('checkForConfigUpdates', () => {
it('should upgrade to latest version', () => { it('should upgrade to latest version', () => {
const config = new Config(configPath); const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.defaultConfigData = {version: 10}; config.defaultConfigData = {version: 10};
config.writeFileSync = jest.fn(); config.writeFileSync = jest.fn();
@@ -276,111 +314,67 @@ describe('common/config', () => {
}); });
}); });
describe('regenerateCombinedConfigData', () => { // TODO: Re-enable when we migrate to ServerManager fully
it('should combine config from all sources', () => { // describe('regenerateCombinedConfigData', () => {
const config = new Config(configPath); // it('should combine config from all sources', () => {
config.predefinedTeams = []; // const config = new Config();
config.useNativeWindow = false; // config.reload = jest.fn();
config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'}; // config.init(configPath, appName, appPath);
config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'}; // config.useNativeWindow = false;
config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'}; // config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'};
config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'}; // config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'};
// config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'};
// config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'};
config.regenerateCombinedConfigData(); // config.regenerateCombinedConfigData();
config.combinedData.darkMode = false; // config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({ // expect(config.combinedData).toStrictEqual({
teams: [], // appName: 'app-name',
registryTeams: [], // useNativeWindow: false,
appName: 'Mattermost', // darkMode: false,
useNativeWindow: false, // otherBuildSetting: 'registry',
darkMode: false, // registrySetting: 'registry',
otherBuildSetting: 'registry', // otherLocalSetting: 'build',
registrySetting: 'registry', // buildSetting: 'build',
otherLocalSetting: 'build', // otherDefaultSetting: 'local',
buildSetting: 'build', // localSetting: 'local',
otherDefaultSetting: 'local', // defaultSetting: 'default',
localSetting: 'local', // });
defaultSetting: 'default', // });
});
});
it('should combine teams from all sources and filter duplicates', () => { // it('should not include any teams in the combined config', () => {
const config = new Config(configPath); // const config = new Config();
config.defaultConfigData = {}; // config.reload = jest.fn();
config.localConfigData = {}; // config.init(configPath, appName, appPath);
config.buildConfigData = {enableServerManagement: true}; // config.defaultConfigData = {};
config.registryConfigData = {}; // config.localConfigData = {};
config.predefinedTeams = [team, team]; // config.buildConfigData = {enableServerManagement: true};
config.useNativeWindow = false; // config.registryConfigData = {};
config.localConfigData = {teams: [ // config.predefinedTeams.push(team, team);
team, // config.useNativeWindow = false;
{ // config.localConfigData = {teams: [
...team, // team,
name: 'local-team-2', // {
url: 'http://local-team-2.com', // ...team,
}, // name: 'local-team-2',
{ // url: 'http://local-team-2.com',
...team, // },
name: 'local-team-1', // {
order: 1, // ...team,
url: 'http://local-team-1.com', // name: 'local-team-1',
}, // order: 1,
]}; // url: 'http://local-team-1.com',
// },
// ]};
config.regenerateCombinedConfigData(); // config.regenerateCombinedConfigData();
config.combinedData.darkMode = false; // config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({ // expect(config.combinedData).toStrictEqual({
teams: [ // appName: 'app-name',
team, // useNativeWindow: false,
{ // darkMode: false,
...team, // enableServerManagement: true,
name: 'local-team-2', // });
order: 1, // });
url: 'http://local-team-2.com', // });
},
{
...team,
name: 'local-team-1',
order: 2,
url: 'http://local-team-1.com',
},
],
registryTeams: [],
appName: 'Mattermost',
useNativeWindow: false,
darkMode: false,
enableServerManagement: true,
});
});
it('should not include local teams if enableServerManagement is false', () => {
const config = new Config(configPath);
config.defaultConfigData = {};
config.localConfigData = {};
config.buildConfigData = {enableServerManagement: false};
config.registryConfigData = {};
config.predefinedTeams = [team, team];
config.useNativeWindow = false;
config.localConfigData = {teams: [
team,
{
...team,
name: 'local-team-1',
order: 1,
url: 'http://local-team-1.com',
},
]};
config.regenerateCombinedConfigData();
config.combinedData.darkMode = false;
expect(config.combinedData).toStrictEqual({
teams: [team],
registryTeams: [],
appName: 'Mattermost',
useNativeWindow: false,
darkMode: false,
enableServerManagement: false,
});
});
});
}); });

View File

@@ -7,7 +7,6 @@ import os from 'os';
import path from 'path'; import path from 'path';
import {EventEmitter} from 'events'; import {EventEmitter} from 'events';
import {ipcMain, nativeTheme, app} from 'electron';
import { import {
AnyConfig, AnyConfig,
@@ -15,18 +14,13 @@ import {
CombinedConfig, CombinedConfig,
ConfigServer, ConfigServer,
Config as ConfigType, Config as ConfigType,
LocalConfiguration,
RegistryConfig as RegistryConfigType, RegistryConfig as RegistryConfigType,
TeamWithTabs,
} from 'types/config'; } from 'types/config';
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION, UPDATE_PATHS} from 'common/communication';
import * as Validator from 'common/Validator';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import Utils from 'common/utils/util'; import Utils, {copy} from 'common/utils/util';
import * as Validator from 'common/Validator';
import {configPath} from 'main/constants';
import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences'; import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences';
import upgradeConfigData from './upgradePreferences'; import upgradeConfigData from './upgradePreferences';
@@ -36,61 +30,28 @@ import migrateConfigItems from './migrationPreferences';
const log = new Logger('Config'); const log = new Logger('Config');
/**
* Handles loading and merging all sources of configuration as well as saving user provided config
*/
function checkWriteableApp() {
if (process.platform === 'win32') {
try {
fs.accessSync(path.join(path.dirname(app.getAppPath()), '../../'), fs.constants.W_OK);
// check to make sure that app-update.yml exists
if (!fs.existsSync(path.join(process.resourcesPath, 'app-update.yml'))) {
log.warn('app-update.yml does not exist, disabling auto-updates');
return false;
}
} catch (error) {
log.info(`${app.getAppPath()}: ${error}`);
log.warn('autoupgrade disabled');
return false;
}
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return __CAN_UPGRADE__; // prevent showing the option if the path is not writeable, like in a managed environment.
}
// temporarily disabling auto updater for macOS due to security issues
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return process.platform !== 'darwin' && __CAN_UPGRADE__;
}
export class Config extends EventEmitter { export class Config extends EventEmitter {
configFilePath: string; private configFilePath?: string;
private appName?: string;
private appPath?: string;
registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
private predefinedServers: ConfigServer[];
private useNativeWindow: boolean;
combinedData?: CombinedConfig; private combinedData?: CombinedConfig;
registryConfigData?: Partial<RegistryConfigType>; private localConfigData?: ConfigType;
defaultConfigData?: ConfigType; private registryConfigData?: Partial<RegistryConfigType>;
buildConfigData?: BuildConfig; private defaultConfigData?: ConfigType;
localConfigData?: ConfigType; private buildConfigData?: BuildConfig;
useNativeWindow: boolean; private canUpgradeValue?: boolean;
canUpgradeValue?: boolean
predefinedTeams: TeamWithTabs[]; constructor() {
constructor(configFilePath: string) {
super(); super();
this.configFilePath = configFilePath;
this.canUpgradeValue = checkWriteableApp();
this.registryConfig = new RegistryConfig(); this.registryConfig = new RegistryConfig();
this.predefinedTeams = []; this.predefinedServers = [];
if (buildConfig.defaultTeams) { if (buildConfig.defaultTeams) {
this.predefinedTeams.push(...buildConfig.defaultTeams.map((team) => getDefaultTeamWithTabsFromTeam(team))); this.predefinedServers.push(...buildConfig.defaultTeams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
} }
try { try {
this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'); this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2');
@@ -99,45 +60,30 @@ export class Config extends EventEmitter {
} }
} }
// separating constructor from init so main can setup event listeners init = (configFilePath: string, appName: string, appPath: string) => {
init = (): void => { this.configFilePath = configFilePath;
this.appName = appName;
this.appPath = appPath;
this.canUpgradeValue = this.checkWriteableApp();
this.reload(); this.reload();
ipcMain.handle(GET_CONFIGURATION, this.handleGetConfiguration);
ipcMain.handle(GET_LOCAL_CONFIGURATION, this.handleGetLocalConfiguration);
ipcMain.handle(UPDATE_TEAMS, this.handleUpdateTeams);
ipcMain.on(UPDATE_CONFIGURATION, this.updateConfiguration);
if (process.platform === 'darwin' || process.platform === 'win32') {
nativeTheme.on('updated', this.handleUpdateTheme);
}
} }
initRegistry = () => { initRegistry = () => {
if (process.platform !== 'win32') {
return Promise.resolve();
}
return new Promise<void>((resolve) => { return new Promise<void>((resolve) => {
this.registryConfig = new RegistryConfig(); this.registryConfig = new RegistryConfig();
this.registryConfig.once(REGISTRY_READ_EVENT, (data) => { this.registryConfig.once(REGISTRY_READ_EVENT, (data) => {
this.loadRegistry(data); this.onLoadRegistry(data);
resolve(); resolve();
}); });
this.registryConfig.init(); this.registryConfig.init();
}); });
} }
/**
* Gets the teams from registry into the config object and reload
*
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
*/
loadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.verbose('Config.loadRegistry', {registryData});
this.registryConfigData = registryData;
if (this.registryConfigData.teams) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team) => getDefaultTeamWithTabsFromTeam(team)));
}
this.reload();
}
/** /**
* Reload all sources of config data * Reload all sources of config data
* *
@@ -146,15 +92,21 @@ export class Config extends EventEmitter {
* @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes * @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes
*/ */
reload = (): void => { reload = (): void => {
this.defaultConfigData = this.loadDefaultConfigData(); this.defaultConfigData = copy(defaultPreferences);
this.buildConfigData = this.loadBuildConfigData(); this.buildConfigData = copy(buildConfig);
const loadedConfig = this.loadLocalConfigFile(); const loadedConfig = this.loadLocalConfigFile();
this.localConfigData = this.checkForConfigUpdates(loadedConfig); this.localConfigData = this.checkForConfigUpdates(loadedConfig);
this.regenerateCombinedConfigData(); this.regenerateCombinedConfigData();
this.emit('update', this.combinedData); this.emit('update', this.combinedData);
} }
/*********************
* Setters and Getters
*********************/
/** /**
* Used to save a single config property * Used to save a single config property
* *
@@ -166,18 +118,8 @@ export class Config extends EventEmitter {
this.setMultiple({[key]: data}); this.setMultiple({[key]: data});
} }
updateConfiguration = (event: Electron.IpcMainEvent, properties: Array<{key: keyof ConfigType; data: ConfigType[keyof ConfigType]}> = []): Partial<ConfigType> | undefined => { setConfigPath = (configPath: string) => {
log.debug('Config.updateConfiguration', properties); this.configFilePath = configPath;
if (properties.length) {
const newData = properties.reduce((obj, data) => {
(obj as any)[data.key] = data.data;
return obj;
}, {} as Partial<ConfigType>);
this.setMultiple(newData);
}
return this.localConfigData;
} }
/** /**
@@ -188,20 +130,12 @@ export class Config extends EventEmitter {
setMultiple = (newData: Partial<ConfigType>) => { setMultiple = (newData: Partial<ConfigType>) => {
log.debug('setMultiple', newData); log.debug('setMultiple', newData);
this.localConfigData = Object.assign({}, this.localConfigData, newData); if (newData.darkMode && newData.darkMode !== this.darkMode) {
if (newData.teams && this.localConfigData) { this.emit('darkModeChange', newData.darkMode);
this.localConfigData.teams = this.filterOutPredefinedTeams(newData.teams as TeamWithTabs[]);
this.predefinedTeams = this.filterInPredefinedTeams(newData.teams as TeamWithTabs[]);
} }
this.localConfigData = Object.assign({}, this.localConfigData, {...newData, teams: this.localConfigData?.teams});
this.regenerateCombinedConfigData(); this.regenerateCombinedConfigData();
this.saveLocalConfigData(); this.saveLocalConfigData();
return this.localConfigData; //this is the only part that changes
}
setRegistryConfigData = (registryConfigData = {teams: []}): void => {
this.registryConfigData = Object.assign({}, registryConfigData);
this.reload();
} }
setServers = (servers: ConfigServer[], lastActiveTeam?: number) => { setServers = (servers: ConfigServer[], lastActiveTeam?: number) => {
@@ -212,50 +146,6 @@ export class Config extends EventEmitter {
this.saveLocalConfigData(); this.saveLocalConfigData();
} }
/**
* Used to replace the existing config data with new config data
*
* @param {object} configData a new, config data object to completely replace the existing config data
*/
replace = (configData: ConfigType) => {
const newConfigData = configData;
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
/**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes
* @emits {error} emitted if saving local config data to file fails
*/
saveLocalConfigData = (): void => {
if (!this.localConfigData) {
return;
}
log.info('Saving config data to file...');
try {
this.writeFile(this.configFilePath, this.localConfigData, (error: NodeJS.ErrnoException | null) => {
if (error) {
if (error.code === 'EBUSY') {
this.saveLocalConfigData();
} else {
this.emit('error', error);
}
}
this.emit('update', this.combinedData);
});
} catch (error) {
this.emit('error', error);
}
}
// getters for accessing the various config data inputs // getters for accessing the various config data inputs
get data() { get data() {
@@ -288,6 +178,9 @@ export class Config extends EventEmitter {
get localTeams() { get localTeams() {
return this.localConfigData?.teams ?? defaultPreferences.teams; return this.localConfigData?.teams ?? defaultPreferences.teams;
} }
get predefinedTeams() {
return this.predefinedServers;
}
get enableHardwareAcceleration() { get enableHardwareAcceleration() {
return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration; return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration;
} }
@@ -361,29 +254,67 @@ export class Config extends EventEmitter {
return this.combinedData?.appLanguage; return this.combinedData?.appLanguage;
} }
// initialization/processing methods
/** /**
* Returns a copy of the app's default config data * Gets the teams from registry into the config object and reload
*
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
*/ */
loadDefaultConfigData = () => {
return this.copy(defaultPreferences); private onLoadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.debug('loadRegistry', {registryData});
this.registryConfigData = registryData;
if (this.registryConfigData.teams) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
}
this.reload();
} }
/** /**
* Returns a copy of the app's build config data * Config file loading methods
*/ */
loadBuildConfigData = () => {
return this.copy(buildConfig); /**
* Used to save the current set of local config data to disk
*
* @emits {update} emitted once all data has been saved
* @emits {synchronize} emitted once all data has been saved; used to notify other config instances of changes
* @emits {error} emitted if saving local config data to file fails
*/
private saveLocalConfigData = (): void => {
if (!(this.configFilePath && this.localConfigData)) {
return;
}
log.verbose('Saving config data to file...');
try {
this.writeFile(this.configFilePath, this.localConfigData, (error: NodeJS.ErrnoException | null) => {
if (error) {
if (error.code === 'EBUSY') {
this.saveLocalConfigData();
} else {
this.emit('error', error);
}
}
this.emit('update', this.combinedData);
});
} catch (error) {
this.emit('error', error);
}
} }
/** /**
* Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found * Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found
*/ */
loadLocalConfigFile = (): AnyConfig => { private loadLocalConfigFile = (): AnyConfig => {
if (!this.configFilePath) {
throw new Error('Unable to read from config, no path specified');
}
let configData: AnyConfig; let configData: AnyConfig;
try { try {
configData = this.readFileSync(this.configFilePath); configData = JSON.parse(fs.readFileSync(this.configFilePath, 'utf8'));
// validate based on config file version // validate based on config file version
configData = Validator.validateConfigData(configData); configData = Validator.validateConfigData(configData);
@@ -393,9 +324,9 @@ export class Config extends EventEmitter {
} }
} catch (e) { } catch (e) {
log.warn('Failed to load configuration file from the filesystem. Using defaults.'); log.warn('Failed to load configuration file from the filesystem. Using defaults.');
configData = this.copy(this.defaultConfigData); configData = copy(this.defaultConfigData);
this.writeFileSync(this.configFilePath, configData); this.writeFile(this.configFilePath, configData);
} }
return configData; return configData;
} }
@@ -405,18 +336,22 @@ export class Config extends EventEmitter {
* *
* @param {*} data locally stored data * @param {*} data locally stored data
*/ */
checkForConfigUpdates = (data: AnyConfig) => { private checkForConfigUpdates = (data: AnyConfig) => {
if (!this.configFilePath) {
throw new Error('Config not initialized');
}
let configData = data; let configData = data;
if (this.defaultConfigData) { if (this.defaultConfigData) {
try { try {
if (configData.version !== this.defaultConfigData.version) { if (configData.version !== this.defaultConfigData.version) {
configData = upgradeConfigData(configData); configData = upgradeConfigData(configData);
this.writeFileSync(this.configFilePath, configData); this.writeFile(this.configFilePath, configData);
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`); log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
} }
const didMigrate = migrateConfigItems(configData); const didMigrate = migrateConfigItems(configData);
if (didMigrate) { if (didMigrate) {
this.writeFileSync(this.configFilePath, configData); this.writeFile(this.configFilePath, configData);
log.info('Migrating config items successfully.'); log.info('Migrating config items successfully.');
} }
} catch (error) { } catch (error) {
@@ -430,43 +365,45 @@ export class Config extends EventEmitter {
/** /**
* Properly combines all sources of data into a single, manageable set of all config data * Properly combines all sources of data into a single, manageable set of all config data
*/ */
regenerateCombinedConfigData = () => { private regenerateCombinedConfigData = () => {
if (!this.appName) {
throw new Error('Config not initialized, cannot regenerate');
}
// combine all config data in the correct order // combine all config data in the correct order
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData, {useNativeWindow: this.useNativeWindow}); this.combinedData = Object.assign({},
this.defaultConfigData,
this.localConfigData,
this.buildConfigData,
this.registryConfigData,
{useNativeWindow: this.useNativeWindow},
);
// remove unecessary data pulled from default and build config // We don't want to include the servers in the combined config, they should only be accesible via the ServerManager
delete this.combinedData!.defaultTeams; //delete (this.combinedData as any).teams;
delete (this.combinedData as any).defaultTeams;
// IMPORTANT: properly combine teams from all sources
let combinedTeams: TeamWithTabs[] = [];
if (this.combinedData) {
// TODO: This can be removed after we fully migrate to ServerManager
let combinedTeams: ConfigServer[] = [];
combinedTeams.push(...this.predefinedTeams); combinedTeams.push(...this.predefinedTeams);
// - add locally defined teams only if server management is enabled
if (this.localConfigData && this.enableServerManagement) { if (this.localConfigData && this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams || []); combinedTeams.push(...this.localConfigData.teams || []);
} }
this.predefinedTeams = this.filterOutDuplicateTeams(this.predefinedTeams);
combinedTeams = this.filterOutDuplicateTeams(combinedTeams); combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams); combinedTeams = this.sortUnorderedTeams(combinedTeams);
if (this.combinedData) {
this.combinedData.teams = combinedTeams; this.combinedData.teams = combinedTeams;
this.combinedData.registryTeams = this.registryConfigData?.teams || [];
if (process.platform === 'darwin' || process.platform === 'win32') { this.combinedData.appName = this.appName;
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
}
this.combinedData.appName = app.name;
} }
} }
/** /**
* Returns the provided list of teams with duplicates filtered out * Returns the provided list of teams with duplicates filtered out
* * TODO: This can be removed after we fully migrate to ServerManager
* @param {array} teams array of teams to check for duplicates * @param {array} teams array of teams to check for duplicates
*/ */
filterOutDuplicateTeams = (teams: TeamWithTabs[]) => { private filterOutDuplicateTeams = (teams: ConfigServer[]) => {
let newTeams = teams; let newTeams = teams;
const uniqueURLs = new Set(); const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => { newTeams = newTeams.filter((team) => {
@@ -475,41 +412,12 @@ export class Config extends EventEmitter {
return newTeams; return newTeams;
} }
/**
* Returns the provided array fo teams with existing teams filtered out
* @param {array} teams array of teams to check for already defined teams
*/
filterOutPredefinedTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) === -1; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/**
* Returns the provided array fo teams with existing teams includes
* @param {array} teams array of teams to check for already defined teams
*/
filterInPredefinedTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams;
// filter out predefined teams
newTeams = newTeams.filter((newTeam) => {
return this.predefinedTeams.findIndex((existingTeam) => newTeam.url === existingTeam.url) >= 0; // eslint-disable-line max-nested-callbacks
});
return newTeams;
}
/** /**
* Apply a default sort order to the team list, if no order is specified. * Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort * @param {array} teams to sort
* TODO: This can be removed after we fully migrate to ServerManager
*/ */
sortUnorderedTeams = (teams: TeamWithTabs[]) => { private sortUnorderedTeams = (teams: ConfigServer[]) => {
// We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur // We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur
const mappedTeams = teams.map((team, index) => ({team, originalOrder: index})); const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
@@ -538,12 +446,7 @@ export class Config extends EventEmitter {
} }
// helper functions // helper functions
private writeFile = (filePath: string, configData: Partial<ConfigType>, callback?: fs.NoParamCallback) => {
readFileSync = (filePath: string) => {
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
}
writeFile = (filePath: string, configData: Partial<ConfigType>, callback: fs.NoParamCallback) => {
if (!this.defaultConfigData) { if (!this.defaultConfigData) {
return; return;
} }
@@ -552,101 +455,52 @@ export class Config extends EventEmitter {
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version); throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
} }
const json = JSON.stringify(configData, null, ' '); const json = JSON.stringify(configData, null, ' ');
if (callback) {
fs.writeFile(filePath, json, 'utf8', callback); fs.writeFile(filePath, json, 'utf8', callback);
} } else {
writeFileSync = (filePath: string, config: Partial<ConfigType>) => {
if (!this.defaultConfigData) {
return;
}
if (config.version !== this.defaultConfigData.version) {
throw new Error('version ' + config.version + ' is not equal to ' + this.defaultConfigData.version);
}
const dir = path.dirname(filePath); const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) { if (!fs.existsSync(dir)) {
fs.mkdirSync(dir); fs.mkdirSync(dir);
} }
const json = JSON.stringify(config, null, ' ');
fs.writeFileSync(filePath, json, 'utf8'); fs.writeFileSync(filePath, json, 'utf8');
} }
merge = <T, T2>(base: T, target: T2) => {
return Object.assign({}, base, target);
} }
copy = <T>(data: T) => { private checkWriteableApp = () => {
return Object.assign({}, data); if (!this.appPath) {
throw new Error('Config not initialized, cannot regenerate');
} }
handleGetConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof CombinedConfig) => { if (process.platform === 'win32') {
log.debug('Config.handleGetConfiguration', option); try {
fs.accessSync(path.join(path.dirname(this.appPath), '../../'), fs.constants.W_OK);
const config = {...this.combinedData}; // check to make sure that app-update.yml exists
if (option) { if (!fs.existsSync(path.join(process.resourcesPath, 'app-update.yml'))) {
return config[option]; log.warn('app-update.yml does not exist, disabling auto-updates');
return false;
} }
return config; } catch (error) {
log.info(`${this.appPath}: ${error}`);
log.warn('autoupgrade disabled');
return false;
} }
handleGetLocalConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof ConfigType) => { // eslint-disable-next-line no-undef
log.debug('Config.handleGetLocalConfiguration', option); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const config: Partial<LocalConfiguration> = {...this.localConfigData}; return __CAN_UPGRADE__; // prevent showing the option if the path is not writeable, like in a managed environment.
config.appName = app.name;
config.enableServerManagement = this.combinedData?.enableServerManagement;
config.canUpgrade = this.canUpgrade;
if (option) {
return config[option];
}
return config;
} }
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: TeamWithTabs[]) => { // temporarily disabling auto updater for macOS due to security issues
log.debug('Config.handleUpdateTeams'); // eslint-disable-next-line no-undef
log.silly('Config.handleUpdateTeams', newTeams); // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.set('teams', newTeams); return process.platform !== 'darwin' && __CAN_UPGRADE__;
return this.combinedData!.teams;
}
/**
* Detects changes in darkmode if it is windows or osx, updates the config and propagates the changes
* @emits 'darkModeChange'
*/
handleUpdateTheme = () => {
log.debug('Config.handleUpdateTheme');
if (this.combinedData && this.combinedData.darkMode !== nativeTheme.shouldUseDarkColors) {
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
this.emit('darkModeChange', this.combinedData.darkMode);
} }
} }
/** const config = new Config();
* Manually toggles dark mode for OSes that don't have a native dark mode setting
* @emits 'darkModeChange'
*/
toggleDarkModeManually = () => {
if (!this.combinedData) {
return;
}
this.set('darkMode', !this.combinedData.darkMode);
this.emit('darkModeChange', this.combinedData.darkMode);
}
}
const config = new Config(configPath);
export default config; export default config;
ipcMain.on(UPDATE_PATHS, () => {
log.debug('Config.UPDATE_PATHS');
config.configFilePath = configPath;
if (config.combinedData) {
config.reload();
}
});

View File

@@ -5,7 +5,7 @@ import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgrade
import pastDefaultPreferences from 'common/config/pastDefaultPreferences'; import pastDefaultPreferences from 'common/config/pastDefaultPreferences';
jest.mock('common/tabs/TabView', () => ({ jest.mock('common/tabs/TabView', () => ({
getDefaultTeamWithTabsFromTeam: (value) => ({ getDefaultConfigTeamFromTeam: (value) => ({
...value, ...value,
tabs: [ tabs: [
{ {
@@ -122,7 +122,6 @@ describe('common/config/upgradePreferences', () => {
name: 'tab2', name: 'tab2',
}, },
], ],
lastActiveTab: 0,
}, { }, {
name: 'Secondary team', name: 'Secondary team',
url: 'http://server-2.com', url: 'http://server-2.com',
@@ -135,7 +134,6 @@ describe('common/config/upgradePreferences', () => {
name: 'tab2', name: 'tab2',
}, },
], ],
lastActiveTab: 0,
}], }],
}); });
}); });

View File

@@ -4,7 +4,7 @@
import {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config'; import {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import pastDefaultPreferences from './pastDefaultPreferences'; import pastDefaultPreferences from './pastDefaultPreferences';
@@ -37,10 +37,7 @@ export function upgradeV2toV3(configV2: ConfigV2) {
const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2); const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2);
config.version = 3; config.version = 3;
config.teams = configV2.teams.map((value) => { config.teams = configV2.teams.map((value) => {
return { return getDefaultConfigTeamFromTeam(value);
...getDefaultTeamWithTabsFromTeam(value),
lastActiveTab: 0,
};
}); });
config.lastActiveTeam = 0; config.lastActiveTeam = 0;
config.spellCheckerLocales = []; config.spellCheckerLocales = [];

View File

@@ -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 {FullTeam} from 'types/config'; import {Team} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
@@ -21,7 +21,7 @@ export interface TabView {
get shouldNotify(): boolean; get shouldNotify(): boolean;
} }
export function getDefaultTeamWithTabsFromTeam(team: FullTeam) { export function getDefaultConfigTeamFromTeam(team: Team & {order: number; lastActiveTab?: number}) {
return { return {
...team, ...team,
tabs: getDefaultTabs(), tabs: getDefaultTabs(),

View File

@@ -66,6 +66,10 @@ export const escapeRegex = (s?: string) => {
return s.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&'); return s.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
}; };
export function copy<T>(data: T) {
return Object.assign({}, data);
}
export default { export default {
runMode, runMode,
shorten, shorten,

View File

@@ -1,9 +1,9 @@
// 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 {app, ipcMain} from 'electron'; import {app, ipcMain, nativeTheme} from 'electron';
import {CombinedConfig} from 'types/config'; import {CombinedConfig, ConfigServer, Config as ConfigType} from 'types/config';
import {DARK_MODE_CHANGE, EMIT_CONFIGURATION, RELOAD_CONFIGURATION} from 'common/communication'; import {DARK_MODE_CHANGE, EMIT_CONFIGURATION, RELOAD_CONFIGURATION} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
@@ -25,6 +25,49 @@ const log = new Logger('App.Config');
// config event handlers // config event handlers
// //
export function handleGetConfiguration() {
log.debug('handleGetConfiguration');
return Config.data;
}
export function handleGetLocalConfiguration() {
log.debug('handleGetLocalConfiguration');
return {
...Config.localData,
appName: app.name,
enableServerManagement: Config.enableServerManagement,
canUpgrade: Config.canUpgrade,
};
}
export function updateConfiguration(event: Electron.IpcMainEvent, properties: Array<{key: keyof ConfigType; data: ConfigType[keyof ConfigType]}> = []) {
log.debug('updateConfiguration', properties);
if (properties.length) {
const newData = properties.reduce((obj, data) => {
(obj as any)[data.key] = data.data;
return obj;
}, {} as Partial<ConfigType>);
Config.setMultiple(newData);
}
}
export function handleUpdateTheme() {
log.debug('Config.handleUpdateTheme');
Config.set('darkMode', nativeTheme.shouldUseDarkColors);
}
export function handleUpdateTeams(event: Electron.IpcMainInvokeEvent, newTeams: ConfigServer[]) {
log.debug('Config.handleUpdateTeams');
log.silly('Config.handleUpdateTeams', newTeams);
Config.setServers(newTeams);
return Config.teams;
}
export function handleConfigUpdate(newConfig: CombinedConfig) { export function handleConfigUpdate(newConfig: CombinedConfig) {
if (newConfig.logLevel) { if (newConfig.logLevel) {
setLoggingLevel(newConfig.logLevel); setLoggingLevel(newConfig.logLevel);

View File

@@ -59,6 +59,9 @@ jest.mock('electron', () => ({
removeHandler: jest.fn(), removeHandler: jest.fn(),
removeListener: jest.fn(), removeListener: jest.fn(),
}, },
nativeTheme: {
on: jest.fn(),
},
screen: { screen: {
on: jest.fn(), on: jest.fn(),
}, },
@@ -111,6 +114,7 @@ jest.mock('main/allowProtocolDialog', () => ({
jest.mock('main/app/app', () => ({})); jest.mock('main/app/app', () => ({}));
jest.mock('main/app/config', () => ({ jest.mock('main/app/config', () => ({
handleConfigUpdate: jest.fn(), handleConfigUpdate: jest.fn(),
handleUpdateTheme: jest.fn(),
})); }));
jest.mock('main/app/intercom', () => ({ jest.mock('main/app/intercom', () => ({
handleMainWindowIsShown: jest.fn(), handleMainWindowIsShown: jest.fn(),

View File

@@ -3,7 +3,7 @@
import path from 'path'; import path from 'path';
import {app, ipcMain, session} from 'electron'; import {app, ipcMain, nativeTheme, session} from 'electron';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer'; import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
import isDev from 'electron-is-dev'; import isDev from 'electron-is-dev';
@@ -35,6 +35,11 @@ import {
PING_DOMAIN, PING_DOMAIN,
MAIN_WINDOW_SHOWN, MAIN_WINDOW_SHOWN,
OPEN_APP_MENU, OPEN_APP_MENU,
GET_CONFIGURATION,
GET_LOCAL_CONFIGURATION,
UPDATE_CONFIGURATION,
UPDATE_PATHS,
UPDATE_TEAMS,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@@ -47,7 +52,7 @@ import AutoLauncher from 'main/AutoLauncher';
import updateManager from 'main/autoUpdater'; import updateManager from 'main/autoUpdater';
import {setupBadge} from 'main/badge'; import {setupBadge} from 'main/badge';
import CertificateManager from 'main/certificateManager'; import CertificateManager from 'main/certificateManager';
import {updatePaths} from 'main/constants'; import {configPath, updatePaths} from 'main/constants';
import CriticalErrorHandler from 'main/CriticalErrorHandler'; import CriticalErrorHandler from 'main/CriticalErrorHandler';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import i18nManager from 'main/i18nManager'; import i18nManager from 'main/i18nManager';
@@ -71,7 +76,15 @@ import {
handleAppWindowAllClosed, handleAppWindowAllClosed,
handleChildProcessGone, handleChildProcessGone,
} from './app'; } from './app';
import {handleConfigUpdate, handleDarkModeChange} from './config'; import {
handleConfigUpdate,
handleDarkModeChange,
handleGetConfiguration,
handleGetLocalConfiguration,
handleUpdateTheme,
updateConfiguration,
handleUpdateTeams,
} from './config';
import { import {
handleMainWindowIsShown, handleMainWindowIsShown,
handleAppVersion, handleAppVersion,
@@ -177,7 +190,15 @@ async function initializeConfig() {
resolve(); resolve();
}); });
Config.init(); Config.init(configPath, app.name, app.getAppPath());
ipcMain.on(UPDATE_PATHS, () => {
log.debug('Config.UPDATE_PATHS');
Config.setConfigPath(configPath);
if (Config.data) {
Config.reload();
}
});
}); });
} }
@@ -232,6 +253,11 @@ function initializeBeforeAppReady() {
} else if (mainProtocol) { } else if (mainProtocol) {
app.setAsDefaultProtocolClient(mainProtocol); app.setAsDefaultProtocolClient(mainProtocol);
} }
if (process.platform === 'darwin' || process.platform === 'win32') {
nativeTheme.on('updated', handleUpdateTheme);
handleUpdateTheme();
}
} }
function initializeInterCommunicationEventListeners() { function initializeInterCommunicationEventListeners() {
@@ -269,6 +295,10 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload); ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
ipcMain.on(START_UPGRADE, handleStartUpgrade); ipcMain.on(START_UPGRADE, handleStartUpgrade);
ipcMain.handle(PING_DOMAIN, handlePingDomain); ipcMain.handle(PING_DOMAIN, handlePingDomain);
ipcMain.handle(GET_CONFIGURATION, handleGetConfiguration);
ipcMain.handle(GET_LOCAL_CONFIGURATION, handleGetLocalConfiguration);
ipcMain.handle(UPDATE_TEAMS, handleUpdateTeams);
ipcMain.on(UPDATE_CONFIGURATION, updateConfiguration);
} }
function initializeAfterAppReady() { function initializeAfterAppReady() {

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import Config from 'common/config'; import Config from 'common/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalURLString, getLocalPreload} from 'main/utils';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@@ -20,10 +20,10 @@ import {
} from './intercom'; } from './intercom';
jest.mock('common/config', () => ({ jest.mock('common/config', () => ({
set: jest.fn(), setServers: jest.fn(),
})); }));
jest.mock('common/tabs/TabView', () => ({ jest.mock('common/tabs/TabView', () => ({
getDefaultTeamWithTabsFromTeam: jest.fn(), getDefaultConfigTeamFromTeam: jest.fn(),
})); }));
jest.mock('main/notifications', () => ({})); jest.mock('main/notifications', () => ({}));
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
@@ -75,8 +75,8 @@ const teams = [
describe('main/app/intercom', () => { describe('main/app/intercom', () => {
describe('handleCloseTab', () => { describe('handleCloseTab', () => {
beforeEach(() => { beforeEach(() => {
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); Config.teams = JSON.parse(JSON.stringify(teams));
}); });
@@ -94,8 +94,8 @@ describe('main/app/intercom', () => {
describe('handleOpenTab', () => { describe('handleOpenTab', () => {
beforeEach(() => { beforeEach(() => {
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); Config.teams = JSON.parse(JSON.stringify(teams));
}); });
@@ -117,12 +117,12 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); Config.teams = JSON.parse(JSON.stringify(teams));
getDefaultTeamWithTabsFromTeam.mockImplementation((team) => ({ getDefaultConfigTeamFromTeam.mockImplementation((team) => ({
...team, ...team,
tabs, tabs,
})); }));
@@ -156,8 +156,8 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); Config.teams = JSON.parse(JSON.stringify(teams));
}); });
@@ -199,8 +199,8 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); Config.teams = JSON.parse(JSON.stringify(teams));
}); });
@@ -248,8 +248,8 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.teams = JSON.parse(JSON.stringify([])); Config.teams = JSON.parse(JSON.stringify([]));
}); });
@@ -271,8 +271,8 @@ describe('main/app/intercom', () => {
isVisible: () => true, isVisible: () => true,
}); });
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
Config.registryConfigData = { Config.registryConfigData = {
teams: JSON.parse(JSON.stringify([{ teams: JSON.parse(JSON.stringify([{
@@ -281,6 +281,7 @@ describe('main/app/intercom', () => {
url: 'https://someurl.here', url: 'https://someurl.here',
}])), }])),
}; };
Config.teams = JSON.parse(JSON.stringify(teams));
handleMainWindowIsShown(); handleMainWindowIsShown();
expect(ModalManager.addModal).not.toHaveBeenCalled(); expect(ModalManager.addModal).not.toHaveBeenCalled();

View File

@@ -8,7 +8,7 @@ import {MentionData} from 'types/notification';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {ping} from 'common/utils/requests'; import {ping} from 'common/utils/requests';
import {displayMention} from 'main/notifications'; import {displayMention} from 'main/notifications';
@@ -69,7 +69,7 @@ export function handleCloseTab(event: IpcMainEvent, serverName: string, tabName:
}); });
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name; const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name;
WindowManager.switchTab(serverName, nextTab); WindowManager.switchTab(serverName, nextTab);
Config.set('teams', teams); Config.setServers(teams);
} }
export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) { export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) {
@@ -86,7 +86,7 @@ export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName:
} }
}); });
WindowManager.switchTab(serverName, tabName); WindowManager.switchTab(serverName, tabName);
Config.set('teams', teams); Config.setServers(teams);
} }
export function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) { export function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) {
@@ -153,9 +153,9 @@ export function handleNewServerModal() {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = Config.teams; const teams = Config.teams;
const order = teams.length; const order = teams.length;
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order}); const newTeam = getDefaultConfigTeamFromTeam({...data, order});
teams.push(newTeam); teams.push(newTeam);
Config.set('teams', teams); Config.setServers(teams);
updateServerInfos([newTeam]); updateServerInfos([newTeam]);
WindowManager.switchServer(newTeam.name, true); WindowManager.switchServer(newTeam.name, true);
}).catch((e) => { }).catch((e) => {
@@ -198,7 +198,7 @@ export function handleEditServerModal(e: IpcMainEvent, name: string) {
const teams = Config.teams; const teams = Config.teams;
teams[serverIndex].name = data.name; teams[serverIndex].name = data.name;
teams[serverIndex].url = data.url; teams[serverIndex].url = data.url;
Config.set('teams', teams); Config.setServers(teams);
updateServerInfos([teams[serverIndex]]); updateServerInfos([teams[serverIndex]]);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
@@ -238,7 +238,7 @@ export function handleRemoveServerModal(e: IpcMainEvent, name: string) {
value.order--; value.order--;
} }
}); });
Config.set('teams', teams); Config.setServers(teams);
} }
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
@@ -267,9 +267,9 @@ export function handleWelcomeScreenModal() {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = Config.teams; const teams = Config.teams;
const order = teams.length; const order = teams.length;
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order}); const newTeam = getDefaultConfigTeamFromTeam({...data, order});
teams.push(newTeam); teams.push(newTeam);
Config.set('teams', teams); Config.setServers(teams);
updateServerInfos([newTeam]); updateServerInfos([newTeam]);
WindowManager.switchServer(newTeam.name, true); WindowManager.switchServer(newTeam.name, true);
}).catch((e) => { }).catch((e) => {
@@ -324,10 +324,7 @@ export function handleUpdateLastActive(event: IpcMainEvent, serverName: string,
team.lastActiveTab = viewOrder; team.lastActiveTab = viewOrder;
} }
}); });
Config.setMultiple({ Config.setServers(teams, teams.find((team) => team.name === serverName)?.order || 0);
teams,
lastActiveTeam: teams.find((team) => team.name === serverName)?.order || 0,
});
} }
export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise<string> { export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise<string> {

View File

@@ -40,7 +40,7 @@ jest.mock('electron', () => ({
})); }));
jest.mock('common/config', () => ({ jest.mock('common/config', () => ({
set: jest.fn(), setServers: jest.fn(),
})); }));
jest.mock('common/JsonFileManager'); jest.mock('common/JsonFileManager');
jest.mock('common/utils/util', () => ({ jest.mock('common/utils/util', () => ({
@@ -95,8 +95,8 @@ describe('main/app/utils', () => {
beforeEach(() => { beforeEach(() => {
Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0'); Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0');
Config.set.mockImplementation((name, value) => { Config.setServers.mockImplementation((value) => {
Config[name] = value; Config.teams = value;
}); });
const teamsCopy = JSON.parse(JSON.stringify(teams)); const teamsCopy = JSON.parse(JSON.stringify(teams));
Config.teams = teamsCopy; Config.teams = teamsCopy;

View File

@@ -47,8 +47,8 @@ export function openDeepLink(deeplinkingUrl: string) {
} }
export function updateSpellCheckerLocales() { export function updateSpellCheckerLocales() {
if (Config.data?.spellCheckerLocales.length && app.isReady()) { if (Config.spellCheckerLocales.length && app.isReady()) {
session.defaultSession.setSpellCheckerLanguages(Config.data?.spellCheckerLocales); session.defaultSession.setSpellCheckerLanguages(Config.spellCheckerLocales);
} }
} }
@@ -67,7 +67,7 @@ export function updateServerInfos(teams: TeamWithTabs[]) {
hasUpdates = hasUpdates || openExtraTabs(data, team); hasUpdates = hasUpdates || openExtraTabs(data, team);
}); });
if (hasUpdates) { if (hasUpdates) {
Config.set('teams', teams); Config.setServers(teams);
} }
}).catch((reason: any) => { }).catch((reason: any) => {
log.error('Error getting server infos', reason); log.error('Error getting server infos', reason);

View File

@@ -8,6 +8,8 @@ import {DiagnosticStepResponse} from 'types/diagnostics';
import Config from 'common/config'; import Config from 'common/config';
import * as Validator from 'common/Validator'; import * as Validator from 'common/Validator';
import {configPath} from 'main/constants';
import DiagnosticsStep from '../DiagnosticStep'; import DiagnosticsStep from '../DiagnosticStep';
const stepName = 'Step-2'; const stepName = 'Step-2';
@@ -15,13 +17,13 @@ const stepDescriptiveName = 'configValidation';
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => { const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
try { try {
const configData = JSON.parse(fs.readFileSync(Config.configFilePath, 'utf8')); const configData = JSON.parse(fs.readFileSync(configPath, 'utf8'));
// validate based on config file version // validate based on config file version
const validData = Validator.validateConfigData(configData); const validData = Validator.validateConfigData(configData);
if (!validData) { if (!validData) {
throw new Error(`Config validation failed. Config: ${JSON.stringify(Config.combinedData, null, 4)}`); throw new Error(`Config validation failed. Config: ${JSON.stringify(Config.data, null, 4)}`);
} }
return { return {

View File

@@ -15,7 +15,7 @@ const stepDescriptiveName = 'serverConnectivity';
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => { const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
try { try {
const teams = Config.combinedData?.teams || []; const teams = Config.teams || [];
await Promise.all(teams.map(async (team) => { await Promise.all(teams.map(async (team) => {
logger.debug('Pinging server: ', team.url); logger.debug('Pinging server: ', team.url);

View File

@@ -65,7 +65,6 @@ jest.mock('common/tabs/TabView', () => ({
describe('main/menus/app', () => { describe('main/menus/app', () => {
const config = { const config = {
data: {
enableServerManagement: true, enableServerManagement: true,
teams: [{ teams: [{
name: 'example', name: 'example',
@@ -113,7 +112,6 @@ describe('main/menus/app', () => {
lastActiveTab: 0, lastActiveTab: 0,
}], }],
helpLink: 'http://link-to-help.site.com', helpLink: 'http://link-to-help.site.com',
},
}; };
beforeEach(() => { beforeEach(() => {
getDarwinDoNotDisturb.mockReturnValue(false); getDarwinDoNotDisturb.mockReturnValue(false);
@@ -218,7 +216,7 @@ describe('main/menus/app', () => {
const menu = createTemplate(modifiedConfig); const menu = createTemplate(modifiedConfig);
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
expect(signInOption).not.toBe(undefined); expect(signInOption).toBe(undefined);
}); });
it('should not show `Sign in to Another Server` if no teams are configured', () => { it('should not show `Sign in to Another Server` if no teams are configured', () => {
@@ -239,7 +237,7 @@ describe('main/menus/app', () => {
const menu = createTemplate(modifiedConfig); const menu = createTemplate(modifiedConfig);
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
expect(signInOption).not.toBe(undefined); expect(signInOption).toBe(undefined);
}); });
it('should show the first 9 servers (using order) in the Window menu', () => { it('should show the first 9 servers (using order) in the Window menu', () => {
@@ -250,8 +248,6 @@ describe('main/menus/app', () => {
return id; return id;
}); });
const modifiedConfig = { const modifiedConfig = {
data: {
...config.data,
teams: [...Array(15).keys()].map((key) => ({ teams: [...Array(15).keys()].map((key) => ({
name: `server-${key}`, name: `server-${key}`,
url: `http://server-${key}.com`, url: `http://server-${key}.com`,
@@ -264,7 +260,6 @@ describe('main/menus/app', () => {
}, },
], ],
})), })),
},
}; };
const menu = createTemplate(modifiedConfig); const menu = createTemplate(modifiedConfig);
const windowMenu = menu.find((item) => item.label === '&Window'); const windowMenu = menu.find((item) => item.label === '&Window');
@@ -292,14 +287,12 @@ describe('main/menus/app', () => {
} }
return id; return id;
}); });
WindowManager.getCurrentTeamName.mockImplementation(() => config.data.teams[0].name); WindowManager.getCurrentTeamName.mockImplementation(() => config.teams[0].name);
const modifiedConfig = { const modifiedConfig = {
data: {
...config.data,
teams: [ teams: [
{ {
...config.data.teams[0], ...config.teams[0],
tabs: [...Array(15).keys()].map((key) => ({ tabs: [...Array(15).keys()].map((key) => ({
name: `tab-${key}`, name: `tab-${key}`,
isOpen: true, isOpen: true,
@@ -307,7 +300,6 @@ describe('main/menus/app', () => {
})), })),
}, },
], ],
},
}; };
const menu = createTemplate(modifiedConfig); const menu = createTemplate(modifiedConfig);
const windowMenu = menu.find((item) => item.label === '&Window'); const windowMenu = menu.find((item) => item.label === '&Window');

View File

@@ -49,7 +49,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}, },
}); });
if (config.data?.enableServerManagement === true && config.data?.teams.length > 0) { if (config.enableServerManagement === true && config.teams.length > 0) {
platformAppMenu.push({ platformAppMenu.push({
label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'), label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'),
click() { click() {
@@ -203,7 +203,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
viewSubMenu.push({ viewSubMenu.push({
label: localizeMessage('main.menus.app.view.toggleDarkMode', 'Toggle Dark Mode'), label: localizeMessage('main.menus.app.view.toggleDarkMode', 'Toggle Dark Mode'),
click() { click() {
config.toggleDarkModeManually(); config.set('darkMode', !config.darkMode);
}, },
}); });
} }
@@ -231,7 +231,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}], }],
}); });
const teams = config.data?.teams || []; const teams = config.teams || [];
const windowMenu = { const windowMenu = {
id: 'window', id: 'window',
label: localizeMessage('main.menus.app.window', '&Window'), label: localizeMessage('main.menus.app.window', '&Window'),
@@ -251,7 +251,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'), label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'),
accelerator: 'CmdOrCtrl+W', accelerator: 'CmdOrCtrl+W',
}, separatorItem, }, separatorItem,
...(config.data?.teams.length ? [{ ...(config.teams.length ? [{
label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'), label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
click() { click() {
@@ -325,11 +325,11 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}); });
} }
} }
if (config.data?.helpLink) { if (config.helpLink) {
submenu.push({ submenu.push({
label: localizeMessage('main.menus.app.help.learnMore', 'Learn More...'), label: localizeMessage('main.menus.app.help.learnMore', 'Learn More...'),
click() { click() {
shell.openExternal(config.data!.helpLink); shell.openExternal(config.helpLink!);
}, },
}); });
submenu.push(separatorItem); submenu.push(separatorItem);

View File

@@ -19,6 +19,7 @@ import {
UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM,
} from 'common/communication'; } from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import Config from 'common/config';
import { import {
DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_FULL_WIDTH,
DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT,
@@ -26,10 +27,9 @@ import {
TAB_BAR_HEIGHT, TAB_BAR_HEIGHT,
} from 'common/utils/constants'; } from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
const log = new Logger('DownloadsDropdownMenuView'); const log = new Logger('DownloadsDropdownMenuView');
@@ -42,11 +42,11 @@ export default class DownloadsDropdownMenuView {
darkMode: boolean; darkMode: boolean;
windowBounds: Electron.Rectangle; windowBounds: Electron.Rectangle;
constructor(darkMode: boolean) { constructor() {
this.open = false; this.open = false;
this.item = undefined; this.item = undefined;
this.coordinates = undefined; this.coordinates = undefined;
this.darkMode = darkMode; this.darkMode = Config.darkMode;
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen); ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);

View File

@@ -62,6 +62,7 @@ jest.mock('electron', () => {
}; };
}); });
jest.mock('main/downloadsManager', () => ({ jest.mock('main/downloadsManager', () => ({
getDownloads: jest.fn(),
onOpen: jest.fn(), onOpen: jest.fn(),
onClose: jest.fn(), onClose: jest.fn(),
})); }));

View File

@@ -19,11 +19,12 @@ import {
DOWNLOADS_DROPDOWN_OPEN_FILE, DOWNLOADS_DROPDOWN_OPEN_FILE,
} from 'common/communication'; } from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import Config from 'common/config';
import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOWNLOADS_DROPDOWN_FULL_WIDTH} from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
const log = new Logger('DownloadsDropdownView'); const log = new Logger('DownloadsDropdownView');
@@ -36,9 +37,9 @@ export default class DownloadsDropdownView {
view: BrowserView; view: BrowserView;
windowBounds: Electron.Rectangle; windowBounds: Electron.Rectangle;
constructor(downloads: DownloadedItems, darkMode: boolean) { constructor() {
this.downloads = downloads; this.downloads = downloadsManager.getDownloads();
this.darkMode = darkMode; this.darkMode = Config.darkMode;
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen); ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose);

View File

@@ -15,10 +15,6 @@ jest.mock('electron', () => ({
}, },
})); }));
jest.mock('common/config', () => ({
teams: [],
}));
jest.mock('main/views/webContentEvents', () => ({ jest.mock('main/views/webContentEvents', () => ({
addWebContentsEventListeners: jest.fn(), addWebContentsEventListeners: jest.fn(),
})); }));

View File

@@ -17,7 +17,6 @@ import {
GET_MODAL_UNCLOSEABLE, GET_MODAL_UNCLOSEABLE,
RESIZE_MODAL, RESIZE_MODAL,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getAdjustedWindowBoundaries} from 'main/utils'; import {getAdjustedWindowBoundaries} from 'main/utils';
@@ -91,7 +90,7 @@ export class ModalManager {
if (index === 0) { if (index === 0) {
WindowManager.sendToRenderer(MODAL_OPEN); WindowManager.sendToRenderer(MODAL_OPEN);
modal.show(undefined, Boolean(withDevTools)); modal.show(undefined, Boolean(withDevTools));
WebContentsEventManager.addWebContentsEventListeners(modal.view.webContents, () => Config.teams.concat()); WebContentsEventManager.addWebContentsEventListeners(modal.view.webContents);
} else { } else {
WindowManager.sendToRenderer(MODAL_CLOSE); WindowManager.sendToRenderer(MODAL_CLOSE);
modal.hide(); modal.hide();

View File

@@ -42,7 +42,7 @@ describe('main/views/teamDropdownView', () => {
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0}); MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
}); });
const teamDropdownView = new TeamDropdownView([], false, true); const teamDropdownView = new TeamDropdownView();
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
it('should account for three dot menu, tab bar and shadow', () => { it('should account for three dot menu, tab bar and shadow', () => {
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300}); expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
@@ -55,7 +55,7 @@ describe('main/views/teamDropdownView', () => {
}); });
it('should change the view bounds based on open/closed state', () => { it('should change the view bounds based on open/closed state', () => {
const teamDropdownView = new TeamDropdownView([], false, true); const teamDropdownView = new TeamDropdownView();
teamDropdownView.bounds = {width: 400, height: 300}; teamDropdownView.bounds = {width: 400, height: 300};
teamDropdownView.handleOpen(); teamDropdownView.handleOpen();
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds); expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
@@ -65,7 +65,7 @@ describe('main/views/teamDropdownView', () => {
describe('addGpoToTeams', () => { describe('addGpoToTeams', () => {
it('should return teams with "isGPO": false when no config.registryTeams exist', () => { it('should return teams with "isGPO": false when no config.registryTeams exist', () => {
const teamDropdownView = new TeamDropdownView([], false, true); const teamDropdownView = new TeamDropdownView();
const teams = [{ const teams = [{
name: 'team-1', name: 'team-1',
url: 'https://mattermost.team-1.com', url: 'https://mattermost.team-1.com',
@@ -86,7 +86,7 @@ describe('main/views/teamDropdownView', () => {
}]); }]);
}); });
it('should return teams with "isGPO": true if they exist in config.registryTeams', () => { it('should return teams with "isGPO": true if they exist in config.registryTeams', () => {
const teamDropdownView = new TeamDropdownView([], false, true); const teamDropdownView = new TeamDropdownView();
const teams = [{ const teams = [{
name: 'team-1', name: 'team-1',
url: 'https://mattermost.team-1.com', url: 'https://mattermost.team-1.com',

View File

@@ -15,9 +15,12 @@ import {
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SET_ACTIVE_VIEW, SET_ACTIVE_VIEW,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import * as AppState from '../appState'; import * as AppState from '../appState';
import WindowManager from '../windows/windowManager'; import WindowManager from '../windows/windowManager';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
@@ -38,10 +41,10 @@ export default class TeamDropdownView {
windowBounds?: Electron.Rectangle; windowBounds?: Electron.Rectangle;
isOpen: boolean; isOpen: boolean;
constructor(teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) { constructor() {
this.teams = this.addGpoToTeams(teams, []); this.teams = this.addGpoToTeams(Config.teams, []);
this.darkMode = darkMode; this.darkMode = Config.darkMode;
this.enableServerManagement = enableServerManagement; this.enableServerManagement = Config.enableServerManagement;
this.isOpen = false; this.isOpen = false;
this.windowBounds = MainWindow.getBounds(); this.windowBounds = MainWindow.getBounds();

View File

@@ -36,7 +36,6 @@ import {SECOND} from 'common/utils/constants';
import Config from 'common/config'; import Config from 'common/config';
import {getTabViewName} from 'common/tabs/TabView'; import {getTabViewName} from 'common/tabs/TabView';
import downloadsManager from 'main/downloadsManager';
import {MattermostView} from 'main/views/MattermostView'; import {MattermostView} from 'main/views/MattermostView';
import { import {
@@ -208,9 +207,9 @@ export class WindowManager {
mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen')); mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen'));
mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen')); mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen'));
this.teamDropdown = new TeamDropdownView(Config.teams, Config.darkMode, Config.enableServerManagement); this.teamDropdown = new TeamDropdownView();
this.downloadsDropdown = new DownloadsDropdownView(downloadsManager.getDownloads(), Config.darkMode); this.downloadsDropdown = new DownloadsDropdownView();
this.downloadsDropdownMenu = new DownloadsDropdownMenuView(Config.darkMode); this.downloadsDropdownMenu = new DownloadsDropdownMenuView();
this.initializeViewManager(); this.initializeViewManager();
} }

View File

@@ -120,7 +120,7 @@ export type BuildConfig = {
} }
export type RegistryConfig = { export type RegistryConfig = {
teams: FullTeam[]; teams: Team[];
enableServerManagement: boolean; enableServerManagement: boolean;
enableAutoUpdater: boolean; enableAutoUpdater: boolean;
} }