[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:
Devin Binnie
2023-04-04 10:01:40 -04:00
committed by GitHub
parent c682cf5dd2
commit 22ec280945
46 changed files with 1131 additions and 990 deletions

View File

@@ -7,6 +7,7 @@ import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communi
import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from 'common/tabs/MessagingTabView';
import MainWindow from '../windows/mainWindow';
import * as WindowManager from '../windows/windowManager';
import * as appState from '../appState';
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', () => ({
sendToRenderer: jest.fn(),
focusThreeDotMenu: jest.fn(),
}));
jest.mock('../appState', () => ({
updateMentions: jest.fn(),
@@ -54,9 +58,10 @@ const tabView = new MessagingTabView(server);
describe('main/views/MattermostView', () => {
describe('load', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
mattermostView.loadSuccess = jest.fn();
mattermostView.loadRetry = jest.fn();
});
@@ -112,11 +117,12 @@ describe('main/views/MattermostView', () => {
describe('retry', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
const retryInBackgroundFn = jest.fn();
beforeEach(() => {
jest.useFakeTimers();
MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.loadSuccess = jest.fn();
mattermostView.loadRetry = jest.fn();
@@ -175,10 +181,11 @@ describe('main/views/MattermostView', () => {
describe('loadSuccess', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
jest.useFakeTimers();
MainWindow.get.mockReturnValue(window);
mattermostView.emit = jest.fn();
mattermostView.setBounds = jest.fn();
mattermostView.setInitialized = jest.fn();
@@ -202,10 +209,11 @@ describe('main/views/MattermostView', () => {
describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
jest.useFakeTimers();
MainWindow.get.mockReturnValue(window);
mattermostView.setBounds = jest.fn();
mattermostView.focus = jest.fn();
});
@@ -253,9 +261,10 @@ describe('main/views/MattermostView', () => {
describe('destroy', () => {
const window = {removeBrowserView: jest.fn(), on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.destroy = jest.fn();
});
@@ -280,17 +289,18 @@ describe('main/views/MattermostView', () => {
describe('handleInputEvents', () => {
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', () => {
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: 'keyUp'});
expect(WindowManager.focusThreeDotMenu).toHaveBeenCalled();
expect(MainWindow.focusThreeDotMenu).toHaveBeenCalled();
});
it('should not open three dot menu on holding Alt', () => {
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', () => {
@@ -298,15 +308,16 @@ describe('main/views/MattermostView', () => {
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyDown'});
mattermostView.handleInputEvents(null, {key: 'F', type: 'keyUp'});
mattermostView.handleInputEvents(null, {key: 'Alt', type: 'keyUp'});
expect(WindowManager.focusThreeDotMenu).not.toHaveBeenCalled();
expect(MainWindow.focusThreeDotMenu).not.toHaveBeenCalled();
});
});
describe('handleDidNavigate', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
mattermostView.setBounds = jest.fn();
});
@@ -325,9 +336,10 @@ describe('main/views/MattermostView', () => {
describe('handleUpdateTarget', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
mattermostView.emit = jest.fn();
});
@@ -355,8 +367,7 @@ describe('main/views/MattermostView', () => {
});
describe('updateMentionsFromTitle', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, window, {});
const mattermostView = new MattermostView(tabView, {}, {});
it('should parse mentions from title', () => {
mattermostView.updateMentionsFromTitle('(7) Mattermost');

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// 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 log from 'electron-log';
@@ -25,6 +25,8 @@ import {MattermostServer} from 'common/servers/MattermostServer';
import {TabView, TabTuple} from 'common/tabs/TabView';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow';
import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils';
import WindowManager from '../windows/windowManager';
@@ -43,7 +45,6 @@ const MENTIONS_GROUP = 2;
export class MattermostView extends EventEmitter {
tab: TabView;
window: BrowserWindow;
view: BrowserView;
isVisible: boolean;
isLoggedIn: boolean;
@@ -63,10 +64,9 @@ export class MattermostView extends EventEmitter {
private altPressStatus: boolean;
constructor(tab: TabView, serverInfo: ServerInfo, win: BrowserWindow, options: BrowserViewConstructorOptions) {
constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) {
super();
this.tab = tab;
this.window = win;
this.serverInfo = serverInfo;
const preload = getLocalPreload('preload.js');
@@ -118,7 +118,7 @@ export class MattermostView extends EventEmitter {
this.altPressStatus = false;
this.window.on('blur', () => {
MainWindow.get()?.on('blur', () => {
this.altPressStatus = false;
});
}
@@ -224,6 +224,11 @@ export class MattermostView extends EventEmitter {
loadSuccess = (loadURL: string) => {
return () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
log.verbose(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
this.maxRetries = MAX_SERVER_RETRIES;
@@ -235,21 +240,26 @@ export class MattermostView extends EventEmitter {
this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
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) => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.hasBeenShown = true;
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
if (request && !this.isVisible) {
this.window.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(this.window, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
mainWindow.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
if (this.status === Status.READY) {
this.focus();
}
} else if (!request && this.isVisible) {
this.window.removeBrowserView(this.view);
mainWindow.removeBrowserView(this.view);
}
this.isVisible = request;
}
@@ -268,9 +278,7 @@ export class MattermostView extends EventEmitter {
destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id);
appState.updateMentions(this.tab.name, 0, false);
if (this.window) {
this.window.removeBrowserView(this.view);
}
MainWindow.get()?.removeBrowserView(this.view);
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
@@ -352,7 +360,7 @@ export class MattermostView extends EventEmitter {
this.registerAltKeyPressed(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});
if (shouldHaveBackBar(this.tab.url || '', url)) {
this.setBounds(getWindowBoundaries(this.window, true));
this.setBounds(getWindowBoundaries(MainWindow.get()!, true));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
log.info('show back button');
} else {
this.setBounds(getWindowBoundaries(this.window));
this.setBounds(getWindowBoundaries(MainWindow.get()!));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
log.info('hide back button');
}

View File

@@ -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 MainWindow from 'main/windows/mainWindow';
import DownloadsDropdownMenuView from './downloadsDropdownMenuView';
jest.mock('main/utils', () => ({
@@ -50,6 +52,11 @@ jest.mock('electron', () => {
jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(),
}));
jest.mock('main/downloadsManager', () => ({}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
getBounds: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
@@ -60,24 +67,21 @@ jest.mock('fs', () => ({
}));
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(() => {
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('getBounds', () => {
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});
});
});
it('should change the view bounds based on open/closed state', () => {
const downloadsDropdownMenuView = new DownloadsDropdownMenuView();
downloadsDropdownMenuView.bounds = {width: 400, height: 300};
downloadsDropdownMenuView.handleOpen();
expect(downloadsDropdownMenuView.view.setBounds).toBeCalledWith(downloadsDropdownMenuView.bounds);

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
import log from 'electron-log';
@@ -30,40 +30,23 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import downloadsManager from 'main/downloadsManager';
import MainWindow from 'main/windows/mainWindow';
export default class DownloadsDropdownMenuView {
open: boolean;
view: BrowserView;
bounds?: Electron.Rectangle;
bounds: Electron.Rectangle;
item?: DownloadedItem;
coordinates?: CoordinatesToJsonType;
darkMode: boolean;
window: BrowserWindow;
windowBounds: Electron.Rectangle;
constructor(window: BrowserWindow, darkMode: boolean) {
constructor(darkMode: boolean) {
this.open = false;
this.item = undefined;
this.coordinates = undefined;
this.window = window;
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(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);
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_CLEAR_FILE, this.clearFile);
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) => {
@@ -98,9 +102,12 @@ export default class DownloadsDropdownMenuView {
updateWindowBounds = () => {
log.debug('DownloadsDropdownMenuView.updateWindowBounds');
this.windowBounds = this.window.getContentBounds();
this.updateDownloadsDropdownMenu();
this.repositionDownloadsDropdownMenu();
const mainWindow = MainWindow.get();
if (mainWindow) {
this.windowBounds = mainWindow.getContentBounds();
this.updateDownloadsDropdownMenu();
this.repositionDownloadsDropdownMenu();
}
}
updateDownloadsDropdownMenu = () => {
@@ -131,7 +138,7 @@ export default class DownloadsDropdownMenuView {
this.item = item;
this.bounds = this.getBounds(DOWNLOADS_DROPDOWN_MENU_FULL_WIDTH, DOWNLOADS_DROPDOWN_MENU_FULL_HEIGHT);
this.view.setBounds(this.bounds);
this.window.setTopBrowserView(this.view);
MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus();
this.updateDownloadsDropdownMenu();
}

View File

@@ -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 MainWindow from 'main/windows/mainWindow';
import DownloadsDropdownView from './downloadsDropdownView';
jest.mock('main/utils', () => ({
@@ -59,42 +61,35 @@ jest.mock('electron', () => {
Notification: NotificationMock,
};
});
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
getBounds: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
describe('main/views/DownloadsDropdownView', () => {
beforeEach(() => {
MainWindow.get.mockReturnValue({addBrowserView: jest.fn(), setTopBrowserView: jest.fn()});
getDarwinDoNotDisturb.mockReturnValue(false);
});
describe('getBounds', () => {
it('should be placed far right when window is large enough', () => {
const window = {
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
const downloadsDropdownView = new DownloadsDropdownView();
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', () => {
const window = {
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
const downloadsDropdownView = new DownloadsDropdownView();
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', () => {
const window = {
getContentBounds: () => ({width: 800, height: 600, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
const downloadsDropdownView = new DownloadsDropdownView(window, {}, false);
MainWindow.getBounds.mockReturnValue({width: 800, height: 600, x: 0, y: 0});
const downloadsDropdownView = new DownloadsDropdownView();
downloadsDropdownView.bounds = {width: 400, height: 300};
downloadsDropdownView.handleOpen();
expect(downloadsDropdownView.view.setBounds).toBeCalledWith(downloadsDropdownView.bounds);

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// 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';
@@ -25,23 +25,38 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import downloadsManager from 'main/downloadsManager';
import MainWindow from 'main/windows/mainWindow';
export default class DownloadsDropdownView {
bounds?: Electron.Rectangle;
darkMode: boolean;
downloads: DownloadedItems;
item: DownloadedItem | undefined;
item?: DownloadedItem;
view: BrowserView;
window: BrowserWindow;
windowBounds: Electron.Rectangle;
constructor(window: BrowserWindow, downloads: DownloadedItems, darkMode: boolean) {
constructor(downloads: DownloadedItems, darkMode: boolean) {
this.downloads = downloads;
this.window = window;
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);
const preload = getLocalPreload('desktopAPI.js');
@@ -55,20 +70,8 @@ export default class DownloadsDropdownView {
}});
this.view.webContents.loadURL(getLocalURLString('downloadsDropdown.html'));
this.window.addBrowserView(this.view);
this.view.webContents.session.webRequest.onHeadersReceived(downloadsManager.webRequestOnHeadersReceivedHandler);
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);
mainWindow.addBrowserView(this.view);
}
updateDownloads = (event: IpcMainEvent, downloads: DownloadedItems) => {
@@ -99,9 +102,12 @@ export default class DownloadsDropdownView {
updateWindowBounds = () => {
log.debug('DownloadsDropdownView.updateWindowBounds');
this.windowBounds = this.window.getContentBounds();
this.updateDownloadsDropdown();
this.repositionDownloadsDropdown();
const mainWindow = MainWindow.get();
if (mainWindow) {
this.windowBounds = mainWindow.getContentBounds();
this.updateDownloadsDropdown();
this.repositionDownloadsDropdown();
}
}
updateDownloadsDropdown = () => {
@@ -124,7 +130,7 @@ export default class DownloadsDropdownView {
}
this.view.setBounds(this.bounds);
this.window.setTopBrowserView(this.view);
MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus();
downloadsManager.onOpen();
WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);
@@ -172,7 +178,7 @@ export default class DownloadsDropdownView {
}
repositionDownloadsDropdown = () => {
if (!this.bounds) {
if (!(this.bounds && this.windowBounds)) {
return;
}
this.bounds = {

View File

@@ -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 MainWindow from 'main/windows/mainWindow';
import TeamDropdownView from './teamDropdownView';
jest.mock('main/utils', () => ({
@@ -24,20 +26,23 @@ jest.mock('electron', () => ({
on: jest.fn(),
},
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
getBounds: jest.fn(),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
}));
jest.mock('../windows/windowManager', () => ({
sendToRenderer: jest.fn(),
}));
describe('main/views/teamDropdownView', () => {
const window = {
getContentBounds: () => ({width: 500, height: 400, x: 0, y: 0}),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
};
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') {
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});
@@ -50,7 +55,7 @@ describe('main/views/teamDropdownView', () => {
});
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.handleOpen();
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
@@ -60,7 +65,7 @@ describe('main/views/teamDropdownView', () => {
describe('addGpoToTeams', () => {
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 = [{
name: 'team-1',
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', () => {
const teamDropdownView = new TeamDropdownView(window, [], false, true);
const teamDropdownView = new TeamDropdownView([], false, true);
const teams = [{
name: 'team-1',
url: 'https://mattermost.team-1.com',

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
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 {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import MainWindow from '../windows/mainWindow';
export default class TeamDropdownView {
view: BrowserView;
@@ -33,18 +34,16 @@ export default class TeamDropdownView {
unreads?: Map<string, boolean>;
mentions?: Map<string, number>;
expired?: Map<string, boolean>;
window: BrowserWindow;
windowBounds: Electron.Rectangle;
windowBounds?: Electron.Rectangle;
isOpen: boolean;
constructor(window: BrowserWindow, teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
constructor(teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
this.teams = this.addGpoToTeams(teams, []);
this.window = window;
this.darkMode = darkMode;
this.enableServerManagement = enableServerManagement;
this.isOpen = false;
this.windowBounds = this.window.getContentBounds();
this.windowBounds = MainWindow.getBounds();
const preload = getLocalPreload('desktopAPI.js');
this.view = new BrowserView({webPreferences: {
@@ -57,7 +56,7 @@ export default class TeamDropdownView {
}});
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(CLOSE_TEAMS_DROPDOWN, this.handleClose);
@@ -95,7 +94,7 @@ export default class TeamDropdownView {
}
updateWindowBounds = () => {
this.windowBounds = this.window.getContentBounds();
this.windowBounds = MainWindow.getBounds();
this.updateDropdown();
}
@@ -123,7 +122,7 @@ export default class TeamDropdownView {
return;
}
this.view.setBounds(this.bounds);
this.window.setTopBrowserView(this.view);
MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus();
WindowManager.sendToRenderer(OPEN_TEAMS_DROPDOWN);
this.isOpen = true;

View File

@@ -7,11 +7,13 @@
import {dialog, ipcMain} from 'electron';
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 {getTabViewName} from 'common/tabs/TabView';
import {equalUrlsIgnoringSubpath} from 'common/utils/url';
import MainWindow from 'main/windows/mainWindow';
import {MattermostView} from './MattermostView';
import {ViewManager} from './viewManager';
@@ -56,6 +58,9 @@ jest.mock('main/server/serverInfo', () => ({
ServerInfo: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
jest.mock('./MattermostView', () => ({
MattermostView: jest.fn(),
}));
@@ -174,15 +179,18 @@ describe('main/views/viewManager', () => {
});
describe('reloadConfiguration', () => {
const viewManager = new ViewManager({});
const viewManager = new ViewManager();
beforeEach(() => {
viewManager.loadView = jest.fn();
viewManager.showByName = jest.fn();
viewManager.showInitial = jest.fn();
viewManager.mainWindow.webContents = {
send: jest.fn(),
const mainWindow = {
webContents: {
send: jest.fn(),
},
};
MainWindow.get.mockReturnValue(mainWindow);
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
name: `${srv.name}-${tabName}`,
@@ -653,10 +661,11 @@ describe('main/views/viewManager', () => {
setTopBrowserView: jest.fn(),
addBrowserView: jest.fn(),
};
const viewManager = new ViewManager(window);
const viewManager = new ViewManager();
const loadingScreen = {webContents: {send: jest.fn(), isLoading: () => false}};
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
viewManager.createLoadingScreen = jest.fn();
viewManager.setLoadingScreenBounds = jest.fn();
window.getBrowserViews.mockImplementation(() => []);

View File

@@ -1,8 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, dialog, ipcMain, IpcMainEvent} from 'electron';
import log from 'electron-log';
import {BrowserView, BrowserWindow, dialog, ipcMain, IpcMainEvent} from 'electron';
import {BrowserViewConstructorOptions} from 'electron/main';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {Tab, TeamWithTabs} from 'types/config';
@@ -33,6 +35,7 @@ import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
import {localizeMessage} from 'main/i18nManager';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow';
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
@@ -57,23 +60,17 @@ export class ViewManager {
currentView?: string;
urlView?: BrowserView;
urlViewCancel?: () => void;
mainWindow: BrowserWindow;
loadingScreen?: BrowserView;
loadingScreenState: LoadingScreenState;
constructor(mainWindow: BrowserWindow) {
constructor() {
this.lastActiveServer = Config.lastActiveTeam;
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.mainWindow = mainWindow;
this.closedViews = new Map();
this.loadingScreenState = LoadingScreenState.HIDDEN;
}
updateMainWindow = (mainWindow: BrowserWindow) => {
this.mainWindow = mainWindow;
}
getServers = () => {
return Config.teams.concat();
}
@@ -86,7 +83,7 @@ export class ViewManager {
makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => {
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.load(url);
view.on(UPDATE_TARGET_URL, this.showURLView);
@@ -186,7 +183,7 @@ export class ViewManager {
this.currentView = undefined;
this.showInitial();
} else {
this.mainWindow.webContents.send(SET_ACTIVE_VIEW);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW);
}
}
@@ -196,7 +193,7 @@ export class ViewManager {
if (view) {
this.currentView = 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 {
this.showInitial();
@@ -221,7 +218,7 @@ export class ViewManager {
}
}
} else {
this.mainWindow.webContents.send(SET_ACTIVE_VIEW, null, null);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null);
ipcMain.emit(MAIN_WINDOW_SHOWN);
}
}
@@ -248,7 +245,7 @@ export class ViewManager {
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);
if (newView.isReady()) {
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 localURL = getLocalURLString('urlView.html', query);
urlView.webContents.loadURL(localURL);
this.mainWindow.addBrowserView(urlView);
const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? this.mainWindow.getBounds();
MainWindow.get()?.addBrowserView(urlView);
const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? MainWindow.get()!.getBounds();
const hideView = () => {
delete this.urlViewCancel;
try {
this.mainWindow.removeBrowserView(urlView);
MainWindow.get()?.removeBrowserView(urlView);
} catch (e) {
log.error('Failed to remove URL view', e);
}
@@ -421,9 +418,11 @@ export class ViewManager {
}
setLoadingScreenBounds = () => {
if (this.loadingScreen) {
this.loadingScreen.setBounds(getWindowBoundaries(this.mainWindow));
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.loadingScreen?.setBounds(getWindowBoundaries(mainWindow));
}
createLoadingScreen = () => {
@@ -441,6 +440,11 @@ export class ViewManager {
}
showLoadingScreen = () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
if (!this.loadingScreen) {
this.createLoadingScreen();
}
@@ -455,10 +459,10 @@ export class ViewManager {
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
}
if (this.mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
this.mainWindow.setTopBrowserView(this.loadingScreen!);
if (mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
mainWindow.setTopBrowserView(this.loadingScreen!);
} else {
this.mainWindow.addBrowserView(this.loadingScreen!);
mainWindow.addBrowserView(this.loadingScreen!);
}
this.setLoadingScreenBounds();
@@ -474,7 +478,7 @@ export class ViewManager {
hideLoadingScreen = () => {
if (this.loadingScreen && this.loadingScreenState !== LoadingScreenState.HIDDEN) {
this.loadingScreenState = LoadingScreenState.HIDDEN;
this.mainWindow.removeBrowserView(this.loadingScreen);
MainWindow.get()?.removeBrowserView(this.loadingScreen);
}
}