Files
mattermostest/src/main/main.ts

982 lines
34 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
/* eslint-disable max-lines */
import fs from 'fs';
import path from 'path';
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 {CombinedConfig, Team, TeamWithTabs} from 'types/config';
import {MentionData} from 'types/notification';
import {RemoteInfo} from 'types/server';
import {Boundaries} from 'types/utils';
import {
SWITCH_SERVER,
FOCUS_BROWSERVIEW,
QUIT,
DARK_MODE_CHANGE,
DOUBLE_CLICK_ON_WINDOW,
SHOW_NEW_SERVER_MODAL,
WINDOW_CLOSE,
WINDOW_MAXIMIZE,
WINDOW_MINIMIZE,
WINDOW_RESTORE,
NOTIFY_MENTION,
GET_DOWNLOAD_LOCATION,
SHOW_SETTINGS_WINDOW,
RELOAD_CONFIGURATION,
USER_ACTIVITY_UPDATE,
EMIT_CONFIGURATION,
SWITCH_TAB,
CLOSE_TAB,
OPEN_TAB,
SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL,
UPDATE_SHORTCUT_MENU,
UPDATE_LAST_ACTIVE,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
} from 'common/communication';
import Config from 'common/config';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getDefaultTeamWithTabsFromTeam, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import Utils from 'common/utils/util';
import urlUtils from 'common/utils/url';
import {protocols} from '../../electron-builder.json';
import AutoLauncher from './AutoLauncher';
import CriticalErrorHandler from './CriticalErrorHandler';
import upgradeAutoLaunch from './autoLaunch';
import CertificateStore from './certificateStore';
import TrustedOriginsStore from './trustedOrigins';
import appMenu from './menus/app';
import trayMenu from './menus/tray';
import allowProtocolDialog from './allowProtocolDialog';
import AppVersionManager from './AppVersionManager';
import initCookieManager from './cookieManager';
import UserActivityMonitor from './UserActivityMonitor';
import * as WindowManager from './windows/windowManager';
import {displayMention, displayDownloadCompleted} from './notifications';
import parseArgs from './ParseArgs';
import {addModal} from './views/modalManager';
import {getLocalURLString, getLocalPreload} from './utils';
import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tray';
import {AuthManager} from './authManager';
import {CertificateManager} from './certificateManager';
import {setupBadge, setUnreadBadgeSetting} from './badge';
import {ServerInfo} from './server/serverInfo';
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept();
}
// pull out required electron components like this
// as not all components can be referenced before the app is ready
const {
app,
Menu,
ipcMain,
dialog,
session,
} = electron;
const criticalErrorHandler = new CriticalErrorHandler();
const userActivityMonitor = new UserActivityMonitor();
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: CertificateStore;
let trustedOriginsStore;
let scheme: string;
let appVersion = null;
let config: Config;
let authManager: AuthManager;
let certificateManager: CertificateManager;
let didCheckForAddServerModal = false;
/**
* Main entry point for the application, ensures that everything initializes in the proper order
*/
async function initialize() {
process.on('uncaughtException', criticalErrorHandler.processUncaughtExceptionHandler.bind(criticalErrorHandler));
global.willAppQuit = false;
// initialization that can run before the app is ready
initializeArgs();
await initializeConfig();
initializeAppEventListeners();
initializeBeforeAppReady();
// wait for registry config data to load and app ready event
await Promise.all([
app.whenReady(),
]);
// no need to continue initializing if app is quitting
if (global.willAppQuit) {
return;
}
// initialization that should run once the app is ready
initializeInterCommunicationEventListeners();
initializeAfterAppReady();
}
// attempt to initialize the application
try {
initialize();
} catch (error) {
throw new Error(`App initialization failed: ${error.toString()}`);
}
//
// initialization sub functions
//
function initializeArgs() {
global.args = parseArgs(process.argv.slice(1));
// output the application version via cli when requested (-v or --version)
if (global.args.version) {
process.stdout.write(`v.${app.getVersion()}\n`);
process.exit(0); // eslint-disable-line no-process-exit
}
global.isDev = isDev && !global.args.disableDevMode; // this doesn't seem to be right and isn't used as the single source of truth
if (global.args.dataDir) {
app.setPath('userData', path.resolve(global.args.dataDir));
}
}
async function initializeConfig() {
const loadConfig = new Promise<void>((resolve) => {
config = new Config(app.getPath('userData') + '/config.json');
config.once('update', (configData) => {
config.on('update', handleConfigUpdate);
config.on('synchronize', handleConfigSynchronize);
config.on('darkModeChange', handleDarkModeChange);
handleConfigUpdate(configData);
// can only call this before the app is ready
if (config.enableHardwareAcceleration === false) {
app.disableHardwareAcceleration();
}
resolve();
});
config.init();
});
return loadConfig;
}
function initializeAppEventListeners() {
app.on('second-instance', handleAppSecondInstance);
app.on('window-all-closed', handleAppWindowAllClosed);
app.on('browser-window-created', handleAppBrowserWindowCreated);
app.on('activate', handleAppActivate);
app.on('before-quit', handleAppBeforeQuit);
app.on('certificate-error', handleAppCertificateError);
app.on('select-client-certificate', handleSelectCertificate);
app.on('gpu-process-crashed', handleAppGPUProcessCrashed);
app.on('login', handleAppLogin);
app.on('will-finish-launching', handleAppWillFinishLaunching);
}
function initializeBeforeAppReady() {
if (!config || !config.data) {
log.error('No config loaded');
return;
}
if (process.env.NODE_ENV !== 'test') {
app.enableSandbox();
}
certificateStore = new CertificateStore(path.resolve(app.getPath('userData'), 'certificate.json'));
trustedOriginsStore = new TrustedOriginsStore(path.resolve(app.getPath('userData'), 'trustedOrigins.json'));
trustedOriginsStore.load();
// prevent using a different working directory, which happens on windows running after installation.
const expectedPath = path.dirname(process.execPath);
if (process.cwd() !== expectedPath && !isDev) {
log.warn(`Current working directory is ${process.cwd()}, changing into ${expectedPath}`);
process.chdir(expectedPath);
}
refreshTrayImages(config.trayIconTheme);
// If there is already an instance, quit this one
const gotTheLock = app.requestSingleInstanceLock();
if (!gotTheLock) {
app.exit();
global.willAppQuit = true;
}
allowProtocolDialog.init();
authManager = new AuthManager(config.data, trustedOriginsStore);
certificateManager = new CertificateManager();
if (isDev) {
log.info('In development mode, deeplinking is disabled');
} else if (protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]) {
scheme = protocols[0].schemes[0];
app.setAsDefaultProtocolClient(scheme);
}
}
function initializeInterCommunicationEventListeners() {
ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig);
ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
ipcMain.handle('get-app-version', handleAppVersion);
ipcMain.on('update-menu', handleUpdateMenuEvent);
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateShortcutMenuEvent);
ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView);
ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive);
if (process.platform !== 'darwin') {
ipcMain.on('open-app-menu', handleOpenAppMenu);
}
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
ipcMain.on(SWITCH_TAB, handleSwitchTab);
ipcMain.on(CLOSE_TAB, handleCloseTab);
ipcMain.on(OPEN_TAB, handleOpenTab);
ipcMain.on(QUIT, handleQuit);
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick);
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, handleGetAvailableSpellCheckerLanguages);
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
}
//
// config event handlers
//
function handleConfigUpdate(newConfig: CombinedConfig) {
if (!newConfig) {
return;
}
if (process.platform === 'win32' || process.platform === 'linux') {
const appLauncher = new AutoLauncher();
const autoStartTask = config.autostart ? appLauncher.enable() : appLauncher.disable();
autoStartTask.then(() => {
log.info('config.autostart has been configured:', newConfig.autostart);
}).catch((err) => {
log.error('error:', err);
});
WindowManager.setConfig(newConfig);
if (authManager) {
authManager.handleConfigUpdate(newConfig);
}
setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge);
updateSpellCheckerLocales();
}
ipcMain.emit('update-menu', true, config);
ipcMain.emit(EMIT_CONFIGURATION, true, newConfig);
}
function handleConfigSynchronize() {
if (!config.data) {
return;
}
// TODO: send this to server manager
WindowManager.setConfig(config.data);
setUnreadBadgeSetting(config.data.showUnreadBadge);
if (config.data.downloadLocation) {
try {
app.setPath('downloads', config.data.downloadLocation);
} catch (e) {
log.error(`There was a problem trying to set the default download path: ${e}`);
}
}
if (app.isReady()) {
WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
}
if (process.platform === 'win32' && !didCheckForAddServerModal && typeof config.registryConfigData !== 'undefined') {
didCheckForAddServerModal = true;
if (config.teams.length === 0) {
handleNewServerModal();
}
}
}
function handleReloadConfig() {
config.reload();
WindowManager.setConfig(config.data!);
}
function handleAppVersion() {
return {
name: app.getName(),
version: app.getVersion(),
};
}
function handleDarkModeChange(darkMode: boolean) {
refreshTrayImages(config.trayIconTheme);
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
WindowManager.updateLoadingScreenDarkMode(darkMode);
ipcMain.emit(EMIT_CONFIGURATION, true, config.data);
}
//
// app event handlers
//
// activate first app instance, subsequent instances will quit themselves
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);
if (deeplinkingUrl) {
openDeepLink(deeplinkingUrl);
}
}
function handleAppWindowAllClosed() {
// On OS X it is common for applications and their menu bar
// to stay active until the user quits explicitly with Cmd + Q
if (process.platform !== 'darwin') {
app.quit();
}
}
function handleAppBrowserWindowCreated(event: Event, newWindow: BrowserWindow) {
// Screen cannot be required before app is ready
resizeScreen(newWindow);
}
function handleAppActivate() {
WindowManager.showMainWindow();
}
function handleAppBeforeQuit() {
// Make sure tray icon gets removed if the user exits via CTRL-Q
destroyTray();
global.willAppQuit = true;
}
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: 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: electron.Event, webContents: electron.WebContents, url: string, error: string, certificate: electron.Certificate, callback: (isTrusted: boolean) => void) {
const parsedURL = new URL(url);
if (!parsedURL) {
return;
}
const origin = parsedURL.origin;
if (certificateStore.isExplicitlyUntrusted(origin)) {
event.preventDefault();
log.warn(`Ignoring previously untrusted certificate for ${origin}`);
callback(false);
} else if (certificateStore.isTrusted(origin, certificate)) {
event.preventDefault();
callback(true);
} else {
// update the callback
const errorID = `${origin}:${error}`;
// if we are already showing that error, don't add more dialogs
if (certificateErrorCallbacks.has(errorID)) {
log.warn(`Ignoring already shown dialog for ${errorID}`);
certificateErrorCallbacks.set(errorID, callback);
return;
}
const extraDetail = certificateStore.isExisting(origin) ? 'Certificate is different from previous one.\n\n' : '';
const detail = `${extraDetail}origin: ${origin}\nError: ${error}`;
certificateErrorCallbacks.set(errorID, callback);
// 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.',
type: 'error',
detail,
buttons: ['More Details', 'Cancel Connection'],
cancelId: 1,
}).then(
({response}) => {
if (response === 0) {
return dialog.showMessageBox(mainWindow, {
title: 'Certificate Not Trusted',
message: `Certificate from "${certificate.issuerName}" is not trusted.`,
detail: extraDetail,
type: 'error',
buttons: ['Trust Insecure Certificate', 'Cancel Connection'],
cancelId: 1,
checkboxChecked: false,
checkboxLabel: "Don't ask again",
});
}
return {response, checkboxChecked: false};
}).then(
({response: responseTwo, checkboxChecked}) => {
if (responseTwo === 0) {
certificateStore.add(origin, certificate);
certificateStore.save();
certificateErrorCallbacks.get(errorID)(true);
certificateErrorCallbacks.delete(errorID);
webContents.loadURL(url);
} else {
if (checkboxChecked) {
certificateStore.add(origin, certificate, true);
certificateStore.save();
}
certificateErrorCallbacks.get(errorID)(false);
certificateErrorCallbacks.delete(errorID);
}
}).catch(
(dialogError) => {
log.error(`There was an error with the Certificate Error dialog: ${dialogError}`);
certificateErrorCallbacks.delete(errorID);
});
}
}
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: electron.Event, killed: boolean) {
log.error(`The GPU process has crashed (killed = ${killed})`);
}
function openDeepLink(deeplinkingUrl: string) {
try {
WindowManager.showMainWindow(deeplinkingUrl);
} catch (err) {
log.error(`There was an error opening the deeplinking url: ${err}`);
}
}
function handleAppWillFinishLaunching() {
// Protocol handler for osx
app.on('open-url', (event, url) => {
log.info(`Handling deeplinking url: ${url}`);
event.preventDefault();
const deeplinkingUrl = getDeeplinkingURL([url]);
if (deeplinkingUrl) {
if (app.isReady() && deeplinkingUrl) {
openDeepLink(deeplinkingUrl);
} else {
app.once('ready', () => openDeepLink(deeplinkingUrl));
}
}
});
}
function handleSwitchServer(event: IpcMainEvent, serverName: string) {
WindowManager.switchServer(serverName);
}
function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) {
WindowManager.switchTab(serverName, tabName);
}
function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) {
const teams = config.teams;
teams.forEach((team) => {
if (team.name === serverName) {
team.tabs.forEach((tab) => {
if (tab.name === tabName) {
tab.isOpen = false;
}
});
}
});
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name;
WindowManager.switchTab(serverName, nextTab);
config.set('teams', teams);
}
function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) {
const teams = config.teams;
teams.forEach((team) => {
if (team.name === serverName) {
team.tabs.forEach((tab) => {
if (tab.name === tabName) {
tab.isOpen = true;
}
});
}
});
WindowManager.switchTab(serverName, tabName);
config.set('teams', teams);
}
function handleNewServerModal() {
const html = getLocalURLString('newServer.html');
const modalPreload = getLocalPreload('modalPreload.js');
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;
const order = teams.length;
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order});
teams.push(newTeam);
config.set('teams', teams);
updateServerInfos([newTeam]);
WindowManager.switchServer(newTeam.name, true);
}).catch((e) => {
// e is undefined for user cancellation
if (e) {
log.error(`there was an error in the new server modal: ${e}`);
}
});
} else {
log.warn('There is already a new server modal');
}
}
function handleEditServerModal(e: IpcMainEvent, name: string) {
const html = getLocalURLString('editServer.html');
const modalPreload = getLocalPreload('modalPreload.js');
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
const serverIndex = config.teams.findIndex((team) => team.name === name);
if (serverIndex < 0) {
return;
}
const modalPromise = addModal<Team, Team>('editServer', html, modalPreload, config.teams[serverIndex], mainWindow);
if (modalPromise) {
modalPromise.then((data) => {
const teams = config.teams;
teams[serverIndex].name = data.name;
teams[serverIndex].url = data.url;
config.set('teams', teams);
}).catch((e) => {
// e is undefined for user cancellation
if (e) {
log.error(`there was an error in the edit server modal: ${e}`);
}
});
} else {
log.warn('There is already an edit server modal');
}
}
function handleRemoveServerModal(e: IpcMainEvent, name: string) {
const html = getLocalURLString('removeServer.html');
const modalPreload = getLocalPreload('modalPreload.js');
const mainWindow = WindowManager.getMainWindow();
if (!mainWindow) {
return;
}
const modalPromise = addModal<string, boolean>('removeServer', html, modalPreload, name, mainWindow);
if (modalPromise) {
modalPromise.then((remove) => {
if (remove) {
const teams = config.teams;
const removedTeam = teams.findIndex((team) => team.name === name);
if (removedTeam < 0) {
return;
}
const removedOrder = teams[removedTeam].order;
teams.splice(removedTeam, 1);
teams.forEach((value) => {
if (value.order > removedOrder) {
value.order--;
}
});
config.set('teams', teams);
}
}).catch((e) => {
// e is undefined for user cancellation
if (e) {
log.error(`there was an error in the edit server modal: ${e}`);
}
});
} else {
log.warn('There is already an edit server modal');
}
}
function updateSpellCheckerLocales() {
if (config.data?.spellCheckerLocales.length && app.isReady()) {
session.defaultSession.setSpellCheckerLanguages(config.data?.spellCheckerLocales);
}
}
function initializeAfterAppReady() {
updateServerInfos(config.teams);
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
const defaultSession = session.defaultSession;
if (process.platform !== 'darwin') {
defaultSession.on('spellcheck-dictionary-download-failure', (event, lang) => {
if (config.spellCheckerURL) {
log.error(`There was an error while trying to load the dictionary definitions for ${lang} fromfully the specified url. Please review you have access to the needed files. Url used was ${config.spellCheckerURL}`);
} else {
log.warn(`There was an error while trying to download the dictionary definitions for ${lang}, spellchecking might not work properly.`);
}
});
if (config.spellCheckerURL) {
const spellCheckerURL = config.spellCheckerURL.endsWith('/') ? config.spellCheckerURL : `${config.spellCheckerURL}/`;
log.info(`Configuring spellchecker using download URL: ${spellCheckerURL}`);
defaultSession.setSpellCheckerDictionaryDownloadURL(spellCheckerURL);
defaultSession.on('spellcheck-dictionary-download-success', (event, lang) => {
log.info(`Dictionary definitions downloaded successfully for ${lang}`);
});
}
updateSpellCheckerLocales();
}
const appVersionJson = path.join(app.getPath('userData'), 'app-state.json');
appVersion = new AppVersionManager(appVersionJson);
if (wasUpdated(appVersion.lastAppVersion)) {
clearAppCache();
}
appVersion.lastAppVersion = app.getVersion();
if (!global.isDev) {
upgradeAutoLaunch();
}
if (global.isDev) {
installExtension(REACT_DEVELOPER_TOOLS).
then((name) => log.info(`Added Extension: ${name}`)).
catch((err) => log.error('An error occurred: ', err));
}
// Workaround for MM-22193
// From this post: https://github.com/electron/electron/issues/19468#issuecomment-549593139
// Electron 6 has a bug that affects users on Windows 10 using dark mode, causing the app to hang
// This workaround deletes a file that stops that from happening
if (process.platform === 'win32') {
const appUserDataPath = app.getPath('userData');
const devToolsExtensionsPath = path.join(appUserDataPath, 'DevTools Extensions');
try {
fs.unlinkSync(devToolsExtensionsPath);
} catch (_) {
// don't complain if the file doesn't exist
}
}
let deeplinkingURL;
// Protocol handler for win32
if (process.platform === 'win32') {
const args = process.argv.slice(1);
if (Array.isArray(args) && args.length > 0) {
deeplinkingURL = getDeeplinkingURL(args);
}
}
initCookieManager(defaultSession);
WindowManager.showMainWindow(deeplinkingURL);
criticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
// listen for status updates and pass on to renderer
userActivityMonitor.on('status', (status) => {
WindowManager.sendToMattermostViews(USER_ACTIVITY_UPDATE, status);
});
// start monitoring user activity (needs to be started after the app is ready)
userActivityMonitor.startMonitoring();
if (shouldShowTrayIcon()) {
setupTray(config.trayIconTheme);
}
setupBadge();
defaultSession.on('will-download', (event, item, webContents) => {
const filename = item.getFilename();
const fileElements = filename.split('.');
const filters = [];
if (fileElements.length > 1) {
filters.push({
name: 'All files',
extensions: ['*'],
});
}
item.setSaveDialogOptions({
title: filename,
defaultPath: path.resolve(config.downloadLocation, filename),
filters,
});
item.on('done', (doneEvent, state) => {
if (state === 'completed') {
displayDownloadCompleted(filename, item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || '');
}
});
});
ipcMain.emit('update-menu', true, config);
ipcMain.emit('update-dict');
// supported permission types
const supportedPermissionTypes = [
'media',
'geolocation',
'notifications',
'fullscreen',
'openExternal',
];
// handle permission requests
// - approve if a supported permission type and the request comes from the renderer or one of the defined servers
defaultSession.setPermissionRequestHandler((webContents, permission, callback) => {
// is the requested permission type supported?
if (!supportedPermissionTypes.includes(permission)) {
callback(false);
return;
}
// is the request coming from the renderer?
const mainWindow = WindowManager.getMainWindow();
if (mainWindow && webContents.id === mainWindow.webContents.id) {
callback(true);
return;
}
const requestingURL = webContents.getURL();
// is the requesting url trusted?
callback(urlUtils.isTrustedURL(requestingURL, config.teams));
});
// only check for non-Windows, as with Windows we have to wait for GPO teams
if (process.platform !== 'win32' || typeof config.registryConfigData !== 'undefined') {
if (config.teams.length === 0) {
setTimeout(() => {
handleNewServerModal();
}, 200);
}
}
}
//
// ipc communication event handlers
//
function handleMentionNotification(event: IpcMainEvent, title: string, body: string, channel: {id: string}, teamId: string, url: string, silent: boolean, data: MentionData) {
displayMention(title, body, channel, teamId, url, silent, event.sender, data);
}
function updateServerInfos(teams: TeamWithTabs[]) {
const serverInfos: Array<Promise<RemoteInfo | string | undefined>> = [];
teams.forEach((team) => {
const serverInfo = new ServerInfo(new MattermostServer(team.name, team.url));
serverInfos.push(serverInfo.promise);
});
Promise.all(serverInfos).then((data: Array<RemoteInfo | string | undefined>) => {
const teams = config.teams;
teams.forEach((team) => openExtraTabs(data, team));
config.set('teams', teams);
}).catch((reason: any) => {
log.error('Error getting server infos', reason);
});
}
function openExtraTabs(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
if (remoteInfo) {
team.tabs.forEach((tab) => {
if (tab.name !== TAB_MESSAGING && remoteInfo.serverVersion && Utils.isServerVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0')) {
if (tab.name === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && tab.isOpen !== false) {
log.info(`opening ${team.name}___${tab.name} on hasPlaybooks`);
tab.isOpen = true;
}
if (tab.name === TAB_FOCALBOARD && remoteInfo.hasFocalboard && tab.isOpen !== false) {
log.info(`opening ${team.name}___${tab.name} on hasFocalboard`);
tab.isOpen = true;
}
}
});
}
}
function handleOpenAppMenu() {
const windowMenu = Menu.getApplicationMenu();
if (!windowMenu) {
log.error('No application menu found');
return;
}
windowMenu.popup({
window: WindowManager.getMainWindow(),
x: 18,
y: 18,
});
}
function handleCloseAppMenu() {
WindowManager.focusBrowserView();
}
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);
}
}
function handleUpdateShortcutMenuEvent(event: IpcMainEvent) {
handleUpdateMenuEvent(event, config);
}
async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: string) {
const message = 'Specify the folder where files will download';
const result = await dialog.showOpenDialog({defaultPath: startFrom || config.downloadLocation,
message,
properties:
['openDirectory', 'createDirectory', 'dontAddToRecent', 'promptToCreate']});
return result.filePaths[0];
}
//
// helper functions
//
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];
if (url && scheme && url.startsWith(scheme) && urlUtils.isValidURI(url)) {
return url;
}
}
return undefined;
}
function shouldShowTrayIcon() {
return config.showTrayIcon || process.platform === 'win32';
}
function wasUpdated(lastAppVersion?: string) {
return lastAppVersion !== app.getVersion();
}
function clearAppCache() {
// TODO: clear cache on browserviews, not in the renderer.
const mainWindow = WindowManager.getMainWindow();
if (mainWindow) {
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
} else {
//Wait for mainWindow
setTimeout(clearAppCache, 100);
}
}
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;
}
// is half the screen within the display?
const midX = state.x + (state.width / 2);
const midY = state.y + (state.height / 2);
return !(midX > display.maxX || midY > display.maxY);
}
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();
const display = boundaries.find((boundary) => {
return isWithinDisplay(state, boundary);
});
if (typeof display === 'undefined') {
return {};
}
return {x: state.x, y: state.y};
}
function resizeScreen(browserWindow: BrowserWindow) {
function handle() {
const position = browserWindow.getPosition();
const size = browserWindow.getSize();
const validPosition = getValidWindowPosition({
x: position[0],
y: position[1],
width: size[0],
height: size[1],
});
if (typeof validPosition.x !== 'undefined' || typeof validPosition.y !== 'undefined') {
browserWindow.setPosition(validPosition.x || 0, validPosition.y || 0);
} else {
browserWindow.center();
}
}
browserWindow.on('restore', handle);
handle();
}
function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewName: string) {
const teams = config.teams;
teams.forEach((team) => {
if (team.name === serverName) {
const viewOrder = team?.tabs.find((tab) => tab.name === viewName)?.order || 0;
team.lastActiveTab = viewOrder;
}
});
config.set('teams', teams);
config.set('lastActiveTeam', teams.find((team) => team.name === serverName)?.order || 0);
}
function handleGetAvailableSpellCheckerLanguages() {
return session.defaultSession.availableSpellCheckerLanguages;
}