Migrate app to TypeScript (#1637)
* Initial setup and migrated src/common * WIP * WIP * WIP * Main module basically finished * Renderer process migrated * Added CI step and some fixes * Fixed remainder of issues and added proper ESLint config * Fixed a couple issues * Progress! * Some more fixes * Fixed a test * Fix build step * PR feedback
This commit is contained in:
parent
422673a740
commit
1b3d0eac8f
|
@ -108,6 +108,7 @@ jobs:
|
|||
apt_opts: "--no-install-recommends"
|
||||
- restore_cache:
|
||||
key: lint-{{ arch }}-{{ .Branch }}-{{ checksum "package-lock.json" }}
|
||||
- run: npm run check-types
|
||||
- run: ELECTRON_DISABLE_SANDBOX=1 xvfb-run npm run test
|
||||
- run: mkdir -p /tmp/test-results
|
||||
- run: cp test-results.xml /tmp/test-results/
|
||||
|
|
104
.eslintrc.json
104
.eslintrc.json
|
@ -74,58 +74,58 @@
|
|||
"scripts/manipulate_windows_zip.js",
|
||||
"scripts/check_build_config.js",
|
||||
"LICENSE.txt",
|
||||
"src/utils/util.js",
|
||||
"src/main.js",
|
||||
"src/browser/js/contextMenu.js",
|
||||
"src/browser/updater.jsx",
|
||||
"src/browser/js/badge.js",
|
||||
"src/browser/webview/mattermost.js",
|
||||
"src/browser/components/RemoveServerModal.jsx",
|
||||
"src/browser/components/MainPage.jsx",
|
||||
"src/browser/components/HoveringURL.jsx",
|
||||
"src/browser/components/AutoSaveIndicator.jsx",
|
||||
"src/browser/components/MattermostView.jsx",
|
||||
"src/browser/components/TabBar.jsx",
|
||||
"src/browser/components/DestructiveConfirmModal.jsx",
|
||||
"src/browser/components/ErrorView.jsx",
|
||||
"src/browser/components/UpdaterPage.jsx",
|
||||
"src/browser/components/PermissionRequestDialog.jsx",
|
||||
"src/browser/components/Finder.jsx",
|
||||
"src/browser/components/SettingsPage.jsx",
|
||||
"src/browser/components/TeamListItem.jsx",
|
||||
"src/browser/components/UpdaterPage/UpdaterPage.stories.jsx",
|
||||
"src/browser/components/Button/Button.stories.jsx",
|
||||
"src/browser/components/TeamList.jsx",
|
||||
"src/browser/components/LoginModal.jsx",
|
||||
"src/browser/components/NewTeamModal.jsx",
|
||||
"src/browser/settings.jsx",
|
||||
"src/browser/index.jsx",
|
||||
"src/common/deepmerge.js",
|
||||
"src/common/config/index.js",
|
||||
"src/common/config/buildConfig.js",
|
||||
"src/common/config/pastDefaultPreferences.js",
|
||||
"src/common/config/upgradePreferences.js",
|
||||
"src/common/config/RegistryConfig.js",
|
||||
"src/common/osVersion.js",
|
||||
"src/common/config/defaultPreferences.js",
|
||||
"src/common/JsonFileManager.js",
|
||||
"src/main/certificateStore.js",
|
||||
"src/main/mainWindow.js",
|
||||
"src/main/allowProtocolDialog.js",
|
||||
"src/main/permissionRequestHandler.js",
|
||||
"src/main/squirrelStartup.js",
|
||||
"src/main/autoLaunch.js",
|
||||
"src/main/PermissionManager.js",
|
||||
"src/main/AutoLauncher.js",
|
||||
"src/main/AppStateManager.js",
|
||||
"src/main/menus/tray.js",
|
||||
"src/main/CriticalErrorHandler.js",
|
||||
"src/main/cookieManager.js",
|
||||
"src/main/utils.js",
|
||||
"src/main/downloadURL.js",
|
||||
"src/main/autoUpdater.js",
|
||||
"src/main/SpellChecker.js",
|
||||
"src/main/menus/app.js"
|
||||
"src/utils/util.ts",
|
||||
"src/main/main.ts",
|
||||
"src/main/contextMenu.ts",
|
||||
"src/renderer/updater.tsx",
|
||||
"src/main/badge.ts",
|
||||
"src/common/deepmerge.ts",
|
||||
"src/common/config/index.ts",
|
||||
"src/common/config/buildConfig.ts",
|
||||
"src/common/config/pastDefaultPreferences.ts",
|
||||
"src/common/config/upgradePreferences.ts",
|
||||
"src/common/config/RegistryConfig.ts",
|
||||
"src/common/osVersion.ts",
|
||||
"src/common/config/defaultPreferences.ts",
|
||||
"src/common/JsonFileManager.ts",
|
||||
"src/main/certificateStore.ts",
|
||||
"src/main/mainWindow.ts",
|
||||
"src/main/allowProtocolDialog.ts",
|
||||
"src/main/permissionRequestHandler.ts",
|
||||
"src/main/squirrelStartup.ts",
|
||||
"src/main/autoLaunch.ts",
|
||||
"src/main/PermissionManager.ts",
|
||||
"src/main/AutoLauncher.ts",
|
||||
"src/main/AppStateManager.ts",
|
||||
"src/main/menus/tray.ts",
|
||||
"src/main/CriticalErrorHandler.ts",
|
||||
"src/main/cookieManager.ts",
|
||||
"src/main/utils.ts",
|
||||
"src/main/downloadURL.ts",
|
||||
"src/main/autoUpdater.ts",
|
||||
"src/main/SpellChecker.ts",
|
||||
"src/main/menus/app.ts",
|
||||
"src/main/preload/mattermost.js",
|
||||
"src/renderer/components/RemoveServerModal.tsx",
|
||||
"src/renderer/components/MainPage.tsx",
|
||||
"src/renderer/components/HoveringURL.tsx",
|
||||
"src/renderer/components/AutoSaveIndicator.tsx",
|
||||
"src/renderer/components/MattermostView.tsx",
|
||||
"src/renderer/components/TabBar.tsx",
|
||||
"src/renderer/components/DestructiveConfirmModal.tsx",
|
||||
"src/renderer/components/ErrorView.tsx",
|
||||
"src/renderer/components/UpdaterPage.tsx",
|
||||
"src/renderer/components/PermissionRequestDialog.tsx",
|
||||
"src/renderer/components/Finder.tsx",
|
||||
"src/renderer/components/SettingsPage.tsx",
|
||||
"src/renderer/components/TeamListItem.tsx",
|
||||
"src/renderer/components/UpdaterPage/UpdaterPage.stories.tsx",
|
||||
"src/renderer/components/Button/Button.stories.tsx",
|
||||
"src/renderer/components/TeamList.tsx",
|
||||
"src/renderer/components/LoginModal.tsx",
|
||||
"src/renderer/components/NewTeamModal.tsx",
|
||||
"src/renderer/settings.tsx",
|
||||
"src/renderer/index.tsx"
|
||||
],
|
||||
"rules": {
|
||||
"header/header": [
|
||||
|
|
|
@ -13,6 +13,10 @@ module.exports = (api) => { // eslint-disable-line import/no-commonjs
|
|||
},
|
||||
}],
|
||||
'@babel/preset-react',
|
||||
['@babel/typescript', {
|
||||
allExtensions: true,
|
||||
isTSX: true,
|
||||
}],
|
||||
],
|
||||
plugins: ['@babel/plugin-proposal-object-rest-spread', '@babel/plugin-proposal-class-properties'],
|
||||
};
|
||||
|
|
22537
package-lock.json
generated
22537
package-lock.json
generated
File diff suppressed because it is too large
Load diff
45
package.json
45
package.json
|
@ -38,9 +38,9 @@
|
|||
"watch:main": "node scripts/watch_main_and_preload.js",
|
||||
"watch:renderer": "webpack-dev-server --config webpack.config.renderer.js",
|
||||
"test": "npm-run-all lint:js test:unit test:e2e",
|
||||
"test:e2e": "npm-run-all test:e2e:build test:e2e:run",
|
||||
"test:e2e:build": "cross-env NODE_ENV=test npm run build",
|
||||
"test:e2e:run": "cross-env NODE_ENV=test electron-mocha -r @babel/register --reporter mocha-circleci-reporter --recursive test/specs",
|
||||
"test:e2e": "cross-env NODE_ENV=test npm-run-all build test:e2e:build test:e2e:run",
|
||||
"test:e2e:build": "webpack-cli --bail --config webpack.config.test.js",
|
||||
"test:e2e:run": "electron-mocha -r @babel/register --reporter mocha-circleci-reporter dist/tests/e2e_bundle.js",
|
||||
"test:unit": "npm-run-all test:unit:build test:unit:run",
|
||||
"test:unit:build": "cross-env NODE_ENV=test webpack-cli --bail --config webpack.config.test.js",
|
||||
"test:unit:run": "cross-env NODE_ENV=test mocha --reporter mocha-circleci-reporter dist/tests/test_bundle.js",
|
||||
|
@ -48,22 +48,35 @@
|
|||
"package:windows": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --win --x64 --ia32 --publish=never",
|
||||
"package:mac": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --mac --x64 --arm64 --publish=never",
|
||||
"package:linux": "cross-env NODE_ENV=production npm-run-all check-build-config build-prod && electron-builder --linux --x64 --ia32 --publish=never",
|
||||
"lint:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx .",
|
||||
"lint:js-quiet": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx . --quiet",
|
||||
"fix:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --quiet --ext .js --ext .jsx . --fix",
|
||||
"check-build-config": "node -r @babel/register scripts/check_build_config.js"
|
||||
"lint:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx --ext .ts --ext .tsx .",
|
||||
"lint:js-quiet": "eslint --ignore-path .gitignore --ignore-pattern node_modules --ext .js --ext .jsx --ext .ts --ext .tsx . --quiet",
|
||||
"fix:js": "eslint --ignore-path .gitignore --ignore-pattern node_modules --quiet --ext .js --ext .jsx --ext .ts --ext .tsx . --fix",
|
||||
"check-build-config": "npm-run-all check-build-config:build check-build-config:run",
|
||||
"check-build-config:build": "babel ./src/common/config/buildConfig.ts -o ./dist/buildConfig.js",
|
||||
"check-build-config:run": "node -r @babel/register scripts/check_build_config.js",
|
||||
"check-types": "tsc"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/cli": "^7.14.5",
|
||||
"@babel/core": "^7.2.0",
|
||||
"@babel/plugin-proposal-class-properties": "^7.8.3",
|
||||
"@babel/plugin-proposal-object-rest-spread": "^7.2.0",
|
||||
"@babel/preset-env": "^7.2.0",
|
||||
"@babel/preset-react": "^7.10.4",
|
||||
"@babel/register": "^7.0.0",
|
||||
"@storybook/addon-actions": "^4.0.11",
|
||||
"@storybook/react": "^4.0.11",
|
||||
"@typescript-eslint/eslint-plugin": "4.15.0",
|
||||
"@typescript-eslint/parser": "4.15.0",
|
||||
"@storybook/addon-actions": "^6.2.9",
|
||||
"@storybook/react": "^6.2.9",
|
||||
"@types/auto-launch": "^5.0.1",
|
||||
"@types/electron-devtools-installer": "^2.2.0",
|
||||
"@types/hapi__joi": "^17.1.6",
|
||||
"@types/react": "^17.0.11",
|
||||
"@types/react-bootstrap": "^0.32.25",
|
||||
"@types/react-dom": "^17.0.8",
|
||||
"@types/underscore": "^1.11.2",
|
||||
"@types/valid-url": "^1.0.3",
|
||||
"@types/winreg": "^1.2.30",
|
||||
"@typescript-eslint/eslint-plugin": "4.28.0",
|
||||
"@typescript-eslint/parser": "4.28.0",
|
||||
"7zip-bin": "^4.1.0",
|
||||
"awesome-node-loader": "^1.1.1",
|
||||
"babel-eslint": "^10.0.3",
|
||||
|
@ -91,12 +104,14 @@
|
|||
"file-loader": "^2.0.0",
|
||||
"image-webpack-loader": "5.0.0",
|
||||
"mdi-react": "^6.2.0",
|
||||
"mini-css-extract-plugin": "1.6.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-circleci-reporter": "0.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"shebang-loader": "^0.0.1",
|
||||
"spectron": "^14.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"typescript": "4.1.3",
|
||||
"typescript": "^4.3.4",
|
||||
"url-loader": "^1.1.2",
|
||||
"webpack": "^4.44.2",
|
||||
"webpack-cli": "^3.1.2",
|
||||
|
@ -108,7 +123,7 @@
|
|||
"auto-launch": "^5.0.5",
|
||||
"bootstrap": "^3.3.7",
|
||||
"brace-expansion": "^2.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
"classnames": "^2.3.1",
|
||||
"electron-context-menu": "^2.5.0",
|
||||
"electron-devtools-installer": "^3.1.1",
|
||||
"electron-is-dev": "^2.0.0",
|
||||
|
@ -116,9 +131,9 @@
|
|||
"electron-updater": "4.3.8",
|
||||
"font-awesome": "^4.7.0",
|
||||
"prop-types": "^15.6.2",
|
||||
"react": "^16.6.3",
|
||||
"react": "^16.14.0",
|
||||
"react-bootstrap": "~0.32.4",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-dom": "^16.14.0",
|
||||
"react-smooth-dnd": "github:mattermost/react-smooth-dnd#af6b471295007274560a375799622c1cd52d678a",
|
||||
"react-transition-group": "^2.5.0",
|
||||
"semver": "^5.5.0",
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
const buildConfig = require('../src/common/config/buildConfig');
|
||||
const buildConfig = require('../dist/buildConfig');
|
||||
|
||||
function validateBuildConfig(config) {
|
||||
if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) {
|
||||
|
|
|
@ -3,18 +3,21 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import fs from 'fs';
|
||||
|
||||
export default class JsonFileManager {
|
||||
constructor(file) {
|
||||
export default class JsonFileManager<T> {
|
||||
jsonFile: string;
|
||||
json: T;
|
||||
|
||||
constructor(file: string) {
|
||||
this.jsonFile = file;
|
||||
try {
|
||||
this.json = JSON.parse(fs.readFileSync(file, 'utf-8'));
|
||||
} catch (err) {
|
||||
this.json = {};
|
||||
this.json = {} as T;
|
||||
}
|
||||
}
|
||||
|
||||
writeToFile() {
|
||||
fs.writeFile(this.jsonFile, JSON.stringify(this.json, null, 2), (err) => {
|
||||
writeToFile(): void {
|
||||
fs.writeFile(this.jsonFile, JSON.stringify(this.json, undefined, 2), (err) => {
|
||||
if (err) {
|
||||
// No real point in bringing electron-log into this otherwise electron-free file
|
||||
// eslint-disable-next-line no-console
|
||||
|
@ -23,17 +26,17 @@ export default class JsonFileManager {
|
|||
});
|
||||
}
|
||||
|
||||
setJson(json) {
|
||||
setJson(json: T): void {
|
||||
this.json = json;
|
||||
this.writeToFile();
|
||||
}
|
||||
|
||||
setValue(key, value) {
|
||||
setValue(key: keyof T, value: T[keyof T]): void {
|
||||
this.json[key] = value;
|
||||
this.writeToFile();
|
||||
}
|
||||
|
||||
getValue(key) {
|
||||
getValue(key: keyof T): T[keyof T] {
|
||||
return this.json[key];
|
||||
}
|
||||
}
|
|
@ -6,6 +6,8 @@ import {EventEmitter} from 'events';
|
|||
import log from 'electron-log';
|
||||
import WindowsRegistry from 'winreg-utf8';
|
||||
|
||||
import {RegistryConfig as RegistryConfigType, Team} from 'types/config';
|
||||
|
||||
const REGISTRY_HIVE_LIST = [WindowsRegistry.HKLM, WindowsRegistry.HKCU];
|
||||
const BASE_REGISTRY_KEY_PATH = '\\Software\\Policies\\Mattermost';
|
||||
export const REGISTRY_READ_EVENT = 'registry-read';
|
||||
|
@ -14,6 +16,9 @@ export const REGISTRY_READ_EVENT = 'registry-read';
|
|||
* Handles loading config data from the Windows registry set manually or by GPO
|
||||
*/
|
||||
export default class RegistryConfig extends EventEmitter {
|
||||
initialized: boolean;
|
||||
data: Partial<RegistryConfigType>;
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
this.initialized = false;
|
||||
|
@ -33,7 +38,7 @@ export default class RegistryConfig extends EventEmitter {
|
|||
try {
|
||||
const servers = await this.getServersListFromRegistry();
|
||||
if (servers.length) {
|
||||
this.data.teams.push(...servers);
|
||||
this.data.teams!.push(...servers);
|
||||
}
|
||||
} catch (error) {
|
||||
log.warn('[RegistryConfig] Nothing retrieved for \'DefaultServerList\'', error);
|
||||
|
@ -70,12 +75,12 @@ export default class RegistryConfig extends EventEmitter {
|
|||
*/
|
||||
async getServersListFromRegistry() {
|
||||
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
|
||||
return defaultServers.flat(2).reduce((servers, server, index) => {
|
||||
return defaultServers.flat(2).reduce((servers: Team[], server, index) => {
|
||||
if (server) {
|
||||
servers.push({
|
||||
name: server.name,
|
||||
url: server.value,
|
||||
order: server.order || index,
|
||||
name: (server as WindowsRegistry.RegistryItem).name,
|
||||
url: (server as WindowsRegistry.RegistryItem).value,
|
||||
order: index,
|
||||
});
|
||||
}
|
||||
return servers;
|
||||
|
@ -106,7 +111,7 @@ export default class RegistryConfig extends EventEmitter {
|
|||
* @param {string} key Path to the registry key to return
|
||||
* @param {string} name Name of specific entry in the registry key to retrieve (optional)
|
||||
*/
|
||||
async getRegistryEntry(key, name) {
|
||||
async getRegistryEntry(key: string, name?: string) {
|
||||
const results = [];
|
||||
for (const hive of REGISTRY_HIVE_LIST) {
|
||||
results.push(this.getRegistryEntryValues(hive, key, name));
|
||||
|
@ -121,18 +126,18 @@ export default class RegistryConfig extends EventEmitter {
|
|||
* @param {WindowsRegistry} regKey A configured instance of the WindowsRegistry class
|
||||
* @param {string} name Name of the specific entry to retrieve (optional)
|
||||
*/
|
||||
getRegistryEntryValues(hive, key, name) {
|
||||
getRegistryEntryValues(hive: string, key: string, name?: string) {
|
||||
const registry = new WindowsRegistry({hive, key, utf8: true});
|
||||
return new Promise((resolve, reject) => {
|
||||
return new Promise<string | WindowsRegistry.RegistryItem[] | undefined>((resolve, reject) => {
|
||||
try {
|
||||
registry.values((error, results) => {
|
||||
registry.values((error: Error, results: WindowsRegistry.RegistryItem[]) => {
|
||||
if (error || !results || results.length === 0) {
|
||||
resolve();
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
if (name) { // looking for a single entry value
|
||||
const registryItem = results.find((item) => item.name === name);
|
||||
resolve(registryItem && registryItem.value ? registryItem.value : null);
|
||||
resolve(registryItem && registryItem.value ? registryItem.value : undefined);
|
||||
} else { // looking for an entry list
|
||||
resolve(results);
|
||||
}
|
|
@ -2,6 +2,8 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BuildConfig} from 'types/config';
|
||||
|
||||
// For detailed guides, please refer to https://docs.mattermost.com/deployment/desktop-app-deployment.html
|
||||
|
||||
/**
|
||||
|
@ -17,7 +19,7 @@
|
|||
* when "enableServerManagement is set to false
|
||||
* @prop {[]} managedResources - Defines which paths are managed
|
||||
*/
|
||||
const buildConfig = {
|
||||
const buildConfig: BuildConfig = {
|
||||
defaultTeams: [/*
|
||||
{
|
||||
name: 'example',
|
|
@ -7,7 +7,9 @@
|
|||
* @param {number} version - Scheme version. (Not application version)
|
||||
*/
|
||||
|
||||
const getDefaultDownloadLocation = () => {
|
||||
import {ConfigV2} from 'types/config';
|
||||
|
||||
export const getDefaultDownloadLocation = () => {
|
||||
switch (process.platform) {
|
||||
case 'darwin':
|
||||
return `/Users/${process.env.USER || process.env.USERNAME}/Downloads`;
|
||||
|
@ -18,7 +20,7 @@ const getDefaultDownloadLocation = () => {
|
|||
}
|
||||
};
|
||||
|
||||
const defaultPreferences = {
|
||||
const defaultPreferences: ConfigV2 = {
|
||||
version: 2,
|
||||
teams: [],
|
||||
showTrayIcon: true,
|
|
@ -9,11 +9,21 @@ import {EventEmitter} from 'events';
|
|||
import {ipcMain, nativeTheme, app} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import * as Validator from '../../main/Validator';
|
||||
import {
|
||||
AnyConfig,
|
||||
BuildConfig,
|
||||
CombinedConfig,
|
||||
Config as ConfigType,
|
||||
LocalConfiguration,
|
||||
RegistryConfig as RegistryConfigType,
|
||||
Team,
|
||||
} from 'types/config';
|
||||
|
||||
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import defaultPreferences from './defaultPreferences';
|
||||
import * as Validator from '../../main/Validator';
|
||||
|
||||
import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences';
|
||||
import upgradeConfigData from './upgradePreferences';
|
||||
import buildConfig from './buildConfig';
|
||||
import RegistryConfig, {REGISTRY_READ_EVENT} from './RegistryConfig';
|
||||
|
@ -21,14 +31,25 @@ import RegistryConfig, {REGISTRY_READ_EVENT} from './RegistryConfig';
|
|||
/**
|
||||
* Handles loading and merging all sources of configuration as well as saving user provided config
|
||||
*/
|
||||
|
||||
export default class Config extends EventEmitter {
|
||||
constructor(configFilePath) {
|
||||
configFilePath: string;
|
||||
|
||||
registryConfig?: RegistryConfig;
|
||||
|
||||
combinedData?: CombinedConfig;
|
||||
registryConfigData?: Partial<RegistryConfigType>;
|
||||
defaultConfigData?: ConfigType;
|
||||
buildConfigData?: BuildConfig;
|
||||
localConfigData?: ConfigType;
|
||||
|
||||
constructor(configFilePath: string) {
|
||||
super();
|
||||
this.configFilePath = configFilePath;
|
||||
}
|
||||
|
||||
// separating constructor from init so main can setup event listeners
|
||||
init = () => {
|
||||
init = (): void => {
|
||||
this.registryConfig = new RegistryConfig();
|
||||
this.registryConfig.once(REGISTRY_READ_EVENT, this.loadRegistry);
|
||||
this.registryConfig.init();
|
||||
|
@ -40,7 +61,7 @@ export default class Config extends EventEmitter {
|
|||
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
|
||||
*/
|
||||
|
||||
loadRegistry = (registryData) => {
|
||||
loadRegistry = (registryData: Partial<RegistryConfigType>): void => {
|
||||
this.registryConfigData = registryData;
|
||||
this.reload();
|
||||
ipcMain.handle(GET_CONFIGURATION, this.handleGetConfiguration);
|
||||
|
@ -59,11 +80,11 @@ export default class Config extends EventEmitter {
|
|||
* @emits {update} emitted once all data has been loaded and merged
|
||||
* @emits {synchronize} emitted when requested by a call to method; used to notify other config instances of changes
|
||||
*/
|
||||
reload = () => {
|
||||
reload = (): void => {
|
||||
this.defaultConfigData = this.loadDefaultConfigData();
|
||||
this.buildConfigData = this.loadBuildConfigData();
|
||||
this.localConfigData = this.loadLocalConfigFile();
|
||||
this.localConfigData = this.checkForConfigUpdates(this.localConfigData);
|
||||
const loadedConfig = this.loadLocalConfigFile();
|
||||
this.localConfigData = this.checkForConfigUpdates(loadedConfig);
|
||||
this.regenerateCombinedConfigData();
|
||||
|
||||
this.emit('update', this.combinedData);
|
||||
|
@ -76,9 +97,9 @@ export default class Config extends EventEmitter {
|
|||
* @param {string} key name of config property to be saved
|
||||
* @param {*} data value to save for provided key
|
||||
*/
|
||||
set = (key, data) => {
|
||||
if (key) {
|
||||
this.localConfigData[key] = data;
|
||||
set = (key: keyof ConfigType, data: ConfigType[keyof ConfigType]): void => {
|
||||
if (key && this.localConfigData) {
|
||||
this.localConfigData = Object.assign({}, this.localConfigData, {[key]: data});
|
||||
this.regenerateCombinedConfigData();
|
||||
this.saveLocalConfigData();
|
||||
}
|
||||
|
@ -89,13 +110,9 @@ export default class Config extends EventEmitter {
|
|||
*
|
||||
* @param {array} properties an array of config properties to save
|
||||
*/
|
||||
setMultiple = (event, properties = []) => {
|
||||
setMultiple = (event: Electron.IpcMainEvent, properties: Array<{key: keyof ConfigType; data: ConfigType[keyof ConfigType]}> = []): Partial<ConfigType> | undefined => {
|
||||
if (properties.length) {
|
||||
properties.forEach(({key, data}) => {
|
||||
if (key) {
|
||||
this.localConfigData[key] = data;
|
||||
}
|
||||
});
|
||||
this.localConfigData = Object.assign({}, this.localConfigData, ...properties.map(({key, data}) => ({[key]: data})));
|
||||
this.regenerateCombinedConfigData();
|
||||
this.saveLocalConfigData();
|
||||
}
|
||||
|
@ -103,7 +120,7 @@ export default class Config extends EventEmitter {
|
|||
return this.localConfigData; //this is the only part that changes
|
||||
}
|
||||
|
||||
setRegistryConfigData = (registryConfigData = {teams: []}) => {
|
||||
setRegistryConfigData = (registryConfigData = {teams: []}): void => {
|
||||
this.registryConfigData = Object.assign({}, registryConfigData);
|
||||
this.reload();
|
||||
}
|
||||
|
@ -113,7 +130,7 @@ export default class Config extends EventEmitter {
|
|||
*
|
||||
* @param {object} configData a new, config data object to completely replace the existing config data
|
||||
*/
|
||||
replace = (configData) => {
|
||||
replace = (configData: ConfigType) => {
|
||||
const newConfigData = configData;
|
||||
|
||||
this.localConfigData = Object.assign({}, this.localConfigData, newConfigData);
|
||||
|
@ -129,11 +146,15 @@ export default class Config extends EventEmitter {
|
|||
* @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 = () => {
|
||||
saveLocalConfigData = (): void => {
|
||||
if (!this.localConfigData) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
this.writeFile(this.configFilePath, this.localConfigData, (error) => {
|
||||
this.writeFile(this.configFilePath, this.localConfigData, (error: NodeJS.ErrnoException | null) => {
|
||||
if (error) {
|
||||
throw new Error(error);
|
||||
throw new Error(error.message);
|
||||
}
|
||||
this.emit('update', this.combinedData);
|
||||
this.emit('synchronize');
|
||||
|
@ -149,13 +170,13 @@ export default class Config extends EventEmitter {
|
|||
return this.combinedData;
|
||||
}
|
||||
get localData() {
|
||||
return this.localConfigData;
|
||||
return this.localConfigData ?? defaultPreferences;
|
||||
}
|
||||
get defaultData() {
|
||||
return this.defaultConfigData;
|
||||
return this.defaultConfigData ?? defaultPreferences;
|
||||
}
|
||||
get buildData() {
|
||||
return this.buildConfigData;
|
||||
return this.buildConfigData ?? buildConfig;
|
||||
}
|
||||
get registryData() {
|
||||
return this.registryConfigData;
|
||||
|
@ -164,52 +185,55 @@ export default class Config extends EventEmitter {
|
|||
// convenience getters
|
||||
|
||||
get version() {
|
||||
return this.combinedData.version;
|
||||
return this.combinedData?.version ?? defaultPreferences.version;
|
||||
}
|
||||
get teams() {
|
||||
return this.combinedData.teams;
|
||||
return this.combinedData?.teams ?? defaultPreferences.teams;
|
||||
}
|
||||
get darkMode() {
|
||||
return this.combinedData.darkMode;
|
||||
return this.combinedData?.darkMode ?? defaultPreferences.darkMode;
|
||||
}
|
||||
get localTeams() {
|
||||
return this.localConfigData.teams;
|
||||
return this.localConfigData?.teams ?? defaultPreferences.version;
|
||||
}
|
||||
get predefinedTeams() {
|
||||
return [...this.buildConfigData.defaultTeams, ...this.registryConfigData.teams];
|
||||
return [...this.buildConfigData?.defaultTeams ?? [], ...this.registryConfigData?.teams ?? []];
|
||||
}
|
||||
get enableHardwareAcceleration() {
|
||||
return this.combinedData.enableHardwareAcceleration;
|
||||
return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration;
|
||||
}
|
||||
get enableServerManagement() {
|
||||
return this.combinedData.enableServerManagement;
|
||||
return this.combinedData?.enableServerManagement ?? buildConfig.enableServerManagement;
|
||||
}
|
||||
get enableAutoUpdater() {
|
||||
return this.combinedData.enableAutoUpdater;
|
||||
return this.combinedData?.enableAutoUpdater ?? buildConfig.enableAutoUpdater;
|
||||
}
|
||||
get autostart() {
|
||||
return this.combinedData.autostart;
|
||||
return this.combinedData?.autostart ?? defaultPreferences.autostart;
|
||||
}
|
||||
get notifications() {
|
||||
return this.combinedData.notifications;
|
||||
return this.combinedData?.notifications ?? defaultPreferences.notifications;
|
||||
}
|
||||
get showUnreadBadge() {
|
||||
return this.combinedData.showUnreadBadge;
|
||||
return this.combinedData?.showUnreadBadge ?? defaultPreferences.showUnreadBadge;
|
||||
}
|
||||
get useSpellChecker() {
|
||||
return this.combinedData.useSpellChecker;
|
||||
return this.combinedData?.useSpellChecker ?? defaultPreferences.useSpellChecker;
|
||||
}
|
||||
get spellCheckerLocale() {
|
||||
return this.combinedData.spellCheckerLocale;
|
||||
return this.combinedData?.spellCheckerLocale ?? defaultPreferences.spellCheckerLocale;
|
||||
}
|
||||
get showTrayIcon() {
|
||||
return this.combinedData.showTrayIcon;
|
||||
return this.combinedData?.showTrayIcon ?? defaultPreferences.showTrayIcon;
|
||||
}
|
||||
get trayIconTheme() {
|
||||
return this.combinedData.trayIconTheme;
|
||||
return this.combinedData?.trayIconTheme ?? defaultPreferences.trayIconTheme;
|
||||
}
|
||||
get downloadLocation() {
|
||||
return this.combinedData?.downloadLocation ?? getDefaultDownloadLocation();
|
||||
}
|
||||
get helpLink() {
|
||||
return this.combinedData.helpLink;
|
||||
return this.combinedData?.helpLink;
|
||||
}
|
||||
|
||||
// initialization/processing methods
|
||||
|
@ -231,22 +255,21 @@ export default class Config extends EventEmitter {
|
|||
/**
|
||||
* Loads and returns locally stored config data from the filesystem or returns app defaults if no file is found
|
||||
*/
|
||||
loadLocalConfigFile = () => {
|
||||
let configData = {};
|
||||
loadLocalConfigFile = (): AnyConfig => {
|
||||
let configData: AnyConfig;
|
||||
try {
|
||||
configData = this.readFileSync(this.configFilePath);
|
||||
|
||||
// validate based on config file version
|
||||
if (configData.version > 1) {
|
||||
configData = Validator.validateV2ConfigData(configData);
|
||||
} else {
|
||||
switch (configData.version) {
|
||||
case 1:
|
||||
configData = Validator.validateV1ConfigData(configData);
|
||||
break;
|
||||
default:
|
||||
configData = Validator.validateV0ConfigData(configData);
|
||||
}
|
||||
switch (configData.version) {
|
||||
case 2:
|
||||
configData = Validator.validateV2ConfigData(configData)!;
|
||||
break;
|
||||
case 1:
|
||||
configData = Validator.validateV1ConfigData(configData)!;
|
||||
break;
|
||||
default:
|
||||
configData = Validator.validateV0ConfigData(configData)!;
|
||||
}
|
||||
if (!configData) {
|
||||
throw new Error('Provided configuration file does not validate, using defaults instead.');
|
||||
|
@ -255,12 +278,6 @@ export default class Config extends EventEmitter {
|
|||
log.warn('Failed to load configuration file from the filesystem. Using defaults.');
|
||||
configData = this.copy(this.defaultConfigData);
|
||||
|
||||
// add default team to teams if one exists and there arent currently any teams
|
||||
if (!configData.teams.length && this.defaultConfigData.defaultTeam) {
|
||||
configData.teams.push(this.defaultConfigData.defaultTeam);
|
||||
}
|
||||
delete configData.defaultTeam;
|
||||
|
||||
this.writeFileSync(this.configFilePath, configData);
|
||||
}
|
||||
return configData;
|
||||
|
@ -271,18 +288,21 @@ export default class Config extends EventEmitter {
|
|||
*
|
||||
* @param {*} data locally stored data
|
||||
*/
|
||||
checkForConfigUpdates = (data) => {
|
||||
checkForConfigUpdates = (data: AnyConfig) => {
|
||||
let configData = data;
|
||||
try {
|
||||
if (configData.version !== this.defaultConfigData.version) {
|
||||
configData = upgradeConfigData(configData);
|
||||
this.writeFileSync(this.configFilePath, configData);
|
||||
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
|
||||
if (this.defaultConfigData) {
|
||||
try {
|
||||
if (configData.version !== this.defaultConfigData.version) {
|
||||
configData = upgradeConfigData(configData);
|
||||
this.writeFileSync(this.configFilePath, configData);
|
||||
log.info(`Configuration updated to version ${this.defaultConfigData.version} successfully.`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
|
||||
}
|
||||
} catch (error) {
|
||||
log.error(`Failed to update configuration to version ${this.defaultConfigData.version}.`);
|
||||
}
|
||||
return configData;
|
||||
|
||||
return configData as ConfigType;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -293,38 +313,37 @@ export default class Config extends EventEmitter {
|
|||
this.combinedData = Object.assign({}, this.defaultConfigData, this.localConfigData, this.buildConfigData, this.registryConfigData);
|
||||
|
||||
// remove unecessary data pulled from default and build config
|
||||
delete this.combinedData.defaultTeam;
|
||||
delete this.combinedData.defaultTeams;
|
||||
delete this.combinedData!.defaultTeams;
|
||||
|
||||
// IMPORTANT: properly combine teams from all sources
|
||||
let combinedTeams = [];
|
||||
|
||||
// - start by adding default teams from buildConfig, if any
|
||||
if (this.buildConfigData.defaultTeams && this.buildConfigData.defaultTeams.length) {
|
||||
if (this.buildConfigData?.defaultTeams?.length) {
|
||||
combinedTeams.push(...this.buildConfigData.defaultTeams);
|
||||
}
|
||||
|
||||
// - add registry defined teams, if any
|
||||
if (this.registryConfigData.teams && this.registryConfigData.teams.length) {
|
||||
if (this.registryConfigData?.teams?.length) {
|
||||
combinedTeams.push(...this.registryConfigData.teams);
|
||||
}
|
||||
|
||||
// - add locally defined teams only if server management is enabled
|
||||
if (this.enableServerManagement) {
|
||||
combinedTeams.push(...this.localConfigData.teams);
|
||||
if (this.localConfigData && this.enableServerManagement) {
|
||||
combinedTeams.push(...this.localConfigData.teams || []);
|
||||
}
|
||||
|
||||
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
|
||||
combinedTeams = this.sortUnorderedTeams(combinedTeams);
|
||||
|
||||
this.combinedData.teams = combinedTeams;
|
||||
this.combinedData.localTeams = this.localConfigData.teams;
|
||||
this.combinedData.buildTeams = this.buildConfigData.defaultTeams;
|
||||
this.combinedData.registryTeams = this.registryConfigData.teams;
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
|
||||
if (this.combinedData) {
|
||||
this.combinedData.teams = combinedTeams;
|
||||
this.combinedData.registryTeams = this.registryConfigData?.teams || [];
|
||||
if (process.platform === 'darwin' || process.platform === 'win32') {
|
||||
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
|
||||
}
|
||||
this.combinedData.appName = app.name;
|
||||
}
|
||||
this.combinedData.appName = app.name;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -332,7 +351,7 @@ export default class Config extends EventEmitter {
|
|||
*
|
||||
* @param {array} teams array of teams to check for duplicates
|
||||
*/
|
||||
filterOutDuplicateTeams = (teams) => {
|
||||
filterOutDuplicateTeams = (teams: Team[]) => {
|
||||
let newTeams = teams;
|
||||
const uniqueURLs = new Set();
|
||||
newTeams = newTeams.filter((team) => {
|
||||
|
@ -345,7 +364,7 @@ export default class Config extends EventEmitter {
|
|||
* 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) => {
|
||||
filterOutPredefinedTeams = (teams: Team[]) => {
|
||||
let newTeams = teams;
|
||||
|
||||
// filter out predefined teams
|
||||
|
@ -360,17 +379,17 @@ export default class Config extends EventEmitter {
|
|||
* Apply a default sort order to the team list, if no order is specified.
|
||||
* @param {array} teams to sort
|
||||
*/
|
||||
sortUnorderedTeams = (teams) => {
|
||||
sortUnorderedTeams = (teams: Team[]) => {
|
||||
// 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}));
|
||||
|
||||
// Make a best pass at interpreting sort order. If an order is not specified, assume it is 0.
|
||||
//
|
||||
const newTeams = mappedTeams.sort((x, y) => {
|
||||
if (x.team.order == null) {
|
||||
if (!x.team.order) {
|
||||
x.team.order = 0;
|
||||
}
|
||||
if (y.team.order == null) {
|
||||
if (!y.team.order) {
|
||||
y.team.order = 0;
|
||||
}
|
||||
|
||||
|
@ -390,11 +409,15 @@ export default class Config extends EventEmitter {
|
|||
|
||||
// helper functions
|
||||
|
||||
readFileSync = (filePath) => {
|
||||
readFileSync = (filePath: string) => {
|
||||
return JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
||||
}
|
||||
|
||||
writeFile = (filePath, configData, callback) => {
|
||||
writeFile = (filePath: string, configData: Partial<ConfigType>, callback: fs.NoParamCallback) => {
|
||||
if (!this.defaultConfigData) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configData.version !== this.defaultConfigData.version) {
|
||||
throw new Error('version ' + configData.version + ' is not equal to ' + this.defaultConfigData.version);
|
||||
}
|
||||
|
@ -402,7 +425,11 @@ export default class Config extends EventEmitter {
|
|||
fs.writeFile(filePath, json, 'utf8', callback);
|
||||
}
|
||||
|
||||
writeFileSync = (filePath, config) => {
|
||||
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);
|
||||
}
|
||||
|
@ -416,15 +443,15 @@ export default class Config extends EventEmitter {
|
|||
fs.writeFileSync(filePath, json, 'utf8');
|
||||
}
|
||||
|
||||
merge = (base, target) => {
|
||||
merge = <T, T2>(base: T, target: T2) => {
|
||||
return Object.assign({}, base, target);
|
||||
}
|
||||
|
||||
copy = (data) => {
|
||||
copy = <T>(data: T) => {
|
||||
return Object.assign({}, data);
|
||||
}
|
||||
|
||||
handleGetConfiguration = (event, option) => {
|
||||
handleGetConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof CombinedConfig) => {
|
||||
const config = {...this.combinedData};
|
||||
if (option) {
|
||||
return config[option];
|
||||
|
@ -432,19 +459,19 @@ export default class Config extends EventEmitter {
|
|||
return config;
|
||||
}
|
||||
|
||||
handleGetLocalConfiguration = (event, option) => {
|
||||
const config = {...this.localConfigData};
|
||||
handleGetLocalConfiguration = (event: Electron.IpcMainInvokeEvent, option: keyof ConfigType) => {
|
||||
const config: Partial<LocalConfiguration> = {...this.localConfigData};
|
||||
config.appName = app.name;
|
||||
config.enableServerManagement = this.combinedData.enableServerManagement;
|
||||
config.enableServerManagement = this.combinedData?.enableServerManagement;
|
||||
if (option) {
|
||||
return config[option];
|
||||
}
|
||||
return config;
|
||||
}
|
||||
|
||||
handleUpdateTeams = (event, newTeams) => {
|
||||
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: Team[]) => {
|
||||
this.set('teams', newTeams);
|
||||
return this.combinedData.teams;
|
||||
return this.combinedData!.teams;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -452,7 +479,7 @@ export default class Config extends EventEmitter {
|
|||
* @emits 'darkModeChange'
|
||||
*/
|
||||
handleUpdateTheme = () => {
|
||||
if (this.combinedData.darkMode !== nativeTheme.shouldUseDarkColors) {
|
||||
if (this.combinedData && this.combinedData.darkMode !== nativeTheme.shouldUseDarkColors) {
|
||||
this.combinedData.darkMode = nativeTheme.shouldUseDarkColors;
|
||||
this.emit('darkModeChange', this.combinedData.darkMode);
|
||||
}
|
||||
|
@ -463,6 +490,10 @@ export default class Config extends EventEmitter {
|
|||
* @emits 'darkModeChange'
|
||||
*/
|
||||
toggleDarkModeManually = () => {
|
||||
if (!this.combinedData) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.set('darkMode', !this.combinedData.darkMode);
|
||||
this.emit('darkModeChange', this.combinedData.darkMode);
|
||||
}
|
|
@ -1,12 +1,14 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {ConfigV0, ConfigV1} from 'types/config';
|
||||
|
||||
import defaultPreferences from './defaultPreferences';
|
||||
|
||||
const pastDefaultPreferences = {
|
||||
0: {
|
||||
url: '',
|
||||
},
|
||||
} as ConfigV0,
|
||||
1: {
|
||||
version: 1,
|
||||
teams: [],
|
||||
|
@ -23,9 +25,8 @@ const pastDefaultPreferences = {
|
|||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
spellCheckerLocale: 'en-US',
|
||||
},
|
||||
} as ConfigV1,
|
||||
2: defaultPreferences,
|
||||
};
|
||||
|
||||
pastDefaultPreferences[`${defaultPreferences.version}`] = defaultPreferences;
|
||||
|
||||
export default pastDefaultPreferences;
|
|
@ -1,42 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import pastDefaultPreferences from './pastDefaultPreferences';
|
||||
|
||||
function deepCopy(object) {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
function upgradeV0toV1(configV0) {
|
||||
const config = deepCopy(pastDefaultPreferences['1']);
|
||||
if (config.version !== 1) {
|
||||
throw new Error('pastDefaultPreferences[\'1\'].version is not equal to 1');
|
||||
}
|
||||
config.teams.push({
|
||||
name: 'Primary team',
|
||||
url: configV0.url,
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
function upgradeV1toV2(configV1) {
|
||||
const config = deepCopy(configV1);
|
||||
config.version = 2;
|
||||
config.teams.forEach((value, index) => {
|
||||
value.order = index;
|
||||
});
|
||||
config.darkMode = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
export default function upgradeToLatest(config) {
|
||||
const configVersion = config.version ? config.version : 0;
|
||||
switch (configVersion) {
|
||||
case 1:
|
||||
return upgradeToLatest(upgradeV1toV2(config));
|
||||
case 0:
|
||||
return upgradeToLatest(upgradeV0toV1(config));
|
||||
default:
|
||||
return config;
|
||||
}
|
||||
}
|
42
src/common/config/upgradePreferences.ts
Normal file
42
src/common/config/upgradePreferences.ts
Normal file
|
@ -0,0 +1,42 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
|
||||
|
||||
import pastDefaultPreferences from './pastDefaultPreferences';
|
||||
|
||||
function deepCopy<T>(object: T): T {
|
||||
return JSON.parse(JSON.stringify(object));
|
||||
}
|
||||
|
||||
function upgradeV0toV1(configV0: ConfigV0) {
|
||||
const config = deepCopy(pastDefaultPreferences[1]);
|
||||
config.teams.push({
|
||||
name: 'Primary team',
|
||||
url: configV0.url,
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
function upgradeV1toV2(configV1: ConfigV1) {
|
||||
const config: ConfigV2 = Object.assign({}, deepCopy<ConfigV2>(pastDefaultPreferences[2]), configV1);
|
||||
config.version = 2;
|
||||
config.teams = configV1.teams.map((value, index) => {
|
||||
return {
|
||||
...value,
|
||||
order: index,
|
||||
};
|
||||
});
|
||||
return config;
|
||||
}
|
||||
|
||||
export default function upgradeToLatest(config: AnyConfig): ConfigV2 {
|
||||
switch (config.version) {
|
||||
case 2:
|
||||
return config as ConfigV2;
|
||||
case 1:
|
||||
return upgradeToLatest(upgradeV1toV2(config as ConfigV1));
|
||||
default:
|
||||
return upgradeToLatest(upgradeV0toV1(config as ConfigV0));
|
||||
}
|
||||
}
|
|
@ -3,6 +3,6 @@
|
|||
// See LICENSE.txt for license information.
|
||||
import deepmerge from 'deepmerge';
|
||||
|
||||
export default function deepMergeProxy(x, y, options) {
|
||||
export default function deepMergeProxy<T>(x: Partial<T>, y: Partial<T>, options: deepmerge.Options) {
|
||||
return deepmerge(x, y, options); // due to webpack conversion
|
||||
}
|
|
@ -9,7 +9,7 @@ const releaseSplit = os.release().split('.');
|
|||
export default {
|
||||
major: parseInt(releaseSplit[0], 10),
|
||||
minor: parseInt(releaseSplit[1], 10),
|
||||
isLowerThanOrEqualWindows8_1() {
|
||||
isLowerThanOrEqualWindows8_1(): boolean {
|
||||
if (process.platform !== 'win32') {
|
||||
return false;
|
||||
}
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Team} from 'types/config';
|
||||
import {ServerFromURL} from 'types/utils';
|
||||
import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
|
||||
|
||||
import buildConfig from '../config/buildConfig';
|
||||
|
@ -19,34 +21,31 @@ const customLoginRegexPaths = [
|
|||
/^\/login\/sso\/saml$/i,
|
||||
];
|
||||
|
||||
function getDomain(inputURL) {
|
||||
function getDomain(inputURL: URL | string) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
return parsedURL.origin;
|
||||
return parsedURL?.origin;
|
||||
}
|
||||
|
||||
function isValidURL(testURL) {
|
||||
function isValidURL(testURL: string) {
|
||||
return Boolean(isHttpUri(testURL) || isHttpsUri(testURL)) && parseURL(testURL) !== null;
|
||||
}
|
||||
|
||||
function isValidURI(testURL) {
|
||||
function isValidURI(testURL: string) {
|
||||
return Boolean(isUri(testURL));
|
||||
}
|
||||
|
||||
function parseURL(inputURL) {
|
||||
if (!inputURL) {
|
||||
return null;
|
||||
}
|
||||
function parseURL(inputURL: URL | string) {
|
||||
if (inputURL instanceof URL) {
|
||||
return inputURL;
|
||||
}
|
||||
try {
|
||||
return new URL(inputURL.replace(/([^:]\/)\/+/g, '$1'));
|
||||
} catch (e) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function getHost(inputURL) {
|
||||
function getHost(inputURL: URL | string) {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (parsedURL) {
|
||||
return parsedURL.origin;
|
||||
|
@ -57,7 +56,7 @@ function getHost(inputURL) {
|
|||
// isInternalURL determines if the target url is internal to the application.
|
||||
// - currentURL is the current url inside the webview
|
||||
// - basename is the global export from the Mattermost application defining the subpath, if any
|
||||
function isInternalURL(targetURL, currentURL, basename = '/') {
|
||||
function isInternalURL(targetURL: URL, currentURL: URL, basename = '/') {
|
||||
if (targetURL.host !== currentURL.host) {
|
||||
return false;
|
||||
}
|
||||
|
@ -69,16 +68,16 @@ function isInternalURL(targetURL, currentURL, basename = '/') {
|
|||
return true;
|
||||
}
|
||||
|
||||
function getServerInfo(serverUrl) {
|
||||
function getServerInfo(serverUrl: URL | string) {
|
||||
const parsedServer = parseURL(serverUrl);
|
||||
if (!parsedServer) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
// does the server have a subpath?
|
||||
const pn = parsedServer.pathname.toLowerCase();
|
||||
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
||||
return {origin: parsedServer.origin, subpath, url: parsedServer};
|
||||
return {subpath, url: parsedServer};
|
||||
}
|
||||
|
||||
function getManagedResources() {
|
||||
|
@ -89,20 +88,23 @@ function getManagedResources() {
|
|||
return buildConfig.managedResources || [];
|
||||
}
|
||||
|
||||
function isAdminUrl(serverUrl, inputUrl) {
|
||||
function isAdminUrl(serverUrl: URL | string, inputUrl: URL | string) {
|
||||
const parsedURL = parseURL(inputUrl);
|
||||
const server = getServerInfo(serverUrl);
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server.url, parsedURL))) {
|
||||
return null;
|
||||
}
|
||||
return (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}/admin_console/`) ||
|
||||
parsedURL.pathname.toLowerCase().startsWith('/admin_console/'));
|
||||
}
|
||||
|
||||
function isTeamUrl(serverUrl, inputUrl, withApi) {
|
||||
function isTeamUrl(serverUrl: URL | string, inputUrl: URL | string, withApi?: boolean) {
|
||||
if (!serverUrl || !inputUrl) {
|
||||
return false;
|
||||
}
|
||||
const parsedURL = parseURL(inputUrl);
|
||||
const server = getServerInfo(serverUrl);
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server, parsedURL))) {
|
||||
if (!parsedURL || !server || (!equalUrlsIgnoringSubpath(server.url, parsedURL))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -128,19 +130,19 @@ function isTeamUrl(serverUrl, inputUrl, withApi) {
|
|||
parsedURL.pathname.toLowerCase().startsWith(`/${testPath}/`))));
|
||||
}
|
||||
|
||||
function isPluginUrl(serverUrl, inputURL) {
|
||||
function isPluginUrl(serverUrl: URL | string, inputURL: URL | string) {
|
||||
const server = getServerInfo(serverUrl);
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL || !server) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
equalUrlsIgnoringSubpath(server, parsedURL) &&
|
||||
equalUrlsIgnoringSubpath(server.url, parsedURL) &&
|
||||
(parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}plugins/`) ||
|
||||
parsedURL.pathname.toLowerCase().startsWith('/plugins/')));
|
||||
}
|
||||
|
||||
function isManagedResource(serverUrl, inputURL) {
|
||||
function isManagedResource(serverUrl: URL | string, inputURL: URL | string) {
|
||||
const server = getServerInfo(serverUrl);
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL || !server) {
|
||||
|
@ -150,19 +152,22 @@ function isManagedResource(serverUrl, inputURL) {
|
|||
const managedResources = getManagedResources();
|
||||
|
||||
return (
|
||||
equalUrlsIgnoringSubpath(server, parsedURL) && managedResources && managedResources.length &&
|
||||
equalUrlsIgnoringSubpath(server.url, parsedURL) && managedResources && managedResources.length &&
|
||||
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
|
||||
}
|
||||
|
||||
function getServer(inputURL, teams, ignoreScheme = false) {
|
||||
function getServer(inputURL: URL | string, teams: Team[], ignoreScheme = false): ServerFromURL | undefined {
|
||||
const parsedURL = parseURL(inputURL);
|
||||
if (!parsedURL) {
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
let parsedServerUrl;
|
||||
let secondOption = null;
|
||||
let secondOption;
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
parsedServerUrl = parseURL(teams[i].url);
|
||||
if (!parsedServerUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
|
@ -180,21 +185,21 @@ function getServer(inputURL, teams, ignoreScheme = false) {
|
|||
}
|
||||
|
||||
// next two functions are defined to clarify intent
|
||||
function equalUrlsWithSubpath(url1, url2, ignoreScheme) {
|
||||
function equalUrlsWithSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
|
||||
if (ignoreScheme) {
|
||||
return url1.host === url2.host && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
||||
}
|
||||
return url1.origin === url2.origin && url2.pathname.toLowerCase().startsWith(url1.pathname.toLowerCase());
|
||||
}
|
||||
|
||||
function equalUrlsIgnoringSubpath(url1, url2, ignoreScheme) {
|
||||
function equalUrlsIgnoringSubpath(url1: URL, url2: URL, ignoreScheme?: boolean) {
|
||||
if (ignoreScheme) {
|
||||
return url1.host.toLowerCase() === url2.host.toLowerCase();
|
||||
}
|
||||
return url1.origin.toLowerCase() === url2.origin.toLowerCase();
|
||||
}
|
||||
|
||||
function isTrustedURL(url, teams) {
|
||||
function isTrustedURL(url: URL | string, teams: Team[]) {
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return false;
|
||||
|
@ -202,8 +207,8 @@ function isTrustedURL(url, teams) {
|
|||
return getServer(parsedURL, teams) !== null;
|
||||
}
|
||||
|
||||
function isCustomLoginURL(url, server, teams) {
|
||||
const subpath = (server === null || typeof server === 'undefined') ? '' : server.url.pathname;
|
||||
function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: Team[]): boolean {
|
||||
const subpath = server ? server.url.pathname : '';
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
return false;
|
||||
|
@ -212,7 +217,7 @@ function isCustomLoginURL(url, server, teams) {
|
|||
return false;
|
||||
}
|
||||
const urlPath = parsedURL.pathname;
|
||||
if ((subpath !== '' || subpath !== '/') && urlPath.startsWith(subpath)) {
|
||||
if (subpath !== '' && subpath !== '/' && urlPath.startsWith(subpath)) {
|
||||
const replacement = subpath.endsWith('/') ? '/' : '';
|
||||
const replacedPath = urlPath.replace(subpath, replacement);
|
||||
for (const regexPath of customLoginRegexPaths) {
|
|
@ -2,7 +2,7 @@
|
|||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import electron, {BrowserWindow} from 'electron';
|
||||
import electron from 'electron';
|
||||
|
||||
import {DEVELOPMENT, PRODUCTION} from './constants';
|
||||
|
||||
|
@ -27,26 +27,9 @@ function runMode() {
|
|||
return process.env.NODE_ENV === PRODUCTION ? PRODUCTION : DEVELOPMENT;
|
||||
}
|
||||
|
||||
// workaround until electron 12 hits, since fromWebContents return a null value if using a webcontent from browserview
|
||||
function browserWindowFromWebContents(content) {
|
||||
let window;
|
||||
if (content.type === 'browserview') {
|
||||
for (const win of BrowserWindow.getAllWindows()) {
|
||||
for (const view of win.getBrowserViews()) {
|
||||
if (view.webContents.id === content.id) {
|
||||
window = win;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
window = BrowserWindow.fromWebContents(content);
|
||||
}
|
||||
return window;
|
||||
}
|
||||
|
||||
const DEFAULT_MAX = 20;
|
||||
|
||||
function shorten(string, max) {
|
||||
function shorten(string: string, max?: number) {
|
||||
const maxLength = (max && max >= 4) ? max : DEFAULT_MAX;
|
||||
if (string.length >= maxLength) {
|
||||
return `${string.slice(0, maxLength - 3)}...`;
|
||||
|
@ -57,6 +40,5 @@ function shorten(string, max) {
|
|||
export default {
|
||||
getDisplayBoundaries,
|
||||
runMode,
|
||||
browserWindowFromWebContents,
|
||||
shorten,
|
||||
};
|
|
@ -2,12 +2,14 @@
|
|||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import {AppState} from 'types/appState';
|
||||
|
||||
import JsonFileManager from '../common/JsonFileManager';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default class AppVersionManager extends JsonFileManager {
|
||||
constructor(file) {
|
||||
export default class AppVersionManager extends JsonFileManager<AppState> {
|
||||
constructor(file: string) {
|
||||
super(file);
|
||||
|
||||
// ensure data loaded from file is valid
|
||||
|
@ -33,7 +35,7 @@ export default class AppVersionManager extends JsonFileManager {
|
|||
}
|
||||
|
||||
set updateCheckedDate(date) {
|
||||
this.setValue('updateCheckedDate', date.toISOString());
|
||||
this.setValue('updateCheckedDate', date?.toISOString());
|
||||
}
|
||||
|
||||
get updateCheckedDate() {
|
|
@ -8,6 +8,8 @@ import isDev from 'electron-is-dev';
|
|||
import log from 'electron-log';
|
||||
|
||||
export default class AutoLauncher {
|
||||
appLauncher: AutoLaunch;
|
||||
|
||||
constructor() {
|
||||
this.appLauncher = new AutoLaunch({
|
||||
name: app.name,
|
|
@ -7,7 +7,7 @@ import fs from 'fs';
|
|||
import os from 'os';
|
||||
import path from 'path';
|
||||
|
||||
import {app, dialog} from 'electron';
|
||||
import {app, BrowserWindow, dialog} from 'electron';
|
||||
|
||||
import log from 'electron-log';
|
||||
|
||||
|
@ -15,15 +15,17 @@ const BUTTON_OK = 'OK';
|
|||
const BUTTON_SHOW_DETAILS = 'Show Details';
|
||||
const BUTTON_REOPEN = 'Reopen';
|
||||
|
||||
function createErrorReport(err) {
|
||||
function createErrorReport(err: Error) {
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
return `Application: ${app.name} ${app.getVersion()} [commit: ${__HASH_VERSION__}]\n` +
|
||||
`Platform: ${os.type()} ${os.release()} ${os.arch()}\n` +
|
||||
`${err.stack}`;
|
||||
}
|
||||
|
||||
function openDetachedExternal(url) {
|
||||
const spawnOption = {detached: true, stdio: 'ignore'};
|
||||
function openDetachedExternal(url: string) {
|
||||
const spawnOption = {detached: true, stdio: 'ignore' as any};
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
return spawn('cmd', ['/C', 'start', url], spawnOption);
|
||||
|
@ -32,20 +34,21 @@ function openDetachedExternal(url) {
|
|||
case 'linux':
|
||||
return spawn('xdg-open', [url], spawnOption);
|
||||
default:
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
export default class CriticalErrorHandler {
|
||||
constructor() {
|
||||
this.mainWindow = null;
|
||||
}
|
||||
mainWindow?: BrowserWindow;
|
||||
|
||||
setMainWindow(mainWindow) {
|
||||
setMainWindow(mainWindow: BrowserWindow) {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
windowUnresponsiveHandler() {
|
||||
if (!this.mainWindow) {
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox(this.mainWindow, {
|
||||
type: 'warning',
|
||||
title: app.name,
|
||||
|
@ -59,7 +62,7 @@ export default class CriticalErrorHandler {
|
|||
});
|
||||
}
|
||||
|
||||
processUncaughtExceptionHandler(err) {
|
||||
processUncaughtExceptionHandler(err: Error) {
|
||||
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
|
||||
const report = createErrorReport(err);
|
||||
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
|
||||
|
@ -69,9 +72,11 @@ export default class CriticalErrorHandler {
|
|||
if (process.platform === 'darwin') {
|
||||
buttons.reverse();
|
||||
}
|
||||
const bindWindow = this.mainWindow && this.mainWindow.isVisible() ? this.mainWindow : null;
|
||||
if (!this.mainWindow?.isVisible) {
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox(
|
||||
bindWindow,
|
||||
this.mainWindow,
|
||||
{
|
||||
type: 'error',
|
||||
title: app.name,
|
||||
|
@ -102,7 +107,7 @@ export default class CriticalErrorHandler {
|
|||
app.exit(-1);
|
||||
});
|
||||
} else {
|
||||
log.err(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
|
||||
log.error(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
|
@ -4,9 +4,11 @@
|
|||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export class MattermostServer {
|
||||
constructor(name, serverUrl) {
|
||||
name: string;
|
||||
url: URL;
|
||||
constructor(name: string, serverUrl: string) {
|
||||
this.name = name;
|
||||
this.url = urlUtils.parseURL(serverUrl);
|
||||
this.url = urlUtils.parseURL(serverUrl)!;
|
||||
if (!this.url) {
|
||||
throw new Error('Invalid url for creating a server');
|
||||
}
|
||||
|
@ -19,12 +21,12 @@ export class MattermostServer {
|
|||
return {origin: this.url.origin, subpath, url: this.url.toString()};
|
||||
}
|
||||
|
||||
sameOrigin = (otherURL) => {
|
||||
sameOrigin = (otherURL: string) => {
|
||||
const parsedUrl = urlUtils.parseURL(otherURL);
|
||||
return parsedUrl && this.url.origin === parsedUrl.origin;
|
||||
}
|
||||
|
||||
equals = (otherServer) => {
|
||||
equals = (otherServer: MattermostServer) => {
|
||||
return (this.name === otherServer.name) && (this.url.toString() === otherServer.url.toString());
|
||||
}
|
||||
}
|
|
@ -1,17 +1,18 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Args} from 'types/args';
|
||||
import yargs from 'yargs';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default function parse(args) {
|
||||
export default function parse(args: string[]) {
|
||||
return validateArgs(parseArgs(triageArgs(args)));
|
||||
}
|
||||
|
||||
function triageArgs(args) {
|
||||
function triageArgs(args: string[]) {
|
||||
// ensure any args following a possible deeplink are discarded
|
||||
if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
|
||||
const scheme = protocols[0].schemes[0].toLowerCase();
|
||||
|
@ -23,7 +24,7 @@ function triageArgs(args) {
|
|||
return args;
|
||||
}
|
||||
|
||||
function parseArgs(args) {
|
||||
function parseArgs(args: string[]) {
|
||||
return yargs.
|
||||
alias('dataDir', 'd').string('dataDir').describe('dataDir', 'Set the path to where user data is stored.').
|
||||
alias('disableDevMode', 'p').boolean('disableDevMode').describe('disableDevMode', 'Disable development mode. Allows for testing as if it was Production.').
|
||||
|
@ -32,6 +33,6 @@ function parseArgs(args) {
|
|||
parse(args);
|
||||
}
|
||||
|
||||
function validateArgs(args) {
|
||||
function validateArgs(args: Args) {
|
||||
return Validator.validateArgs(args) || {};
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import EventEmitter from 'events';
|
||||
import {EventEmitter} from 'events';
|
||||
|
||||
import electron from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
@ -12,12 +12,21 @@ const {app} = electron;
|
|||
* Monitors system idle time, listens for system events and fires status updates as needed
|
||||
*/
|
||||
export default class UserActivityMonitor extends EventEmitter {
|
||||
isActive: boolean;
|
||||
idleTime: number;
|
||||
lastSetActive?: number;
|
||||
systemIdleTimeIntervalID: number;
|
||||
config: {
|
||||
updateFrequencyMs: number;
|
||||
inactiveThresholdMs: number;
|
||||
statusUpdateThresholdMs: number;
|
||||
};
|
||||
|
||||
constructor() {
|
||||
super();
|
||||
|
||||
this.isActive = true;
|
||||
this.idleTime = 0;
|
||||
this.lastSetActive = null;
|
||||
this.systemIdleTimeIntervalID = -1;
|
||||
|
||||
this.config = {
|
||||
|
@ -58,13 +67,14 @@ export default class UserActivityMonitor extends EventEmitter {
|
|||
|
||||
this.config = Object.assign({}, this.config, config);
|
||||
|
||||
// TODO: Node typings don't map Timeout to number, but then clearInterval requires a number?
|
||||
this.systemIdleTimeIntervalID = setInterval(() => {
|
||||
try {
|
||||
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
|
||||
} catch (err) {
|
||||
log.error('Error getting system idle time:', err);
|
||||
}
|
||||
}, this.config.updateFrequencyMs);
|
||||
}, this.config.updateFrequencyMs) as unknown as number;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -80,7 +90,7 @@ export default class UserActivityMonitor extends EventEmitter {
|
|||
* @param {integer} idleTime
|
||||
* @private
|
||||
*/
|
||||
updateIdleTime(idleTime) {
|
||||
updateIdleTime(idleTime: number) {
|
||||
this.idleTime = idleTime;
|
||||
if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers
|
||||
this.setActivityState(false);
|
||||
|
@ -110,7 +120,7 @@ export default class UserActivityMonitor extends EventEmitter {
|
|||
this.sendStatusUpdate(false);
|
||||
this.lastSetActive = now;
|
||||
} else if (!isActive) {
|
||||
this.lastSetActive = null;
|
||||
delete this.lastSetActive;
|
||||
}
|
||||
}
|
||||
|
|
@ -4,6 +4,13 @@ import log from 'electron-log';
|
|||
|
||||
import Joi from '@hapi/joi';
|
||||
|
||||
import {Args} from 'types/args';
|
||||
import {ConfigV0, ConfigV1, ConfigV2} from 'types/config';
|
||||
import {SavedWindowState} from 'types/mainWindow';
|
||||
import {AppState} from 'types/appState';
|
||||
import {ComparableCertificate} from 'types/certificate';
|
||||
import {PermissionType, TrustedOrigin} from 'types/trustedOrigin';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
const defaultOptions = {
|
||||
|
@ -14,14 +21,14 @@ const defaultWindowHeight = 700;
|
|||
const minWindowWidth = 400;
|
||||
const minWindowHeight = 240;
|
||||
|
||||
const argsSchema = Joi.object({
|
||||
const argsSchema = Joi.object<Args>({
|
||||
hidden: Joi.boolean(),
|
||||
disableDevMode: Joi.boolean(),
|
||||
dataDir: Joi.string(),
|
||||
version: Joi.boolean(),
|
||||
});
|
||||
|
||||
const boundsInfoSchema = Joi.object({
|
||||
const boundsInfoSchema = Joi.object<SavedWindowState>({
|
||||
x: Joi.number().integer().default(0),
|
||||
y: Joi.number().integer().default(0),
|
||||
width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth),
|
||||
|
@ -30,17 +37,17 @@ const boundsInfoSchema = Joi.object({
|
|||
fullscreen: Joi.boolean().default(false),
|
||||
});
|
||||
|
||||
const appStateSchema = Joi.object({
|
||||
const appStateSchema = Joi.object<AppState>({
|
||||
lastAppVersion: Joi.string(),
|
||||
skippedVersion: Joi.string(),
|
||||
updateCheckedDate: Joi.string(),
|
||||
});
|
||||
|
||||
const configDataSchemaV0 = Joi.object({
|
||||
const configDataSchemaV0 = Joi.object<ConfigV0>({
|
||||
url: Joi.string().required(),
|
||||
});
|
||||
|
||||
const configDataSchemaV1 = Joi.object({
|
||||
const configDataSchemaV1 = Joi.object<ConfigV1>({
|
||||
version: Joi.number().min(1).default(1),
|
||||
teams: Joi.array().items(Joi.object({
|
||||
name: Joi.string().required(),
|
||||
|
@ -61,7 +68,7 @@ const configDataSchemaV1 = Joi.object({
|
|||
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
|
||||
});
|
||||
|
||||
const configDataSchemaV2 = Joi.object({
|
||||
const configDataSchemaV2 = Joi.object<ConfigV2>({
|
||||
version: Joi.number().min(2).default(2),
|
||||
teams: Joi.array().items(Joi.object({
|
||||
name: Joi.string().required(),
|
||||
|
@ -88,13 +95,13 @@ const configDataSchemaV2 = Joi.object({
|
|||
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
|
||||
const certificateStoreSchema = Joi.object().pattern(
|
||||
Joi.string().uri(),
|
||||
Joi.object({
|
||||
Joi.object<ComparableCertificate>({
|
||||
data: Joi.string(),
|
||||
issuerName: Joi.string(),
|
||||
}),
|
||||
);
|
||||
|
||||
const originPermissionsSchema = Joi.object().keys({
|
||||
const originPermissionsSchema = Joi.object<TrustedOrigin>().keys({
|
||||
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
|
||||
});
|
||||
|
||||
|
@ -108,27 +115,27 @@ const trustedOriginsSchema = Joi.object({}).pattern(
|
|||
const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i));
|
||||
|
||||
// validate bounds_info.json
|
||||
export function validateArgs(data) {
|
||||
export function validateArgs(data: Args) {
|
||||
return validateAgainstSchema(data, argsSchema);
|
||||
}
|
||||
|
||||
// validate bounds_info.json
|
||||
export function validateBoundsInfo(data) {
|
||||
export function validateBoundsInfo(data: SavedWindowState) {
|
||||
return validateAgainstSchema(data, boundsInfoSchema);
|
||||
}
|
||||
|
||||
// validate app_state.json
|
||||
export function validateAppState(data) {
|
||||
export function validateAppState(data: AppState) {
|
||||
return validateAgainstSchema(data, appStateSchema);
|
||||
}
|
||||
|
||||
// validate v.0 config.json
|
||||
export function validateV0ConfigData(data) {
|
||||
export function validateV0ConfigData(data: ConfigV0) {
|
||||
return validateAgainstSchema(data, configDataSchemaV0);
|
||||
}
|
||||
|
||||
// validate v.1 config.json
|
||||
export function validateV1ConfigData(data) {
|
||||
export function validateV1ConfigData(data: ConfigV1) {
|
||||
if (Array.isArray(data.teams) && data.teams.length) {
|
||||
// first replace possible backslashes with forward slashes
|
||||
let teams = data.teams.map(({name, url}) => {
|
||||
|
@ -148,7 +155,7 @@ export function validateV1ConfigData(data) {
|
|||
return validateAgainstSchema(data, configDataSchemaV1);
|
||||
}
|
||||
|
||||
export function validateV2ConfigData(data) {
|
||||
export function validateV2ConfigData(data: ConfigV2) {
|
||||
if (Array.isArray(data.teams) && data.teams.length) {
|
||||
// first replace possible backslashes with forward slashes
|
||||
let teams = data.teams.map(({name, url, order}) => {
|
||||
|
@ -169,39 +176,39 @@ export function validateV2ConfigData(data) {
|
|||
}
|
||||
|
||||
// validate certificate.json
|
||||
export function validateCertificateStore(data) {
|
||||
export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
return validateAgainstSchema(jsonData, certificateStoreSchema);
|
||||
}
|
||||
|
||||
// validate allowedProtocols.json
|
||||
export function validateAllowedProtocols(data) {
|
||||
export function validateAllowedProtocols(data: string[]) {
|
||||
return validateAgainstSchema(data, allowedProtocolsSchema);
|
||||
}
|
||||
|
||||
export function validateTrustedOriginsStore(data) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
export function validateTrustedOriginsStore(data: string | Record<PermissionType, TrustedOrigin>) {
|
||||
const jsonData: Record<PermissionType, TrustedOrigin> = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
return validateAgainstSchema(jsonData, trustedOriginsSchema);
|
||||
}
|
||||
|
||||
export function validateOriginPermissions(data) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
export function validateOriginPermissions(data: string | TrustedOrigin) {
|
||||
const jsonData: TrustedOrigin = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
return validateAgainstSchema(jsonData, originPermissionsSchema);
|
||||
}
|
||||
|
||||
function validateAgainstSchema(data, schema) {
|
||||
function validateAgainstSchema<T>(data: T, schema: Joi.ObjectSchema<T> | Joi.ArraySchema): T | null {
|
||||
if (typeof data !== 'object') {
|
||||
log.error(`Input 'data' is not an object we can validate: ${typeof data}`);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
if (!schema) {
|
||||
log.error('No schema provided to validate');
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
const {error, value} = schema.validate(data, defaultOptions);
|
||||
if (error) {
|
||||
log.error(`Validation failed due to: ${error}`);
|
||||
return false;
|
||||
return null;
|
||||
}
|
||||
return value;
|
||||
}
|
|
@ -16,9 +16,9 @@ import * as Validator from './Validator';
|
|||
import {getMainWindow} from './windows/windowManager';
|
||||
|
||||
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
|
||||
let allowedProtocols = [];
|
||||
let allowedProtocols: string[] = [];
|
||||
|
||||
function addScheme(scheme) {
|
||||
function addScheme(scheme: string) {
|
||||
const proto = `${scheme}:`;
|
||||
if (!allowedProtocols.includes(proto)) {
|
||||
allowedProtocols.push(proto);
|
||||
|
@ -41,12 +41,16 @@ function init() {
|
|||
});
|
||||
}
|
||||
|
||||
function handleDialogEvent(protocol, URL) {
|
||||
function handleDialogEvent(protocol: string, URL: string) {
|
||||
if (allowedProtocols.indexOf(protocol) !== -1) {
|
||||
shell.openExternal(URL);
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox(getMainWindow(), {
|
||||
const mainWindow = getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
dialog.showMessageBox(mainWindow, {
|
||||
title: 'Non http(s) protocol',
|
||||
message: `${protocol} link requires an external application.`,
|
||||
detail: `The requested link is ${URL} . Do you want to continue?`,
|
||||
|
@ -63,7 +67,7 @@ function handleDialogEvent(protocol, URL) {
|
|||
switch (response) {
|
||||
case 1: {
|
||||
allowedProtocols.push(protocol);
|
||||
function handleError(err) {
|
||||
function handleError(err: NodeJS.ErrnoException | null) {
|
||||
if (err) {
|
||||
log.error(err);
|
||||
}
|
|
@ -9,13 +9,13 @@ import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED} from 'commo
|
|||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
const status = {
|
||||
unreads: new Map(),
|
||||
mentions: new Map(),
|
||||
expired: new Map(),
|
||||
unreads: new Map<string, boolean>(),
|
||||
mentions: new Map<string, number>(),
|
||||
expired: new Map<string, boolean>(),
|
||||
emitter: new events.EventEmitter(),
|
||||
};
|
||||
|
||||
const emitMentions = (serverName) => {
|
||||
const emitMentions = (serverName: string) => {
|
||||
const newMentions = getMentions(serverName);
|
||||
const newUnreads = getUnreads(serverName);
|
||||
const isExpired = getIsExpired(serverName);
|
||||
|
@ -24,11 +24,11 @@ const emitMentions = (serverName) => {
|
|||
emitStatus();
|
||||
};
|
||||
|
||||
const emitTray = (expired, mentions, unreads) => {
|
||||
const emitTray = (expired?: boolean, mentions?: number, unreads?: boolean) => {
|
||||
status.emitter.emit(UPDATE_TRAY, expired, Boolean(mentions), unreads);
|
||||
};
|
||||
|
||||
const emitBadge = (expired, mentions, unreads) => {
|
||||
const emitBadge = (expired?: boolean, mentions?: number, unreads?: boolean) => {
|
||||
status.emitter.emit(UPDATE_BADGE, expired, mentions, unreads);
|
||||
};
|
||||
|
||||
|
@ -40,7 +40,7 @@ export const emitStatus = () => {
|
|||
emitBadge(expired, mentions, unreads);
|
||||
};
|
||||
|
||||
export const updateMentions = (serverName, mentions, unreads) => {
|
||||
export const updateMentions = (serverName: string, mentions: number, unreads?: boolean) => {
|
||||
if (typeof unreads !== 'undefined') {
|
||||
status.unreads.set(serverName, Boolean(unreads));
|
||||
}
|
||||
|
@ -48,20 +48,20 @@ export const updateMentions = (serverName, mentions, unreads) => {
|
|||
emitMentions(serverName);
|
||||
};
|
||||
|
||||
export const updateUnreads = (serverName, unreads) => {
|
||||
export const updateUnreads = (serverName: string, unreads: boolean) => {
|
||||
status.unreads.set(serverName, Boolean(unreads));
|
||||
emitMentions(serverName);
|
||||
};
|
||||
|
||||
export const getUnreads = (serverName) => {
|
||||
export const getUnreads = (serverName: string) => {
|
||||
return status.unreads.get(serverName) || false;
|
||||
};
|
||||
|
||||
export const getMentions = (serverName) => {
|
||||
export const getMentions = (serverName: string) => {
|
||||
return status.mentions.get(serverName) || 0; // this might be undefined as a way to tell that we don't know as it might need to login still.
|
||||
};
|
||||
|
||||
export const getIsExpired = (serverName) => {
|
||||
export const getIsExpired = (serverName: string) => {
|
||||
return status.expired.get(serverName) || false;
|
||||
};
|
||||
|
||||
|
@ -101,11 +101,11 @@ export const anyExpired = () => {
|
|||
};
|
||||
|
||||
// add any other event emitter methods if needed
|
||||
export const on = (event, listener) => {
|
||||
export const on = (event: string, listener: (...args: any[]) => void) => {
|
||||
status.emitter.on(event, listener);
|
||||
};
|
||||
|
||||
export const setSessionExpired = (serverName, expired) => {
|
||||
export const setSessionExpired = (serverName: string, expired: boolean) => {
|
||||
const isExpired = Boolean(expired);
|
||||
const old = status.expired.get(serverName);
|
||||
status.expired.set(serverName, isExpired);
|
|
@ -1,90 +0,0 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
|
||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
import {addModal} from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||
const permissionModalHtml = getLocalURLString('permissionModal.html');
|
||||
|
||||
export class AuthManager {
|
||||
constructor(config, trustedOriginsStore) {
|
||||
this.config = config;
|
||||
this.trustedOriginsStore = trustedOriginsStore;
|
||||
this.loginCallbackMap = new Map();
|
||||
|
||||
config.on('update', this.handleConfigUpdate);
|
||||
}
|
||||
|
||||
handleConfigUpdate = (newConfig) => {
|
||||
this.config = newConfig;
|
||||
}
|
||||
|
||||
handleAppLogin = (event, webContents, request, authInfo, callback) => {
|
||||
event.preventDefault();
|
||||
const parsedURL = new URL(request.url);
|
||||
const server = urlUtils.getServer(parsedURL, this.config.teams);
|
||||
|
||||
this.loginCallbackMap.set(request.url, typeof callback === 'undefined' ? null : 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)) {
|
||||
this.popLoginModal(request, authInfo);
|
||||
} else {
|
||||
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
popLoginModal = (request, authInfo) => {
|
||||
const modalPromise = addModal(`login-${request.url}`, loginModalHtml, modalPreload, {request, authInfo}, WindowManager.getMainWindow());
|
||||
modalPromise.then((data) => {
|
||||
const {username, password} = data;
|
||||
this.handleLoginCredentialsEvent(request, username, password);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing login request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
|
||||
popPermissionModal = (request, authInfo, permission) => {
|
||||
const modalPromise = addModal(`permission-${request.url}`, permissionModalHtml, modalPreload, {url: request.url, permission}, WindowManager.getMainWindow());
|
||||
modalPromise.then(() => {
|
||||
this.handlePermissionGranted(request.url, permission);
|
||||
this.addToLoginQueue(request, authInfo);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing permission request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
|
||||
handleLoginCredentialsEvent = (request, username, password) => {
|
||||
const callback = this.loginCallbackMap.get(request.url);
|
||||
if (typeof callback === 'undefined') {
|
||||
log.error(`Failed to retrieve login callback for ${request.url}`);
|
||||
return;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback(username, password);
|
||||
}
|
||||
this.loginCallbackMap.delete(request.url);
|
||||
}
|
||||
|
||||
handleCancelLoginEvent = (request) => {
|
||||
log.info(`Cancelling request for ${request ? request.url : 'unknown'}`);
|
||||
this.handleLoginCredentialsEvent(request); // we use undefined to cancel the request
|
||||
}
|
||||
|
||||
handlePermissionGranted(url, permission) {
|
||||
this.trustedOriginsStore.addPermission(url, permission);
|
||||
this.trustedOriginsStore.save();
|
||||
}
|
||||
}
|
119
src/main/authManager.ts
Normal file
119
src/main/authManager.ts
Normal file
|
@ -0,0 +1,119 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {AuthenticationResponseDetails, AuthInfo, WebContents} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {CombinedConfig} from 'types/config';
|
||||
import {PermissionType} from 'types/trustedOrigin';
|
||||
import {LoginModalData} from 'types/auth';
|
||||
|
||||
import {BASIC_AUTH_PERMISSION} from 'common/permissions';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
import {addModal} from './views/modalManager';
|
||||
import {getLocalURLString, getLocalPreload} from './utils';
|
||||
import TrustedOriginsStore from './trustedOrigins';
|
||||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||
const permissionModalHtml = getLocalURLString('permissionModal.html');
|
||||
|
||||
type LoginModalResult = {
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
|
||||
export class AuthManager {
|
||||
config: CombinedConfig;
|
||||
trustedOriginsStore: TrustedOriginsStore;
|
||||
loginCallbackMap: Map<string, ((username?: string, password?: string) => void) | undefined>;
|
||||
|
||||
constructor(config: CombinedConfig, trustedOriginsStore: TrustedOriginsStore) {
|
||||
this.config = config;
|
||||
this.trustedOriginsStore = trustedOriginsStore;
|
||||
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) => {
|
||||
event.preventDefault();
|
||||
const parsedURL = new URL(request.url);
|
||||
const server = urlUtils.getServer(parsedURL, this.config.teams);
|
||||
if (!server) {
|
||||
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
|
||||
if (urlUtils.isTrustedURL(request.url, this.config.teams) || urlUtils.isCustomLoginURL(parsedURL, server, this.config.teams) || this.trustedOriginsStore.checkPermission(request.url, BASIC_AUTH_PERMISSION)) {
|
||||
this.popLoginModal(request, authInfo);
|
||||
} else {
|
||||
this.popPermissionModal(request, authInfo, BASIC_AUTH_PERMISSION);
|
||||
}
|
||||
}
|
||||
|
||||
popLoginModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo) => {
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
const modalPromise = addModal<LoginModalData, LoginModalResult>(`login-${request.url}`, loginModalHtml, modalPreload, {request, authInfo}, mainWindow);
|
||||
if (modalPromise) {
|
||||
modalPromise.then((data) => {
|
||||
const {username, password} = data;
|
||||
this.handleLoginCredentialsEvent(request, username, password);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing login request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
popPermissionModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo, permission: PermissionType) => {
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
const modalPromise = addModal(`permission-${request.url}`, permissionModalHtml, modalPreload, {url: request.url, permission}, mainWindow);
|
||||
if (modalPromise) {
|
||||
modalPromise.then(() => {
|
||||
this.handlePermissionGranted(request.url, permission);
|
||||
this.popLoginModal(request, authInfo);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing permission request', err);
|
||||
}
|
||||
this.handleCancelLoginEvent(request);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleLoginCredentialsEvent = (request: AuthenticationResponseDetails, username?: string, password?: string) => {
|
||||
const callback = this.loginCallbackMap.get(request.url);
|
||||
if (typeof callback === 'undefined') {
|
||||
log.error(`Failed to retrieve login callback for ${request.url}`);
|
||||
return;
|
||||
}
|
||||
if (callback != null) {
|
||||
callback(username, password);
|
||||
}
|
||||
this.loginCallbackMap.delete(request.url);
|
||||
}
|
||||
|
||||
handleCancelLoginEvent = (request: AuthenticationResponseDetails) => {
|
||||
log.info(`Cancelling request for ${request ? request.url : 'unknown'}`);
|
||||
this.handleLoginCredentialsEvent(request); // we use undefined to cancel the request
|
||||
}
|
||||
|
||||
handlePermissionGranted(url: string, permission: PermissionType) {
|
||||
this.trustedOriginsStore.addPermission(url, permission);
|
||||
this.trustedOriginsStore.save();
|
||||
}
|
||||
}
|
|
@ -2,9 +2,13 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
// TODO: This needs to be rebuilt anyways, skipping TS migration for now
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
//@ts-nocheck
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import {app, BrowserWindow, dialog, ipcMain, shell} from 'electron';
|
||||
import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, ipcMain, IpcMainEvent, shell} from 'electron';
|
||||
|
||||
import log from 'electron-log';
|
||||
import {autoUpdater, CancellationToken} from 'electron-updater';
|
||||
|
@ -18,18 +22,18 @@ autoUpdater.log.transports.file.level = 'info';
|
|||
|
||||
let updaterModal = null;
|
||||
|
||||
function createEventListener(win, eventName) {
|
||||
return (event) => {
|
||||
function createEventListener(win: BrowserWindow, eventName: string) {
|
||||
return (event: IpcMainEvent) => {
|
||||
if (event.sender === win.webContents) {
|
||||
win.emit(eventName);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function createUpdaterModal(parentWindow, options) {
|
||||
function createUpdaterModal(parentWindow: BrowserWindow, options: {linuxAppIcon: string; notifyOnly: boolean}) {
|
||||
const windowWidth = 480;
|
||||
const windowHeight = 280;
|
||||
const windowOptions = {
|
||||
const windowOptions: BrowserWindowConstructorOptions = {
|
||||
title: `${app.name} Updater`,
|
||||
parent: parentWindow,
|
||||
modal: true,
|
||||
|
@ -67,7 +71,7 @@ function createUpdaterModal(parentWindow, options) {
|
|||
return modal;
|
||||
}
|
||||
|
||||
function isUpdateApplicable(now, skippedVersion, updateInfo) {
|
||||
function isUpdateApplicable(now: Date, skippedVersion, updateInfo) {
|
||||
const releaseTime = new Date(updateInfo.releaseDate).getTime();
|
||||
|
||||
// 48 hours after a new version is added to releases.mattermost.com, user receives a “New update is available” dialog
|
||||
|
@ -83,7 +87,7 @@ function isUpdateApplicable(now, skippedVersion, updateInfo) {
|
|||
return true;
|
||||
}
|
||||
|
||||
function downloadAndInstall(cancellationToken) {
|
||||
function downloadAndInstall(cancellationToken?: CancellationToken) {
|
||||
autoUpdater.on('update-downloaded', () => {
|
||||
global.willAppQuit = true;
|
||||
autoUpdater.quitAndInstall();
|
||||
|
@ -150,7 +154,7 @@ function initialize(appState, mainWindow, notifyOnly = false) {
|
|||
});
|
||||
}
|
||||
|
||||
function shouldCheckForUpdatesOnStart(updateCheckedDate) {
|
||||
function shouldCheckForUpdatesOnStart(updateCheckedDate: Date) {
|
||||
if (updateCheckedDate) {
|
||||
if (Date.now() - updateCheckedDate.getTime() < UPDATER_INTERVAL_IN_MS) {
|
||||
return false;
|
||||
|
@ -167,6 +171,8 @@ function checkForUpdates(isManual = false) {
|
|||
}
|
||||
|
||||
class AutoUpdaterConfig {
|
||||
data: {notifyOnly?: boolean};
|
||||
|
||||
constructor() {
|
||||
this.data = {};
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
|
@ -10,9 +11,9 @@ import * as AppState from './appState';
|
|||
|
||||
const MAX_WIN_COUNT = 99;
|
||||
|
||||
let showUnreadBadgeSetting;
|
||||
let showUnreadBadgeSetting: boolean;
|
||||
|
||||
function showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||
let description = 'You have no unread messages';
|
||||
let text;
|
||||
if (sessionExpired) {
|
||||
|
@ -28,7 +29,7 @@ function showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount) {
|
|||
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
|
||||
}
|
||||
|
||||
function showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||
let badge = '';
|
||||
if (sessionExpired) {
|
||||
badge = '•';
|
||||
|
@ -40,28 +41,28 @@ function showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount) {
|
|||
app.dock.setBadge(badge);
|
||||
}
|
||||
|
||||
function showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount) {
|
||||
function showBadgeLinux(sessionExpired: boolean, mentionCount: number) {
|
||||
if (app.isUnityRunning()) {
|
||||
const countExpired = sessionExpired ? 1 : 0;
|
||||
app.setBadgeCount(mentionCount + countExpired);
|
||||
}
|
||||
}
|
||||
|
||||
function showBadge(sessionExpired, mentionCount, showUnreadBadge) {
|
||||
function showBadge(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||
switch (process.platform) {
|
||||
case 'win32':
|
||||
showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount);
|
||||
showBadgeWindows(sessionExpired, mentionCount, showUnreadBadge);
|
||||
break;
|
||||
case 'darwin':
|
||||
showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount);
|
||||
showBadgeOSX(sessionExpired, mentionCount, showUnreadBadge);
|
||||
break;
|
||||
case 'linux':
|
||||
showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount);
|
||||
showBadgeLinux(sessionExpired, mentionCount);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
export function setUnreadBadgeSetting(showUnreadBadge) {
|
||||
export function setUnreadBadgeSetting(showUnreadBadge: boolean) {
|
||||
showUnreadBadgeSetting = showUnreadBadge;
|
||||
AppState.emitStatus();
|
||||
}
|
|
@ -1,6 +1,9 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
import {Certificate, WebContents} from 'electron';
|
||||
|
||||
import {CertificateModalData} from 'types/certificate';
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
|
@ -10,12 +13,18 @@ import {getLocalURLString, getLocalPreload} from './utils';
|
|||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
const html = getLocalURLString('certificateModal.html');
|
||||
|
||||
type CertificateModalResult = {
|
||||
cert: Certificate;
|
||||
}
|
||||
|
||||
export class CertificateManager {
|
||||
certificateRequestCallbackMap: Map<string, (certificate?: Certificate | undefined) => void>;
|
||||
|
||||
constructor() {
|
||||
this.certificateRequestCallbackMap = new Map();
|
||||
}
|
||||
|
||||
handleSelectCertificate = (event, webContents, url, list, callback) => {
|
||||
handleSelectCertificate = (event: Event, webContents: WebContents, url: string, list: Certificate[], callback: (certificate?: Certificate | undefined) => void) => {
|
||||
if (list.length > 1) {
|
||||
event.preventDefault(); // prevent the app from getting the first certificate available
|
||||
|
||||
|
@ -27,20 +36,26 @@ export class CertificateManager {
|
|||
}
|
||||
}
|
||||
|
||||
popCertificateModal = (url, list) => {
|
||||
const modalPromise = addModal(`certificate-${url}`, html, modalPreload, {url, list}, WindowManager.getMainWindow());
|
||||
modalPromise.then((data) => {
|
||||
const {cert} = data;
|
||||
this.handleSelectedCertificate(url, cert);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing certificate selection', err);
|
||||
}
|
||||
this.handleSelectedCertificate(url);
|
||||
});
|
||||
popCertificateModal = (url: string, list: Certificate[]) => {
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
const modalPromise = addModal<CertificateModalData, CertificateModalResult>(`certificate-${url}`, html, modalPreload, {url, list}, mainWindow);
|
||||
if (modalPromise) {
|
||||
modalPromise.then((data) => {
|
||||
const {cert} = data;
|
||||
this.handleSelectedCertificate(url, cert);
|
||||
}).catch((err) => {
|
||||
if (err) {
|
||||
log.error('Error processing certificate selection', err);
|
||||
}
|
||||
this.handleSelectedCertificate(url);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
handleSelectedCertificate = (server, cert) => {
|
||||
handleSelectedCertificate = (server: string, cert?: Certificate) => {
|
||||
const callback = this.certificateRequestCallbackMap.get(server);
|
||||
if (!callback) {
|
||||
log.error(`there was no callback associated with: ${server}`);
|
|
@ -1,68 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
function comparableCertificate(certificate) {
|
||||
return {
|
||||
data: certificate.data.toString(),
|
||||
issuerName: certificate.issuerName,
|
||||
};
|
||||
}
|
||||
|
||||
function areEqual(certificate0, certificate1) {
|
||||
if (certificate0.data !== certificate1.data) {
|
||||
return false;
|
||||
}
|
||||
if (certificate0.issuerName !== certificate1.issuerName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function CertificateStore(storeFile) {
|
||||
this.storeFile = storeFile;
|
||||
let storeStr;
|
||||
try {
|
||||
storeStr = fs.readFileSync(storeFile, 'utf-8');
|
||||
const result = Validator.validateCertificateStore(storeStr);
|
||||
if (!result) {
|
||||
throw new Error('Provided certificate store file does not validate, using defaults instead.');
|
||||
}
|
||||
this.data = result;
|
||||
} catch (e) {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
CertificateStore.prototype.save = function save() {
|
||||
fs.writeFileSync(this.storeFile, JSON.stringify(this.data, null, ' '));
|
||||
};
|
||||
|
||||
CertificateStore.prototype.add = function add(targetURL, certificate) {
|
||||
this.data[urlUtils.getHost(targetURL)] = comparableCertificate(certificate);
|
||||
};
|
||||
|
||||
CertificateStore.prototype.isExisting = function isExisting(targetURL) {
|
||||
return Object.prototype.hasOwnProperty.call(this.data, urlUtils.getHost(targetURL));
|
||||
};
|
||||
|
||||
CertificateStore.prototype.isTrusted = function isTrusted(targetURL, certificate) {
|
||||
const host = urlUtils.getHost(targetURL);
|
||||
if (!this.isExisting(targetURL)) {
|
||||
return false;
|
||||
}
|
||||
return areEqual(this.data[host], comparableCertificate(certificate));
|
||||
};
|
||||
|
||||
export default {
|
||||
load(storeFile) {
|
||||
return new CertificateStore(storeFile);
|
||||
},
|
||||
};
|
71
src/main/certificateStore.ts
Normal file
71
src/main/certificateStore.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import {Certificate} from 'electron';
|
||||
|
||||
import {ComparableCertificate} from 'types/certificate';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
function comparableCertificate(certificate: Certificate): ComparableCertificate {
|
||||
return {
|
||||
data: certificate.data.toString(),
|
||||
issuerName: certificate.issuerName,
|
||||
};
|
||||
}
|
||||
|
||||
function areEqual(certificate0: ComparableCertificate, certificate1: ComparableCertificate) {
|
||||
if (certificate0.data !== certificate1.data) {
|
||||
return false;
|
||||
}
|
||||
if (certificate0.issuerName !== certificate1.issuerName) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
export default class CertificateStore {
|
||||
storeFile: string;
|
||||
data: Record<string, ComparableCertificate>;
|
||||
|
||||
constructor(storeFile: string) {
|
||||
this.storeFile = storeFile;
|
||||
let storeStr;
|
||||
try {
|
||||
storeStr = fs.readFileSync(storeFile, 'utf-8');
|
||||
const result = Validator.validateCertificateStore(storeStr);
|
||||
if (!result) {
|
||||
throw new Error('Provided certificate store file does not validate, using defaults instead.');
|
||||
}
|
||||
this.data = result;
|
||||
} catch (e) {
|
||||
this.data = {};
|
||||
}
|
||||
}
|
||||
|
||||
save = () => {
|
||||
fs.writeFileSync(this.storeFile, JSON.stringify(this.data, null, ' '));
|
||||
};
|
||||
|
||||
add = (targetURL: string, certificate: Certificate) => {
|
||||
this.data[urlUtils.getHost(targetURL)] = comparableCertificate(certificate);
|
||||
};
|
||||
|
||||
isExisting = (targetURL: string) => {
|
||||
return Object.prototype.hasOwnProperty.call(this.data, urlUtils.getHost(targetURL));
|
||||
};
|
||||
|
||||
isTrusted = (targetURL: string, certificate: Certificate) => {
|
||||
const host = urlUtils.getHost(targetURL);
|
||||
if (!this.isExisting(targetURL)) {
|
||||
return false;
|
||||
}
|
||||
return areEqual(this.data[host], comparableCertificate(certificate));
|
||||
};
|
||||
}
|
|
@ -1,18 +1,19 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import electronContextMenu from 'electron-context-menu';
|
||||
import {BrowserView, BrowserWindow, ContextMenuParams, Event, WebContents} from 'electron';
|
||||
import electronContextMenu, {Options} from 'electron-context-menu';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
const defaultMenuOptions = {
|
||||
shouldShowMenu: (e, p) => {
|
||||
shouldShowMenu: (e: Event, p: ContextMenuParams) => {
|
||||
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
|
||||
let isInternalSrc;
|
||||
try {
|
||||
const srcurl = urlUtils.parseURL(p.srcURL);
|
||||
isInternalSrc = srcurl.protocol === 'file:';
|
||||
isInternalSrc = srcurl?.protocol === 'file:';
|
||||
} catch (err) {
|
||||
isInternalSrc = false;
|
||||
}
|
||||
|
@ -27,8 +28,12 @@ const defaultMenuOptions = {
|
|||
};
|
||||
|
||||
export default class ContextMenu {
|
||||
constructor(options, view) {
|
||||
const providedOptions = options || {};
|
||||
view: BrowserWindow | BrowserView;
|
||||
menuOptions: Options;
|
||||
menuDispose?: () => void;
|
||||
|
||||
constructor(options: Options, view: BrowserWindow | BrowserView) {
|
||||
const providedOptions: Options = options || {};
|
||||
|
||||
this.menuOptions = Object.assign({}, defaultMenuOptions, providedOptions);
|
||||
this.view = view;
|
||||
|
@ -39,7 +44,7 @@ export default class ContextMenu {
|
|||
dispose = () => {
|
||||
if (this.menuDispose) {
|
||||
this.menuDispose();
|
||||
this.menuDispose = null;
|
||||
delete this.menuDispose;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -50,7 +55,7 @@ export default class ContextMenu {
|
|||
* Work-around issue with passing `WebContents` to `electron-context-menu` in Electron 11
|
||||
* @see https://github.com/sindresorhus/electron-context-menu/issues/123
|
||||
*/
|
||||
const options = {window: {webContents: this.view.webContents, inspectElement: this.view.webContents.inspectElement.bind(this.view.webContents), isDestroyed: this.view.webContents.isDestroyed.bind(this.view.webContents), off: this.view.webContents.off.bind(this.view.webContents)}, ...this.menuOptions};
|
||||
const options = {window: {webContents: this.view.webContents, inspectElement: this.view.webContents.inspectElement.bind(this.view.webContents), isDestroyed: this.view.webContents.isDestroyed.bind(this.view.webContents), off: this.view.webContents.off.bind(this.view.webContents)} as unknown as WebContents, ...this.menuOptions};
|
||||
this.menuDispose = electronContextMenu(options);
|
||||
}
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import {app} from 'electron';
|
||||
import {app, Session} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
function flushCookiesStore(session) {
|
||||
function flushCookiesStore(session: Session) {
|
||||
session.cookies.flushStore().catch((err) => {
|
||||
log.error(`There was a problem flushing cookies:\n${err}`);
|
||||
});
|
||||
}
|
||||
|
||||
export default function initCookieManager(session) {
|
||||
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
|
|
@ -1,65 +0,0 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
import zlib from 'zlib';
|
||||
|
||||
import electron from 'electron';
|
||||
const {app, dialog} = electron;
|
||||
|
||||
import * as WindowManager from './windows/windowManager';
|
||||
|
||||
export default function downloadURL(URL, callback) {
|
||||
const {net} = electron;
|
||||
const request = net.request(URL);
|
||||
request.setHeader('Accept-Encoding', 'gzip,deflate');
|
||||
request.on('response', (response) => {
|
||||
const file = getAttachmentName(response.headers);
|
||||
const dialogOptions = {
|
||||
defaultPath: path.join(app.getPath('downloads'), file),
|
||||
};
|
||||
dialog.showSaveDialog(
|
||||
WindowManager.getMainWindow(true),
|
||||
dialogOptions,
|
||||
).then(
|
||||
(filename) => {
|
||||
if (filename) {
|
||||
saveResponseBody(response, filename, callback);
|
||||
}
|
||||
},
|
||||
).catch((err) => {
|
||||
callback(err);
|
||||
});
|
||||
}).on('error', callback);
|
||||
request.end();
|
||||
}
|
||||
|
||||
function getAttachmentName(headers) {
|
||||
if (headers['content-disposition']) {
|
||||
const contentDisposition = headers['content-disposition'][0];
|
||||
const matched = contentDisposition.match(/filename="(.*)"/);
|
||||
if (matched) {
|
||||
return path.basename(matched[1]);
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function saveResponseBody(response, filename, callback) {
|
||||
const output = fs.createWriteStream(filename);
|
||||
output.on('close', callback);
|
||||
switch (response.headers['content-encoding']) {
|
||||
case 'gzip':
|
||||
response.pipe(zlib.createGunzip()).pipe(output).on('error', callback);
|
||||
break;
|
||||
case 'deflate':
|
||||
response.pipe(zlib.createInflate()).pipe(output).on('error', callback);
|
||||
break;
|
||||
default:
|
||||
response.pipe(output).on('error', callback);
|
||||
break;
|
||||
}
|
||||
}
|
|
@ -1,18 +1,23 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
import fs from 'fs';
|
||||
|
||||
import path from 'path';
|
||||
|
||||
import electron from 'electron';
|
||||
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 Utils from 'common/utils/util';
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {MentionData} from 'types/notification';
|
||||
|
||||
import {Boundaries} from 'types/utils';
|
||||
|
||||
import {
|
||||
SWITCH_SERVER,
|
||||
|
@ -33,6 +38,10 @@ import {
|
|||
} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import {protocols} from '../../electron-builder.json';
|
||||
|
||||
import AutoLauncher from './AutoLauncher';
|
||||
|
@ -76,13 +85,13 @@ 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 = null;
|
||||
let trustedOriginsStore = null;
|
||||
let scheme = null;
|
||||
let certificateStore: CertificateStore;
|
||||
let trustedOriginsStore;
|
||||
let scheme: string;
|
||||
let appVersion = null;
|
||||
let config = null;
|
||||
let authManager = null;
|
||||
let certificateManager = null;
|
||||
let config: Config;
|
||||
let authManager: AuthManager;
|
||||
let certificateManager: CertificateManager;
|
||||
|
||||
/**
|
||||
* Main entry point for the application, ensures that everything initializes in the proper order
|
||||
|
@ -140,7 +149,7 @@ function initializeArgs() {
|
|||
}
|
||||
|
||||
async function initializeConfig() {
|
||||
const loadConfig = new Promise((resolve) => {
|
||||
const loadConfig = new Promise<void>((resolve) => {
|
||||
config = new Config(app.getPath('userData') + '/config.json');
|
||||
config.once('update', (configData) => {
|
||||
config.on('update', handleConfigUpdate);
|
||||
|
@ -169,7 +178,11 @@ function initializeAppEventListeners() {
|
|||
}
|
||||
|
||||
function initializeBeforeAppReady() {
|
||||
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||
if (!config || !config.data) {
|
||||
log.error('No config loaded');
|
||||
return;
|
||||
}
|
||||
certificateStore = new CertificateStore(path.resolve(app.getPath('userData'), 'certificate.json'));
|
||||
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
|
||||
trustedOriginsStore.load();
|
||||
|
||||
|
@ -196,7 +209,7 @@ function initializeBeforeAppReady() {
|
|||
|
||||
allowProtocolDialog.init();
|
||||
|
||||
authManager = new AuthManager(config, trustedOriginsStore);
|
||||
authManager = new AuthManager(config.data, trustedOriginsStore);
|
||||
certificateManager = new CertificateManager();
|
||||
|
||||
if (isDev) {
|
||||
|
@ -237,7 +250,10 @@ function initializeInterCommunicationEventListeners() {
|
|||
// config event handlers
|
||||
//
|
||||
|
||||
function handleConfigUpdate(newConfig) {
|
||||
function handleConfigUpdate(newConfig: Config) {
|
||||
if (!newConfig.data) {
|
||||
return;
|
||||
}
|
||||
if (process.platform === 'win32' || process.platform === 'linux') {
|
||||
const appLauncher = new AutoLauncher();
|
||||
const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable();
|
||||
|
@ -247,6 +263,7 @@ function handleConfigUpdate(newConfig) {
|
|||
log.error('error:', err);
|
||||
});
|
||||
WindowManager.setConfig(newConfig.data);
|
||||
authManager.handleConfigUpdate(newConfig.data);
|
||||
setUnreadBadgeSetting(newConfig.data && newConfig.data.showUnreadBadge);
|
||||
}
|
||||
|
||||
|
@ -254,6 +271,10 @@ function handleConfigUpdate(newConfig) {
|
|||
}
|
||||
|
||||
function handleConfigSynchronize() {
|
||||
if (!config.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: send this to server manager
|
||||
WindowManager.setConfig(config.data);
|
||||
setUnreadBadgeSetting(config.data.showUnreadBadge);
|
||||
|
@ -267,7 +288,7 @@ function handleConfigSynchronize() {
|
|||
|
||||
function handleReloadConfig() {
|
||||
config.reload();
|
||||
WindowManager.setConfig(config.data);
|
||||
WindowManager.setConfig(config.data!);
|
||||
}
|
||||
|
||||
function handleAppVersion() {
|
||||
|
@ -277,7 +298,7 @@ function handleAppVersion() {
|
|||
};
|
||||
}
|
||||
|
||||
function handleDarkModeChange(darkMode) {
|
||||
function handleDarkModeChange(darkMode: boolean) {
|
||||
refreshTrayImages(config.trayIconTheme);
|
||||
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
|
||||
WindowManager.updateLoadingScreenDarkMode(darkMode);
|
||||
|
@ -288,11 +309,13 @@ function handleDarkModeChange(darkMode) {
|
|||
//
|
||||
|
||||
// activate first app instance, subsequent instances will quit themselves
|
||||
function handleAppSecondInstance(event, argv) {
|
||||
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);
|
||||
openDeepLink(deeplinkingUrl);
|
||||
if (deeplinkingUrl) {
|
||||
openDeepLink(deeplinkingUrl);
|
||||
}
|
||||
}
|
||||
|
||||
function handleAppWindowAllClosed() {
|
||||
|
@ -303,9 +326,9 @@ function handleAppWindowAllClosed() {
|
|||
}
|
||||
}
|
||||
|
||||
function handleAppBrowserWindowCreated(error, newWindow) {
|
||||
function handleAppBrowserWindowCreated(event: Event, newWindow: BrowserWindow) {
|
||||
// Screen cannot be required before app is ready
|
||||
resizeScreen(electron.screen, newWindow);
|
||||
resizeScreen(newWindow);
|
||||
}
|
||||
|
||||
function handleAppActivate() {
|
||||
|
@ -318,18 +341,18 @@ function handleAppBeforeQuit() {
|
|||
global.willAppQuit = true;
|
||||
}
|
||||
|
||||
function handleQuit(e, reason, stack) {
|
||||
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, webContents, url, list, callback) {
|
||||
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, webContents, url, error, certificate, 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;
|
||||
|
@ -355,6 +378,9 @@ function handleAppCertificateError(event, webContents, url, error, certificate,
|
|||
|
||||
// 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.',
|
||||
|
@ -395,15 +421,15 @@ function handleAppCertificateError(event, webContents, url, error, certificate,
|
|||
}
|
||||
}
|
||||
|
||||
function handleAppLogin(event, webContents, request, authInfo, callback) {
|
||||
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, killed) {
|
||||
function handleAppGPUProcessCrashed(event: electron.Event, killed: boolean) {
|
||||
log.error(`The GPU process has crashed (killed = ${killed})`);
|
||||
}
|
||||
|
||||
function openDeepLink(deeplinkingUrl) {
|
||||
function openDeepLink(deeplinkingUrl: string) {
|
||||
try {
|
||||
WindowManager.showMainWindow(deeplinkingUrl);
|
||||
} catch (err) {
|
||||
|
@ -427,7 +453,7 @@ function handleAppWillFinishLaunching() {
|
|||
});
|
||||
}
|
||||
|
||||
function handleSwitchServer(event, serverName) {
|
||||
function handleSwitchServer(event: IpcMainEvent, serverName: string) {
|
||||
WindowManager.switchServer(serverName);
|
||||
}
|
||||
|
||||
|
@ -436,7 +462,11 @@ function handleNewServerModal() {
|
|||
|
||||
const modalPreload = getLocalPreload('modalPreload.js');
|
||||
|
||||
const modalPromise = addModal('newServer', html, modalPreload, {}, WindowManager.getMainWindow());
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
if (!mainWindow) {
|
||||
return;
|
||||
}
|
||||
const modalPromise = addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow);
|
||||
if (modalPromise) {
|
||||
modalPromise.then((data) => {
|
||||
const teams = config.teams;
|
||||
|
@ -506,7 +536,7 @@ function initializeAfterAppReady() {
|
|||
WindowManager.showSettingsWindow();
|
||||
}
|
||||
|
||||
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow());
|
||||
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
|
||||
|
||||
// listen for status updates and pass on to renderer
|
||||
userActivityMonitor.on('status', (status) => {
|
||||
|
@ -519,7 +549,7 @@ function initializeAfterAppReady() {
|
|||
if (shouldShowTrayIcon()) {
|
||||
setupTray(config.trayIconTheme);
|
||||
}
|
||||
setupBadge(config.showUnreadBadge);
|
||||
setupBadge();
|
||||
|
||||
session.defaultSession.on('will-download', (event, item, webContents) => {
|
||||
const filename = item.getFilename();
|
||||
|
@ -533,13 +563,13 @@ function initializeAfterAppReady() {
|
|||
}
|
||||
item.setSaveDialogOptions({
|
||||
title: filename,
|
||||
defaultPath: path.resolve(config.combinedData.downloadLocation, filename),
|
||||
defaultPath: path.resolve(config.downloadLocation, filename),
|
||||
filters,
|
||||
});
|
||||
|
||||
item.on('done', (doneEvent, state) => {
|
||||
if (state === 'completed') {
|
||||
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams));
|
||||
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams)!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@ -584,7 +614,7 @@ function initializeAfterAppReady() {
|
|||
// ipc communication event handlers
|
||||
//
|
||||
|
||||
function handleMentionNotification(event, title, body, channel, teamId, silent, data) {
|
||||
function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, silent: boolean, data: MentionData) {
|
||||
displayMention(title, body, channel, teamId, silent, event.sender, data);
|
||||
}
|
||||
|
||||
|
@ -605,23 +635,21 @@ function handleCloseAppMenu() {
|
|||
WindowManager.focusBrowserView();
|
||||
}
|
||||
|
||||
function handleUpdateMenuEvent(event, menuConfig) {
|
||||
// TODO: this might make sense to move to window manager? so it updates the window referenced if needed.
|
||||
const mainWindow = WindowManager.getMainWindow();
|
||||
function handleUpdateMenuEvent(event: IpcMainEvent, menuConfig: Config) {
|
||||
const aMenu = appMenu.createMenu(menuConfig);
|
||||
Menu.setApplicationMenu(aMenu);
|
||||
aMenu.addListener('menu-will-close', handleCloseAppMenu);
|
||||
|
||||
// set up context menu for tray icon
|
||||
if (shouldShowTrayIcon()) {
|
||||
const tMenu = trayMenu.createMenu(menuConfig.data);
|
||||
setTrayMenu(tMenu, mainWindow);
|
||||
const tMenu = trayMenu.createMenu(menuConfig.data!);
|
||||
setTrayMenu(tMenu);
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSelectDownload(event, startFrom) {
|
||||
async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
|
||||
const message = 'Specify the folder where files will download';
|
||||
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.data.downloadLocation,
|
||||
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation,
|
||||
message,
|
||||
properties:
|
||||
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
|
||||
|
@ -632,7 +660,7 @@ async function handleSelectDownload(event, startFrom) {
|
|||
// helper functions
|
||||
//
|
||||
|
||||
function getDeeplinkingURL(args) {
|
||||
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];
|
||||
|
@ -640,14 +668,14 @@ function getDeeplinkingURL(args) {
|
|||
return url;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function shouldShowTrayIcon() {
|
||||
return config.showTrayIcon || process.platform === 'win32';
|
||||
}
|
||||
|
||||
function wasUpdated(lastAppVersion) {
|
||||
function wasUpdated(lastAppVersion?: string) {
|
||||
return lastAppVersion !== app.getVersion();
|
||||
}
|
||||
|
||||
|
@ -662,7 +690,7 @@ function clearAppCache() {
|
|||
}
|
||||
}
|
||||
|
||||
function isWithinDisplay(state, display) {
|
||||
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;
|
||||
|
@ -674,7 +702,7 @@ function isWithinDisplay(state, display) {
|
|||
return !(midX > display.maxX || midY > display.maxY);
|
||||
}
|
||||
|
||||
function getValidWindowPosition(state) {
|
||||
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();
|
||||
|
@ -688,7 +716,7 @@ function getValidWindowPosition(state) {
|
|||
return {x: state.x, y: state.y};
|
||||
}
|
||||
|
||||
function resizeScreen(screen, browserWindow) {
|
||||
function resizeScreen(browserWindow: BrowserWindow) {
|
||||
function handle() {
|
||||
const position = browserWindow.getPosition();
|
||||
const size = browserWindow.getSize();
|
|
@ -3,14 +3,15 @@
|
|||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import {app, Menu, session, shell, webContents} from 'electron';
|
||||
import {app, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
|
||||
|
||||
import {ADD_SERVER, SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
function createTemplate(config) {
|
||||
const separatorItem = {
|
||||
function createTemplate(config: Config) {
|
||||
const separatorItem: MenuItemConstructorOptions = {
|
||||
type: 'separator',
|
||||
};
|
||||
|
||||
|
@ -39,7 +40,7 @@ function createTemplate(config) {
|
|||
},
|
||||
});
|
||||
|
||||
if (config.data.enableServerManagement === true) {
|
||||
if (config.data?.enableServerManagement === true) {
|
||||
platformAppMenu.push({
|
||||
label: 'Sign in to Another Server',
|
||||
click() {
|
||||
|
@ -53,7 +54,7 @@ function createTemplate(config) {
|
|||
separatorItem, {
|
||||
role: 'hide',
|
||||
}, {
|
||||
role: 'hideothers',
|
||||
role: 'hideOthers',
|
||||
}, {
|
||||
role: 'unhide',
|
||||
}, separatorItem, {
|
||||
|
@ -139,7 +140,7 @@ function createTemplate(config) {
|
|||
}
|
||||
return 'Ctrl+Shift+I';
|
||||
})(),
|
||||
click(item, focusedWindow) {
|
||||
click(item: Electron.MenuItem, focusedWindow?: WebContents) {
|
||||
if (focusedWindow) {
|
||||
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
|
||||
if (focusedWindow.isDevToolsOpened()) {
|
||||
|
@ -193,7 +194,7 @@ function createTemplate(config) {
|
|||
}],
|
||||
});
|
||||
|
||||
const teams = config.data.teams || [];
|
||||
const teams = config.data?.teams || [];
|
||||
const windowMenu = {
|
||||
label: '&Window',
|
||||
submenu: [{
|
||||
|
@ -209,7 +210,7 @@ function createTemplate(config) {
|
|||
label: team.name,
|
||||
accelerator: `CmdOrCtrl+${i + 1}`,
|
||||
click() {
|
||||
WindowManager.switchServer(team.name, true);
|
||||
WindowManager.switchServer(team.name);
|
||||
},
|
||||
};
|
||||
}), separatorItem, {
|
||||
|
@ -230,17 +231,19 @@ function createTemplate(config) {
|
|||
};
|
||||
template.push(windowMenu);
|
||||
const submenu = [];
|
||||
if (config.data.helpLink) {
|
||||
if (config.data?.helpLink) {
|
||||
submenu.push({
|
||||
label: 'Learn More...',
|
||||
click() {
|
||||
shell.openExternal(config.data.helpLink);
|
||||
shell.openExternal(config.data!.helpLink);
|
||||
},
|
||||
});
|
||||
submenu.push(separatorItem);
|
||||
}
|
||||
submenu.push({
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
label: `Version ${app.getVersion()} commit: ${__HASH_VERSION__}`,
|
||||
enabled: false,
|
||||
});
|
||||
|
@ -249,8 +252,9 @@ function createTemplate(config) {
|
|||
return template;
|
||||
}
|
||||
|
||||
function createMenu(config) {
|
||||
return Menu.buildFromTemplate(createTemplate(config));
|
||||
function createMenu(config: Config) {
|
||||
// TODO: Electron is enforcing certain variables that it doesn't need
|
||||
return Menu.buildFromTemplate(createTemplate(config) as Array<MenuItemConstructorOptions | MenuItem>);
|
||||
}
|
||||
|
||||
export default {
|
|
@ -3,18 +3,19 @@
|
|||
// See LICENSE.txt for license information.
|
||||
'use strict';
|
||||
|
||||
import {Menu} from 'electron';
|
||||
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
|
||||
import {CombinedConfig} from 'types/config';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
function createTemplate(config) {
|
||||
function createTemplate(config: CombinedConfig) {
|
||||
const teams = config.teams;
|
||||
const template = [
|
||||
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team) => {
|
||||
return {
|
||||
label: team.name,
|
||||
click: () => {
|
||||
WindowManager.switchServer(team.name, true);
|
||||
WindowManager.switchServer(team.name);
|
||||
},
|
||||
};
|
||||
}), {
|
||||
|
@ -33,8 +34,9 @@ function createTemplate(config) {
|
|||
return template;
|
||||
}
|
||||
|
||||
function createMenu(config) {
|
||||
return Menu.buildFromTemplate(createTemplate(config));
|
||||
function createMenu(config: CombinedConfig) {
|
||||
// TODO: Electron is enforcing certain variables that it doesn't need
|
||||
return Menu.buildFromTemplate(createTemplate(config) as Array<MenuItemConstructorOptions | MenuItem>);
|
||||
}
|
||||
|
||||
export default {
|
|
@ -3,6 +3,7 @@
|
|||
|
||||
import path from 'path';
|
||||
import {app, Notification} from 'electron';
|
||||
import {ServerFromURL} from 'types/utils';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
|
||||
|
@ -11,11 +12,12 @@ const defaultOptions = {
|
|||
title: 'Download Complete',
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal',
|
||||
urgency: 'normal' as Notification['urgency'],
|
||||
body: '',
|
||||
};
|
||||
|
||||
export class DownloadNotification extends Notification {
|
||||
constructor(fileName, serverInfo) {
|
||||
constructor(fileName: string, serverInfo: ServerFromURL) {
|
||||
const options = {...defaultOptions};
|
||||
if (process.platform === 'win32') {
|
||||
options.icon = appIconURL;
|
|
@ -4,6 +4,8 @@
|
|||
import path from 'path';
|
||||
import {app, Notification} from 'electron';
|
||||
|
||||
import {MentionOptions} from 'types/notification';
|
||||
|
||||
import osVersion from 'common/osVersion';
|
||||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
@ -13,23 +15,28 @@ const defaultOptions = {
|
|||
title: 'Someone mentioned you',
|
||||
silent: false,
|
||||
icon: appIconURL,
|
||||
urgency: 'normal',
|
||||
urgency: 'normal' as Notification['urgency'],
|
||||
};
|
||||
export const DEFAULT_WIN7 = 'Ding';
|
||||
|
||||
export class Mention extends Notification {
|
||||
constructor(customOptions, channel, teamId) {
|
||||
customSound: boolean;
|
||||
channel: {id: string}; // TODO: Channel from mattermost-redux
|
||||
teamId: string;
|
||||
|
||||
constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) {
|
||||
super({...defaultOptions, ...customOptions});
|
||||
const options = {...defaultOptions, ...customOptions};
|
||||
if (process.platform === 'darwin') {
|
||||
// Notification Center shows app's icon, so there were two icons on the notification.
|
||||
Reflect.deleteProperty(options, 'icon');
|
||||
}
|
||||
const isWin7 = (process.platform === 'win32' && osVersion.isLowerThanOrEqualWindows8_1() && DEFAULT_WIN7);
|
||||
const customSound = !options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7);
|
||||
const customSound = Boolean(!options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7));
|
||||
if (customSound) {
|
||||
options.silent = true;
|
||||
}
|
||||
super(options);
|
||||
|
||||
this.customSound = customSound;
|
||||
this.channel = channel;
|
||||
this.teamId = teamId;
|
|
@ -4,6 +4,9 @@
|
|||
import {shell, Notification} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {MentionData} from 'types/notification';
|
||||
import {ServerFromURL} from 'types/utils';
|
||||
|
||||
import {PLAY_SOUND} from 'common/communication';
|
||||
|
||||
import * as windowManager from '../windows/windowManager';
|
||||
|
@ -13,7 +16,7 @@ import {DownloadNotification} from './Download';
|
|||
|
||||
const currentNotifications = new Map();
|
||||
|
||||
export function displayMention(title, body, channel, teamId, silent, webcontents, data) {
|
||||
export function displayMention(title: string, body: string, channel: {id: string}, teamId: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
|
||||
if (!Notification.isSupported()) {
|
||||
log.error('notification not supported');
|
||||
return;
|
||||
|
@ -49,14 +52,14 @@ export function displayMention(title, body, channel, teamId, silent, webcontents
|
|||
|
||||
mention.on('click', () => {
|
||||
if (serverName) {
|
||||
windowManager.switchServer(serverName, true);
|
||||
windowManager.switchServer(serverName);
|
||||
webcontents.send('notification-clicked', {channel, teamId});
|
||||
}
|
||||
});
|
||||
mention.show();
|
||||
}
|
||||
|
||||
export function displayDownloadCompleted(fileName, path, serverInfo) {
|
||||
export function displayDownloadCompleted(fileName: string, path: string, serverInfo: ServerFromURL) {
|
||||
if (!Notification.isSupported()) {
|
||||
log.error('notification not supported');
|
||||
return;
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
'use strict';
|
||||
|
||||
|
|
|
@ -11,12 +11,12 @@ import * as AppState from '../appState';
|
|||
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
||||
let trayImages;
|
||||
let trayIcon;
|
||||
let trayImages: Record<string, Electron.NativeImage>;
|
||||
let trayIcon: Tray;
|
||||
let lastStatus = 'normal';
|
||||
let lastMessage = app.name;
|
||||
|
||||
export function refreshTrayImages(trayIconTheme) {
|
||||
export function refreshTrayImages(trayIconTheme: string) {
|
||||
const winTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
|
||||
|
||||
switch (process.platform) {
|
||||
|
@ -69,7 +69,7 @@ export function refreshTrayImages(trayIconTheme) {
|
|||
return trayImages;
|
||||
}
|
||||
|
||||
export function setupTray(icontheme) {
|
||||
export function setupTray(icontheme: string) {
|
||||
refreshTrayImages(icontheme);
|
||||
trayIcon = new Tray(trayImages.normal);
|
||||
if (process.platform === 'darwin') {
|
||||
|
@ -103,7 +103,7 @@ export function setupTray(icontheme) {
|
|||
});
|
||||
}
|
||||
|
||||
function setTray(status, message) {
|
||||
function setTray(status: string, message: string) {
|
||||
lastStatus = status;
|
||||
lastMessage = message;
|
||||
trayIcon.setImage(trayImages[status]);
|
||||
|
@ -116,17 +116,8 @@ export function destroyTray() {
|
|||
}
|
||||
}
|
||||
|
||||
export function setTrayMenu(tMenu, mainWindow) {
|
||||
if (process.platform === 'darwin' || process.platform === 'linux') {
|
||||
// store the information, if the tray was initialized, for checking in the settings, if the application
|
||||
// was restarted after setting "Show icon on menu bar"
|
||||
if (trayIcon) {
|
||||
trayIcon.setContextMenu(tMenu);
|
||||
mainWindow.trayWasVisible = true;
|
||||
} else {
|
||||
mainWindow.trayWasVisible = false;
|
||||
}
|
||||
} else if (trayIcon) {
|
||||
export function setTrayMenu(tMenu: Electron.Menu) {
|
||||
if (trayIcon) {
|
||||
trayIcon.setContextMenu(tMenu);
|
||||
}
|
||||
}
|
|
@ -7,12 +7,16 @@ import fs from 'fs';
|
|||
|
||||
import log from 'electron-log';
|
||||
|
||||
import urlUtils from '../common/utils/url';
|
||||
import {TrustedOrigin, PermissionType} from 'types/trustedOrigin';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import * as Validator from './Validator';
|
||||
|
||||
export default class TrustedOriginsStore {
|
||||
constructor(storeFile) {
|
||||
storeFile: string;
|
||||
data?: Map<string, TrustedOrigin>;
|
||||
|
||||
constructor(storeFile: string) {
|
||||
this.storeFile = storeFile;
|
||||
}
|
||||
|
||||
|
@ -40,18 +44,24 @@ export default class TrustedOriginsStore {
|
|||
}
|
||||
|
||||
// don't use this, is for ease of mocking it on testing
|
||||
saveToFile(stringMap) {
|
||||
saveToFile(stringMap: string) {
|
||||
fs.writeFileSync(this.storeFile, stringMap);
|
||||
}
|
||||
|
||||
save = () => {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
this.saveToFile(JSON.stringify(Object.fromEntries((this.data.entries())), null, ' '));
|
||||
};
|
||||
|
||||
// if permissions or targetUrl are invalid, this function will throw an error
|
||||
// this function stablishes all the permissions at once, overwriting whatever was before
|
||||
// to enable just one permission use addPermission instead.
|
||||
set = (targetURL, permissions) => {
|
||||
set = (targetURL: string, permissions: Record<PermissionType, boolean>) => {
|
||||
if (!this.data) {
|
||||
return;
|
||||
}
|
||||
const validPermissions = Validator.validateOriginPermissions(permissions);
|
||||
if (!validPermissions) {
|
||||
throw new Error(`Invalid permissions set for trusting ${targetURL}`);
|
||||
|
@ -60,30 +70,28 @@ export default class TrustedOriginsStore {
|
|||
};
|
||||
|
||||
// enables usage of `targetURL` for `permission`
|
||||
addPermission = (targetURL, permission) => {
|
||||
addPermission = (targetURL: string, permission: PermissionType) => {
|
||||
const origin = urlUtils.getHost(targetURL);
|
||||
const currentPermissions = this.data.get(origin) || {};
|
||||
currentPermissions[permission] = true;
|
||||
this.set(origin, currentPermissions);
|
||||
this.set(origin, {[permission]: true});
|
||||
}
|
||||
|
||||
delete = (targetURL) => {
|
||||
delete = (targetURL: string) => {
|
||||
let host;
|
||||
try {
|
||||
host = urlUtils.getHost(targetURL);
|
||||
this.data.delete(host);
|
||||
this.data?.delete(host);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
isExisting = (targetURL) => {
|
||||
return (typeof this.data.get(urlUtils.getHost(targetURL)) !== 'undefined');
|
||||
isExisting = (targetURL: string) => {
|
||||
return this.data?.has(urlUtils.getHost(targetURL)) || false;
|
||||
};
|
||||
|
||||
// if user hasn't set his preferences, it will return null (falsy)
|
||||
checkPermission = (targetURL, permission) => {
|
||||
checkPermission = (targetURL: string, permission: PermissionType) => {
|
||||
if (!permission) {
|
||||
log.error(`Missing permission request on ${targetURL}`);
|
||||
return null;
|
||||
|
@ -96,7 +104,7 @@ export default class TrustedOriginsStore {
|
|||
return null;
|
||||
}
|
||||
|
||||
const urlPermissions = this.data.get(origin);
|
||||
return urlPermissions ? urlPermissions[permission] : null;
|
||||
const urlPermissions = this.data?.get(origin);
|
||||
return urlPermissions ? urlPermissions[permission] : undefined;
|
||||
}
|
||||
}
|
|
@ -2,16 +2,18 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import electron, {app} from 'electron';
|
||||
import electron, {app, BrowserWindow} from 'electron';
|
||||
import path from 'path';
|
||||
|
||||
import {Args} from 'types/args';
|
||||
|
||||
import {PRODUCTION} from 'common/utils/constants';
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
const TAB_BAR_HEIGHT = 40;
|
||||
const BACK_BAR_HEIGHT = 36;
|
||||
|
||||
export function shouldBeHiddenOnStartup(parsedArgv) {
|
||||
export function shouldBeHiddenOnStartup(parsedArgv: Args) {
|
||||
if (parsedArgv.hidden) {
|
||||
return true;
|
||||
}
|
||||
|
@ -23,12 +25,12 @@ export function shouldBeHiddenOnStartup(parsedArgv) {
|
|||
return false;
|
||||
}
|
||||
|
||||
export function getWindowBoundaries(win, hasBackBar = false) {
|
||||
export function getWindowBoundaries(win: BrowserWindow, hasBackBar = false) {
|
||||
const {width, height} = win.getContentBounds();
|
||||
return getAdjustedWindowBoundaries(width, height, hasBackBar);
|
||||
}
|
||||
|
||||
export function getAdjustedWindowBoundaries(width, height, hasBackBar = false) {
|
||||
export function getAdjustedWindowBoundaries(width: number, height: number, hasBackBar = false) {
|
||||
return {
|
||||
x: 0,
|
||||
y: TAB_BAR_HEIGHT + (hasBackBar ? BACK_BAR_HEIGHT : 0),
|
||||
|
@ -37,12 +39,12 @@ export function getAdjustedWindowBoundaries(width, height, hasBackBar = false) {
|
|||
};
|
||||
}
|
||||
|
||||
export function getLocalURLString(urlPath, query, isMain) {
|
||||
export function getLocalURLString(urlPath: string, query?: Map<string, string>, isMain?: boolean) {
|
||||
const localURL = getLocalURL(urlPath, query, isMain);
|
||||
return localURL.href;
|
||||
}
|
||||
|
||||
export function getLocalURL(urlPath, query, isMain) {
|
||||
export function getLocalURL(urlPath: string, query?: Map<string, string>, isMain?: boolean) {
|
||||
let pathname;
|
||||
const processPath = isMain ? '' : '/renderer';
|
||||
const mode = Utils.runMode();
|
||||
|
@ -57,7 +59,7 @@ export function getLocalURL(urlPath, query, isMain) {
|
|||
const localUrl = new URL(`${protocol}://${hostname}${port}`);
|
||||
localUrl.pathname = pathname;
|
||||
if (query) {
|
||||
query.forEach((value, key) => {
|
||||
query.forEach((value: string, key: string) => {
|
||||
localUrl.searchParams.append(encodeURIComponent(key), encodeURIComponent(value));
|
||||
});
|
||||
}
|
||||
|
@ -65,7 +67,7 @@ export function getLocalURL(urlPath, query, isMain) {
|
|||
return localUrl;
|
||||
}
|
||||
|
||||
export function getLocalPreload(file) {
|
||||
export function getLocalPreload(file: string) {
|
||||
if (Utils.runMode() === PRODUCTION) {
|
||||
return path.join(electron.app.getAppPath(), `${file}`);
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView, app, ipcMain} from 'electron';
|
||||
import {BrowserView, app, ipcMain, BrowserWindow} from 'electron';
|
||||
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
|
@ -21,6 +22,8 @@ import {
|
|||
LOADSCREEN_END,
|
||||
} from 'common/communication';
|
||||
|
||||
import {MattermostServer} from 'main/MattermostServer';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
@ -28,49 +31,67 @@ import * as appState from '../appState';
|
|||
|
||||
import {removeWebContentsListeners} from './webContentEvents';
|
||||
|
||||
const READY = 1;
|
||||
const WAITING_MM = 2;
|
||||
const LOADING = 0;
|
||||
const ERROR = -1;
|
||||
enum Status {
|
||||
LOADING,
|
||||
READY,
|
||||
WAITING_MM,
|
||||
ERROR = -1,
|
||||
}
|
||||
|
||||
const ASTERISK_GROUP = 3;
|
||||
const MENTIONS_GROUP = 2;
|
||||
|
||||
export class MattermostView extends EventEmitter {
|
||||
constructor(server, win, options) {
|
||||
server: MattermostServer;
|
||||
window: BrowserWindow;
|
||||
view: BrowserView;
|
||||
isVisible: boolean;
|
||||
options: BrowserViewConstructorOptions;
|
||||
|
||||
removeLoading?: number;
|
||||
|
||||
/**
|
||||
* for backward compatibility when reading the title.
|
||||
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
|
||||
*/
|
||||
usesAsteriskForUnreads?: boolean;
|
||||
|
||||
faviconMemoize: Map<string, boolean>;
|
||||
currentFavicon?: string;
|
||||
isInitialized: boolean;
|
||||
hasBeenShown: boolean;
|
||||
altLastPressed?: boolean;
|
||||
contextMenu: ContextMenu;
|
||||
|
||||
status?: Status;
|
||||
retryLoad?: NodeJS.Timeout;
|
||||
maxRetries: number;
|
||||
|
||||
constructor(server: MattermostServer, win: BrowserWindow, options: BrowserViewConstructorOptions) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.window = win;
|
||||
|
||||
const preload = getLocalPreload('preload.js');
|
||||
const spellcheck = ((!options || typeof options.spellcheck === 'undefined') ? true : options.spellcheck);
|
||||
this.options = {
|
||||
webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
preload,
|
||||
spellcheck,
|
||||
additionalArguments: [
|
||||
`version=${app.version}`,
|
||||
`version=${app.getVersion()}`,
|
||||
`appName=${app.name}`,
|
||||
],
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
...options.webPreferences,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
this.isVisible = false;
|
||||
this.view = new BrowserView(this.options);
|
||||
this.removeLoading = null;
|
||||
this.resetLoadingStatus();
|
||||
|
||||
/**
|
||||
* for backward compatibility when reading the title.
|
||||
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
|
||||
*/
|
||||
this.usesAsteriskForUnreads = null;
|
||||
|
||||
this.faviconMemoize = new Map();
|
||||
this.currentFavicon = null;
|
||||
log.info(`BrowserView created for server ${this.server.name}`);
|
||||
|
||||
this.isInitialized = false;
|
||||
|
@ -82,24 +103,40 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
|
||||
this.contextMenu = new ContextMenu({}, this.view);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
}
|
||||
|
||||
// use the same name as the server
|
||||
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
|
||||
get name() {
|
||||
return this.server.name;
|
||||
return this.server?.name;
|
||||
}
|
||||
|
||||
resetLoadingStatus = () => {
|
||||
if (this.status !== LOADING) { // if it's already loading, don't touch anything
|
||||
this.retryLoad = null;
|
||||
this.status = LOADING;
|
||||
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything
|
||||
delete this.retryLoad;
|
||||
this.status = Status.LOADING;
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
}
|
||||
}
|
||||
|
||||
load = (someURL) => {
|
||||
const loadURL = (typeof someURL === 'undefined') ? `${this.server.url.toString()}` : urlUtils.parseURL(someURL).toString();
|
||||
load = (someURL?: URL | string) => {
|
||||
if (!this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loadURL: string;
|
||||
if (someURL) {
|
||||
const parsedURL = urlUtils.parseURL(someURL);
|
||||
if (parsedURL) {
|
||||
loadURL = parsedURL.toString();
|
||||
} else {
|
||||
log.error('Cannot parse provided url, using current server url', someURL);
|
||||
loadURL = this.server.url.toString();
|
||||
}
|
||||
} else {
|
||||
loadURL = this.server.url.toString();
|
||||
}
|
||||
log.info(`[${Util.shorten(this.server.name)}] Loading ${loadURL}`);
|
||||
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
|
@ -107,7 +144,7 @@ export class MattermostView extends EventEmitter {
|
|||
});
|
||||
}
|
||||
|
||||
retry = (loadURL) => {
|
||||
retry = (loadURL: string) => {
|
||||
return () => {
|
||||
// window was closed while retrying
|
||||
if (!this.view || !this.view.webContents) {
|
||||
|
@ -121,43 +158,43 @@ export class MattermostView extends EventEmitter {
|
|||
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.server.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
|
||||
this.status = ERROR;
|
||||
this.status = Status.ERROR;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
loadRetry = (loadURL, err) => {
|
||||
loadRetry = (loadURL: string, err: any) => {
|
||||
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
|
||||
WindowManager.sendToRenderer(LOAD_RETRY, this.server.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.server.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
|
||||
}
|
||||
|
||||
loadSuccess = (loadURL) => {
|
||||
loadSuccess = (loadURL: string) => {
|
||||
return () => {
|
||||
log.info(`[${Util.shorten(this.server.name)}] finished loading ${loadURL}`);
|
||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
if (this.status === LOADING) {
|
||||
if (this.status === Status.LOADING) {
|
||||
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
|
||||
this.handleTitleUpdate(null, this.view.webContents.getTitle());
|
||||
this.updateMentionsFromTitle(this.view.webContents.getTitle());
|
||||
this.findUnreadState(null);
|
||||
}
|
||||
this.status = WAITING_MM;
|
||||
this.status = Status.WAITING_MM;
|
||||
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL.toString());
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL);
|
||||
this.view.webContents.send(SET_SERVER_NAME, this.server.name);
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
|
||||
};
|
||||
}
|
||||
|
||||
show = (requestedVisibility) => {
|
||||
show = (requestedVisibility?: boolean) => {
|
||||
this.hasBeenShown = true;
|
||||
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
|
||||
if (request && !this.isVisible) {
|
||||
this.window.addBrowserView(this.view);
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
|
||||
if (this.status === READY) {
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
|
||||
if (this.status === Status.READY) {
|
||||
this.focus();
|
||||
}
|
||||
} else if (!request && this.isVisible) {
|
||||
|
@ -173,15 +210,12 @@ export class MattermostView extends EventEmitter {
|
|||
|
||||
hide = () => this.show(false);
|
||||
|
||||
setBounds = (boundaries) => {
|
||||
setBounds = (boundaries: Electron.Rectangle) => {
|
||||
// todo: review this, as it might not work properly with devtools/minimizing/resizing
|
||||
this.view.setBounds(boundaries);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
if (this.retryLoad) {
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
removeWebContentsListeners(this.view.webContents.id);
|
||||
if (this.window) {
|
||||
this.window.removeBrowserView(this.view);
|
||||
|
@ -189,12 +223,17 @@ export class MattermostView extends EventEmitter {
|
|||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.view.webContents.destroy();
|
||||
|
||||
this.window = null;
|
||||
this.server = null;
|
||||
this.isVisible = false;
|
||||
clearTimeout(this.retryLoad);
|
||||
if (this.retryLoad) {
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
if (this.removeLoading) {
|
||||
clearTimeout(this.removeLoading);
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
|
@ -206,22 +245,22 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
|
||||
isReady = () => {
|
||||
return this.status !== LOADING;
|
||||
return this.status !== Status.LOADING;
|
||||
}
|
||||
|
||||
needsLoadingScreen = () => {
|
||||
return !(this.status === READY || this.status === ERROR);
|
||||
return !(this.status === Status.READY || this.status === Status.ERROR);
|
||||
}
|
||||
|
||||
setInitialized = (timedout) => {
|
||||
this.status = READY;
|
||||
setInitialized = (timedout?: boolean) => {
|
||||
this.status = Status.READY;
|
||||
|
||||
if (timedout) {
|
||||
log.info(`${this.server.name} timeout expired will show the browserview`);
|
||||
this.emit(LOADSCREEN_END, this.server.name);
|
||||
}
|
||||
clearTimeout(this.removeLoading);
|
||||
this.removeLoading = null;
|
||||
delete this.removeLoading;
|
||||
}
|
||||
|
||||
openDevTools = () => {
|
||||
|
@ -229,15 +268,15 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
|
||||
getWebContents = () => {
|
||||
if (this.status === READY) {
|
||||
if (this.status === Status.READY) {
|
||||
return this.view.webContents;
|
||||
} else if (this.window) {
|
||||
return this.window.webContents; // if it's not ready you are looking at the renderer process
|
||||
}
|
||||
return WindowManager.getMainWindow.webContents;
|
||||
return WindowManager.getMainWindow()?.webContents;
|
||||
}
|
||||
|
||||
handleInputEvents = (_, input) => {
|
||||
handleInputEvents = (_: Event, input: Input) => {
|
||||
// Handler for pressing the Alt key to focus the 3-dot menu
|
||||
if (input.key === 'Alt' && input.type === 'keyUp' && this.altLastPressed) {
|
||||
this.altLastPressed = false;
|
||||
|
@ -253,8 +292,8 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
handleDidNavigate = (event, url) => {
|
||||
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url, url) || urlUtils.isAdminUrl(this.server.url, url);
|
||||
handleDidNavigate = (event: Event, url: string) => {
|
||||
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url || '', url) || urlUtils.isAdminUrl(this.server.url || '', url);
|
||||
if (isUrlTeamUrl) {
|
||||
this.setBounds(getWindowBoundaries(this.window));
|
||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
||||
|
@ -266,15 +305,19 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
}
|
||||
|
||||
handleUpdateTarget = (e, url) => {
|
||||
if (!this.server.sameOrigin(url)) {
|
||||
handleUpdateTarget = (e: Event, url: string) => {
|
||||
if (!url || !this.server.sameOrigin(url)) {
|
||||
this.emit(UPDATE_TARGET_URL, url);
|
||||
}
|
||||
}
|
||||
|
||||
titleParser = /(\((\d+)\) )?(\*)?/g
|
||||
|
||||
handleTitleUpdate = (e, title) => {
|
||||
handleTitleUpdate = (e: Event, title: string) => {
|
||||
this.updateMentionsFromTitle(title);
|
||||
}
|
||||
|
||||
updateMentionsFromTitle = (title: string) => {
|
||||
//const title = this.view.webContents.getTitle();
|
||||
const resultsIterator = title.matchAll(this.titleParser);
|
||||
const results = resultsIterator.next(); // we are only interested in the first set
|
||||
|
@ -293,13 +336,13 @@ export class MattermostView extends EventEmitter {
|
|||
appState.updateMentions(this.server.name, mentions, unreads);
|
||||
}
|
||||
|
||||
handleFaviconUpdate = (e, favicons) => {
|
||||
handleFaviconUpdate = (e: Event, favicons: string[]) => {
|
||||
if (!this.usesAsteriskForUnreads) {
|
||||
// if unread state is stored for that favicon, retrieve value.
|
||||
// if not, get related info from preload and store it for future changes
|
||||
this.currentFavicon = favicons[0];
|
||||
if (this.faviconMemoize.has(favicons[0])) {
|
||||
appState.updateUnreads(this.server.name, this.faviconMemoize.get(favicons[0]));
|
||||
appState.updateUnreads(this.server.name, Boolean(this.faviconMemoize.get(favicons[0])));
|
||||
} else {
|
||||
this.findUnreadState(favicons[0]);
|
||||
}
|
||||
|
@ -307,7 +350,7 @@ export class MattermostView extends EventEmitter {
|
|||
}
|
||||
|
||||
// if favicon is null, it will affect appState, but won't be memoized
|
||||
findUnreadState = (favicon) => {
|
||||
findUnreadState = (favicon: string | null) => {
|
||||
try {
|
||||
this.view.webContents.send(IS_UNREAD, favicon, this.server.name);
|
||||
} catch (err) {
|
||||
|
@ -318,12 +361,12 @@ export class MattermostView extends EventEmitter {
|
|||
|
||||
// if favicon is null, it means it is the initial load,
|
||||
// so don't memoize as we don't have the favicons and there is no rush to find out.
|
||||
handleFaviconIsUnread = (e, favicon, serverName, result) => {
|
||||
handleFaviconIsUnread = (e: Event, favicon: string, serverName: string, result: boolean) => {
|
||||
if (this.server && serverName === this.server.name) {
|
||||
if (favicon) {
|
||||
this.faviconMemoize.set(favicon, result);
|
||||
}
|
||||
if (favicon === null || favicon === this.currentFavicon) {
|
||||
if (!favicon || favicon === this.currentFavicon) {
|
||||
appState.updateUnreads(serverName, result);
|
||||
}
|
||||
}
|
|
@ -1,23 +1,24 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ipcMain} from 'electron';
|
||||
import {BrowserWindow, ipcMain} from 'electron';
|
||||
import {IpcMainEvent, IpcMainInvokeEvent} from 'electron/main';
|
||||
|
||||
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication.js';
|
||||
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
import {ModalView} from './modalView';
|
||||
|
||||
let modalQueue = [];
|
||||
let modalQueue: Array<ModalView<any, any>> = [];
|
||||
|
||||
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
|
||||
// should we return the original promise if called multiple times with the same key?
|
||||
export function addModal(key, html, preload, data, win) {
|
||||
export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow) {
|
||||
const foundModal = modalQueue.find((modal) => modal.key === key);
|
||||
if (!foundModal) {
|
||||
const modalPromise = new Promise((resolve, reject) => {
|
||||
const mv = new ModalView(key, html, preload, data, resolve, reject, win);
|
||||
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
|
||||
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win);
|
||||
modalQueue.push(mv);
|
||||
});
|
||||
|
||||
|
@ -34,7 +35,7 @@ ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
|
|||
ipcMain.on(MODAL_RESULT, handleModalResult);
|
||||
ipcMain.on(MODAL_CANCEL, handleModalCancel);
|
||||
|
||||
function findModalByCaller(event) {
|
||||
function findModalByCaller(event: IpcMainInvokeEvent) {
|
||||
if (modalQueue.length) {
|
||||
const requestModal = modalQueue.find((modal) => {
|
||||
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
|
||||
|
@ -44,7 +45,7 @@ function findModalByCaller(event) {
|
|||
return null;
|
||||
}
|
||||
|
||||
function handleInfoRequest(event) {
|
||||
function handleInfoRequest(event: IpcMainInvokeEvent) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
return requestModal.handleInfoRequest();
|
||||
|
@ -53,12 +54,11 @@ function handleInfoRequest(event) {
|
|||
}
|
||||
|
||||
export function showModal() {
|
||||
let noWindow;
|
||||
const withDevTools = process.env.MM_DEBUG_MODALS || false;
|
||||
modalQueue.forEach((modal, index) => {
|
||||
if (index === 0) {
|
||||
WindowManager.sendToRenderer(MODAL_OPEN);
|
||||
modal.show(noWindow, withDevTools);
|
||||
modal.show(undefined, Boolean(withDevTools));
|
||||
} else {
|
||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||
modal.hide();
|
||||
|
@ -66,7 +66,7 @@ export function showModal() {
|
|||
});
|
||||
}
|
||||
|
||||
function handleModalResult(event, data) {
|
||||
function handleModalResult(event: IpcMainEvent, data: unknown) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.resolve(data);
|
||||
|
@ -80,7 +80,7 @@ function handleModalResult(event, data) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleModalCancel(event, data) {
|
||||
function handleModalCancel(event: IpcMainEvent, data: unknown) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.reject(data);
|
|
@ -1,18 +1,31 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView} from 'electron';
|
||||
import {BrowserView, BrowserWindow} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries} from '../utils';
|
||||
|
||||
const ACTIVE = 'active';
|
||||
const SHOWING = 'showing';
|
||||
const DONE = 'done';
|
||||
enum Status {
|
||||
ACTIVE,
|
||||
SHOWING,
|
||||
DONE
|
||||
}
|
||||
|
||||
export class ModalView {
|
||||
constructor(key, html, preload, data, onResolve, onReject, currentWindow) {
|
||||
export class ModalView<T, T2> {
|
||||
key: string;
|
||||
html: string;
|
||||
data: T;
|
||||
view: BrowserView;
|
||||
onReject: (value: T2) => void;
|
||||
onResolve: (value: T2) => void;
|
||||
window: BrowserWindow;
|
||||
windowAttached?: BrowserWindow;
|
||||
status: Status;
|
||||
contextMenu: ContextMenu;
|
||||
|
||||
constructor(key: string, html: string, preload: string, data: T, onResolve: (value: T2) => void, onReject: (value: T2) => void, currentWindow: BrowserWindow) {
|
||||
this.key = key;
|
||||
this.html = html;
|
||||
this.data = data;
|
||||
|
@ -26,8 +39,8 @@ export class ModalView {
|
|||
this.onReject = onReject;
|
||||
this.onResolve = onResolve;
|
||||
this.window = currentWindow;
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
|
||||
this.status = Status.ACTIVE;
|
||||
try {
|
||||
this.view.webContents.loadURL(this.html);
|
||||
} catch (e) {
|
||||
|
@ -38,7 +51,7 @@ export class ModalView {
|
|||
this.contextMenu = new ContextMenu({}, this.view);
|
||||
}
|
||||
|
||||
show = (win, withDevTools) => {
|
||||
show = (win?: BrowserWindow, withDevTools?: boolean) => {
|
||||
if (this.windowAttached) {
|
||||
// we'll reatach
|
||||
this.windowAttached.removeBrowserView(this.view);
|
||||
|
@ -53,7 +66,7 @@ export class ModalView {
|
|||
horizontal: true,
|
||||
vertical: true,
|
||||
});
|
||||
this.status = SHOWING;
|
||||
this.status = Status.SHOWING;
|
||||
if (this.view.webContents.isLoading()) {
|
||||
this.view.webContents.once('did-finish-load', () => {
|
||||
this.view.webContents.focus();
|
||||
|
@ -77,10 +90,12 @@ export class ModalView {
|
|||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.view.webContents.destroy();
|
||||
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
delete this.windowAttached;
|
||||
this.status = Status.ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -88,21 +103,21 @@ export class ModalView {
|
|||
return this.data;
|
||||
}
|
||||
|
||||
reject = (data) => {
|
||||
reject = (data: T2) => {
|
||||
if (this.onReject) {
|
||||
this.onReject(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
this.status = Status.DONE;
|
||||
}
|
||||
|
||||
resolve = (data) => {
|
||||
resolve = (data: T2) => {
|
||||
if (this.onResolve) {
|
||||
this.onResolve(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
this.status = Status.DONE;
|
||||
}
|
||||
|
||||
isActive = () => this.status !== DONE;
|
||||
isActive = () => this.status !== Status.DONE;
|
||||
}
|
|
@ -1,7 +1,10 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
import {BrowserView, dialog} from 'electron';
|
||||
import {BrowserView, BrowserWindow, dialog} from 'electron';
|
||||
import {BrowserViewConstructorOptions} from 'electron/main';
|
||||
|
||||
import {CombinedConfig, Team} from 'types/config';
|
||||
|
||||
import {SECOND} from 'common/utils/constants';
|
||||
import {
|
||||
|
@ -26,16 +29,23 @@ const URL_VIEW_DURATION = 10 * SECOND;
|
|||
const URL_VIEW_HEIGHT = 36;
|
||||
|
||||
export class ViewManager {
|
||||
constructor(config, mainWindow) {
|
||||
configServers: Team[];
|
||||
viewOptions: BrowserViewConstructorOptions;
|
||||
views: Map<string, MattermostView>;
|
||||
currentView?: string;
|
||||
urlView?: BrowserView;
|
||||
urlViewCancel?: () => void;
|
||||
mainWindow: BrowserWindow;
|
||||
loadingScreen?: BrowserView;
|
||||
|
||||
constructor(config: CombinedConfig, mainWindow: BrowserWindow) {
|
||||
this.configServers = config.teams;
|
||||
this.viewOptions = {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.currentView = null;
|
||||
this.urlView = null;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
updateMainWindow = (mainWindow) => {
|
||||
updateMainWindow = (mainWindow: BrowserWindow) => {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
|
@ -43,7 +53,7 @@ export class ViewManager {
|
|||
return this.configServers;
|
||||
}
|
||||
|
||||
loadServer = (server) => {
|
||||
loadServer = (server: Team) => {
|
||||
const srv = new MattermostServer(server.name, server.url);
|
||||
const view = new MattermostView(srv, this.mainWindow, this.viewOptions);
|
||||
this.views.set(server.name, view);
|
||||
|
@ -61,7 +71,7 @@ export class ViewManager {
|
|||
this.configServers.forEach((server) => this.loadServer(server));
|
||||
}
|
||||
|
||||
reloadConfiguration = (configServers) => {
|
||||
reloadConfiguration = (configServers: Team[]) => {
|
||||
this.configServers = configServers.concat();
|
||||
const oldviews = this.views;
|
||||
this.views = new Map();
|
||||
|
@ -72,11 +82,11 @@ export class ViewManager {
|
|||
if (recycle && recycle.isVisible) {
|
||||
setFocus = recycle.name;
|
||||
}
|
||||
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url).toString()) {
|
||||
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) {
|
||||
oldviews.delete(recycle.name);
|
||||
this.views.set(recycle.name, recycle);
|
||||
} else {
|
||||
this.loadServer(server, this.mainWindow);
|
||||
this.loadServer(server);
|
||||
}
|
||||
});
|
||||
oldviews.forEach((unused) => {
|
||||
|
@ -98,12 +108,12 @@ export class ViewManager {
|
|||
}
|
||||
}
|
||||
|
||||
showByName = (name) => {
|
||||
showByName = (name: string) => {
|
||||
const newView = this.views.get(name);
|
||||
if (newView.isVisible) {
|
||||
return;
|
||||
}
|
||||
if (newView) {
|
||||
if (newView.isVisible) {
|
||||
return;
|
||||
}
|
||||
if (this.currentView && this.currentView !== name) {
|
||||
const previous = this.getCurrentView();
|
||||
if (previous) {
|
||||
|
@ -116,6 +126,10 @@ export class ViewManager {
|
|||
this.showLoadingScreen();
|
||||
}
|
||||
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name);
|
||||
if (!serverInfo) {
|
||||
log.error(`Couldn't find a server in the config with the name ${newView.server.name}`);
|
||||
return;
|
||||
}
|
||||
newView.window.webContents.send(SET_SERVER_KEY, serverInfo.order);
|
||||
if (newView.isReady()) {
|
||||
// if view is not ready, the renderer will have something to display instead.
|
||||
|
@ -148,18 +162,22 @@ export class ViewManager {
|
|||
view.focus();
|
||||
}
|
||||
}
|
||||
activateView = (viewName) => {
|
||||
activateView = (viewName: string) => {
|
||||
if (this.currentView === viewName) {
|
||||
this.showByName(this.currentView);
|
||||
}
|
||||
const view = this.views.get(viewName);
|
||||
if (!view) {
|
||||
log.error(`Couldn't find a view with the name ${viewName}`);
|
||||
return;
|
||||
}
|
||||
addWebContentsEventListeners(view, this.getServers);
|
||||
}
|
||||
|
||||
finishLoading = (server) => {
|
||||
finishLoading = (server: string) => {
|
||||
const view = this.views.get(server);
|
||||
if (view && this.getCurrentView() === view) {
|
||||
this.showByName(this.currentView);
|
||||
this.showByName(this.currentView!);
|
||||
this.fadeLoadingScreen();
|
||||
}
|
||||
}
|
||||
|
@ -169,19 +187,23 @@ export class ViewManager {
|
|||
}
|
||||
|
||||
getCurrentView() {
|
||||
return this.views.get(this.currentView);
|
||||
if (this.currentView) {
|
||||
return this.views.get(this.currentView);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
openViewDevTools = () => {
|
||||
const view = this.getCurrentView();
|
||||
if (view) {
|
||||
view.openDevTools({mode: 'detach'});
|
||||
view.openDevTools();
|
||||
} else {
|
||||
log.error(`couldn't find ${this.currentView}`);
|
||||
}
|
||||
}
|
||||
|
||||
findByWebContent(webContentId) {
|
||||
findByWebContent(webContentId: number) {
|
||||
let found = null;
|
||||
let serverName;
|
||||
let view;
|
||||
|
@ -198,7 +220,7 @@ export class ViewManager {
|
|||
return found;
|
||||
}
|
||||
|
||||
showURLView = (url) => {
|
||||
showURLView = (url: URL | string) => {
|
||||
if (this.urlViewCancel) {
|
||||
this.urlViewCancel();
|
||||
}
|
||||
|
@ -213,9 +235,8 @@ export class ViewManager {
|
|||
const query = new Map([['url', urlString]]);
|
||||
const localURL = getLocalURLString('urlView.html', query);
|
||||
urlView.webContents.loadURL(localURL);
|
||||
const currentWindow = this.getCurrentView().window;
|
||||
currentWindow.addBrowserView(urlView);
|
||||
const boundaries = currentWindow.getBounds();
|
||||
this.mainWindow.addBrowserView(urlView);
|
||||
const boundaries = this.mainWindow.getBounds();
|
||||
urlView.setBounds({
|
||||
x: 0,
|
||||
y: boundaries.height - URL_VIEW_HEIGHT,
|
||||
|
@ -224,11 +245,13 @@ export class ViewManager {
|
|||
});
|
||||
|
||||
const hideView = () => {
|
||||
this.urlViewCancel = null;
|
||||
currentWindow.removeBrowserView(urlView);
|
||||
delete this.urlViewCancel;
|
||||
this.mainWindow.removeBrowserView(urlView);
|
||||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
urlView.webContents.destroy();
|
||||
};
|
||||
|
||||
|
@ -263,12 +286,12 @@ export class ViewManager {
|
|||
this.createLoadingScreen();
|
||||
}
|
||||
|
||||
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||
|
||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen)) {
|
||||
this.mainWindow.setTopBrowserView(this.loadingScreen);
|
||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
|
||||
this.mainWindow.setTopBrowserView(this.loadingScreen!);
|
||||
} else {
|
||||
this.mainWindow.addBrowserView(this.loadingScreen);
|
||||
this.mainWindow.addBrowserView(this.loadingScreen!);
|
||||
}
|
||||
|
||||
this.setLoadingScreenBounds();
|
||||
|
@ -286,7 +309,7 @@ export class ViewManager {
|
|||
}
|
||||
}
|
||||
|
||||
setServerInitialized = (server) => {
|
||||
setServerInitialized = (server: string) => {
|
||||
const view = this.views.get(server);
|
||||
if (view) {
|
||||
view.setInitialized();
|
||||
|
@ -296,30 +319,40 @@ export class ViewManager {
|
|||
}
|
||||
}
|
||||
|
||||
updateLoadingScreenDarkMode = (darkMode) => {
|
||||
updateLoadingScreenDarkMode = (darkMode: boolean) => {
|
||||
if (this.loadingScreen) {
|
||||
this.loadingScreen.webContents.send(GET_LOADING_SCREEN_DATA, {darkMode});
|
||||
}
|
||||
}
|
||||
|
||||
deeplinkSuccess = (serverName) => {
|
||||
deeplinkSuccess = (serverName: string) => {
|
||||
const view = this.views.get(serverName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
this.showByName(serverName);
|
||||
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
|
||||
};
|
||||
|
||||
deeplinkFailed = (serverName, err, url) => {
|
||||
const view = this.views.get(serverName);
|
||||
deeplinkFailed = (serverName: string, err: string, url: string) => {
|
||||
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`);
|
||||
const view = this.views.get(serverName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||
}
|
||||
|
||||
handleDeepLink = (url) => {
|
||||
handleDeepLink = (url: string | URL) => {
|
||||
if (url) {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, this.configServers, true);
|
||||
if (server) {
|
||||
const view = this.views.get(server.name);
|
||||
if (!view) {
|
||||
log.error(`Couldn't find a view matching the name ${server.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// attempting to change parsedURL protocol results in it not being modified.
|
||||
const urlWithSchema = `${view.server.url.origin}${parsedURL.pathname}${parsedURL.search}`;
|
||||
|
@ -333,7 +366,7 @@ export class ViewManager {
|
|||
}
|
||||
};
|
||||
|
||||
sendToAllViews = (channel, ...args) => {
|
||||
sendToAllViews = (channel: string, ...args: any[]) => {
|
||||
this.views.forEach((view) => view.view.webContents.send(channel, ...args));
|
||||
}
|
||||
}
|
|
@ -1,12 +1,12 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserWindow, shell} from 'electron';
|
||||
import {BrowserWindow, shell, WebContents} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {DEVELOPMENT, PRODUCTION} from 'common/utils/constants';
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
|
@ -15,26 +15,32 @@ import {protocols} from '../../../electron-builder.json';
|
|||
import allowProtocolDialog from '../allowProtocolDialog';
|
||||
import {composeUserAgent} from '../utils';
|
||||
|
||||
const customLogins = {};
|
||||
const listeners = {};
|
||||
let popupWindow = null;
|
||||
import {MattermostView} from './MattermostView';
|
||||
|
||||
function isTrustedPopupWindow(webContents) {
|
||||
type CustomLogin = {
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
const customLogins: Record<number, CustomLogin> = {};
|
||||
const listeners: Record<number, () => void> = {};
|
||||
let popupWindow: BrowserWindow | undefined;
|
||||
|
||||
function isTrustedPopupWindow(webContents: WebContents) {
|
||||
if (!webContents) {
|
||||
return false;
|
||||
}
|
||||
if (!popupWindow) {
|
||||
return false;
|
||||
}
|
||||
return Utils.browserWindowFromWebContents(webContents) === popupWindow;
|
||||
return BrowserWindow.fromWebContents(webContents) === popupWindow;
|
||||
}
|
||||
|
||||
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
||||
|
||||
const generateWillNavigate = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const generateWillNavigate = (getServersFunction: () => Team[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const configServers = getServersFunction();
|
||||
const server = urlUtils.getServer(parsedURL, configServers);
|
||||
|
||||
|
@ -42,7 +48,7 @@ const generateWillNavigate = (getServersFunction) => {
|
|||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||
return;
|
||||
}
|
||||
if (parsedURL.protocol === 'mailto:') {
|
||||
|
@ -51,30 +57,24 @@ const generateWillNavigate = (getServersFunction) => {
|
|||
if (customLogins[contentID].inProgress) {
|
||||
return;
|
||||
}
|
||||
const mode = Utils.runMode();
|
||||
if (((mode === DEVELOPMENT || mode === PRODUCTION) &&
|
||||
(parsedURL.path === 'renderer/index.html' || parsedURL.path === 'renderer/settings.html'))) {
|
||||
log.info('loading settings page');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Prevented desktop from navigating to: ${url}`);
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
const generateDidStartNavigation = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const generateDidStartNavigation = (getServersFunction: () => Team[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const serverList = getServersFunction();
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, serverList);
|
||||
|
||||
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||
customLogins[contentID].inProgress = true;
|
||||
} else if (customLogins[contentID].inProgress) {
|
||||
customLogins[contentID].inProgress = false;
|
||||
|
@ -82,8 +82,8 @@ const generateDidStartNavigation = (getServersFunction) => {
|
|||
};
|
||||
};
|
||||
|
||||
const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
return (event, url) => {
|
||||
const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?: boolean) => {
|
||||
return (event: Event, url: string) => {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
event.preventDefault();
|
||||
|
@ -146,14 +146,14 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
|||
log.info(`${url} is an admin console page, preventing to open a new window`);
|
||||
return;
|
||||
}
|
||||
if (popupWindow && !popupWindow.closed && popupWindow.getURL() === url) {
|
||||
if (popupWindow && popupWindow.webContents.getURL() === url) {
|
||||
log.info(`Popup window already open at provided url: ${url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: move popups to its own and have more than one.
|
||||
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||
if (!popupWindow || popupWindow.closed) {
|
||||
if (!popupWindow) {
|
||||
popupWindow = new BrowserWindow({
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
//parent: WindowManager.getMainWindow(),
|
||||
|
@ -167,10 +167,10 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
|||
},
|
||||
});
|
||||
popupWindow.once('ready-to-show', () => {
|
||||
popupWindow.show();
|
||||
popupWindow!.show();
|
||||
});
|
||||
popupWindow.once('closed', () => {
|
||||
popupWindow = null;
|
||||
popupWindow = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -187,13 +187,13 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
|||
};
|
||||
};
|
||||
|
||||
export const removeWebContentsListeners = (id) => {
|
||||
export const removeWebContentsListeners = (id: number) => {
|
||||
if (listeners[id]) {
|
||||
listeners[id]();
|
||||
}
|
||||
};
|
||||
|
||||
export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => Team[]) => {
|
||||
const contents = mmview.view.webContents;
|
||||
|
||||
// initialize custom login tracking
|
||||
|
@ -206,7 +206,7 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
|||
}
|
||||
|
||||
const willNavigate = generateWillNavigate(getServersFunction);
|
||||
contents.on('will-navigate', willNavigate);
|
||||
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // TODO: Electron types don't include sender for some reason
|
||||
|
||||
// handle custom login requests (oath, saml):
|
||||
// 1. are we navigating to a supported local custom login path from the `/login` page?
|
||||
|
@ -214,9 +214,9 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
|||
// 2. are we finished with the custom login process?
|
||||
// - indicate custom login is NOT in progress
|
||||
const didStartNavigation = generateDidStartNavigation(getServersFunction);
|
||||
contents.on('did-start-navigation', didStartNavigation);
|
||||
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||
|
||||
const spellcheck = mmview.options.webPreferences.spellcheck;
|
||||
const spellcheck = mmview.options.webPreferences?.spellcheck;
|
||||
const newWindow = generateNewWindowListener(getServersFunction, spellcheck);
|
||||
contents.on('new-window', newWindow);
|
||||
|
||||
|
@ -227,8 +227,8 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
|||
|
||||
const removeListeners = () => {
|
||||
try {
|
||||
contents.removeListener('will-navigate', willNavigate);
|
||||
contents.removeListener('did-start-navigation', didStartNavigation);
|
||||
contents.removeListener('will-navigate', willNavigate as (e: Event, u: string) => void);
|
||||
contents.removeListener('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||
contents.removeListener('new-window', newWindow);
|
||||
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
|
||||
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
|
|
@ -6,19 +6,24 @@ import fs from 'fs';
|
|||
import path from 'path';
|
||||
import os from 'os';
|
||||
|
||||
import {app, BrowserWindow, ipcMain} from 'electron';
|
||||
import {app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {CombinedConfig} from 'types/config';
|
||||
import {SavedWindowState} from 'types/mainWindow';
|
||||
|
||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS} from 'common/communication';
|
||||
|
||||
import * as Validator from '../Validator';
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
||||
|
||||
function saveWindowState(file, window) {
|
||||
const windowState = window.getBounds();
|
||||
windowState.maximized = window.isMaximized();
|
||||
windowState.fullscreen = window.isFullScreen();
|
||||
function saveWindowState(file: string, window: BrowserWindow) {
|
||||
const windowState: SavedWindowState = {
|
||||
...window.getBounds(),
|
||||
maximized: window.isMaximized(),
|
||||
fullscreen: window.isFullScreen(),
|
||||
};
|
||||
try {
|
||||
fs.writeFileSync(file, JSON.stringify(windowState));
|
||||
} catch (e) {
|
||||
|
@ -31,7 +36,7 @@ function isFramelessWindow() {
|
|||
return os.platform() === 'darwin' || (os.platform() === 'win32' && os.release().startsWith('10'));
|
||||
}
|
||||
|
||||
function createMainWindow(config, options) {
|
||||
function createMainWindow(config: CombinedConfig, options: {linuxAppIcon: string}) {
|
||||
const defaultWindowWidth = 1000;
|
||||
const defaultWindowHeight = 700;
|
||||
const minimumWindowWidth = 400;
|
||||
|
@ -40,26 +45,23 @@ function createMainWindow(config, options) {
|
|||
// Create the browser window.
|
||||
const preload = getLocalPreload('mainWindow.js');
|
||||
const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json');
|
||||
let windowOptions;
|
||||
let savedWindowState;
|
||||
try {
|
||||
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||
windowOptions = Validator.validateBoundsInfo(windowOptions);
|
||||
if (!windowOptions) {
|
||||
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||
savedWindowState = Validator.validateBoundsInfo(savedWindowState);
|
||||
if (!savedWindowState) {
|
||||
throw new Error('Provided bounds info file does not validate, using defaults instead.');
|
||||
}
|
||||
} catch (e) {
|
||||
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
||||
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
|
||||
savedWindowState = {width: defaultWindowWidth, height: defaultWindowHeight};
|
||||
}
|
||||
|
||||
const {maximized: windowIsMaximized} = windowOptions;
|
||||
const {maximized: windowIsMaximized} = savedWindowState;
|
||||
|
||||
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
windowOptions.icon = options.linuxAppIcon;
|
||||
}
|
||||
Object.assign(windowOptions, {
|
||||
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, savedWindowState, {
|
||||
title: app.name,
|
||||
fullscreenable: true,
|
||||
show: false, // don't start the window until it is ready and only if it isn't hidden
|
||||
|
@ -67,8 +69,8 @@ function createMainWindow(config, options) {
|
|||
minWidth: minimumWindowWidth,
|
||||
minHeight: minimumWindowHeight,
|
||||
frame: !isFramelessWindow(),
|
||||
fullscreen: windowOptions.fullscreen,
|
||||
titleBarStyle: 'hidden',
|
||||
fullscreen: savedWindowState.fullscreen,
|
||||
titleBarStyle: 'hidden' as const,
|
||||
trafficLightPosition: {x: 12, y: 24},
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
webPreferences: {
|
||||
|
@ -81,10 +83,18 @@ function createMainWindow(config, options) {
|
|||
},
|
||||
});
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
windowOptions.icon = options.linuxAppIcon;
|
||||
}
|
||||
|
||||
const mainWindow = new BrowserWindow(windowOptions);
|
||||
mainWindow.setMenuBarVisibility(false);
|
||||
|
||||
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => mainWindow.isFullScreen());
|
||||
try {
|
||||
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => mainWindow.isFullScreen());
|
||||
} catch (e) {
|
||||
log.error('Tried to register second handler, skipping');
|
||||
}
|
||||
|
||||
const localURL = getLocalURLString('index.html');
|
||||
mainWindow.loadURL(localURL).catch(
|
||||
|
@ -122,7 +132,7 @@ function createMainWindow(config, options) {
|
|||
saveWindowState(boundsInfoPath, mainWindow);
|
||||
} else { // Minimize or hide the window for close button.
|
||||
event.preventDefault();
|
||||
function hideWindow(window) {
|
||||
function hideWindow(window: BrowserWindow) {
|
||||
window.blur(); // To move focus to the next top-level window in Windows
|
||||
window.hide();
|
||||
}
|
|
@ -3,15 +3,15 @@
|
|||
|
||||
import {BrowserWindow} from 'electron';
|
||||
import log from 'electron-log';
|
||||
import {CombinedConfig} from 'types/config';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
||||
|
||||
export function createSettingsWindow(mainWindow, config, withDevTools) {
|
||||
export function createSettingsWindow(mainWindow: BrowserWindow, config: CombinedConfig, withDevTools: boolean) {
|
||||
const preload = getLocalPreload('mainWindow.js');
|
||||
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
|
||||
const settingsWindow = new BrowserWindow({
|
||||
...config.data,
|
||||
parent: mainWindow,
|
||||
title: 'Desktop App Settings',
|
||||
fullscreen: false,
|
|
@ -2,28 +2,32 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import path from 'path';
|
||||
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain} from 'electron';
|
||||
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {CombinedConfig} from 'types/config';
|
||||
|
||||
import {MAXIMIZE_CHANGE, HISTORY, GET_LOADING_SCREEN_DATA, REACT_APP_INITIALIZED, LOADING_SCREEN_ANIMATION_FINISHED, FOCUS_THREE_DOT_MENU} from 'common/communication';
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
import {getAdjustedWindowBoundaries} from '../utils';
|
||||
|
||||
import {ViewManager} from '../views/viewManager';
|
||||
import {CriticalErrorHandler} from '../CriticalErrorHandler';
|
||||
import CriticalErrorHandler from '../CriticalErrorHandler';
|
||||
|
||||
import {createSettingsWindow} from './settingsWindow';
|
||||
import createMainWindow from './mainWindow';
|
||||
|
||||
// singleton module to manage application's windows
|
||||
|
||||
const status = {
|
||||
mainWindow: null,
|
||||
settingsWindow: null,
|
||||
config: null,
|
||||
viewManager: null,
|
||||
type WindowManagerStatus = {
|
||||
mainWindow?: BrowserWindow;
|
||||
settingsWindow?: BrowserWindow;
|
||||
config?: CombinedConfig;
|
||||
viewManager?: ViewManager;
|
||||
};
|
||||
|
||||
const status: WindowManagerStatus = {};
|
||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||
|
||||
ipcMain.on(HISTORY, handleHistory);
|
||||
|
@ -31,12 +35,12 @@ ipcMain.handle(GET_LOADING_SCREEN_DATA, handleLoadingScreenDataRequest);
|
|||
ipcMain.on(REACT_APP_INITIALIZED, handleReactAppInitialized);
|
||||
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, handleLoadingScreenAnimationFinished);
|
||||
|
||||
export function setConfig(data) {
|
||||
export function setConfig(data: CombinedConfig) {
|
||||
if (data) {
|
||||
status.config = data;
|
||||
}
|
||||
if (status.viewManager) {
|
||||
status.viewManager.reloadConfiguration(status.config.teams);
|
||||
if (status.viewManager && status.config) {
|
||||
status.viewManager.reloadConfiguration(status.config.teams || []);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -47,17 +51,20 @@ export function showSettingsWindow() {
|
|||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
}
|
||||
const withDevTools = process.env.MM_DEBUG_SETTINGS || false;
|
||||
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
|
||||
|
||||
status.settingsWindow = createSettingsWindow(status.mainWindow, status.config, withDevTools);
|
||||
if (!status.config) {
|
||||
return;
|
||||
}
|
||||
status.settingsWindow = createSettingsWindow(status.mainWindow!, status.config, withDevTools);
|
||||
status.settingsWindow.on('closed', () => {
|
||||
status.settingsWindow = null;
|
||||
delete status.settingsWindow;
|
||||
focusBrowserView();
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function showMainWindow(deeplinkingURL) {
|
||||
export function showMainWindow(deeplinkingURL?: string | URL) {
|
||||
if (status.mainWindow) {
|
||||
if (status.mainWindow.isVisible()) {
|
||||
status.mainWindow.focus();
|
||||
|
@ -65,6 +72,9 @@ export function showMainWindow(deeplinkingURL) {
|
|||
status.mainWindow.show();
|
||||
}
|
||||
} else {
|
||||
if (!status.config) {
|
||||
return;
|
||||
}
|
||||
status.mainWindow = createMainWindow(status.config, {
|
||||
linuxAppIcon: path.join(assetsDir, 'linux', 'app_icon.png'),
|
||||
});
|
||||
|
@ -77,14 +87,13 @@ export function showMainWindow(deeplinkingURL) {
|
|||
// window handlers
|
||||
status.mainWindow.on('closed', () => {
|
||||
log.warn('main window closed');
|
||||
status.mainWindow = null;
|
||||
delete status.mainWindow;
|
||||
});
|
||||
status.mainWindow.on('unresponsive', () => {
|
||||
const criticalErrorHandler = new CriticalErrorHandler();
|
||||
criticalErrorHandler.setMainWindow(status.mainWindow);
|
||||
criticalErrorHandler.setMainWindow(status.mainWindow!);
|
||||
criticalErrorHandler.windowUnresponsiveHandler();
|
||||
});
|
||||
status.mainWindow.on('crashed', handleMainWindowWebContentsCrashed);
|
||||
status.mainWindow.on('maximize', handleMaximizeMainWindow);
|
||||
status.mainWindow.on('unmaximize', handleUnmaximizeMainWindow);
|
||||
status.mainWindow.on('resize', handleResizeMainWindow);
|
||||
|
@ -103,24 +112,18 @@ export function showMainWindow(deeplinkingURL) {
|
|||
initializeViewManager();
|
||||
|
||||
if (deeplinkingURL) {
|
||||
status.viewManager.handleDeepLink(deeplinkingURL);
|
||||
status.viewManager!.handleDeepLink(deeplinkingURL);
|
||||
}
|
||||
}
|
||||
|
||||
export function getMainWindow(ensureCreated) {
|
||||
if (ensureCreated && status.mainWindow === null) {
|
||||
export function getMainWindow(ensureCreated?: boolean) {
|
||||
if (ensureCreated && !status.mainWindow) {
|
||||
showMainWindow();
|
||||
}
|
||||
return status.mainWindow;
|
||||
}
|
||||
|
||||
export function on(event, listener) {
|
||||
return status.mainWindow.on(event, listener);
|
||||
}
|
||||
|
||||
function handleMainWindowWebContentsCrashed() {
|
||||
throw new Error('webContents \'crashed\' event has been emitted');
|
||||
}
|
||||
export const on = status.mainWindow?.on;
|
||||
|
||||
function handleMaximizeMainWindow() {
|
||||
sendToRenderer(MAXIMIZE_CHANGE, true);
|
||||
|
@ -131,8 +134,11 @@ function handleUnmaximizeMainWindow() {
|
|||
}
|
||||
|
||||
function handleResizeMainWindow() {
|
||||
if (!(status.viewManager && status.mainWindow)) {
|
||||
return;
|
||||
}
|
||||
const currentView = status.viewManager.getCurrentView();
|
||||
let bounds;
|
||||
let bounds: Partial<Electron.Rectangle>;
|
||||
|
||||
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
|
||||
// https://github.com/electron/electron/issues/28699
|
||||
|
@ -146,7 +152,7 @@ function handleResizeMainWindow() {
|
|||
|
||||
const setBoundsFunction = () => {
|
||||
if (currentView) {
|
||||
currentView.setBounds(getAdjustedWindowBoundaries(bounds.width, bounds.height, !urlUtils.isTeamUrl(currentView.server.url, currentView.view.webContents.getURL())));
|
||||
currentView.setBounds(getAdjustedWindowBoundaries(bounds.width!, bounds.height!, !urlUtils.isTeamUrl(currentView.server.url, currentView.view.webContents.getURL())));
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -160,17 +166,17 @@ function handleResizeMainWindow() {
|
|||
status.viewManager.setLoadingScreenBounds();
|
||||
}
|
||||
|
||||
export function sendToRenderer(channel, ...args) {
|
||||
export function sendToRenderer(channel: string, ...args: any[]) {
|
||||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
}
|
||||
status.mainWindow.webContents.send(channel, ...args);
|
||||
status.mainWindow!.webContents.send(channel, ...args);
|
||||
if (status.settingsWindow && status.settingsWindow.isVisible()) {
|
||||
status.settingsWindow.webContents.send(channel, ...args);
|
||||
}
|
||||
}
|
||||
|
||||
export function sendToAll(channel, ...args) {
|
||||
export function sendToAll(channel: string, ...args: any[]) {
|
||||
sendToRenderer(channel, ...args);
|
||||
if (status.settingsWindow) {
|
||||
status.settingsWindow.webContents.send(channel, ...args);
|
||||
|
@ -179,7 +185,7 @@ export function sendToAll(channel, ...args) {
|
|||
// TODO: should we include popups?
|
||||
}
|
||||
|
||||
export function sendToMattermostViews(channel, ...args) {
|
||||
export function sendToMattermostViews(channel: string, ...args: any[]) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.sendToAllViews(channel, ...args);
|
||||
}
|
||||
|
@ -190,16 +196,16 @@ export function restoreMain() {
|
|||
if (!status.mainWindow) {
|
||||
showMainWindow();
|
||||
}
|
||||
if (!status.mainWindow.isVisible() || status.mainWindow.isMinimized()) {
|
||||
if (status.mainWindow.isMinimized()) {
|
||||
status.mainWindow.restore();
|
||||
if (!status.mainWindow!.isVisible() || status.mainWindow!.isMinimized()) {
|
||||
if (status.mainWindow!.isMinimized()) {
|
||||
status.mainWindow!.restore();
|
||||
} else {
|
||||
status.mainWindow.show();
|
||||
status.mainWindow!.show();
|
||||
}
|
||||
if (status.settingsWindow) {
|
||||
status.settingsWindow.focus();
|
||||
} else {
|
||||
status.mainWindow.focus();
|
||||
status.mainWindow!.focus();
|
||||
}
|
||||
if (process.platform === 'darwin') {
|
||||
app.dock.show();
|
||||
|
@ -207,31 +213,36 @@ export function restoreMain() {
|
|||
} else if (status.settingsWindow) {
|
||||
status.settingsWindow.focus();
|
||||
} else {
|
||||
status.mainWindow.focus();
|
||||
status.mainWindow!.focus();
|
||||
}
|
||||
}
|
||||
|
||||
export function flashFrame(flash) {
|
||||
export function flashFrame(flash: boolean) {
|
||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||
status.mainWindow.flashFrame(flash);
|
||||
status.mainWindow?.flashFrame(flash);
|
||||
if (status.settingsWindow) {
|
||||
// main might be hidden behind the settings
|
||||
status.settingsWindow.flashFrame(flash);
|
||||
}
|
||||
}
|
||||
if (process.platform === 'darwin' && status.config.notifications.bounceIcon) {
|
||||
app.dock.bounce(status.config.notifications.bounceIconType);
|
||||
if (process.platform === 'darwin' && status.config?.notifications.bounceIcon) {
|
||||
app.dock.bounce(status.config?.notifications.bounceIconType);
|
||||
}
|
||||
}
|
||||
|
||||
function drawBadge(text, small) {
|
||||
function drawBadge(text: string, small: boolean) {
|
||||
const scale = 2; // should rely display dpi
|
||||
const size = (small ? 20 : 16) * scale;
|
||||
const canvas = document.createElement('canvas');
|
||||
canvas.setAttribute('width', size);
|
||||
canvas.setAttribute('height', size);
|
||||
canvas.setAttribute('width', `${size}`);
|
||||
canvas.setAttribute('height', `${size}`);
|
||||
const ctx = canvas.getContext('2d');
|
||||
|
||||
if (!ctx) {
|
||||
log.error('Could not create canvas context');
|
||||
return null;
|
||||
}
|
||||
|
||||
// circle
|
||||
ctx.fillStyle = '#FF1744'; // Material Red A400
|
||||
ctx.beginPath();
|
||||
|
@ -248,7 +259,7 @@ function drawBadge(text, small) {
|
|||
return canvas.toDataURL();
|
||||
}
|
||||
|
||||
function createDataURL(text, small) {
|
||||
function createDataURL(text: string, small: boolean) {
|
||||
const win = status.mainWindow;
|
||||
if (!win) {
|
||||
return null;
|
||||
|
@ -263,26 +274,28 @@ function createDataURL(text, small) {
|
|||
return win.webContents.executeJavaScript(code);
|
||||
}
|
||||
|
||||
export async function setOverlayIcon(badgeText, description, small) {
|
||||
export async function setOverlayIcon(badgeText: string | undefined, description: string, small: boolean) {
|
||||
if (process.platform === 'win32') {
|
||||
let overlay = null;
|
||||
if (status.mainWindow && badgeText) {
|
||||
try {
|
||||
const dataUrl = await createDataURL(badgeText, small);
|
||||
overlay = nativeImage.createFromDataURL(dataUrl);
|
||||
} catch (err) {
|
||||
log.error(`Couldn't generate a badge: ${err}`);
|
||||
if (status.mainWindow) {
|
||||
if (badgeText) {
|
||||
try {
|
||||
const dataUrl = await createDataURL(badgeText, small);
|
||||
overlay = nativeImage.createFromDataURL(dataUrl);
|
||||
} catch (err) {
|
||||
log.error(`Couldn't generate a badge: ${err}`);
|
||||
}
|
||||
}
|
||||
status.mainWindow.setOverlayIcon(overlay, description);
|
||||
}
|
||||
status.mainWindow.setOverlayIcon(overlay, description);
|
||||
}
|
||||
}
|
||||
|
||||
export function isMainWindow(window) {
|
||||
export function isMainWindow(window: BrowserWindow) {
|
||||
return status.mainWindow && status.mainWindow === window;
|
||||
}
|
||||
|
||||
export function handleDoubleClick(e, windowType) {
|
||||
export function handleDoubleClick(e: IpcMainEvent, windowType?: string) {
|
||||
let action = 'Maximize';
|
||||
if (process.platform === 'darwin') {
|
||||
action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
|
@ -311,16 +324,16 @@ export function handleDoubleClick(e, windowType) {
|
|||
}
|
||||
|
||||
function initializeViewManager() {
|
||||
if (!status.viewManager) {
|
||||
if (!status.viewManager && status.config && status.mainWindow) {
|
||||
status.viewManager = new ViewManager(status.config, status.mainWindow);
|
||||
status.viewManager.load();
|
||||
status.viewManager.showInitial();
|
||||
}
|
||||
}
|
||||
|
||||
export function switchServer(serverName) {
|
||||
export function switchServer(serverName: string) {
|
||||
showMainWindow();
|
||||
status.viewManager.showByName(serverName);
|
||||
status.viewManager?.showByName(serverName);
|
||||
}
|
||||
|
||||
export function focusBrowserView() {
|
||||
|
@ -346,11 +359,11 @@ export function focusThreeDotMenu() {
|
|||
|
||||
function handleLoadingScreenDataRequest() {
|
||||
return {
|
||||
darkMode: status.config.darkMode,
|
||||
darkMode: status.config?.darkMode || false,
|
||||
};
|
||||
}
|
||||
|
||||
function handleReactAppInitialized(_, server) {
|
||||
function handleReactAppInitialized(e: IpcMainEvent, server: string) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.setServerInitialized(server);
|
||||
}
|
||||
|
@ -362,27 +375,19 @@ function handleLoadingScreenAnimationFinished() {
|
|||
}
|
||||
}
|
||||
|
||||
export function updateLoadingScreenDarkMode(darkMode) {
|
||||
export function updateLoadingScreenDarkMode(darkMode: boolean) {
|
||||
if (status.viewManager) {
|
||||
status.viewManager.updateLoadingScreenDarkMode(darkMode);
|
||||
}
|
||||
}
|
||||
|
||||
export function getServerNameByWebContentsId(webContentsId) {
|
||||
if (status.viewManager) {
|
||||
return status.viewManager.findByWebContent(webContentsId);
|
||||
}
|
||||
return null;
|
||||
export function getServerNameByWebContentsId(webContentsId: number) {
|
||||
return status.viewManager?.findByWebContent(webContentsId);
|
||||
}
|
||||
|
||||
export function close() {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
if (focused.id === status.mainWindow.id) {
|
||||
// TODO: figure out logic for closing
|
||||
focused.close();
|
||||
} else {
|
||||
focused.close();
|
||||
}
|
||||
focused?.close();
|
||||
}
|
||||
export function maximize() {
|
||||
const focused = BrowserWindow.getFocusedWindow();
|
||||
|
@ -404,21 +409,21 @@ export function restore() {
|
|||
}
|
||||
|
||||
export function reload() {
|
||||
const currentView = status.viewManager.getCurrentView();
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
if (currentView) {
|
||||
status.viewManager.showLoadingScreen();
|
||||
status.viewManager?.showLoadingScreen();
|
||||
currentView.reload();
|
||||
}
|
||||
}
|
||||
|
||||
export function sendToFind() {
|
||||
const currentView = status.viewManager.getCurrentView();
|
||||
const currentView = status.viewManager?.getCurrentView();
|
||||
if (currentView) {
|
||||
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: ['CmdOrCtrl', 'Shift']});
|
||||
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
|
||||
}
|
||||
}
|
||||
|
||||
export function handleHistory(event, offset) {
|
||||
export function handleHistory(event: IpcMainEvent, offset: number) {
|
||||
if (status.viewManager) {
|
||||
const activeView = status.viewManager.getCurrentView();
|
||||
if (activeView && activeView.view.webContents.canGoToOffset(offset)) {
|
|
@ -1,35 +1,42 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Alert} from 'react-bootstrap';
|
||||
|
||||
const baseClassName = 'AutoSaveIndicator';
|
||||
const leaveClassName = `${baseClassName}-Leave`;
|
||||
|
||||
const SAVING_STATE_SAVING = 'saving';
|
||||
const SAVING_STATE_SAVED = 'saved';
|
||||
const SAVING_STATE_ERROR = 'error';
|
||||
const SAVING_STATE_DONE = 'done';
|
||||
export enum SavingState {
|
||||
SAVING_STATE_SAVING = 'saving',
|
||||
SAVING_STATE_SAVED = 'saved',
|
||||
SAVING_STATE_ERROR = 'error',
|
||||
SAVING_STATE_DONE = 'done',
|
||||
}
|
||||
|
||||
function getClassNameAndMessage(savingState, errorMessage) {
|
||||
function getClassNameAndMessage(savingState: SavingState, errorMessage?: string) {
|
||||
switch (savingState) {
|
||||
case SAVING_STATE_SAVING:
|
||||
case SavingState.SAVING_STATE_SAVING:
|
||||
return {className: baseClassName, message: 'Saving...'};
|
||||
case SAVING_STATE_SAVED:
|
||||
case SavingState.SAVING_STATE_SAVED:
|
||||
return {className: baseClassName, message: 'Saved'};
|
||||
case SAVING_STATE_ERROR:
|
||||
case SavingState.SAVING_STATE_ERROR:
|
||||
return {className: `${baseClassName}`, message: errorMessage};
|
||||
case SAVING_STATE_DONE:
|
||||
case SavingState.SAVING_STATE_DONE:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
|
||||
default:
|
||||
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
|
||||
}
|
||||
}
|
||||
|
||||
export default function AutoSaveIndicator(props) {
|
||||
type Props = {
|
||||
id?: string;
|
||||
savingState: SavingState;
|
||||
errorMessage?: string;
|
||||
};
|
||||
|
||||
export default function AutoSaveIndicator(props: Props) {
|
||||
const {savingState, errorMessage, ...rest} = props;
|
||||
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
|
||||
return (
|
||||
|
@ -42,15 +49,3 @@ export default function AutoSaveIndicator(props) {
|
|||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
AutoSaveIndicator.propTypes = {
|
||||
savingState: PropTypes.string.isRequired,
|
||||
errorMessage: PropTypes.string,
|
||||
};
|
||||
|
||||
Object.assign(AutoSaveIndicator, {
|
||||
SAVING_STATE_SAVING,
|
||||
SAVING_STATE_SAVED,
|
||||
SAVING_STATE_ERROR,
|
||||
SAVING_STATE_DONE,
|
||||
});
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
|
@ -1,12 +1,21 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Modal} from 'react-bootstrap';
|
||||
|
||||
export default function DestructiveConfirmationModal(props) {
|
||||
type Props = {
|
||||
title: string;
|
||||
body: React.ReactNode;
|
||||
acceptLabel: string;
|
||||
cancelLabel: string;
|
||||
onHide: () => void;
|
||||
onAccept: React.MouseEventHandler<Button>;
|
||||
onCancel: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
export default function DestructiveConfirmationModal(props: Props) {
|
||||
const {
|
||||
title,
|
||||
body,
|
||||
|
@ -14,9 +23,13 @@ export default function DestructiveConfirmationModal(props) {
|
|||
cancelLabel,
|
||||
onAccept,
|
||||
onCancel,
|
||||
onHide,
|
||||
...rest} = props;
|
||||
return (
|
||||
<Modal {...rest}>
|
||||
<Modal
|
||||
onHide={onHide}
|
||||
{...rest}
|
||||
>
|
||||
<Modal.Header closeButton={true}>
|
||||
<Modal.Title>{title}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
@ -34,12 +47,3 @@ export default function DestructiveConfirmationModal(props) {
|
|||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
DestructiveConfirmationModal.propTypes = {
|
||||
title: PropTypes.string.isRequired,
|
||||
body: PropTypes.node.isRequired,
|
||||
acceptLabel: PropTypes.string.isRequired,
|
||||
cancelLabel: PropTypes.string.isRequired,
|
||||
onAccept: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,14 +1,21 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row, Col} from 'react-bootstrap';
|
||||
|
||||
export default function ErrorView(props) {
|
||||
type Props = {
|
||||
errorInfo?: string;
|
||||
url?: string;
|
||||
id?: string;
|
||||
active?: boolean;
|
||||
appName?: string;
|
||||
};
|
||||
|
||||
export default function ErrorView(props: Props) {
|
||||
const classNames = ['container', 'ErrorView'];
|
||||
if (!props.active) {
|
||||
classNames.push('ErrorView-hidden');
|
||||
|
@ -58,7 +65,7 @@ export default function ErrorView(props) {
|
|||
target='_blank'
|
||||
rel='noreferrer'
|
||||
>
|
||||
{props.errorInfo.validatedURL}
|
||||
{props.url}
|
||||
</a>{' from a browser window.'}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
|
@ -78,11 +85,3 @@ export default function ErrorView(props) {
|
|||
</Grid>
|
||||
);
|
||||
}
|
||||
|
||||
ErrorView.propTypes = {
|
||||
errorInfo: PropTypes.string,
|
||||
url: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
appName: PropTypes.string,
|
||||
};
|
|
@ -2,10 +2,15 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Row, Button} from 'react-bootstrap';
|
||||
|
||||
export default class ExtraBar extends React.PureComponent {
|
||||
type Props = {
|
||||
darkMode?: boolean;
|
||||
goBack?: () => void;
|
||||
show?: boolean;
|
||||
};
|
||||
|
||||
export default class ExtraBar extends React.PureComponent<Props> {
|
||||
handleBack = () => {
|
||||
if (this.props.goBack) {
|
||||
this.props.goBack();
|
||||
|
@ -42,9 +47,3 @@ export default class ExtraBar extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
ExtraBar.propTypes = {
|
||||
darkMode: PropTypes.bool,
|
||||
goBack: PropTypes.func,
|
||||
show: PropTypes.bool,
|
||||
};
|
|
@ -3,11 +3,10 @@
|
|||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useAnimationEnd from '../../hooks/useAnimationEnd.js';
|
||||
import useAnimationEnd from '../../hooks/useAnimationEnd';
|
||||
|
||||
import LoadingIcon from './LoadingIcon.jsx';
|
||||
import LoadingIcon from './LoadingIcon';
|
||||
|
||||
const LOADING_STATE = {
|
||||
INITIALIZING: 'initializing', // animation graphics are hidden
|
||||
|
@ -18,6 +17,12 @@ const LOADING_STATE = {
|
|||
|
||||
const ANIMATION_COMPLETION_DELAY = 500;
|
||||
|
||||
type Props = {
|
||||
loading: boolean;
|
||||
darkMode: boolean;
|
||||
onLoadAnimationComplete?: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* A function component for rendering the animated MM logo loading sequence
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
|
@ -27,7 +32,7 @@ const ANIMATION_COMPLETION_DELAY = 500;
|
|||
function LoadingAnimation({
|
||||
loading = false,
|
||||
darkMode = false,
|
||||
onLoadAnimationComplete = null},
|
||||
onLoadAnimationComplete = undefined}: Props,
|
||||
) {
|
||||
const loadingIconContainerRef = React.useRef(null);
|
||||
const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING);
|
||||
|
@ -55,14 +60,14 @@ function LoadingAnimation({
|
|||
}, [loadingAnimationComplete]);
|
||||
|
||||
// listen for end of the css logo animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
|
||||
setTimeout(() => {
|
||||
setLoadingAnimationComplete(true);
|
||||
}, ANIMATION_COMPLETION_DELAY);
|
||||
}, 'LoadingAnimation__compass-shrink');
|
||||
|
||||
// listen for end of final css logo fade/shrink animation sequence
|
||||
useAnimationEnd(loadingIconContainerRef, () => {
|
||||
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
|
||||
if (onLoadAnimationComplete) {
|
||||
onLoadAnimationComplete();
|
||||
}
|
||||
|
@ -84,10 +89,4 @@ function LoadingAnimation({
|
|||
);
|
||||
}
|
||||
|
||||
LoadingAnimation.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onLoadAnimationComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingAnimation;
|
|
@ -1,4 +1,4 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export {default} from './LoadingAnimation.jsx';
|
||||
export {default} from './LoadingAnimation';
|
|
@ -3,19 +3,24 @@
|
|||
|
||||
import React from 'react';
|
||||
import classNames from 'classnames';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import useTransitionEnd from '../hooks/useTransitionEnd.js';
|
||||
import useTransitionEnd from '../hooks/useTransitionEnd';
|
||||
|
||||
import LoadingAnimation from './LoadingAnimation';
|
||||
|
||||
type Props = {
|
||||
loading?: boolean;
|
||||
darkMode?: boolean;
|
||||
onFadeOutComplete?: () => void;
|
||||
};
|
||||
|
||||
/**
|
||||
* A function component for rendering the desktop app loading screen
|
||||
* @param {boolean} loading - Prop that indicates whether currently loading or not
|
||||
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
|
||||
* @param {() => void} onFadeOutComplete - Function to call when the loading animation is completely finished
|
||||
*/
|
||||
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}) {
|
||||
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}: Props) {
|
||||
const loadingScreenRef = React.useRef(null);
|
||||
|
||||
const [loadingIsComplete, setLoadingIsComplete] = React.useState(true);
|
||||
|
@ -35,10 +40,10 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
|
|||
setLoadAnimationIsComplete(true);
|
||||
}
|
||||
|
||||
useTransitionEnd(loadingScreenRef, React.useCallback(() => {
|
||||
useTransitionEnd<HTMLDivElement>(loadingScreenRef, React.useCallback(() => {
|
||||
setFadeOutIsComplete(true);
|
||||
onFadeOutComplete();
|
||||
}), ['opacity']);
|
||||
}, []), ['opacity']);
|
||||
|
||||
function loadingInProgress() {
|
||||
return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete);
|
||||
|
@ -69,10 +74,4 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
|
|||
return loadingInProgress() ? loadingScreen : null;
|
||||
}
|
||||
|
||||
LoadingScreen.propTypes = {
|
||||
loading: PropTypes.bool,
|
||||
darkMode: PropTypes.bool,
|
||||
onFadeOutComplete: PropTypes.func,
|
||||
};
|
||||
|
||||
export default LoadingScreen;
|
|
@ -1,11 +1,14 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Grid, Row} from 'react-bootstrap';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
import {IpcRendererEvent} from 'electron/renderer';
|
||||
import {DropResult} from 'react-smooth-dnd';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {
|
||||
FOCUS_BROWSERVIEW,
|
||||
|
@ -42,18 +45,54 @@ import closeButton from '../../assets/titlebar/chrome-close.svg';
|
|||
|
||||
import {playSound} from '../notificationSounds';
|
||||
|
||||
import TabBar from './TabBar.jsx';
|
||||
import ExtraBar from './ExtraBar.jsx';
|
||||
import ErrorView from './ErrorView.jsx';
|
||||
import TabBar from './TabBar';
|
||||
import ExtraBar from './ExtraBar';
|
||||
import ErrorView from './ErrorView';
|
||||
|
||||
const LOADING = 1;
|
||||
const DONE = 2;
|
||||
const RETRY = -1;
|
||||
const FAILED = 0;
|
||||
const NOSERVERS = -2;
|
||||
enum Status {
|
||||
LOADING = 1,
|
||||
DONE = 2,
|
||||
RETRY = -1,
|
||||
FAILED = 0,
|
||||
NOSERVERS = -2,
|
||||
}
|
||||
|
||||
export default class MainPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
teams: Team[];
|
||||
showAddServerButton: boolean;
|
||||
moveTabs: (originalOrder: number, newOrder: number) => Promise<number | undefined>;
|
||||
openMenu: () => void;
|
||||
darkMode: boolean;
|
||||
appName: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
key: number;
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
targetURL: string;
|
||||
maximized: boolean;
|
||||
tabStatus: Map<string, TabStatus>;
|
||||
darkMode: boolean;
|
||||
modalOpen?: boolean;
|
||||
fullScreen?: boolean;
|
||||
showExtraBar?: boolean;
|
||||
};
|
||||
|
||||
type TabStatus = {
|
||||
status: Status;
|
||||
extra?: {
|
||||
url: string;
|
||||
error: string;
|
||||
};
|
||||
}
|
||||
|
||||
export default class MainPage extends React.PureComponent<Props, State> {
|
||||
topBar: React.RefObject<HTMLDivElement>;
|
||||
threeDotMenu: React.RefObject<HTMLButtonElement>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.topBar = React.createRef();
|
||||
|
@ -66,7 +105,7 @@ export default class MainPage extends React.PureComponent {
|
|||
mentionCounts: {},
|
||||
targetURL: '',
|
||||
maximized: false,
|
||||
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: LOADING, extra: null}])),
|
||||
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: Status.LOADING}])),
|
||||
darkMode: this.props.darkMode,
|
||||
};
|
||||
}
|
||||
|
@ -79,10 +118,10 @@ export default class MainPage extends React.PureComponent {
|
|||
return this.state.tabStatus.get(tabname);
|
||||
}
|
||||
}
|
||||
return {status: NOSERVERS};
|
||||
return {status: Status.NOSERVERS};
|
||||
}
|
||||
|
||||
updateTabStatus(server, newStatusValue) {
|
||||
updateTabStatus(server: string, newStatusValue: TabStatus) {
|
||||
const status = new Map(this.state.tabStatus);
|
||||
status.set(server, newStatusValue);
|
||||
this.setState({tabStatus: status});
|
||||
|
@ -93,7 +132,7 @@ export default class MainPage extends React.PureComponent {
|
|||
window.ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}, but retrying`);
|
||||
const statusValue = {
|
||||
status: RETRY,
|
||||
status: Status.RETRY,
|
||||
extra: {
|
||||
retry,
|
||||
error: err,
|
||||
|
@ -104,13 +143,13 @@ export default class MainPage extends React.PureComponent {
|
|||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
|
||||
this.updateTabStatus(server, {status: DONE});
|
||||
this.updateTabStatus(server, {status: Status.DONE});
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}`);
|
||||
const statusValue = {
|
||||
status: FAILED,
|
||||
status: Status.FAILED,
|
||||
extra: {
|
||||
error: err,
|
||||
url: loadUrl,
|
||||
|
@ -198,44 +237,50 @@ export default class MainPage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleMaximizeState = (_, maximized) => {
|
||||
handleMaximizeState = (_: IpcRendererEvent, maximized: boolean) => {
|
||||
this.setState({maximized});
|
||||
}
|
||||
|
||||
handleFullScreenState = (isFullScreen) => {
|
||||
handleFullScreenState = (isFullScreen: boolean) => {
|
||||
this.setState({fullScreen: isFullScreen});
|
||||
}
|
||||
|
||||
handleSetServerKey = (key) => {
|
||||
handleSetServerKey = (key: number) => {
|
||||
const newKey = (this.props.teams.length + key) % this.props.teams.length;
|
||||
this.setState({key: newKey});
|
||||
}
|
||||
|
||||
handleSelect = (name, key) => {
|
||||
handleSelect = (name: string, key: number) => {
|
||||
window.ipcRenderer.send(SWITCH_SERVER, name);
|
||||
this.handleSetServerKey(key);
|
||||
}
|
||||
|
||||
handleDragAndDrop = async (dropResult) => {
|
||||
handleDragAndDrop = async (dropResult: DropResult) => {
|
||||
const {removedIndex, addedIndex} = dropResult;
|
||||
if (removedIndex === null || addedIndex === null) {
|
||||
return;
|
||||
}
|
||||
if (removedIndex !== addedIndex) {
|
||||
const teamIndex = await this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
|
||||
if (!teamIndex) {
|
||||
return;
|
||||
}
|
||||
const name = this.props.teams[teamIndex].name;
|
||||
this.handleSelect(name, teamIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleClose = (e) => {
|
||||
handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
|
||||
window.ipcRenderer.send(WINDOW_CLOSE);
|
||||
}
|
||||
|
||||
handleMinimize = (e) => {
|
||||
handleMinimize = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.send(WINDOW_MINIMIZE);
|
||||
}
|
||||
|
||||
handleMaximize = (e) => {
|
||||
handleMaximize = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
e.stopPropagation();
|
||||
window.ipcRenderer.send(WINDOW_MAXIMIZE);
|
||||
}
|
||||
|
@ -246,7 +291,7 @@ export default class MainPage extends React.PureComponent {
|
|||
|
||||
openMenu = () => {
|
||||
if (window.process.platform !== 'darwin') {
|
||||
this.threeDotMenu.current.blur();
|
||||
this.threeDotMenu.current?.blur();
|
||||
}
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
@ -263,10 +308,6 @@ export default class MainPage extends React.PureComponent {
|
|||
window.ipcRenderer.send(FOCUS_BROWSERVIEW);
|
||||
}
|
||||
|
||||
setInputRef = (ref) => {
|
||||
this.inputRef = ref;
|
||||
}
|
||||
|
||||
render() {
|
||||
const tabsRow = (
|
||||
<TabBar
|
||||
|
@ -352,7 +393,7 @@ export default class MainPage extends React.PureComponent {
|
|||
>
|
||||
<div
|
||||
ref={this.topBar}
|
||||
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
|
||||
className={'topBar-bg'}
|
||||
>
|
||||
<button
|
||||
className='three-dot-menu'
|
||||
|
@ -383,32 +424,29 @@ export default class MainPage extends React.PureComponent {
|
|||
return null;
|
||||
}
|
||||
switch (tabStatus.status) {
|
||||
case NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
|
||||
case Status.NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
|
||||
component = (
|
||||
<ErrorView
|
||||
id={'NoServers'}
|
||||
className='errorView'
|
||||
errorInfo={'No Servers configured'}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
retry={null}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case FAILED:
|
||||
case Status.FAILED:
|
||||
component = (
|
||||
<ErrorView
|
||||
id={this.state.key + '-fail'}
|
||||
className='errorView'
|
||||
errorInfo={tabStatus.extra ? tabStatus.extra.error : null}
|
||||
errorInfo={tabStatus.extra?.error}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
appName={this.props.appName}
|
||||
/>);
|
||||
break;
|
||||
case LOADING:
|
||||
case RETRY:
|
||||
case DONE:
|
||||
case Status.LOADING:
|
||||
case Status.RETRY:
|
||||
case Status.DONE:
|
||||
component = null;
|
||||
}
|
||||
return component;
|
||||
|
@ -441,12 +479,3 @@ export default class MainPage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
MainPage.propTypes = {
|
||||
teams: PropTypes.array.isRequired,
|
||||
showAddServerButton: PropTypes.bool.isRequired,
|
||||
moveTabs: PropTypes.func.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
darkMode: PropTypes.bool.isRequired,
|
||||
appName: PropTypes.string.isRequired,
|
||||
};
|
|
@ -1,19 +1,42 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
|
||||
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent {
|
||||
type Props = {
|
||||
onClose: () => void;
|
||||
onSave?: (team: TeamWithIndex) => void;
|
||||
team?: TeamWithIndex;
|
||||
editMode?: boolean;
|
||||
show?: boolean;
|
||||
restoreFocus?: boolean;
|
||||
currentOrder?: number;
|
||||
setInputRef?: (inputRef: HTMLInputElement) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
teamName: string;
|
||||
teamUrl: string;
|
||||
teamIndex?: number;
|
||||
teamOrder: number;
|
||||
saveStarted: boolean;
|
||||
}
|
||||
|
||||
export default class NewTeamModal extends React.PureComponent<Props, State> {
|
||||
wasShown?: boolean;
|
||||
teamNameInputRef?: HTMLInputElement;
|
||||
|
||||
static defaultProps = {
|
||||
restoreFocus: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.wasShown = false;
|
||||
|
@ -29,7 +52,7 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
this.setState({
|
||||
teamName: this.props.team ? this.props.team.name : '',
|
||||
teamUrl: this.props.team ? this.props.team.url : '',
|
||||
teamIndex: this.props.team ? this.props.team.index : false,
|
||||
teamIndex: this.props.team?.index,
|
||||
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
|
||||
saveStarted: false,
|
||||
});
|
||||
|
@ -46,7 +69,7 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
return this.getTeamNameValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamNameChange = (e) => {
|
||||
handleTeamNameChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({
|
||||
teamName: e.target.value,
|
||||
});
|
||||
|
@ -72,7 +95,7 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
return this.getTeamUrlValidationError() === null ? null : 'error';
|
||||
}
|
||||
|
||||
handleTeamUrlChange = (e) => {
|
||||
handleTeamUrlChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({
|
||||
teamUrl: e.target.value,
|
||||
});
|
||||
|
@ -102,10 +125,10 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
saveStarted: true,
|
||||
}, () => {
|
||||
if (this.validateForm()) {
|
||||
this.props.onSave({
|
||||
this.props.onSave?.({
|
||||
url: this.state.teamUrl,
|
||||
name: this.state.teamName,
|
||||
index: this.state.teamIndex,
|
||||
index: this.state.teamIndex!,
|
||||
order: this.state.teamOrder,
|
||||
});
|
||||
}
|
||||
|
@ -139,7 +162,7 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
show={this.props.show}
|
||||
id='newServerModal'
|
||||
enforceFocus={true}
|
||||
onEntered={() => this.teamNameInputRef.focus()}
|
||||
onEntered={() => this.teamNameInputRef?.focus()}
|
||||
onHide={this.props.onClose}
|
||||
restoreFocus={this.props.restoreFocus}
|
||||
onKeyDown={(e) => {
|
||||
|
@ -231,14 +254,3 @@ export default class NewTeamModal extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
NewTeamModal.propTypes = {
|
||||
onClose: PropTypes.func,
|
||||
onSave: PropTypes.func,
|
||||
team: PropTypes.object,
|
||||
editMode: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
restoreFocus: PropTypes.bool,
|
||||
currentOrder: PropTypes.number,
|
||||
setInputRef: PropTypes.func,
|
||||
};
|
|
@ -1,14 +1,21 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal} from 'react-bootstrap';
|
||||
import {Button, Modal} from 'react-bootstrap';
|
||||
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal.jsx';
|
||||
import DestructiveConfirmationModal from './DestructiveConfirmModal';
|
||||
|
||||
export default function RemoveServerModal(props) {
|
||||
type Props = {
|
||||
show: boolean;
|
||||
serverName: string;
|
||||
onHide: () => void;
|
||||
onAccept: React.MouseEventHandler<Button>;
|
||||
onCancel: React.MouseEventHandler<Button>;
|
||||
}
|
||||
|
||||
export default function RemoveServerModal(props: Props) {
|
||||
const {serverName, ...rest} = props;
|
||||
return (
|
||||
<DestructiveConfirmationModal
|
||||
|
@ -30,7 +37,3 @@ export default function RemoveServerModal(props) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
RemoveServerModal.propTypes = {
|
||||
serverName: PropTypes.string.isRequired,
|
||||
};
|
|
@ -1,38 +1,79 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
/* eslint-disable max-lines */
|
||||
|
||||
import 'renderer/css/settings.css';
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row, Button} from 'react-bootstrap';
|
||||
|
||||
import {debounce} from 'underscore';
|
||||
|
||||
import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
|
||||
import {DeepPartial} from 'types/utils';
|
||||
|
||||
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import TeamList from './TeamList.jsx';
|
||||
import AutoSaveIndicator from './AutoSaveIndicator.jsx';
|
||||
import TeamList from './TeamList';
|
||||
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
|
||||
|
||||
const CONFIG_TYPE_SERVERS = 'servers';
|
||||
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
|
||||
|
||||
function backToIndex(serverName) {
|
||||
type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
|
||||
|
||||
type State = DeepPartial<CombinedConfig> & {
|
||||
ready: boolean;
|
||||
maximized?: boolean;
|
||||
teams?: Team[];
|
||||
showAddTeamForm: boolean;
|
||||
trayWasVisible?: boolean;
|
||||
firstRun?: boolean;
|
||||
savingState: SavingStateItems;
|
||||
userOpenedDownloadDialog: boolean;
|
||||
}
|
||||
|
||||
type SavingStateItems = {
|
||||
appOptions: SavingState;
|
||||
servers: SavingState;
|
||||
};
|
||||
|
||||
type SaveQueueItem = {
|
||||
configType: ConfigType;
|
||||
key: keyof CombinedConfig;
|
||||
data: CombinedConfig[keyof CombinedConfig];
|
||||
}
|
||||
|
||||
function backToIndex(serverName: string) {
|
||||
window.ipcRenderer.send(SWITCH_SERVER, serverName);
|
||||
window.close();
|
||||
}
|
||||
|
||||
export default class SettingsPage extends React.PureComponent {
|
||||
constructor(props) {
|
||||
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
|
||||
trayIconThemeRef: React.RefObject<FormGroup>;
|
||||
downloadLocationRef: React.RefObject<HTMLInputElement>;
|
||||
showTrayIconRef: React.RefObject<Checkbox>;
|
||||
autostartRef: React.RefObject<Checkbox>;
|
||||
minimizeToTrayRef: React.RefObject<Checkbox>;
|
||||
flashWindowRef: React.RefObject<Checkbox>;
|
||||
bounceIconRef: React.RefObject<Checkbox>;
|
||||
showUnreadBadgeRef: React.RefObject<Checkbox>;
|
||||
useSpellCheckerRef: React.RefObject<Checkbox>;
|
||||
enableHardwareAccelerationRef: React.RefObject<Checkbox>;
|
||||
|
||||
saveQueue: SaveQueueItem[];
|
||||
|
||||
constructor(props: Record<string, never>) {
|
||||
super(props);
|
||||
this.state = {
|
||||
ready: false,
|
||||
teams: [],
|
||||
showAddTeamForm: false,
|
||||
savingState: {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
appOptions: SavingState.SAVING_STATE_DONE,
|
||||
servers: SavingState.SAVING_STATE_DONE,
|
||||
},
|
||||
userOpenedDownloadDialog: false,
|
||||
};
|
||||
|
@ -67,26 +108,26 @@ export default class SettingsPage extends React.PureComponent {
|
|||
|
||||
getConfig = () => {
|
||||
window.ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
|
||||
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config)});
|
||||
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config) as Omit<State, 'ready'>});
|
||||
});
|
||||
}
|
||||
|
||||
convertConfigDataToState = (configData, currentState = {}) => {
|
||||
const newState = Object.assign({}, configData);
|
||||
convertConfigDataToState = (configData: Partial<LocalConfiguration>, currentState: Partial<State> = {}) => {
|
||||
const newState = Object.assign({} as State, configData);
|
||||
newState.showAddTeamForm = currentState.showAddTeamForm || false;
|
||||
newState.trayWasVisible = currentState.trayWasVisible || false;
|
||||
if (newState.teams.length === 0 && currentState.firstRun !== false) {
|
||||
if (newState.teams?.length === 0 && currentState.firstRun !== false) {
|
||||
newState.firstRun = false;
|
||||
newState.showAddTeamForm = true;
|
||||
}
|
||||
newState.savingState = currentState.savingState || {
|
||||
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
servers: AutoSaveIndicator.SAVING_STATE_DONE,
|
||||
appOptions: SavingState.SAVING_STATE_DONE,
|
||||
servers: SavingState.SAVING_STATE_DONE,
|
||||
};
|
||||
return newState;
|
||||
}
|
||||
|
||||
saveSetting = (configType, {key, data}) => {
|
||||
saveSetting = (configType: ConfigType, {key, data}: {key: keyof CombinedConfig; data: CombinedConfig[keyof CombinedConfig]}) => {
|
||||
this.saveQueue.push({
|
||||
configType,
|
||||
key,
|
||||
|
@ -115,25 +156,25 @@ export default class SettingsPage extends React.PureComponent {
|
|||
|
||||
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
|
||||
if (count > 0) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING;
|
||||
} else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED;
|
||||
this.resetSaveState(configType);
|
||||
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVING;
|
||||
} else if (count === 0 && savingState[configType as keyof SavingStateItems] === SavingState.SAVING_STATE_SAVING) {
|
||||
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVED;
|
||||
this.resetSaveState(configType as keyof SavingStateItems);
|
||||
}
|
||||
});
|
||||
|
||||
this.setState({savingState});
|
||||
}
|
||||
|
||||
resetSaveState = debounce((configType) => {
|
||||
if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) {
|
||||
resetSaveState = debounce((configType: keyof SavingStateItems) => {
|
||||
if (this.state.savingState[configType] !== SavingState.SAVING_STATE_SAVING) {
|
||||
const savingState = Object.assign({}, this.state.savingState);
|
||||
savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE;
|
||||
savingState[configType] = SavingState.SAVING_STATE_DONE;
|
||||
this.setState({savingState});
|
||||
}
|
||||
}, 2000);
|
||||
|
||||
handleTeamsChange = (teams) => {
|
||||
handleTeamsChange = (teams: Team[]) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
showAddTeamForm: false,
|
||||
|
@ -145,7 +186,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleChangeShowTrayIcon = () => {
|
||||
const shouldShowTrayIcon = !this.showTrayIconRef.current.props.checked;
|
||||
const shouldShowTrayIcon = !this.showTrayIconRef.current?.props.checked;
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
|
||||
this.setState({
|
||||
showTrayIcon: shouldShowTrayIcon,
|
||||
|
@ -158,7 +199,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleChangeTrayIconTheme = (theme) => {
|
||||
handleChangeTrayIconTheme = (theme: string) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
|
||||
this.setState({
|
||||
trayIconTheme: theme,
|
||||
|
@ -166,14 +207,14 @@ export default class SettingsPage extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleChangeAutoStart = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current?.props.checked});
|
||||
this.setState({
|
||||
autostart: !this.autostartRef.current.props.checked,
|
||||
autostart: !this.autostartRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeMinimizeToTray = () => {
|
||||
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current.props.checked;
|
||||
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current?.props.checked;
|
||||
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
|
||||
this.setState({
|
||||
|
@ -185,10 +226,10 @@ export default class SettingsPage extends React.PureComponent {
|
|||
this.setState({
|
||||
showAddTeamForm: !this.state.showAddTeamForm,
|
||||
});
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
}
|
||||
|
||||
setShowTeamFormVisibility = (val) => {
|
||||
setShowTeamFormVisibility = (val: boolean) => {
|
||||
this.setState({
|
||||
showAddTeamForm: val,
|
||||
});
|
||||
|
@ -199,13 +240,13 @@ export default class SettingsPage extends React.PureComponent {
|
|||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
|
||||
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -215,18 +256,18 @@ export default class SettingsPage extends React.PureComponent {
|
|||
key: 'notifications',
|
||||
data: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
bounceIcon: !this.bounceIconRef.current?.props.checked,
|
||||
},
|
||||
});
|
||||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIcon: !this.bounceIconRef.current.props.checked,
|
||||
bounceIcon: !this.bounceIconRef.current?.props.checked,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleBounceIconType = (event) => {
|
||||
handleBounceIconType = (event: React.ChangeEvent<Radio & HTMLInputElement>) => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
|
||||
key: 'notifications',
|
||||
data: {
|
||||
|
@ -237,33 +278,33 @@ export default class SettingsPage extends React.PureComponent {
|
|||
this.setState({
|
||||
notifications: {
|
||||
...this.state.notifications,
|
||||
bounceIconType: event.target.value,
|
||||
bounceIconType: event.target.value as 'critical' | 'informational',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
handleShowUnreadBadge = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current?.props.checked});
|
||||
this.setState({
|
||||
showUnreadBadge: !this.showUnreadBadgeRef.current.props.checked,
|
||||
showUnreadBadge: !this.showUnreadBadgeRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeUseSpellChecker = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current?.props.checked});
|
||||
this.setState({
|
||||
useSpellChecker: !this.useSpellCheckerRef.current.props.checked,
|
||||
useSpellChecker: !this.useSpellCheckerRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
handleChangeEnableHardwareAcceleration = () => {
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current.props.checked});
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current?.props.checked});
|
||||
this.setState({
|
||||
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current.props.checked,
|
||||
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current?.props.checked,
|
||||
});
|
||||
}
|
||||
|
||||
saveDownloadLocation = (location) => {
|
||||
saveDownloadLocation = (location: string) => {
|
||||
if (!location) {
|
||||
return;
|
||||
}
|
||||
|
@ -273,7 +314,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
|
||||
}
|
||||
|
||||
handleChangeDownloadLocation = (e) => {
|
||||
handleChangeDownloadLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
this.saveDownloadLocation(e.target.value);
|
||||
}
|
||||
|
||||
|
@ -285,8 +326,8 @@ export default class SettingsPage extends React.PureComponent {
|
|||
this.setState({userOpenedDownloadDialog: false});
|
||||
}
|
||||
|
||||
updateTeam = (index, newData) => {
|
||||
const teams = this.state.teams;
|
||||
updateTeam = (index: number, newData: Team) => {
|
||||
const teams = this.state.teams || [];
|
||||
teams[index] = newData;
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
|
@ -294,8 +335,8 @@ export default class SettingsPage extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
addServer = (team) => {
|
||||
const teams = this.state.teams;
|
||||
addServer = (team: Team) => {
|
||||
const teams = this.state.teams || [];
|
||||
teams.push(team);
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
|
@ -303,12 +344,6 @@ export default class SettingsPage extends React.PureComponent {
|
|||
});
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
// @eslint-ignore
|
||||
this.threeDotMenu.current.blur();
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
|
||||
}
|
||||
|
@ -317,7 +352,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
const settingsPage = {
|
||||
navbar: {
|
||||
backgroundColor: '#fff',
|
||||
position: 'relative',
|
||||
position: 'relative' as const,
|
||||
},
|
||||
close: {
|
||||
textDecoration: 'none',
|
||||
|
@ -329,7 +364,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
color: '#bbb',
|
||||
},
|
||||
heading: {
|
||||
textAlign: 'center',
|
||||
textAlign: 'center' as const,
|
||||
fontSize: '24px',
|
||||
margin: '0',
|
||||
padding: '1em 0',
|
||||
|
@ -356,7 +391,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
padding: '0 12px',
|
||||
borderRadius: '4px',
|
||||
border: '1px solid #ccc',
|
||||
fontWeight: '500',
|
||||
fontWeight: 500,
|
||||
},
|
||||
|
||||
downloadLocationButton: {
|
||||
|
@ -372,14 +407,12 @@ export default class SettingsPage extends React.PureComponent {
|
|||
<Row>
|
||||
<Col md={12}>
|
||||
<TeamList
|
||||
teams={this.state.teams}
|
||||
teams={this.state.teams!}
|
||||
showAddTeamForm={this.state.showAddTeamForm}
|
||||
toggleAddTeamForm={this.toggleShowTeamForm}
|
||||
setAddTeamFormVisibility={this.setShowTeamFormVisibility}
|
||||
onTeamsChange={this.handleTeamsChange}
|
||||
updateTeam={this.updateTeam}
|
||||
addServer={this.addServer}
|
||||
allowTeamEdit={this.state.enableServerManagement}
|
||||
onTeamClick={(name) => {
|
||||
backToIndex(name);
|
||||
}}
|
||||
|
@ -576,7 +609,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
name='trayIconTheme'
|
||||
value='light'
|
||||
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('light', event)}
|
||||
onChange={() => this.handleChangeTrayIconTheme('light')}
|
||||
>
|
||||
{'Light'}
|
||||
</Radio>
|
||||
|
@ -586,7 +619,7 @@ export default class SettingsPage extends React.PureComponent {
|
|||
name='trayIconTheme'
|
||||
value='dark'
|
||||
defaultChecked={this.state.trayIconTheme === 'dark'}
|
||||
onChange={(event) => this.handleChangeTrayIconTheme('dark', event)}
|
||||
onChange={() => this.handleChangeTrayIconTheme('dark')}
|
||||
>{'Dark'}</Radio>
|
||||
</FormGroup>,
|
||||
);
|
||||
|
@ -719,7 +752,3 @@ export default class SettingsPage extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
SettingsPage.propTypes = {
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
};
|
|
@ -1,17 +1,39 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Nav, NavItem} from 'react-bootstrap';
|
||||
import {Container, Draggable} from 'react-smooth-dnd';
|
||||
import {Container, Draggable, OnDropCallback} from 'react-smooth-dnd';
|
||||
import PlusIcon from 'mdi-react/PlusIcon';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import {GET_CONFIGURATION} from 'common/communication';
|
||||
|
||||
export default class TabBar extends React.PureComponent { // need "this"
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
activeKey: number;
|
||||
id: string;
|
||||
isDarkMode: boolean;
|
||||
onSelect: (name: string, index: number) => void;
|
||||
teams: Team[];
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
showAddServerButton: boolean;
|
||||
onAddServer: () => void;
|
||||
onDrop: OnDropCallback;
|
||||
tabsDisabled?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
hasGPOTeams: boolean;
|
||||
};
|
||||
|
||||
export default class TabBar extends React.PureComponent<Props, State> { // need "this"
|
||||
container?: React.RefObject<Container>;
|
||||
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
hasGPOTeams: false,
|
||||
|
@ -37,7 +59,7 @@ export default class TabBar extends React.PureComponent { // need "this"
|
|||
mentionCount = this.props.mentionCounts[index];
|
||||
}
|
||||
|
||||
let badgeDiv;
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-expired'/>
|
||||
|
@ -63,7 +85,6 @@ export default class TabBar extends React.PureComponent { // need "this"
|
|||
draggable={false}
|
||||
ref={id}
|
||||
active={this.props.activeKey === index}
|
||||
activeKey={this.props.activeKey}
|
||||
onMouseDown={() => {
|
||||
this.props.onSelect(team.name, index);
|
||||
}}
|
||||
|
@ -98,7 +119,6 @@ export default class TabBar extends React.PureComponent { // need "this"
|
|||
eventKey='addServerButton'
|
||||
draggable={false}
|
||||
title='Add new server'
|
||||
activeKey={this.props.activeKey}
|
||||
onSelect={() => {
|
||||
this.props.onAddServer();
|
||||
}}
|
||||
|
@ -111,7 +131,7 @@ export default class TabBar extends React.PureComponent { // need "this"
|
|||
);
|
||||
}
|
||||
|
||||
const navContainer = (ref) => (
|
||||
const navContainer = (ref: React.RefObject<Nav>) => (
|
||||
<Nav
|
||||
ref={ref}
|
||||
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
|
||||
|
@ -136,18 +156,3 @@ export default class TabBar extends React.PureComponent { // need "this"
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
activeKey: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
isDarkMode: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
teams: PropTypes.array,
|
||||
sessionsExpired: PropTypes.object,
|
||||
unreadCounts: PropTypes.object,
|
||||
mentionCounts: PropTypes.object,
|
||||
showAddServerButton: PropTypes.bool,
|
||||
onAddServer: PropTypes.func,
|
||||
onDrop: PropTypes.func,
|
||||
tabsDisabled: PropTypes.bool,
|
||||
};
|
|
@ -1,17 +1,34 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {ListGroup} from 'react-bootstrap';
|
||||
|
||||
import TeamListItem from './TeamListItem.jsx';
|
||||
import NewTeamModal from './NewTeamModal.jsx';
|
||||
import RemoveServerModal from './RemoveServerModal.jsx';
|
||||
import {Team, TeamWithIndex} from 'types/config';
|
||||
|
||||
export default class TeamList extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import TeamListItem from './TeamListItem';
|
||||
import NewTeamModal from './NewTeamModal';
|
||||
import RemoveServerModal from './RemoveServerModal';
|
||||
|
||||
type Props = {
|
||||
onTeamClick: (teamName: string) => void;
|
||||
onTeamsChange: (teams: Team[]) => void;
|
||||
showAddTeamForm?: boolean;
|
||||
teams: Team[];
|
||||
addServer: (team: Team) => void;
|
||||
updateTeam: (index: number, team: Team) => void;
|
||||
setAddTeamFormVisibility: (visible: boolean) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
team: TeamWithIndex;
|
||||
showEditTeamForm: boolean;
|
||||
indexToRemoveServer: number;
|
||||
}
|
||||
|
||||
export default class TeamList extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
|
@ -20,13 +37,13 @@ export default class TeamList extends React.PureComponent {
|
|||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: props.teams.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamRemove = (index) => {
|
||||
handleTeamRemove = (index: number) => {
|
||||
console.log(index);
|
||||
const teams = this.props.teams;
|
||||
const removedOrder = this.props.teams[index].order;
|
||||
|
@ -39,7 +56,7 @@ export default class TeamList extends React.PureComponent {
|
|||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
handleTeamAdd = (team) => {
|
||||
handleTeamAdd = (team: TeamWithIndex) => {
|
||||
const teams = this.props.teams;
|
||||
|
||||
// check if team already exists and then change existing team or add new one
|
||||
|
@ -56,7 +73,7 @@ export default class TeamList extends React.PureComponent {
|
|||
team: {
|
||||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: teams.length,
|
||||
},
|
||||
});
|
||||
|
@ -64,7 +81,7 @@ export default class TeamList extends React.PureComponent {
|
|||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
openServerRemoveModal = (indexForServer) => {
|
||||
openServerRemoveModal = (indexForServer: number) => {
|
||||
this.setState({indexToRemoveServer: indexForServer});
|
||||
}
|
||||
|
||||
|
@ -72,16 +89,16 @@ export default class TeamList extends React.PureComponent {
|
|||
this.setState({indexToRemoveServer: -1});
|
||||
}
|
||||
|
||||
handleTeamRemovePrompt = (index) => {
|
||||
handleTeamRemovePrompt = (index: number) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
this.openServerRemoveModal(index);
|
||||
};
|
||||
}
|
||||
|
||||
handleTeamEditing = (team, index) => {
|
||||
handleTeamEditing = (team: Team, index: number) => {
|
||||
return () => {
|
||||
document.activeElement.blur();
|
||||
(document.activeElement as HTMLElement).blur();
|
||||
this.setState({
|
||||
showEditTeamForm: true,
|
||||
team: {
|
||||
|
@ -119,7 +136,7 @@ export default class TeamList extends React.PureComponent {
|
|||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: this.props.teams.length,
|
||||
},
|
||||
});
|
||||
|
@ -137,12 +154,11 @@ export default class TeamList extends React.PureComponent {
|
|||
this.props.updateTeam(newTeam.index, teamData);
|
||||
}
|
||||
this.setState({
|
||||
showNewTeamModal: false,
|
||||
showEditTeamForm: false,
|
||||
team: {
|
||||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
index: 0,
|
||||
order: newTeam.order + 1,
|
||||
},
|
||||
});
|
||||
|
@ -175,13 +191,3 @@ export default class TeamList extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamList.propTypes = {
|
||||
onTeamClick: PropTypes.func,
|
||||
onTeamsChange: PropTypes.func,
|
||||
showAddTeamForm: PropTypes.bool,
|
||||
teams: PropTypes.array,
|
||||
addServer: PropTypes.func,
|
||||
updateTeam: PropTypes.func,
|
||||
setAddTeamFormVisibility: PropTypes.func,
|
||||
};
|
|
@ -1,11 +1,18 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
export default class TeamListItem extends React.PureComponent {
|
||||
type Props = {
|
||||
name: string;
|
||||
onTeamEditing: () => void;
|
||||
onTeamRemove: () => void;
|
||||
onTeamClick: React.MouseEventHandler<HTMLDivElement>;
|
||||
url: string;
|
||||
};
|
||||
|
||||
export default class TeamListItem extends React.PureComponent<Props> {
|
||||
handleTeamRemove = () => {
|
||||
this.props.onTeamRemove();
|
||||
}
|
||||
|
@ -39,11 +46,3 @@ export default class TeamListItem extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
TeamListItem.propTypes = {
|
||||
name: PropTypes.string,
|
||||
onTeamEditing: PropTypes.func,
|
||||
onTeamRemove: PropTypes.func,
|
||||
onTeamClick: PropTypes.func,
|
||||
url: PropTypes.string,
|
||||
};
|
|
@ -1,12 +1,17 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
|
||||
|
||||
function InstallButton(props) {
|
||||
type InstallButtonProps = {
|
||||
notifyOnly?: boolean;
|
||||
onClickInstall?: React.MouseEventHandler<Button>;
|
||||
onClickDownload?: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
function InstallButton(props: InstallButtonProps) {
|
||||
if (props.notifyOnly) {
|
||||
return (
|
||||
<Button
|
||||
|
@ -23,13 +28,20 @@ function InstallButton(props) {
|
|||
);
|
||||
}
|
||||
|
||||
InstallButton.propTypes = {
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
type UpdaterPageProps = {
|
||||
appName: string;
|
||||
notifyOnly?: boolean;
|
||||
isDownloading?: boolean;
|
||||
progress?: number;
|
||||
onClickInstall?: React.MouseEventHandler<Button>;
|
||||
onClickDownload?: React.MouseEventHandler<Button>;
|
||||
onClickReleaseNotes?: React.MouseEventHandler<HTMLAnchorElement>;
|
||||
onClickRemind?: React.MouseEventHandler<Button>;
|
||||
onClickSkip?: React.MouseEventHandler<Button>;
|
||||
onClickCancel?: React.MouseEventHandler<Button>;
|
||||
};
|
||||
|
||||
function UpdaterPage(props) {
|
||||
function UpdaterPage(props: UpdaterPageProps) {
|
||||
let footer;
|
||||
if (props.isDownloading) {
|
||||
footer = (
|
||||
|
@ -97,17 +109,4 @@ function UpdaterPage(props) {
|
|||
);
|
||||
}
|
||||
|
||||
UpdaterPage.propTypes = {
|
||||
appName: propTypes.string.isRequired,
|
||||
notifyOnly: propTypes.bool.isRequired,
|
||||
isDownloading: propTypes.bool.isRequired,
|
||||
progress: propTypes.number,
|
||||
onClickInstall: propTypes.func.isRequired,
|
||||
onClickDownload: propTypes.func.isRequired,
|
||||
onClickReleaseNotes: propTypes.func.isRequired,
|
||||
onClickRemind: propTypes.func.isRequired,
|
||||
onClickSkip: propTypes.func.isRequired,
|
||||
onClickCancel: propTypes.func.isRequired,
|
||||
};
|
||||
|
||||
export default UpdaterPage;
|
|
@ -1,13 +1,13 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import {storiesOf} from '@storybook/react';
|
||||
|
||||
import {action} from '@storybook/addon-actions';
|
||||
|
||||
import UpdaterPage from '../UpdaterPage.jsx';
|
||||
import UpdaterPage from '../UpdaterPage';
|
||||
import '../../css/components/UpdaterPage.css';
|
||||
|
||||
/*
|
|
@ -2,16 +2,20 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Row, Col} from 'react-bootstrap';
|
||||
import {Certificate} from 'electron/renderer';
|
||||
|
||||
export default class ShowCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
certificate: PropTypes.object,
|
||||
onOk: PropTypes.func.isRequired,
|
||||
};
|
||||
type Props = {
|
||||
certificate: Certificate;
|
||||
onOk: () => void;
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
type State = {
|
||||
certificate?: Certificate;
|
||||
}
|
||||
|
||||
export default class ShowCertificateModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
certificate: props.certificate,
|
||||
|
@ -19,12 +23,12 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
handleOk = () => {
|
||||
this.setState({certificate: null});
|
||||
this.setState({certificate: undefined});
|
||||
this.props.onOk();
|
||||
}
|
||||
|
||||
render() {
|
||||
const certificateSection = (descriptor) => {
|
||||
const certificateSection = (descriptor: React.ReactNode) => {
|
||||
return (
|
||||
<Fragment>
|
||||
<dt className={'certificate-key'}>{descriptor}</dt>
|
||||
|
@ -32,7 +36,7 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
</Fragment>
|
||||
);
|
||||
};
|
||||
const certificateItem = (descriptor, value) => {
|
||||
const certificateItem = (descriptor: React.ReactNode, value: React.ReactNode) => {
|
||||
const val = value ? `${value}` : <span/>;
|
||||
return (
|
||||
<Fragment>
|
||||
|
@ -47,6 +51,7 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Body>
|
||||
{'No certificate Selected'}
|
||||
|
@ -55,22 +60,22 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
|
||||
const utcSeconds = (date) => {
|
||||
const utcSeconds = (date: number) => {
|
||||
const d = new Date(0);
|
||||
d.setUTCSeconds(date);
|
||||
return d;
|
||||
};
|
||||
|
||||
const expiration = utcSeconds(this.state.certificate.validExpiry);
|
||||
const creation = utcSeconds(this.state.certificate.validStart);
|
||||
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
|
||||
const expiration = utcSeconds(this.state.certificate?.validExpiry || 0);
|
||||
const creation = utcSeconds(this.state.certificate?.validStart || 0);
|
||||
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
|
||||
const dateLocale = 'en-US';
|
||||
return (
|
||||
<Modal
|
||||
bsClass='modal'
|
||||
className='show-certificate'
|
||||
show={this.state.certificate !== null}
|
||||
scrollable={'true'}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header className={'no-border'}>
|
||||
<Modal.Title>{'Certificate information'}</Modal.Title>
|
||||
|
@ -79,20 +84,20 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
<p className='details'>{'Details'}</p>
|
||||
<dl>
|
||||
{certificateSection('Subject Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
|
||||
{certificateItem('Common Name', this.state.certificate?.subject.commonName)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Issuer Name')}
|
||||
{certificateItem('Common Name', this.state.certificate.issuer.commonName)}
|
||||
{certificateItem('Common Name', this.state.certificate?.issuer.commonName)}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateItem('Serial Number', this.state.certificate.serialNumber)}
|
||||
{certificateItem('Serial Number', this.state.certificate?.serialNumber)}
|
||||
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
|
||||
</dl>
|
||||
<dl>
|
||||
{certificateSection('Public Key Info')}
|
||||
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
|
||||
{certificateItem('Algorithm', this.state.certificate?.fingerprint.split('/')[0])}
|
||||
</dl>
|
||||
</Modal.Body>
|
||||
<Modal.Footer className={'no-border'}>
|
||||
|
@ -100,7 +105,6 @@ export default class ShowCertificateModal extends React.PureComponent {
|
|||
<Row>
|
||||
<Col>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
className={'primary'}
|
||||
>{'Close'}</Button>
|
|
@ -2,9 +2,8 @@
|
|||
// See LICENSE.txt for license information.
|
||||
|
||||
import React from 'react';
|
||||
import propTypes from 'prop-types';
|
||||
|
||||
export default function UrlDescription(props) {
|
||||
export default function UrlDescription(props: {url: string}) {
|
||||
if (props.url) {
|
||||
return (
|
||||
<div className='HoveringURL HoveringURL-left'>
|
||||
|
@ -12,8 +11,6 @@ export default function UrlDescription(props) {
|
|||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
UrlDescription.propTypes = {
|
||||
url: propTypes.string,
|
||||
};
|
||||
return null;
|
||||
}
|
|
@ -12,18 +12,18 @@ import React from 'react';
|
|||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useAnimationEnd(
|
||||
ref,
|
||||
callback,
|
||||
animationName,
|
||||
function useAnimationEnd<T extends Element>(
|
||||
ref: React.RefObject<T>,
|
||||
callback: (event: Event) => void,
|
||||
animationName: string,
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
): void {
|
||||
React.useEffect(() => {
|
||||
if (!ref.current) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function handleAnimationend(event) {
|
||||
function handleAnimationend(event: Event & {animationName?: string}) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
||||
|
@ -36,7 +36,7 @@ function useAnimationEnd(
|
|||
ref.current.addEventListener('animationend', handleAnimationend);
|
||||
|
||||
return () => {
|
||||
ref.current.removeEventListener('animationend', handleAnimationend);
|
||||
ref.current?.removeEventListener('animationend', handleAnimationend);
|
||||
};
|
||||
}, [ref, callback, animationName, listenForEventBubbling]);
|
||||
}
|
|
@ -12,10 +12,10 @@ import React from 'react';
|
|||
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
|
||||
* ignores events bubbling up from descendent elements
|
||||
*/
|
||||
function useTransitionend(
|
||||
ref,
|
||||
callback,
|
||||
properties,
|
||||
function useTransitionend<T extends Element>(
|
||||
ref: React.RefObject<T>,
|
||||
callback: (event: Event) => void,
|
||||
properties: string[],
|
||||
listenForEventBubbling = true,
|
||||
) {
|
||||
React.useEffect(() => {
|
||||
|
@ -23,7 +23,7 @@ function useTransitionend(
|
|||
return undefined;
|
||||
}
|
||||
|
||||
function handleTransitionEnd(event) {
|
||||
function handleTransitionEnd(event: Event & {propertyName?: string}) {
|
||||
if (!listenForEventBubbling && event.target !== ref.current) {
|
||||
return;
|
||||
}
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
|
@ -8,11 +8,18 @@ import 'renderer/css/index.css';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {CombinedConfig, Team} from 'types/config';
|
||||
|
||||
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
|
||||
|
||||
import MainPage from './components/MainPage.jsx';
|
||||
class Root extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import MainPage from './components/MainPage';
|
||||
|
||||
type State = {
|
||||
config?: CombinedConfig;
|
||||
}
|
||||
|
||||
class Root extends React.PureComponent<Record<string, never>, State> {
|
||||
constructor(props: Record<string, never>) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
@ -39,7 +46,10 @@ class Root extends React.PureComponent {
|
|||
this.setState({config});
|
||||
}
|
||||
|
||||
moveTabs = async (originalOrder, newOrder) => {
|
||||
moveTabs = async (originalOrder: number, newOrder: number): Promise<number | undefined> => {
|
||||
if (!this.state.config) {
|
||||
throw new Error('No config');
|
||||
}
|
||||
const teams = this.state.config.teams.concat();
|
||||
const tabOrder = teams.map((team, index) => {
|
||||
return {
|
||||
|
@ -62,12 +72,9 @@ class Root extends React.PureComponent {
|
|||
return teamIndex;
|
||||
};
|
||||
|
||||
teamConfigChange = async (updatedTeams, callback) => {
|
||||
const updatedConfig = await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
|
||||
teamConfigChange = async (updatedTeams: Team[]) => {
|
||||
await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
|
||||
await this.reloadConfig();
|
||||
if (callback) {
|
||||
callback(updatedConfig);
|
||||
}
|
||||
};
|
||||
|
||||
reloadConfig = async () => {
|
||||
|
@ -75,7 +82,7 @@ class Root extends React.PureComponent {
|
|||
this.setState({config});
|
||||
};
|
||||
|
||||
requestConfig = async (exitOnError) => {
|
||||
requestConfig = async (exitOnError?: boolean) => {
|
||||
// todo: should we block?
|
||||
try {
|
||||
const configRequest = await window.ipcRenderer.invoke(GET_CONFIGURATION);
|
||||
|
@ -115,6 +122,8 @@ class Root extends React.PureComponent {
|
|||
}
|
||||
window.ipcRenderer.invoke('get-app-version').then(({name, version}) => {
|
||||
// eslint-disable-next-line no-undef
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
console.log(`Starting ${name} v${version} commit: ${__HASH_VERSION__}`);
|
||||
});
|
||||
|
|
@ -3,10 +3,11 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {Certificate} from 'electron/renderer';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import SelectCertificateModal from './certificateModal.jsx';
|
||||
import SelectCertificateModal from './certificateModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
@ -16,7 +17,7 @@ const handleCancel = () => {
|
|||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const handleSelect = (cert) => {
|
||||
const handleSelect = (cert: Certificate) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {cert}}, window.location.href);
|
||||
};
|
||||
|
|
@ -1,27 +1,34 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Certificate} from 'electron/renderer';
|
||||
import React, {Fragment} from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
|
||||
|
||||
import {CertificateModalData} from 'types/certificate';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
import ShowCertificateModal from '../../components/showCertificateModal.jsx';
|
||||
import ShowCertificateModal from '../../components/showCertificateModal';
|
||||
|
||||
export default class SelectCertificateModal extends React.PureComponent {
|
||||
static propTypes = {
|
||||
onSelect: PropTypes.func.isRequired,
|
||||
onCancel: PropTypes.func,
|
||||
getCertInfo: PropTypes.func,
|
||||
}
|
||||
type Props = {
|
||||
onSelect: (cert: Certificate) => void;
|
||||
onCancel?: () => void;
|
||||
getCertInfo: () => void;
|
||||
}
|
||||
|
||||
constructor(props) {
|
||||
type State = {
|
||||
selectedIndex?: number;
|
||||
showCertificate?: Certificate;
|
||||
url?: string;
|
||||
list?: Certificate[];
|
||||
}
|
||||
|
||||
export default class SelectCertificateModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
selectedIndex: null,
|
||||
showCertificate: null,
|
||||
};
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
|
@ -34,7 +41,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
window.removeEventListener('message', this.handleCertInfoMessage);
|
||||
}
|
||||
|
||||
handleCertInfoMessage = (event) => {
|
||||
handleCertInfoMessage = (event: {data: ModalMessage<CertificateModalData>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, list} = event.data.data;
|
||||
|
@ -46,13 +53,13 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
selectfn = (index) => {
|
||||
selectfn = (index: number) => {
|
||||
return (() => {
|
||||
this.setState({selectedIndex: index});
|
||||
});
|
||||
};
|
||||
|
||||
renderCert = (cert, index) => {
|
||||
renderCert = (cert: Certificate, index: number) => {
|
||||
const issuer = (cert.issuerName || (cert.issuer && cert.issuer.commonName) || '');
|
||||
const subject = (cert.subjectName || (cert.subject && cert.subject.commonName) || '');
|
||||
const serial = cert.serialNumber || '';
|
||||
|
@ -75,7 +82,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
</tr>);
|
||||
};
|
||||
|
||||
renderCerts = (certificateList) => {
|
||||
renderCerts = (certificateList: Certificate[]) => {
|
||||
if (certificateList) {
|
||||
const certs = certificateList.map(this.renderCert);
|
||||
return (
|
||||
|
@ -88,12 +95,15 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
getSelectedCert = () => {
|
||||
return this.state.selectedIndex === null ? null : this.state.list[this.state.selectedIndex];
|
||||
if (this.state.list && this.state.selectedIndex) {
|
||||
return this.state.list[this.state.selectedIndex];
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
handleOk = () => {
|
||||
const cert = this.getSelectedCert();
|
||||
if (cert !== null) {
|
||||
if (cert) {
|
||||
this.props.onSelect(cert);
|
||||
}
|
||||
}
|
||||
|
@ -104,7 +114,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
certificateInfoClose = () => {
|
||||
this.setState({showCertificate: null});
|
||||
this.setState({showCertificate: undefined});
|
||||
}
|
||||
|
||||
render() {
|
||||
|
@ -122,6 +132,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
bsClass='modal'
|
||||
className='certificate-modal'
|
||||
show={Boolean(this.state.list && this.state.url)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title >{'Select a certificate'}</Modal.Title>
|
||||
|
@ -131,7 +142,6 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
<Table
|
||||
striped={true}
|
||||
hover={true}
|
||||
size={'sm'}
|
||||
responsive={true}
|
||||
className='certificate-list'
|
||||
tabIndex={1}
|
||||
|
@ -144,7 +154,7 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{this.renderCerts(this.state.list)}
|
||||
{this.renderCerts(this.state.list!)}
|
||||
<tr/* this is to correct table height without affecting real rows *//>
|
||||
</tbody>
|
||||
</Table>
|
||||
|
@ -154,7 +164,6 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
<Row>
|
||||
<Col sm={4}>
|
||||
<Button
|
||||
variant={'info'}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
onClick={this.handleCertificateInfo}
|
||||
className={'info'}
|
||||
|
@ -163,11 +172,9 @@ export default class SelectCertificateModal extends React.PureComponent {
|
|||
<Col sm={8}>
|
||||
<Button
|
||||
onClick={this.props.onCancel}
|
||||
variant={'secondary'}
|
||||
className={'secondary'}
|
||||
>{'Cancel'}</Button>
|
||||
<Button
|
||||
variant={'primary'}
|
||||
onClick={this.handleOk}
|
||||
disabled={this.state.selectedIndex === null}
|
||||
className={'primary'}
|
|
@ -4,18 +4,27 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication.js';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import LoadingScreen from '../../components/LoadingScreen.jsx';
|
||||
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
|
||||
|
||||
import LoadingScreen from '../../components/LoadingScreen';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
import 'renderer/css/components/LoadingAnimation.css';
|
||||
import 'renderer/css/components/LoadingScreen.css';
|
||||
|
||||
class LoadingScreenRoot extends React.PureComponent {
|
||||
constructor() {
|
||||
super();
|
||||
type Props = Record<string, never>;
|
||||
|
||||
type State = {
|
||||
showLoadingScreen: boolean;
|
||||
darkMode: boolean;
|
||||
}
|
||||
|
||||
class LoadingScreenRoot extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showLoadingScreen: true,
|
||||
darkMode: false,
|
||||
|
@ -32,7 +41,7 @@ class LoadingScreenRoot extends React.PureComponent {
|
|||
window.removeEventListener('message', this.handleMessageEvent);
|
||||
}
|
||||
|
||||
handleMessageEvent = (event) => {
|
||||
handleMessageEvent = (event: {data: ModalMessage<any>}) => {
|
||||
if (event.data.type === RECEIVED_LOADING_SCREEN_DATA) {
|
||||
this.setState({
|
||||
darkMode: event.data.data.darkMode,
|
|
@ -3,19 +3,20 @@
|
|||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import {AuthenticationResponseDetails} from 'electron/renderer';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
|
||||
|
||||
import LoginModal from './loginModal.jsx';
|
||||
import LoginModal from './loginModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const handleLoginCancel = (request) => {
|
||||
const handleLoginCancel = (request: AuthenticationResponseDetails) => {
|
||||
window.postMessage({type: MODAL_CANCEL, data: {request}}, window.location.href);
|
||||
};
|
||||
|
||||
const handleLogin = (request, username, password) => {
|
||||
const handleLogin = (request: AuthenticationResponseDetails, username: string, password: string) => {
|
||||
window.postMessage({type: MODAL_RESULT, data: {request, username, password}}, window.location.href);
|
||||
};
|
||||
|
|
@ -3,20 +3,34 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
|
||||
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {LoginModalData} from 'types/auth';
|
||||
import {ModalMessage} from 'types/modals';
|
||||
import {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer';
|
||||
|
||||
export default class LoginModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
import urlUtils from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
|
||||
type Props = {
|
||||
onCancel: (request: AuthenticationResponseDetails) => void;
|
||||
onLogin: (request: AuthenticationResponseDetails, username: string, password: string) => void;
|
||||
getAuthInfo: () => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
username: string;
|
||||
password: string;
|
||||
request?: AuthenticationResponseDetails;
|
||||
authInfo?: AuthInfo;
|
||||
};
|
||||
|
||||
export default class LoginModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -30,7 +44,7 @@ export default class LoginModal extends React.PureComponent {
|
|||
window.removeEventListener('message', this.handleAuthInfoMessage);
|
||||
}
|
||||
|
||||
handleAuthInfoMessage = (event) => {
|
||||
handleAuthInfoMessage = (event: {data: ModalMessage<LoginModalData>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {request, authInfo} = event.data.data;
|
||||
|
@ -42,33 +56,33 @@ export default class LoginModal extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
handleSubmit = (event) => {
|
||||
handleSubmit = (event: React.MouseEvent<Button>) => {
|
||||
event.preventDefault();
|
||||
this.props.onLogin(this.state.request, this.state.username, this.state.password);
|
||||
this.props.onLogin(this.state.request!, this.state.username, this.state.password);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
request: undefined,
|
||||
authInfo: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
handleCancel = (event) => {
|
||||
handleCancel = (event: React.MouseEvent<Button>) => {
|
||||
event.preventDefault();
|
||||
this.props.onCancel(this.state.request);
|
||||
this.props.onCancel(this.state.request!);
|
||||
this.setState({
|
||||
username: '',
|
||||
password: '',
|
||||
request: null,
|
||||
authInfo: null,
|
||||
request: undefined,
|
||||
authInfo: undefined,
|
||||
});
|
||||
}
|
||||
|
||||
setUsername = (e) => {
|
||||
setUsername = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({username: e.target.value});
|
||||
}
|
||||
|
||||
setPassword = (e) => {
|
||||
setPassword = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
|
||||
this.setState({password: e.target.value});
|
||||
}
|
||||
|
||||
|
@ -80,11 +94,14 @@ export default class LoginModal extends React.PureComponent {
|
|||
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
|
||||
} else {
|
||||
const tmpURL = urlUtils.parseURL(this.state.request.url);
|
||||
theServer = `The server ${tmpURL.protocol}//${tmpURL.host}`;
|
||||
theServer = `The server ${tmpURL?.protocol}//${tmpURL?.host}`;
|
||||
}
|
||||
const message = `${theServer} requires a username and password.`;
|
||||
return (
|
||||
<Modal show={Boolean(this.state.request && this.state.authInfo)}>
|
||||
<Modal
|
||||
show={Boolean(this.state.request && this.state.authInfo)}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{'Authentication Required'}</Modal.Title>
|
||||
</Modal.Header>
|
||||
|
@ -148,9 +165,3 @@ export default class LoginModal extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
LoginModal.propTypes = {
|
||||
onCancel: PropTypes.func,
|
||||
onLogin: PropTypes.func,
|
||||
getAuthInfo: PropTypes.func,
|
||||
};
|
|
@ -4,21 +4,20 @@
|
|||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
||||
const queryString = window.location.search;
|
||||
const urlParams = new URLSearchParams(queryString);
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication.js';
|
||||
import {TeamWithIndex} from 'types/config';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal.jsx'; //'./addServer.jsx';
|
||||
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication';
|
||||
|
||||
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
|
||||
|
||||
const onClose = () => {
|
||||
window.postMessage({type: MODAL_CANCEL}, window.location.href);
|
||||
};
|
||||
|
||||
const onSave = (data) => {
|
||||
const onSave = (data: TeamWithIndex) => {
|
||||
window.postMessage({type: MODAL_RESULT, data}, window.location.href);
|
||||
};
|
||||
|
||||
|
@ -29,7 +28,6 @@ const start = async () => {
|
|||
onSave={onSave}
|
||||
editMode={false}
|
||||
show={true}
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
|
@ -4,9 +4,9 @@
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication.js';
|
||||
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication';
|
||||
|
||||
import PermissionModal from './permissionModal.jsx';
|
||||
import PermissionModal from './permissionModal';
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/modals.css';
|
||||
|
@ -23,7 +23,7 @@ const getPermissionInfo = () => {
|
|||
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
|
||||
};
|
||||
|
||||
const openExternalLink = (protocol, url) => {
|
||||
const openExternalLink = (protocol: string, url: string) => {
|
||||
window.postMessage({type: MODAL_SEND_IPC_MESSAGE, data: {type: 'confirm-protocol', args: [protocol, url]}}, window.location.href);
|
||||
};
|
||||
|
|
@ -3,14 +3,29 @@
|
|||
|
||||
import React from 'react';
|
||||
import {Modal, Button} from 'react-bootstrap';
|
||||
import PropTypes from 'prop-types';
|
||||
|
||||
import {PermissionType} from 'types/trustedOrigin';
|
||||
|
||||
import {ModalMessage} from 'types/modals';
|
||||
|
||||
import urlUtil from 'common/utils/url';
|
||||
import {MODAL_INFO} from 'common/communication';
|
||||
import {PERMISSION_DESCRIPTION} from 'common/permissions';
|
||||
|
||||
export default class PermissionModal extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
handleDeny: React.MouseEventHandler<Button>;
|
||||
handleGrant: React.MouseEventHandler<Button>;
|
||||
getPermissionInfo: () => void;
|
||||
openExternalLink: (protocol: string, url: string) => void;
|
||||
};
|
||||
|
||||
type State = {
|
||||
url?: string;
|
||||
permission?: PermissionType;
|
||||
}
|
||||
|
||||
export default class PermissionModal extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = {};
|
||||
}
|
||||
|
@ -25,7 +40,7 @@ export default class PermissionModal extends React.PureComponent {
|
|||
window.removeEventListener('message', this.handlePermissionInfoMessage);
|
||||
}
|
||||
|
||||
handlePermissionInfoMessage = (event) => {
|
||||
handlePermissionInfoMessage = (event: {data: ModalMessage<{url: string; permission: PermissionType}>}) => {
|
||||
switch (event.data.type) {
|
||||
case MODAL_INFO: {
|
||||
const {url, permission} = event.data.data;
|
||||
|
@ -38,7 +53,7 @@ export default class PermissionModal extends React.PureComponent {
|
|||
}
|
||||
|
||||
getModalTitle() {
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission]} Required`;
|
||||
return `${PERMISSION_DESCRIPTION[this.state.permission!]} Required`;
|
||||
}
|
||||
|
||||
getModalBody() {
|
||||
|
@ -46,12 +61,12 @@ export default class PermissionModal extends React.PureComponent {
|
|||
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
|
||||
const originLink = url ? originDisplay : '';
|
||||
|
||||
const click = (e) => {
|
||||
const click = (e: React.MouseEvent<HTMLAnchorElement>) => {
|
||||
e.preventDefault();
|
||||
let parseUrl;
|
||||
try {
|
||||
parseUrl = urlUtil.parseURL(originLink);
|
||||
this.props.openExternalLink(parseUrl.protocol, originLink);
|
||||
this.props.openExternalLink(parseUrl!.protocol, originLink);
|
||||
} catch (err) {
|
||||
console.error(`invalid url ${originLink} supplied to externallink: ${err}`);
|
||||
}
|
||||
|
@ -60,7 +75,7 @@ export default class PermissionModal extends React.PureComponent {
|
|||
return (
|
||||
<div>
|
||||
<p>
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
|
||||
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission!]}.`}
|
||||
</p>
|
||||
<p>
|
||||
<span>{'This request originated from '}</span>
|
||||
|
@ -78,6 +93,7 @@ export default class PermissionModal extends React.PureComponent {
|
|||
show={Boolean(this.state.url && this.state.permission)}
|
||||
id='requestPermissionModal'
|
||||
enforceFocus={true}
|
||||
onHide={() => {}}
|
||||
>
|
||||
<Modal.Header>
|
||||
<Modal.Title>{this.getModalTitle()}</Modal.Title>
|
||||
|
@ -100,10 +116,3 @@ export default class PermissionModal extends React.PureComponent {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
PermissionModal.propTypes = {
|
||||
handleDeny: PropTypes.func,
|
||||
handleGrant: PropTypes.func,
|
||||
getPermissionInfo: PropTypes.func,
|
||||
openExternalLink: PropTypes.func,
|
||||
};
|
|
@ -9,12 +9,12 @@ const urlParams = new URLSearchParams(queryString);
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import UrlDescription from '../../components/urlDescription.jsx';
|
||||
import UrlDescription from '../../components/urlDescription';
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<UrlDescription
|
||||
url={decodeURIComponent(urlParams.get('url'))}
|
||||
url={decodeURIComponent(urlParams.get('url')!)}
|
||||
/>,
|
||||
document.getElementById('app'),
|
||||
);
|
|
@ -22,7 +22,7 @@ const notificationSounds = new Map([
|
|||
['Upstairs', upstairs],
|
||||
]);
|
||||
|
||||
export const playSound = throttle((soundName) => {
|
||||
export const playSound = throttle((soundName: string) => {
|
||||
if (soundName) {
|
||||
const audio = new Audio(notificationSounds.get(soundName));
|
||||
audio.play();
|
|
@ -1,6 +1,6 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import 'bootstrap/dist/css/bootstrap.min.css';
|
||||
import 'renderer/css/index.css';
|
||||
|
@ -9,19 +9,11 @@ import 'renderer/css/settings.css';
|
|||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
|
||||
import SettingsPage from './components/SettingsPage.jsx';
|
||||
|
||||
function openMenu() {
|
||||
if (window.process.platform !== 'darwin') {
|
||||
window.ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
import SettingsPage from './components/SettingsPage';
|
||||
|
||||
const start = async () => {
|
||||
ReactDOM.render(
|
||||
<SettingsPage
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
<SettingsPage/>,
|
||||
document.getElementById('app'),
|
||||
);
|
||||
};
|
|
@ -1,21 +1,29 @@
|
|||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
|
||||
import url from 'url';
|
||||
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom';
|
||||
import propTypes from 'prop-types';
|
||||
import {remote} from 'electron';
|
||||
|
||||
import UpdaterPage from './components/UpdaterPage.jsx';
|
||||
import UpdaterPage from './components/UpdaterPage';
|
||||
|
||||
const thisURL = url.parse(location.href, true);
|
||||
const notifyOnly = thisURL.query.notifyOnly === 'true';
|
||||
|
||||
class UpdaterPageContainer extends React.PureComponent {
|
||||
constructor(props) {
|
||||
type Props = {
|
||||
notifyOnly: boolean;
|
||||
initialState: State;
|
||||
};
|
||||
|
||||
type State = {
|
||||
|
||||
}
|
||||
|
||||
class UpdaterPageContainer extends React.PureComponent<Props, State> {
|
||||
constructor(props: Props) {
|
||||
super(props);
|
||||
this.state = props.initialState;
|
||||
}
|
||||
|
@ -143,11 +151,6 @@ class UpdaterPageContainer extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
UpdaterPageContainer.propTypes = {
|
||||
notifyOnly: propTypes.bool,
|
||||
initialState: propTypes.object,
|
||||
};
|
||||
|
||||
ReactDOM.render(
|
||||
<UpdaterPageContainer
|
||||
notifyOnly={notifyOnly}
|
8
src/types/appState.ts
Normal file
8
src/types/appState.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type AppState = {
|
||||
lastAppVersion?: string;
|
||||
skippedVersion?: string;
|
||||
updateCheckedDate?: string;
|
||||
};
|
4
src/types/args.ts
Normal file
4
src/types/args.ts
Normal file
|
@ -0,0 +1,4 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type Args = typeof global.args;
|
9
src/types/auth.ts
Normal file
9
src/types/auth.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {AuthenticationResponseDetails, AuthInfo} from 'electron/common';
|
||||
|
||||
export type LoginModalData = {
|
||||
request: AuthenticationResponseDetails;
|
||||
authInfo: AuthInfo;
|
||||
}
|
14
src/types/certificate.ts
Normal file
14
src/types/certificate.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Certificate} from 'electron/common';
|
||||
|
||||
export type ComparableCertificate = {
|
||||
data: string;
|
||||
issuerName: string;
|
||||
}
|
||||
|
||||
export type CertificateModalData = {
|
||||
url: string;
|
||||
list: Certificate[];
|
||||
}
|
81
src/types/config.ts
Normal file
81
src/types/config.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type Team = {
|
||||
name: string;
|
||||
url: string;
|
||||
order: number;
|
||||
}
|
||||
|
||||
export type TeamWithIndex = Team & {index: number};
|
||||
|
||||
export type Config = ConfigV2;
|
||||
|
||||
export type ConfigV2 = {
|
||||
version: 2;
|
||||
teams: Team[];
|
||||
showTrayIcon: boolean;
|
||||
trayIconTheme: string;
|
||||
minimizeToTray: boolean;
|
||||
notifications: {
|
||||
flashWindow: number;
|
||||
bounceIcon: boolean;
|
||||
bounceIconType: 'critical' | 'informational';
|
||||
};
|
||||
showUnreadBadge: boolean;
|
||||
useSpellChecker: boolean;
|
||||
enableHardwareAcceleration: boolean;
|
||||
autostart: boolean;
|
||||
spellCheckerLocale: string;
|
||||
darkMode: boolean;
|
||||
downloadLocation: string;
|
||||
}
|
||||
|
||||
export type ConfigV1 = {
|
||||
version: 1;
|
||||
teams: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
}>;
|
||||
showTrayIcon: boolean;
|
||||
trayIconTheme: string;
|
||||
minimizeToTray: boolean;
|
||||
notifications: {
|
||||
flashWindow: number;
|
||||
bounceIcon: boolean;
|
||||
bounceIconType: 'critical' | 'informational';
|
||||
};
|
||||
showUnreadBadge: boolean;
|
||||
useSpellChecker: boolean;
|
||||
enableHardwareAcceleration: boolean;
|
||||
autostart: boolean;
|
||||
spellCheckerLocale: string;
|
||||
}
|
||||
|
||||
export type ConfigV0 = {version: 0; url: string};
|
||||
|
||||
export type AnyConfig = ConfigV2 | ConfigV1 | ConfigV0;
|
||||
|
||||
export type BuildConfig = {
|
||||
defaultTeams?: Team[];
|
||||
helpLink: string;
|
||||
enableServerManagement: boolean;
|
||||
enableAutoUpdater: boolean;
|
||||
managedResources: string[];
|
||||
}
|
||||
|
||||
export type RegistryConfig = {
|
||||
teams: Team[];
|
||||
enableServerManagement: boolean;
|
||||
enableAutoUpdater: boolean;
|
||||
}
|
||||
|
||||
export type CombinedConfig = ConfigV2 & BuildConfig & {
|
||||
registryTeams: Team[];
|
||||
appName: string;
|
||||
}
|
||||
|
||||
export type LocalConfiguration = Config & {
|
||||
appName: string;
|
||||
enableServerManagement: boolean;
|
||||
}
|
5
src/types/external/file-types.d.ts
vendored
Normal file
5
src/types/external/file-types.d.ts
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
declare module '*.mp3';
|
||||
declare module '*.svg';
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue