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

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