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