[MM-51871] Migrate mainWindow and settingsWindow to singletons (#2650)
* Migrate mainWindow to singleton * Migrate settingsWindow to singleton * PR feedback * Missed a couple unwrapping cases
This commit is contained in:
@@ -42,30 +42,10 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
|
|
||||||
describe('main/CriticalErrorHandler', () => {
|
describe('main/CriticalErrorHandler', () => {
|
||||||
const criticalErrorHandler = new CriticalErrorHandler();
|
const criticalErrorHandler = new CriticalErrorHandler();
|
||||||
beforeEach(() => {
|
|
||||||
criticalErrorHandler.setMainWindow({});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('windowUnresponsiveHandler', () => {
|
|
||||||
it('should do nothing when mainWindow is null', () => {
|
|
||||||
criticalErrorHandler.setMainWindow(null);
|
|
||||||
criticalErrorHandler.windowUnresponsiveHandler();
|
|
||||||
expect(dialog.showMessageBox).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call app.relaunch when user elects not to wait', async () => {
|
|
||||||
const promise = Promise.resolve({response: 0});
|
|
||||||
dialog.showMessageBox.mockImplementation(() => promise);
|
|
||||||
criticalErrorHandler.windowUnresponsiveHandler();
|
|
||||||
await promise;
|
|
||||||
expect(app.relaunch).toBeCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('processUncaughtExceptionHandler', () => {
|
describe('processUncaughtExceptionHandler', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
app.isReady.mockImplementation(() => true);
|
app.isReady.mockImplementation(() => true);
|
||||||
criticalErrorHandler.setMainWindow({isVisible: true});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should throw error if app is not ready', () => {
|
it('should throw error if app is not ready', () => {
|
||||||
@@ -76,16 +56,6 @@ describe('main/CriticalErrorHandler', () => {
|
|||||||
expect(dialog.showMessageBox).not.toBeCalled();
|
expect(dialog.showMessageBox).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing if main window is null or not visible', () => {
|
|
||||||
criticalErrorHandler.setMainWindow(null);
|
|
||||||
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
|
|
||||||
expect(dialog.showMessageBox).not.toBeCalled();
|
|
||||||
|
|
||||||
criticalErrorHandler.setMainWindow({isVisible: false});
|
|
||||||
criticalErrorHandler.processUncaughtExceptionHandler(new Error('test'));
|
|
||||||
expect(dialog.showMessageBox).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should open external file on Show Details', async () => {
|
it('should open external file on Show Details', async () => {
|
||||||
path.join.mockImplementation(() => 'testfile.txt');
|
path.join.mockImplementation(() => 'testfile.txt');
|
||||||
const promise = Promise.resolve({response: process.platform === 'darwin' ? 2 : 0});
|
const promise = Promise.resolve({response: process.platform === 'darwin' ? 2 : 0});
|
||||||
|
@@ -7,132 +7,111 @@ import fs from 'fs';
|
|||||||
import os from 'os';
|
import os from 'os';
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {app, BrowserWindow, dialog} from 'electron';
|
import {app, dialog} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
|
||||||
function createErrorReport(err: Error) {
|
|
||||||
// eslint-disable-next-line no-undef
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
return `Application: ${app.name} ${app.getVersion()}${__HASH_VERSION__ ? ` [commit: ${__HASH_VERSION__}]` : ''}\n` +
|
|
||||||
`Platform: ${os.type()} ${os.release()} ${os.arch()}\n` +
|
|
||||||
`${err.stack}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function openDetachedExternal(url: string) {
|
|
||||||
const spawnOption = {detached: true, stdio: 'ignore' as const};
|
|
||||||
switch (process.platform) {
|
|
||||||
case 'win32':
|
|
||||||
return spawn('cmd', ['/C', 'start', url], spawnOption);
|
|
||||||
case 'darwin':
|
|
||||||
return spawn('open', [url], spawnOption);
|
|
||||||
case 'linux':
|
|
||||||
return spawn('xdg-open', [url], spawnOption);
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export class CriticalErrorHandler {
|
export class CriticalErrorHandler {
|
||||||
mainWindow?: BrowserWindow;
|
init = () => {
|
||||||
|
process.on('unhandledRejection', this.processUncaughtExceptionHandler);
|
||||||
setMainWindow(mainWindow: BrowserWindow) {
|
process.on('uncaughtException', this.processUncaughtExceptionHandler);
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
windowUnresponsiveHandler() {
|
private processUncaughtExceptionHandler = (err: Error) => {
|
||||||
if (!this.mainWindow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dialog.showMessageBox(this.mainWindow, {
|
|
||||||
type: 'warning',
|
|
||||||
title: app.name,
|
|
||||||
message: localizeMessage('main.CriticalErrorHandler.unresponsive.dialog.message', 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?'),
|
|
||||||
buttons: [
|
|
||||||
localizeMessage('label.no', 'No'),
|
|
||||||
localizeMessage('label.yes', 'Yes'),
|
|
||||||
],
|
|
||||||
defaultId: 0,
|
|
||||||
}).then(({response}) => {
|
|
||||||
if (response === 0) {
|
|
||||||
log.error('BrowserWindow \'unresponsive\' event has been emitted');
|
|
||||||
app.relaunch();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
processUncaughtExceptionHandler(err: Error) {
|
|
||||||
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
|
|
||||||
const report = createErrorReport(err);
|
|
||||||
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
|
|
||||||
|
|
||||||
if (process.env.NODE_ENV === 'test') {
|
if (process.env.NODE_ENV === 'test') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (app.isReady()) {
|
if (app.isReady()) {
|
||||||
const buttons = [
|
this.showExceptionDialog(err);
|
||||||
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
|
||||||
localizeMessage('label.ok', 'OK'),
|
|
||||||
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
|
||||||
];
|
|
||||||
let indexOfReopen = 2;
|
|
||||||
let indexOfShowDetails = 0;
|
|
||||||
if (process.platform === 'darwin') {
|
|
||||||
buttons.reverse();
|
|
||||||
indexOfReopen = 0;
|
|
||||||
indexOfShowDetails = 2;
|
|
||||||
}
|
|
||||||
if (!this.mainWindow?.isVisible) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dialog.showMessageBox(
|
|
||||||
this.mainWindow,
|
|
||||||
{
|
|
||||||
type: 'error',
|
|
||||||
title: app.name,
|
|
||||||
message: localizeMessage(
|
|
||||||
'main.CriticalErrorHandler.uncaughtException.dialog.message',
|
|
||||||
'The {appName} app quit unexpectedly. Click "{showDetails}" to learn more or "{reopen}" to open the application again.\n\nInternal error: {err}',
|
|
||||||
{
|
|
||||||
appName: app.name,
|
|
||||||
showDetails: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
|
||||||
reopen: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
|
||||||
err: err.message,
|
|
||||||
},
|
|
||||||
),
|
|
||||||
buttons,
|
|
||||||
defaultId: indexOfReopen,
|
|
||||||
noLink: true,
|
|
||||||
},
|
|
||||||
).then(({response}) => {
|
|
||||||
let child;
|
|
||||||
switch (response) {
|
|
||||||
case indexOfShowDetails:
|
|
||||||
child = openDetachedExternal(file);
|
|
||||||
if (child) {
|
|
||||||
child.on(
|
|
||||||
'error',
|
|
||||||
(spawnError) => {
|
|
||||||
log.error(spawnError);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
child.unref();
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
case indexOfReopen:
|
|
||||||
app.relaunch();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
app.exit(-1);
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
log.error(`Window wasn't ready to handle the error: ${err}\ntrace: ${err.stack}`);
|
app.once('ready', () => {
|
||||||
throw err;
|
this.showExceptionDialog(err);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private showExceptionDialog = (err: Error) => {
|
||||||
|
const file = path.join(app.getPath('userData'), `uncaughtException-${Date.now()}.txt`);
|
||||||
|
const report = this.createErrorReport(err);
|
||||||
|
fs.writeFileSync(file, report.replace(new RegExp('\\n', 'g'), os.EOL));
|
||||||
|
|
||||||
|
const buttons = [
|
||||||
|
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
||||||
|
localizeMessage('label.ok', 'OK'),
|
||||||
|
localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
||||||
|
];
|
||||||
|
let indexOfReopen = 2;
|
||||||
|
let indexOfShowDetails = 0;
|
||||||
|
if (process.platform === 'darwin') {
|
||||||
|
buttons.reverse();
|
||||||
|
indexOfReopen = 0;
|
||||||
|
indexOfShowDetails = 2;
|
||||||
|
}
|
||||||
|
dialog.showMessageBox(
|
||||||
|
{
|
||||||
|
type: 'error',
|
||||||
|
title: app.name,
|
||||||
|
message: localizeMessage(
|
||||||
|
'main.CriticalErrorHandler.uncaughtException.dialog.message',
|
||||||
|
'The {appName} app quit unexpectedly. Click "{showDetails}" to learn more or "{reopen}" to open the application again.\n\nInternal error: {err}',
|
||||||
|
{
|
||||||
|
appName: app.name,
|
||||||
|
showDetails: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.showDetails', 'Show Details'),
|
||||||
|
reopen: localizeMessage('main.CriticalErrorHandler.uncaughtException.button.reopen', 'Reopen'),
|
||||||
|
err: err.message,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
buttons,
|
||||||
|
defaultId: indexOfReopen,
|
||||||
|
noLink: true,
|
||||||
|
},
|
||||||
|
).then(({response}) => {
|
||||||
|
let child;
|
||||||
|
switch (response) {
|
||||||
|
case indexOfShowDetails:
|
||||||
|
child = this.openDetachedExternal(file);
|
||||||
|
if (child) {
|
||||||
|
child.on(
|
||||||
|
'error',
|
||||||
|
(spawnError) => {
|
||||||
|
log.error(spawnError);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
child.unref();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case indexOfReopen:
|
||||||
|
app.relaunch();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
app.exit(-1);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private openDetachedExternal = (url: string) => {
|
||||||
|
const spawnOption = {detached: true, stdio: 'ignore' as const};
|
||||||
|
switch (process.platform) {
|
||||||
|
case 'win32':
|
||||||
|
return spawn('cmd', ['/C', 'start', url], spawnOption);
|
||||||
|
case 'darwin':
|
||||||
|
return spawn('open', [url], spawnOption);
|
||||||
|
case 'linux':
|
||||||
|
return spawn('xdg-open', [url], spawnOption);
|
||||||
|
default:
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private createErrorReport = (err: Error) => {
|
||||||
|
// eslint-disable-next-line no-undef
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
return `Application: ${app.name} ${app.getVersion()}${__HASH_VERSION__ ? ` [commit: ${__HASH_VERSION__}]` : ''}\n` +
|
||||||
|
`Platform: ${os.type()} ${os.release()} ${os.arch()}\n` +
|
||||||
|
`${err.stack}`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const criticalErrorHandler = new CriticalErrorHandler();
|
const criticalErrorHandler = new CriticalErrorHandler();
|
||||||
|
@@ -6,7 +6,7 @@ import fs from 'fs';
|
|||||||
|
|
||||||
import {shell, dialog} from 'electron';
|
import {shell, dialog} from 'electron';
|
||||||
|
|
||||||
import WindowManager from './windows/windowManager';
|
import MainWindow from './windows/mainWindow';
|
||||||
|
|
||||||
import {AllowProtocolDialog} from './allowProtocolDialog';
|
import {AllowProtocolDialog} from './allowProtocolDialog';
|
||||||
|
|
||||||
@@ -42,8 +42,8 @@ jest.mock('common/Validator', () => ({
|
|||||||
validateAllowedProtocols: (protocols) => protocols,
|
validateAllowedProtocols: (protocols) => protocols,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./windows/windowManager', () => ({
|
jest.mock('./windows/mainWindow', () => ({
|
||||||
getMainWindow: jest.fn(),
|
get: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
@@ -117,7 +117,7 @@ describe('main/allowProtocolDialog', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should not open message box if main window is missing', () => {
|
it('should not open message box if main window is missing', () => {
|
||||||
WindowManager.getMainWindow.mockImplementation(() => null);
|
MainWindow.get.mockImplementation(() => null);
|
||||||
allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com');
|
allowProtocolDialog.handleDialogEvent('mattermost:', 'mattermost://community.mattermost.com');
|
||||||
expect(shell.openExternal).not.toBeCalled();
|
expect(shell.openExternal).not.toBeCalled();
|
||||||
expect(dialog.showMessageBox).not.toBeCalled();
|
expect(dialog.showMessageBox).not.toBeCalled();
|
||||||
@@ -125,7 +125,7 @@ describe('main/allowProtocolDialog', () => {
|
|||||||
|
|
||||||
describe('main window not null', () => {
|
describe('main window not null', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WindowManager.getMainWindow.mockImplementation(() => ({}));
|
MainWindow.get.mockImplementation(() => ({}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open the window but not save when clicking Yes', async () => {
|
it('should open the window but not save when clicking Yes', async () => {
|
||||||
|
@@ -13,7 +13,7 @@ import {localizeMessage} from 'main/i18nManager';
|
|||||||
import buildConfig from 'common/config/buildConfig';
|
import buildConfig from 'common/config/buildConfig';
|
||||||
import * as Validator from 'common/Validator';
|
import * as Validator from 'common/Validator';
|
||||||
|
|
||||||
import WindowManager from './windows/windowManager';
|
import MainWindow from './windows/mainWindow';
|
||||||
import {allowedProtocolFile} from './constants';
|
import {allowedProtocolFile} from './constants';
|
||||||
|
|
||||||
export class AllowProtocolDialog {
|
export class AllowProtocolDialog {
|
||||||
@@ -47,7 +47,7 @@ export class AllowProtocolDialog {
|
|||||||
shell.openExternal(URL);
|
shell.openExternal(URL);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -4,6 +4,7 @@
|
|||||||
import {app, dialog} from 'electron';
|
import {app, dialog} from 'electron';
|
||||||
|
|
||||||
import CertificateStore from 'main/certificateStore';
|
import CertificateStore from 'main/certificateStore';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
import {handleAppWillFinishLaunching, handleAppCertificateError, certificateErrorCallbacks} from 'main/app/app';
|
import {handleAppWillFinishLaunching, handleAppCertificateError, certificateErrorCallbacks} from 'main/app/app';
|
||||||
@@ -45,13 +46,15 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
}));
|
}));
|
||||||
jest.mock('main/tray/tray', () => ({}));
|
jest.mock('main/tray/tray', () => ({}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getMainWindow: jest.fn(),
|
|
||||||
getViewNameByWebContentsId: jest.fn(),
|
getViewNameByWebContentsId: jest.fn(),
|
||||||
getServerNameByWebContentsId: jest.fn(),
|
getServerNameByWebContentsId: jest.fn(),
|
||||||
viewManager: {
|
viewManager: {
|
||||||
views: new Map(),
|
views: new Map(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
describe('main/app/app', () => {
|
describe('main/app/app', () => {
|
||||||
describe('handleAppWillFinishLaunching', () => {
|
describe('handleAppWillFinishLaunching', () => {
|
||||||
@@ -103,7 +106,7 @@ describe('main/app/app', () => {
|
|||||||
const certificate = {};
|
const certificate = {};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
WindowManager.getMainWindow.mockReturnValue(mainWindow);
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
WindowManager.getServerNameByWebContentsId.mockReturnValue('test-team');
|
WindowManager.getServerNameByWebContentsId.mockReturnValue('test-team');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -12,6 +12,7 @@ import CertificateStore from 'main/certificateStore';
|
|||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
import {destroyTray} from 'main/tray/tray';
|
import {destroyTray} from 'main/tray/tray';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {getDeeplinkingURL, openDeepLink, resizeScreen} from './utils';
|
import {getDeeplinkingURL, openDeepLink, resizeScreen} from './utils';
|
||||||
|
|
||||||
@@ -115,7 +116,7 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
|
|||||||
certificateErrorCallbacks.set(errorID, callback);
|
certificateErrorCallbacks.set(errorID, callback);
|
||||||
|
|
||||||
// TODO: should we move this to window manager or provide a handler for dialogs?
|
// TODO: should we move this to window manager or provide a handler for dialogs?
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -61,9 +61,11 @@ export function handleConfigUpdate(newConfig: CombinedConfig) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updateServerInfos(newConfig.teams);
|
if (app.isReady()) {
|
||||||
WindowManager.initializeCurrentServerName();
|
updateServerInfos(newConfig.teams);
|
||||||
handleMainWindowIsShown();
|
WindowManager.initializeCurrentServerName();
|
||||||
|
handleMainWindowIsShown();
|
||||||
|
}
|
||||||
|
|
||||||
handleUpdateMenuEvent();
|
handleUpdateMenuEvent();
|
||||||
if (newConfig.trayIconTheme) {
|
if (newConfig.trayIconTheme) {
|
||||||
|
@@ -9,6 +9,7 @@ import Config from 'common/config';
|
|||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
import parseArgs from 'main/ParseArgs';
|
import parseArgs from 'main/ParseArgs';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
import {initialize} from './initialize';
|
import {initialize} from './initialize';
|
||||||
@@ -138,8 +139,7 @@ jest.mock('main/badge', () => ({
|
|||||||
}));
|
}));
|
||||||
jest.mock('main/certificateManager', () => ({}));
|
jest.mock('main/certificateManager', () => ({}));
|
||||||
jest.mock('main/CriticalErrorHandler', () => ({
|
jest.mock('main/CriticalErrorHandler', () => ({
|
||||||
processUncaughtExceptionHandler: jest.fn(),
|
init: jest.fn(),
|
||||||
setMainWindow: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
jest.mock('main/notifications', () => ({
|
jest.mock('main/notifications', () => ({
|
||||||
displayDownloadCompleted: jest.fn(),
|
displayDownloadCompleted: jest.fn(),
|
||||||
@@ -157,13 +157,17 @@ jest.mock('main/UserActivityMonitor', () => ({
|
|||||||
startMonitoring: jest.fn(),
|
startMonitoring: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getMainWindow: jest.fn(),
|
|
||||||
showMainWindow: jest.fn(),
|
showMainWindow: jest.fn(),
|
||||||
sendToMattermostViews: jest.fn(),
|
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
getServerNameByWebContentsId: jest.fn(),
|
getServerNameByWebContentsId: jest.fn(),
|
||||||
getServerURLFromWebContentsId: jest.fn(),
|
getServerURLFromWebContentsId: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/settingsWindow', () => ({
|
||||||
|
show: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
const originalProcess = process;
|
const originalProcess = process;
|
||||||
describe('main/app/initialize', () => {
|
describe('main/app/initialize', () => {
|
||||||
beforeAll(() => {
|
beforeAll(() => {
|
||||||
@@ -269,7 +273,7 @@ describe('main/app/initialize', () => {
|
|||||||
expect(callback).toHaveBeenCalledWith(false);
|
expect(callback).toHaveBeenCalledWith(false);
|
||||||
|
|
||||||
callback = jest.fn();
|
callback = jest.fn();
|
||||||
WindowManager.getMainWindow.mockReturnValue({webContents: {id: 1}});
|
MainWindow.get.mockReturnValue({webContents: {id: 1}});
|
||||||
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
|
||||||
cb({id: 1, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
|
cb({id: 1, getURL: () => 'http://server-1.com'}, 'openExternal', callback);
|
||||||
});
|
});
|
||||||
|
@@ -52,10 +52,12 @@ import CriticalErrorHandler from 'main/CriticalErrorHandler';
|
|||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import i18nManager from 'main/i18nManager';
|
import i18nManager from 'main/i18nManager';
|
||||||
import parseArgs from 'main/ParseArgs';
|
import parseArgs from 'main/ParseArgs';
|
||||||
|
import SettingsWindow from 'main/windows/settingsWindow';
|
||||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||||
import {refreshTrayImages, setupTray} from 'main/tray/tray';
|
import {refreshTrayImages, setupTray} from 'main/tray/tray';
|
||||||
import UserActivityMonitor from 'main/UserActivityMonitor';
|
import UserActivityMonitor from 'main/UserActivityMonitor';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {protocols} from '../../../electron-builder.json';
|
import {protocols} from '../../../electron-builder.json';
|
||||||
|
|
||||||
@@ -105,7 +107,7 @@ export const mainProtocol = protocols?.[0]?.schemes?.[0];
|
|||||||
* Main entry point for the application, ensures that everything initializes in the proper order
|
* Main entry point for the application, ensures that everything initializes in the proper order
|
||||||
*/
|
*/
|
||||||
export async function initialize() {
|
export async function initialize() {
|
||||||
process.on('uncaughtException', CriticalErrorHandler.processUncaughtExceptionHandler.bind(CriticalErrorHandler));
|
CriticalErrorHandler.init();
|
||||||
global.willAppQuit = false;
|
global.willAppQuit = false;
|
||||||
|
|
||||||
// initialization that can run before the app is ready
|
// initialization that can run before the app is ready
|
||||||
@@ -258,7 +260,7 @@ function initializeInterCommunicationEventListeners() {
|
|||||||
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
|
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
|
||||||
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
|
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
|
||||||
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
|
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
|
||||||
ipcMain.on(SHOW_SETTINGS_WINDOW, WindowManager.showSettingsWindow);
|
ipcMain.on(SHOW_SETTINGS_WINDOW, SettingsWindow.show);
|
||||||
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
|
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
|
||||||
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
|
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
|
||||||
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
|
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
|
||||||
@@ -344,8 +346,6 @@ function initializeAfterAppReady() {
|
|||||||
|
|
||||||
WindowManager.showMainWindow(deeplinkingURL);
|
WindowManager.showMainWindow(deeplinkingURL);
|
||||||
|
|
||||||
CriticalErrorHandler.setMainWindow(WindowManager.getMainWindow()!);
|
|
||||||
|
|
||||||
// listen for status updates and pass on to renderer
|
// listen for status updates and pass on to renderer
|
||||||
UserActivityMonitor.on('status', (status) => {
|
UserActivityMonitor.on('status', (status) => {
|
||||||
log.debug('Initialize.UserActivityMonitor.on(status)', status);
|
log.debug('Initialize.UserActivityMonitor.on(status)', status);
|
||||||
@@ -396,7 +396,7 @@ function initializeAfterAppReady() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// is the request coming from the renderer?
|
// is the request coming from the renderer?
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (mainWindow && webContents.id === mainWindow.webContents.id) {
|
if (mainWindow && webContents.id === mainWindow.webContents.id) {
|
||||||
callback(true);
|
callback(true);
|
||||||
return;
|
return;
|
||||||
|
@@ -5,6 +5,7 @@ import Config from 'common/config';
|
|||||||
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
@@ -33,10 +34,12 @@ jest.mock('main/views/modalManager', () => ({
|
|||||||
addModal: jest.fn(),
|
addModal: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getMainWindow: jest.fn(),
|
|
||||||
switchServer: jest.fn(),
|
switchServer: jest.fn(),
|
||||||
switchTab: jest.fn(),
|
switchTab: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('./app', () => ({}));
|
jest.mock('./app', () => ({}));
|
||||||
jest.mock('./utils', () => ({
|
jest.mock('./utils', () => ({
|
||||||
@@ -111,7 +114,7 @@ describe('main/app/intercom', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getLocalURLString.mockReturnValue('/some/index.html');
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
WindowManager.getMainWindow.mockReturnValue({});
|
MainWindow.get.mockReturnValue({});
|
||||||
|
|
||||||
Config.set.mockImplementation((name, value) => {
|
Config.set.mockImplementation((name, value) => {
|
||||||
Config[name] = value;
|
Config[name] = value;
|
||||||
@@ -150,7 +153,7 @@ describe('main/app/intercom', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getLocalURLString.mockReturnValue('/some/index.html');
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
WindowManager.getMainWindow.mockReturnValue({});
|
MainWindow.get.mockReturnValue({});
|
||||||
|
|
||||||
Config.set.mockImplementation((name, value) => {
|
Config.set.mockImplementation((name, value) => {
|
||||||
Config[name] = value;
|
Config[name] = value;
|
||||||
@@ -193,7 +196,7 @@ describe('main/app/intercom', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getLocalURLString.mockReturnValue('/some/index.html');
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
WindowManager.getMainWindow.mockReturnValue({});
|
MainWindow.get.mockReturnValue({});
|
||||||
|
|
||||||
Config.set.mockImplementation((name, value) => {
|
Config.set.mockImplementation((name, value) => {
|
||||||
Config[name] = value;
|
Config[name] = value;
|
||||||
@@ -242,7 +245,7 @@ describe('main/app/intercom', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
getLocalURLString.mockReturnValue('/some/index.html');
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
WindowManager.getMainWindow.mockReturnValue({});
|
MainWindow.get.mockReturnValue({});
|
||||||
|
|
||||||
Config.set.mockImplementation((name, value) => {
|
Config.set.mockImplementation((name, value) => {
|
||||||
Config[name] = value;
|
Config[name] = value;
|
||||||
@@ -263,7 +266,7 @@ describe('main/app/intercom', () => {
|
|||||||
it('MM-48079 should not show onboarding screen or server screen if GPO server is pre-configured', () => {
|
it('MM-48079 should not show onboarding screen or server screen if GPO server is pre-configured', () => {
|
||||||
getLocalURLString.mockReturnValue('/some/index.html');
|
getLocalURLString.mockReturnValue('/some/index.html');
|
||||||
getLocalPreload.mockReturnValue('/some/preload.js');
|
getLocalPreload.mockReturnValue('/some/preload.js');
|
||||||
WindowManager.getMainWindow.mockReturnValue({
|
MainWindow.get.mockReturnValue({
|
||||||
isVisible: () => true,
|
isVisible: () => true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ import {displayMention} from 'main/notifications';
|
|||||||
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {handleAppBeforeQuit} from './app';
|
import {handleAppBeforeQuit} from './app';
|
||||||
import {updateServerInfos} from './utils';
|
import {updateServerInfos} from './utils';
|
||||||
@@ -121,7 +122,7 @@ export function handleMainWindowIsShown() {
|
|||||||
* calls of this function will notification re-evaluate the booleans passed to "handleShowOnboardingScreens".
|
* calls of this function will notification re-evaluate the booleans passed to "handleShowOnboardingScreens".
|
||||||
*/
|
*/
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
|
|
||||||
log.debug('intercom.handleMainWindowIsShown', {configTeams: Config.teams, showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)});
|
log.debug('intercom.handleMainWindowIsShown', {configTeams: Config.teams, showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)});
|
||||||
if (mainWindow?.isVisible()) {
|
if (mainWindow?.isVisible()) {
|
||||||
@@ -140,7 +141,7 @@ export function handleNewServerModal() {
|
|||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -172,7 +173,7 @@ export function handleEditServerModal(e: IpcMainEvent, name: string) {
|
|||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -214,7 +215,7 @@ export function handleRemoveServerModal(e: IpcMainEvent, name: string) {
|
|||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -254,7 +255,7 @@ export function handleWelcomeScreenModal() {
|
|||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -293,7 +294,7 @@ export function handleOpenAppMenu() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
windowMenu.popup({
|
windowMenu.popup({
|
||||||
window: WindowManager.getMainWindow(),
|
window: MainWindow.get(),
|
||||||
x: 18,
|
x: 18,
|
||||||
y: 18,
|
y: 18,
|
||||||
});
|
});
|
||||||
|
@@ -60,6 +60,7 @@ jest.mock('main/server/serverInfo', () => ({
|
|||||||
ServerInfo: jest.fn(),
|
ServerInfo: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('main/tray/tray', () => ({}));
|
jest.mock('main/tray/tray', () => ({}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({}));
|
||||||
jest.mock('main/windows/windowManager', () => ({}));
|
jest.mock('main/windows/windowManager', () => ({}));
|
||||||
|
|
||||||
jest.mock('./initialize', () => ({
|
jest.mock('./initialize', () => ({
|
||||||
|
@@ -28,6 +28,7 @@ import {createMenu as createTrayMenu} from 'main/menus/tray';
|
|||||||
import {ServerInfo} from 'main/server/serverInfo';
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
import {setTrayMenu} from 'main/tray/tray';
|
import {setTrayMenu} from 'main/tray/tray';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {mainProtocol} from './initialize';
|
import {mainProtocol} from './initialize';
|
||||||
|
|
||||||
@@ -140,7 +141,7 @@ export function wasUpdated(lastAppVersion?: string) {
|
|||||||
|
|
||||||
export function clearAppCache() {
|
export function clearAppCache() {
|
||||||
// TODO: clear cache on browserviews, not in the renderer.
|
// TODO: clear cache on browserviews, not in the renderer.
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (mainWindow) {
|
if (mainWindow) {
|
||||||
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
mainWindow.webContents.session.clearCache().then(mainWindow.reload);
|
||||||
} else {
|
} else {
|
||||||
|
@@ -3,6 +3,7 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {AuthManager} from 'main/authManager';
|
import {AuthManager} from 'main/authManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
|
|
||||||
@@ -84,8 +85,11 @@ jest.mock('main/trustedOrigins', () => ({
|
|||||||
save: jest.fn(),
|
save: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn().mockImplementation(() => ({})),
|
||||||
|
}));
|
||||||
|
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getMainWindow: jest.fn().mockImplementation(() => ({})),
|
|
||||||
getServerURLFromWebContentsId: jest.fn(),
|
getServerURLFromWebContentsId: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
@@ -151,7 +155,7 @@ describe('main/authManager', () => {
|
|||||||
const authManager = new AuthManager();
|
const authManager = new AuthManager();
|
||||||
|
|
||||||
it('should not pop modal when no main window exists', () => {
|
it('should not pop modal when no main window exists', () => {
|
||||||
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
MainWindow.get.mockImplementationOnce(() => null);
|
||||||
authManager.popLoginModal({url: 'http://anormalurl.com'}, {
|
authManager.popLoginModal({url: 'http://anormalurl.com'}, {
|
||||||
isProxy: false,
|
isProxy: false,
|
||||||
host: 'anormalurl',
|
host: 'anormalurl',
|
||||||
@@ -219,7 +223,7 @@ describe('main/authManager', () => {
|
|||||||
const authManager = new AuthManager();
|
const authManager = new AuthManager();
|
||||||
|
|
||||||
it('should not pop modal when no main window exists', () => {
|
it('should not pop modal when no main window exists', () => {
|
||||||
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
MainWindow.get.mockImplementationOnce(() => null);
|
||||||
authManager.popPermissionModal({url: 'http://anormalurl.com'}, {
|
authManager.popPermissionModal({url: 'http://anormalurl.com'}, {
|
||||||
isProxy: false,
|
isProxy: false,
|
||||||
host: 'anormalurl',
|
host: 'anormalurl',
|
||||||
|
@@ -13,6 +13,7 @@ import modalManager from 'main/views/modalManager';
|
|||||||
import TrustedOriginsStore from 'main/trustedOrigins';
|
import TrustedOriginsStore from 'main/trustedOrigins';
|
||||||
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
import {getLocalURLString, getLocalPreload} from 'main/utils';
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
const loginModalHtml = getLocalURLString('loginModal.html');
|
const loginModalHtml = getLocalURLString('loginModal.html');
|
||||||
@@ -52,7 +53,7 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popLoginModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo) => {
|
popLoginModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo) => {
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -71,7 +72,7 @@ export class AuthManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popPermissionModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo, permission: PermissionType) => {
|
popPermissionModal = (request: AuthenticationResponseDetails, authInfo: AuthInfo, permission: PermissionType) => {
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -2,24 +2,29 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {app} from 'electron';
|
import {app, nativeImage} from 'electron';
|
||||||
|
|
||||||
|
import MainWindow from './windows/mainWindow';
|
||||||
|
|
||||||
import * as Badge from './badge';
|
import * as Badge from './badge';
|
||||||
|
|
||||||
import WindowManager from './windows/windowManager';
|
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
jest.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
dock: {
|
dock: {
|
||||||
setBadge: jest.fn(),
|
setBadge: jest.fn(),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
nativeImage: {
|
||||||
|
createFromDataURL: jest.fn(),
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('./appState', () => ({
|
jest.mock('./appState', () => ({
|
||||||
updateBadge: jest.fn(),
|
updateBadge: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('./windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('./windows/windowManager', () => ({
|
jest.mock('./windows/windowManager', () => ({
|
||||||
setOverlayIcon: jest.fn(),
|
setOverlayIcon: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -30,35 +35,69 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
|
|
||||||
describe('main/badge', () => {
|
describe('main/badge', () => {
|
||||||
describe('showBadgeWindows', () => {
|
describe('showBadgeWindows', () => {
|
||||||
it('should show dot when session expired', () => {
|
const window = {
|
||||||
|
setOverlayIcon: jest.fn(),
|
||||||
|
webContents: {
|
||||||
|
executeJavaScript: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
let promise;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
window.webContents.executeJavaScript.mockImplementation((code) => {
|
||||||
|
promise = new Promise((resolve) => resolve(code));
|
||||||
|
return promise;
|
||||||
|
});
|
||||||
|
nativeImage.createFromDataURL.mockImplementation((url) => url);
|
||||||
|
Badge.setUnreadBadgeSetting(false);
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show dot when session expired', async () => {
|
||||||
Badge.showBadgeWindows(true, 0, false);
|
Badge.showBadgeWindows(true, 0, false);
|
||||||
expect(WindowManager.setOverlayIcon).toBeCalledWith('•', expect.any(String), expect.any(Boolean));
|
await promise;
|
||||||
|
expect(window.setOverlayIcon).toBeCalledWith(expect.stringContaining('window.drawBadge(\'•\', false)'), expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show mention count when has mention count', () => {
|
it('should show mention count when has mention count', async () => {
|
||||||
Badge.showBadgeWindows(true, 50, false);
|
Badge.showBadgeWindows(true, 50, false);
|
||||||
expect(WindowManager.setOverlayIcon).toBeCalledWith('50', expect.any(String), false);
|
await promise;
|
||||||
|
expect(window.setOverlayIcon).toBeCalledWith(expect.stringContaining('window.drawBadge(\'50\', false)'), expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show 99+ when has mention count over 99', () => {
|
it('should show 99+ when has mention count over 99', async () => {
|
||||||
Badge.showBadgeWindows(true, 200, false);
|
Badge.showBadgeWindows(true, 200, false);
|
||||||
expect(WindowManager.setOverlayIcon).toBeCalledWith('99+', expect.any(String), true);
|
await promise;
|
||||||
|
expect(window.setOverlayIcon).toBeCalledWith(expect.stringContaining('window.drawBadge(\'99+\', true)'), expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not show dot when has unreads but setting is off', () => {
|
it('should not show dot when has unreads but setting is off', async () => {
|
||||||
Badge.showBadgeWindows(false, 0, true);
|
Badge.showBadgeWindows(false, 0, true);
|
||||||
expect(WindowManager.setOverlayIcon).not.toBeCalledWith('•', expect.any(String), expect.any(Boolean));
|
await promise;
|
||||||
|
expect(window.setOverlayIcon).toBeCalledWith(null, expect.any(String));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show dot when has unreads', () => {
|
it('should show dot when has unreads', async () => {
|
||||||
Badge.setUnreadBadgeSetting(true);
|
Badge.setUnreadBadgeSetting(true);
|
||||||
Badge.showBadgeWindows(false, 0, true);
|
Badge.showBadgeWindows(false, 0, true);
|
||||||
expect(WindowManager.setOverlayIcon).toBeCalledWith('•', expect.any(String), expect.any(Boolean));
|
await promise;
|
||||||
Badge.setUnreadBadgeSetting(false);
|
expect(window.setOverlayIcon).toBeCalledWith(expect.stringContaining('window.drawBadge(\'•\', false)'), expect.any(String));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showBadgeOSX', () => {
|
describe('showBadgeOSX', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Badge.setUnreadBadgeSetting(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
it('should show dot when session expired', () => {
|
it('should show dot when session expired', () => {
|
||||||
Badge.showBadgeOSX(true, 0, false);
|
Badge.showBadgeOSX(true, 0, false);
|
||||||
expect(app.dock.setBadge).toBeCalledWith('•');
|
expect(app.dock.setBadge).toBeCalledWith('•');
|
||||||
|
@@ -2,20 +2,78 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {app} from 'electron';
|
import {BrowserWindow, app, nativeImage} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {UPDATE_BADGE} from 'common/communication';
|
import {UPDATE_BADGE} from 'common/communication';
|
||||||
|
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
|
||||||
import WindowManager from './windows/windowManager';
|
|
||||||
import * as AppState from './appState';
|
import * as AppState from './appState';
|
||||||
|
import MainWindow from './windows/mainWindow';
|
||||||
|
|
||||||
const MAX_WIN_COUNT = 99;
|
const MAX_WIN_COUNT = 99;
|
||||||
|
|
||||||
let showUnreadBadgeSetting: boolean;
|
let showUnreadBadgeSetting: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Badge generation for Windows
|
||||||
|
*/
|
||||||
|
|
||||||
|
function drawBadge(text: string, small: boolean) {
|
||||||
|
const scale = 2; // should rely display dpi
|
||||||
|
const size = (small ? 20 : 16) * scale;
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.setAttribute('width', `${size}`);
|
||||||
|
canvas.setAttribute('height', `${size}`);
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
|
||||||
|
if (!ctx) {
|
||||||
|
log.error('Could not create canvas context');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// circle
|
||||||
|
ctx.fillStyle = '#FF1744'; // Material Red A400
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
||||||
|
ctx.fill();
|
||||||
|
|
||||||
|
// text
|
||||||
|
ctx.fillStyle = '#ffffff';
|
||||||
|
ctx.textAlign = 'center';
|
||||||
|
ctx.textBaseline = 'middle';
|
||||||
|
ctx.font = (11 * scale) + 'px sans-serif';
|
||||||
|
ctx.fillText(text, size / 2, size / 2, size);
|
||||||
|
|
||||||
|
return canvas.toDataURL();
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDataURL(win: BrowserWindow, text: string, small: boolean) {
|
||||||
|
// since we don't have a document/canvas object in the main process, we use the webcontents from the window to draw.
|
||||||
|
const code = `
|
||||||
|
window.drawBadge = ${drawBadge};
|
||||||
|
window.drawBadge('${text || ''}', ${small});
|
||||||
|
`;
|
||||||
|
return win.webContents.executeJavaScript(code);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function setOverlayIcon(badgeText: string | undefined, description: string, small: boolean) {
|
||||||
|
let overlay = null;
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
if (mainWindow) {
|
||||||
|
if (badgeText) {
|
||||||
|
try {
|
||||||
|
const dataUrl = await createDataURL(mainWindow, badgeText, small);
|
||||||
|
overlay = nativeImage.createFromDataURL(dataUrl);
|
||||||
|
} catch (err) {
|
||||||
|
log.error('Could not generate a badge:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
mainWindow.setOverlayIcon(overlay, description);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
export function showBadgeWindows(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||||
let description = localizeMessage('main.badge.noUnreads', 'You have no unread messages');
|
let description = localizeMessage('main.badge.noUnreads', 'You have no unread messages');
|
||||||
let text;
|
let text;
|
||||||
@@ -29,7 +87,7 @@ export function showBadgeWindows(sessionExpired: boolean, mentionCount: number,
|
|||||||
text = '•';
|
text = '•';
|
||||||
description = localizeMessage('main.badge.sessionExpired', 'Session Expired: Please sign in to continue receiving notifications.');
|
description = localizeMessage('main.badge.sessionExpired', 'Session Expired: Please sign in to continue receiving notifications.');
|
||||||
}
|
}
|
||||||
WindowManager.setOverlayIcon(text, description, mentionCount > 99);
|
setOverlayIcon(text, description, mentionCount > 99);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
export function showBadgeOSX(sessionExpired: boolean, mentionCount: number, showUnreadBadge: boolean) {
|
||||||
|
@@ -2,12 +2,12 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
import ModalManager from 'main/views/modalManager';
|
import ModalManager from 'main/views/modalManager';
|
||||||
import {CertificateManager} from 'main/certificateManager';
|
import {CertificateManager} from 'main/certificateManager';
|
||||||
|
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
getMainWindow: jest.fn().mockImplementation(() => ({})),
|
get: jest.fn().mockImplementation(() => ({})),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('main/views/modalManager', () => ({
|
jest.mock('main/views/modalManager', () => ({
|
||||||
@@ -50,7 +50,7 @@ describe('main/certificateManager', () => {
|
|||||||
const certificateManager = new CertificateManager();
|
const certificateManager = new CertificateManager();
|
||||||
|
|
||||||
it('should not pop modal when no main window exists', () => {
|
it('should not pop modal when no main window exists', () => {
|
||||||
WindowManager.getMainWindow.mockImplementationOnce(() => null);
|
MainWindow.get.mockImplementationOnce(() => null);
|
||||||
certificateManager.popCertificateModal('http://anormalurl.com', [{data: 'test 1'}, {data: 'test 2'}, {data: 'test 3'}]);
|
certificateManager.popCertificateModal('http://anormalurl.com', [{data: 'test 1'}, {data: 'test 2'}, {data: 'test 3'}]);
|
||||||
expect(ModalManager.addModal).not.toBeCalled();
|
expect(ModalManager.addModal).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
@@ -5,10 +5,9 @@ import {Certificate, WebContents} from 'electron';
|
|||||||
|
|
||||||
import {CertificateModalData} from 'types/certificate';
|
import {CertificateModalData} from 'types/certificate';
|
||||||
|
|
||||||
import WindowManager from './windows/windowManager';
|
|
||||||
|
|
||||||
import modalManager from './views/modalManager';
|
import modalManager from './views/modalManager';
|
||||||
import {getLocalURLString, getLocalPreload} from './utils';
|
import {getLocalURLString, getLocalPreload} from './utils';
|
||||||
|
import MainWindow from './windows/mainWindow';
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
const html = getLocalURLString('certificateModal.html');
|
const html = getLocalURLString('certificateModal.html');
|
||||||
@@ -39,7 +38,7 @@ export class CertificateManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
popCertificateModal = (url: string, list: Certificate[]) => {
|
popCertificateModal = (url: string, list: Certificate[]) => {
|
||||||
const mainWindow = WindowManager.getMainWindow();
|
const mainWindow = MainWindow.get();
|
||||||
if (!mainWindow) {
|
if (!mainWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@@ -3,9 +3,7 @@
|
|||||||
|
|
||||||
import Diagnostics from '.';
|
import Diagnostics from '.';
|
||||||
|
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/mainWindow', () => ({}));
|
||||||
mainWindow: {},
|
|
||||||
}));
|
|
||||||
jest.mock('common/config', () => ({
|
jest.mock('common/config', () => ({
|
||||||
configFilePath: 'mock/config/filepath/',
|
configFilePath: 'mock/config/filepath/',
|
||||||
}));
|
}));
|
||||||
|
@@ -5,7 +5,7 @@ import {ElectronLog} from 'electron-log';
|
|||||||
|
|
||||||
import {DiagnosticStepResponse} from 'types/diagnostics';
|
import {DiagnosticStepResponse} from 'types/diagnostics';
|
||||||
|
|
||||||
import windowManager from 'main/windows/windowManager';
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import DiagnosticsStep from '../DiagnosticStep';
|
import DiagnosticsStep from '../DiagnosticStep';
|
||||||
|
|
||||||
@@ -17,11 +17,11 @@ const stepDescriptiveName = 'BrowserWindowsChecks';
|
|||||||
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
|
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
|
||||||
try {
|
try {
|
||||||
/** Main window check */
|
/** Main window check */
|
||||||
if (!windowManager.mainWindowReady) {
|
if (!MainWindow.isReady) {
|
||||||
throw new Error('Main window not ready');
|
throw new Error('Main window not ready');
|
||||||
}
|
}
|
||||||
const mainWindowVisibilityStatus = browserWindowVisibilityStatus('mainWindow', windowManager.mainWindow);
|
const mainWindowVisibilityStatus = browserWindowVisibilityStatus('mainWindow', MainWindow.get());
|
||||||
const webContentsOk = webContentsCheck(windowManager.mainWindow?.webContents);
|
const webContentsOk = webContentsCheck(MainWindow.get()?.webContents);
|
||||||
|
|
||||||
if (mainWindowVisibilityStatus.some((status) => !status.ok) || !webContentsOk) {
|
if (mainWindowVisibilityStatus.some((status) => !status.ok) || !webContentsOk) {
|
||||||
return {
|
return {
|
||||||
|
@@ -76,6 +76,7 @@ jest.mock('fs', () => ({
|
|||||||
jest.mock('macos-notification-state', () => ({
|
jest.mock('macos-notification-state', () => ({
|
||||||
getDoNotDisturb: jest.fn(),
|
getDoNotDisturb: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/notifications', () => ({}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
@@ -49,10 +49,15 @@ jest.mock('macos-notification-state', () => ({
|
|||||||
jest.mock('main/i18nManager', () => ({
|
jest.mock('main/i18nManager', () => ({
|
||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/downloadsManager', () => ({
|
||||||
|
hasDownloads: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('main/diagnostics', () => ({}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
getCurrentTeamName: jest.fn(),
|
getCurrentTeamName: jest.fn(),
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/settingsWindow', () => ({}));
|
||||||
jest.mock('common/tabs/TabView', () => ({
|
jest.mock('common/tabs/TabView', () => ({
|
||||||
getTabDisplayName: (name) => name,
|
getTabDisplayName: (name) => name,
|
||||||
}));
|
}));
|
||||||
|
@@ -16,6 +16,7 @@ import WindowManager from 'main/windows/windowManager';
|
|||||||
import {UpdateManager} from 'main/autoUpdater';
|
import {UpdateManager} from 'main/autoUpdater';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
import Diagnostics from 'main/diagnostics';
|
import Diagnostics from 'main/diagnostics';
|
||||||
|
import SettingsWindow from 'main/windows/settingsWindow';
|
||||||
|
|
||||||
export function createTemplate(config: Config, updateManager: UpdateManager) {
|
export function createTemplate(config: Config, updateManager: UpdateManager) {
|
||||||
const separatorItem: MenuItemConstructorOptions = {
|
const separatorItem: MenuItemConstructorOptions = {
|
||||||
@@ -43,7 +44,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
|
|||||||
label: settingsLabel,
|
label: settingsLabel,
|
||||||
accelerator: 'CmdOrCtrl+,',
|
accelerator: 'CmdOrCtrl+,',
|
||||||
click() {
|
click() {
|
||||||
WindowManager.showSettingsWindow();
|
SettingsWindow.show();
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -9,6 +9,7 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/windows/settingsWindow', () => ({}));
|
||||||
jest.mock('main/windows/windowManager', () => ({}));
|
jest.mock('main/windows/windowManager', () => ({}));
|
||||||
|
|
||||||
describe('main/menus/tray', () => {
|
describe('main/menus/tray', () => {
|
||||||
|
@@ -8,6 +8,7 @@ import {CombinedConfig} from 'types/config';
|
|||||||
|
|
||||||
import WindowManager from 'main/windows/windowManager';
|
import WindowManager from 'main/windows/windowManager';
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
import SettingsWindow from 'main/windows/settingsWindow';
|
||||||
|
|
||||||
export function createTemplate(config: CombinedConfig) {
|
export function createTemplate(config: CombinedConfig) {
|
||||||
const teams = config.teams;
|
const teams = config.teams;
|
||||||
@@ -24,7 +25,7 @@ export function createTemplate(config: CombinedConfig) {
|
|||||||
}, {
|
}, {
|
||||||
label: process.platform === 'darwin' ? localizeMessage('main.menus.tray.preferences', 'Preferences...') : localizeMessage('main.menus.tray.settings', 'Settings'),
|
label: process.platform === 'darwin' ? localizeMessage('main.menus.tray.preferences', 'Preferences...') : localizeMessage('main.menus.tray.settings', 'Settings'),
|
||||||
click: () => {
|
click: () => {
|
||||||
WindowManager.showSettingsWindow();
|
SettingsWindow.show();
|
||||||
},
|
},
|
||||||
}, {
|
}, {
|
||||||
type: 'separator',
|
type: 'separator',
|
||||||
|
@@ -4,16 +4,18 @@
|
|||||||
'use strict';
|
'use strict';
|
||||||
import cp from 'child_process';
|
import cp from 'child_process';
|
||||||
|
|
||||||
import {Notification, shell} from 'electron';
|
import {Notification, shell, app} from 'electron';
|
||||||
|
|
||||||
import {getFocusAssist} from 'windows-focus-assist';
|
import {getFocusAssist} from 'windows-focus-assist';
|
||||||
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
||||||
|
|
||||||
import {PLAY_SOUND} from 'common/communication';
|
import {PLAY_SOUND} from 'common/communication';
|
||||||
|
import Config from 'common/config';
|
||||||
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
|
||||||
|
import MainWindow from '../windows/mainWindow';
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
|
|
||||||
import getLinuxDoNotDisturb from './dnd-linux';
|
import getLinuxDoNotDisturb from './dnd-linux';
|
||||||
@@ -55,6 +57,9 @@ jest.mock('electron', () => {
|
|||||||
return {
|
return {
|
||||||
app: {
|
app: {
|
||||||
getAppPath: () => '/path/to/app',
|
getAppPath: () => '/path/to/app',
|
||||||
|
dock: {
|
||||||
|
bounce: jest.fn(),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
Notification: NotificationMock,
|
Notification: NotificationMock,
|
||||||
shell: {
|
shell: {
|
||||||
@@ -70,7 +75,9 @@ jest.mock('windows-focus-assist', () => ({
|
|||||||
jest.mock('macos-notification-state', () => ({
|
jest.mock('macos-notification-state', () => ({
|
||||||
getDoNotDisturb: jest.fn(),
|
getDoNotDisturb: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('../windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('../windows/windowManager', () => ({
|
jest.mock('../windows/windowManager', () => ({
|
||||||
getServerNameByWebContentsId: () => 'server_name',
|
getServerNameByWebContentsId: () => 'server_name',
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
@@ -82,12 +89,25 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
localizeMessage: jest.fn(),
|
localizeMessage: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('common/config', () => ({}));
|
||||||
|
|
||||||
describe('main/notifications', () => {
|
describe('main/notifications', () => {
|
||||||
describe('displayMention', () => {
|
describe('displayMention', () => {
|
||||||
|
const mainWindow = {
|
||||||
|
flashFrame: jest.fn(),
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
Notification.isSupported.mockImplementation(() => true);
|
Notification.isSupported.mockImplementation(() => true);
|
||||||
getFocusAssist.mockReturnValue({value: false});
|
getFocusAssist.mockReturnValue({value: false});
|
||||||
getDarwinDoNotDisturb.mockReturnValue(false);
|
getDarwinDoNotDisturb.mockReturnValue(false);
|
||||||
|
Config.notifications = {};
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
jest.resetAllMocks();
|
||||||
|
Config.notifications = {};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should do nothing when Notification is not supported', () => {
|
it('should do nothing when Notification is not supported', () => {
|
||||||
@@ -220,11 +240,108 @@ describe('main/notifications', () => {
|
|||||||
);
|
);
|
||||||
const mention = mentions.find((m) => m.body === 'mention_click_body');
|
const mention = mentions.find((m) => m.body === 'mention_click_body');
|
||||||
mention.value.click();
|
mention.value.click();
|
||||||
expect(WindowManager.switchTab).toHaveBeenCalledWith('server_name', TAB_MESSAGING);
|
expect(WindowManager.switchTab).toBeCalledWith('server_name', TAB_MESSAGING);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('linux/windows - should not flash frame when config item is not set', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'linux',
|
||||||
|
});
|
||||||
|
displayMention(
|
||||||
|
'click_test',
|
||||||
|
'mention_click_body',
|
||||||
|
{id: 'channel_id'},
|
||||||
|
'team_id',
|
||||||
|
'http://server-1.com/team_id/channel_id',
|
||||||
|
false,
|
||||||
|
{id: 1, send: jest.fn()},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
expect(mainWindow.flashFrame).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('linux/windows - should flash frame when config item is set', () => {
|
||||||
|
Config.notifications = {
|
||||||
|
flashWindow: true,
|
||||||
|
};
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'linux',
|
||||||
|
});
|
||||||
|
displayMention(
|
||||||
|
'click_test',
|
||||||
|
'mention_click_body',
|
||||||
|
{id: 'channel_id'},
|
||||||
|
'team_id',
|
||||||
|
'http://server-1.com/team_id/channel_id',
|
||||||
|
false,
|
||||||
|
{id: 1, send: jest.fn()},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
expect(mainWindow.flashFrame).toBeCalledWith(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mac - should not bounce icon when config item is not set', () => {
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'darwin',
|
||||||
|
});
|
||||||
|
displayMention(
|
||||||
|
'click_test',
|
||||||
|
'mention_click_body',
|
||||||
|
{id: 'channel_id'},
|
||||||
|
'team_id',
|
||||||
|
'http://server-1.com/team_id/channel_id',
|
||||||
|
false,
|
||||||
|
{id: 1, send: jest.fn()},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
expect(app.dock.bounce).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('mac - should bounce icon when config item is set', () => {
|
||||||
|
Config.notifications = {
|
||||||
|
bounceIcon: true,
|
||||||
|
bounceIconType: 'critical',
|
||||||
|
};
|
||||||
|
const originalPlatform = process.platform;
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: 'darwin',
|
||||||
|
});
|
||||||
|
displayMention(
|
||||||
|
'click_test',
|
||||||
|
'mention_click_body',
|
||||||
|
{id: 'channel_id'},
|
||||||
|
'team_id',
|
||||||
|
'http://server-1.com/team_id/channel_id',
|
||||||
|
false,
|
||||||
|
{id: 1, send: jest.fn()},
|
||||||
|
{},
|
||||||
|
);
|
||||||
|
Object.defineProperty(process, 'platform', {
|
||||||
|
value: originalPlatform,
|
||||||
|
});
|
||||||
|
expect(app.dock.bounce).toHaveBeenCalledWith('critical');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('displayDownloadCompleted', () => {
|
describe('displayDownloadCompleted', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
Notification.isSupported.mockImplementation(() => true);
|
||||||
|
getFocusAssist.mockReturnValue({value: false});
|
||||||
|
getDarwinDoNotDisturb.mockReturnValue(false);
|
||||||
|
});
|
||||||
|
|
||||||
it('should open file when clicked', () => {
|
it('should open file when clicked', () => {
|
||||||
getDarwinDoNotDisturb.mockReturnValue(false);
|
getDarwinDoNotDisturb.mockReturnValue(false);
|
||||||
localizeMessage.mockReturnValue('test_filename');
|
localizeMessage.mockReturnValue('test_filename');
|
||||||
|
@@ -1,16 +1,18 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {shell, Notification} from 'electron';
|
import {app, shell, Notification} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
|
||||||
|
|
||||||
import {MentionData} from 'types/notification';
|
import {MentionData} from 'types/notification';
|
||||||
|
|
||||||
|
import Config from 'common/config';
|
||||||
import {PLAY_SOUND} from 'common/communication';
|
import {PLAY_SOUND} from 'common/communication';
|
||||||
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
import {TAB_MESSAGING} from 'common/tabs/TabView';
|
||||||
|
|
||||||
|
import MainWindow from '../windows/mainWindow';
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
|
|
||||||
import {Mention} from './Mention';
|
import {Mention} from './Mention';
|
||||||
@@ -61,7 +63,7 @@ export function displayMention(title: string, body: string, channel: {id: string
|
|||||||
if (notificationSound) {
|
if (notificationSound) {
|
||||||
WindowManager.sendToRenderer(PLAY_SOUND, notificationSound);
|
WindowManager.sendToRenderer(PLAY_SOUND, notificationSound);
|
||||||
}
|
}
|
||||||
WindowManager.flashFrame(true);
|
flashFrame(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
mention.on('click', () => {
|
mention.on('click', () => {
|
||||||
@@ -89,7 +91,7 @@ export function displayDownloadCompleted(fileName: string, path: string, serverN
|
|||||||
const download = new DownloadNotification(fileName, serverName);
|
const download = new DownloadNotification(fileName, serverName);
|
||||||
|
|
||||||
download.on('show', () => {
|
download.on('show', () => {
|
||||||
WindowManager.flashFrame(true);
|
flashFrame(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
download.on('click', () => {
|
download.on('click', () => {
|
||||||
@@ -153,3 +155,14 @@ function getDoNotDisturb() {
|
|||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function flashFrame(flash: boolean) {
|
||||||
|
if (process.platform === 'linux' || process.platform === 'win32') {
|
||||||
|
if (Config.notifications.flashWindow) {
|
||||||
|
MainWindow.get()?.flashFrame(flash);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (process.platform === 'darwin' && Config.notifications.bounceIcon) {
|
||||||
|
app.dock.bounce(Config.notifications.bounceIconType);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -46,8 +46,8 @@ jest.mock('electron', () => {
|
|||||||
jest.mock('main/app/utils', () => ({
|
jest.mock('main/app/utils', () => ({
|
||||||
handleUpdateMenuEvent: jest.fn(),
|
handleUpdateMenuEvent: jest.fn(),
|
||||||
updateSpellCheckerLocales: jest.fn(),
|
updateSpellCheckerLocales: jest.fn(),
|
||||||
updateServerInfos: jest.fn(),
|
|
||||||
setLoggingLevel: jest.fn(),
|
setLoggingLevel: jest.fn(),
|
||||||
|
updateServerInfos: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('main/app/intercom', () => ({
|
jest.mock('main/app/intercom', () => ({
|
||||||
handleMainWindowIsShown: jest.fn(),
|
handleMainWindowIsShown: jest.fn(),
|
||||||
|
@@ -8,8 +8,9 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron';
|
|||||||
import {UPDATE_TRAY} from 'common/communication';
|
import {UPDATE_TRAY} from 'common/communication';
|
||||||
|
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
import WindowManager from 'main/windows/windowManager';
|
||||||
|
|
||||||
import WindowManager from '../windows/windowManager';
|
|
||||||
import * as AppState from '../appState';
|
import * as AppState from '../appState';
|
||||||
|
|
||||||
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
const assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||||
@@ -85,9 +86,10 @@ export function setupTray(iconTheme: string) {
|
|||||||
|
|
||||||
trayIcon.setToolTip(app.name);
|
trayIcon.setToolTip(app.name);
|
||||||
trayIcon.on('click', () => {
|
trayIcon.on('click', () => {
|
||||||
if (WindowManager.mainWindow!.isVisible()) {
|
const mainWindow = MainWindow.get(true)!;
|
||||||
WindowManager.mainWindow!.blur(); // To move focus to the next top-level window in Windows
|
if (mainWindow.isVisible()) {
|
||||||
WindowManager.mainWindow!.hide();
|
mainWindow.blur(); // To move focus to the next top-level window in Windows
|
||||||
|
mainWindow.hide();
|
||||||
} else {
|
} else {
|
||||||
WindowManager.restoreMain();
|
WindowManager.restoreMain();
|
||||||
}
|
}
|
||||||
|
@@ -18,6 +18,10 @@ import {BACK_BAR_HEIGHT, customLoginRegexPaths, PRODUCTION, TAB_BAR_HEIGHT} from
|
|||||||
import UrlUtils from 'common/utils/url';
|
import UrlUtils from 'common/utils/url';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
|
|
||||||
|
export function isInsideRectangle(container: Electron.Rectangle, rect: Electron.Rectangle) {
|
||||||
|
return container.x <= rect.x && container.y <= rect.y && container.width >= rect.width && container.height >= rect.height;
|
||||||
|
}
|
||||||
|
|
||||||
export function shouldBeHiddenOnStartup(parsedArgv: Args) {
|
export function shouldBeHiddenOnStartup(parsedArgv: Args) {
|
||||||
if (parsedArgv.hidden) {
|
if (parsedArgv.hidden) {
|
||||||
return true;
|
return true;
|
||||||
|
@@ -7,6 +7,7 @@ import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communi
|
|||||||
import {MattermostServer} from 'common/servers/MattermostServer';
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
import MessagingTabView from 'common/tabs/MessagingTabView';
|
import MessagingTabView from 'common/tabs/MessagingTabView';
|
||||||
|
|
||||||
|
import MainWindow from '../windows/mainWindow';
|
||||||
import * as WindowManager from '../windows/windowManager';
|
import * as WindowManager from '../windows/windowManager';
|
||||||
import * as appState from '../appState';
|
import * as appState from '../appState';
|
||||||
import Utils from '../utils';
|
import Utils from '../utils';
|
||||||
@@ -30,9 +31,12 @@ jest.mock('electron', () => ({
|
|||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('../windows/mainWindow', () => ({
|
||||||
|
focusThreeDotMenu: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('../windows/windowManager', () => ({
|
jest.mock('../windows/windowManager', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
focusThreeDotMenu: jest.fn(),
|
|
||||||
}));
|
}));
|
||||||
jest.mock('../appState', () => ({
|
jest.mock('../appState', () => ({
|
||||||
updateMentions: jest.fn(),
|
updateMentions: jest.fn(),
|
||||||
@@ -54,9 +58,10 @@ const tabView = new MessagingTabView(server);
|
|||||||
describe('main/views/MattermostView', () => {
|
describe('main/views/MattermostView', () => {
|
||||||
describe('load', () => {
|
describe('load', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.loadSuccess = jest.fn();
|
mattermostView.loadSuccess = jest.fn();
|
||||||
mattermostView.loadRetry = jest.fn();
|
mattermostView.loadRetry = jest.fn();
|
||||||
});
|
});
|
||||||
@@ -112,11 +117,12 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('retry', () => {
|
describe('retry', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
const retryInBackgroundFn = jest.fn();
|
const retryInBackgroundFn = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve());
|
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve());
|
||||||
mattermostView.loadSuccess = jest.fn();
|
mattermostView.loadSuccess = jest.fn();
|
||||||
mattermostView.loadRetry = jest.fn();
|
mattermostView.loadRetry = jest.fn();
|
||||||
@@ -175,10 +181,11 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('loadSuccess', () => {
|
describe('loadSuccess', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.emit = jest.fn();
|
mattermostView.emit = jest.fn();
|
||||||
mattermostView.setBounds = jest.fn();
|
mattermostView.setBounds = jest.fn();
|
||||||
mattermostView.setInitialized = jest.fn();
|
mattermostView.setInitialized = jest.fn();
|
||||||
@@ -202,10 +209,11 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('show', () => {
|
describe('show', () => {
|
||||||
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn()};
|
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.setBounds = jest.fn();
|
mattermostView.setBounds = jest.fn();
|
||||||
mattermostView.focus = jest.fn();
|
mattermostView.focus = jest.fn();
|
||||||
});
|
});
|
||||||
@@ -253,9 +261,10 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('destroy', () => {
|
describe('destroy', () => {
|
||||||
const window = {removeBrowserView: jest.fn(), on: jest.fn()};
|
const window = {removeBrowserView: jest.fn(), on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.view.webContents.destroy = jest.fn();
|
mattermostView.view.webContents.destroy = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -280,17 +289,18 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('handleInputEvents', () => {
|
describe('handleInputEvents', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
it('should open three dot menu on pressing Alt', () => {
|
it('should open three dot menu on pressing Alt', () => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown', alt: true, shift: false, control: false, meta: false});
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown', alt: true, shift: false, control: false, meta: false});
|
||||||
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
||||||
expect(WindowManager.focusThreeDotMenu).toHaveBeenCalled();
|
expect(MainWindow.focusThreeDotMenu).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not open three dot menu on holding Alt', () => {
|
it('should not open three dot menu on holding Alt', () => {
|
||||||
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown'});
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyDown'});
|
||||||
expect(WindowManager.focusThreeDotMenu).not.toHaveBeenCalled();
|
expect(MainWindow.focusThreeDotMenu).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not open three dot menu on Alt as key combp', () => {
|
it('should not open three dot menu on Alt as key combp', () => {
|
||||||
@@ -298,15 +308,16 @@ describe('main/views/MattermostView', () => {
|
|||||||
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyDown'});
|
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyDown'});
|
||||||
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyUp'});
|
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyUp'});
|
||||||
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
|
||||||
expect(WindowManager.focusThreeDotMenu).not.toHaveBeenCalled();
|
expect(MainWindow.focusThreeDotMenu).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('handleDidNavigate', () => {
|
describe('handleDidNavigate', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.setBounds = jest.fn();
|
mattermostView.setBounds = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -325,9 +336,10 @@ describe('main/views/MattermostView', () => {
|
|||||||
|
|
||||||
describe('handleUpdateTarget', () => {
|
describe('handleUpdateTarget', () => {
|
||||||
const window = {on: jest.fn()};
|
const window = {on: jest.fn()};
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
mattermostView.emit = jest.fn();
|
mattermostView.emit = jest.fn();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -355,8 +367,7 @@ describe('main/views/MattermostView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('updateMentionsFromTitle', () => {
|
describe('updateMentionsFromTitle', () => {
|
||||||
const window = {on: jest.fn()};
|
const mattermostView = new MattermostView(tabView, {}, {});
|
||||||
const mattermostView = new MattermostView(tabView, {}, window, {});
|
|
||||||
|
|
||||||
it('should parse mentions from title', () => {
|
it('should parse mentions from title', () => {
|
||||||
mattermostView.updateMentionsFromTitle('(7) Mattermost');
|
mattermostView.updateMentionsFromTitle('(7) Mattermost');
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserView, app, ipcMain, BrowserWindow} from 'electron';
|
import {BrowserView, app, ipcMain} from 'electron';
|
||||||
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
@@ -25,6 +25,8 @@ import {MattermostServer} from 'common/servers/MattermostServer';
|
|||||||
import {TabView, TabTuple} from 'common/tabs/TabView';
|
import {TabView, TabTuple} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import {ServerInfo} from 'main/server/serverInfo';
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils';
|
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils';
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
@@ -43,7 +45,6 @@ const MENTIONS_GROUP = 2;
|
|||||||
|
|
||||||
export class MattermostView extends EventEmitter {
|
export class MattermostView extends EventEmitter {
|
||||||
tab: TabView;
|
tab: TabView;
|
||||||
window: BrowserWindow;
|
|
||||||
view: BrowserView;
|
view: BrowserView;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
isLoggedIn: boolean;
|
isLoggedIn: boolean;
|
||||||
@@ -63,10 +64,9 @@ export class MattermostView extends EventEmitter {
|
|||||||
|
|
||||||
private altPressStatus: boolean;
|
private altPressStatus: boolean;
|
||||||
|
|
||||||
constructor(tab: TabView, serverInfo: ServerInfo, win: BrowserWindow, options: BrowserViewConstructorOptions) {
|
constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) {
|
||||||
super();
|
super();
|
||||||
this.tab = tab;
|
this.tab = tab;
|
||||||
this.window = win;
|
|
||||||
this.serverInfo = serverInfo;
|
this.serverInfo = serverInfo;
|
||||||
|
|
||||||
const preload = getLocalPreload('preload.js');
|
const preload = getLocalPreload('preload.js');
|
||||||
@@ -118,7 +118,7 @@ export class MattermostView extends EventEmitter {
|
|||||||
|
|
||||||
this.altPressStatus = false;
|
this.altPressStatus = false;
|
||||||
|
|
||||||
this.window.on('blur', () => {
|
MainWindow.get()?.on('blur', () => {
|
||||||
this.altPressStatus = false;
|
this.altPressStatus = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -224,6 +224,11 @@ export class MattermostView extends EventEmitter {
|
|||||||
|
|
||||||
loadSuccess = (loadURL: string) => {
|
loadSuccess = (loadURL: string) => {
|
||||||
return () => {
|
return () => {
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
log.verbose(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
|
log.verbose(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
|
||||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
|
WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
|
||||||
this.maxRetries = MAX_SERVER_RETRIES;
|
this.maxRetries = MAX_SERVER_RETRIES;
|
||||||
@@ -235,21 +240,26 @@ export class MattermostView extends EventEmitter {
|
|||||||
this.status = Status.WAITING_MM;
|
this.status = Status.WAITING_MM;
|
||||||
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
||||||
this.emit(LOAD_SUCCESS, this.tab.name, loadURL);
|
this.emit(LOAD_SUCCESS, this.tab.name, loadURL);
|
||||||
this.setBounds(getWindowBoundaries(this.window, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
|
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
show = (requestedVisibility?: boolean) => {
|
show = (requestedVisibility?: boolean) => {
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
this.hasBeenShown = true;
|
this.hasBeenShown = true;
|
||||||
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
|
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
|
||||||
if (request && !this.isVisible) {
|
if (request && !this.isVisible) {
|
||||||
this.window.addBrowserView(this.view);
|
mainWindow.addBrowserView(this.view);
|
||||||
this.setBounds(getWindowBoundaries(this.window, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
|
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
|
||||||
if (this.status === Status.READY) {
|
if (this.status === Status.READY) {
|
||||||
this.focus();
|
this.focus();
|
||||||
}
|
}
|
||||||
} else if (!request && this.isVisible) {
|
} else if (!request && this.isVisible) {
|
||||||
this.window.removeBrowserView(this.view);
|
mainWindow.removeBrowserView(this.view);
|
||||||
}
|
}
|
||||||
this.isVisible = request;
|
this.isVisible = request;
|
||||||
}
|
}
|
||||||
@@ -268,9 +278,7 @@ export class MattermostView extends EventEmitter {
|
|||||||
destroy = () => {
|
destroy = () => {
|
||||||
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id);
|
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id);
|
||||||
appState.updateMentions(this.tab.name, 0, false);
|
appState.updateMentions(this.tab.name, 0, false);
|
||||||
if (this.window) {
|
MainWindow.get()?.removeBrowserView(this.view);
|
||||||
this.window.removeBrowserView(this.view);
|
|
||||||
}
|
|
||||||
|
|
||||||
// workaround to eliminate zombie processes
|
// workaround to eliminate zombie processes
|
||||||
// https://github.com/mattermost/desktop/pull/1519
|
// https://github.com/mattermost/desktop/pull/1519
|
||||||
@@ -352,7 +360,7 @@ export class MattermostView extends EventEmitter {
|
|||||||
this.registerAltKeyPressed(input);
|
this.registerAltKeyPressed(input);
|
||||||
|
|
||||||
if (this.isAltKeyReleased(input)) {
|
if (this.isAltKeyReleased(input)) {
|
||||||
WindowManager.focusThreeDotMenu();
|
MainWindow.focusThreeDotMenu();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -360,11 +368,11 @@ export class MattermostView extends EventEmitter {
|
|||||||
log.debug('MattermostView.handleDidNavigate', {tabName: this.tab.name, url});
|
log.debug('MattermostView.handleDidNavigate', {tabName: this.tab.name, url});
|
||||||
|
|
||||||
if (shouldHaveBackBar(this.tab.url || '', url)) {
|
if (shouldHaveBackBar(this.tab.url || '', url)) {
|
||||||
this.setBounds(getWindowBoundaries(this.window, true));
|
this.setBounds(getWindowBoundaries(MainWindow.get()!, true));
|
||||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
|
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
|
||||||
log.info('show back button');
|
log.info('show back button');
|
||||||
} else {
|
} else {
|
||||||
this.setBounds(getWindowBoundaries(this.window));
|
this.setBounds(getWindowBoundaries(MainWindow.get()!));
|
||||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
||||||
log.info('hide back button');
|
log.info('hide back button');
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,8 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state
|
|||||||
|
|
||||||
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, TAB_BAR_HEIGHT} from 'common/utils/constants';
|
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT, DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, TAB_BAR_HEIGHT} from 'common/utils/constants';
|
||||||
|
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import DownloadsDropdownMenuView from './downloadsDropdownMenuView';
|
import DownloadsDropdownMenuView from './downloadsDropdownMenuView';
|
||||||
|
|
||||||
jest.mock('main/utils', () => ({
|
jest.mock('main/utils', () => ({
|
||||||
@@ -50,6 +52,11 @@ jest.mock('electron', () => {
|
|||||||
jest.mock('macos-notification-state', () => ({
|
jest.mock('macos-notification-state', () => ({
|
||||||
getDoNotDisturb: jest.fn(),
|
getDoNotDisturb: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/downloadsManager', () => ({}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
getBounds: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -60,24 +67,21 @@ jest.mock('fs', () => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
describe('main/views/DownloadsDropdownMenuView', () => {
|
describe('main/views/DownloadsDropdownMenuView', () => {
|
||||||
const window = {
|
|
||||||
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
|
|
||||||
addBrowserView: jest.fn(),
|
|
||||||
setTopBrowserView: jest.fn(),
|
|
||||||
};
|
|
||||||
const downloadsDropdownMenuView = new DownloadsDropdownMenuView(window, {}, false);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
|
||||||
|
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
|
||||||
getDarwinDoNotDisturb.mockReturnValue(false);
|
getDarwinDoNotDisturb.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getBounds', () => {
|
describe('getBounds', () => {
|
||||||
it('should be placed top-left inside the downloads dropdown if coordinates not used', () => {
|
it('should be placed top-left inside the downloads dropdown if coordinates not used', () => {
|
||||||
|
const downloadsDropdownMenuView = new DownloadsDropdownMenuView();
|
||||||
expect(downloadsDropdownMenuView.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH - DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT});
|
expect(downloadsDropdownMenuView.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH - DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the view bounds based on open/closed state', () => {
|
it('should change the view bounds based on open/closed state', () => {
|
||||||
|
const downloadsDropdownMenuView = new DownloadsDropdownMenuView();
|
||||||
downloadsDropdownMenuView.bounds = {width: 400, height: 300};
|
downloadsDropdownMenuView.bounds = {width: 400, height: 300};
|
||||||
downloadsDropdownMenuView.handleOpen();
|
downloadsDropdownMenuView.handleOpen();
|
||||||
expect(downloadsDropdownMenuView.view.setBounds).toBeCalledWith(downloadsDropdownMenuView.bounds);
|
expect(downloadsDropdownMenuView.view.setBounds).toBeCalledWith(downloadsDropdownMenuView.bounds);
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
|
import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
|
||||||
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
@@ -30,40 +30,23 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
|
|||||||
|
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
export default class DownloadsDropdownMenuView {
|
export default class DownloadsDropdownMenuView {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
view: BrowserView;
|
view: BrowserView;
|
||||||
bounds?: Electron.Rectangle;
|
bounds: Electron.Rectangle;
|
||||||
item?: DownloadedItem;
|
item?: DownloadedItem;
|
||||||
coordinates?: CoordinatesToJsonType;
|
coordinates?: CoordinatesToJsonType;
|
||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
window: BrowserWindow;
|
|
||||||
windowBounds: Electron.Rectangle;
|
windowBounds: Electron.Rectangle;
|
||||||
|
|
||||||
constructor(window: BrowserWindow, darkMode: boolean) {
|
constructor(darkMode: boolean) {
|
||||||
this.open = false;
|
this.open = false;
|
||||||
this.item = undefined;
|
this.item = undefined;
|
||||||
this.coordinates = undefined;
|
this.coordinates = undefined;
|
||||||
this.window = window;
|
|
||||||
this.darkMode = darkMode;
|
this.darkMode = darkMode;
|
||||||
|
|
||||||
this.windowBounds = this.window.getContentBounds();
|
|
||||||
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
|
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
|
||||||
this.view = new BrowserView({webPreferences: {
|
|
||||||
preload,
|
|
||||||
|
|
||||||
// Workaround for this issue: https://github.com/electron/electron/issues/30993
|
|
||||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
|
||||||
// @ts-ignore
|
|
||||||
transparent: true,
|
|
||||||
}});
|
|
||||||
|
|
||||||
this.view.webContents.loadURL(getLocalURLString('downloadsDropdownMenu.html'));
|
|
||||||
this.window.addBrowserView(this.view);
|
|
||||||
|
|
||||||
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen);
|
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen);
|
||||||
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);
|
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);
|
||||||
ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle);
|
ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle);
|
||||||
@@ -74,6 +57,27 @@ export default class DownloadsDropdownMenuView {
|
|||||||
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD, this.cancelDownload);
|
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CANCEL_DOWNLOAD, this.cancelDownload);
|
||||||
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE, this.clearFile);
|
ipcMain.on(DOWNLOADS_DROPDOWN_MENU_CLEAR_FILE, this.clearFile);
|
||||||
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem);
|
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem);
|
||||||
|
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
const windowBounds = MainWindow.getBounds();
|
||||||
|
if (!(mainWindow && windowBounds)) {
|
||||||
|
throw new Error('Cannot initialize downloadsDropdownMenuView, missing MainWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowBounds = windowBounds;
|
||||||
|
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
|
||||||
|
|
||||||
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
this.view = new BrowserView({webPreferences: {
|
||||||
|
preload,
|
||||||
|
|
||||||
|
// Workaround for this issue: https://github.com/electron/electron/issues/30993
|
||||||
|
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||||
|
// @ts-ignore
|
||||||
|
transparent: true,
|
||||||
|
}});
|
||||||
|
this.view.webContents.loadURL(getLocalURLString('downloadsDropdownMenu.html'));
|
||||||
|
mainWindow.addBrowserView(this.view);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateItem = (event: IpcMainEvent, item: DownloadedItem) => {
|
updateItem = (event: IpcMainEvent, item: DownloadedItem) => {
|
||||||
@@ -98,9 +102,12 @@ export default class DownloadsDropdownMenuView {
|
|||||||
updateWindowBounds = () => {
|
updateWindowBounds = () => {
|
||||||
log.debug('DownloadsDropdownMenuView.updateWindowBounds');
|
log.debug('DownloadsDropdownMenuView.updateWindowBounds');
|
||||||
|
|
||||||
this.windowBounds = this.window.getContentBounds();
|
const mainWindow = MainWindow.get();
|
||||||
this.updateDownloadsDropdownMenu();
|
if (mainWindow) {
|
||||||
this.repositionDownloadsDropdownMenu();
|
this.windowBounds = mainWindow.getContentBounds();
|
||||||
|
this.updateDownloadsDropdownMenu();
|
||||||
|
this.repositionDownloadsDropdownMenu();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadsDropdownMenu = () => {
|
updateDownloadsDropdownMenu = () => {
|
||||||
@@ -131,7 +138,7 @@ export default class DownloadsDropdownMenuView {
|
|||||||
this.item = item;
|
this.item = item;
|
||||||
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
|
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
|
||||||
this.view.setBounds(this.bounds);
|
this.view.setBounds(this.bounds);
|
||||||
this.window.setTopBrowserView(this.view);
|
MainWindow.get()?.setTopBrowserView(this.view);
|
||||||
this.view.webContents.focus();
|
this.view.webContents.focus();
|
||||||
this.updateDownloadsDropdownMenu();
|
this.updateDownloadsDropdownMenu();
|
||||||
}
|
}
|
||||||
|
@@ -7,6 +7,8 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state
|
|||||||
|
|
||||||
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
|
import {DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, TAB_BAR_HEIGHT} from 'common/utils/constants';
|
||||||
|
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import DownloadsDropdownView from './downloadsDropdownView';
|
import DownloadsDropdownView from './downloadsDropdownView';
|
||||||
|
|
||||||
jest.mock('main/utils', () => ({
|
jest.mock('main/utils', () => ({
|
||||||
@@ -59,42 +61,35 @@ jest.mock('electron', () => {
|
|||||||
Notification: NotificationMock,
|
Notification: NotificationMock,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
getBounds: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('main/windows/windowManager', () => ({
|
jest.mock('main/windows/windowManager', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('main/views/DownloadsDropdownView', () => {
|
describe('main/views/DownloadsDropdownView', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
|
||||||
getDarwinDoNotDisturb.mockReturnValue(false);
|
getDarwinDoNotDisturb.mockReturnValue(false);
|
||||||
});
|
});
|
||||||
describe('getBounds', () => {
|
describe('getBounds', () => {
|
||||||
it('should be placed far right when window is large enough', () => {
|
it('should be placed far right when window is large enough', () => {
|
||||||
const window = {
|
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
|
||||||
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
|
const downloadsDropdownView = new DownloadsDropdownView();
|
||||||
addBrowserView: jest.fn(),
|
|
||||||
setTopBrowserView: jest.fn(),
|
|
||||||
};
|
|
||||||
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
|
|
||||||
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
|
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 800 - DOWNLOADS_DROPDOWN_FULL_WIDTH, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
|
||||||
});
|
});
|
||||||
it('should be placed left if window is very small', () => {
|
it('should be placed left if window is very small', () => {
|
||||||
const window = {
|
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
|
||||||
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
|
const downloadsDropdownView = new DownloadsDropdownView();
|
||||||
addBrowserView: jest.fn(),
|
|
||||||
setTopBrowserView: jest.fn(),
|
|
||||||
};
|
|
||||||
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
|
|
||||||
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 0, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
|
expect(downloadsDropdownView.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT)).toStrictEqual({x: 0, y: TAB_BAR_HEIGHT, width: DOWNLOADS_DROPDOWN_FULL_WIDTH, height: DOWNLOADS_DROPDOWN_HEIGHT});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should change the view bounds based on open/closed state', () => {
|
it('should change the view bounds based on open/closed state', () => {
|
||||||
const window = {
|
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
|
||||||
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
|
const downloadsDropdownView = new DownloadsDropdownView();
|
||||||
addBrowserView: jest.fn(),
|
|
||||||
setTopBrowserView: jest.fn(),
|
|
||||||
};
|
|
||||||
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
|
|
||||||
downloadsDropdownView.bounds = {width: 400, height: 300};
|
downloadsDropdownView.bounds = {width: 400, height: 300};
|
||||||
downloadsDropdownView.handleOpen();
|
downloadsDropdownView.handleOpen();
|
||||||
expect(downloadsDropdownView.view.setBounds).toBeCalledWith(downloadsDropdownView.bounds);
|
expect(downloadsDropdownView.view.setBounds).toBeCalledWith(downloadsDropdownView.bounds);
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
import {BrowserView, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
|
||||||
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
@@ -25,23 +25,38 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
|
|||||||
|
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
export default class DownloadsDropdownView {
|
export default class DownloadsDropdownView {
|
||||||
bounds?: Electron.Rectangle;
|
bounds?: Electron.Rectangle;
|
||||||
darkMode: boolean;
|
darkMode: boolean;
|
||||||
downloads: DownloadedItems;
|
downloads: DownloadedItems;
|
||||||
item: DownloadedItem | undefined;
|
item?: DownloadedItem;
|
||||||
view: BrowserView;
|
view: BrowserView;
|
||||||
window: BrowserWindow;
|
|
||||||
windowBounds: Electron.Rectangle;
|
windowBounds: Electron.Rectangle;
|
||||||
|
|
||||||
constructor(window: BrowserWindow, downloads: DownloadedItems, darkMode: boolean) {
|
constructor(downloads: DownloadedItems, darkMode: boolean) {
|
||||||
this.downloads = downloads;
|
this.downloads = downloads;
|
||||||
this.window = window;
|
|
||||||
this.darkMode = darkMode;
|
this.darkMode = darkMode;
|
||||||
this.item = undefined;
|
|
||||||
|
|
||||||
this.windowBounds = this.window.getContentBounds();
|
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen);
|
||||||
|
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose);
|
||||||
|
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
|
||||||
|
ipcMain.on(REQUEST_DOWNLOADS_DROPDOWN_INFO, this.updateDownloadsDropdown);
|
||||||
|
ipcMain.on(REQUEST_CLEAR_DOWNLOADS_DROPDOWN, this.clearDownloads);
|
||||||
|
ipcMain.on(RECEIVE_DOWNLOADS_DROPDOWN_SIZE, this.handleReceivedDownloadsDropdownSize);
|
||||||
|
ipcMain.on(DOWNLOADS_DROPDOWN_OPEN_FILE, this.openFile);
|
||||||
|
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloads);
|
||||||
|
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem);
|
||||||
|
ipcMain.handle(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, this.getDownloadImageThumbnailLocation);
|
||||||
|
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
const windowBounds = MainWindow.getBounds();
|
||||||
|
if (!(mainWindow && windowBounds)) {
|
||||||
|
throw new Error('Cannot initialize downloadsDropdownView, missing MainWindow');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.windowBounds = windowBounds;
|
||||||
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT);
|
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_FULL_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT);
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
@@ -55,20 +70,8 @@ export default class DownloadsDropdownView {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html'));
|
this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html'));
|
||||||
this.window.addBrowserView(this.view);
|
|
||||||
|
|
||||||
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
|
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
|
||||||
|
mainWindow.addBrowserView(this.view);
|
||||||
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen);
|
|
||||||
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose);
|
|
||||||
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
|
|
||||||
ipcMain.on(REQUEST_DOWNLOADS_DROPDOWN_INFO, this.updateDownloadsDropdown);
|
|
||||||
ipcMain.on(REQUEST_CLEAR_DOWNLOADS_DROPDOWN, this.clearDownloads);
|
|
||||||
ipcMain.on(RECEIVE_DOWNLOADS_DROPDOWN_SIZE, this.handleReceivedDownloadsDropdownSize);
|
|
||||||
ipcMain.on(DOWNLOADS_DROPDOWN_OPEN_FILE, this.openFile);
|
|
||||||
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN, this.updateDownloads);
|
|
||||||
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, this.updateDownloadsDropdownMenuItem);
|
|
||||||
ipcMain.handle(GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, this.getDownloadImageThumbnailLocation);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloads = (event: IpcMainEvent, downloads: DownloadedItems) => {
|
updateDownloads = (event: IpcMainEvent, downloads: DownloadedItems) => {
|
||||||
@@ -99,9 +102,12 @@ export default class DownloadsDropdownView {
|
|||||||
updateWindowBounds = () => {
|
updateWindowBounds = () => {
|
||||||
log.debug('DownloadsDropdownView.updateWindowBounds');
|
log.debug('DownloadsDropdownView.updateWindowBounds');
|
||||||
|
|
||||||
this.windowBounds = this.window.getContentBounds();
|
const mainWindow = MainWindow.get();
|
||||||
this.updateDownloadsDropdown();
|
if (mainWindow) {
|
||||||
this.repositionDownloadsDropdown();
|
this.windowBounds = mainWindow.getContentBounds();
|
||||||
|
this.updateDownloadsDropdown();
|
||||||
|
this.repositionDownloadsDropdown();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
updateDownloadsDropdown = () => {
|
updateDownloadsDropdown = () => {
|
||||||
@@ -124,7 +130,7 @@ export default class DownloadsDropdownView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.view.setBounds(this.bounds);
|
this.view.setBounds(this.bounds);
|
||||||
this.window.setTopBrowserView(this.view);
|
MainWindow.get()?.setTopBrowserView(this.view);
|
||||||
this.view.webContents.focus();
|
this.view.webContents.focus();
|
||||||
downloadsManager.onOpen();
|
downloadsManager.onOpen();
|
||||||
WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);
|
WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);
|
||||||
@@ -172,7 +178,7 @@ export default class DownloadsDropdownView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
repositionDownloadsDropdown = () => {
|
repositionDownloadsDropdown = () => {
|
||||||
if (!this.bounds) {
|
if (!(this.bounds && this.windowBounds)) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.bounds = {
|
this.bounds = {
|
||||||
|
@@ -5,6 +5,8 @@
|
|||||||
|
|
||||||
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
||||||
|
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import TeamDropdownView from './teamDropdownView';
|
import TeamDropdownView from './teamDropdownView';
|
||||||
|
|
||||||
jest.mock('main/utils', () => ({
|
jest.mock('main/utils', () => ({
|
||||||
@@ -24,20 +26,23 @@ jest.mock('electron', () => ({
|
|||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
getBounds: jest.fn(),
|
||||||
|
addBrowserView: jest.fn(),
|
||||||
|
setTopBrowserView: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('../windows/windowManager', () => ({
|
jest.mock('../windows/windowManager', () => ({
|
||||||
sendToRenderer: jest.fn(),
|
sendToRenderer: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
describe('main/views/teamDropdownView', () => {
|
describe('main/views/teamDropdownView', () => {
|
||||||
const window = {
|
|
||||||
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
|
|
||||||
addBrowserView: jest.fn(),
|
|
||||||
setTopBrowserView: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
describe('getBounds', () => {
|
describe('getBounds', () => {
|
||||||
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
beforeEach(() => {
|
||||||
|
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
|
||||||
|
});
|
||||||
|
|
||||||
|
const teamDropdownView = new TeamDropdownView([], false, true);
|
||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
it('should account for three dot menu, tab bar and shadow', () => {
|
it('should account for three dot menu, tab bar and shadow', () => {
|
||||||
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
|
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
|
||||||
@@ -50,7 +55,7 @@ describe('main/views/teamDropdownView', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should change the view bounds based on open/closed state', () => {
|
it('should change the view bounds based on open/closed state', () => {
|
||||||
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
const teamDropdownView = new TeamDropdownView([], false, true);
|
||||||
teamDropdownView.bounds = {width: 400, height: 300};
|
teamDropdownView.bounds = {width: 400, height: 300};
|
||||||
teamDropdownView.handleOpen();
|
teamDropdownView.handleOpen();
|
||||||
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
|
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
|
||||||
@@ -60,7 +65,7 @@ describe('main/views/teamDropdownView', () => {
|
|||||||
|
|
||||||
describe('addGpoToTeams', () => {
|
describe('addGpoToTeams', () => {
|
||||||
it('should return teams with "isGPO": false when no config.registryTeams exist', () => {
|
it('should return teams with "isGPO": false when no config.registryTeams exist', () => {
|
||||||
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
const teamDropdownView = new TeamDropdownView([], false, true);
|
||||||
const teams = [{
|
const teams = [{
|
||||||
name: 'team-1',
|
name: 'team-1',
|
||||||
url: 'https://mattermost.team-1.com',
|
url: 'https://mattermost.team-1.com',
|
||||||
@@ -81,7 +86,7 @@ describe('main/views/teamDropdownView', () => {
|
|||||||
}]);
|
}]);
|
||||||
});
|
});
|
||||||
it('should return teams with "isGPO": true if they exist in config.registryTeams', () => {
|
it('should return teams with "isGPO": true if they exist in config.registryTeams', () => {
|
||||||
const teamDropdownView = new TeamDropdownView(window, [], false, true);
|
const teamDropdownView = new TeamDropdownView([], false, true);
|
||||||
const teams = [{
|
const teams = [{
|
||||||
name: 'team-1',
|
name: 'team-1',
|
||||||
url: 'https://mattermost.team-1.com',
|
url: 'https://mattermost.team-1.com',
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
|
import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
|
||||||
|
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
@@ -21,6 +21,7 @@ import * as AppState from '../appState';
|
|||||||
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
|
||||||
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
import {getLocalPreload, getLocalURLString} from 'main/utils';
|
||||||
import WindowManager from '../windows/windowManager';
|
import WindowManager from '../windows/windowManager';
|
||||||
|
import MainWindow from '../windows/mainWindow';
|
||||||
|
|
||||||
export default class TeamDropdownView {
|
export default class TeamDropdownView {
|
||||||
view: BrowserView;
|
view: BrowserView;
|
||||||
@@ -33,18 +34,16 @@ export default class TeamDropdownView {
|
|||||||
unreads?: Map<string, boolean>;
|
unreads?: Map<string, boolean>;
|
||||||
mentions?: Map<string, number>;
|
mentions?: Map<string, number>;
|
||||||
expired?: Map<string, boolean>;
|
expired?: Map<string, boolean>;
|
||||||
window: BrowserWindow;
|
windowBounds?: Electron.Rectangle;
|
||||||
windowBounds: Electron.Rectangle;
|
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
|
|
||||||
constructor(window: BrowserWindow, teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
|
constructor(teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
|
||||||
this.teams = this.addGpoToTeams(teams, []);
|
this.teams = this.addGpoToTeams(teams, []);
|
||||||
this.window = window;
|
|
||||||
this.darkMode = darkMode;
|
this.darkMode = darkMode;
|
||||||
this.enableServerManagement = enableServerManagement;
|
this.enableServerManagement = enableServerManagement;
|
||||||
this.isOpen = false;
|
this.isOpen = false;
|
||||||
|
|
||||||
this.windowBounds = this.window.getContentBounds();
|
this.windowBounds = MainWindow.getBounds();
|
||||||
|
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
this.view = new BrowserView({webPreferences: {
|
this.view = new BrowserView({webPreferences: {
|
||||||
@@ -57,7 +56,7 @@ export default class TeamDropdownView {
|
|||||||
}});
|
}});
|
||||||
|
|
||||||
this.view.webContents.loadURL(getLocalURLString('dropdown.html'));
|
this.view.webContents.loadURL(getLocalURLString('dropdown.html'));
|
||||||
this.window.addBrowserView(this.view);
|
MainWindow.get()?.addBrowserView(this.view);
|
||||||
|
|
||||||
ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen);
|
ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen);
|
||||||
ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose);
|
ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose);
|
||||||
@@ -95,7 +94,7 @@ export default class TeamDropdownView {
|
|||||||
}
|
}
|
||||||
|
|
||||||
updateWindowBounds = () => {
|
updateWindowBounds = () => {
|
||||||
this.windowBounds = this.window.getContentBounds();
|
this.windowBounds = MainWindow.getBounds();
|
||||||
this.updateDropdown();
|
this.updateDropdown();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,7 +122,7 @@ export default class TeamDropdownView {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.view.setBounds(this.bounds);
|
this.view.setBounds(this.bounds);
|
||||||
this.window.setTopBrowserView(this.view);
|
MainWindow.get()?.setTopBrowserView(this.view);
|
||||||
this.view.webContents.focus();
|
this.view.webContents.focus();
|
||||||
WindowManager.sendToRenderer(OPEN_TEAMS_DROPDOWN);
|
WindowManager.sendToRenderer(OPEN_TEAMS_DROPDOWN);
|
||||||
this.isOpen = true;
|
this.isOpen = true;
|
||||||
|
@@ -7,11 +7,13 @@
|
|||||||
import {dialog, ipcMain} from 'electron';
|
import {dialog, ipcMain} from 'electron';
|
||||||
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
|
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
|
||||||
|
|
||||||
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication';
|
import {LOAD_SUCCESS, MAIN_WINDOW_SHOWN, BROWSER_HISTORY_PUSH} from 'common/communication';
|
||||||
import {MattermostServer} from 'common/servers/MattermostServer';
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
import {getTabViewName} from 'common/tabs/TabView';
|
import {getTabViewName} from 'common/tabs/TabView';
|
||||||
import {equalUrlsIgnoringSubpath} from 'common/utils/url';
|
import {equalUrlsIgnoringSubpath} from 'common/utils/url';
|
||||||
|
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {MattermostView} from './MattermostView';
|
import {MattermostView} from './MattermostView';
|
||||||
import {ViewManager} from './viewManager';
|
import {ViewManager} from './viewManager';
|
||||||
|
|
||||||
@@ -56,6 +58,9 @@ jest.mock('main/server/serverInfo', () => ({
|
|||||||
ServerInfo: jest.fn(),
|
ServerInfo: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
jest.mock('main/windows/mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
jest.mock('./MattermostView', () => ({
|
jest.mock('./MattermostView', () => ({
|
||||||
MattermostView: jest.fn(),
|
MattermostView: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -174,15 +179,18 @@ describe('main/views/viewManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('reloadConfiguration', () => {
|
describe('reloadConfiguration', () => {
|
||||||
const viewManager = new ViewManager({});
|
const viewManager = new ViewManager();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
viewManager.loadView = jest.fn();
|
viewManager.loadView = jest.fn();
|
||||||
viewManager.showByName = jest.fn();
|
viewManager.showByName = jest.fn();
|
||||||
viewManager.showInitial = jest.fn();
|
viewManager.showInitial = jest.fn();
|
||||||
viewManager.mainWindow.webContents = {
|
const mainWindow = {
|
||||||
send: jest.fn(),
|
webContents: {
|
||||||
|
send: jest.fn(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
|
|
||||||
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
|
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
|
||||||
name: `${srv.name}-${tabName}`,
|
name: `${srv.name}-${tabName}`,
|
||||||
@@ -653,10 +661,11 @@ describe('main/views/viewManager', () => {
|
|||||||
setTopBrowserView: jest.fn(),
|
setTopBrowserView: jest.fn(),
|
||||||
addBrowserView: jest.fn(),
|
addBrowserView: jest.fn(),
|
||||||
};
|
};
|
||||||
const viewManager = new ViewManager(window);
|
const viewManager = new ViewManager();
|
||||||
const loadingScreen = {webContents: {send: jest.fn(), isLoading: () => false}};
|
const loadingScreen = {webContents: {send: jest.fn(), isLoading: () => false}};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(window);
|
||||||
viewManager.createLoadingScreen = jest.fn();
|
viewManager.createLoadingScreen = jest.fn();
|
||||||
viewManager.setLoadingScreenBounds = jest.fn();
|
viewManager.setLoadingScreenBounds = jest.fn();
|
||||||
window.getBrowserViews.mockImplementation(() => []);
|
window.getBrowserViews.mockImplementation(() => []);
|
||||||
|
@@ -1,8 +1,10 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {BrowserView, dialog, ipcMain, IpcMainEvent} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import {BrowserView, BrowserWindow, dialog, ipcMain, IpcMainEvent} from 'electron';
|
|
||||||
import {BrowserViewConstructorOptions} from 'electron/main';
|
import {BrowserViewConstructorOptions} from 'electron/main';
|
||||||
|
|
||||||
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
|
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
|
||||||
|
|
||||||
import {Tab, TeamWithTabs} from 'types/config';
|
import {Tab, TeamWithTabs} from 'types/config';
|
||||||
@@ -33,6 +35,7 @@ import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
|
|||||||
|
|
||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
import {ServerInfo} from 'main/server/serverInfo';
|
import {ServerInfo} from 'main/server/serverInfo';
|
||||||
|
import MainWindow from 'main/windows/mainWindow';
|
||||||
|
|
||||||
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
|
||||||
|
|
||||||
@@ -57,23 +60,17 @@ export class ViewManager {
|
|||||||
currentView?: string;
|
currentView?: string;
|
||||||
urlView?: BrowserView;
|
urlView?: BrowserView;
|
||||||
urlViewCancel?: () => void;
|
urlViewCancel?: () => void;
|
||||||
mainWindow: BrowserWindow;
|
|
||||||
loadingScreen?: BrowserView;
|
loadingScreen?: BrowserView;
|
||||||
loadingScreenState: LoadingScreenState;
|
loadingScreenState: LoadingScreenState;
|
||||||
|
|
||||||
constructor(mainWindow: BrowserWindow) {
|
constructor() {
|
||||||
this.lastActiveServer = Config.lastActiveTeam;
|
this.lastActiveServer = Config.lastActiveTeam;
|
||||||
this.viewOptions = {webPreferences: {spellcheck: Config.useSpellChecker}};
|
this.viewOptions = {webPreferences: {spellcheck: Config.useSpellChecker}};
|
||||||
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
this.closedViews = new Map();
|
this.closedViews = new Map();
|
||||||
this.loadingScreenState = LoadingScreenState.HIDDEN;
|
this.loadingScreenState = LoadingScreenState.HIDDEN;
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMainWindow = (mainWindow: BrowserWindow) => {
|
|
||||||
this.mainWindow = mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
getServers = () => {
|
getServers = () => {
|
||||||
return Config.teams.concat();
|
return Config.teams.concat();
|
||||||
}
|
}
|
||||||
@@ -86,7 +83,7 @@ export class ViewManager {
|
|||||||
|
|
||||||
makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => {
|
makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => {
|
||||||
const tabView = this.getServerView(srv, tab.name);
|
const tabView = this.getServerView(srv, tab.name);
|
||||||
const view = new MattermostView(tabView, serverInfo, this.mainWindow, this.viewOptions);
|
const view = new MattermostView(tabView, serverInfo, this.viewOptions);
|
||||||
view.once(LOAD_SUCCESS, this.activateView);
|
view.once(LOAD_SUCCESS, this.activateView);
|
||||||
view.load(url);
|
view.load(url);
|
||||||
view.on(UPDATE_TARGET_URL, this.showURLView);
|
view.on(UPDATE_TARGET_URL, this.showURLView);
|
||||||
@@ -186,7 +183,7 @@ export class ViewManager {
|
|||||||
this.currentView = undefined;
|
this.currentView = undefined;
|
||||||
this.showInitial();
|
this.showInitial();
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow.webContents.send(SET_ACTIVE_VIEW);
|
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -196,7 +193,7 @@ export class ViewManager {
|
|||||||
if (view) {
|
if (view) {
|
||||||
this.currentView = view.name;
|
this.currentView = view.name;
|
||||||
this.showByName(view.name);
|
this.showByName(view.name);
|
||||||
this.mainWindow.webContents.send(SET_ACTIVE_VIEW, view.tab.server.name, view.tab.type);
|
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.name, view.tab.type);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.showInitial();
|
this.showInitial();
|
||||||
@@ -221,7 +218,7 @@ export class ViewManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow.webContents.send(SET_ACTIVE_VIEW, null, null);
|
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null);
|
||||||
ipcMain.emit(MAIN_WINDOW_SHOWN);
|
ipcMain.emit(MAIN_WINDOW_SHOWN);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -248,7 +245,7 @@ export class ViewManager {
|
|||||||
this.showLoadingScreen();
|
this.showLoadingScreen();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
newView.window.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type);
|
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type);
|
||||||
ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type);
|
ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type);
|
||||||
if (newView.isReady()) {
|
if (newView.isReady()) {
|
||||||
ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type);
|
ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type);
|
||||||
@@ -375,13 +372,13 @@ export class ViewManager {
|
|||||||
const query = new Map([['url', urlString]]);
|
const query = new Map([['url', urlString]]);
|
||||||
const localURL = getLocalURLString('urlView.html', query);
|
const localURL = getLocalURLString('urlView.html', query);
|
||||||
urlView.webContents.loadURL(localURL);
|
urlView.webContents.loadURL(localURL);
|
||||||
this.mainWindow.addBrowserView(urlView);
|
MainWindow.get()?.addBrowserView(urlView);
|
||||||
const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? this.mainWindow.getBounds();
|
const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? MainWindow.get()!.getBounds();
|
||||||
|
|
||||||
const hideView = () => {
|
const hideView = () => {
|
||||||
delete this.urlViewCancel;
|
delete this.urlViewCancel;
|
||||||
try {
|
try {
|
||||||
this.mainWindow.removeBrowserView(urlView);
|
MainWindow.get()?.removeBrowserView(urlView);
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log.error('Failed to remove URL view', e);
|
log.error('Failed to remove URL view', e);
|
||||||
}
|
}
|
||||||
@@ -421,9 +418,11 @@ export class ViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
setLoadingScreenBounds = () => {
|
setLoadingScreenBounds = () => {
|
||||||
if (this.loadingScreen) {
|
const mainWindow = MainWindow.get();
|
||||||
this.loadingScreen.setBounds(getWindowBoundaries(this.mainWindow));
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
|
this.loadingScreen?.setBounds(getWindowBoundaries(mainWindow));
|
||||||
}
|
}
|
||||||
|
|
||||||
createLoadingScreen = () => {
|
createLoadingScreen = () => {
|
||||||
@@ -441,6 +440,11 @@ export class ViewManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoadingScreen = () => {
|
showLoadingScreen = () => {
|
||||||
|
const mainWindow = MainWindow.get();
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!this.loadingScreen) {
|
if (!this.loadingScreen) {
|
||||||
this.createLoadingScreen();
|
this.createLoadingScreen();
|
||||||
}
|
}
|
||||||
@@ -455,10 +459,10 @@ export class ViewManager {
|
|||||||
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
|
if (mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
|
||||||
this.mainWindow.setTopBrowserView(this.loadingScreen!);
|
mainWindow.setTopBrowserView(this.loadingScreen!);
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow.addBrowserView(this.loadingScreen!);
|
mainWindow.addBrowserView(this.loadingScreen!);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setLoadingScreenBounds();
|
this.setLoadingScreenBounds();
|
||||||
@@ -474,7 +478,7 @@ export class ViewManager {
|
|||||||
hideLoadingScreen = () => {
|
hideLoadingScreen = () => {
|
||||||
if (this.loadingScreen && this.loadingScreenState !== LoadingScreenState.HIDDEN) {
|
if (this.loadingScreen && this.loadingScreenState !== LoadingScreenState.HIDDEN) {
|
||||||
this.loadingScreenState = LoadingScreenState.HIDDEN;
|
this.loadingScreenState = LoadingScreenState.HIDDEN;
|
||||||
this.mainWindow.removeBrowserView(this.loadingScreen);
|
MainWindow.get()?.removeBrowserView(this.loadingScreen);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -10,21 +10,24 @@ import {BrowserWindow, screen, app, globalShortcut, dialog} from 'electron';
|
|||||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants';
|
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH} from 'common/utils/constants';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
|
||||||
import * as Validator from 'common/Validator';
|
import * as Validator from 'common/Validator';
|
||||||
|
|
||||||
import createMainWindow from './mainWindow';
|
import ContextMenu from '../contextMenu';
|
||||||
|
import {isInsideRectangle} from '../utils';
|
||||||
|
import {MainWindow} from './mainWindow';
|
||||||
|
|
||||||
jest.mock('path', () => ({
|
jest.mock('path', () => ({
|
||||||
join: jest.fn(),
|
join: jest.fn(),
|
||||||
|
resolve: jest.fn(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
jest.mock('electron', () => ({
|
jest.mock('electron', () => ({
|
||||||
app: {
|
app: {
|
||||||
|
getAppPath: jest.fn(),
|
||||||
getPath: jest.fn(),
|
getPath: jest.fn(),
|
||||||
hide: jest.fn(),
|
hide: jest.fn(),
|
||||||
quit: jest.fn(),
|
quit: jest.fn(),
|
||||||
|
relaunch: jest.fn(),
|
||||||
},
|
},
|
||||||
dialog: {
|
dialog: {
|
||||||
showMessageBox: jest.fn(),
|
showMessageBox: jest.fn(),
|
||||||
@@ -65,6 +68,7 @@ jest.mock('common/Validator', () => ({
|
|||||||
jest.mock('../contextMenu', () => jest.fn());
|
jest.mock('../contextMenu', () => jest.fn());
|
||||||
|
|
||||||
jest.mock('../utils', () => ({
|
jest.mock('../utils', () => ({
|
||||||
|
isInsideRectangle: jest.fn(),
|
||||||
getLocalPreload: jest.fn(),
|
getLocalPreload: jest.fn(),
|
||||||
getLocalURLString: jest.fn(),
|
getLocalURLString: jest.fn(),
|
||||||
}));
|
}));
|
||||||
@@ -76,7 +80,7 @@ jest.mock('main/i18nManager', () => ({
|
|||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
describe('main/windows/mainWindow', () => {
|
describe('main/windows/mainWindow', () => {
|
||||||
describe('createMainWindow', () => {
|
describe('init', () => {
|
||||||
const baseWindow = {
|
const baseWindow = {
|
||||||
setMenuBarVisibility: jest.fn(),
|
setMenuBarVisibility: jest.fn(),
|
||||||
loadURL: jest.fn(),
|
loadURL: jest.fn(),
|
||||||
@@ -90,6 +94,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
webContents: {
|
webContents: {
|
||||||
on: jest.fn(),
|
on: jest.fn(),
|
||||||
send: jest.fn(),
|
send: jest.fn(),
|
||||||
|
setWindowOpenHandler: jest.fn(),
|
||||||
},
|
},
|
||||||
isMaximized: jest.fn(),
|
isMaximized: jest.fn(),
|
||||||
isFullScreen: jest.fn(),
|
isFullScreen: jest.fn(),
|
||||||
@@ -104,6 +109,7 @@ describe('main/windows/mainWindow', () => {
|
|||||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
||||||
path.join.mockImplementation(() => 'anyfile.txt');
|
path.join.mockImplementation(() => 'anyfile.txt');
|
||||||
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
||||||
|
isInsideRectangle.mockReturnValue(true);
|
||||||
Validator.validateBoundsInfo.mockImplementation((data) => data);
|
Validator.validateBoundsInfo.mockImplementation((data) => data);
|
||||||
ContextMenu.mockImplementation(() => ({
|
ContextMenu.mockImplementation(() => ({
|
||||||
reload: jest.fn(),
|
reload: jest.fn(),
|
||||||
@@ -115,7 +121,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should set window size using bounds read from file', () => {
|
it('should set window size using bounds read from file', () => {
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
x: 400,
|
x: 400,
|
||||||
y: 300,
|
y: 300,
|
||||||
@@ -126,16 +133,10 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open in fullscreen if fullscreen set to true', () => {
|
|
||||||
createMainWindow({fullscreen: true});
|
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
|
||||||
fullscreen: true,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set default window size when failing to read bounds from file', () => {
|
it('should set default window size when failing to read bounds from file', () => {
|
||||||
fs.readFileSync.mockImplementation(() => 'just a bunch of garbage');
|
fs.readFileSync.mockImplementation(() => 'just a bunch of garbage');
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
width: DEFAULT_WINDOW_WIDTH,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: DEFAULT_WINDOW_HEIGHT,
|
height: DEFAULT_WINDOW_HEIGHT,
|
||||||
@@ -145,27 +146,15 @@ describe('main/windows/mainWindow', () => {
|
|||||||
it('should set default window size when bounds are outside the normal screen', () => {
|
it('should set default window size when bounds are outside the normal screen', () => {
|
||||||
fs.readFileSync.mockImplementation(() => '{"x":-400,"y":-300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":-400,"y":-300,"width":1280,"height":700,"maximized":false,"fullscreen":false}');
|
||||||
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
screen.getDisplayMatching.mockImplementation(() => ({bounds: {x: 0, y: 0, width: 1920, height: 1080}}));
|
||||||
createMainWindow({});
|
isInsideRectangle.mockReturnValue(false);
|
||||||
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
||||||
width: DEFAULT_WINDOW_WIDTH,
|
width: DEFAULT_WINDOW_WIDTH,
|
||||||
height: DEFAULT_WINDOW_HEIGHT,
|
height: DEFAULT_WINDOW_HEIGHT,
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set linux app icon', () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'linux',
|
|
||||||
});
|
|
||||||
createMainWindow({linuxAppIcon: 'linux-icon.png'});
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
|
|
||||||
icon: 'linux-icon.png',
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset zoom level and maximize if applicable on ready-to-show', () => {
|
it('should reset zoom level and maximize if applicable on ready-to-show', () => {
|
||||||
const window = {
|
const window = {
|
||||||
...baseWindow,
|
...baseWindow,
|
||||||
@@ -178,7 +167,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
||||||
Config.hideOnStart = false;
|
Config.hideOnStart = false;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(window.webContents.zoomLevel).toStrictEqual(0);
|
expect(window.webContents.zoomLevel).toStrictEqual(0);
|
||||||
expect(window.maximize).toBeCalled();
|
expect(window.maximize).toBeCalled();
|
||||||
});
|
});
|
||||||
@@ -195,7 +185,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
fs.readFileSync.mockImplementation(() => '{"x":400,"y":300,"width":1280,"height":700,"maximized":true,"fullscreen":false}');
|
||||||
Config.hideOnStart = true;
|
Config.hideOnStart = true;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(window.show).not.toHaveBeenCalled();
|
expect(window.show).not.toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,7 +201,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({}, {});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
global.willAppQuit = false;
|
global.willAppQuit = false;
|
||||||
expect(fs.writeFileSync).toHaveBeenCalled();
|
expect(fs.writeFileSync).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -231,7 +223,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
Config.minimizeToTray = true;
|
Config.minimizeToTray = true;
|
||||||
Config.alwaysMinimize = true;
|
Config.alwaysMinimize = true;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Config.minimizeToTray = false;
|
Config.minimizeToTray = false;
|
||||||
Config.alwaysMinimize = false;
|
Config.alwaysMinimize = false;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
@@ -255,7 +248,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
Config.alwaysClose = true;
|
Config.alwaysClose = true;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Config.alwaysClose = false;
|
Config.alwaysClose = false;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
@@ -278,11 +272,13 @@ describe('main/windows/mainWindow', () => {
|
|||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
dialog.showMessageBox.mockResolvedValue({response: 1});
|
dialog.showMessageBox.mockResolvedValue({response: 1});
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(app.quit).not.toHaveBeenCalled();
|
expect(app.quit).not.toHaveBeenCalled();
|
||||||
const promise = Promise.resolve({response: 0});
|
const promise = Promise.resolve({response: 0});
|
||||||
dialog.showMessageBox.mockImplementation(() => promise);
|
dialog.showMessageBox.mockImplementation(() => promise);
|
||||||
createMainWindow({});
|
const mainWindow2 = new MainWindow();
|
||||||
|
mainWindow2.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -306,7 +302,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
Config.minimizeToTray = true;
|
Config.minimizeToTray = true;
|
||||||
Config.alwaysMinimize = true;
|
Config.alwaysMinimize = true;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Config.minimizeToTray = false;
|
Config.minimizeToTray = false;
|
||||||
Config.alwaysMinimize = false;
|
Config.alwaysMinimize = false;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
@@ -330,7 +327,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
Config.alwaysClose = true;
|
Config.alwaysClose = true;
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Config.alwaysClose = false;
|
Config.alwaysClose = false;
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
@@ -353,11 +351,13 @@ describe('main/windows/mainWindow', () => {
|
|||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
dialog.showMessageBox.mockResolvedValue({response: 1});
|
dialog.showMessageBox.mockResolvedValue({response: 1});
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
expect(app.quit).not.toHaveBeenCalled();
|
expect(app.quit).not.toHaveBeenCalled();
|
||||||
const promise = Promise.resolve({response: 0});
|
const promise = Promise.resolve({response: 0});
|
||||||
dialog.showMessageBox.mockImplementation(() => promise);
|
dialog.showMessageBox.mockImplementation(() => promise);
|
||||||
createMainWindow({});
|
const mainWindow2 = new MainWindow();
|
||||||
|
mainWindow2.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -379,7 +379,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -407,7 +408,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -434,7 +436,8 @@ describe('main/windows/mainWindow', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
@@ -456,11 +459,28 @@ describe('main/windows/mainWindow', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
BrowserWindow.mockImplementation(() => window);
|
BrowserWindow.mockImplementation(() => window);
|
||||||
createMainWindow({});
|
const mainWindow = new MainWindow();
|
||||||
|
mainWindow.init();
|
||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: originalPlatform,
|
value: originalPlatform,
|
||||||
});
|
});
|
||||||
expect(globalShortcut.registerAll).toHaveBeenCalledWith(['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'], expect.any(Function));
|
expect(globalShortcut.registerAll).toHaveBeenCalledWith(['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'], expect.any(Function));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('onUnresponsive', () => {
|
||||||
|
const mainWindow = new MainWindow();
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mainWindow.win = {};
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call app.relaunch when user elects not to wait', async () => {
|
||||||
|
const promise = Promise.resolve({response: 0});
|
||||||
|
dialog.showMessageBox.mockImplementation(() => promise);
|
||||||
|
mainWindow.onUnresponsive();
|
||||||
|
await promise;
|
||||||
|
expect(app.relaunch).toBeCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -3,14 +3,16 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
import fs from 'fs';
|
||||||
|
|
||||||
|
import path from 'path';
|
||||||
|
|
||||||
import os from 'os';
|
import os from 'os';
|
||||||
|
|
||||||
import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, globalShortcut, ipcMain, screen} from 'electron';
|
import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, globalShortcut, Input, ipcMain, screen} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {SavedWindowState} from 'types/mainWindow';
|
import {SavedWindowState} from 'types/mainWindow';
|
||||||
|
|
||||||
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS} from 'common/communication';
|
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, FOCUS_THREE_DOT_MENU} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants';
|
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants';
|
||||||
import Utils from 'common/utils/util';
|
import Utils from 'common/utils/util';
|
||||||
@@ -20,53 +22,121 @@ import {boundsInfoPath} from 'main/constants';
|
|||||||
import {localizeMessage} from 'main/i18nManager';
|
import {localizeMessage} from 'main/i18nManager';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
import {getLocalPreload, getLocalURLString, isInsideRectangle} from '../utils';
|
||||||
|
|
||||||
function saveWindowState(file: string, window: BrowserWindow) {
|
const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'];
|
||||||
const windowState: SavedWindowState = {
|
|
||||||
...window.getBounds(),
|
|
||||||
maximized: window.isMaximized(),
|
|
||||||
fullscreen: window.isFullScreen(),
|
|
||||||
};
|
|
||||||
try {
|
|
||||||
fs.writeFileSync(file, JSON.stringify(windowState));
|
|
||||||
} catch (e) {
|
|
||||||
// [Linux] error happens only when the window state is changed before the config dir is created.
|
|
||||||
log.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function isInsideRectangle(container: Electron.Rectangle, rect: Electron.Rectangle) {
|
export class MainWindow {
|
||||||
return container.x <= rect.x && container.y <= rect.y && container.width >= rect.width && container.height >= rect.height;
|
private win?: BrowserWindow;
|
||||||
}
|
|
||||||
|
|
||||||
function isFramelessWindow() {
|
private savedWindowState: SavedWindowState;
|
||||||
return os.platform() === 'darwin' || (os.platform() === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'));
|
private ready: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean}) {
|
constructor() {
|
||||||
// Create the browser window.
|
// Create the browser window.
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
this.ready = false;
|
||||||
let savedWindowState: any;
|
this.savedWindowState = this.getSavedWindowState();
|
||||||
try {
|
|
||||||
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen());
|
||||||
savedWindowState = Validator.validateBoundsInfo(savedWindowState);
|
|
||||||
if (!savedWindowState) {
|
|
||||||
throw new Error('Provided bounds info file does not validate, using defaults instead.');
|
|
||||||
}
|
|
||||||
const matchingScreen = screen.getDisplayMatching(savedWindowState);
|
|
||||||
if (!(matchingScreen && (isInsideRectangle(matchingScreen.bounds, savedWindowState) || savedWindowState.maximized))) {
|
|
||||||
throw new Error('Provided bounds info are outside the bounds of your screen, using defaults instead.');
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
|
||||||
savedWindowState = {width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const {maximized: windowIsMaximized} = savedWindowState;
|
init = () => {
|
||||||
|
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, this.savedWindowState, {
|
||||||
|
title: app.name,
|
||||||
|
fullscreenable: true,
|
||||||
|
show: false, // don't start the window until it is ready and only if it isn't hidden
|
||||||
|
paintWhenInitiallyHidden: true, // we want it to start painting to get info from the webapp
|
||||||
|
minWidth: MINIMUM_WINDOW_WIDTH,
|
||||||
|
minHeight: MINIMUM_WINDOW_HEIGHT,
|
||||||
|
frame: !this.isFramelessWindow(),
|
||||||
|
fullscreen: this.shouldStartFullScreen(),
|
||||||
|
titleBarStyle: 'hidden' as const,
|
||||||
|
trafficLightPosition: {x: 12, y: 12},
|
||||||
|
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||||
|
webPreferences: {
|
||||||
|
disableBlinkFeatures: 'Auxclick',
|
||||||
|
preload: getLocalPreload('desktopAPI.js'),
|
||||||
|
spellcheck: typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
if (process.platform === 'linux') {
|
||||||
const isFullScreen = () => {
|
windowOptions.icon = path.join(path.resolve(app.getAppPath(), 'assets'), 'linux', 'app_icon.png');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.win = new BrowserWindow(windowOptions);
|
||||||
|
this.win.setMenuBarVisibility(false);
|
||||||
|
|
||||||
|
if (!this.win) {
|
||||||
|
throw new Error('unable to create main window');
|
||||||
|
}
|
||||||
|
|
||||||
|
const localURL = getLocalURLString('index.html');
|
||||||
|
this.win.loadURL(localURL).catch(
|
||||||
|
(reason) => {
|
||||||
|
log.error('failed to load', reason);
|
||||||
|
});
|
||||||
|
this.win.once('ready-to-show', () => {
|
||||||
|
if (!this.win) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.win.webContents.zoomLevel = 0;
|
||||||
|
|
||||||
|
if (Config.hideOnStart === false) {
|
||||||
|
this.win.show();
|
||||||
|
if (this.savedWindowState.maximized) {
|
||||||
|
this.win.maximize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.ready = true;
|
||||||
|
});
|
||||||
|
|
||||||
|
this.win.once('restore', () => {
|
||||||
|
this.win?.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
this.win.on('close', this.onClose);
|
||||||
|
this.win.on('closed', this.onClosed);
|
||||||
|
this.win.on('focus', this.onFocus);
|
||||||
|
this.win.on('blur', this.onBlur);
|
||||||
|
this.win.on('unresponsive', this.onUnresponsive);
|
||||||
|
|
||||||
|
this.win.webContents.on('before-input-event', this.onBeforeInputEvent);
|
||||||
|
|
||||||
|
// Should not allow the main window to generate a window of its own
|
||||||
|
this.win.webContents.setWindowOpenHandler(() => ({action: 'deny'}));
|
||||||
|
if (process.env.MM_DEBUG_SETTINGS) {
|
||||||
|
this.win.webContents.openDevTools({mode: 'detach'});
|
||||||
|
}
|
||||||
|
|
||||||
|
const contextMenu = new ContextMenu({}, this.win);
|
||||||
|
contextMenu.reload();
|
||||||
|
}
|
||||||
|
|
||||||
|
get isReady() {
|
||||||
|
return this.ready;
|
||||||
|
}
|
||||||
|
|
||||||
|
get = (ensureCreated?: boolean) => {
|
||||||
|
if (ensureCreated && !this.win) {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
|
||||||
|
getBounds = () => {
|
||||||
|
return this.win?.getContentBounds();
|
||||||
|
}
|
||||||
|
|
||||||
|
focusThreeDotMenu = () => {
|
||||||
|
if (this.win) {
|
||||||
|
this.win.webContents.focus();
|
||||||
|
this.win.webContents.send(FOCUS_THREE_DOT_MENU);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private shouldStartFullScreen = () => {
|
||||||
if (global?.args?.fullscreen !== undefined) {
|
if (global?.args?.fullscreen !== undefined) {
|
||||||
return global.args.fullscreen;
|
return global.args.fullscreen;
|
||||||
}
|
}
|
||||||
@@ -74,75 +144,94 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||||||
if (Config.startInFullscreen) {
|
if (Config.startInFullscreen) {
|
||||||
return Config.startInFullscreen;
|
return Config.startInFullscreen;
|
||||||
}
|
}
|
||||||
return options.fullscreen || savedWindowState.fullscreen || false;
|
return this.savedWindowState.fullscreen || false;
|
||||||
};
|
|
||||||
|
|
||||||
const windowOptions: BrowserWindowConstructorOptions = Object.assign({}, savedWindowState, {
|
|
||||||
title: app.name,
|
|
||||||
fullscreenable: true,
|
|
||||||
show: false, // don't start the window until it is ready and only if it isn't hidden
|
|
||||||
paintWhenInitiallyHidden: true, // we want it to start painting to get info from the webapp
|
|
||||||
minWidth: MINIMUM_WINDOW_WIDTH,
|
|
||||||
minHeight: MINIMUM_WINDOW_HEIGHT,
|
|
||||||
frame: !isFramelessWindow(),
|
|
||||||
fullscreen: isFullScreen(),
|
|
||||||
titleBarStyle: 'hidden' as const,
|
|
||||||
trafficLightPosition: {x: 12, y: 12},
|
|
||||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
|
||||||
webPreferences: {
|
|
||||||
disableBlinkFeatures: 'Auxclick',
|
|
||||||
preload,
|
|
||||||
spellcheck,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
windowOptions.icon = options.linuxAppIcon;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const mainWindow = new BrowserWindow(windowOptions);
|
private isFramelessWindow = () => {
|
||||||
mainWindow.setMenuBarVisibility(false);
|
return os.platform() === 'darwin' || (os.platform() === 'win32' && Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'));
|
||||||
|
|
||||||
try {
|
|
||||||
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => mainWindow.isFullScreen());
|
|
||||||
} catch (e) {
|
|
||||||
log.error('Tried to register second handler, skipping');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const localURL = getLocalURLString('index.html');
|
private getSavedWindowState = () => {
|
||||||
mainWindow.loadURL(localURL).catch(
|
let savedWindowState: any;
|
||||||
(reason) => {
|
try {
|
||||||
log.error(`Main window failed to load: ${reason}`);
|
savedWindowState = JSON.parse(fs.readFileSync(boundsInfoPath, 'utf-8'));
|
||||||
});
|
savedWindowState = Validator.validateBoundsInfo(savedWindowState);
|
||||||
mainWindow.once('ready-to-show', () => {
|
if (!savedWindowState) {
|
||||||
mainWindow.webContents.zoomLevel = 0;
|
throw new Error('Provided bounds info file does not validate, using defaults instead.');
|
||||||
|
}
|
||||||
|
const matchingScreen = screen.getDisplayMatching(savedWindowState);
|
||||||
|
if (!(matchingScreen && (isInsideRectangle(matchingScreen.bounds, savedWindowState) || savedWindowState.maximized))) {
|
||||||
|
throw new Error('Provided bounds info are outside the bounds of your screen, using defaults instead.');
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
// Follow Electron's defaults, except for window dimensions which targets 1024x768 screen resolution.
|
||||||
|
savedWindowState = {width: DEFAULT_WINDOW_WIDTH, height: DEFAULT_WINDOW_HEIGHT};
|
||||||
|
}
|
||||||
|
return savedWindowState;
|
||||||
|
}
|
||||||
|
|
||||||
if (Config.hideOnStart === false) {
|
private saveWindowState = (file: string, window: BrowserWindow) => {
|
||||||
mainWindow.show();
|
const windowState: SavedWindowState = {
|
||||||
if (windowIsMaximized) {
|
...window.getBounds(),
|
||||||
mainWindow.maximize();
|
maximized: window.isMaximized(),
|
||||||
|
fullscreen: window.isFullScreen(),
|
||||||
|
};
|
||||||
|
try {
|
||||||
|
fs.writeFileSync(file, JSON.stringify(windowState));
|
||||||
|
} catch (e) {
|
||||||
|
// [Linux] error happens only when the window state is changed before the config dir is created.
|
||||||
|
log.error('failed to save window state', e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private onBeforeInputEvent = (event: Event, input: Input) => {
|
||||||
|
// Register keyboard shortcuts
|
||||||
|
// Add Alt+Cmd+(Right|Left) as alternative to switch between servers
|
||||||
|
if (this.win && process.platform === 'darwin') {
|
||||||
|
if (input.alt && input.meta) {
|
||||||
|
if (input.key === 'ArrowRight') {
|
||||||
|
this.win.webContents.send(SELECT_NEXT_TAB);
|
||||||
|
}
|
||||||
|
if (input.key === 'ArrowLeft') {
|
||||||
|
this.win.webContents.send(SELECT_PREVIOUS_TAB);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
mainWindow.once('restore', () => {
|
private onFocus = () => {
|
||||||
mainWindow.restore();
|
// Only add shortcuts when window is in focus
|
||||||
});
|
if (process.platform === 'linux') {
|
||||||
|
globalShortcut.registerAll(ALT_MENU_KEYS, () => {
|
||||||
|
// do nothing because we want to supress the menu popping up
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// App should save bounds when a window is closed.
|
private onBlur = () => {
|
||||||
// However, 'close' is not fired in some situations(shutdown, ctrl+c)
|
if (!this.win) {
|
||||||
// because main process is killed in such situations.
|
return;
|
||||||
// 'blur' event was effective in order to avoid this.
|
}
|
||||||
// Ideally, app should detect that OS is shutting down.
|
|
||||||
mainWindow.on('blur', () => {
|
|
||||||
saveWindowState(boundsInfoPath, mainWindow);
|
|
||||||
});
|
|
||||||
|
|
||||||
mainWindow.on('close', (event) => {
|
globalShortcut.unregisterAll();
|
||||||
|
|
||||||
|
// App should save bounds when a window is closed.
|
||||||
|
// However, 'close' is not fired in some situations(shutdown, ctrl+c)
|
||||||
|
// because main process is killed in such situations.
|
||||||
|
// 'blur' event was effective in order to avoid this.
|
||||||
|
// Ideally, app should detect that OS is shutting down.
|
||||||
|
this.saveWindowState(boundsInfoPath, this.win);
|
||||||
|
}
|
||||||
|
|
||||||
|
private onClose = (event: Event) => {
|
||||||
log.debug('MainWindow.on.close');
|
log.debug('MainWindow.on.close');
|
||||||
|
|
||||||
|
if (!this.win) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (global.willAppQuit) { // when [Ctrl|Cmd]+Q
|
if (global.willAppQuit) { // when [Ctrl|Cmd]+Q
|
||||||
saveWindowState(boundsInfoPath, mainWindow);
|
this.saveWindowState(boundsInfoPath, this.win);
|
||||||
} else { // Minimize or hide the window for close button.
|
} else { // Minimize or hide the window for close button.
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
function hideWindow(window: BrowserWindow) {
|
function hideWindow(window: BrowserWindow) {
|
||||||
@@ -154,9 +243,9 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||||||
case 'linux':
|
case 'linux':
|
||||||
if (Config.minimizeToTray) {
|
if (Config.minimizeToTray) {
|
||||||
if (Config.alwaysMinimize) {
|
if (Config.alwaysMinimize) {
|
||||||
hideWindow(mainWindow);
|
hideWindow(this.win);
|
||||||
} else {
|
} else {
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(this.win, {
|
||||||
title: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.title', 'Minimize to Tray'),
|
title: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.title', 'Minimize to Tray'),
|
||||||
message: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.message', '{appName} will continue to run in the system tray. This can be disabled in Settings.', {appName: app.name}),
|
message: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.message', '{appName} will continue to run in the system tray. This can be disabled in Settings.', {appName: app.name}),
|
||||||
type: 'info',
|
type: 'info',
|
||||||
@@ -164,13 +253,13 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||||||
checkboxLabel: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel', 'Don\'t show again'),
|
checkboxLabel: localizeMessage('main.windows.mainWindow.minimizeToTray.dialog.checkboxLabel', 'Don\'t show again'),
|
||||||
}).then((result: {response: number; checkboxChecked: boolean}) => {
|
}).then((result: {response: number; checkboxChecked: boolean}) => {
|
||||||
Config.set('alwaysMinimize', result.checkboxChecked);
|
Config.set('alwaysMinimize', result.checkboxChecked);
|
||||||
hideWindow(mainWindow);
|
hideWindow(this.win!);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (Config.alwaysClose) {
|
} else if (Config.alwaysClose) {
|
||||||
app.quit();
|
app.quit();
|
||||||
} else {
|
} else {
|
||||||
dialog.showMessageBox(mainWindow, {
|
dialog.showMessageBox(this.win, {
|
||||||
title: localizeMessage('main.windows.mainWindow.closeApp.dialog.title', 'Close Application'),
|
title: localizeMessage('main.windows.mainWindow.closeApp.dialog.title', 'Close Application'),
|
||||||
message: localizeMessage('main.windows.mainWindow.closeApp.dialog.message', 'Are you sure you want to quit?'),
|
message: localizeMessage('main.windows.mainWindow.closeApp.dialog.message', 'Are you sure you want to quit?'),
|
||||||
detail: localizeMessage('main.windows.mainWindow.closeApp.dialog.detail', 'You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.', {appName: app.name}),
|
detail: localizeMessage('main.windows.mainWindow.closeApp.dialog.detail', 'You will no longer receive notifications for messages. If you want to leave {appName} running in the system tray, you can enable this in Settings.', {appName: app.name}),
|
||||||
@@ -191,11 +280,11 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||||||
break;
|
break;
|
||||||
case 'darwin':
|
case 'darwin':
|
||||||
// need to leave fullscreen first, then hide the window
|
// need to leave fullscreen first, then hide the window
|
||||||
if (mainWindow.isFullScreen()) {
|
if (this.win.isFullScreen()) {
|
||||||
mainWindow.once('leave-full-screen', () => {
|
this.win.once('leave-full-screen', () => {
|
||||||
app.hide();
|
app.hide();
|
||||||
});
|
});
|
||||||
mainWindow.setFullScreen(false);
|
this.win.setFullScreen(false);
|
||||||
} else {
|
} else {
|
||||||
app.hide();
|
app.hide();
|
||||||
}
|
}
|
||||||
@@ -203,39 +292,35 @@ function createMainWindow(options: {linuxAppIcon: string; fullscreen?: boolean})
|
|||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
}
|
||||||
|
|
||||||
// Register keyboard shortcuts
|
private onClosed = () => {
|
||||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
log.verbose('main window closed');
|
||||||
// Add Alt+Cmd+(Right|Left) as alternative to switch between servers
|
delete this.win;
|
||||||
if (process.platform === 'darwin') {
|
this.ready = false;
|
||||||
if (input.alt && input.meta) {
|
}
|
||||||
if (input.key === 'ArrowRight') {
|
|
||||||
mainWindow.webContents.send(SELECT_NEXT_TAB);
|
private onUnresponsive = () => {
|
||||||
}
|
if (!this.win) {
|
||||||
if (input.key === 'ArrowLeft') {
|
throw new Error('BrowserWindow \'unresponsive\' event has been emitted');
|
||||||
mainWindow.webContents.send(SELECT_PREVIOUS_TAB);
|
}
|
||||||
}
|
dialog.showMessageBox(this.win, {
|
||||||
|
type: 'warning',
|
||||||
|
title: app.name,
|
||||||
|
message: localizeMessage('main.CriticalErrorHandler.unresponsive.dialog.message', 'The window is no longer responsive.\nDo you wait until the window becomes responsive again?'),
|
||||||
|
buttons: [
|
||||||
|
localizeMessage('label.no', 'No'),
|
||||||
|
localizeMessage('label.yes', 'Yes'),
|
||||||
|
],
|
||||||
|
defaultId: 0,
|
||||||
|
}).then(({response}) => {
|
||||||
|
if (response === 0) {
|
||||||
|
log.error('BrowserWindow \'unresponsive\' event has been emitted');
|
||||||
|
app.relaunch();
|
||||||
}
|
}
|
||||||
}
|
});
|
||||||
});
|
}
|
||||||
|
|
||||||
// Only add shortcuts when window is in focus
|
|
||||||
mainWindow.on('focus', () => {
|
|
||||||
if (process.platform === 'linux') {
|
|
||||||
globalShortcut.registerAll(['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P'], () => {
|
|
||||||
// do nothing because we want to supress the menu popping up
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
mainWindow.on('blur', () => {
|
|
||||||
globalShortcut.unregisterAll();
|
|
||||||
});
|
|
||||||
|
|
||||||
const contextMenu = new ContextMenu({}, mainWindow);
|
|
||||||
contextMenu.reload();
|
|
||||||
|
|
||||||
return mainWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default createMainWindow;
|
const mainWindow = new MainWindow();
|
||||||
|
export default mainWindow;
|
||||||
|
@@ -1,40 +1,73 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import {BrowserWindow} from 'electron';
|
import {BrowserWindow, ipcMain} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
import {SHOW_SETTINGS_WINDOW} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
|
|
||||||
import ContextMenu from '../contextMenu';
|
import ContextMenu from '../contextMenu';
|
||||||
import {getLocalPreload, getLocalURLString} from '../utils';
|
import {getLocalPreload, getLocalURLString} from '../utils';
|
||||||
|
|
||||||
export function createSettingsWindow(mainWindow: BrowserWindow, withDevTools: boolean) {
|
import MainWindow from './mainWindow';
|
||||||
const preload = getLocalPreload('desktopAPI.js');
|
|
||||||
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
|
||||||
const settingsWindow = new BrowserWindow({
|
|
||||||
parent: mainWindow,
|
|
||||||
title: 'Desktop App Settings',
|
|
||||||
fullscreen: false,
|
|
||||||
webPreferences: {
|
|
||||||
preload,
|
|
||||||
spellcheck,
|
|
||||||
}});
|
|
||||||
|
|
||||||
const contextMenu = new ContextMenu({}, settingsWindow);
|
export class SettingsWindow {
|
||||||
contextMenu.reload();
|
private win?: BrowserWindow;
|
||||||
|
|
||||||
const localURL = getLocalURLString('settings.html');
|
constructor() {
|
||||||
settingsWindow.setMenuBarVisibility(false);
|
ipcMain.on(SHOW_SETTINGS_WINDOW, this.show);
|
||||||
settingsWindow.loadURL(localURL).catch(
|
}
|
||||||
(reason) => {
|
|
||||||
log.error(`Settings window failed to load: ${reason}`);
|
show = () => {
|
||||||
log.info(process.env);
|
if (this.win) {
|
||||||
});
|
this.win.show();
|
||||||
settingsWindow.show();
|
} else {
|
||||||
|
this.create();
|
||||||
if (withDevTools) {
|
}
|
||||||
settingsWindow.webContents.openDevTools({mode: 'detach'});
|
}
|
||||||
|
|
||||||
|
get = () => {
|
||||||
|
return this.win;
|
||||||
|
}
|
||||||
|
|
||||||
|
private create = () => {
|
||||||
|
const mainWindow = MainWindow.get(true);
|
||||||
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const preload = getLocalPreload('desktopAPI.js');
|
||||||
|
const spellcheck = (typeof Config.useSpellChecker === 'undefined' ? true : Config.useSpellChecker);
|
||||||
|
this.win = new BrowserWindow({
|
||||||
|
parent: mainWindow,
|
||||||
|
title: 'Desktop App Settings',
|
||||||
|
fullscreen: false,
|
||||||
|
webPreferences: {
|
||||||
|
preload,
|
||||||
|
spellcheck,
|
||||||
|
}});
|
||||||
|
|
||||||
|
const contextMenu = new ContextMenu({}, this.win);
|
||||||
|
contextMenu.reload();
|
||||||
|
|
||||||
|
const localURL = getLocalURLString('settings.html');
|
||||||
|
this.win.setMenuBarVisibility(false);
|
||||||
|
this.win.loadURL(localURL).catch(
|
||||||
|
(reason) => {
|
||||||
|
log.error('failed to load', reason);
|
||||||
|
});
|
||||||
|
this.win.show();
|
||||||
|
|
||||||
|
if (Boolean(process.env.MM_DEBUG_SETTINGS) || false) {
|
||||||
|
this.win.webContents.openDevTools({mode: 'detach'});
|
||||||
|
}
|
||||||
|
|
||||||
|
this.win.on('closed', () => {
|
||||||
|
delete this.win;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return settingsWindow;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const settingsWindow = new SettingsWindow();
|
||||||
|
export default settingsWindow;
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
import {app, systemPreferences, desktopCapturer} from 'electron';
|
import {systemPreferences, desktopCapturer} from 'electron';
|
||||||
|
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
|
||||||
@@ -17,9 +17,8 @@ import {
|
|||||||
} from 'main/utils';
|
} from 'main/utils';
|
||||||
|
|
||||||
import {WindowManager} from './windowManager';
|
import {WindowManager} from './windowManager';
|
||||||
import createMainWindow from './mainWindow';
|
import MainWindow from './mainWindow';
|
||||||
import {createSettingsWindow} from './settingsWindow';
|
import SettingsWindow from './settingsWindow';
|
||||||
|
|
||||||
import CallsWidgetWindow from './callsWidgetWindow';
|
import CallsWidgetWindow from './callsWidgetWindow';
|
||||||
|
|
||||||
jest.mock('path', () => ({
|
jest.mock('path', () => ({
|
||||||
@@ -78,9 +77,13 @@ jest.mock('../views/teamDropdownView', () => jest.fn());
|
|||||||
jest.mock('../views/downloadsDropdownView', () => jest.fn());
|
jest.mock('../views/downloadsDropdownView', () => jest.fn());
|
||||||
jest.mock('../views/downloadsDropdownMenuView', () => jest.fn());
|
jest.mock('../views/downloadsDropdownMenuView', () => jest.fn());
|
||||||
jest.mock('./settingsWindow', () => ({
|
jest.mock('./settingsWindow', () => ({
|
||||||
createSettingsWindow: jest.fn(),
|
show: jest.fn(),
|
||||||
|
get: jest.fn(),
|
||||||
|
}));
|
||||||
|
jest.mock('./mainWindow', () => ({
|
||||||
|
get: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
}));
|
}));
|
||||||
jest.mock('./mainWindow', () => jest.fn());
|
|
||||||
jest.mock('../downloadsManager', () => ({
|
jest.mock('../downloadsManager', () => ({
|
||||||
getDownloads: () => {},
|
getDownloads: () => {},
|
||||||
}));
|
}));
|
||||||
@@ -104,40 +107,6 @@ describe('main/windows/windowManager', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('showSettingsWindow', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
windowManager.showMainWindow = jest.fn();
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
delete windowManager.settingsWindow;
|
|
||||||
delete windowManager.mainWindow;
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show settings window if it exists', () => {
|
|
||||||
const settingsWindow = {show: jest.fn()};
|
|
||||||
windowManager.settingsWindow = settingsWindow;
|
|
||||||
windowManager.showSettingsWindow();
|
|
||||||
expect(settingsWindow.show).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create windows if they dont exist and delete the settings window when it is closed', () => {
|
|
||||||
let callback;
|
|
||||||
createSettingsWindow.mockReturnValue({on: (event, cb) => {
|
|
||||||
if (event === 'closed') {
|
|
||||||
callback = cb;
|
|
||||||
}
|
|
||||||
}});
|
|
||||||
windowManager.showSettingsWindow();
|
|
||||||
expect(windowManager.showMainWindow).toHaveBeenCalled();
|
|
||||||
expect(createSettingsWindow).toHaveBeenCalled();
|
|
||||||
expect(windowManager.settingsWindow).toBeDefined();
|
|
||||||
|
|
||||||
callback();
|
|
||||||
expect(windowManager.settingsWindow).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('showMainWindow', () => {
|
describe('showMainWindow', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.viewManager = {
|
windowManager.viewManager = {
|
||||||
@@ -146,56 +115,38 @@ describe('main/windows/windowManager', () => {
|
|||||||
};
|
};
|
||||||
windowManager.initializeViewManager = jest.fn();
|
windowManager.initializeViewManager = jest.fn();
|
||||||
|
|
||||||
|
const mainWindow = {
|
||||||
|
visible: false,
|
||||||
|
isVisible: () => mainWindow.visible,
|
||||||
|
show: jest.fn(),
|
||||||
|
focus: jest.fn(),
|
||||||
|
on: jest.fn(),
|
||||||
|
once: jest.fn(),
|
||||||
|
webContents: {
|
||||||
|
setWindowOpenHandler: jest.fn(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
mainWindow.show.mockImplementation(() => {
|
||||||
|
mainWindow.visible = true;
|
||||||
|
});
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
delete windowManager.mainWindow;
|
jest.resetAllMocks();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show main window if it exists and focus it if it is already visible', () => {
|
it('should show main window if it exists and focus it if it is already visible', () => {
|
||||||
windowManager.mainWindow = {
|
windowManager.showMainWindow();
|
||||||
visible: false,
|
expect(mainWindow.show).toHaveBeenCalled();
|
||||||
isVisible: () => windowManager.mainWindow.visible,
|
|
||||||
show: jest.fn().mockImplementation(() => {
|
|
||||||
windowManager.mainWindow.visible = true;
|
|
||||||
}),
|
|
||||||
focus: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
windowManager.showMainWindow();
|
windowManager.showMainWindow();
|
||||||
expect(windowManager.mainWindow.show).toHaveBeenCalled();
|
expect(mainWindow.focus).toHaveBeenCalled();
|
||||||
|
|
||||||
windowManager.showMainWindow();
|
|
||||||
expect(windowManager.mainWindow.focus).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should quit the app when the main window fails to create', () => {
|
|
||||||
windowManager.showMainWindow();
|
|
||||||
expect(app.quit).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create the main window and add listeners', () => {
|
|
||||||
const window = {
|
|
||||||
on: jest.fn(),
|
|
||||||
once: jest.fn(),
|
|
||||||
webContents: {
|
|
||||||
setWindowOpenHandler: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
createMainWindow.mockReturnValue(window);
|
|
||||||
windowManager.showMainWindow();
|
|
||||||
expect(windowManager.mainWindow).toBe(window);
|
|
||||||
expect(window.on).toHaveBeenCalled();
|
|
||||||
expect(window.webContents.setWindowOpenHandler).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should open deep link when provided', () => {
|
it('should open deep link when provided', () => {
|
||||||
const window = {
|
|
||||||
on: jest.fn(),
|
|
||||||
once: jest.fn(),
|
|
||||||
webContents: {
|
|
||||||
setWindowOpenHandler: jest.fn(),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
createMainWindow.mockReturnValue(window);
|
|
||||||
windowManager.showMainWindow('mattermost://server-1.com/subpath');
|
windowManager.showMainWindow('mattermost://server-1.com/subpath');
|
||||||
expect(windowManager.viewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath');
|
expect(windowManager.viewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath');
|
||||||
});
|
});
|
||||||
@@ -218,7 +169,7 @@ describe('main/windows/windowManager', () => {
|
|||||||
getCurrentView: () => view,
|
getCurrentView: () => view,
|
||||||
setLoadingScreenBounds: jest.fn(),
|
setLoadingScreenBounds: jest.fn(),
|
||||||
};
|
};
|
||||||
windowManager.mainWindow = {
|
const mainWindow = {
|
||||||
getContentBounds: () => ({width: 800, height: 600}),
|
getContentBounds: () => ({width: 800, height: 600}),
|
||||||
getSize: () => [1000, 900],
|
getSize: () => [1000, 900],
|
||||||
};
|
};
|
||||||
@@ -227,6 +178,7 @@ describe('main/windows/windowManager', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
jest.useFakeTimers();
|
jest.useFakeTimers();
|
||||||
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
||||||
});
|
});
|
||||||
@@ -281,15 +233,16 @@ describe('main/windows/windowManager', () => {
|
|||||||
setLoadingScreenBounds: jest.fn(),
|
setLoadingScreenBounds: jest.fn(),
|
||||||
loadingScreenState: 3,
|
loadingScreenState: 3,
|
||||||
};
|
};
|
||||||
windowManager.mainWindow = {
|
|
||||||
getContentBounds: () => ({width: 1000, height: 900}),
|
|
||||||
getSize: () => [1000, 900],
|
|
||||||
};
|
|
||||||
windowManager.teamDropdown = {
|
windowManager.teamDropdown = {
|
||||||
updateWindowBounds: jest.fn(),
|
updateWindowBounds: jest.fn(),
|
||||||
};
|
};
|
||||||
|
const mainWindow = {
|
||||||
|
getContentBounds: () => ({width: 1000, height: 900}),
|
||||||
|
getSize: () => [1000, 900],
|
||||||
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -333,12 +286,13 @@ describe('main/windows/windowManager', () => {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
windowManager.mainWindow = {
|
const mainWindow = {
|
||||||
getContentBounds: () => ({width: 800, height: 600}),
|
getContentBounds: () => ({width: 800, height: 600}),
|
||||||
getSize: () => [1000, 900],
|
getSize: () => [1000, 900],
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -392,7 +346,7 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('restoreMain', () => {
|
describe('restoreMain', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.mainWindow = {
|
const mainWindow = {
|
||||||
isVisible: jest.fn(),
|
isVisible: jest.fn(),
|
||||||
isMinimized: jest.fn(),
|
isMinimized: jest.fn(),
|
||||||
restore: jest.fn(),
|
restore: jest.fn(),
|
||||||
@@ -400,134 +354,62 @@ describe('main/windows/windowManager', () => {
|
|||||||
focus: jest.fn(),
|
focus: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
jest.resetAllMocks();
|
jest.resetAllMocks();
|
||||||
delete windowManager.settingsWindow;
|
delete windowManager.settingsWindow;
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should restore main window if minimized', () => {
|
it('should restore main window if minimized', () => {
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
mainWindow.isMinimized.mockReturnValue(true);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.mainWindow.restore).toHaveBeenCalled();
|
expect(mainWindow.restore).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show main window if not visible or minimized', () => {
|
it('should show main window if not visible or minimized', () => {
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
mainWindow.isVisible.mockReturnValue(false);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
mainWindow.isMinimized.mockReturnValue(false);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.mainWindow.show).toHaveBeenCalled();
|
expect(mainWindow.show).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should focus main window if visible and not minimized', () => {
|
it('should focus main window if visible and not minimized', () => {
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
mainWindow.isVisible.mockReturnValue(true);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
mainWindow.isMinimized.mockReturnValue(false);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.mainWindow.focus).toHaveBeenCalled();
|
expect(mainWindow.focus).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should focus settings window regardless of main window state if it exists', () => {
|
it('should focus settings window regardless of main window state if it exists', () => {
|
||||||
windowManager.settingsWindow = {
|
const settingsWindow = {focus: jest.fn()};
|
||||||
focus: jest.fn(),
|
SettingsWindow.get.mockReturnValue(settingsWindow);
|
||||||
};
|
|
||||||
|
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
mainWindow.isVisible.mockReturnValue(false);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
mainWindow.isMinimized.mockReturnValue(false);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
expect(settingsWindow.focus).toHaveBeenCalled();
|
||||||
windowManager.settingsWindow.focus.mockClear();
|
settingsWindow.focus.mockClear();
|
||||||
|
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
mainWindow.isVisible.mockReturnValue(true);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(false);
|
mainWindow.isMinimized.mockReturnValue(false);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
expect(settingsWindow.focus).toHaveBeenCalled();
|
||||||
windowManager.settingsWindow.focus.mockClear();
|
settingsWindow.focus.mockClear();
|
||||||
|
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(false);
|
mainWindow.isVisible.mockReturnValue(false);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
mainWindow.isMinimized.mockReturnValue(true);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
expect(settingsWindow.focus).toHaveBeenCalled();
|
||||||
windowManager.settingsWindow.focus.mockClear();
|
settingsWindow.focus.mockClear();
|
||||||
|
|
||||||
windowManager.mainWindow.isVisible.mockReturnValue(true);
|
mainWindow.isVisible.mockReturnValue(true);
|
||||||
windowManager.mainWindow.isMinimized.mockReturnValue(true);
|
mainWindow.isMinimized.mockReturnValue(true);
|
||||||
windowManager.restoreMain();
|
windowManager.restoreMain();
|
||||||
expect(windowManager.settingsWindow.focus).toHaveBeenCalled();
|
expect(settingsWindow.focus).toHaveBeenCalled();
|
||||||
windowManager.settingsWindow.focus.mockClear();
|
settingsWindow.focus.mockClear();
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('flashFrame', () => {
|
|
||||||
const windowManager = new WindowManager();
|
|
||||||
windowManager.mainWindow = {
|
|
||||||
flashFrame: jest.fn(),
|
|
||||||
};
|
|
||||||
windowManager.settingsWindow = {
|
|
||||||
flashFrame: jest.fn(),
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
Config.notifications = {};
|
|
||||||
});
|
|
||||||
|
|
||||||
afterEach(() => {
|
|
||||||
jest.resetAllMocks();
|
|
||||||
Config.notifications = {};
|
|
||||||
});
|
|
||||||
|
|
||||||
it('linux/windows - should not flash frame when config item is not set', () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'linux',
|
|
||||||
});
|
|
||||||
windowManager.flashFrame(true);
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
expect(windowManager.mainWindow.flashFrame).not.toBeCalled();
|
|
||||||
expect(windowManager.settingsWindow.flashFrame).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('linux/windows - should flash frame when config item is set', () => {
|
|
||||||
Config.notifications = {
|
|
||||||
flashWindow: true,
|
|
||||||
};
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'linux',
|
|
||||||
});
|
|
||||||
windowManager.flashFrame(true);
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
expect(windowManager.mainWindow.flashFrame).toBeCalledWith(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('mac - should not bounce icon when config item is not set', () => {
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'darwin',
|
|
||||||
});
|
|
||||||
windowManager.flashFrame(true);
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
expect(app.dock.bounce).not.toBeCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('mac - should bounce icon when config item is set', () => {
|
|
||||||
Config.notifications = {
|
|
||||||
bounceIcon: true,
|
|
||||||
bounceIconType: 'critical',
|
|
||||||
};
|
|
||||||
const originalPlatform = process.platform;
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: 'darwin',
|
|
||||||
});
|
|
||||||
windowManager.flashFrame(true);
|
|
||||||
Object.defineProperty(process, 'platform', {
|
|
||||||
value: originalPlatform,
|
|
||||||
});
|
|
||||||
expect(app.dock.bounce).toHaveBeenCalledWith('critical');
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -569,13 +451,13 @@ describe('main/windows/windowManager', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should maximize when not maximized and vice versa', () => {
|
it('should maximize when not maximized and vice versa', () => {
|
||||||
windowManager.mainWindow = mainWindow;
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
|
|
||||||
windowManager.mainWindow.isMaximized.mockReturnValue(false);
|
mainWindow.isMaximized.mockReturnValue(false);
|
||||||
windowManager.handleDoubleClick();
|
windowManager.handleDoubleClick();
|
||||||
expect(mainWindow.maximize).toHaveBeenCalled();
|
expect(mainWindow.maximize).toHaveBeenCalled();
|
||||||
|
|
||||||
windowManager.mainWindow.isMaximized.mockReturnValue(true);
|
mainWindow.isMaximized.mockReturnValue(true);
|
||||||
windowManager.handleDoubleClick();
|
windowManager.handleDoubleClick();
|
||||||
expect(mainWindow.unmaximize).toHaveBeenCalled();
|
expect(mainWindow.unmaximize).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
@@ -585,16 +467,15 @@ describe('main/windows/windowManager', () => {
|
|||||||
Object.defineProperty(process, 'platform', {
|
Object.defineProperty(process, 'platform', {
|
||||||
value: 'darwin',
|
value: 'darwin',
|
||||||
});
|
});
|
||||||
windowManager.flashFrame(true);
|
|
||||||
|
|
||||||
systemPreferences.getUserDefault.mockReturnValue('Minimize');
|
systemPreferences.getUserDefault.mockReturnValue('Minimize');
|
||||||
windowManager.settingsWindow = settingsWindow;
|
SettingsWindow.get.mockReturnValue(settingsWindow);
|
||||||
|
|
||||||
windowManager.settingsWindow.isMinimized.mockReturnValue(false);
|
settingsWindow.isMinimized.mockReturnValue(false);
|
||||||
windowManager.handleDoubleClick(null, 'settings');
|
windowManager.handleDoubleClick(null, 'settings');
|
||||||
expect(settingsWindow.minimize).toHaveBeenCalled();
|
expect(settingsWindow.minimize).toHaveBeenCalled();
|
||||||
|
|
||||||
windowManager.settingsWindow.isMinimized.mockReturnValue(true);
|
settingsWindow.isMinimized.mockReturnValue(true);
|
||||||
windowManager.handleDoubleClick(null, 'settings');
|
windowManager.handleDoubleClick(null, 'settings');
|
||||||
expect(settingsWindow.restore).toHaveBeenCalled();
|
expect(settingsWindow.restore).toHaveBeenCalled();
|
||||||
|
|
||||||
@@ -1418,10 +1299,10 @@ describe('main/windows/windowManager', () => {
|
|||||||
|
|
||||||
describe('handleCallsError', () => {
|
describe('handleCallsError', () => {
|
||||||
const windowManager = new WindowManager();
|
const windowManager = new WindowManager();
|
||||||
windowManager.switchServer = jest.fn();
|
const mainWindow = {
|
||||||
windowManager.mainWindow = {
|
|
||||||
focus: jest.fn(),
|
focus: jest.fn(),
|
||||||
};
|
};
|
||||||
|
windowManager.switchServer = jest.fn();
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
CallsWidgetWindow.mockImplementation(() => {
|
CallsWidgetWindow.mockImplementation(() => {
|
||||||
@@ -1436,6 +1317,7 @@ describe('main/windows/windowManager', () => {
|
|||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
MainWindow.get.mockReturnValue(mainWindow);
|
||||||
});
|
});
|
||||||
|
|
||||||
afterEach(() => {
|
afterEach(() => {
|
||||||
@@ -1447,7 +1329,7 @@ describe('main/windows/windowManager', () => {
|
|||||||
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
windowManager.callsWidgetWindow = new CallsWidgetWindow();
|
||||||
windowManager.handleCallsError('', {err: 'client-error'});
|
windowManager.handleCallsError('', {err: 'client-error'});
|
||||||
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
|
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
|
||||||
expect(windowManager.mainWindow.focus).toHaveBeenCalled();
|
expect(mainWindow.focus).toHaveBeenCalled();
|
||||||
expect(windowManager.callsWidgetWindow.getMainView().view.webContents.send).toHaveBeenCalledWith('calls-error', {err: 'client-error'});
|
expect(windowManager.callsWidgetWindow.getMainView().view.webContents.send).toHaveBeenCalledWith('calls-error', {err: 'client-error'});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -4,7 +4,7 @@
|
|||||||
/* eslint-disable max-lines */
|
/* eslint-disable max-lines */
|
||||||
import path from 'path';
|
import path from 'path';
|
||||||
|
|
||||||
import {app, BrowserWindow, nativeImage, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent, desktopCapturer} from 'electron';
|
import {app, BrowserWindow, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent, desktopCapturer} from 'electron';
|
||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -55,7 +55,6 @@ import {
|
|||||||
} from '../utils';
|
} from '../utils';
|
||||||
|
|
||||||
import {ViewManager, LoadingScreenState} from '../views/viewManager';
|
import {ViewManager, LoadingScreenState} from '../views/viewManager';
|
||||||
import CriticalErrorHandler from '../CriticalErrorHandler';
|
|
||||||
|
|
||||||
import TeamDropdownView from '../views/teamDropdownView';
|
import TeamDropdownView from '../views/teamDropdownView';
|
||||||
import DownloadsDropdownView from '../views/downloadsDropdownView';
|
import DownloadsDropdownView from '../views/downloadsDropdownView';
|
||||||
@@ -63,19 +62,16 @@ import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView';
|
|||||||
|
|
||||||
import downloadsManager from 'main/downloadsManager';
|
import downloadsManager from 'main/downloadsManager';
|
||||||
|
|
||||||
import {createSettingsWindow} from './settingsWindow';
|
import MainWindow from './mainWindow';
|
||||||
import createMainWindow from './mainWindow';
|
|
||||||
|
|
||||||
import CallsWidgetWindow from './callsWidgetWindow';
|
import CallsWidgetWindow from './callsWidgetWindow';
|
||||||
|
import SettingsWindow from './settingsWindow';
|
||||||
|
|
||||||
// singleton module to manage application's windows
|
// singleton module to manage application's windows
|
||||||
|
|
||||||
export class WindowManager {
|
export class WindowManager {
|
||||||
assetsDir: string;
|
assetsDir: string;
|
||||||
|
|
||||||
mainWindow?: BrowserWindow;
|
|
||||||
mainWindowReady: boolean;
|
|
||||||
settingsWindow?: BrowserWindow;
|
|
||||||
callsWidgetWindow?: CallsWidgetWindow;
|
callsWidgetWindow?: CallsWidgetWindow;
|
||||||
viewManager?: ViewManager;
|
viewManager?: ViewManager;
|
||||||
teamDropdown?: TeamDropdownView;
|
teamDropdown?: TeamDropdownView;
|
||||||
@@ -85,7 +81,6 @@ export class WindowManager {
|
|||||||
missingScreensharePermissions?: boolean;
|
missingScreensharePermissions?: boolean;
|
||||||
|
|
||||||
constructor() {
|
constructor() {
|
||||||
this.mainWindowReady = false;
|
|
||||||
this.assetsDir = path.resolve(app.getAppPath(), 'assets');
|
this.assetsDir = path.resolve(app.getAppPath(), 'assets');
|
||||||
|
|
||||||
ipcMain.on(HISTORY, this.handleHistory);
|
ipcMain.on(HISTORY, this.handleHistory);
|
||||||
@@ -145,7 +140,7 @@ export class WindowManager {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.callsWidgetWindow = new CallsWidgetWindow(this.mainWindow!, currentView, {
|
this.callsWidgetWindow = new CallsWidgetWindow(MainWindow.get()!, currentView, {
|
||||||
callID: msg.callID,
|
callID: msg.callID,
|
||||||
title: msg.title,
|
title: msg.title,
|
||||||
rootID: msg.rootID,
|
rootID: msg.rootID,
|
||||||
@@ -160,7 +155,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
if (this.callsWidgetWindow) {
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
this.switchServer(this.callsWidgetWindow.getServerName());
|
||||||
this.mainWindow?.focus();
|
MainWindow.get()?.focus();
|
||||||
this.callsWidgetWindow.getMainView().view.webContents.send(DESKTOP_SOURCES_MODAL_REQUEST);
|
this.callsWidgetWindow.getMainView().view.webContents.send(DESKTOP_SOURCES_MODAL_REQUEST);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -170,7 +165,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
if (this.callsWidgetWindow) {
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
this.switchServer(this.callsWidgetWindow.getServerName());
|
||||||
this.mainWindow?.focus();
|
MainWindow.get()?.focus();
|
||||||
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, this.callsWidgetWindow.getChannelURL());
|
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, this.callsWidgetWindow.getChannelURL());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -180,7 +175,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
if (this.callsWidgetWindow) {
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
this.switchServer(this.callsWidgetWindow.getServerName());
|
||||||
this.mainWindow?.focus();
|
MainWindow.get()?.focus();
|
||||||
this.callsWidgetWindow.getMainView().view.webContents.send(CALLS_ERROR, msg);
|
this.callsWidgetWindow.getMainView().view.webContents.send(CALLS_ERROR, msg);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -190,7 +185,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
if (this.callsWidgetWindow) {
|
if (this.callsWidgetWindow) {
|
||||||
this.switchServer(this.callsWidgetWindow.getServerName());
|
this.switchServer(this.callsWidgetWindow.getServerName());
|
||||||
this.mainWindow?.focus();
|
MainWindow.get()?.focus();
|
||||||
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, msg.link);
|
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, msg.link);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -201,100 +196,49 @@ export class WindowManager {
|
|||||||
this.callsWidgetWindow?.close();
|
this.callsWidgetWindow?.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
showSettingsWindow = () => {
|
|
||||||
log.debug('WindowManager.showSettingsWindow');
|
|
||||||
|
|
||||||
if (this.settingsWindow) {
|
|
||||||
this.settingsWindow.show();
|
|
||||||
} else {
|
|
||||||
if (!this.mainWindow) {
|
|
||||||
this.showMainWindow();
|
|
||||||
}
|
|
||||||
const withDevTools = Boolean(process.env.MM_DEBUG_SETTINGS) || false;
|
|
||||||
|
|
||||||
this.settingsWindow = createSettingsWindow(this.mainWindow!, withDevTools);
|
|
||||||
this.settingsWindow.on('closed', () => {
|
|
||||||
delete this.settingsWindow;
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
showMainWindow = (deeplinkingURL?: string | URL) => {
|
showMainWindow = (deeplinkingURL?: string | URL) => {
|
||||||
log.debug('WindowManager.showMainWindow', deeplinkingURL);
|
log.debug('WindowManager.showMainWindow', deeplinkingURL);
|
||||||
|
|
||||||
if (this.mainWindow) {
|
const mainWindow = MainWindow.get();
|
||||||
if (this.mainWindow.isVisible()) {
|
if (mainWindow) {
|
||||||
this.mainWindow.focus();
|
if (mainWindow.isVisible()) {
|
||||||
|
mainWindow.focus();
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow.show();
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
this.mainWindowReady = false;
|
this.createMainWindow();
|
||||||
this.mainWindow = createMainWindow({
|
|
||||||
linuxAppIcon: path.join(this.assetsDir, 'linux', 'app_icon.png'),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!this.mainWindow) {
|
|
||||||
log.error('unable to create main window');
|
|
||||||
app.quit();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.mainWindow.once('ready-to-show', () => {
|
|
||||||
this.mainWindowReady = true;
|
|
||||||
});
|
|
||||||
|
|
||||||
// window handlers
|
|
||||||
this.mainWindow.on('closed', () => {
|
|
||||||
log.warn('main window closed');
|
|
||||||
delete this.mainWindow;
|
|
||||||
this.mainWindowReady = false;
|
|
||||||
});
|
|
||||||
this.mainWindow.on('unresponsive', () => {
|
|
||||||
CriticalErrorHandler.setMainWindow(this.mainWindow!);
|
|
||||||
CriticalErrorHandler.windowUnresponsiveHandler();
|
|
||||||
});
|
|
||||||
this.mainWindow.on('maximize', this.handleMaximizeMainWindow);
|
|
||||||
this.mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow);
|
|
||||||
if (process.platform !== 'darwin') {
|
|
||||||
this.mainWindow.on('resize', this.handleResizeMainWindow);
|
|
||||||
}
|
|
||||||
this.mainWindow.on('will-resize', this.handleWillResizeMainWindow);
|
|
||||||
this.mainWindow.on('resized', this.handleResizedMainWindow);
|
|
||||||
this.mainWindow.on('focus', this.focusBrowserView);
|
|
||||||
this.mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen'));
|
|
||||||
this.mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen'));
|
|
||||||
|
|
||||||
// Should not allow the main window to generate a window of its own
|
|
||||||
this.mainWindow.webContents.setWindowOpenHandler(() => ({action: 'deny'}));
|
|
||||||
|
|
||||||
if (process.env.MM_DEBUG_SETTINGS) {
|
|
||||||
this.mainWindow.webContents.openDevTools({mode: 'detach'});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.viewManager) {
|
|
||||||
this.viewManager.updateMainWindow(this.mainWindow);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.teamDropdown = new TeamDropdownView(this.mainWindow, Config.teams, Config.darkMode, Config.enableServerManagement);
|
|
||||||
this.downloadsDropdown = new DownloadsDropdownView(this.mainWindow, downloadsManager.getDownloads(), Config.darkMode);
|
|
||||||
this.downloadsDropdownMenu = new DownloadsDropdownMenuView(this.mainWindow, Config.darkMode);
|
|
||||||
}
|
}
|
||||||
this.initializeViewManager();
|
|
||||||
|
|
||||||
if (deeplinkingURL) {
|
if (deeplinkingURL) {
|
||||||
this.viewManager!.handleDeepLink(deeplinkingURL);
|
this.viewManager?.handleDeepLink(deeplinkingURL);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
getMainWindow = (ensureCreated?: boolean) => {
|
private createMainWindow = () => {
|
||||||
if (ensureCreated && !this.mainWindow) {
|
const mainWindow = MainWindow.get(true);
|
||||||
this.showMainWindow();
|
if (!mainWindow) {
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
return this.mainWindow;
|
|
||||||
}
|
|
||||||
|
|
||||||
on = this.mainWindow?.on;
|
// window handlers
|
||||||
|
mainWindow.on('maximize', this.handleMaximizeMainWindow);
|
||||||
|
mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow);
|
||||||
|
if (process.platform !== 'darwin') {
|
||||||
|
mainWindow.on('resize', this.handleResizeMainWindow);
|
||||||
|
}
|
||||||
|
mainWindow.on('will-resize', this.handleWillResizeMainWindow);
|
||||||
|
mainWindow.on('resized', this.handleResizedMainWindow);
|
||||||
|
mainWindow.on('focus', this.focusBrowserView);
|
||||||
|
mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen'));
|
||||||
|
mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen'));
|
||||||
|
|
||||||
|
this.teamDropdown = new TeamDropdownView(Config.teams, Config.darkMode, Config.enableServerManagement);
|
||||||
|
this.downloadsDropdown = new DownloadsDropdownView(downloadsManager.getDownloads(), Config.darkMode);
|
||||||
|
this.downloadsDropdownMenu = new DownloadsDropdownMenuView(Config.darkMode);
|
||||||
|
|
||||||
|
this.initializeViewManager();
|
||||||
|
}
|
||||||
|
|
||||||
handleMaximizeMainWindow = () => {
|
handleMaximizeMainWindow = () => {
|
||||||
this.downloadsDropdown?.updateWindowBounds();
|
this.downloadsDropdown?.updateWindowBounds();
|
||||||
@@ -313,7 +257,7 @@ export class WindowManager {
|
|||||||
handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => {
|
handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => {
|
||||||
log.silly('WindowManager.handleWillResizeMainWindow');
|
log.silly('WindowManager.handleWillResizeMainWindow');
|
||||||
|
|
||||||
if (!(this.viewManager && this.mainWindow)) {
|
if (!(this.viewManager && MainWindow.get())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -343,7 +287,7 @@ export class WindowManager {
|
|||||||
handleResizedMainWindow = () => {
|
handleResizedMainWindow = () => {
|
||||||
log.silly('WindowManager.handleResizedMainWindow');
|
log.silly('WindowManager.handleResizedMainWindow');
|
||||||
|
|
||||||
if (this.mainWindow) {
|
if (MainWindow.get()) {
|
||||||
const bounds = this.getBounds();
|
const bounds = this.getBounds();
|
||||||
this.throttledWillResize(bounds);
|
this.throttledWillResize(bounds);
|
||||||
ipcMain.emit(RESIZE_MODAL, null, bounds);
|
ipcMain.emit(RESIZE_MODAL, null, bounds);
|
||||||
@@ -368,7 +312,7 @@ export class WindowManager {
|
|||||||
handleResizeMainWindow = () => {
|
handleResizeMainWindow = () => {
|
||||||
log.silly('WindowManager.handleResizeMainWindow');
|
log.silly('WindowManager.handleResizeMainWindow');
|
||||||
|
|
||||||
if (!(this.viewManager && this.mainWindow)) {
|
if (!(this.viewManager && MainWindow.get())) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (this.isResizing) {
|
if (this.isResizing) {
|
||||||
@@ -405,15 +349,16 @@ export class WindowManager {
|
|||||||
private getBounds = () => {
|
private getBounds = () => {
|
||||||
let bounds;
|
let bounds;
|
||||||
|
|
||||||
if (this.mainWindow) {
|
const mainWindow = MainWindow.get();
|
||||||
|
if (mainWindow) {
|
||||||
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
|
// Workaround for linux maximizing/minimizing, which doesn't work properly because of these bugs:
|
||||||
// https://github.com/electron/electron/issues/28699
|
// https://github.com/electron/electron/issues/28699
|
||||||
// https://github.com/electron/electron/issues/28106
|
// https://github.com/electron/electron/issues/28106
|
||||||
if (process.platform === 'linux') {
|
if (process.platform === 'linux') {
|
||||||
const size = this.mainWindow.getSize();
|
const size = mainWindow.getSize();
|
||||||
bounds = {width: size[0], height: size[1]};
|
bounds = {width: size[0], height: size[1]};
|
||||||
} else {
|
} else {
|
||||||
bounds = this.mainWindow.getContentBounds();
|
bounds = mainWindow.getContentBounds();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -422,7 +367,8 @@ export class WindowManager {
|
|||||||
|
|
||||||
// max retries allows the message to get to the renderer even if it is sent while the app is starting up.
|
// max retries allows the message to get to the renderer even if it is sent while the app is starting up.
|
||||||
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => {
|
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => {
|
||||||
if (!this.mainWindow || !this.mainWindowReady) {
|
const mainWindow = MainWindow.get();
|
||||||
|
if (!mainWindow || !MainWindow.isReady) {
|
||||||
if (maxRetries > 0) {
|
if (maxRetries > 0) {
|
||||||
log.info(`Can't send ${channel}, will retry`);
|
log.info(`Can't send ${channel}, will retry`);
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -433,14 +379,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.mainWindow!.webContents.send(channel, ...args);
|
mainWindow.webContents.send(channel, ...args);
|
||||||
if (this.settingsWindow && this.settingsWindow.isVisible()) {
|
SettingsWindow.get()?.webContents.send(channel, ...args);
|
||||||
try {
|
|
||||||
this.settingsWindow.webContents.send(channel, ...args);
|
|
||||||
} catch (e) {
|
|
||||||
log.error(`There was an error while trying to communicate with the renderer: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sendToRenderer = (channel: string, ...args: unknown[]) => {
|
sendToRenderer = (channel: string, ...args: unknown[]) => {
|
||||||
@@ -449,9 +389,7 @@ export class WindowManager {
|
|||||||
|
|
||||||
sendToAll = (channel: string, ...args: unknown[]) => {
|
sendToAll = (channel: string, ...args: unknown[]) => {
|
||||||
this.sendToRenderer(channel, ...args);
|
this.sendToRenderer(channel, ...args);
|
||||||
if (this.settingsWindow) {
|
SettingsWindow.get()?.webContents.send(channel, ...args);
|
||||||
this.settingsWindow.webContents.send(channel, ...args);
|
|
||||||
}
|
|
||||||
|
|
||||||
// TODO: should we include popups?
|
// TODO: should we include popups?
|
||||||
}
|
}
|
||||||
@@ -464,103 +402,31 @@ export class WindowManager {
|
|||||||
|
|
||||||
restoreMain = () => {
|
restoreMain = () => {
|
||||||
log.info('restoreMain');
|
log.info('restoreMain');
|
||||||
if (!this.mainWindow) {
|
|
||||||
this.showMainWindow();
|
const mainWindow = MainWindow.get(true);
|
||||||
|
if (!mainWindow) {
|
||||||
|
throw new Error('Main window does not exist');
|
||||||
}
|
}
|
||||||
if (!this.mainWindow!.isVisible() || this.mainWindow!.isMinimized()) {
|
|
||||||
if (this.mainWindow!.isMinimized()) {
|
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
|
||||||
this.mainWindow!.restore();
|
if (mainWindow.isMinimized()) {
|
||||||
|
mainWindow.restore();
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow!.show();
|
mainWindow.show();
|
||||||
}
|
}
|
||||||
if (this.settingsWindow) {
|
const settingsWindow = SettingsWindow.get();
|
||||||
this.settingsWindow.focus();
|
if (settingsWindow) {
|
||||||
|
settingsWindow.focus();
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow!.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
} else if (this.settingsWindow) {
|
} else if (SettingsWindow.get()) {
|
||||||
this.settingsWindow.focus();
|
SettingsWindow.get()?.focus();
|
||||||
} else {
|
} else {
|
||||||
this.mainWindow!.focus();
|
mainWindow.focus();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
flashFrame = (flash: boolean) => {
|
|
||||||
if (process.platform === 'linux' || process.platform === 'win32') {
|
|
||||||
if (Config.notifications.flashWindow) {
|
|
||||||
this.mainWindow?.flashFrame(flash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (process.platform === 'darwin' && Config.notifications.bounceIcon) {
|
|
||||||
app.dock.bounce(Config.notifications.bounceIconType);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
drawBadge = (text: string, small: boolean) => {
|
|
||||||
const scale = 2; // should rely display dpi
|
|
||||||
const size = (small ? 20 : 16) * scale;
|
|
||||||
const canvas = document.createElement('canvas');
|
|
||||||
canvas.setAttribute('width', `${size}`);
|
|
||||||
canvas.setAttribute('height', `${size}`);
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
|
|
||||||
if (!ctx) {
|
|
||||||
log.error('Could not create canvas context');
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// circle
|
|
||||||
ctx.fillStyle = '#FF1744'; // Material Red A400
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(size / 2, size / 2, size / 2, 0, Math.PI * 2);
|
|
||||||
ctx.fill();
|
|
||||||
|
|
||||||
// text
|
|
||||||
ctx.fillStyle = '#ffffff';
|
|
||||||
ctx.textAlign = 'center';
|
|
||||||
ctx.textBaseline = 'middle';
|
|
||||||
ctx.font = (11 * scale) + 'px sans-serif';
|
|
||||||
ctx.fillText(text, size / 2, size / 2, size);
|
|
||||||
|
|
||||||
return canvas.toDataURL();
|
|
||||||
}
|
|
||||||
|
|
||||||
createDataURL = (text: string, small: boolean) => {
|
|
||||||
const win = this.mainWindow;
|
|
||||||
if (!win) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// since we don't have a document/canvas object in the main process, we use the webcontents from the window to draw.
|
|
||||||
const safeSmall = Boolean(small);
|
|
||||||
const code = `
|
|
||||||
window.drawBadge = ${this.drawBadge};
|
|
||||||
window.drawBadge('${text || ''}', ${safeSmall});
|
|
||||||
`;
|
|
||||||
return win.webContents.executeJavaScript(code);
|
|
||||||
}
|
|
||||||
|
|
||||||
setOverlayIcon = async (badgeText: string | undefined, description: string, small: boolean) => {
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
let overlay = null;
|
|
||||||
if (this.mainWindow) {
|
|
||||||
if (badgeText) {
|
|
||||||
try {
|
|
||||||
const dataUrl = await this.createDataURL(badgeText, small);
|
|
||||||
overlay = nativeImage.createFromDataURL(dataUrl);
|
|
||||||
} catch (err) {
|
|
||||||
log.error(`Couldn't generate a badge: ${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.mainWindow.setOverlayIcon(overlay, description);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
isMainWindow = (window: BrowserWindow) => {
|
|
||||||
return this.mainWindow && this.mainWindow === window;
|
|
||||||
}
|
|
||||||
|
|
||||||
handleDoubleClick = (e: IpcMainEvent, windowType?: string) => {
|
handleDoubleClick = (e: IpcMainEvent, windowType?: string) => {
|
||||||
log.debug('WindowManager.handleDoubleClick', windowType);
|
log.debug('WindowManager.handleDoubleClick', windowType);
|
||||||
|
|
||||||
@@ -568,7 +434,7 @@ export class WindowManager {
|
|||||||
if (process.platform === 'darwin') {
|
if (process.platform === 'darwin') {
|
||||||
action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||||
}
|
}
|
||||||
const win = (windowType === 'settings') ? this.settingsWindow : this.mainWindow;
|
const win = (windowType === 'settings') ? SettingsWindow.get() : MainWindow.get();
|
||||||
if (!win) {
|
if (!win) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -592,8 +458,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
initializeViewManager = () => {
|
initializeViewManager = () => {
|
||||||
if (!this.viewManager && Config && this.mainWindow) {
|
if (!this.viewManager && Config) {
|
||||||
this.viewManager = new ViewManager(this.mainWindow);
|
this.viewManager = new ViewManager();
|
||||||
this.viewManager.load();
|
this.viewManager.load();
|
||||||
this.viewManager.showInitial();
|
this.viewManager.showInitial();
|
||||||
this.initializeCurrentServerName();
|
this.initializeCurrentServerName();
|
||||||
@@ -658,10 +524,8 @@ export class WindowManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
focusThreeDotMenu = () => {
|
focusThreeDotMenu = () => {
|
||||||
if (this.mainWindow) {
|
MainWindow.get()?.webContents.focus();
|
||||||
this.mainWindow.webContents.focus();
|
MainWindow.get()?.webContents.send(FOCUS_THREE_DOT_MENU);
|
||||||
this.mainWindow.webContents.send(FOCUS_THREE_DOT_MENU);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
handleLoadingScreenDataRequest = () => {
|
handleLoadingScreenDataRequest = () => {
|
||||||
|
Reference in New Issue
Block a user