[MM-51874] Migrate loading screen to singleton (#2655)

* Migrate loadingScreen to singleton

* REVERT ME when MainWindow singleton changes are merged

* Revert "REVERT ME when MainWindow singleton changes are merged"

This reverts commit 2de5520117b9aefb8eeb161d493de7cb275f7a5b.
This commit is contained in:
Devin Binnie
2023-04-04 11:49:40 -04:00
committed by GitHub
parent 22ec280945
commit 39150137b6
10 changed files with 204 additions and 170 deletions

View File

@@ -44,6 +44,7 @@ jest.mock('main/badge', () => ({
jest.mock('main/tray/tray', () => ({
refreshTrayImages: jest.fn(),
}));
jest.mock('main/views/loadingScreen', () => ({}));
jest.mock('main/windows/windowManager', () => ({
handleUpdateConfig: jest.fn(),
sendToRenderer: jest.fn(),

View File

@@ -12,6 +12,7 @@ import Config from 'common/config';
import AutoLauncher from 'main/AutoLauncher';
import {setUnreadBadgeSetting} from 'main/badge';
import {refreshTrayImages} from 'main/tray/tray';
import LoadingScreen from 'main/views/loadingScreen';
import WindowManager from 'main/windows/windowManager';
import {handleMainWindowIsShown} from './intercom';
@@ -80,7 +81,7 @@ export function handleDarkModeChange(darkMode: boolean) {
refreshTrayImages(Config.trayIconTheme);
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode);
WindowManager.updateLoadingScreenDarkMode(darkMode);
LoadingScreen.setDarkMode(darkMode);
ipcMain.emit(EMIT_CONFIGURATION, true, Config.data);
}

View File

@@ -0,0 +1,56 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import MainWindow from 'main/windows/mainWindow';
import {LoadingScreen} from './loadingScreen';
jest.mock('electron', () => ({
ipcMain: {
on: jest.fn(),
},
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
describe('main/views/loadingScreen', () => {
describe('show', () => {
const mainWindow = {
getBrowserViews: jest.fn(),
setTopBrowserView: jest.fn(),
addBrowserView: jest.fn(),
};
const loadingScreen = new LoadingScreen();
loadingScreen.create = jest.fn();
loadingScreen.setBounds = jest.fn();
const view = {webContents: {send: jest.fn(), isLoading: () => false}};
beforeEach(() => {
mainWindow.getBrowserViews.mockImplementation(() => []);
MainWindow.get.mockReturnValue(mainWindow);
});
afterEach(() => {
delete loadingScreen.view;
jest.resetAllMocks();
});
it('should create new loading screen if one doesnt exist and add it to the window', () => {
loadingScreen.create.mockImplementation(() => {
loadingScreen.view = view;
});
loadingScreen.show();
expect(loadingScreen.create).toHaveBeenCalled();
expect(mainWindow.addBrowserView).toHaveBeenCalled();
});
it('should set the browser view as top if already exists and needs to be shown', () => {
loadingScreen.view = view;
mainWindow.getBrowserViews.mockImplementation(() => [view]);
loadingScreen.show();
expect(mainWindow.setTopBrowserView).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,115 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {BrowserView, app, ipcMain} from 'electron';
import log from 'electron-log';
import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils';
import MainWindow from 'main/windows/mainWindow';
enum LoadingScreenState {
VISIBLE = 1,
FADING = 2,
HIDDEN = 3,
}
export class LoadingScreen {
private view?: BrowserView;
private state: LoadingScreenState;
constructor() {
this.state = LoadingScreenState.HIDDEN;
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleAnimationFinished);
}
/**
* Loading Screen
*/
setBounds = () => {
if (this.view) {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.view.setBounds(getWindowBoundaries(mainWindow));
}
}
setDarkMode = (darkMode: boolean) => {
this.view?.webContents.send(DARK_MODE_CHANGE, darkMode);
}
isHidden = () => {
return this.state === LoadingScreenState.HIDDEN;
}
show = () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
if (!this.view) {
this.create();
}
this.state = LoadingScreenState.VISIBLE;
if (this.view?.webContents.isLoading()) {
this.view.webContents.once('did-finish-load', () => {
this.view!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
});
} else {
this.view!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
}
if (mainWindow.getBrowserViews().includes(this.view!)) {
mainWindow.setTopBrowserView(this.view!);
} else {
mainWindow.addBrowserView(this.view!);
}
this.setBounds();
}
fade = () => {
if (this.view && this.state === LoadingScreenState.VISIBLE) {
this.state = LoadingScreenState.FADING;
this.view.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, false);
}
}
private create = () => {
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,
}});
const localURL = getLocalURLString('loadingScreen.html');
this.view.webContents.loadURL(localURL);
}
private handleAnimationFinished = () => {
log.debug('handleLoadingScreenAnimationFinished');
if (this.view && this.state !== LoadingScreenState.HIDDEN) {
this.state = LoadingScreenState.HIDDEN;
MainWindow.get()?.removeBrowserView(this.view);
}
if (process.env.NODE_ENV === 'test') {
app.emit('e2e-app-loaded');
}
}
}
const loadingScreen = new LoadingScreen();
export default loadingScreen;

View File

@@ -16,6 +16,7 @@ import MainWindow from 'main/windows/mainWindow';
import {MattermostView} from './MattermostView';
import {ViewManager} from './viewManager';
import LoadingScreen from './loadingScreen';
jest.mock('electron', () => ({
app: {
@@ -57,7 +58,10 @@ jest.mock('main/i18nManager', () => ({
jest.mock('main/server/serverInfo', () => ({
ServerInfo: jest.fn(),
}));
jest.mock('main/views/loadingScreen', () => ({
show: jest.fn(),
fade: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
@@ -78,7 +82,6 @@ describe('main/views/viewManager', () => {
const destroyFn = jest.fn();
beforeEach(() => {
viewManager.createLoadingScreen = jest.fn();
viewManager.showByName = jest.fn();
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({name: `${srv.name}-${tabName}`}));
MattermostView.mockImplementation((tab) => ({
@@ -92,7 +95,6 @@ describe('main/views/viewManager', () => {
afterEach(() => {
jest.resetAllMocks();
viewManager.loadingScreen = undefined;
viewManager.closedViews = new Map();
viewManager.views = new Map();
});
@@ -112,7 +114,6 @@ describe('main/views/viewManager', () => {
it('should add view to views map and add listeners', () => {
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}, 'http://server-1.com/subpath');
expect(viewManager.views.has('server1-tab1')).toBe(true);
expect(viewManager.createLoadingScreen).toHaveBeenCalled();
expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView);
expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath');
});
@@ -218,7 +219,6 @@ describe('main/views/viewManager', () => {
afterEach(() => {
jest.resetAllMocks();
delete viewManager.loadingScreen;
delete viewManager.currentView;
viewManager.closedViews = new Map();
viewManager.views = new Map();
@@ -582,8 +582,6 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.getCurrentView = jest.fn();
viewManager.showLoadingScreen = jest.fn();
viewManager.fadeLoadingScreen = jest.fn();
});
afterEach(() => {
@@ -641,7 +639,7 @@ describe('main/views/viewManager', () => {
view.needsLoadingScreen.mockImplementation(() => true);
viewManager.views.set('view1', view);
viewManager.showByName('view1');
expect(viewManager.showLoadingScreen).toHaveBeenCalled();
expect(LoadingScreen.show).toHaveBeenCalled();
});
it('should show the view when not errored', () => {
@@ -655,44 +653,6 @@ describe('main/views/viewManager', () => {
});
});
describe('showLoadingScreen', () => {
const window = {
getBrowserViews: jest.fn(),
setTopBrowserView: jest.fn(),
addBrowserView: jest.fn(),
};
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(() => []);
});
afterEach(() => {
jest.resetAllMocks();
delete viewManager.loadingScreen;
});
it('should create new loading screen if one doesnt exist and add it to the window', () => {
viewManager.createLoadingScreen.mockImplementation(() => {
viewManager.loadingScreen = loadingScreen;
});
viewManager.showLoadingScreen();
expect(viewManager.createLoadingScreen).toHaveBeenCalled();
expect(window.addBrowserView).toHaveBeenCalled();
});
it('should set the browser view as top if already exists and needs to be shown', () => {
viewManager.loadingScreen = loadingScreen;
window.getBrowserViews.mockImplementation(() => [loadingScreen]);
viewManager.showLoadingScreen();
expect(window.setTopBrowserView).toHaveBeenCalled();
});
});
describe('getViewByURL', () => {
const viewManager = new ViewManager({});
viewManager.getServers = () => [

View File

@@ -14,7 +14,6 @@ import {
UPDATE_TARGET_URL,
LOAD_SUCCESS,
LOAD_FAILED,
TOGGLE_LOADING_SCREEN_VISIBILITY,
LOADSCREEN_END,
SET_ACTIVE_VIEW,
OPEN_TAB,
@@ -22,7 +21,6 @@ import {
UPDATE_LAST_ACTIVE,
UPDATE_URL_VIEW_WIDTH,
MAIN_WINDOW_SHOWN,
DARK_MODE_CHANGE,
} from 'common/communication';
import Config from 'common/config';
import urlUtils, {equalUrlsIgnoringSubpath} from 'common/utils/url';
@@ -37,21 +35,16 @@ import {localizeMessage} from 'main/i18nManager';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow';
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
import {getLocalURLString, getLocalPreload} from '../utils';
import {MattermostView} from './MattermostView';
import modalManager from './modalManager';
import WebContentsEventManager from './webContentEvents';
import LoadingScreen from './loadingScreen';
const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 20;
export enum LoadingScreenState {
VISIBLE = 1,
FADING = 2,
HIDDEN = 3,
}
export class ViewManager {
lastActiveServer?: number;
viewOptions: BrowserViewConstructorOptions;
@@ -60,15 +53,12 @@ export class ViewManager {
currentView?: string;
urlView?: BrowserView;
urlViewCancel?: () => void;
loadingScreen?: BrowserView;
loadingScreenState: LoadingScreenState;
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.closedViews = new Map();
this.loadingScreenState = LoadingScreenState.HIDDEN;
}
getServers = () => {
@@ -97,9 +87,6 @@ export class ViewManager {
if (this.closedViews.has(view.name)) {
this.closedViews.delete(view.name);
}
if (!this.loadingScreen) {
this.createLoadingScreen();
}
}
loadView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string) => {
@@ -242,7 +229,7 @@ export class ViewManager {
if (!newView.isErrored()) {
newView.show();
if (newView.needsLoadingScreen()) {
this.showLoadingScreen();
LoadingScreen.show();
}
}
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type);
@@ -290,7 +277,7 @@ export class ViewManager {
const view = this.views.get(server);
if (view && this.getCurrentView() === view) {
this.showByName(this.currentView!);
this.fadeLoadingScreen();
LoadingScreen.fade();
}
}
@@ -314,7 +301,7 @@ export class ViewManager {
failLoading = (tabName: string) => {
log.debug('viewManager.failLoading', tabName);
this.fadeLoadingScreen();
LoadingScreen.fade();
if (this.currentView === tabName) {
this.getCurrentView()?.hide();
}
@@ -417,87 +404,16 @@ export class ViewManager {
}
}
setLoadingScreenBounds = () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.loadingScreen?.setBounds(getWindowBoundaries(mainWindow));
}
createLoadingScreen = () => {
const preload = getLocalPreload('desktopAPI.js');
this.loadingScreen = 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,
}});
const localURL = getLocalURLString('loadingScreen.html');
this.loadingScreen.webContents.loadURL(localURL);
}
showLoadingScreen = () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
if (!this.loadingScreen) {
this.createLoadingScreen();
}
this.loadingScreenState = LoadingScreenState.VISIBLE;
if (this.loadingScreen?.webContents.isLoading()) {
this.loadingScreen.webContents.once('did-finish-load', () => {
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
});
} else {
this.loadingScreen!.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, true);
}
if (mainWindow.getBrowserViews().includes(this.loadingScreen!)) {
mainWindow.setTopBrowserView(this.loadingScreen!);
} else {
mainWindow.addBrowserView(this.loadingScreen!);
}
this.setLoadingScreenBounds();
}
fadeLoadingScreen = () => {
if (this.loadingScreen && this.loadingScreenState === LoadingScreenState.VISIBLE) {
this.loadingScreenState = LoadingScreenState.FADING;
this.loadingScreen.webContents.send(TOGGLE_LOADING_SCREEN_VISIBILITY, false);
}
}
hideLoadingScreen = () => {
if (this.loadingScreen && this.loadingScreenState !== LoadingScreenState.HIDDEN) {
this.loadingScreenState = LoadingScreenState.HIDDEN;
MainWindow.get()?.removeBrowserView(this.loadingScreen);
}
}
setServerInitialized = (server: string) => {
const view = this.views.get(server);
if (view) {
view.setInitialized();
if (this.getCurrentView() === view) {
this.fadeLoadingScreen();
LoadingScreen.fade();
}
}
}
updateLoadingScreenDarkMode = (darkMode: boolean) => {
if (this.loadingScreen) {
this.loadingScreen.webContents.send(DARK_MODE_CHANGE, darkMode);
}
}
deeplinkSuccess = (viewName: string) => {
log.debug('viewManager.deeplinkSuccess', viewName);

View File

@@ -10,6 +10,7 @@ import {
MINIMUM_CALLS_WIDGET_HEIGHT,
CALLS_PLUGIN_ID,
} from 'common/utils/constants';
import WebContentsEventManager from '../views/webContentEvents';
import CallsWidgetWindow from './callsWidgetWindow';

View File

@@ -14,6 +14,7 @@ import * as Validator from 'common/Validator';
import ContextMenu from '../contextMenu';
import {isInsideRectangle} from '../utils';
import {MainWindow} from './mainWindow';
jest.mock('path', () => ({

View File

@@ -15,6 +15,7 @@ import {
resetScreensharePermissionsMacOS,
openScreensharePermissionsSettingsMacOS,
} from 'main/utils';
import LoadingScreen from '../views/loadingScreen';
import {WindowManager} from './windowManager';
import MainWindow from './mainWindow';
@@ -68,11 +69,12 @@ jest.mock('../utils', () => ({
}));
jest.mock('../views/viewManager', () => ({
ViewManager: jest.fn(),
LoadingScreenState: {
HIDDEN: 3,
},
}));
jest.mock('../CriticalErrorHandler', () => jest.fn());
jest.mock('../views/loadingScreen', () => ({
isHidden: jest.fn(),
setBounds: jest.fn(),
}));
jest.mock('../views/teamDropdownView', () => jest.fn());
jest.mock('../views/downloadsDropdownView', () => jest.fn());
jest.mock('../views/downloadsDropdownMenuView', () => jest.fn());
@@ -193,7 +195,7 @@ describe('main/windows/windowManager', () => {
it('should update loading screen and team dropdown bounds', () => {
windowManager.handleResizeMainWindow();
expect(windowManager.viewManager.setLoadingScreenBounds).toHaveBeenCalled();
expect(LoadingScreen.setBounds).toHaveBeenCalled();
expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled();
});
@@ -254,12 +256,13 @@ describe('main/windows/windowManager', () => {
it('should update loading screen and team dropdown bounds', () => {
const event = {preventDefault: jest.fn()};
windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600});
expect(windowManager.viewManager.setLoadingScreenBounds).toHaveBeenCalled();
expect(LoadingScreen.setBounds).toHaveBeenCalled();
expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled();
});
it('should not resize if the app is already resizing', () => {
windowManager.isResizing = true;
LoadingScreen.isHidden.mockReturnValue(true);
const event = {preventDefault: jest.fn()};
windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600});
expect(view.setBounds).not.toHaveBeenCalled();

View File

@@ -18,7 +18,6 @@ import {
MAXIMIZE_CHANGE,
HISTORY,
REACT_APP_INITIALIZED,
LOADING_SCREEN_ANIMATION_FINISHED,
FOCUS_THREE_DOT_MENU,
GET_DARK_MODE,
UPDATE_SHORTCUT_MENU,
@@ -45,6 +44,7 @@ import {SECOND} from 'common/utils/constants';
import Config from 'common/config';
import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView';
import downloadsManager from 'main/downloadsManager';
import {MattermostView} from 'main/views/MattermostView';
import {
@@ -54,14 +54,12 @@ import {
openScreensharePermissionsSettingsMacOS,
} from '../utils';
import {ViewManager, LoadingScreenState} from '../views/viewManager';
import {ViewManager} from '../views/viewManager';
import LoadingScreen from '../views/loadingScreen';
import TeamDropdownView from '../views/teamDropdownView';
import DownloadsDropdownView from '../views/downloadsDropdownView';
import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView';
import downloadsManager from 'main/downloadsManager';
import MainWindow from './mainWindow';
import CallsWidgetWindow from './callsWidgetWindow';
@@ -86,7 +84,6 @@ export class WindowManager {
ipcMain.on(HISTORY, this.handleHistory);
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleLoadingScreenAnimationFinished);
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
ipcMain.on(BROWSER_HISTORY_BUTTON, this.handleBrowserHistoryButton);
ipcMain.on(APP_LOGGED_IN, this.handleAppLoggedIn);
@@ -270,14 +267,14 @@ export class WindowManager {
return;
}
if (this.isResizing && this.viewManager.loadingScreenState === LoadingScreenState.HIDDEN && this.viewManager.getCurrentView()) {
if (this.isResizing && LoadingScreen.isHidden() && this.viewManager.getCurrentView()) {
log.debug('prevented resize');
event.preventDefault();
return;
}
this.throttledWillResize(newBounds);
this.viewManager?.setLoadingScreenBounds();
LoadingScreen.setBounds();
this.teamDropdown?.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds();
@@ -324,7 +321,8 @@ export class WindowManager {
// Another workaround since the window doesn't update properly under Linux for some reason
// See above comment
setTimeout(this.setCurrentViewBounds, 10, bounds);
this.viewManager.setLoadingScreenBounds();
LoadingScreen.setBounds();
this.teamDropdown?.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds();
@@ -542,24 +540,6 @@ export class WindowManager {
}
}
handleLoadingScreenAnimationFinished = () => {
log.debug('WindowManager.handleLoadingScreenAnimationFinished');
if (this.viewManager) {
this.viewManager.hideLoadingScreen();
}
if (process.env.NODE_ENV === 'test') {
app.emit('e2e-app-loaded');
}
}
updateLoadingScreenDarkMode = (darkMode: boolean) => {
if (this.viewManager) {
this.viewManager.updateLoadingScreenDarkMode(darkMode);
}
}
getViewNameByWebContentsId = (webContentsId: number) => {
const view = this.viewManager?.findViewByWebContent(webContentsId);
return view?.name;
@@ -599,7 +579,7 @@ export class WindowManager {
reload = () => {
const currentView = this.viewManager?.getCurrentView();
if (currentView) {
this.viewManager?.showLoadingScreen();
LoadingScreen.show();
currentView.reload();
}
}