From 829b49571fdb88b1daba5e8eaddf003fada22bf0 Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Tue, 26 Jul 2022 10:25:13 -0400 Subject: [PATCH] [MM-43941] Optimize app resizing on Windows/Linux by waiting for the viewport to resize (#2204) * [MM-43941] Optimize app resizing on Windows/Linux by waiting for the viewport to resize * Some mitigations for Windows * Make the logs sillier * Fixed the no servers case Co-authored-by: Mattermod --- src/common/communication.ts | 2 + src/main/preload/mattermost.js | 4 ++ src/main/views/viewManager.ts | 2 +- src/main/windows/windowManager.test.js | 80 +++++++++++++++++------ src/main/windows/windowManager.ts | 89 ++++++++++++++++++-------- 5 files changed, 132 insertions(+), 45 deletions(-) diff --git a/src/common/communication.ts b/src/common/communication.ts index 6d04a559..3465b76f 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -126,3 +126,5 @@ export const PING_DOMAIN_RESPONSE = 'ping-domain-response'; export const GET_LANGUAGE_INFORMATION = 'get-language-information'; export const RETRIEVED_LANGUAGE_INFORMATION = 'retrieved-language-information'; export const GET_AVAILABLE_LANGUAGES = 'get-available-languages'; + +export const VIEW_FINISHED_RESIZING = 'view-finished-resizing'; diff --git a/src/main/preload/mattermost.js b/src/main/preload/mattermost.js index 6f8e2b9b..644fc71c 100644 --- a/src/main/preload/mattermost.js +++ b/src/main/preload/mattermost.js @@ -29,6 +29,7 @@ import { GET_VIEW_WEBCONTENTS_ID, DISPATCH_GET_DESKTOP_SOURCES, DESKTOP_SOURCES_RESULT, + VIEW_FINISHED_RESIZING, } from 'common/communication'; const UNREAD_COUNT_INTERVAL = 1000; @@ -292,3 +293,6 @@ ipcRenderer.on(DESKTOP_SOURCES_RESULT, (event, sources) => { /* eslint-enable no-magic-numbers */ +window.addEventListener('resize', () => { + ipcRenderer.send(VIEW_FINISHED_RESIZING); +}); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index d387223c..a09647a6 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -40,7 +40,7 @@ import WebContentsEventManager from './webContentEvents'; const URL_VIEW_DURATION = 10 * SECOND; const URL_VIEW_HEIGHT = 20; -enum LoadingScreenState { +export enum LoadingScreenState { VISIBLE = 1, FADING = 2, HIDDEN = 3, diff --git a/src/main/windows/windowManager.test.js b/src/main/windows/windowManager.test.js index c75fb42c..075136e7 100644 --- a/src/main/windows/windowManager.test.js +++ b/src/main/windows/windowManager.test.js @@ -58,6 +58,9 @@ jest.mock('../utils', () => ({ })); jest.mock('../views/viewManager', () => ({ ViewManager: jest.fn(), + LoadingScreenState: { + HIDDEN: 3, + }, })); jest.mock('../CriticalErrorHandler', () => jest.fn()); jest.mock('../views/teamDropdownView', () => jest.fn()); @@ -203,6 +206,7 @@ describe('main/windows/windowManager', () => { }); afterEach(() => { + jest.runAllTimers(); jest.resetAllMocks(); }); @@ -212,33 +216,71 @@ describe('main/windows/windowManager', () => { expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); }); - it('should use getContentBounds when the platform is not linux', () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { - value: 'win32', - }); - windowManager.handleResizeMainWindow(); - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); - expect(view.setBounds).toHaveBeenCalledWith({width: 800, height: 600}); - }); - it('should use getSize when the platform is linux', () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { - value: 'linux', - }); windowManager.handleResizeMainWindow(); - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); expect(view.setBounds).not.toHaveBeenCalled(); jest.runAllTimers(); expect(view.setBounds).toHaveBeenCalledWith({width: 1000, height: 900}); }); }); + describe('handleWillResizeMainWindow', () => { + const windowManager = new WindowManager(); + const view = { + setBounds: jest.fn(), + tab: { + url: 'http://server-1.com', + }, + view: { + webContents: { + getURL: jest.fn(), + }, + }, + }; + windowManager.viewManager = { + getCurrentView: () => view, + setLoadingScreenBounds: jest.fn(), + loadingScreenState: 3, + }; + windowManager.mainWindow = { + getContentBounds: () => ({width: 800, height: 600}), + getSize: () => [1000, 900], + }; + windowManager.teamDropdown = { + updateWindowBounds: jest.fn(), + }; + + beforeEach(() => { + getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); + }); + + afterEach(() => { + windowManager.isResizing = false; + jest.resetAllMocks(); + }); + + 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(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); + }); + + it('should not resize if the app is already resizing', () => { + windowManager.isResizing = true; + const event = {preventDefault: jest.fn()}; + windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); + expect(view.setBounds).not.toHaveBeenCalled(); + }); + + it('should use provided bounds', () => { + const event = {preventDefault: jest.fn()}; + windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); + expect(windowManager.isResizing).toBe(true); + expect(view.setBounds).toHaveBeenCalledWith({width: 800, height: 600}); + }); + }); + describe('restoreMain', () => { const windowManager = new WindowManager(); windowManager.mainWindow = { diff --git a/src/main/windows/windowManager.ts b/src/main/windows/windowManager.ts index f551f06b..ffc28149 100644 --- a/src/main/windows/windowManager.ts +++ b/src/main/windows/windowManager.ts @@ -26,15 +26,18 @@ import { DISPATCH_GET_DESKTOP_SOURCES, DESKTOP_SOURCES_RESULT, RELOAD_CURRENT_VIEW, + VIEW_FINISHED_RESIZING, } from 'common/communication'; import urlUtils from 'common/utils/url'; import {SECOND} from 'common/utils/constants'; import Config from 'common/config'; import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView'; +import {MattermostView} from 'main/views/MattermostView'; + import {getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; -import {ViewManager} from '../views/viewManager'; +import {ViewManager, LoadingScreenState} from '../views/viewManager'; import CriticalErrorHandler from '../CriticalErrorHandler'; import TeamDropdownView from '../views/teamDropdownView'; @@ -71,6 +74,7 @@ export class WindowManager { ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId); ipcMain.on(DISPATCH_GET_DESKTOP_SOURCES, this.handleGetDesktopSources); ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); + ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); } handleUpdateConfig = () => { @@ -134,7 +138,12 @@ export class WindowManager { }); this.mainWindow.on('maximize', this.handleMaximizeMainWindow); this.mainWindow.on('unmaximize', this.handleUnmaximizeMainWindow); - this.mainWindow.on('resize', this.handleResizeMainWindow); + if (process.platform === 'linux') { + this.mainWindow.on('resize', this.handleResizeMainWindow); + } else { + 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')); @@ -173,43 +182,73 @@ export class WindowManager { this.sendToRenderer(MAXIMIZE_CHANGE, false); } - handleResizeMainWindow = () => { - log.debug('WindowManager.handleResizeMainWindow'); + isResizing = false; + + handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => { + log.silly('WindowManager.handleWillResizeMainWindow'); if (!(this.viewManager && this.mainWindow)) { return; } - const currentView = this.viewManager.getCurrentView(); - let bounds: Partial; - // 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/28106 - if (process.platform === 'linux') { - const size = this.mainWindow.getSize(); - bounds = {width: size[0], height: size[1]}; - } else { - bounds = this.mainWindow.getContentBounds(); + if (this.isResizing && this.viewManager.loadingScreenState === LoadingScreenState.HIDDEN && this.viewManager.getCurrentView()) { + log.silly('prevented resize'); + event.preventDefault(); + return; } - const setBoundsFunction = () => { - if (currentView) { - currentView.setBounds(getAdjustedWindowBoundaries(bounds.width!, bounds.height!, shouldHaveBackBar(currentView.tab.url, currentView.view.webContents.getURL()))); - } - }; + this.throttledWillResize(newBounds); + this.viewManager?.setLoadingScreenBounds(); + this.teamDropdown?.updateWindowBounds(); + ipcMain.emit(RESIZE_MODAL, null, newBounds); + } - // Another workaround since the window doesn't update properly under Linux for some reason + handleResizedMainWindow = () => { + if (this.mainWindow) { + this.throttledWillResize(this.mainWindow?.getContentBounds()); + } + this.isResizing = false; + } + + handleViewFinishedResizing = () => { + this.isResizing = false; + } + + private throttledWillResize = (newBounds: Electron.Rectangle) => { + this.isResizing = true; + this.setCurrentViewBounds(newBounds); + } + + handleResizeMainWindow = () => { + log.silly('WindowManager.handleResizeMainWindow'); + + if (!(this.viewManager && this.mainWindow)) { + return; + } + const size = this.mainWindow.getSize(); + const bounds = {width: size[0], height: size[1]}; + + // Another workaround since the window doesn't update p roperly under Linux for some reason // See above comment - if (process.platform === 'linux') { - setTimeout(setBoundsFunction, 10); - } else { - setBoundsFunction(); - } + setTimeout(this.setCurrentViewBounds, 10, bounds); this.viewManager.setLoadingScreenBounds(); this.teamDropdown?.updateWindowBounds(); ipcMain.emit(RESIZE_MODAL, null, bounds); + }; + + setCurrentViewBounds = (bounds: {width: number; height: number}) => { + const currentView = this.viewManager?.getCurrentView(); + if (currentView) { + const adjustedBounds = getAdjustedWindowBoundaries(bounds.width, bounds.height, shouldHaveBackBar(currentView.tab.url, currentView.view.webContents.getURL())); + this.setBoundsFunction(currentView, adjustedBounds); + } } + private setBoundsFunction = (currentView: MattermostView, bounds: Electron.Rectangle) => { + log.silly('setBoundsFunction', bounds.width, bounds.height); + currentView.setBounds(bounds); + }; + // 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: any[]) => { if (!this.mainWindow || !this.mainWindowReady) {