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:
Devin Binnie
2021-06-28 09:51:23 -04:00
committed by GitHub
parent 422673a740
commit 1b3d0eac8f
115 changed files with 16246 additions and 9921 deletions

View File

@@ -2,12 +2,14 @@
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import {AppState} from 'types/appState';
import JsonFileManager from '../common/JsonFileManager';
import * as Validator from './Validator';
export default class AppVersionManager extends JsonFileManager {
constructor(file) {
export default class AppVersionManager extends JsonFileManager<AppState> {
constructor(file: string) {
super(file);
// ensure data loaded from file is valid
@@ -33,7 +35,7 @@ export default class AppVersionManager extends JsonFileManager {
}
set updateCheckedDate(date) {
this.setValue('updateCheckedDate', date.toISOString());
this.setValue('updateCheckedDate', date?.toISOString());
}
get updateCheckedDate() {

View File

@@ -8,6 +8,8 @@ import isDev from 'electron-is-dev';
import log from 'electron-log';
export default class AutoLauncher {
appLauncher: AutoLaunch;
constructor() {
this.appLauncher = new AutoLaunch({
name: app.name,

View File

@@ -7,7 +7,7 @@ import fs from 'fs';
import os from 'os';
import path from 'path';
import {app, dialog} from 'electron';
import {app, BrowserWindow, dialog} from 'electron';
import log from 'electron-log';
@@ -15,15 +15,17 @@ const BUTTON_OK = 'OK';
const BUTTON_SHOW_DETAILS = 'Show Details';
const BUTTON_REOPEN = 'Reopen';
function createErrorReport(err) {
function createErrorReport(err: Error) {
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
return `Application: ${app.name} ${app.getVersion()} [commit: ${__HASH_VERSION__}]\n` +
`Platform: ${os.type()} ${os.release()} ${os.arch()}\n` +
`${err.stack}`;
}
function openDetachedExternal(url) {
const spawnOption = {detached: true, stdio: 'ignore'};
function openDetachedExternal(url: string) {
const spawnOption = {detached: true, stdio: 'ignore' as any};
switch (process.platform) {
case 'win32':
return spawn('cmd', ['/C', 'start', url], spawnOption);
@@ -32,20 +34,21 @@ function openDetachedExternal(url) {
case 'linux':
return spawn('xdg-open', [url], spawnOption);
default:
return null;
return undefined;
}
}
export default class CriticalErrorHandler {
constructor() {
this.mainWindow = null;
}
mainWindow?: BrowserWindow;
setMainWindow(mainWindow) {
setMainWindow(mainWindow: BrowserWindow) {
this.mainWindow = mainWindow;
}
windowUnresponsiveHandler() {
if (!this.mainWindow) {
return;
}
dialog.showMessageBox(this.mainWindow, {
type: 'warning',
title: app.name,
@@ -59,7 +62,7 @@ export default class CriticalErrorHandler {
});
}
processUncaughtExceptionHandler(err) {
processUncaughtExceptionHandler(err: Error) {
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
const report = createErrorReport(err);
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
@@ -69,9 +72,11 @@ export default class CriticalErrorHandler {
if (process.platform === 'darwin') {
buttons.reverse();
}
const bindWindow = this.mainWindow && this.mainWindow.isVisible() ? this.mainWindow : null;
if (!this.mainWindow?.isVisible) {
return;
}
dialog.showMessageBox(
bindWindow,
this.mainWindow,
{
type: 'error',
title: app.name,
@@ -102,7 +107,7 @@ export default class CriticalErrorHandler {
app.exit(-1);
});
} else {
log.err(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
log.error(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
throw err;
}
}

View File

@@ -4,9 +4,11 @@
import urlUtils from 'common/utils/url';
export class MattermostServer {
constructor(name, serverUrl) {
name: string;
url: URL;
constructor(name: string, serverUrl: string) {
this.name = name;
this.url = urlUtils.parseURL(serverUrl);
this.url = urlUtils.parseURL(serverUrl)!;
if (!this.url) {
throw new Error('Invalid url for creating a server');
}
@@ -19,12 +21,12 @@ export class MattermostServer {
return {origin: this.url.origin, subpath, url: this.url.toString()};
}
sameOrigin = (otherURL) => {
sameOrigin = (otherURL: string) => {
const parsedUrl = urlUtils.parseURL(otherURL);
return parsedUrl && this.url.origin === parsedUrl.origin;
}
equals = (otherServer) => {
equals = (otherServer: MattermostServer) => {
return (this.name === otherServer.name) && (this.url.toString() === otherServer.url.toString());
}
}

View File

@@ -1,17 +1,18 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Args} from 'types/args';
import yargs from 'yargs';
import {protocols} from '../../electron-builder.json';
import * as Validator from './Validator';
export default function parse(args) {
export default function parse(args: string[]) {
return validateArgs(parseArgs(triageArgs(args)));
}
function triageArgs(args) {
function triageArgs(args: string[]) {
// ensure any args following a possible deeplink are discarded
if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
const scheme = protocols[0].schemes[0].toLowerCase();
@@ -23,7 +24,7 @@ function triageArgs(args) {
return args;
}
function parseArgs(args) {
function parseArgs(args: string[]) {
return yargs.
alias('dataDir', 'd').string('dataDir').describe('dataDir', 'Set the path to where user data is stored.').
alias('disableDevMode', 'p').boolean('disableDevMode').describe('disableDevMode', 'Disable development mode. Allows for testing as if it was Production.').
@@ -32,6 +33,6 @@ function parseArgs(args) {
parse(args);
}
function validateArgs(args) {
function validateArgs(args: Args) {
return Validator.validateArgs(args) || {};
}

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import EventEmitter from 'events';
import {EventEmitter} from 'events';
import electron from 'electron';
import log from 'electron-log';
@@ -12,12 +12,21 @@ const {app} = electron;
* Monitors system idle time, listens for system events and fires status updates as needed
*/
export default class UserActivityMonitor extends EventEmitter {
isActive: boolean;
idleTime: number;
lastSetActive?: number;
systemIdleTimeIntervalID: number;
config: {
updateFrequencyMs: number;
inactiveThresholdMs: number;
statusUpdateThresholdMs: number;
};
constructor() {
super();
this.isActive = true;
this.idleTime = 0;
this.lastSetActive = null;
this.systemIdleTimeIntervalID = -1;
this.config = {
@@ -58,13 +67,14 @@ export default class UserActivityMonitor extends EventEmitter {
this.config = Object.assign({}, this.config, config);
// TODO: Node typings don't map Timeout to number, but then clearInterval requires a number?
this.systemIdleTimeIntervalID = setInterval(() => {
try {
this.updateIdleTime(electron.powerMonitor.getSystemIdleTime());
} catch (err) {
log.error('Error getting system idle time:', err);
}
}, this.config.updateFrequencyMs);
}, this.config.updateFrequencyMs) as unknown as number;
}
/**
@@ -80,7 +90,7 @@ export default class UserActivityMonitor extends EventEmitter {
* @param {integer} idleTime
* @private
*/
updateIdleTime(idleTime) {
updateIdleTime(idleTime: number) {
this.idleTime = idleTime;
if (idleTime * 1000 > this.config.inactiveThresholdMs) { // eslint-disable-line no-magic-numbers
this.setActivityState(false);
@@ -110,7 +120,7 @@ export default class UserActivityMonitor extends EventEmitter {
this.sendStatusUpdate(false);
this.lastSetActive = now;
} else if (!isActive) {
this.lastSetActive = null;
delete this.lastSetActive;
}
}

View File

@@ -4,6 +4,13 @@ import log from 'electron-log';
import Joi from '@hapi/joi';
import {Args} from 'types/args';
import {ConfigV0, ConfigV1, ConfigV2} from 'types/config';
import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState';
import {ComparableCertificate} from 'types/certificate';
import {PermissionType, TrustedOrigin} from 'types/trustedOrigin';
import urlUtils from 'common/utils/url';
const defaultOptions = {
@@ -14,14 +21,14 @@ const defaultWindowHeight = 700;
const minWindowWidth = 400;
const minWindowHeight = 240;
const argsSchema = Joi.object({
const argsSchema = Joi.object<Args>({
hidden: Joi.boolean(),
disableDevMode: Joi.boolean(),
dataDir: Joi.string(),
version: Joi.boolean(),
});
const boundsInfoSchema = Joi.object({
const boundsInfoSchema = Joi.object<SavedWindowState>({
x: Joi.number().integer().default(0),
y: Joi.number().integer().default(0),
width: Joi.number().integer().min(minWindowWidth).required().default(defaultWindowWidth),
@@ -30,17 +37,17 @@ const boundsInfoSchema = Joi.object({
fullscreen: Joi.boolean().default(false),
});
const appStateSchema = Joi.object({
const appStateSchema = Joi.object<AppState>({
lastAppVersion: Joi.string(),
skippedVersion: Joi.string(),
updateCheckedDate: Joi.string(),
});
const configDataSchemaV0 = Joi.object({
const configDataSchemaV0 = Joi.object<ConfigV0>({
url: Joi.string().required(),
});
const configDataSchemaV1 = Joi.object({
const configDataSchemaV1 = Joi.object<ConfigV1>({
version: Joi.number().min(1).default(1),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
@@ -61,7 +68,7 @@ const configDataSchemaV1 = Joi.object({
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
});
const configDataSchemaV2 = Joi.object({
const configDataSchemaV2 = Joi.object<ConfigV2>({
version: Joi.number().min(2).default(2),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
@@ -88,13 +95,13 @@ const configDataSchemaV2 = Joi.object({
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
const certificateStoreSchema = Joi.object().pattern(
Joi.string().uri(),
Joi.object({
Joi.object<ComparableCertificate>({
data: Joi.string(),
issuerName: Joi.string(),
}),
);
const originPermissionsSchema = Joi.object().keys({
const originPermissionsSchema = Joi.object<TrustedOrigin>().keys({
canBasicAuth: Joi.boolean().default(false), // we can add more permissions later if we want
});
@@ -108,27 +115,27 @@ const trustedOriginsSchema = Joi.object({}).pattern(
const allowedProtocolsSchema = Joi.array().items(Joi.string().regex(/^[a-z-]+:$/i));
// validate bounds_info.json
export function validateArgs(data) {
export function validateArgs(data: Args) {
return validateAgainstSchema(data, argsSchema);
}
// validate bounds_info.json
export function validateBoundsInfo(data) {
export function validateBoundsInfo(data: SavedWindowState) {
return validateAgainstSchema(data, boundsInfoSchema);
}
// validate app_state.json
export function validateAppState(data) {
export function validateAppState(data: AppState) {
return validateAgainstSchema(data, appStateSchema);
}
// validate v.0 config.json
export function validateV0ConfigData(data) {
export function validateV0ConfigData(data: ConfigV0) {
return validateAgainstSchema(data, configDataSchemaV0);
}
// validate v.1 config.json
export function validateV1ConfigData(data) {
export function validateV1ConfigData(data: ConfigV1) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url}) => {
@@ -148,7 +155,7 @@ export function validateV1ConfigData(data) {
return validateAgainstSchema(data, configDataSchemaV1);
}
export function validateV2ConfigData(data) {
export function validateV2ConfigData(data: ConfigV2) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url, order}) => {
@@ -169,39 +176,39 @@ export function validateV2ConfigData(data) {
}
// validate certificate.json
export function validateCertificateStore(data) {
export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, certificateStoreSchema);
}
// validate allowedProtocols.json
export function validateAllowedProtocols(data) {
export function validateAllowedProtocols(data: string[]) {
return validateAgainstSchema(data, allowedProtocolsSchema);
}
export function validateTrustedOriginsStore(data) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
export function validateTrustedOriginsStore(data: string | Record<PermissionType, TrustedOrigin>) {
const jsonData: Record<PermissionType, TrustedOrigin> = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, trustedOriginsSchema);
}
export function validateOriginPermissions(data) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
export function validateOriginPermissions(data: string | TrustedOrigin) {
const jsonData: TrustedOrigin = (typeof data === 'object' ? data : JSON.parse(data));
return validateAgainstSchema(jsonData, originPermissionsSchema);
}
function validateAgainstSchema(data, schema) {
function validateAgainstSchema<T>(data: T, schema: Joi.ObjectSchema<T> | Joi.ArraySchema): T | null {
if (typeof data !== 'object') {
log.error(`Input 'data' is not an object we can validate: ${typeof data}`);
return false;
return null;
}
if (!schema) {
log.error('No schema provided to validate');
return false;
return null;
}
const {error, value} = schema.validate(data, defaultOptions);
if (error) {
log.error(`Validation failed due to: ${error}`);
return false;
return null;
}
return value;
}

View File

@@ -16,9 +16,9 @@ import * as Validator from './Validator';
import {getMainWindow} from './windows/windowManager';
const allowedProtocolFile = path.resolve(app.getPath('userData'), 'allowedProtocols.json');
let allowedProtocols = [];
let allowedProtocols: string[] = [];
function addScheme(scheme) {
function addScheme(scheme: string) {
const proto = `${scheme}:`;
if (!allowedProtocols.includes(proto)) {
allowedProtocols.push(proto);
@@ -41,12 +41,16 @@ function init() {
});
}
function handleDialogEvent(protocol, URL) {
function handleDialogEvent(protocol: string, URL: string) {
if (allowedProtocols.indexOf(protocol) !== -1) {
shell.openExternal(URL);
return;
}
dialog.showMessageBox(getMainWindow(), {
const mainWindow = getMainWindow();
if (!mainWindow) {
return;
}
dialog.showMessageBox(mainWindow, {
title: 'Non http(s) protocol',
message: `${protocol} link requires an external application.`,
detail: `The requested link is ${URL} . Do you want to continue?`,
@@ -63,7 +67,7 @@ function handleDialogEvent(protocol, URL) {
switch (response) {
case 1: {
allowedProtocols.push(protocol);
function handleError(err) {
function handleError(err: NodeJS.ErrnoException | null) {
if (err) {
log.error(err);
}

View File

@@ -9,13 +9,13 @@ import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED} from 'commo
import * as WindowManager from './windows/windowManager';
const status = {
unreads: new Map(),
mentions: new Map(),
expired: new Map(),
unreads: new Map<string, boolean>(),
mentions: new Map<string, number>(),
expired: new Map<string, boolean>(),
emitter: new events.EventEmitter(),
};
const emitMentions = (serverName) => {
const emitMentions = (serverName: string) => {
const newMentions = getMentions(serverName);
const newUnreads = getUnreads(serverName);
const isExpired = getIsExpired(serverName);
@@ -24,11 +24,11 @@ const emitMentions = (serverName) => {
emitStatus();
};
const emitTray = (expired, mentions, unreads) => {
const emitTray = (expired?: boolean, mentions?: number, unreads?: boolean) => {
status.emitter.emit(UPDATE_TRAY, expired, Boolean(mentions), unreads);
};
const emitBadge = (expired, mentions, unreads) => {
const emitBadge = (expired?: boolean, mentions?: number, unreads?: boolean) => {
status.emitter.emit(UPDATE_BADGE, expired, mentions, unreads);
};
@@ -40,7 +40,7 @@ export const emitStatus = () => {
emitBadge(expired, mentions, unreads);
};
export const updateMentions = (serverName, mentions, unreads) => {
export const updateMentions = (serverName: string, mentions: number, unreads?: boolean) => {
if (typeof unreads !== 'undefined') {
status.unreads.set(serverName, Boolean(unreads));
}
@@ -48,20 +48,20 @@ export const updateMentions = (serverName, mentions, unreads) => {
emitMentions(serverName);
};
export const updateUnreads = (serverName, unreads) => {
export const updateUnreads = (serverName: string, unreads: boolean) => {
status.unreads.set(serverName, Boolean(unreads));
emitMentions(serverName);
};
export const getUnreads = (serverName) => {
export const getUnreads = (serverName: string) => {
return status.unreads.get(serverName) || false;
};
export const getMentions = (serverName) => {
export const getMentions = (serverName: string) => {
return status.mentions.get(serverName) || 0; // this might be undefined as a way to tell that we don't know as it might need to login still.
};
export const getIsExpired = (serverName) => {
export const getIsExpired = (serverName: string) => {
return status.expired.get(serverName) || false;
};
@@ -101,11 +101,11 @@ export const anyExpired = () => {
};
// add any other event emitter methods if needed
export const on = (event, listener) => {
export const on = (event: string, listener: (...args: any[]) => void) => {
status.emitter.on(event, listener);
};
export const setSessionExpired = (serverName, expired) => {
export const setSessionExpired = (serverName: string, expired: boolean) => {
const isExpired = Boolean(expired);
const old = status.expired.get(serverName);
status.expired.set(serverName, isExpired);

View File

@@ -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
View 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();
}
}

View File

@@ -2,9 +2,13 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// TODO: This needs to be rebuilt anyways, skipping TS migration for now
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
//@ts-nocheck
import path from 'path';
import {app, BrowserWindow, dialog, ipcMain, shell} from 'electron';
import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, ipcMain, IpcMainEvent, shell} from 'electron';
import log from 'electron-log';
import {autoUpdater, CancellationToken} from 'electron-updater';
@@ -18,18 +22,18 @@ autoUpdater.log.transports.file.level = 'info';
let updaterModal = null;
function createEventListener(win, eventName) {
return (event) => {
function createEventListener(win: BrowserWindow, eventName: string) {
return (event: IpcMainEvent) => {
if (event.sender === win.webContents) {
win.emit(eventName);
}
};
}
function createUpdaterModal(parentWindow, options) {
function createUpdaterModal(parentWindow: BrowserWindow, options: {linuxAppIcon: string; notifyOnly: boolean}) {
const windowWidth = 480;
const windowHeight = 280;
const windowOptions = {
const windowOptions: BrowserWindowConstructorOptions = {
title: `${app.name} Updater`,
parent: parentWindow,
modal: true,
@@ -67,7 +71,7 @@ function createUpdaterModal(parentWindow, options) {
return modal;
}
function isUpdateApplicable(now, skippedVersion, updateInfo) {
function isUpdateApplicable(now: Date, skippedVersion, updateInfo) {
const releaseTime = new Date(updateInfo.releaseDate).getTime();
// 48 hours after a new version is added to releases.mattermost.com, user receives a “New update is available” dialog
@@ -83,7 +87,7 @@ function isUpdateApplicable(now, skippedVersion, updateInfo) {
return true;
}
function downloadAndInstall(cancellationToken) {
function downloadAndInstall(cancellationToken?: CancellationToken) {
autoUpdater.on('update-downloaded', () => {
global.willAppQuit = true;
autoUpdater.quitAndInstall();
@@ -150,7 +154,7 @@ function initialize(appState, mainWindow, notifyOnly = false) {
});
}
function shouldCheckForUpdatesOnStart(updateCheckedDate) {
function shouldCheckForUpdatesOnStart(updateCheckedDate: Date) {
if (updateCheckedDate) {
if (Date.now() - updateCheckedDate.getTime() < UPDATER_INTERVAL_IN_MS) {
return false;
@@ -167,6 +171,8 @@ function checkForUpdates(isManual = false) {
}
class AutoUpdaterConfig {
data: {notifyOnly?: boolean};
constructor() {
this.data = {};
}

View File

@@ -1,3 +1,4 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
@@ -10,9 +11,9 @@ import * as AppState from './appState';
const MAX_WIN_COUNT = 99;
let showUnreadBadgeSetting;
let showUnreadBadgeSetting: boolean;
function showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount) {
function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
let description = 'You have no unread messages';
let text;
if (sessionExpired) {
@@ -28,7 +29,7 @@ function showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount) {
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
}
function showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount) {
function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
let badge = '';
if (sessionExpired) {
badge = '•';
@@ -40,28 +41,28 @@ function showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount) {
app.dock.setBadge(badge);
}
function showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount) {
function showBadgeLinux(sessionExpired: boolean, mentionCount: number) {
if (app.isUnityRunning()) {
const countExpired = sessionExpired ? 1 : 0;
app.setBadgeCount(mentionCount + countExpired);
}
}
function showBadge(sessionExpired, mentionCount, showUnreadBadge) {
function showBadge(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
switch (process.platform) {
case 'win32':
showBadgeWindows(sessionExpired, showUnreadBadge, mentionCount);
showBadgeWindows(sessionExpired, mentionCount, showUnreadBadge);
break;
case 'darwin':
showBadgeOSX(sessionExpired, showUnreadBadge, mentionCount);
showBadgeOSX(sessionExpired, mentionCount, showUnreadBadge);
break;
case 'linux':
showBadgeLinux(sessionExpired, showUnreadBadge, mentionCount);
showBadgeLinux(sessionExpired, mentionCount);
break;
}
}
export function setUnreadBadgeSetting(showUnreadBadge) {
export function setUnreadBadgeSetting(showUnreadBadge: boolean) {
showUnreadBadgeSetting = showUnreadBadge;
AppState.emitStatus();
}

View File

@@ -1,6 +1,9 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import log from 'electron-log';
import {Certificate, WebContents} from 'electron';
import {CertificateModalData} from 'types/certificate';
import * as WindowManager from './windows/windowManager';
@@ -10,12 +13,18 @@ import {getLocalURLString, getLocalPreload} from './utils';
const modalPreload = getLocalPreload('modalPreload.js');
const html = getLocalURLString('certificateModal.html');
type CertificateModalResult = {
cert: Certificate;
}
export class CertificateManager {
certificateRequestCallbackMap: Map<string, (certificate?: Certificate | undefined) => void>;
constructor() {
this.certificateRequestCallbackMap = new Map();
}
handleSelectCertificate = (event, webContents, url, list, callback) => {
handleSelectCertificate = (event: Event, webContents: WebContents, url: string, list: Certificate[], callback: (certificate?: Certificate | undefined) => void) => {
if (list.length > 1) {
event.preventDefault(); // prevent the app from getting the first certificate available
@@ -27,20 +36,26 @@ export class CertificateManager {
}
}
popCertificateModal = (url, list) => {
const modalPromise = addModal(`certificate-${url}`, html, modalPreload, {url, list}, WindowManager.getMainWindow());
modalPromise.then((data) => {
const {cert} = data;
this.handleSelectedCertificate(url, cert);
}).catch((err) => {
if (err) {
log.error('Error processing certificate selection', err);
}
this.handleSelectedCertificate(url);
});
popCertificateModal = (url: string, list: Certificate[]) => {
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
const modalPromise = addModal<CertificateModalData, CertificateModalResult>(`certificate-${url}`, html, modalPreload, {url, list}, mainWindow);
if (modalPromise) {
modalPromise.then((data) => {
const {cert} = data;
this.handleSelectedCertificate(url, cert);
}).catch((err) => {
if (err) {
log.error('Error processing certificate selection', err);
}
this.handleSelectedCertificate(url);
});
}
}
handleSelectedCertificate = (server, cert) => {
handleSelectedCertificate = (server: string, cert?: Certificate) => {
const callback = this.certificateRequestCallbackMap.get(server);
if (!callback) {
log.error(`there was no callback associated with: ${server}`);

View File

@@ -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);
},
};

View 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));
};
}

View File

@@ -1,18 +1,19 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import electronContextMenu from 'electron-context-menu';
import {BrowserView, BrowserWindow, ContextMenuParams, Event, WebContents} from 'electron';
import electronContextMenu, {Options} from 'electron-context-menu';
import urlUtils from 'common/utils/url';
const defaultMenuOptions = {
shouldShowMenu: (e, p) => {
shouldShowMenu: (e: Event, p: ContextMenuParams) => {
const isInternalLink = p.linkURL.endsWith('#') && p.linkURL.slice(0, -1) === p.pageURL;
let isInternalSrc;
try {
const srcurl = urlUtils.parseURL(p.srcURL);
isInternalSrc = srcurl.protocol === 'file:';
isInternalSrc = srcurl?.protocol === 'file:';
} catch (err) {
isInternalSrc = false;
}
@@ -27,8 +28,12 @@ const defaultMenuOptions = {
};
export default class ContextMenu {
constructor(options, view) {
const providedOptions = options || {};
view: BrowserWindow | BrowserView;
menuOptions: Options;
menuDispose?: () => void;
constructor(options: Options, view: BrowserWindow | BrowserView) {
const providedOptions: Options = options || {};
this.menuOptions = Object.assign({}, defaultMenuOptions, providedOptions);
this.view = view;
@@ -39,7 +44,7 @@ export default class ContextMenu {
dispose = () => {
if (this.menuDispose) {
this.menuDispose();
this.menuDispose = null;
delete this.menuDispose;
}
}
@@ -50,7 +55,7 @@ export default class ContextMenu {
* Work-around issue with passing `WebContents` to `electron-context-menu` in Electron 11
* @see https://github.com/sindresorhus/electron-context-menu/issues/123
*/
const options = {window: {webContents: this.view.webContents, inspectElement: this.view.webContents.inspectElement.bind(this.view.webContents), isDestroyed: this.view.webContents.isDestroyed.bind(this.view.webContents), off: this.view.webContents.off.bind(this.view.webContents)}, ...this.menuOptions};
const options = {window: {webContents: this.view.webContents, inspectElement: this.view.webContents.inspectElement.bind(this.view.webContents), isDestroyed: this.view.webContents.isDestroyed.bind(this.view.webContents), off: this.view.webContents.off.bind(this.view.webContents)} as unknown as WebContents, ...this.menuOptions};
this.menuDispose = electronContextMenu(options);
}
}

View File

@@ -1,16 +1,16 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {app} from 'electron';
import {app, Session} from 'electron';
import log from 'electron-log';
function flushCookiesStore(session) {
function flushCookiesStore(session: Session) {
session.cookies.flushStore().catch((err) => {
log.error(`There was a problem flushing cookies:\n${err}`);
});
}
export default function initCookieManager(session) {
export default function initCookieManager(session: Session) {
// Somehow cookies are not immediately saved to disk.
// So manually flush cookie store to disk on closing the app.
// https://github.com/electron/electron/issues/8416

View File

@@ -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;
}
}

View File

@@ -1,18 +1,23 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
/* eslint-disable max-lines */
import fs from 'fs';
import path from 'path';
import electron from 'electron';
import electron, {BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, Rectangle} from 'electron';
import isDev from 'electron-is-dev';
import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-installer';
import log from 'electron-log';
import 'airbnb-js-shims/target/es2015';
import Utils from 'common/utils/util';
import urlUtils from 'common/utils/url';
import {Team} from 'types/config';
import {MentionData} from 'types/notification';
import {Boundaries} from 'types/utils';
import {
SWITCH_SERVER,
@@ -33,6 +38,10 @@ import {
} from 'common/communication';
import Config from 'common/config';
import Utils from 'common/utils/util';
import urlUtils from 'common/utils/url';
import {protocols} from '../../electron-builder.json';
import AutoLauncher from './AutoLauncher';
@@ -76,13 +85,13 @@ const certificateErrorCallbacks = new Map();
// Keep a global reference of the window object, if you don't, the window will
// be closed automatically when the JavaScript object is garbage collected.
let certificateStore = null;
let trustedOriginsStore = null;
let scheme = null;
let certificateStore: CertificateStore;
let trustedOriginsStore;
let scheme: string;
let appVersion = null;
let config = null;
let authManager = null;
let certificateManager = null;
let config: Config;
let authManager: AuthManager;
let certificateManager: CertificateManager;
/**
* Main entry point for the application, ensures that everything initializes in the proper order
@@ -140,7 +149,7 @@ function initializeArgs() {
}
async function initializeConfig() {
const loadConfig = new Promise((resolve) => {
const loadConfig = new Promise<void>((resolve) => {
config = new Config(app.getPath('userData') + '/config.json');
config.once('update', (configData) => {
config.on('update', handleConfigUpdate);
@@ -169,7 +178,11 @@ function initializeAppEventListeners() {
}
function initializeBeforeAppReady() {
certificateStore = CertificateStore.load(path.resolve(app.getPath('userData'), 'certificate.json'));
if (!config || !config.data) {
log.error('No config loaded');
return;
}
certificateStore = new CertificateStore(path.resolve(app.getPath('userData'), 'certificate.json'));
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
trustedOriginsStore.load();
@@ -196,7 +209,7 @@ function initializeBeforeAppReady() {
allowProtocolDialog.init();
authManager = new AuthManager(config, trustedOriginsStore);
authManager = new AuthManager(config.data, trustedOriginsStore);
certificateManager = new CertificateManager();
if (isDev) {
@@ -237,7 +250,10 @@ function initializeInterCommunicationEventListeners() {
// config event handlers
//
function handleConfigUpdate(newConfig) {
function handleConfigUpdate(newConfig: Config) {
if (!newConfig.data) {
return;
}
if (process.platform === 'win32' || process.platform === 'linux') {
const appLauncher = new AutoLauncher();
const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable();
@@ -247,6 +263,7 @@ function handleConfigUpdate(newConfig) {
log.error('error:', err);
});
WindowManager.setConfig(newConfig.data);
authManager.handleConfigUpdate(newConfig.data);
setUnreadBadgeSetting(newConfig.data && newConfig.data.showUnreadBadge);
}
@@ -254,6 +271,10 @@ function handleConfigUpdate(newConfig) {
}
function handleConfigSynchronize() {
if (!config.data) {
return;
}
// TODO: send this to server manager
WindowManager.setConfig(config.data);
setUnreadBadgeSetting(config.data.showUnreadBadge);
@@ -267,7 +288,7 @@ function handleConfigSynchronize() {
function handleReloadConfig() {
config.reload();
WindowManager.setConfig(config.data);
WindowManager.setConfig(config.data!);
}
function handleAppVersion() {
@@ -277,7 +298,7 @@ function handleAppVersion() {
};
}
function handleDarkModeChange(darkMode) {
function handleDarkModeChange(darkMode: boolean) {
refreshTrayImages(config.trayIconTheme);
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
WindowManager.updateLoadingScreenDarkMode(darkMode);
@@ -288,11 +309,13 @@ function handleDarkModeChange(darkMode) {
//
// activate first app instance, subsequent instances will quit themselves
function handleAppSecondInstance(event, argv) {
function handleAppSecondInstance(event: Event, argv: string[]) {
// Protocol handler for win32
// argv: An array of the second instances (command line / deep linked) arguments
const deeplinkingUrl = getDeeplinkingURL(argv);
openDeepLink(deeplinkingUrl);
if (deeplinkingUrl) {
openDeepLink(deeplinkingUrl);
}
}
function handleAppWindowAllClosed() {
@@ -303,9 +326,9 @@ function handleAppWindowAllClosed() {
}
}
function handleAppBrowserWindowCreated(error, newWindow) {
function handleAppBrowserWindowCreated(event: Event, newWindow: BrowserWindow) {
// Screen cannot be required before app is ready
resizeScreen(electron.screen, newWindow);
resizeScreen(newWindow);
}
function handleAppActivate() {
@@ -318,18 +341,18 @@ function handleAppBeforeQuit() {
global.willAppQuit = true;
}
function handleQuit(e, reason, stack) {
function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
log.error(`Exiting App. Reason: ${reason}`);
log.info(`Stacktrace:\n${stack}`);
handleAppBeforeQuit();
app.quit();
}
function handleSelectCertificate(event, webContents, url, list, callback) {
function handleSelectCertificate(event: electron.Event, webContents: electron.WebContents, url: string, list: electron.Certificate[], callback: (certificate?: electron.Certificate | undefined) => void) {
certificateManager.handleSelectCertificate(event, webContents, url, list, callback);
}
function handleAppCertificateError(event, webContents, url, error, certificate, callback) {
function handleAppCertificateError(event: electron.Event, webContents: electron.WebContents, url: string, error: string, certificate: electron.Certificate, callback: (isTrusted: boolean) => void) {
const parsedURL = new URL(url);
if (!parsedURL) {
return;
@@ -355,6 +378,9 @@ function handleAppCertificateError(event, webContents, url, error, certificate,
// TODO: should we move this to window manager or provide a handler for dialogs?
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
dialog.showMessageBox(mainWindow, {
title: 'Certificate Error',
message: 'There is a configuration issue with this Mattermost server, or someone is trying to intercept your connection. You also may need to sign into the Wi-Fi you are connected to using your web browser.',
@@ -395,15 +421,15 @@ function handleAppCertificateError(event, webContents, url, error, certificate,
}
}
function handleAppLogin(event, webContents, request, authInfo, callback) {
function handleAppLogin(event: electron.Event, webContents: electron.WebContents, request: electron.AuthenticationResponseDetails, authInfo: electron.AuthInfo, callback: (username?: string | undefined, password?: string | undefined) => void) {
authManager.handleAppLogin(event, webContents, request, authInfo, callback);
}
function handleAppGPUProcessCrashed(event, killed) {
function handleAppGPUProcessCrashed(event: electron.Event, killed: boolean) {
log.error(`The GPU process has crashed (killed = ${killed})`);
}
function openDeepLink(deeplinkingUrl) {
function openDeepLink(deeplinkingUrl: string) {
try {
WindowManager.showMainWindow(deeplinkingUrl);
} catch (err) {
@@ -427,7 +453,7 @@ function handleAppWillFinishLaunching() {
});
}
function handleSwitchServer(event, serverName) {
function handleSwitchServer(event: IpcMainEvent, serverName: string) {
WindowManager.switchServer(serverName);
}
@@ -436,7 +462,11 @@ function handleNewServerModal() {
const modalPreload = getLocalPreload('modalPreload.js');
const modalPromise = addModal('newServer', html, modalPreload, {}, WindowManager.getMainWindow());
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
const modalPromise = addModal<unknown, Team>('newServer', html, modalPreload, {}, mainWindow);
if (modalPromise) {
modalPromise.then((data) => {
const teams = config.teams;
@@ -506,7 +536,7 @@ function initializeAfterAppReady() {
WindowManager.showSettingsWindow();
}
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow());
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
// listen for status updates and pass on to renderer
userActivityMonitor.on('status', (status) => {
@@ -519,7 +549,7 @@ function initializeAfterAppReady() {
if (shouldShowTrayIcon()) {
setupTray(config.trayIconTheme);
}
setupBadge(config.showUnreadBadge);
setupBadge();
session.defaultSession.on('will-download', (event, item, webContents) => {
const filename = item.getFilename();
@@ -533,13 +563,13 @@ function initializeAfterAppReady() {
}
item.setSaveDialogOptions({
title: filename,
defaultPath: path.resolve(config.combinedData.downloadLocation, filename),
defaultPath: path.resolve(config.downloadLocation, filename),
filters,
});
item.on('done', (doneEvent, state) => {
if (state === 'completed') {
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams));
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams)!);
}
});
});
@@ -584,7 +614,7 @@ function initializeAfterAppReady() {
// ipc communication event handlers
//
function handleMentionNotification(event, title, body, channel, teamId, silent, data) {
function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, silent: boolean, data: MentionData) {
displayMention(title, body, channel, teamId, silent, event.sender, data);
}
@@ -605,23 +635,21 @@ function handleCloseAppMenu() {
WindowManager.focusBrowserView();
}
function handleUpdateMenuEvent(event, menuConfig) {
// TODO: this might make sense to move to window manager? so it updates the window referenced if needed.
const mainWindow = WindowManager.getMainWindow();
function handleUpdateMenuEvent(event: IpcMainEvent, menuConfig: Config) {
const aMenu = appMenu.createMenu(menuConfig);
Menu.setApplicationMenu(aMenu);
aMenu.addListener('menu-will-close', handleCloseAppMenu);
// set up context menu for tray icon
if (shouldShowTrayIcon()) {
const tMenu = trayMenu.createMenu(menuConfig.data);
setTrayMenu(tMenu, mainWindow);
const tMenu = trayMenu.createMenu(menuConfig.data!);
setTrayMenu(tMenu);
}
}
async function handleSelectDownload(event, startFrom) {
async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
const message = 'Specify the folder where files will download';
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.data.downloadLocation,
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation,
message,
properties:
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
@@ -632,7 +660,7 @@ async function handleSelectDownload(event, startFrom) {
// helper functions
//
function getDeeplinkingURL(args) {
function getDeeplinkingURL(args: string[]) {
if (Array.isArray(args) && args.length) {
// deeplink urls should always be the last argument, but may not be the first (i.e. Windows with the app already running)
const url = args[args.length - 1];
@@ -640,14 +668,14 @@ function getDeeplinkingURL(args) {
return url;
}
}
return null;
return undefined;
}
function shouldShowTrayIcon() {
return config.showTrayIcon || process.platform === 'win32';
}
function wasUpdated(lastAppVersion) {
function wasUpdated(lastAppVersion?: string) {
return lastAppVersion !== app.getVersion();
}
@@ -662,7 +690,7 @@ function clearAppCache() {
}
}
function isWithinDisplay(state, display) {
function isWithinDisplay(state: Rectangle, display: Boundaries) {
const startsWithinDisplay = !(state.x > display.maxX || state.y > display.maxY || state.x < display.minX || state.y < display.minY);
if (!startsWithinDisplay) {
return false;
@@ -674,7 +702,7 @@ function isWithinDisplay(state, display) {
return !(midX > display.maxX || midY > display.maxY);
}
function getValidWindowPosition(state) {
function getValidWindowPosition(state: Rectangle) {
// Check if the previous position is out of the viewable area
// (e.g. because the screen has been plugged off)
const boundaries = Utils.getDisplayBoundaries();
@@ -688,7 +716,7 @@ function getValidWindowPosition(state) {
return {x: state.x, y: state.y};
}
function resizeScreen(screen, browserWindow) {
function resizeScreen(browserWindow: BrowserWindow) {
function handle() {
const position = browserWindow.getPosition();
const size = browserWindow.getSize();

View File

@@ -3,14 +3,15 @@
// See LICENSE.txt for license information.
'use strict';
import {app, Menu, session, shell, webContents} from 'electron';
import {app, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
import {ADD_SERVER, SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
import Config from 'common/config';
import * as WindowManager from '../windows/windowManager';
function createTemplate(config) {
const separatorItem = {
function createTemplate(config: Config) {
const separatorItem: MenuItemConstructorOptions = {
type: 'separator',
};
@@ -39,7 +40,7 @@ function createTemplate(config) {
},
});
if (config.data.enableServerManagement === true) {
if (config.data?.enableServerManagement === true) {
platformAppMenu.push({
label: 'Sign in to Another Server',
click() {
@@ -53,7 +54,7 @@ function createTemplate(config) {
separatorItem, {
role: 'hide',
}, {
role: 'hideothers',
role: 'hideOthers',
}, {
role: 'unhide',
}, separatorItem, {
@@ -139,7 +140,7 @@ function createTemplate(config) {
}
return 'Ctrl+Shift+I';
})(),
click(item, focusedWindow) {
click(item: Electron.MenuItem, focusedWindow?: WebContents) {
if (focusedWindow) {
// toggledevtools opens it in the last known position, so sometimes it goes below the browserview
if (focusedWindow.isDevToolsOpened()) {
@@ -193,7 +194,7 @@ function createTemplate(config) {
}],
});
const teams = config.data.teams || [];
const teams = config.data?.teams || [];
const windowMenu = {
label: '&Window',
submenu: [{
@@ -209,7 +210,7 @@ function createTemplate(config) {
label: team.name,
accelerator: `CmdOrCtrl+${i + 1}`,
click() {
WindowManager.switchServer(team.name, true);
WindowManager.switchServer(team.name);
},
};
}), separatorItem, {
@@ -230,17 +231,19 @@ function createTemplate(config) {
};
template.push(windowMenu);
const submenu = [];
if (config.data.helpLink) {
if (config.data?.helpLink) {
submenu.push({
label: 'Learn More...',
click() {
shell.openExternal(config.data.helpLink);
shell.openExternal(config.data!.helpLink);
},
});
submenu.push(separatorItem);
}
submenu.push({
// eslint-disable-next-line no-undef
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
label: `Version ${app.getVersion()} commit: ${__HASH_VERSION__}`,
enabled: false,
});
@@ -249,8 +252,9 @@ function createTemplate(config) {
return template;
}
function createMenu(config) {
return Menu.buildFromTemplate(createTemplate(config));
function createMenu(config: Config) {
// TODO: Electron is enforcing certain variables that it doesn't need
return Menu.buildFromTemplate(createTemplate(config) as Array<MenuItemConstructorOptions | MenuItem>);
}
export default {

View File

@@ -3,18 +3,19 @@
// See LICENSE.txt for license information.
'use strict';
import {Menu} from 'electron';
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
import {CombinedConfig} from 'types/config';
import * as WindowManager from '../windows/windowManager';
function createTemplate(config) {
function createTemplate(config: CombinedConfig) {
const teams = config.teams;
const template = [
...teams.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).map((team) => {
return {
label: team.name,
click: () => {
WindowManager.switchServer(team.name, true);
WindowManager.switchServer(team.name);
},
};
}), {
@@ -33,8 +34,9 @@ function createTemplate(config) {
return template;
}
function createMenu(config) {
return Menu.buildFromTemplate(createTemplate(config));
function createMenu(config: CombinedConfig) {
// TODO: Electron is enforcing certain variables that it doesn't need
return Menu.buildFromTemplate(createTemplate(config) as Array<MenuItemConstructorOptions | MenuItem>);
}
export default {

View File

@@ -3,6 +3,7 @@
import path from 'path';
import {app, Notification} from 'electron';
import {ServerFromURL} from 'types/utils';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
const appIconURL = path.resolve(assetsDir, 'appicon_48.png');
@@ -11,11 +12,12 @@ const defaultOptions = {
title: 'Download Complete',
silent: false,
icon: appIconURL,
urgency: 'normal',
urgency: 'normal' as Notification['urgency'],
body: '',
};
export class DownloadNotification extends Notification {
constructor(fileName, serverInfo) {
constructor(fileName: string, serverInfo: ServerFromURL) {
const options = {...defaultOptions};
if (process.platform === 'win32') {
options.icon = appIconURL;

View File

@@ -4,6 +4,8 @@
import path from 'path';
import {app, Notification} from 'electron';
import {MentionOptions} from 'types/notification';
import osVersion from 'common/osVersion';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
@@ -13,23 +15,28 @@ const defaultOptions = {
title: 'Someone mentioned you',
silent: false,
icon: appIconURL,
urgency: 'normal',
urgency: 'normal' as Notification['urgency'],
};
export const DEFAULT_WIN7 = 'Ding';
export class Mention extends Notification {
constructor(customOptions, channel, teamId) {
customSound: boolean;
channel: {id: string}; // TODO: Channel from mattermost-redux
teamId: string;
constructor(customOptions: MentionOptions, channel: {id: string}, teamId: string) {
super({...defaultOptions, ...customOptions});
const options = {...defaultOptions, ...customOptions};
if (process.platform === 'darwin') {
// Notification Center shows app's icon, so there were two icons on the notification.
Reflect.deleteProperty(options, 'icon');
}
const isWin7 = (process.platform === 'win32' && osVersion.isLowerThanOrEqualWindows8_1() && DEFAULT_WIN7);
const customSound = !options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7);
const customSound = Boolean(!options.silent && ((options.data && options.data.soundName !== 'None' && options.data.soundName) || isWin7));
if (customSound) {
options.silent = true;
}
super(options);
this.customSound = customSound;
this.channel = channel;
this.teamId = teamId;

View File

@@ -4,6 +4,9 @@
import {shell, Notification} from 'electron';
import log from 'electron-log';
import {MentionData} from 'types/notification';
import {ServerFromURL} from 'types/utils';
import {PLAY_SOUND} from 'common/communication';
import * as windowManager from '../windows/windowManager';
@@ -13,7 +16,7 @@ import {DownloadNotification} from './Download';
const currentNotifications = new Map();
export function displayMention(title, body, channel, teamId, silent, webcontents, data) {
export function displayMention(title: string, body: string, channel: {id: string}, teamId: string, silent: boolean, webcontents: Electron.WebContents, data: MentionData) {
if (!Notification.isSupported()) {
log.error('notification not supported');
return;
@@ -49,14 +52,14 @@ export function displayMention(title, body, channel, teamId, silent, webcontents
mention.on('click', () => {
if (serverName) {
windowManager.switchServer(serverName, true);
windowManager.switchServer(serverName);
webcontents.send('notification-clicked', {channel, teamId});
}
});
mention.show();
}
export function displayDownloadCompleted(fileName, path, serverInfo) {
export function displayDownloadCompleted(fileName: string, path: string, serverInfo: ServerFromURL) {
if (!Notification.isSupported()) {
log.error('notification not supported');
return;

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
'use strict';

View File

@@ -11,12 +11,12 @@ import * as AppState from '../appState';
const assetsDir = path.resolve(app.getAppPath(), 'assets');
let trayImages;
let trayIcon;
let trayImages: Record<string, Electron.NativeImage>;
let trayIcon: Tray;
let lastStatus = 'normal';
let lastMessage = app.name;
export function refreshTrayImages(trayIconTheme) {
export function refreshTrayImages(trayIconTheme: string) {
const winTheme = nativeTheme.shouldUseDarkColors ? 'dark' : 'light';
switch (process.platform) {
@@ -69,7 +69,7 @@ export function refreshTrayImages(trayIconTheme) {
return trayImages;
}
export function setupTray(icontheme) {
export function setupTray(icontheme: string) {
refreshTrayImages(icontheme);
trayIcon = new Tray(trayImages.normal);
if (process.platform === 'darwin') {
@@ -103,7 +103,7 @@ export function setupTray(icontheme) {
});
}
function setTray(status, message) {
function setTray(status: string, message: string) {
lastStatus = status;
lastMessage = message;
trayIcon.setImage(trayImages[status]);
@@ -116,17 +116,8 @@ export function destroyTray() {
}
}
export function setTrayMenu(tMenu, mainWindow) {
if (process.platform === 'darwin' || process.platform === 'linux') {
// store the information, if the tray was initialized, for checking in the settings, if the application
// was restarted after setting "Show icon on menu bar"
if (trayIcon) {
trayIcon.setContextMenu(tMenu);
mainWindow.trayWasVisible = true;
} else {
mainWindow.trayWasVisible = false;
}
} else if (trayIcon) {
export function setTrayMenu(tMenu: Electron.Menu) {
if (trayIcon) {
trayIcon.setContextMenu(tMenu);
}
}

View File

@@ -7,12 +7,16 @@ import fs from 'fs';
import log from 'electron-log';
import urlUtils from '../common/utils/url';
import {TrustedOrigin, PermissionType} from 'types/trustedOrigin';
import urlUtils from 'common/utils/url';
import * as Validator from './Validator';
export default class TrustedOriginsStore {
constructor(storeFile) {
storeFile: string;
data?: Map<string, TrustedOrigin>;
constructor(storeFile: string) {
this.storeFile = storeFile;
}
@@ -40,18 +44,24 @@ export default class TrustedOriginsStore {
}
// don't use this, is for ease of mocking it on testing
saveToFile(stringMap) {
saveToFile(stringMap: string) {
fs.writeFileSync(this.storeFile, stringMap);
}
save = () => {
if (!this.data) {
return;
}
this.saveToFile(JSON.stringify(Object.fromEntries((this.data.entries())), null, ' '));
};
// if permissions or targetUrl are invalid, this function will throw an error
// this function stablishes all the permissions at once, overwriting whatever was before
// to enable just one permission use addPermission instead.
set = (targetURL, permissions) => {
set = (targetURL: string, permissions: Record<PermissionType, boolean>) => {
if (!this.data) {
return;
}
const validPermissions = Validator.validateOriginPermissions(permissions);
if (!validPermissions) {
throw new Error(`Invalid permissions set for trusting ${targetURL}`);
@@ -60,30 +70,28 @@ export default class TrustedOriginsStore {
};
// enables usage of `targetURL` for `permission`
addPermission = (targetURL, permission) => {
addPermission = (targetURL: string, permission: PermissionType) => {
const origin = urlUtils.getHost(targetURL);
const currentPermissions = this.data.get(origin) || {};
currentPermissions[permission] = true;
this.set(origin, currentPermissions);
this.set(origin, {[permission]: true});
}
delete = (targetURL) => {
delete = (targetURL: string) => {
let host;
try {
host = urlUtils.getHost(targetURL);
this.data.delete(host);
this.data?.delete(host);
} catch {
return false;
}
return true;
}
isExisting = (targetURL) => {
return (typeof this.data.get(urlUtils.getHost(targetURL)) !== 'undefined');
isExisting = (targetURL: string) => {
return this.data?.has(urlUtils.getHost(targetURL)) || false;
};
// if user hasn't set his preferences, it will return null (falsy)
checkPermission = (targetURL, permission) => {
checkPermission = (targetURL: string, permission: PermissionType) => {
if (!permission) {
log.error(`Missing permission request on ${targetURL}`);
return null;
@@ -96,7 +104,7 @@ export default class TrustedOriginsStore {
return null;
}
const urlPermissions = this.data.get(origin);
return urlPermissions ? urlPermissions[permission] : null;
const urlPermissions = this.data?.get(origin);
return urlPermissions ? urlPermissions[permission] : undefined;
}
}

View File

@@ -2,16 +2,18 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import electron, {app} from 'electron';
import electron, {app, BrowserWindow} from 'electron';
import path from 'path';
import {Args} from 'types/args';
import {PRODUCTION} from 'common/utils/constants';
import Utils from 'common/utils/util';
const TAB_BAR_HEIGHT = 40;
const BACK_BAR_HEIGHT = 36;
export function shouldBeHiddenOnStartup(parsedArgv) {
export function shouldBeHiddenOnStartup(parsedArgv: Args) {
if (parsedArgv.hidden) {
return true;
}
@@ -23,12 +25,12 @@ export function shouldBeHiddenOnStartup(parsedArgv) {
return false;
}
export function getWindowBoundaries(win, hasBackBar = false) {
export function getWindowBoundaries(win: BrowserWindow, hasBackBar = false) {
const {width, height} = win.getContentBounds();
return getAdjustedWindowBoundaries(width, height, hasBackBar);
}
export function getAdjustedWindowBoundaries(width, height, hasBackBar = false) {
export function getAdjustedWindowBoundaries(width: number, height: number, hasBackBar = false) {
return {
x: 0,
y: TAB_BAR_HEIGHT + (hasBackBar ? BACK_BAR_HEIGHT : 0),
@@ -37,12 +39,12 @@ export function getAdjustedWindowBoundaries(width, height, hasBackBar = false) {
};
}
export function getLocalURLString(urlPath, query, isMain) {
export function getLocalURLString(urlPath: string, query?: Map<string, string>, isMain?: boolean) {
const localURL = getLocalURL(urlPath, query, isMain);
return localURL.href;
}
export function getLocalURL(urlPath, query, isMain) {
export function getLocalURL(urlPath: string, query?: Map<string, string>, isMain?: boolean) {
let pathname;
const processPath = isMain ? '' : '/renderer';
const mode = Utils.runMode();
@@ -57,7 +59,7 @@ export function getLocalURL(urlPath, query, isMain) {
const localUrl = new URL(`${protocol}://${hostname}${port}`);
localUrl.pathname = pathname;
if (query) {
query.forEach((value, key) => {
query.forEach((value: string, key: string) => {
localUrl.searchParams.append(encodeURIComponent(key), encodeURIComponent(value));
});
}
@@ -65,7 +67,7 @@ export function getLocalURL(urlPath, query, isMain) {
return localUrl;
}
export function getLocalPreload(file) {
export function getLocalPreload(file: string) {
if (Utils.runMode() === PRODUCTION) {
return path.join(electron.app.getAppPath(), `${file}`);
}

View File

@@ -1,7 +1,8 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, app, ipcMain} from 'electron';
import {BrowserView, app, ipcMain, BrowserWindow} from 'electron';
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
import log from 'electron-log';
import {EventEmitter} from 'events';
@@ -21,6 +22,8 @@ import {
LOADSCREEN_END,
} from 'common/communication';
import {MattermostServer} from 'main/MattermostServer';
import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
import * as WindowManager from '../windows/windowManager';
@@ -28,49 +31,67 @@ import * as appState from '../appState';
import {removeWebContentsListeners} from './webContentEvents';
const READY = 1;
const WAITING_MM = 2;
const LOADING = 0;
const ERROR = -1;
enum Status {
LOADING,
READY,
WAITING_MM,
ERROR = -1,
}
const ASTERISK_GROUP = 3;
const MENTIONS_GROUP = 2;
export class MattermostView extends EventEmitter {
constructor(server, win, options) {
server: MattermostServer;
window: BrowserWindow;
view: BrowserView;
isVisible: boolean;
options: BrowserViewConstructorOptions;
removeLoading?: number;
/**
* for backward compatibility when reading the title.
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
*/
usesAsteriskForUnreads?: boolean;
faviconMemoize: Map<string, boolean>;
currentFavicon?: string;
isInitialized: boolean;
hasBeenShown: boolean;
altLastPressed?: boolean;
contextMenu: ContextMenu;
status?: Status;
retryLoad?: NodeJS.Timeout;
maxRetries: number;
constructor(server: MattermostServer, win: BrowserWindow, options: BrowserViewConstructorOptions) {
super();
this.server = server;
this.window = win;
const preload = getLocalPreload('preload.js');
const spellcheck = ((!options || typeof options.spellcheck === 'undefined') ? true : options.spellcheck);
this.options = {
webPreferences: {
contextIsolation: process.env.NODE_ENV !== 'test',
preload,
spellcheck,
additionalArguments: [
`version=${app.version}`,
`version=${app.getVersion()}`,
`appName=${app.name}`,
],
enableRemoteModule: process.env.NODE_ENV === 'test',
nodeIntegration: process.env.NODE_ENV === 'test',
...options.webPreferences,
},
...options,
};
this.isVisible = false;
this.view = new BrowserView(this.options);
this.removeLoading = null;
this.resetLoadingStatus();
/**
* for backward compatibility when reading the title.
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
*/
this.usesAsteriskForUnreads = null;
this.faviconMemoize = new Map();
this.currentFavicon = null;
log.info(`BrowserView created for server ${this.server.name}`);
this.isInitialized = false;
@@ -82,24 +103,40 @@ export class MattermostView extends EventEmitter {
}
this.contextMenu = new ContextMenu({}, this.view);
this.maxRetries = MAX_SERVER_RETRIES;
}
// use the same name as the server
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
get name() {
return this.server.name;
return this.server?.name;
}
resetLoadingStatus = () => {
if (this.status !== LOADING) { // if it's already loading, don't touch anything
this.retryLoad = null;
this.status = LOADING;
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything
delete this.retryLoad;
this.status = Status.LOADING;
this.maxRetries = MAX_SERVER_RETRIES;
}
}
load = (someURL) => {
const loadURL = (typeof someURL === 'undefined') ? `${this.server.url.toString()}` : urlUtils.parseURL(someURL).toString();
load = (someURL?: URL | string) => {
if (!this.server) {
return;
}
let loadURL: string;
if (someURL) {
const parsedURL = urlUtils.parseURL(someURL);
if (parsedURL) {
loadURL = parsedURL.toString();
} else {
log.error('Cannot parse provided url, using current server url', someURL);
loadURL = this.server.url.toString();
}
} else {
loadURL = this.server.url.toString();
}
log.info(`[${Util.shorten(this.server.name)}] Loading ${loadURL}`);
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
@@ -107,7 +144,7 @@ export class MattermostView extends EventEmitter {
});
}
retry = (loadURL) => {
retry = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
@@ -121,43 +158,43 @@ export class MattermostView extends EventEmitter {
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.server.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
this.status = ERROR;
this.status = Status.ERROR;
}
});
};
}
loadRetry = (loadURL, err) => {
loadRetry = (loadURL: string, err: any) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
WindowManager.sendToRenderer(LOAD_RETRY, this.server.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.server.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
}
loadSuccess = (loadURL) => {
loadSuccess = (loadURL: string) => {
return () => {
log.info(`[${Util.shorten(this.server.name)}] finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name);
this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === LOADING) {
if (this.status === Status.LOADING) {
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
this.handleTitleUpdate(null, this.view.webContents.getTitle());
this.updateMentionsFromTitle(this.view.webContents.getTitle());
this.findUnreadState(null);
}
this.status = WAITING_MM;
this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.server.name, loadURL.toString());
this.emit(LOAD_SUCCESS, this.server.name, loadURL);
this.view.webContents.send(SET_SERVER_NAME, this.server.name);
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
};
}
show = (requestedVisibility) => {
show = (requestedVisibility?: boolean) => {
this.hasBeenShown = true;
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
if (request && !this.isVisible) {
this.window.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
if (this.status === READY) {
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
if (this.status === Status.READY) {
this.focus();
}
} else if (!request && this.isVisible) {
@@ -173,15 +210,12 @@ export class MattermostView extends EventEmitter {
hide = () => this.show(false);
setBounds = (boundaries) => {
setBounds = (boundaries: Electron.Rectangle) => {
// todo: review this, as it might not work properly with devtools/minimizing/resizing
this.view.setBounds(boundaries);
}
destroy = () => {
if (this.retryLoad) {
clearTimeout(this.retryLoad);
}
removeWebContentsListeners(this.view.webContents.id);
if (this.window) {
this.window.removeBrowserView(this.view);
@@ -189,12 +223,17 @@ export class MattermostView extends EventEmitter {
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.view.webContents.destroy();
this.window = null;
this.server = null;
this.isVisible = false;
clearTimeout(this.retryLoad);
if (this.retryLoad) {
clearTimeout(this.retryLoad);
}
if (this.removeLoading) {
clearTimeout(this.removeLoading);
}
}
focus = () => {
@@ -206,22 +245,22 @@ export class MattermostView extends EventEmitter {
}
isReady = () => {
return this.status !== LOADING;
return this.status !== Status.LOADING;
}
needsLoadingScreen = () => {
return !(this.status === READY || this.status === ERROR);
return !(this.status === Status.READY || this.status === Status.ERROR);
}
setInitialized = (timedout) => {
this.status = READY;
setInitialized = (timedout?: boolean) => {
this.status = Status.READY;
if (timedout) {
log.info(`${this.server.name} timeout expired will show the browserview`);
this.emit(LOADSCREEN_END, this.server.name);
}
clearTimeout(this.removeLoading);
this.removeLoading = null;
delete this.removeLoading;
}
openDevTools = () => {
@@ -229,15 +268,15 @@ export class MattermostView extends EventEmitter {
}
getWebContents = () => {
if (this.status === READY) {
if (this.status === Status.READY) {
return this.view.webContents;
} else if (this.window) {
return this.window.webContents; // if it's not ready you are looking at the renderer process
}
return WindowManager.getMainWindow.webContents;
return WindowManager.getMainWindow()?.webContents;
}
handleInputEvents = (_, input) => {
handleInputEvents = (_: Event, input: Input) => {
// Handler for pressing the Alt key to focus the 3-dot menu
if (input.key === 'Alt' && input.type === 'keyUp' && this.altLastPressed) {
this.altLastPressed = false;
@@ -253,8 +292,8 @@ export class MattermostView extends EventEmitter {
}
}
handleDidNavigate = (event, url) => {
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url, url) || urlUtils.isAdminUrl(this.server.url, url);
handleDidNavigate = (event: Event, url: string) => {
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url || '', url) || urlUtils.isAdminUrl(this.server.url || '', url);
if (isUrlTeamUrl) {
this.setBounds(getWindowBoundaries(this.window));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
@@ -266,15 +305,19 @@ export class MattermostView extends EventEmitter {
}
}
handleUpdateTarget = (e, url) => {
if (!this.server.sameOrigin(url)) {
handleUpdateTarget = (e: Event, url: string) => {
if (!url || !this.server.sameOrigin(url)) {
this.emit(UPDATE_TARGET_URL, url);
}
}
titleParser = /(\((\d+)\) )?(\*)?/g
handleTitleUpdate = (e, title) => {
handleTitleUpdate = (e: Event, title: string) => {
this.updateMentionsFromTitle(title);
}
updateMentionsFromTitle = (title: string) => {
//const title = this.view.webContents.getTitle();
const resultsIterator = title.matchAll(this.titleParser);
const results = resultsIterator.next(); // we are only interested in the first set
@@ -293,13 +336,13 @@ export class MattermostView extends EventEmitter {
appState.updateMentions(this.server.name, mentions, unreads);
}
handleFaviconUpdate = (e, favicons) => {
handleFaviconUpdate = (e: Event, favicons: string[]) => {
if (!this.usesAsteriskForUnreads) {
// if unread state is stored for that favicon, retrieve value.
// if not, get related info from preload and store it for future changes
this.currentFavicon = favicons[0];
if (this.faviconMemoize.has(favicons[0])) {
appState.updateUnreads(this.server.name, this.faviconMemoize.get(favicons[0]));
appState.updateUnreads(this.server.name, Boolean(this.faviconMemoize.get(favicons[0])));
} else {
this.findUnreadState(favicons[0]);
}
@@ -307,7 +350,7 @@ export class MattermostView extends EventEmitter {
}
// if favicon is null, it will affect appState, but won't be memoized
findUnreadState = (favicon) => {
findUnreadState = (favicon: string | null) => {
try {
this.view.webContents.send(IS_UNREAD, favicon, this.server.name);
} catch (err) {
@@ -318,12 +361,12 @@ export class MattermostView extends EventEmitter {
// if favicon is null, it means it is the initial load,
// so don't memoize as we don't have the favicons and there is no rush to find out.
handleFaviconIsUnread = (e, favicon, serverName, result) => {
handleFaviconIsUnread = (e: Event, favicon: string, serverName: string, result: boolean) => {
if (this.server && serverName === this.server.name) {
if (favicon) {
this.faviconMemoize.set(favicon, result);
}
if (favicon === null || favicon === this.currentFavicon) {
if (!favicon || favicon === this.currentFavicon) {
appState.updateUnreads(serverName, result);
}
}

View File

@@ -1,23 +1,24 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {ipcMain} from 'electron';
import {BrowserWindow, ipcMain} from 'electron';
import {IpcMainEvent, IpcMainInvokeEvent} from 'electron/main';
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication.js';
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication';
import * as WindowManager from '../windows/windowManager';
import {ModalView} from './modalView';
let modalQueue = [];
let modalQueue: Array<ModalView<any, any>> = [];
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
// should we return the original promise if called multiple times with the same key?
export function addModal(key, html, preload, data, win) {
export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow) {
const foundModal = modalQueue.find((modal) => modal.key === key);
if (!foundModal) {
const modalPromise = new Promise((resolve, reject) => {
const mv = new ModalView(key, html, preload, data, resolve, reject, win);
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win);
modalQueue.push(mv);
});
@@ -34,7 +35,7 @@ ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
ipcMain.on(MODAL_RESULT, handleModalResult);
ipcMain.on(MODAL_CANCEL, handleModalCancel);
function findModalByCaller(event) {
function findModalByCaller(event: IpcMainInvokeEvent) {
if (modalQueue.length) {
const requestModal = modalQueue.find((modal) => {
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
@@ -44,7 +45,7 @@ function findModalByCaller(event) {
return null;
}
function handleInfoRequest(event) {
function handleInfoRequest(event: IpcMainInvokeEvent) {
const requestModal = findModalByCaller(event);
if (requestModal) {
return requestModal.handleInfoRequest();
@@ -53,12 +54,11 @@ function handleInfoRequest(event) {
}
export function showModal() {
let noWindow;
const withDevTools = process.env.MM_DEBUG_MODALS || false;
modalQueue.forEach((modal, index) => {
if (index === 0) {
WindowManager.sendToRenderer(MODAL_OPEN);
modal.show(noWindow, withDevTools);
modal.show(undefined, Boolean(withDevTools));
} else {
WindowManager.sendToRenderer(MODAL_CLOSE);
modal.hide();
@@ -66,7 +66,7 @@ export function showModal() {
});
}
function handleModalResult(event, data) {
function handleModalResult(event: IpcMainEvent, data: unknown) {
const requestModal = findModalByCaller(event);
if (requestModal) {
requestModal.resolve(data);
@@ -80,7 +80,7 @@ function handleModalResult(event, data) {
}
}
function handleModalCancel(event, data) {
function handleModalCancel(event: IpcMainEvent, data: unknown) {
const requestModal = findModalByCaller(event);
if (requestModal) {
requestModal.reject(data);

View File

@@ -1,18 +1,31 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView} from 'electron';
import {BrowserView, BrowserWindow} from 'electron';
import log from 'electron-log';
import ContextMenu from '../contextMenu';
import {getWindowBoundaries} from '../utils';
const ACTIVE = 'active';
const SHOWING = 'showing';
const DONE = 'done';
enum Status {
ACTIVE,
SHOWING,
DONE
}
export class ModalView {
constructor(key, html, preload, data, onResolve, onReject, currentWindow) {
export class ModalView<T, T2> {
key: string;
html: string;
data: T;
view: BrowserView;
onReject: (value: T2) => void;
onResolve: (value: T2) => void;
window: BrowserWindow;
windowAttached?: BrowserWindow;
status: Status;
contextMenu: ContextMenu;
constructor(key: string, html: string, preload: string, data: T, onResolve: (value: T2) => void, onReject: (value: T2) => void, currentWindow: BrowserWindow) {
this.key = key;
this.html = html;
this.data = data;
@@ -26,8 +39,8 @@ export class ModalView {
this.onReject = onReject;
this.onResolve = onResolve;
this.window = currentWindow;
this.windowAttached = null;
this.status = ACTIVE;
this.status = Status.ACTIVE;
try {
this.view.webContents.loadURL(this.html);
} catch (e) {
@@ -38,7 +51,7 @@ export class ModalView {
this.contextMenu = new ContextMenu({}, this.view);
}
show = (win, withDevTools) => {
show = (win?: BrowserWindow, withDevTools?: boolean) => {
if (this.windowAttached) {
// we'll reatach
this.windowAttached.removeBrowserView(this.view);
@@ -53,7 +66,7 @@ export class ModalView {
horizontal: true,
vertical: true,
});
this.status = SHOWING;
this.status = Status.SHOWING;
if (this.view.webContents.isLoading()) {
this.view.webContents.once('did-finish-load', () => {
this.view.webContents.focus();
@@ -77,10 +90,12 @@ export class ModalView {
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.view.webContents.destroy();
this.windowAttached = null;
this.status = ACTIVE;
delete this.windowAttached;
this.status = Status.ACTIVE;
}
}
@@ -88,21 +103,21 @@ export class ModalView {
return this.data;
}
reject = (data) => {
reject = (data: T2) => {
if (this.onReject) {
this.onReject(data);
}
this.hide();
this.status = DONE;
this.status = Status.DONE;
}
resolve = (data) => {
resolve = (data: T2) => {
if (this.onResolve) {
this.onResolve(data);
}
this.hide();
this.status = DONE;
this.status = Status.DONE;
}
isActive = () => this.status !== DONE;
isActive = () => this.status !== Status.DONE;
}

View File

@@ -1,7 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import log from 'electron-log';
import {BrowserView, dialog} from 'electron';
import {BrowserView, BrowserWindow, dialog} from 'electron';
import {BrowserViewConstructorOptions} from 'electron/main';
import {CombinedConfig, Team} from 'types/config';
import {SECOND} from 'common/utils/constants';
import {
@@ -26,16 +29,23 @@ const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 36;
export class ViewManager {
constructor(config, mainWindow) {
configServers: Team[];
viewOptions: BrowserViewConstructorOptions;
views: Map<string, MattermostView>;
currentView?: string;
urlView?: BrowserView;
urlViewCancel?: () => void;
mainWindow: BrowserWindow;
loadingScreen?: BrowserView;
constructor(config: CombinedConfig, mainWindow: BrowserWindow) {
this.configServers = config.teams;
this.viewOptions = {spellcheck: config.useSpellChecker};
this.viewOptions = {webPreferences: {spellcheck: config.useSpellChecker}};
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
this.currentView = null;
this.urlView = null;
this.mainWindow = mainWindow;
}
updateMainWindow = (mainWindow) => {
updateMainWindow = (mainWindow: BrowserWindow) => {
this.mainWindow = mainWindow;
}
@@ -43,7 +53,7 @@ export class ViewManager {
return this.configServers;
}
loadServer = (server) => {
loadServer = (server: Team) => {
const srv = new MattermostServer(server.name, server.url);
const view = new MattermostView(srv, this.mainWindow, this.viewOptions);
this.views.set(server.name, view);
@@ -61,7 +71,7 @@ export class ViewManager {
this.configServers.forEach((server) => this.loadServer(server));
}
reloadConfiguration = (configServers) => {
reloadConfiguration = (configServers: Team[]) => {
this.configServers = configServers.concat();
const oldviews = this.views;
this.views = new Map();
@@ -72,11 +82,11 @@ export class ViewManager {
if (recycle && recycle.isVisible) {
setFocus = recycle.name;
}
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url).toString()) {
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) {
oldviews.delete(recycle.name);
this.views.set(recycle.name, recycle);
} else {
this.loadServer(server, this.mainWindow);
this.loadServer(server);
}
});
oldviews.forEach((unused) => {
@@ -98,12 +108,12 @@ export class ViewManager {
}
}
showByName = (name) => {
showByName = (name: string) => {
const newView = this.views.get(name);
if (newView.isVisible) {
return;
}
if (newView) {
if (newView.isVisible) {
return;
}
if (this.currentView && this.currentView !== name) {
const previous = this.getCurrentView();
if (previous) {
@@ -116,6 +126,10 @@ export class ViewManager {
this.showLoadingScreen();
}
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name);
if (!serverInfo) {
log.error(`Couldn't find a server in the config with the name ${newView.server.name}`);
return;
}
newView.window.webContents.send(SET_SERVER_KEY, serverInfo.order);
if (newView.isReady()) {
// if view is not ready, the renderer will have something to display instead.
@@ -148,18 +162,22 @@ export class ViewManager {
view.focus();
}
}
activateView = (viewName) => {
activateView = (viewName: string) => {
if (this.currentView === viewName) {
this.showByName(this.currentView);
}
const view = this.views.get(viewName);
if (!view) {
log.error(`Couldn't find a view with the name ${viewName}`);
return;
}
addWebContentsEventListeners(view, this.getServers);
}
finishLoading = (server) => {
finishLoading = (server: string) => {
const view = this.views.get(server);
if (view && this.getCurrentView() === view) {
this.showByName(this.currentView);
this.showByName(this.currentView!);
this.fadeLoadingScreen();
}
}
@@ -169,19 +187,23 @@ export class ViewManager {
}
getCurrentView() {
return this.views.get(this.currentView);
if (this.currentView) {
return this.views.get(this.currentView);
}
return undefined;
}
openViewDevTools = () => {
const view = this.getCurrentView();
if (view) {
view.openDevTools({mode: 'detach'});
view.openDevTools();
} else {
log.error(`couldn't find ${this.currentView}`);
}
}
findByWebContent(webContentId) {
findByWebContent(webContentId: number) {
let found = null;
let serverName;
let view;
@@ -198,7 +220,7 @@ export class ViewManager {
return found;
}
showURLView = (url) => {
showURLView = (url: URL | string) => {
if (this.urlViewCancel) {
this.urlViewCancel();
}
@@ -213,9 +235,8 @@ export class ViewManager {
const query = new Map([['url', urlString]]);
const localURL = getLocalURLString('urlView.html', query);
urlView.webContents.loadURL(localURL);
const currentWindow = this.getCurrentView().window;
currentWindow.addBrowserView(urlView);
const boundaries = currentWindow.getBounds();
this.mainWindow.addBrowserView(urlView);
const boundaries = this.mainWindow.getBounds();
urlView.setBounds({
x: 0,
y: boundaries.height - URL_VIEW_HEIGHT,
@@ -224,11 +245,13 @@ export class ViewManager {
});
const hideView = () => {
this.urlViewCancel = null;
currentWindow.removeBrowserView(urlView);
delete this.urlViewCancel;
this.mainWindow.removeBrowserView(urlView);
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
urlView.webContents.destroy();
};
@@ -263,12 +286,12 @@ export class ViewManager {
this.createLoadingScreen();
}
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen)) {
this.mainWindow.setTopBrowserView(this.loadingScreen);
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
this.mainWindow.setTopBrowserView(this.loadingScreen!);
} else {
this.mainWindow.addBrowserView(this.loadingScreen);
this.mainWindow.addBrowserView(this.loadingScreen!);
}
this.setLoadingScreenBounds();
@@ -286,7 +309,7 @@ export class ViewManager {
}
}
setServerInitialized = (server) => {
setServerInitialized = (server: string) => {
const view = this.views.get(server);
if (view) {
view.setInitialized();
@@ -296,30 +319,40 @@ export class ViewManager {
}
}
updateLoadingScreenDarkMode = (darkMode) => {
updateLoadingScreenDarkMode = (darkMode: boolean) => {
if (this.loadingScreen) {
this.loadingScreen.webContents.send(GET_LOADING_SCREEN_DATA, {darkMode});
}
}
deeplinkSuccess = (serverName) => {
deeplinkSuccess = (serverName: string) => {
const view = this.views.get(serverName);
if (!view) {
return;
}
this.showByName(serverName);
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
};
deeplinkFailed = (serverName, err, url) => {
const view = this.views.get(serverName);
deeplinkFailed = (serverName: string, err: string, url: string) => {
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`);
const view = this.views.get(serverName);
if (!view) {
return;
}
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
}
handleDeepLink = (url) => {
handleDeepLink = (url: string | URL) => {
if (url) {
const parsedURL = urlUtils.parseURL(url);
const parsedURL = urlUtils.parseURL(url)!;
const server = urlUtils.getServer(parsedURL, this.configServers, true);
if (server) {
const view = this.views.get(server.name);
if (!view) {
log.error(`Couldn't find a view matching the name ${server.name}`);
return;
}
// attempting to change parsedURL protocol results in it not being modified.
const urlWithSchema = `${view.server.url.origin}${parsedURL.pathname}${parsedURL.search}`;
@@ -333,7 +366,7 @@ export class ViewManager {
}
};
sendToAllViews = (channel, ...args) => {
sendToAllViews = (channel: string, ...args: any[]) => {
this.views.forEach((view) => view.view.webContents.send(channel, ...args));
}
}

View File

@@ -1,12 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserWindow, shell} from 'electron';
import {BrowserWindow, shell, WebContents} from 'electron';
import log from 'electron-log';
import {DEVELOPMENT, PRODUCTION} from 'common/utils/constants';
import {Team} from 'types/config';
import urlUtils from 'common/utils/url';
import Utils from 'common/utils/util';
import * as WindowManager from '../windows/windowManager';
@@ -15,26 +15,32 @@ import {protocols} from '../../../electron-builder.json';
import allowProtocolDialog from '../allowProtocolDialog';
import {composeUserAgent} from '../utils';
const customLogins = {};
const listeners = {};
let popupWindow = null;
import {MattermostView} from './MattermostView';
function isTrustedPopupWindow(webContents) {
type CustomLogin = {
inProgress: boolean;
}
const customLogins: Record<number, CustomLogin> = {};
const listeners: Record<number, () => void> = {};
let popupWindow: BrowserWindow | undefined;
function isTrustedPopupWindow(webContents: WebContents) {
if (!webContents) {
return false;
}
if (!popupWindow) {
return false;
}
return Utils.browserWindowFromWebContents(webContents) === popupWindow;
return BrowserWindow.fromWebContents(webContents) === popupWindow;
}
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
const generateWillNavigate = (getServersFunction) => {
return (event, url) => {
const generateWillNavigate = (getServersFunction: () => Team[]) => {
return (event: Event & {sender: WebContents}, url: string) => {
const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url);
const parsedURL = urlUtils.parseURL(url)!;
const configServers = getServersFunction();
const server = urlUtils.getServer(parsedURL, configServers);
@@ -42,7 +48,7 @@ const generateWillNavigate = (getServersFunction) => {
return;
}
if (urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
return;
}
if (parsedURL.protocol === 'mailto:') {
@@ -51,30 +57,24 @@ const generateWillNavigate = (getServersFunction) => {
if (customLogins[contentID].inProgress) {
return;
}
const mode = Utils.runMode();
if (((mode === DEVELOPMENT || mode === PRODUCTION) &&
(parsedURL.path === 'renderer/index.html' || parsedURL.path === 'renderer/settings.html'))) {
log.info('loading settings page');
return;
}
log.info(`Prevented desktop from navigating to: ${url}`);
event.preventDefault();
};
};
const generateDidStartNavigation = (getServersFunction) => {
return (event, url) => {
const generateDidStartNavigation = (getServersFunction: () => Team[]) => {
return (event: Event & {sender: WebContents}, url: string) => {
const serverList = getServersFunction();
const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url);
const parsedURL = urlUtils.parseURL(url)!;
const server = urlUtils.getServer(parsedURL, serverList);
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
return;
}
if (urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
customLogins[contentID].inProgress = true;
} else if (customLogins[contentID].inProgress) {
customLogins[contentID].inProgress = false;
@@ -82,8 +82,8 @@ const generateDidStartNavigation = (getServersFunction) => {
};
};
const generateNewWindowListener = (getServersFunction, spellcheck) => {
return (event, url) => {
const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?: boolean) => {
return (event: Event, url: string) => {
const parsedURL = urlUtils.parseURL(url);
if (!parsedURL) {
event.preventDefault();
@@ -146,14 +146,14 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
log.info(`${url} is an admin console page, preventing to open a new window`);
return;
}
if (popupWindow && !popupWindow.closed && popupWindow.getURL() === url) {
if (popupWindow && popupWindow.webContents.getURL() === url) {
log.info(`Popup window already open at provided url: ${url}`);
return;
}
// TODO: move popups to its own and have more than one.
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
if (!popupWindow || popupWindow.closed) {
if (!popupWindow) {
popupWindow = new BrowserWindow({
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
//parent: WindowManager.getMainWindow(),
@@ -167,10 +167,10 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
},
});
popupWindow.once('ready-to-show', () => {
popupWindow.show();
popupWindow!.show();
});
popupWindow.once('closed', () => {
popupWindow = null;
popupWindow = undefined;
});
}
@@ -187,13 +187,13 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
};
};
export const removeWebContentsListeners = (id) => {
export const removeWebContentsListeners = (id: number) => {
if (listeners[id]) {
listeners[id]();
}
};
export const addWebContentsEventListeners = (mmview, getServersFunction) => {
export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => Team[]) => {
const contents = mmview.view.webContents;
// initialize custom login tracking
@@ -206,7 +206,7 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
}
const willNavigate = generateWillNavigate(getServersFunction);
contents.on('will-navigate', willNavigate);
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // TODO: Electron types don't include sender for some reason
// handle custom login requests (oath, saml):
// 1. are we navigating to a supported local custom login path from the `/login` page?
@@ -214,9 +214,9 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
// 2. are we finished with the custom login process?
// - indicate custom login is NOT in progress
const didStartNavigation = generateDidStartNavigation(getServersFunction);
contents.on('did-start-navigation', didStartNavigation);
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
const spellcheck = mmview.options.webPreferences.spellcheck;
const spellcheck = mmview.options.webPreferences?.spellcheck;
const newWindow = generateNewWindowListener(getServersFunction, spellcheck);
contents.on('new-window', newWindow);
@@ -227,8 +227,8 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
const removeListeners = () => {
try {
contents.removeListener('will-navigate', willNavigate);
contents.removeListener('did-start-navigation', didStartNavigation);
contents.removeListener('will-navigate', willNavigate as (e: Event, u: string) => void);
contents.removeListener('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
contents.removeListener('new-window', newWindow);
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);

View File

@@ -6,19 +6,24 @@ import fs from 'fs';
import path from 'path';
import os from 'os';
import {app, BrowserWindow, ipcMain} from 'electron';
import {app, BrowserWindow, BrowserWindowConstructorOptions, ipcMain} from 'electron';
import log from 'electron-log';
import {CombinedConfig} from 'types/config';
import {SavedWindowState} from 'types/mainWindow';
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS} from 'common/communication';
import * as Validator from '../Validator';
import ContextMenu from '../contextMenu';
import {getLocalPreload, getLocalURLString} from '../utils';
function saveWindowState(file, window) {
const windowState = window.getBounds();
windowState.maximized = window.isMaximized();
windowState.fullscreen = window.isFullScreen();
function saveWindowState(file: string, window: BrowserWindow) {
const windowState: SavedWindowState = {
...window.getBounds(),
maximized: window.isMaximized(),
fullscreen: window.isFullScreen(),
};
try {
fs.writeFileSync(file, JSON.stringify(windowState));
} catch (e) {
@@ -31,7 +36,7 @@ function isFramelessWindow() {
return os.platform() === 'darwin' || (os.platform() === 'win32' && os.release().startsWith('10'));
}
function createMainWindow(config, options) {
function createMainWindow(config: CombinedConfig, options: {linuxAppIcon: string}) {
const defaultWindowWidth = 1000;
const defaultWindowHeight = 700;
const minimumWindowWidth = 400;
@@ -40,26 +45,23 @@ function createMainWindow(config, options) {
// Create the browser window.
const preload = getLocalPreload('mainWindow.js');
const boundsInfoPath = path.join(app.getPath('userData'), 'bounds-info.json');
let windowOptions;
let savedWindowState;
try {
windowOptions = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
windowOptions = Validator.validateBoundsInfo(windowOptions);
if (!windowOptions) {
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
savedWindowState = Validator.validateBoundsInfo(savedWindowState);
if (!savedWindowState) {
throw new Error('Provided bounds info file does not validate, using defaults instead.');
}
} catch (e) {
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
windowOptions = {width: defaultWindowWidth, height: defaultWindowHeight};
savedWindowState = {width: defaultWindowWidth, height: defaultWindowHeight};
}
const {maximized: windowIsMaximized} = windowOptions;
const {maximized: windowIsMaximized} = savedWindowState;
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
if (process.platform === 'linux') {
windowOptions.icon = options.linuxAppIcon;
}
Object.assign(windowOptions, {
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, savedWindowState, {
title: app.name,
fullscreenable: true,
show: false, // don't start the window until it is ready and only if it isn't hidden
@@ -67,8 +69,8 @@ function createMainWindow(config, options) {
minWidth: minimumWindowWidth,
minHeight: minimumWindowHeight,
frame: !isFramelessWindow(),
fullscreen: windowOptions.fullscreen,
titleBarStyle: 'hidden',
fullscreen: savedWindowState.fullscreen,
titleBarStyle: 'hidden' as const,
trafficLightPosition: {x: 12, y: 24},
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
webPreferences: {
@@ -81,10 +83,18 @@ function createMainWindow(config, options) {
},
});
if (process.platform === 'linux') {
windowOptions.icon = options.linuxAppIcon;
}
const mainWindow = new BrowserWindow(windowOptions);
mainWindow.setMenuBarVisibility(false);
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => mainWindow.isFullScreen());
try {
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => mainWindow.isFullScreen());
} catch (e) {
log.error('Tried to register second handler, skipping');
}
const localURL = getLocalURLString('index.html');
mainWindow.loadURL(localURL).catch(
@@ -122,7 +132,7 @@ function createMainWindow(config, options) {
saveWindowState(boundsInfoPath, mainWindow);
} else { // Minimize or hide the window for close button.
event.preventDefault();
function hideWindow(window) {
function hideWindow(window: BrowserWindow) {
window.blur(); // To move focus to the next top-level window in Windows
window.hide();
}

View File

@@ -3,15 +3,15 @@
import {BrowserWindow} from 'electron';
import log from 'electron-log';
import {CombinedConfig} from 'types/config';
import ContextMenu from '../contextMenu';
import {getLocalPreload, getLocalURLString} from '../utils';
export function createSettingsWindow(mainWindow, config, withDevTools) {
export function createSettingsWindow(mainWindow: BrowserWindow, config: CombinedConfig, withDevTools: boolean) {
const preload = getLocalPreload('mainWindow.js');
const spellcheck = (typeof config.useSpellChecker === 'undefined' ? true : config.useSpellChecker);
const settingsWindow = new BrowserWindow({
...config.data,
parent: mainWindow,
title: 'Desktop App Settings',
fullscreen: false,

View File

@@ -2,28 +2,32 @@
// See LICENSE.txt for license information.
import path from 'path';
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain} from 'electron';
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent} from 'electron';
import log from 'electron-log';
import {CombinedConfig} from 'types/config';
import {MAXIMIZE_CHANGE, HISTORY, GET_LOADING_SCREEN_DATA, REACT_APP_INITIALIZED, LOADING_SCREEN_ANIMATION_FINISHED, FOCUS_THREE_DOT_MENU} from 'common/communication';
import urlUtils from 'common/utils/url';
import {getAdjustedWindowBoundaries} from '../utils';
import {ViewManager} from '../views/viewManager';
import {CriticalErrorHandler} from '../CriticalErrorHandler';
import CriticalErrorHandler from '../CriticalErrorHandler';
import {createSettingsWindow} from './settingsWindow';
import createMainWindow from './mainWindow';
// singleton module to manage application's windows
const status = {
mainWindow: null,
settingsWindow: null,
config: null,
viewManager: null,
type WindowManagerStatus = {
mainWindow?: BrowserWindow;
settingsWindow?: BrowserWindow;
config?: CombinedConfig;
viewManager?: ViewManager;
};
const status: WindowManagerStatus = {};
const assetsDir = path.resolve(app.getAppPath(), 'assets');
ipcMain.on(HISTORY, handleHistory);
@@ -31,12 +35,12 @@ ipcMain.handle(GET_LOADING_SCREEN_DATA, handleLoadingScreenDataRequest);
ipcMain.on(REACT_APP_INITIALIZED, handleReactAppInitialized);
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, handleLoadingScreenAnimationFinished);
export function setConfig(data) {
export function setConfig(data: CombinedConfig) {
if (data) {
status.config = data;
}
if (status.viewManager) {
status.viewManager.reloadConfiguration(status.config.teams);
if (status.viewManager && status.config) {
status.viewManager.reloadConfiguration(status.config.teams || []);
}
}
@@ -47,17 +51,20 @@ export function showSettingsWindow() {
if (!status.mainWindow) {
showMainWindow();
}
const withDevTools = process.env.MM_DEBUG_SETTINGS || false;
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
status.settingsWindow = createSettingsWindow(status.mainWindow, status.config, withDevTools);
if (!status.config) {
return;
}
status.settingsWindow = createSettingsWindow(status.mainWindow!, status.config, withDevTools);
status.settingsWindow.on('closed', () => {
status.settingsWindow = null;
delete status.settingsWindow;
focusBrowserView();
});
}
}
export function showMainWindow(deeplinkingURL) {
export function showMainWindow(deeplinkingURL?: string | URL) {
if (status.mainWindow) {
if (status.mainWindow.isVisible()) {
status.mainWindow.focus();
@@ -65,6 +72,9 @@ export function showMainWindow(deeplinkingURL) {
status.mainWindow.show();
}
} else {
if (!status.config) {
return;
}
status.mainWindow = createMainWindow(status.config, {
linuxAppIcon: path.join(assetsDir, 'linux', 'app_icon.png'),
});
@@ -77,14 +87,13 @@ export function showMainWindow(deeplinkingURL) {
// window handlers
status.mainWindow.on('closed', () => {
log.warn('main window closed');
status.mainWindow = null;
delete status.mainWindow;
});
status.mainWindow.on('unresponsive', () => {
const criticalErrorHandler = new CriticalErrorHandler();
criticalErrorHandler.setMainWindow(status.mainWindow);
criticalErrorHandler.setMainWindow(status.mainWindow!);
criticalErrorHandler.windowUnresponsiveHandler();
});
status.mainWindow.on('crashed', handleMainWindowWebContentsCrashed);
status.mainWindow.on('maximize', handleMaximizeMainWindow);
status.mainWindow.on('unmaximize', handleUnmaximizeMainWindow);
status.mainWindow.on('resize', handleResizeMainWindow);
@@ -103,24 +112,18 @@ export function showMainWindow(deeplinkingURL) {
initializeViewManager();
if (deeplinkingURL) {
status.viewManager.handleDeepLink(deeplinkingURL);
status.viewManager!.handleDeepLink(deeplinkingURL);
}
}
export function getMainWindow(ensureCreated) {
if (ensureCreated && status.mainWindow === null) {
export function getMainWindow(ensureCreated?: boolean) {
if (ensureCreated && !status.mainWindow) {
showMainWindow();
}
return status.mainWindow;
}
export function on(event, listener) {
return status.mainWindow.on(event, listener);
}
function handleMainWindowWebContentsCrashed() {
throw new Error('webContents \'crashed\' event has been emitted');
}
export const on = status.mainWindow?.on;
function handleMaximizeMainWindow() {
sendToRenderer(MAXIMIZE_CHANGE, true);
@@ -131,8 +134,11 @@ function handleUnmaximizeMainWindow() {
}
function handleResizeMainWindow() {
if (!(status.viewManager && status.mainWindow)) {
return;
}
const currentView = status.viewManager.getCurrentView();
let bounds;
let bounds: Partial<Electron.Rectangle>;
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
// https://github.com/electron/electron/issues/28699
@@ -146,7 +152,7 @@ function handleResizeMainWindow() {
const setBoundsFunction = () => {
if (currentView) {
currentView.setBounds(getAdjustedWindowBoundaries(bounds.width, bounds.height, !urlUtils.isTeamUrl(currentView.server.url, currentView.view.webContents.getURL())));
currentView.setBounds(getAdjustedWindowBoundaries(bounds.width!, bounds.height!, !urlUtils.isTeamUrl(currentView.server.url, currentView.view.webContents.getURL())));
}
};
@@ -160,17 +166,17 @@ function handleResizeMainWindow() {
status.viewManager.setLoadingScreenBounds();
}
export function sendToRenderer(channel, ...args) {
export function sendToRenderer(channel: string, ...args: any[]) {
if (!status.mainWindow) {
showMainWindow();
}
status.mainWindow.webContents.send(channel, ...args);
status.mainWindow!.webContents.send(channel, ...args);
if (status.settingsWindow && status.settingsWindow.isVisible()) {
status.settingsWindow.webContents.send(channel, ...args);
}
}
export function sendToAll(channel, ...args) {
export function sendToAll(channel: string, ...args: any[]) {
sendToRenderer(channel, ...args);
if (status.settingsWindow) {
status.settingsWindow.webContents.send(channel, ...args);
@@ -179,7 +185,7 @@ export function sendToAll(channel, ...args) {
// TODO: should we include popups?
}
export function sendToMattermostViews(channel, ...args) {
export function sendToMattermostViews(channel: string, ...args: any[]) {
if (status.viewManager) {
status.viewManager.sendToAllViews(channel, ...args);
}
@@ -190,16 +196,16 @@ export function restoreMain() {
if (!status.mainWindow) {
showMainWindow();
}
if (!status.mainWindow.isVisible() || status.mainWindow.isMinimized()) {
if (status.mainWindow.isMinimized()) {
status.mainWindow.restore();
if (!status.mainWindow!.isVisible() || status.mainWindow!.isMinimized()) {
if (status.mainWindow!.isMinimized()) {
status.mainWindow!.restore();
} else {
status.mainWindow.show();
status.mainWindow!.show();
}
if (status.settingsWindow) {
status.settingsWindow.focus();
} else {
status.mainWindow.focus();
status.mainWindow!.focus();
}
if (process.platform === 'darwin') {
app.dock.show();
@@ -207,31 +213,36 @@ export function restoreMain() {
} else if (status.settingsWindow) {
status.settingsWindow.focus();
} else {
status.mainWindow.focus();
status.mainWindow!.focus();
}
}
export function flashFrame(flash) {
export function flashFrame(flash: boolean) {
if (process.platform === 'linux' || process.platform === 'win32') {
status.mainWindow.flashFrame(flash);
status.mainWindow?.flashFrame(flash);
if (status.settingsWindow) {
// main might be hidden behind the settings
status.settingsWindow.flashFrame(flash);
}
}
if (process.platform === 'darwin' && status.config.notifications.bounceIcon) {
app.dock.bounce(status.config.notifications.bounceIconType);
if (process.platform === 'darwin' && status.config?.notifications.bounceIcon) {
app.dock.bounce(status.config?.notifications.bounceIconType);
}
}
function drawBadge(text, small) {
function drawBadge(text: string, small: boolean) {
const scale = 2; // should rely display dpi
const size = (small ? 20 : 16) * scale;
const canvas = document.createElement('canvas');
canvas.setAttribute('width', size);
canvas.setAttribute('height', size);
canvas.setAttribute('width', `${size}`);
canvas.setAttribute('height', `${size}`);
const ctx = canvas.getContext('2d');
if (!ctx) {
log.error('Could not create canvas context');
return null;
}
// circle
ctx.fillStyle = '#FF1744'; // Material Red A400
ctx.beginPath();
@@ -248,7 +259,7 @@ function drawBadge(text, small) {
return canvas.toDataURL();
}
function createDataURL(text, small) {
function createDataURL(text: string, small: boolean) {
const win = status.mainWindow;
if (!win) {
return null;
@@ -263,26 +274,28 @@ function createDataURL(text, small) {
return win.webContents.executeJavaScript(code);
}
export async function setOverlayIcon(badgeText, description, small) {
export async function setOverlayIcon(badgeText: string | undefined, description: string, small: boolean) {
if (process.platform === 'win32') {
let overlay = null;
if (status.mainWindow && badgeText) {
try {
const dataUrl = await createDataURL(badgeText, small);
overlay = nativeImage.createFromDataURL(dataUrl);
} catch (err) {
log.error(`Couldn't generate a badge: ${err}`);
if (status.mainWindow) {
if (badgeText) {
try {
const dataUrl = await createDataURL(badgeText, small);
overlay = nativeImage.createFromDataURL(dataUrl);
} catch (err) {
log.error(`Couldn't generate a badge: ${err}`);
}
}
status.mainWindow.setOverlayIcon(overlay, description);
}
status.mainWindow.setOverlayIcon(overlay, description);
}
}
export function isMainWindow(window) {
export function isMainWindow(window: BrowserWindow) {
return status.mainWindow && status.mainWindow === window;
}
export function handleDoubleClick(e, windowType) {
export function handleDoubleClick(e: IpcMainEvent, windowType?: string) {
let action = 'Maximize';
if (process.platform === 'darwin') {
action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
@@ -311,16 +324,16 @@ export function handleDoubleClick(e, windowType) {
}
function initializeViewManager() {
if (!status.viewManager) {
if (!status.viewManager && status.config && status.mainWindow) {
status.viewManager = new ViewManager(status.config, status.mainWindow);
status.viewManager.load();
status.viewManager.showInitial();
}
}
export function switchServer(serverName) {
export function switchServer(serverName: string) {
showMainWindow();
status.viewManager.showByName(serverName);
status.viewManager?.showByName(serverName);
}
export function focusBrowserView() {
@@ -346,11 +359,11 @@ export function focusThreeDotMenu() {
function handleLoadingScreenDataRequest() {
return {
darkMode: status.config.darkMode,
darkMode: status.config?.darkMode || false,
};
}
function handleReactAppInitialized(_, server) {
function handleReactAppInitialized(e: IpcMainEvent, server: string) {
if (status.viewManager) {
status.viewManager.setServerInitialized(server);
}
@@ -362,27 +375,19 @@ function handleLoadingScreenAnimationFinished() {
}
}
export function updateLoadingScreenDarkMode(darkMode) {
export function updateLoadingScreenDarkMode(darkMode: boolean) {
if (status.viewManager) {
status.viewManager.updateLoadingScreenDarkMode(darkMode);
}
}
export function getServerNameByWebContentsId(webContentsId) {
if (status.viewManager) {
return status.viewManager.findByWebContent(webContentsId);
}
return null;
export function getServerNameByWebContentsId(webContentsId: number) {
return status.viewManager?.findByWebContent(webContentsId);
}
export function close() {
const focused = BrowserWindow.getFocusedWindow();
if (focused.id === status.mainWindow.id) {
// TODO: figure out logic for closing
focused.close();
} else {
focused.close();
}
focused?.close();
}
export function maximize() {
const focused = BrowserWindow.getFocusedWindow();
@@ -404,21 +409,21 @@ export function restore() {
}
export function reload() {
const currentView = status.viewManager.getCurrentView();
const currentView = status.viewManager?.getCurrentView();
if (currentView) {
status.viewManager.showLoadingScreen();
status.viewManager?.showLoadingScreen();
currentView.reload();
}
}
export function sendToFind() {
const currentView = status.viewManager.getCurrentView();
const currentView = status.viewManager?.getCurrentView();
if (currentView) {
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: ['CmdOrCtrl', 'Shift']});
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
}
}
export function handleHistory(event, offset) {
export function handleHistory(event: IpcMainEvent, offset: number) {
if (status.viewManager) {
const activeView = status.viewManager.getCurrentView();
if (activeView && activeView.view.webContents.canGoToOffset(offset)) {