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:
@@ -1,7 +1,8 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView, app, ipcMain} from 'electron';
|
||||
import {BrowserView, app, ipcMain, BrowserWindow} from 'electron';
|
||||
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {EventEmitter} from 'events';
|
||||
@@ -21,6 +22,8 @@ import {
|
||||
LOADSCREEN_END,
|
||||
} from 'common/communication';
|
||||
|
||||
import {MattermostServer} from 'main/MattermostServer';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
@@ -28,49 +31,67 @@ import * as appState from '../appState';
|
||||
|
||||
import {removeWebContentsListeners} from './webContentEvents';
|
||||
|
||||
const READY = 1;
|
||||
const WAITING_MM = 2;
|
||||
const LOADING = 0;
|
||||
const ERROR = -1;
|
||||
enum Status {
|
||||
LOADING,
|
||||
READY,
|
||||
WAITING_MM,
|
||||
ERROR = -1,
|
||||
}
|
||||
|
||||
const ASTERISK_GROUP = 3;
|
||||
const MENTIONS_GROUP = 2;
|
||||
|
||||
export class MattermostView extends EventEmitter {
|
||||
constructor(server, win, options) {
|
||||
server: MattermostServer;
|
||||
window: BrowserWindow;
|
||||
view: BrowserView;
|
||||
isVisible: boolean;
|
||||
options: BrowserViewConstructorOptions;
|
||||
|
||||
removeLoading?: number;
|
||||
|
||||
/**
|
||||
* for backward compatibility when reading the title.
|
||||
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
|
||||
*/
|
||||
usesAsteriskForUnreads?: boolean;
|
||||
|
||||
faviconMemoize: Map<string, boolean>;
|
||||
currentFavicon?: string;
|
||||
isInitialized: boolean;
|
||||
hasBeenShown: boolean;
|
||||
altLastPressed?: boolean;
|
||||
contextMenu: ContextMenu;
|
||||
|
||||
status?: Status;
|
||||
retryLoad?: NodeJS.Timeout;
|
||||
maxRetries: number;
|
||||
|
||||
constructor(server: MattermostServer, win: BrowserWindow, options: BrowserViewConstructorOptions) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.window = win;
|
||||
|
||||
const preload = getLocalPreload('preload.js');
|
||||
const spellcheck = ((!options || typeof options.spellcheck === 'undefined') ? true : options.spellcheck);
|
||||
this.options = {
|
||||
webPreferences: {
|
||||
contextIsolation: process.env.NODE_ENV !== 'test',
|
||||
preload,
|
||||
spellcheck,
|
||||
additionalArguments: [
|
||||
`version=${app.version}`,
|
||||
`version=${app.getVersion()}`,
|
||||
`appName=${app.name}`,
|
||||
],
|
||||
enableRemoteModule: process.env.NODE_ENV === 'test',
|
||||
nodeIntegration: process.env.NODE_ENV === 'test',
|
||||
...options.webPreferences,
|
||||
},
|
||||
...options,
|
||||
};
|
||||
this.isVisible = false;
|
||||
this.view = new BrowserView(this.options);
|
||||
this.removeLoading = null;
|
||||
this.resetLoadingStatus();
|
||||
|
||||
/**
|
||||
* for backward compatibility when reading the title.
|
||||
* null means we have yet to figure out if it uses it or not but we consider it false until proven wrong
|
||||
*/
|
||||
this.usesAsteriskForUnreads = null;
|
||||
|
||||
this.faviconMemoize = new Map();
|
||||
this.currentFavicon = null;
|
||||
log.info(`BrowserView created for server ${this.server.name}`);
|
||||
|
||||
this.isInitialized = false;
|
||||
@@ -82,24 +103,40 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
this.contextMenu = new ContextMenu({}, this.view);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
}
|
||||
|
||||
// use the same name as the server
|
||||
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
|
||||
get name() {
|
||||
return this.server.name;
|
||||
return this.server?.name;
|
||||
}
|
||||
|
||||
resetLoadingStatus = () => {
|
||||
if (this.status !== LOADING) { // if it's already loading, don't touch anything
|
||||
this.retryLoad = null;
|
||||
this.status = LOADING;
|
||||
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything
|
||||
delete this.retryLoad;
|
||||
this.status = Status.LOADING;
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
}
|
||||
}
|
||||
|
||||
load = (someURL) => {
|
||||
const loadURL = (typeof someURL === 'undefined') ? `${this.server.url.toString()}` : urlUtils.parseURL(someURL).toString();
|
||||
load = (someURL?: URL | string) => {
|
||||
if (!this.server) {
|
||||
return;
|
||||
}
|
||||
|
||||
let loadURL: string;
|
||||
if (someURL) {
|
||||
const parsedURL = urlUtils.parseURL(someURL);
|
||||
if (parsedURL) {
|
||||
loadURL = parsedURL.toString();
|
||||
} else {
|
||||
log.error('Cannot parse provided url, using current server url', someURL);
|
||||
loadURL = this.server.url.toString();
|
||||
}
|
||||
} else {
|
||||
loadURL = this.server.url.toString();
|
||||
}
|
||||
log.info(`[${Util.shorten(this.server.name)}] Loading ${loadURL}`);
|
||||
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
@@ -107,7 +144,7 @@ export class MattermostView extends EventEmitter {
|
||||
});
|
||||
}
|
||||
|
||||
retry = (loadURL) => {
|
||||
retry = (loadURL: string) => {
|
||||
return () => {
|
||||
// window was closed while retrying
|
||||
if (!this.view || !this.view.webContents) {
|
||||
@@ -121,43 +158,43 @@ export class MattermostView extends EventEmitter {
|
||||
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.server.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
|
||||
this.status = ERROR;
|
||||
this.status = Status.ERROR;
|
||||
}
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
loadRetry = (loadURL, err) => {
|
||||
loadRetry = (loadURL: string, err: any) => {
|
||||
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
|
||||
WindowManager.sendToRenderer(LOAD_RETRY, this.server.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.server.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
|
||||
}
|
||||
|
||||
loadSuccess = (loadURL) => {
|
||||
loadSuccess = (loadURL: string) => {
|
||||
return () => {
|
||||
log.info(`[${Util.shorten(this.server.name)}] finished loading ${loadURL}`);
|
||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
if (this.status === LOADING) {
|
||||
if (this.status === Status.LOADING) {
|
||||
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
|
||||
this.handleTitleUpdate(null, this.view.webContents.getTitle());
|
||||
this.updateMentionsFromTitle(this.view.webContents.getTitle());
|
||||
this.findUnreadState(null);
|
||||
}
|
||||
this.status = WAITING_MM;
|
||||
this.status = Status.WAITING_MM;
|
||||
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL.toString());
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL);
|
||||
this.view.webContents.send(SET_SERVER_NAME, this.server.name);
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
|
||||
};
|
||||
}
|
||||
|
||||
show = (requestedVisibility) => {
|
||||
show = (requestedVisibility?: boolean) => {
|
||||
this.hasBeenShown = true;
|
||||
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
|
||||
if (request && !this.isVisible) {
|
||||
this.window.addBrowserView(this.view);
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url, this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url, this.view.webContents.getURL()))));
|
||||
if (this.status === READY) {
|
||||
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL()))));
|
||||
if (this.status === Status.READY) {
|
||||
this.focus();
|
||||
}
|
||||
} else if (!request && this.isVisible) {
|
||||
@@ -173,15 +210,12 @@ export class MattermostView extends EventEmitter {
|
||||
|
||||
hide = () => this.show(false);
|
||||
|
||||
setBounds = (boundaries) => {
|
||||
setBounds = (boundaries: Electron.Rectangle) => {
|
||||
// todo: review this, as it might not work properly with devtools/minimizing/resizing
|
||||
this.view.setBounds(boundaries);
|
||||
}
|
||||
|
||||
destroy = () => {
|
||||
if (this.retryLoad) {
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
removeWebContentsListeners(this.view.webContents.id);
|
||||
if (this.window) {
|
||||
this.window.removeBrowserView(this.view);
|
||||
@@ -189,12 +223,17 @@ export class MattermostView extends EventEmitter {
|
||||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.view.webContents.destroy();
|
||||
|
||||
this.window = null;
|
||||
this.server = null;
|
||||
this.isVisible = false;
|
||||
clearTimeout(this.retryLoad);
|
||||
if (this.retryLoad) {
|
||||
clearTimeout(this.retryLoad);
|
||||
}
|
||||
if (this.removeLoading) {
|
||||
clearTimeout(this.removeLoading);
|
||||
}
|
||||
}
|
||||
|
||||
focus = () => {
|
||||
@@ -206,22 +245,22 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
isReady = () => {
|
||||
return this.status !== LOADING;
|
||||
return this.status !== Status.LOADING;
|
||||
}
|
||||
|
||||
needsLoadingScreen = () => {
|
||||
return !(this.status === READY || this.status === ERROR);
|
||||
return !(this.status === Status.READY || this.status === Status.ERROR);
|
||||
}
|
||||
|
||||
setInitialized = (timedout) => {
|
||||
this.status = READY;
|
||||
setInitialized = (timedout?: boolean) => {
|
||||
this.status = Status.READY;
|
||||
|
||||
if (timedout) {
|
||||
log.info(`${this.server.name} timeout expired will show the browserview`);
|
||||
this.emit(LOADSCREEN_END, this.server.name);
|
||||
}
|
||||
clearTimeout(this.removeLoading);
|
||||
this.removeLoading = null;
|
||||
delete this.removeLoading;
|
||||
}
|
||||
|
||||
openDevTools = () => {
|
||||
@@ -229,15 +268,15 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
getWebContents = () => {
|
||||
if (this.status === READY) {
|
||||
if (this.status === Status.READY) {
|
||||
return this.view.webContents;
|
||||
} else if (this.window) {
|
||||
return this.window.webContents; // if it's not ready you are looking at the renderer process
|
||||
}
|
||||
return WindowManager.getMainWindow.webContents;
|
||||
return WindowManager.getMainWindow()?.webContents;
|
||||
}
|
||||
|
||||
handleInputEvents = (_, input) => {
|
||||
handleInputEvents = (_: Event, input: Input) => {
|
||||
// Handler for pressing the Alt key to focus the 3-dot menu
|
||||
if (input.key === 'Alt' && input.type === 'keyUp' && this.altLastPressed) {
|
||||
this.altLastPressed = false;
|
||||
@@ -253,8 +292,8 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleDidNavigate = (event, url) => {
|
||||
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url, url) || urlUtils.isAdminUrl(this.server.url, url);
|
||||
handleDidNavigate = (event: Event, url: string) => {
|
||||
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url || '', url) || urlUtils.isAdminUrl(this.server.url || '', url);
|
||||
if (isUrlTeamUrl) {
|
||||
this.setBounds(getWindowBoundaries(this.window));
|
||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
||||
@@ -266,15 +305,19 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
handleUpdateTarget = (e, url) => {
|
||||
if (!this.server.sameOrigin(url)) {
|
||||
handleUpdateTarget = (e: Event, url: string) => {
|
||||
if (!url || !this.server.sameOrigin(url)) {
|
||||
this.emit(UPDATE_TARGET_URL, url);
|
||||
}
|
||||
}
|
||||
|
||||
titleParser = /(\((\d+)\) )?(\*)?/g
|
||||
|
||||
handleTitleUpdate = (e, title) => {
|
||||
handleTitleUpdate = (e: Event, title: string) => {
|
||||
this.updateMentionsFromTitle(title);
|
||||
}
|
||||
|
||||
updateMentionsFromTitle = (title: string) => {
|
||||
//const title = this.view.webContents.getTitle();
|
||||
const resultsIterator = title.matchAll(this.titleParser);
|
||||
const results = resultsIterator.next(); // we are only interested in the first set
|
||||
@@ -293,13 +336,13 @@ export class MattermostView extends EventEmitter {
|
||||
appState.updateMentions(this.server.name, mentions, unreads);
|
||||
}
|
||||
|
||||
handleFaviconUpdate = (e, favicons) => {
|
||||
handleFaviconUpdate = (e: Event, favicons: string[]) => {
|
||||
if (!this.usesAsteriskForUnreads) {
|
||||
// if unread state is stored for that favicon, retrieve value.
|
||||
// if not, get related info from preload and store it for future changes
|
||||
this.currentFavicon = favicons[0];
|
||||
if (this.faviconMemoize.has(favicons[0])) {
|
||||
appState.updateUnreads(this.server.name, this.faviconMemoize.get(favicons[0]));
|
||||
appState.updateUnreads(this.server.name, Boolean(this.faviconMemoize.get(favicons[0])));
|
||||
} else {
|
||||
this.findUnreadState(favicons[0]);
|
||||
}
|
||||
@@ -307,7 +350,7 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
// if favicon is null, it will affect appState, but won't be memoized
|
||||
findUnreadState = (favicon) => {
|
||||
findUnreadState = (favicon: string | null) => {
|
||||
try {
|
||||
this.view.webContents.send(IS_UNREAD, favicon, this.server.name);
|
||||
} catch (err) {
|
||||
@@ -318,12 +361,12 @@ export class MattermostView extends EventEmitter {
|
||||
|
||||
// if favicon is null, it means it is the initial load,
|
||||
// so don't memoize as we don't have the favicons and there is no rush to find out.
|
||||
handleFaviconIsUnread = (e, favicon, serverName, result) => {
|
||||
handleFaviconIsUnread = (e: Event, favicon: string, serverName: string, result: boolean) => {
|
||||
if (this.server && serverName === this.server.name) {
|
||||
if (favicon) {
|
||||
this.faviconMemoize.set(favicon, result);
|
||||
}
|
||||
if (favicon === null || favicon === this.currentFavicon) {
|
||||
if (!favicon || favicon === this.currentFavicon) {
|
||||
appState.updateUnreads(serverName, result);
|
||||
}
|
||||
}
|
@@ -1,23 +1,24 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {ipcMain} from 'electron';
|
||||
import {BrowserWindow, ipcMain} from 'electron';
|
||||
import {IpcMainEvent, IpcMainInvokeEvent} from 'electron/main';
|
||||
|
||||
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication.js';
|
||||
import {RETRIEVE_MODAL_INFO, MODAL_CANCEL, MODAL_RESULT, MODAL_OPEN, MODAL_CLOSE} from 'common/communication';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
import {ModalView} from './modalView';
|
||||
|
||||
let modalQueue = [];
|
||||
let modalQueue: Array<ModalView<any, any>> = [];
|
||||
|
||||
// TODO: add a queue/add differentiation, in case we need to put a modal first in line
|
||||
// should we return the original promise if called multiple times with the same key?
|
||||
export function addModal(key, html, preload, data, win) {
|
||||
export function addModal<T, T2>(key: string, html: string, preload: string, data: T, win: BrowserWindow) {
|
||||
const foundModal = modalQueue.find((modal) => modal.key === key);
|
||||
if (!foundModal) {
|
||||
const modalPromise = new Promise((resolve, reject) => {
|
||||
const mv = new ModalView(key, html, preload, data, resolve, reject, win);
|
||||
const modalPromise = new Promise((resolve: (value: T2) => void, reject) => {
|
||||
const mv = new ModalView<T, T2>(key, html, preload, data, resolve, reject, win);
|
||||
modalQueue.push(mv);
|
||||
});
|
||||
|
||||
@@ -34,7 +35,7 @@ ipcMain.handle(RETRIEVE_MODAL_INFO, handleInfoRequest);
|
||||
ipcMain.on(MODAL_RESULT, handleModalResult);
|
||||
ipcMain.on(MODAL_CANCEL, handleModalCancel);
|
||||
|
||||
function findModalByCaller(event) {
|
||||
function findModalByCaller(event: IpcMainInvokeEvent) {
|
||||
if (modalQueue.length) {
|
||||
const requestModal = modalQueue.find((modal) => {
|
||||
return (modal.view && modal.view.webContents && modal.view.webContents.id === event.sender.id);
|
||||
@@ -44,7 +45,7 @@ function findModalByCaller(event) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function handleInfoRequest(event) {
|
||||
function handleInfoRequest(event: IpcMainInvokeEvent) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
return requestModal.handleInfoRequest();
|
||||
@@ -53,12 +54,11 @@ function handleInfoRequest(event) {
|
||||
}
|
||||
|
||||
export function showModal() {
|
||||
let noWindow;
|
||||
const withDevTools = process.env.MM_DEBUG_MODALS || false;
|
||||
modalQueue.forEach((modal, index) => {
|
||||
if (index === 0) {
|
||||
WindowManager.sendToRenderer(MODAL_OPEN);
|
||||
modal.show(noWindow, withDevTools);
|
||||
modal.show(undefined, Boolean(withDevTools));
|
||||
} else {
|
||||
WindowManager.sendToRenderer(MODAL_CLOSE);
|
||||
modal.hide();
|
||||
@@ -66,7 +66,7 @@ export function showModal() {
|
||||
});
|
||||
}
|
||||
|
||||
function handleModalResult(event, data) {
|
||||
function handleModalResult(event: IpcMainEvent, data: unknown) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.resolve(data);
|
||||
@@ -80,7 +80,7 @@ function handleModalResult(event, data) {
|
||||
}
|
||||
}
|
||||
|
||||
function handleModalCancel(event, data) {
|
||||
function handleModalCancel(event: IpcMainEvent, data: unknown) {
|
||||
const requestModal = findModalByCaller(event);
|
||||
if (requestModal) {
|
||||
requestModal.reject(data);
|
@@ -1,18 +1,31 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView} from 'electron';
|
||||
import {BrowserView, BrowserWindow} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries} from '../utils';
|
||||
|
||||
const ACTIVE = 'active';
|
||||
const SHOWING = 'showing';
|
||||
const DONE = 'done';
|
||||
enum Status {
|
||||
ACTIVE,
|
||||
SHOWING,
|
||||
DONE
|
||||
}
|
||||
|
||||
export class ModalView {
|
||||
constructor(key, html, preload, data, onResolve, onReject, currentWindow) {
|
||||
export class ModalView<T, T2> {
|
||||
key: string;
|
||||
html: string;
|
||||
data: T;
|
||||
view: BrowserView;
|
||||
onReject: (value: T2) => void;
|
||||
onResolve: (value: T2) => void;
|
||||
window: BrowserWindow;
|
||||
windowAttached?: BrowserWindow;
|
||||
status: Status;
|
||||
contextMenu: ContextMenu;
|
||||
|
||||
constructor(key: string, html: string, preload: string, data: T, onResolve: (value: T2) => void, onReject: (value: T2) => void, currentWindow: BrowserWindow) {
|
||||
this.key = key;
|
||||
this.html = html;
|
||||
this.data = data;
|
||||
@@ -26,8 +39,8 @@ export class ModalView {
|
||||
this.onReject = onReject;
|
||||
this.onResolve = onResolve;
|
||||
this.window = currentWindow;
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
|
||||
this.status = Status.ACTIVE;
|
||||
try {
|
||||
this.view.webContents.loadURL(this.html);
|
||||
} catch (e) {
|
||||
@@ -38,7 +51,7 @@ export class ModalView {
|
||||
this.contextMenu = new ContextMenu({}, this.view);
|
||||
}
|
||||
|
||||
show = (win, withDevTools) => {
|
||||
show = (win?: BrowserWindow, withDevTools?: boolean) => {
|
||||
if (this.windowAttached) {
|
||||
// we'll reatach
|
||||
this.windowAttached.removeBrowserView(this.view);
|
||||
@@ -53,7 +66,7 @@ export class ModalView {
|
||||
horizontal: true,
|
||||
vertical: true,
|
||||
});
|
||||
this.status = SHOWING;
|
||||
this.status = Status.SHOWING;
|
||||
if (this.view.webContents.isLoading()) {
|
||||
this.view.webContents.once('did-finish-load', () => {
|
||||
this.view.webContents.focus();
|
||||
@@ -77,10 +90,12 @@ export class ModalView {
|
||||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
this.view.webContents.destroy();
|
||||
|
||||
this.windowAttached = null;
|
||||
this.status = ACTIVE;
|
||||
delete this.windowAttached;
|
||||
this.status = Status.ACTIVE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -88,21 +103,21 @@ export class ModalView {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
reject = (data) => {
|
||||
reject = (data: T2) => {
|
||||
if (this.onReject) {
|
||||
this.onReject(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
this.status = Status.DONE;
|
||||
}
|
||||
|
||||
resolve = (data) => {
|
||||
resolve = (data: T2) => {
|
||||
if (this.onResolve) {
|
||||
this.onResolve(data);
|
||||
}
|
||||
this.hide();
|
||||
this.status = DONE;
|
||||
this.status = Status.DONE;
|
||||
}
|
||||
|
||||
isActive = () => this.status !== DONE;
|
||||
isActive = () => this.status !== Status.DONE;
|
||||
}
|
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import log from 'electron-log';
|
||||
import {BrowserView, dialog} from 'electron';
|
||||
import {BrowserView, BrowserWindow, dialog} from 'electron';
|
||||
import {BrowserViewConstructorOptions} from 'electron/main';
|
||||
|
||||
import {CombinedConfig, Team} from 'types/config';
|
||||
|
||||
import {SECOND} from 'common/utils/constants';
|
||||
import {
|
||||
@@ -26,16 +29,23 @@ const URL_VIEW_DURATION = 10 * SECOND;
|
||||
const URL_VIEW_HEIGHT = 36;
|
||||
|
||||
export class ViewManager {
|
||||
constructor(config, mainWindow) {
|
||||
configServers: Team[];
|
||||
viewOptions: BrowserViewConstructorOptions;
|
||||
views: Map<string, MattermostView>;
|
||||
currentView?: string;
|
||||
urlView?: BrowserView;
|
||||
urlViewCancel?: () => void;
|
||||
mainWindow: BrowserWindow;
|
||||
loadingScreen?: BrowserView;
|
||||
|
||||
constructor(config: CombinedConfig, mainWindow: BrowserWindow) {
|
||||
this.configServers = config.teams;
|
||||
this.viewOptions = {spellcheck: config.useSpellChecker};
|
||||
this.viewOptions = {webPreferences: {spellcheck: config.useSpellChecker}};
|
||||
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
||||
this.currentView = null;
|
||||
this.urlView = null;
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
updateMainWindow = (mainWindow) => {
|
||||
updateMainWindow = (mainWindow: BrowserWindow) => {
|
||||
this.mainWindow = mainWindow;
|
||||
}
|
||||
|
||||
@@ -43,7 +53,7 @@ export class ViewManager {
|
||||
return this.configServers;
|
||||
}
|
||||
|
||||
loadServer = (server) => {
|
||||
loadServer = (server: Team) => {
|
||||
const srv = new MattermostServer(server.name, server.url);
|
||||
const view = new MattermostView(srv, this.mainWindow, this.viewOptions);
|
||||
this.views.set(server.name, view);
|
||||
@@ -61,7 +71,7 @@ export class ViewManager {
|
||||
this.configServers.forEach((server) => this.loadServer(server));
|
||||
}
|
||||
|
||||
reloadConfiguration = (configServers) => {
|
||||
reloadConfiguration = (configServers: Team[]) => {
|
||||
this.configServers = configServers.concat();
|
||||
const oldviews = this.views;
|
||||
this.views = new Map();
|
||||
@@ -72,11 +82,11 @@ export class ViewManager {
|
||||
if (recycle && recycle.isVisible) {
|
||||
setFocus = recycle.name;
|
||||
}
|
||||
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url).toString()) {
|
||||
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) {
|
||||
oldviews.delete(recycle.name);
|
||||
this.views.set(recycle.name, recycle);
|
||||
} else {
|
||||
this.loadServer(server, this.mainWindow);
|
||||
this.loadServer(server);
|
||||
}
|
||||
});
|
||||
oldviews.forEach((unused) => {
|
||||
@@ -98,12 +108,12 @@ export class ViewManager {
|
||||
}
|
||||
}
|
||||
|
||||
showByName = (name) => {
|
||||
showByName = (name: string) => {
|
||||
const newView = this.views.get(name);
|
||||
if (newView.isVisible) {
|
||||
return;
|
||||
}
|
||||
if (newView) {
|
||||
if (newView.isVisible) {
|
||||
return;
|
||||
}
|
||||
if (this.currentView && this.currentView !== name) {
|
||||
const previous = this.getCurrentView();
|
||||
if (previous) {
|
||||
@@ -116,6 +126,10 @@ export class ViewManager {
|
||||
this.showLoadingScreen();
|
||||
}
|
||||
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name);
|
||||
if (!serverInfo) {
|
||||
log.error(`Couldn't find a server in the config with the name ${newView.server.name}`);
|
||||
return;
|
||||
}
|
||||
newView.window.webContents.send(SET_SERVER_KEY, serverInfo.order);
|
||||
if (newView.isReady()) {
|
||||
// if view is not ready, the renderer will have something to display instead.
|
||||
@@ -148,18 +162,22 @@ export class ViewManager {
|
||||
view.focus();
|
||||
}
|
||||
}
|
||||
activateView = (viewName) => {
|
||||
activateView = (viewName: string) => {
|
||||
if (this.currentView === viewName) {
|
||||
this.showByName(this.currentView);
|
||||
}
|
||||
const view = this.views.get(viewName);
|
||||
if (!view) {
|
||||
log.error(`Couldn't find a view with the name ${viewName}`);
|
||||
return;
|
||||
}
|
||||
addWebContentsEventListeners(view, this.getServers);
|
||||
}
|
||||
|
||||
finishLoading = (server) => {
|
||||
finishLoading = (server: string) => {
|
||||
const view = this.views.get(server);
|
||||
if (view && this.getCurrentView() === view) {
|
||||
this.showByName(this.currentView);
|
||||
this.showByName(this.currentView!);
|
||||
this.fadeLoadingScreen();
|
||||
}
|
||||
}
|
||||
@@ -169,19 +187,23 @@ export class ViewManager {
|
||||
}
|
||||
|
||||
getCurrentView() {
|
||||
return this.views.get(this.currentView);
|
||||
if (this.currentView) {
|
||||
return this.views.get(this.currentView);
|
||||
}
|
||||
|
||||
return undefined;
|
||||
}
|
||||
|
||||
openViewDevTools = () => {
|
||||
const view = this.getCurrentView();
|
||||
if (view) {
|
||||
view.openDevTools({mode: 'detach'});
|
||||
view.openDevTools();
|
||||
} else {
|
||||
log.error(`couldn't find ${this.currentView}`);
|
||||
}
|
||||
}
|
||||
|
||||
findByWebContent(webContentId) {
|
||||
findByWebContent(webContentId: number) {
|
||||
let found = null;
|
||||
let serverName;
|
||||
let view;
|
||||
@@ -198,7 +220,7 @@ export class ViewManager {
|
||||
return found;
|
||||
}
|
||||
|
||||
showURLView = (url) => {
|
||||
showURLView = (url: URL | string) => {
|
||||
if (this.urlViewCancel) {
|
||||
this.urlViewCancel();
|
||||
}
|
||||
@@ -213,9 +235,8 @@ export class ViewManager {
|
||||
const query = new Map([['url', urlString]]);
|
||||
const localURL = getLocalURLString('urlView.html', query);
|
||||
urlView.webContents.loadURL(localURL);
|
||||
const currentWindow = this.getCurrentView().window;
|
||||
currentWindow.addBrowserView(urlView);
|
||||
const boundaries = currentWindow.getBounds();
|
||||
this.mainWindow.addBrowserView(urlView);
|
||||
const boundaries = this.mainWindow.getBounds();
|
||||
urlView.setBounds({
|
||||
x: 0,
|
||||
y: boundaries.height - URL_VIEW_HEIGHT,
|
||||
@@ -224,11 +245,13 @@ export class ViewManager {
|
||||
});
|
||||
|
||||
const hideView = () => {
|
||||
this.urlViewCancel = null;
|
||||
currentWindow.removeBrowserView(urlView);
|
||||
delete this.urlViewCancel;
|
||||
this.mainWindow.removeBrowserView(urlView);
|
||||
|
||||
// workaround to eliminate zombie processes
|
||||
// https://github.com/mattermost/desktop/pull/1519
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
urlView.webContents.destroy();
|
||||
};
|
||||
|
||||
@@ -263,12 +286,12 @@ export class ViewManager {
|
||||
this.createLoadingScreen();
|
||||
}
|
||||
|
||||
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||
|
||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen)) {
|
||||
this.mainWindow.setTopBrowserView(this.loadingScreen);
|
||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
|
||||
this.mainWindow.setTopBrowserView(this.loadingScreen!);
|
||||
} else {
|
||||
this.mainWindow.addBrowserView(this.loadingScreen);
|
||||
this.mainWindow.addBrowserView(this.loadingScreen!);
|
||||
}
|
||||
|
||||
this.setLoadingScreenBounds();
|
||||
@@ -286,7 +309,7 @@ export class ViewManager {
|
||||
}
|
||||
}
|
||||
|
||||
setServerInitialized = (server) => {
|
||||
setServerInitialized = (server: string) => {
|
||||
const view = this.views.get(server);
|
||||
if (view) {
|
||||
view.setInitialized();
|
||||
@@ -296,30 +319,40 @@ export class ViewManager {
|
||||
}
|
||||
}
|
||||
|
||||
updateLoadingScreenDarkMode = (darkMode) => {
|
||||
updateLoadingScreenDarkMode = (darkMode: boolean) => {
|
||||
if (this.loadingScreen) {
|
||||
this.loadingScreen.webContents.send(GET_LOADING_SCREEN_DATA, {darkMode});
|
||||
}
|
||||
}
|
||||
|
||||
deeplinkSuccess = (serverName) => {
|
||||
deeplinkSuccess = (serverName: string) => {
|
||||
const view = this.views.get(serverName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
this.showByName(serverName);
|
||||
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
|
||||
};
|
||||
|
||||
deeplinkFailed = (serverName, err, url) => {
|
||||
const view = this.views.get(serverName);
|
||||
deeplinkFailed = (serverName: string, err: string, url: string) => {
|
||||
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`);
|
||||
const view = this.views.get(serverName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||
}
|
||||
|
||||
handleDeepLink = (url) => {
|
||||
handleDeepLink = (url: string | URL) => {
|
||||
if (url) {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, this.configServers, true);
|
||||
if (server) {
|
||||
const view = this.views.get(server.name);
|
||||
if (!view) {
|
||||
log.error(`Couldn't find a view matching the name ${server.name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// attempting to change parsedURL protocol results in it not being modified.
|
||||
const urlWithSchema = `${view.server.url.origin}${parsedURL.pathname}${parsedURL.search}`;
|
||||
@@ -333,7 +366,7 @@ export class ViewManager {
|
||||
}
|
||||
};
|
||||
|
||||
sendToAllViews = (channel, ...args) => {
|
||||
sendToAllViews = (channel: string, ...args: any[]) => {
|
||||
this.views.forEach((view) => view.view.webContents.send(channel, ...args));
|
||||
}
|
||||
}
|
@@ -1,12 +1,12 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserWindow, shell} from 'electron';
|
||||
import {BrowserWindow, shell, WebContents} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {DEVELOPMENT, PRODUCTION} from 'common/utils/constants';
|
||||
import {Team} from 'types/config';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import * as WindowManager from '../windows/windowManager';
|
||||
|
||||
@@ -15,26 +15,32 @@ import {protocols} from '../../../electron-builder.json';
|
||||
import allowProtocolDialog from '../allowProtocolDialog';
|
||||
import {composeUserAgent} from '../utils';
|
||||
|
||||
const customLogins = {};
|
||||
const listeners = {};
|
||||
let popupWindow = null;
|
||||
import {MattermostView} from './MattermostView';
|
||||
|
||||
function isTrustedPopupWindow(webContents) {
|
||||
type CustomLogin = {
|
||||
inProgress: boolean;
|
||||
}
|
||||
|
||||
const customLogins: Record<number, CustomLogin> = {};
|
||||
const listeners: Record<number, () => void> = {};
|
||||
let popupWindow: BrowserWindow | undefined;
|
||||
|
||||
function isTrustedPopupWindow(webContents: WebContents) {
|
||||
if (!webContents) {
|
||||
return false;
|
||||
}
|
||||
if (!popupWindow) {
|
||||
return false;
|
||||
}
|
||||
return Utils.browserWindowFromWebContents(webContents) === popupWindow;
|
||||
return BrowserWindow.fromWebContents(webContents) === popupWindow;
|
||||
}
|
||||
|
||||
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
|
||||
|
||||
const generateWillNavigate = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const generateWillNavigate = (getServersFunction: () => Team[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const configServers = getServersFunction();
|
||||
const server = urlUtils.getServer(parsedURL, configServers);
|
||||
|
||||
@@ -42,7 +48,7 @@ const generateWillNavigate = (getServersFunction) => {
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, configServers)) {
|
||||
return;
|
||||
}
|
||||
if (parsedURL.protocol === 'mailto:') {
|
||||
@@ -51,30 +57,24 @@ const generateWillNavigate = (getServersFunction) => {
|
||||
if (customLogins[contentID].inProgress) {
|
||||
return;
|
||||
}
|
||||
const mode = Utils.runMode();
|
||||
if (((mode === DEVELOPMENT || mode === PRODUCTION) &&
|
||||
(parsedURL.path === 'renderer/index.html' || parsedURL.path === 'renderer/settings.html'))) {
|
||||
log.info('loading settings page');
|
||||
return;
|
||||
}
|
||||
|
||||
log.info(`Prevented desktop from navigating to: ${url}`);
|
||||
event.preventDefault();
|
||||
};
|
||||
};
|
||||
|
||||
const generateDidStartNavigation = (getServersFunction) => {
|
||||
return (event, url) => {
|
||||
const generateDidStartNavigation = (getServersFunction: () => Team[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const serverList = getServersFunction();
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, serverList);
|
||||
|
||||
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||
customLogins[contentID].inProgress = true;
|
||||
} else if (customLogins[contentID].inProgress) {
|
||||
customLogins[contentID].inProgress = false;
|
||||
@@ -82,8 +82,8 @@ const generateDidStartNavigation = (getServersFunction) => {
|
||||
};
|
||||
};
|
||||
|
||||
const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
return (event, url) => {
|
||||
const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?: boolean) => {
|
||||
return (event: Event, url: string) => {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
event.preventDefault();
|
||||
@@ -146,14 +146,14 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
log.info(`${url} is an admin console page, preventing to open a new window`);
|
||||
return;
|
||||
}
|
||||
if (popupWindow && !popupWindow.closed && popupWindow.getURL() === url) {
|
||||
if (popupWindow && popupWindow.webContents.getURL() === url) {
|
||||
log.info(`Popup window already open at provided url: ${url}`);
|
||||
return;
|
||||
}
|
||||
|
||||
// TODO: move popups to its own and have more than one.
|
||||
if (urlUtils.isPluginUrl(server.url, parsedURL) || urlUtils.isManagedResource(server.url, parsedURL)) {
|
||||
if (!popupWindow || popupWindow.closed) {
|
||||
if (!popupWindow) {
|
||||
popupWindow = new BrowserWindow({
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
//parent: WindowManager.getMainWindow(),
|
||||
@@ -167,10 +167,10 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
},
|
||||
});
|
||||
popupWindow.once('ready-to-show', () => {
|
||||
popupWindow.show();
|
||||
popupWindow!.show();
|
||||
});
|
||||
popupWindow.once('closed', () => {
|
||||
popupWindow = null;
|
||||
popupWindow = undefined;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -187,13 +187,13 @@ const generateNewWindowListener = (getServersFunction, spellcheck) => {
|
||||
};
|
||||
};
|
||||
|
||||
export const removeWebContentsListeners = (id) => {
|
||||
export const removeWebContentsListeners = (id: number) => {
|
||||
if (listeners[id]) {
|
||||
listeners[id]();
|
||||
}
|
||||
};
|
||||
|
||||
export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => Team[]) => {
|
||||
const contents = mmview.view.webContents;
|
||||
|
||||
// initialize custom login tracking
|
||||
@@ -206,7 +206,7 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
}
|
||||
|
||||
const willNavigate = generateWillNavigate(getServersFunction);
|
||||
contents.on('will-navigate', willNavigate);
|
||||
contents.on('will-navigate', willNavigate as (e: Event, u: string) => void); // TODO: Electron types don't include sender for some reason
|
||||
|
||||
// handle custom login requests (oath, saml):
|
||||
// 1. are we navigating to a supported local custom login path from the `/login` page?
|
||||
@@ -214,9 +214,9 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
// 2. are we finished with the custom login process?
|
||||
// - indicate custom login is NOT in progress
|
||||
const didStartNavigation = generateDidStartNavigation(getServersFunction);
|
||||
contents.on('did-start-navigation', didStartNavigation);
|
||||
contents.on('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||
|
||||
const spellcheck = mmview.options.webPreferences.spellcheck;
|
||||
const spellcheck = mmview.options.webPreferences?.spellcheck;
|
||||
const newWindow = generateNewWindowListener(getServersFunction, spellcheck);
|
||||
contents.on('new-window', newWindow);
|
||||
|
||||
@@ -227,8 +227,8 @@ export const addWebContentsEventListeners = (mmview, getServersFunction) => {
|
||||
|
||||
const removeListeners = () => {
|
||||
try {
|
||||
contents.removeListener('will-navigate', willNavigate);
|
||||
contents.removeListener('did-start-navigation', didStartNavigation);
|
||||
contents.removeListener('will-navigate', willNavigate as (e: Event, u: string) => void);
|
||||
contents.removeListener('did-start-navigation', didStartNavigation as (e: Event, u: string) => void);
|
||||
contents.removeListener('new-window', newWindow);
|
||||
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
|
||||
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
|
Reference in New Issue
Block a user