diff --git a/e2e/specs/menu_bar/file_menu.test.js b/e2e/specs/menu_bar/file_menu.test.js index d7c2af39..571f0af4 100644 --- a/e2e/specs/menu_bar/file_menu.test.js +++ b/e2e/specs/menu_bar/file_menu.test.js @@ -60,21 +60,12 @@ describe('file_menu/dropdown', function desc() { }); } - it('MM-T804 Preferences in Menu Bar open the Settings page', async () => { - const mainWindow = this.app.windows().find((window) => window.url().includes('index')); - mainWindow.should.not.be.null; - robot.keyTap(',', [env.cmdOrCtrl]); - const settingsWindow = await this.app.waitForEvent('window', { - predicate: (window) => window.url().includes('settings'), - }); - settingsWindow.should.not.be.null; - - if (process.platform !== 'darwin') { - robot.keyTap('w', [env.cmdOrCtrl]); - + if (process.platform !== 'darwin') { + it('MM-T804 Preferences in Menu Bar open the Settings page', async () => { //Opening the menu bar - robot.keyTap('alt'); - robot.keyTap('enter'); + const mainWindow = this.app.windows().find((window) => window.url().includes('index')); + mainWindow.should.not.be.null; + await mainWindow.click('button.three-dot-menu'); robot.keyTap('f'); robot.keyTap('s'); robot.keyTap('enter'); @@ -82,8 +73,8 @@ describe('file_menu/dropdown', function desc() { predicate: (window) => window.url().includes('settings'), }); settingsWindowFromMenu.should.not.be.null; - } - }); + }); + } // TODO: Causes issues on Windows so skipping for Windows if (process.platform !== 'win32') { diff --git a/e2e/specs/server_management/add_server_modal.test.js b/e2e/specs/server_management/add_server_modal.test.js index b9a6393a..11b74fba 100644 --- a/e2e/specs/server_management/add_server_modal.test.js +++ b/e2e/specs/server_management/add_server_modal.test.js @@ -139,6 +139,7 @@ describe('Add Server Modal', function desc() { name: 'TestTeam', url: 'http://example.org/', order: 2, + lastActiveTab: 0, tabs: [ { name: 'TAB_MESSAGING', diff --git a/e2e/specs/server_management/configure_server_modal.test.js b/e2e/specs/server_management/configure_server_modal.test.js index 80fdb584..d48f0cf2 100644 --- a/e2e/specs/server_management/configure_server_modal.test.js +++ b/e2e/specs/server_management/configure_server_modal.test.js @@ -86,6 +86,7 @@ describe('Configure Server Modal', function desc() { url: 'http://example.org/', name: 'TestTeam', order: 0, + lastActiveTab: 0, tabs: [ { name: 'TAB_MESSAGING', diff --git a/src/common/communication.ts b/src/common/communication.ts index 7beb2bc6..26c76139 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -167,3 +167,6 @@ export const GET_ORDERED_TABS_FOR_SERVER = 'get-ordered-tabs-for-server'; export const UPDATE_APPSTATE = 'update-appstate'; export const UPDATE_APPSTATE_TOTALS = 'update-appstate-totals'; export const UPDATE_APPSTATE_FOR_VIEW_ID = 'update-appstate-for-view-id'; + +export const MAIN_WINDOW_CREATED = 'main-window-created'; +export const MAIN_WINDOW_RESIZED = 'main-window-resized'; diff --git a/src/main/app/app.test.js b/src/main/app/app.test.js index 350d2bb9..cfa89ed9 100644 --- a/src/main/app/app.test.js +++ b/src/main/app/app.test.js @@ -38,19 +38,14 @@ jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); jest.mock('main/tray/tray', () => ({})); -jest.mock('main/windows/windowManager', () => ({ - showMainWindow: jest.fn(), -})); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), + show: jest.fn(), })); jest.mock('main/views/viewManager', () => ({ getView: jest.fn(), getViewByWebContentsId: jest.fn(), })); -jest.mock('main/windows/mainWindow', () => ({ - get: jest.fn(), -})); describe('main/app/app', () => { describe('handleAppWillFinishLaunching', () => { diff --git a/src/main/app/app.ts b/src/main/app/app.ts index 5b0e36a5..88b4ea44 100644 --- a/src/main/app/app.ts +++ b/src/main/app/app.ts @@ -10,7 +10,6 @@ import updateManager from 'main/autoUpdater'; import CertificateStore from 'main/certificateStore'; import {localizeMessage} from 'main/i18nManager'; import {destroyTray} from 'main/tray/tray'; -import WindowManager from 'main/windows/windowManager'; import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; @@ -30,8 +29,10 @@ export function handleAppSecondInstance(event: Event, argv: string[]) { // Protocol handler for win32 // argv: An array of the second instance’s (command line / deep linked) arguments - const deeplinkingUrl = getDeeplinkingURL(argv); - WindowManager.showMainWindow(deeplinkingUrl); + const deeplinkingURL = getDeeplinkingURL(argv); + if (deeplinkingURL) { + openDeepLink(deeplinkingURL); + } } export function handleAppWindowAllClosed() { diff --git a/src/main/app/config.test.js b/src/main/app/config.test.js index 557430a5..17d7cc72 100644 --- a/src/main/app/config.test.js +++ b/src/main/app/config.test.js @@ -10,7 +10,7 @@ import {setLoggingLevel} from 'common/log'; import {handleConfigUpdate} from 'main/app/config'; import {handleMainWindowIsShown} from 'main/app/intercom'; -import WindowManager from 'main/windows/windowManager'; +import MainWindow from 'main/windows/mainWindow'; import AutoLauncher from 'main/AutoLauncher'; jest.mock('electron', () => ({ @@ -47,8 +47,7 @@ jest.mock('main/views/viewManager', () => ({ reloadConfiguration: jest.fn(), })); jest.mock('main/views/loadingScreen', () => ({})); -jest.mock('main/windows/windowManager', () => ({ - handleUpdateConfig: jest.fn(), +jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), })); @@ -65,11 +64,11 @@ describe('main/app/config', () => { it('should reload renderer config only when app is ready', () => { handleConfigUpdate({}); - expect(WindowManager.sendToRenderer).not.toBeCalled(); + expect(MainWindow.sendToRenderer).not.toBeCalled(); app.isReady.mockReturnValue(true); handleConfigUpdate({}); - expect(WindowManager.sendToRenderer).toBeCalledWith(RELOAD_CONFIGURATION); + expect(MainWindow.sendToRenderer).toBeCalledWith(RELOAD_CONFIGURATION); }); it('should set download path if applicable', () => { diff --git a/src/main/app/config.ts b/src/main/app/config.ts index 887d737a..3e85663c 100644 --- a/src/main/app/config.ts +++ b/src/main/app/config.ts @@ -13,7 +13,8 @@ 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 MainWindow from 'main/windows/mainWindow'; +import SettingsWindow from 'main/windows/settingsWindow'; import {handleMainWindowIsShown} from './intercom'; import {handleUpdateMenuEvent, updateSpellCheckerLocales} from './utils'; @@ -72,7 +73,8 @@ export function handleConfigUpdate(newConfig: CombinedConfig) { } if (app.isReady()) { - WindowManager.sendToRenderer(RELOAD_CONFIGURATION); + MainWindow.sendToRenderer(RELOAD_CONFIGURATION); + SettingsWindow.sendToRenderer(RELOAD_CONFIGURATION); } setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge); @@ -111,7 +113,8 @@ export function handleDarkModeChange(darkMode: boolean) { log.debug('handleDarkModeChange', darkMode); refreshTrayImages(Config.trayIconTheme); - WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode); + MainWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode); + SettingsWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode); LoadingScreen.setDarkMode(darkMode); ipcMain.emit(EMIT_CONFIGURATION, true, Config.data); diff --git a/src/main/app/index.ts b/src/main/app/index.ts index 62e7335d..a83b1593 100644 --- a/src/main/app/index.ts +++ b/src/main/app/index.ts @@ -5,6 +5,11 @@ import {initialize} from './initialize'; +// TODO: Singletons, we need DI :D +import('main/views/teamDropdownView'); +import('main/views/downloadsDropdownMenuView'); +import('main/views/downloadsDropdownView'); + if (process.env.NODE_ENV !== 'production' && module.hot) { module.hot.accept(); } diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index 4a8bbfa0..eda4c398 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -9,8 +9,8 @@ import Config from 'common/config'; import urlUtils from 'common/utils/url'; import parseArgs from 'main/ParseArgs'; +import ViewManager from 'main/views/viewManager'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; import {initialize} from './initialize'; import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils'; @@ -119,6 +119,7 @@ jest.mock('main/app/config', () => ({ jest.mock('main/app/intercom', () => ({ handleMainWindowIsShown: jest.fn(), })); +jest.mock('main/app/servers', () => ({})); jest.mock('main/app/utils', () => ({ clearAppCache: jest.fn(), getDeeplinkingURL: jest.fn(), @@ -168,18 +169,17 @@ jest.mock('main/UserActivityMonitor', () => ({ jest.mock('main/windows/callsWidgetWindow', () => ({ isCallsWidget: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ - showMainWindow: jest.fn(), - sendToRenderer: jest.fn(), - getServerNameByWebContentsId: jest.fn(), - getServerURLFromWebContentsId: jest.fn(), +jest.mock('main/views/viewManager', () => ({ + getViewByWebContentsId: jest.fn(), + handleDeepLink: jest.fn(), })); -jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/settingsWindow', () => ({ show: jest.fn(), })); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), + show: jest.fn(), + sendToRenderer: jest.fn(), })); const originalProcess = process; describe('main/app/initialize', () => { @@ -272,11 +272,17 @@ describe('main/app/initialize', () => { value: originalPlatform, }); - expect(WindowManager.showMainWindow).toHaveBeenCalledWith('mattermost://server-1.com'); + expect(ViewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com'); }); it('should allow permission requests for supported types from trusted URLs', async () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://server-1.com')); + ViewManager.getViewByWebContentsId.mockReturnValue({ + tab: { + server: { + url: new URL('http://server-1.com'), + }, + }, + }); let callback = jest.fn(); session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => { cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback); diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index f96c1243..1b2c8aab 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -36,6 +36,12 @@ import { GET_ORDERED_SERVERS, GET_ORDERED_TABS_FOR_SERVER, SERVERS_URL_MODIFIED, + GET_DARK_MODE, + WINDOW_CLOSE, + WINDOW_MAXIMIZE, + WINDOW_MINIMIZE, + WINDOW_RESTORE, + DOUBLE_CLICK_ON_WINDOW, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -59,7 +65,6 @@ import {refreshTrayImages, setupTray} from 'main/tray/tray'; import UserActivityMonitor from 'main/UserActivityMonitor'; import ViewManager from 'main/views/viewManager'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; -import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; import {protocols} from '../../../electron-builder.json'; @@ -84,22 +89,21 @@ import { import { handleMainWindowIsShown, handleAppVersion, - handleCloseTab, - handleEditServerModal, handleMentionNotification, - handleNewServerModal, handleOpenAppMenu, - handleOpenTab, handleQuit, - handleRemoveServerModal, handleSelectDownload, - handleSwitchServer, - handleSwitchTab, handlePingDomain, - handleGetOrderedServers, - handleGetOrderedTabsForServer, - handleGetLastActive, } from './intercom'; +import { + handleEditServerModal, + handleNewServerModal, + handleRemoveServerModal, + switchServer, +} from './servers'; +import { + handleCloseTab, handleGetLastActive, handleGetOrderedTabsForServer, handleOpenTab, +} from './tabs'; import { clearAppCache, getDeeplinkingURL, @@ -111,6 +115,14 @@ import { migrateMacAppStore, updateServerInfos, } from './utils'; +import { + handleClose, + handleDoubleClick, + handleGetDarkMode, + handleMaximize, + handleMinimize, + handleRestore, +} from './windows'; export const mainProtocol = protocols?.[0]?.schemes?.[0]; @@ -203,7 +215,7 @@ function initializeAppEventListeners() { app.on('second-instance', handleAppSecondInstance); app.on('window-all-closed', handleAppWindowAllClosed); app.on('browser-window-created', handleAppBrowserWindowCreated); - app.on('activate', () => WindowManager.showMainWindow()); + app.on('activate', () => MainWindow.show()); app.on('before-quit', handleAppBeforeQuit); app.on('certificate-error', handleAppCertificateError); app.on('select-client-certificate', CertificateManager.handleSelectCertificate); @@ -267,8 +279,8 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu); } - ipcMain.on(SWITCH_SERVER, handleSwitchServer); - ipcMain.on(SWITCH_TAB, handleSwitchTab); + ipcMain.on(SWITCH_SERVER, (event, serverId) => switchServer(serverId)); + ipcMain.on(SWITCH_TAB, (event, viewId) => ViewManager.showById(viewId)); ipcMain.on(CLOSE_TAB, handleCloseTab); ipcMain.on(OPEN_TAB, handleOpenTab); @@ -289,8 +301,15 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(UPDATE_SERVER_ORDER, (event, serverOrder) => ServerManager.updateServerOrder(serverOrder)); ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, tabOrder) => ServerManager.updateTabOrder(serverId, tabOrder)); ipcMain.handle(GET_LAST_ACTIVE, handleGetLastActive); - ipcMain.handle(GET_ORDERED_SERVERS, handleGetOrderedServers); + ipcMain.handle(GET_ORDERED_SERVERS, () => ServerManager.getOrderedServers().map((srv) => srv.toMattermostTeam())); ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, handleGetOrderedTabsForServer); + + ipcMain.handle(GET_DARK_MODE, handleGetDarkMode); + ipcMain.on(WINDOW_CLOSE, handleClose); + ipcMain.on(WINDOW_MAXIMIZE, handleMaximize); + ipcMain.on(WINDOW_MINIMIZE, handleMinimize); + ipcMain.on(WINDOW_RESTORE, handleRestore); + ipcMain.on(DOUBLE_CLICK_ON_WINDOW, handleDoubleClick); } async function initializeAfterAppReady() { @@ -364,6 +383,9 @@ async function initializeAfterAppReady() { catch((err) => log.error('An error occurred: ', err)); } + initCookieManager(defaultSession); + MainWindow.show(); + let deeplinkingURL; // Protocol handler for win32 @@ -371,13 +393,12 @@ async function initializeAfterAppReady() { const args = process.argv.slice(1); if (Array.isArray(args) && args.length > 0) { deeplinkingURL = getDeeplinkingURL(args); + if (deeplinkingURL) { + ViewManager.handleDeepLink(deeplinkingURL); + } } } - initCookieManager(defaultSession); - - WindowManager.showMainWindow(deeplinkingURL); - // listen for status updates and pass on to renderer UserActivityMonitor.on('status', (status) => { log.debug('UserActivityMonitor.on(status)', status); @@ -440,7 +461,7 @@ async function initializeAfterAppReady() { } const requestingURL = webContents.getURL(); - const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id); + const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url; if (!serverURL) { callback(false); diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index 7f14528a..5d27f131 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -1,20 +1,12 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; - import {getLocalURLString, getLocalPreload} from 'main/utils'; import ServerManager from 'common/servers/serverManager'; import MainWindow from 'main/windows/mainWindow'; import ModalManager from 'main/views/modalManager'; -import WindowManager from 'main/windows/windowManager'; import { - handleOpenTab, - handleCloseTab, - handleNewServerModal, - handleEditServerModal, - handleRemoveServerModal, handleWelcomeScreenModal, handleMainWindowIsShown, } from './intercom'; @@ -22,9 +14,6 @@ import { jest.mock('common/config', () => ({ setServers: jest.fn(), })); -jest.mock('common/tabs/TabView', () => ({ - getDefaultConfigTeamFromTeam: jest.fn(), -})); jest.mock('main/notifications', () => ({})); jest.mock('common/servers/serverManager', () => ({ setTabIsOpen: jest.fn(), @@ -45,224 +34,13 @@ jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/modalManager', () => ({ addModal: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ - switchServer: jest.fn(), - switchTab: jest.fn(), -})); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), })); jest.mock('./app', () => ({})); -const tabs = [ - { - name: 'tab-1', - order: 0, - isOpen: false, - }, - { - name: 'tab-2', - order: 2, - isOpen: true, - }, - { - name: 'tab-3', - order: 1, - isOpen: true, - }, -]; -const teams = [ - { - id: 'server-1', - name: 'server-1', - url: 'http://server-1.com', - tabs, - }, -]; - describe('main/app/intercom', () => { - describe('handleCloseTab', () => { - it('should close the specified tab and switch to the next open tab', () => { - ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}}); - ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'}); - handleCloseTab(null, 'tab-3'); - expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false); - expect(WindowManager.switchTab).toBeCalledWith('tab-2'); - }); - }); - - describe('handleOpenTab', () => { - it('should open the specified tab', () => { - handleOpenTab(null, 'tab-1'); - expect(WindowManager.switchTab).toBeCalledWith('tab-1'); - }); - }); - - describe('handleNewServerModal', () => { - let teamsCopy; - - beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); - getLocalPreload.mockReturnValue('/some/preload.js'); - MainWindow.get.mockReturnValue({}); - - teamsCopy = JSON.parse(JSON.stringify(teams)); - ServerManager.getAllServers.mockReturnValue([]); - ServerManager.addServer.mockImplementation(() => { - const newTeam = { - id: 'server-1', - name: 'new-team', - url: 'http://new-team.com', - tabs, - }; - teamsCopy = [ - ...teamsCopy, - newTeam, - ]; - return newTeam; - }); - ServerManager.hasServers.mockReturnValue(Boolean(teamsCopy.length)); - - getDefaultConfigTeamFromTeam.mockImplementation((team) => ({ - ...team, - tabs, - })); - }); - - it('should add new team to the config', async () => { - const promise = Promise.resolve({ - name: 'new-team', - url: 'http://new-team.com', - }); - ModalManager.addModal.mockReturnValue(promise); - - handleNewServerModal(); - await promise; - expect(teamsCopy).toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'new-team', - url: 'http://new-team.com', - tabs, - })); - expect(WindowManager.switchServer).toBeCalledWith('server-1', true); - }); - }); - - describe('handleEditServerModal', () => { - let teamsCopy; - - beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); - getLocalPreload.mockReturnValue('/some/preload.js'); - MainWindow.get.mockReturnValue({}); - - teamsCopy = JSON.parse(JSON.stringify(teams)); - ServerManager.getServer.mockImplementation((id) => { - if (id !== teamsCopy[0].id) { - return undefined; - } - return {...teamsCopy[0], toMattermostTeam: jest.fn()}; - }); - ServerManager.editServer.mockImplementation((id, team) => { - if (id !== teamsCopy[0].id) { - return; - } - const newTeam = { - ...teamsCopy[0], - ...team, - }; - teamsCopy = [newTeam]; - }); - ServerManager.getAllServers.mockReturnValue(teamsCopy.map((team) => ({...team, toMattermostTeam: jest.fn()}))); - }); - - it('should do nothing when the server cannot be found', () => { - handleEditServerModal(null, 'bad-server'); - expect(ModalManager.addModal).not.toBeCalled(); - }); - - it('should edit the existing team', async () => { - const promise = Promise.resolve({ - name: 'new-team', - url: 'http://new-team.com', - }); - ModalManager.addModal.mockReturnValue(promise); - - handleEditServerModal(null, 'server-1'); - await promise; - expect(teamsCopy).not.toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'server-1', - url: 'http://server-1.com', - tabs, - })); - expect(teamsCopy).toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'new-team', - url: 'http://new-team.com', - tabs, - })); - }); - }); - - describe('handleRemoveServerModal', () => { - let teamsCopy; - - beforeEach(() => { - getLocalURLString.mockReturnValue('/some/index.html'); - getLocalPreload.mockReturnValue('/some/preload.js'); - MainWindow.get.mockReturnValue({}); - - teamsCopy = JSON.parse(JSON.stringify(teams)); - ServerManager.getServer.mockImplementation((id) => { - if (id !== teamsCopy[0].id) { - return undefined; - } - return teamsCopy[0]; - }); - ServerManager.removeServer.mockImplementation(() => { - teamsCopy = []; - }); - ServerManager.getAllServers.mockReturnValue(teamsCopy); - }); - - it('should remove the existing team', async () => { - const promise = Promise.resolve(true); - ModalManager.addModal.mockReturnValue(promise); - - handleRemoveServerModal(null, 'server-1'); - await promise; - expect(teamsCopy).not.toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'server-1', - url: 'http://server-1.com', - tabs, - })); - }); - - it('should not remove the existing team when clicking Cancel', async () => { - const promise = Promise.resolve(false); - ModalManager.addModal.mockReturnValue(promise); - - expect(teamsCopy).toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'server-1', - url: 'http://server-1.com', - tabs, - })); - - handleRemoveServerModal(null, 'server-1'); - await promise; - expect(teamsCopy).toContainEqual(expect.objectContaining({ - id: 'server-1', - name: 'server-1', - url: 'http://server-1.com', - tabs, - })); - }); - }); - describe('handleWelcomeScreenModal', () => { beforeEach(() => { getLocalURLString.mockReturnValue('/some/index.html'); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 281ed188..7f4cab6a 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -3,7 +3,7 @@ import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; -import {Team, MattermostTeam} from 'types/config'; +import {MattermostTeam} from 'types/config'; import {MentionData} from 'types/notification'; import Config from 'common/config'; @@ -14,10 +14,10 @@ import {displayMention} from 'main/notifications'; import {getLocalPreload, getLocalURLString} from 'main/utils'; import ServerManager from 'common/servers/serverManager'; import ModalManager from 'main/views/modalManager'; -import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; import {handleAppBeforeQuit} from './app'; +import {handleNewServerModal, switchServer} from './servers'; const log = new Logger('App.Intercom'); @@ -35,49 +35,6 @@ export function handleQuit(e: IpcMainEvent, reason: string, stack: string) { app.quit(); } -export function handleSwitchServer(event: IpcMainEvent, serverId: string) { - log.silly('handleSwitchServer', serverId); - WindowManager.switchServer(serverId); -} - -export function handleSwitchTab(event: IpcMainEvent, tabId: string) { - log.silly('handleSwitchTab', {tabId}); - WindowManager.switchTab(tabId); -} - -export function handleCloseTab(event: IpcMainEvent, tabId: string) { - log.debug('handleCloseTab', {tabId}); - - const tab = ServerManager.getTab(tabId); - if (!tab) { - return; - } - ServerManager.setTabIsOpen(tabId, false); - const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id); - WindowManager.switchTab(nextTab.id); -} - -export function handleOpenTab(event: IpcMainEvent, tabId: string) { - log.debug('handleOpenTab', {tabId}); - - ServerManager.setTabIsOpen(tabId, true); - WindowManager.switchTab(tabId); -} - -export function handleGetOrderedServers() { - return ServerManager.getOrderedServers().map((srv) => srv.toMattermostTeam()); -} - -export function handleGetOrderedTabsForServer(event: IpcMainInvokeEvent, serverId: string) { - return ServerManager.getOrderedTabsForServer(serverId).map((tab) => tab.toMattermostTab()); -} - -export function handleGetLastActive() { - const server = ServerManager.getCurrentServer(); - const tab = ServerManager.getLastActiveTabForServer(server.id); - return {server: server.id, tab: tab.id}; -} - function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) { log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible}); @@ -126,101 +83,6 @@ export function handleMainWindowIsShown() { } } -export function handleNewServerModal() { - log.debug('handleNewServerModal'); - - const html = getLocalURLString('newServer.html'); - - const preload = getLocalPreload('desktopAPI.js'); - - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - const modalPromise = ModalManager.addModal('newServer', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers()); - if (modalPromise) { - modalPromise.then((data) => { - const newTeam = ServerManager.addServer(data); - WindowManager.switchServer(newTeam.id, true); - }).catch((e) => { - // e is undefined for user cancellation - if (e) { - log.error(`there was an error in the new server modal: ${e}`); - } - }); - } else { - log.warn('There is already a new server modal'); - } -} - -export function handleEditServerModal(e: IpcMainEvent, id: string) { - log.debug('handleEditServerModal', id); - - const html = getLocalURLString('editServer.html'); - - const preload = getLocalPreload('desktopAPI.js'); - - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - const server = ServerManager.getServer(id); - if (!server) { - return; - } - const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>( - 'editServer', - html, - preload, - { - currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()), - team: server.toMattermostTeam(), - }, - mainWindow); - if (modalPromise) { - modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => { - // e is undefined for user cancellation - if (e) { - log.error(`there was an error in the edit server modal: ${e}`); - } - }); - } else { - log.warn('There is already an edit server modal'); - } -} - -export function handleRemoveServerModal(e: IpcMainEvent, id: string) { - log.debug('handleRemoveServerModal', id); - - const html = getLocalURLString('removeServer.html'); - - const preload = getLocalPreload('desktopAPI.js'); - - const server = ServerManager.getServer(id); - if (!server) { - return; - } - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - const modalPromise = ModalManager.addModal('removeServer', html, preload, server.name, mainWindow); - if (modalPromise) { - modalPromise.then((remove) => { - if (remove) { - ServerManager.removeServer(server.id); - } - }).catch((e) => { - // e is undefined for user cancellation - if (e) { - log.error(`there was an error in the edit server modal: ${e}`); - } - }); - } else { - log.warn('There is already an edit server modal'); - } -} - export function handleWelcomeScreenModal() { log.debug('handleWelcomeScreenModal'); @@ -236,7 +98,7 @@ export function handleWelcomeScreenModal() { if (modalPromise) { modalPromise.then((data) => { const newTeam = ServerManager.addServer(data); - WindowManager.switchServer(newTeam.id, true); + switchServer(newTeam.id, true); }).catch((e) => { // e is undefined for user cancellation if (e) { diff --git a/src/main/app/servers.test.js b/src/main/app/servers.test.js new file mode 100644 index 00000000..01f8cf64 --- /dev/null +++ b/src/main/app/servers.test.js @@ -0,0 +1,318 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import ServerManager from 'common/servers/serverManager'; +import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; + +import ModalManager from 'main/views/modalManager'; +import {getLocalURLString, getLocalPreload} from 'main/utils'; +import MainWindow from 'main/windows/mainWindow'; +import ViewManager from 'main/views/viewManager'; + +import * as Servers from './servers'; + +jest.mock('electron', () => ({ + ipcMain: { + emit: jest.fn(), + }, +})); + +jest.mock('common/servers/serverManager', () => ({ + setTabIsOpen: jest.fn(), + getAllServers: jest.fn(), + hasServers: jest.fn(), + addServer: jest.fn(), + editServer: jest.fn(), + removeServer: jest.fn(), + getServer: jest.fn(), + getTab: jest.fn(), + getLastActiveTabForServer: jest.fn(), + getServerLog: jest.fn(), +})); +jest.mock('common/tabs/TabView', () => ({ + getDefaultConfigTeamFromTeam: jest.fn(), +})); +jest.mock('main/views/modalManager', () => ({ + addModal: jest.fn(), +})); +jest.mock('main/utils', () => ({ + getLocalPreload: jest.fn(), + getLocalURLString: jest.fn(), +})); +jest.mock('main/windows/mainWindow', () => ({ + get: jest.fn(), + show: jest.fn(), +})); +jest.mock('main/views/viewManager', () => ({ + getView: jest.fn(), + showById: jest.fn(), +})); + +const tabs = [ + { + name: 'tab-1', + order: 0, + isOpen: false, + }, + { + name: 'tab-2', + order: 2, + isOpen: true, + }, + { + name: 'tab-3', + order: 1, + isOpen: true, + }, +]; +const teams = [ + { + id: 'server-1', + name: 'server-1', + url: 'http://server-1.com', + tabs, + }, +]; + +describe('main/app/servers', () => { + describe('switchServer', () => { + const views = new Map([ + ['tab-1', {id: 'tab-1'}], + ['tab-2', {id: 'tab-2'}], + ['tab-3', {id: 'tab-3'}], + ]); + + beforeEach(() => { + jest.useFakeTimers(); + const server1 = { + id: 'server-1', + }; + const server2 = { + id: 'server-2', + }; + ServerManager.getServer.mockImplementation((name) => { + switch (name) { + case 'server-1': + return server1; + case 'server-2': + return server2; + default: + return undefined; + } + }); + ServerManager.getServerLog.mockReturnValue({debug: jest.fn(), error: jest.fn()}); + ViewManager.getView.mockImplementation((viewId) => views.get(viewId)); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + afterAll(() => { + jest.runOnlyPendingTimers(); + jest.clearAllTimers(); + jest.useRealTimers(); + }); + + it('should do nothing if cannot find the server', () => { + Servers.switchServer('server-3'); + expect(ViewManager.showById).not.toBeCalled(); + }); + + it('should show first open tab in order when last active not defined', () => { + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); + Servers.switchServer('server-1'); + expect(ViewManager.showById).toHaveBeenCalledWith('tab-3'); + }); + + it('should show last active tab of chosen server', () => { + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'}); + Servers.switchServer('server-2'); + expect(ViewManager.showById).toHaveBeenCalledWith('tab-2'); + }); + + it('should wait for view to exist if specified', () => { + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); + views.delete('tab-3'); + Servers.switchServer('server-1', true); + expect(ViewManager.showById).not.toBeCalled(); + + jest.advanceTimersByTime(200); + expect(ViewManager.showById).not.toBeCalled(); + + views.set('tab-3', {}); + jest.advanceTimersByTime(200); + expect(ViewManager.showById).toBeCalledWith('tab-3'); + }); + }); + + describe('handleNewServerModal', () => { + let teamsCopy; + + beforeEach(() => { + getLocalURLString.mockReturnValue('/some/index.html'); + getLocalPreload.mockReturnValue('/some/preload.js'); + MainWindow.get.mockReturnValue({}); + + teamsCopy = JSON.parse(JSON.stringify(teams)); + ServerManager.getAllServers.mockReturnValue([]); + ServerManager.addServer.mockImplementation(() => { + const newTeam = { + id: 'server-1', + name: 'new-team', + url: 'http://new-team.com', + tabs, + }; + teamsCopy = [ + ...teamsCopy, + newTeam, + ]; + return newTeam; + }); + ServerManager.hasServers.mockReturnValue(Boolean(teamsCopy.length)); + ServerManager.getServerLog.mockReturnValue({debug: jest.fn(), error: jest.fn()}); + + getDefaultConfigTeamFromTeam.mockImplementation((team) => ({ + ...team, + tabs, + })); + }); + + it('should add new team to the config', async () => { + const data = { + name: 'new-team', + url: 'http://new-team.com', + }; + const promise = Promise.resolve(data); + ModalManager.addModal.mockReturnValue(promise); + + Servers.handleNewServerModal(); + await promise; + + expect(ServerManager.addServer).toHaveBeenCalledWith(data); + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'new-team', + url: 'http://new-team.com', + tabs, + })); + + // TODO: For some reason jest won't recognize this as being called + //expect(spy).toHaveBeenCalledWith('server-1', true); + }); + }); + + describe('handleEditServerModal', () => { + let teamsCopy; + + beforeEach(() => { + getLocalURLString.mockReturnValue('/some/index.html'); + getLocalPreload.mockReturnValue('/some/preload.js'); + MainWindow.get.mockReturnValue({}); + + teamsCopy = JSON.parse(JSON.stringify(teams)); + ServerManager.getServer.mockImplementation((id) => { + if (id !== teamsCopy[0].id) { + return undefined; + } + return {...teamsCopy[0], toMattermostTeam: jest.fn()}; + }); + ServerManager.editServer.mockImplementation((id, team) => { + if (id !== teamsCopy[0].id) { + return; + } + const newTeam = { + ...teamsCopy[0], + ...team, + }; + teamsCopy = [newTeam]; + }); + ServerManager.getAllServers.mockReturnValue(teamsCopy.map((team) => ({...team, toMattermostTeam: jest.fn()}))); + }); + + it('should do nothing when the server cannot be found', () => { + Servers.handleEditServerModal(null, 'bad-server'); + expect(ModalManager.addModal).not.toBeCalled(); + }); + + it('should edit the existing team', async () => { + const promise = Promise.resolve({ + name: 'new-team', + url: 'http://new-team.com', + }); + ModalManager.addModal.mockReturnValue(promise); + + Servers.handleEditServerModal(null, 'server-1'); + await promise; + expect(teamsCopy).not.toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'server-1', + url: 'http://server-1.com', + tabs, + })); + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'new-team', + url: 'http://new-team.com', + tabs, + })); + }); + }); + + describe('handleRemoveServerModal', () => { + let teamsCopy; + + beforeEach(() => { + getLocalURLString.mockReturnValue('/some/index.html'); + getLocalPreload.mockReturnValue('/some/preload.js'); + MainWindow.get.mockReturnValue({}); + + teamsCopy = JSON.parse(JSON.stringify(teams)); + ServerManager.getServer.mockImplementation((id) => { + if (id !== teamsCopy[0].id) { + return undefined; + } + return teamsCopy[0]; + }); + ServerManager.removeServer.mockImplementation(() => { + teamsCopy = []; + }); + ServerManager.getAllServers.mockReturnValue(teamsCopy); + }); + + it('should remove the existing team', async () => { + const promise = Promise.resolve(true); + ModalManager.addModal.mockReturnValue(promise); + + Servers.handleRemoveServerModal(null, 'server-1'); + await promise; + expect(teamsCopy).not.toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'server-1', + url: 'http://server-1.com', + tabs, + })); + }); + + it('should not remove the existing team when clicking Cancel', async () => { + const promise = Promise.resolve(false); + ModalManager.addModal.mockReturnValue(promise); + + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'server-1', + url: 'http://server-1.com', + tabs, + })); + + Servers.handleRemoveServerModal(null, 'server-1'); + await promise; + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', + name: 'server-1', + url: 'http://server-1.com', + tabs, + })); + }); + }); +}); diff --git a/src/main/app/servers.ts b/src/main/app/servers.ts new file mode 100644 index 00000000..9cbfb2b8 --- /dev/null +++ b/src/main/app/servers.ts @@ -0,0 +1,134 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {IpcMainEvent, ipcMain} from 'electron'; + +import {MattermostTeam, Team} from 'types/config'; + +import {UPDATE_SHORTCUT_MENU} from 'common/communication'; +import {Logger} from 'common/log'; +import ServerManager from 'common/servers/serverManager'; + +import ViewManager from 'main/views/viewManager'; +import ModalManager from 'main/views/modalManager'; +import MainWindow from 'main/windows/mainWindow'; +import {getLocalPreload, getLocalURLString} from 'main/utils'; + +const log = new Logger('App.Servers'); + +export const switchServer = (serverId: string, waitForViewToExist = false) => { + ServerManager.getServerLog(serverId, 'WindowManager').debug('switchServer'); + MainWindow.show(); + const server = ServerManager.getServer(serverId); + if (!server) { + ServerManager.getServerLog(serverId, 'WindowManager').error('Cannot find server in config'); + return; + } + const nextTab = ServerManager.getLastActiveTabForServer(serverId); + if (waitForViewToExist) { + const timeout = setInterval(() => { + if (ViewManager.getView(nextTab.id)) { + ViewManager.showById(nextTab.id); + clearInterval(timeout); + } + }, 100); + } else { + ViewManager.showById(nextTab.id); + } + ipcMain.emit(UPDATE_SHORTCUT_MENU); +}; + +export const handleNewServerModal = () => { + log.debug('handleNewServerModal'); + + const html = getLocalURLString('newServer.html'); + + const preload = getLocalPreload('desktopAPI.js'); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + const modalPromise = ModalManager.addModal('newServer', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers()); + if (modalPromise) { + modalPromise.then((data) => { + const newTeam = ServerManager.addServer(data); + switchServer(newTeam.id, true); + }).catch((e) => { + // e is undefined for user cancellation + if (e) { + log.error(`there was an error in the new server modal: ${e}`); + } + }); + } else { + log.warn('There is already a new server modal'); + } +}; + +export const handleEditServerModal = (e: IpcMainEvent, id: string) => { + log.debug('handleEditServerModal', id); + + const html = getLocalURLString('editServer.html'); + + const preload = getLocalPreload('desktopAPI.js'); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + const server = ServerManager.getServer(id); + if (!server) { + return; + } + const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>( + 'editServer', + html, + preload, + { + currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()), + team: server.toMattermostTeam(), + }, + mainWindow); + if (modalPromise) { + modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => { + // e is undefined for user cancellation + if (e) { + log.error(`there was an error in the edit server modal: ${e}`); + } + }); + } else { + log.warn('There is already an edit server modal'); + } +}; + +export const handleRemoveServerModal = (e: IpcMainEvent, id: string) => { + log.debug('handleRemoveServerModal', id); + + const html = getLocalURLString('removeServer.html'); + + const preload = getLocalPreload('desktopAPI.js'); + + const server = ServerManager.getServer(id); + if (!server) { + return; + } + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + const modalPromise = ModalManager.addModal('removeServer', html, preload, server.name, mainWindow); + if (modalPromise) { + modalPromise.then((remove) => { + if (remove) { + ServerManager.removeServer(server.id); + } + }).catch((e) => { + // e is undefined for user cancellation + if (e) { + log.error(`there was an error in the edit server modal: ${e}`); + } + }); + } else { + log.warn('There is already an edit server modal'); + } +}; diff --git a/src/main/app/tabs.test.js b/src/main/app/tabs.test.js new file mode 100644 index 00000000..4a2a6daa --- /dev/null +++ b/src/main/app/tabs.test.js @@ -0,0 +1,39 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import ServerManager from 'common/servers/serverManager'; +import ViewManager from 'main/views/viewManager'; + +import { + handleCloseTab, + handleOpenTab, +} from './tabs'; + +jest.mock('common/servers/serverManager', () => ({ + setTabIsOpen: jest.fn(), + getTab: jest.fn(), + getLastActiveTabForServer: jest.fn(), +})); + +jest.mock('main/views/viewManager', () => ({ + showById: jest.fn(), +})); + +describe('main/app/tabs', () => { + describe('handleCloseTab', () => { + it('should close the specified tab and switch to the next open tab', () => { + ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}}); + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'}); + handleCloseTab(null, 'tab-3'); + expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false); + expect(ViewManager.showById).toBeCalledWith('tab-2'); + }); + }); + + describe('handleOpenTab', () => { + it('should open the specified tab', () => { + handleOpenTab(null, 'tab-1'); + expect(ViewManager.showById).toBeCalledWith('tab-1'); + }); + }); +}); diff --git a/src/main/app/tabs.ts b/src/main/app/tabs.ts new file mode 100644 index 00000000..2260be6e --- /dev/null +++ b/src/main/app/tabs.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {IpcMainEvent, IpcMainInvokeEvent} from 'electron'; + +import ServerManager from 'common/servers/serverManager'; +import {Logger} from 'common/log'; + +import ViewManager from 'main/views/viewManager'; + +const log = new Logger('App.Tabs'); + +export const handleCloseTab = (event: IpcMainEvent, tabId: string) => { + log.debug('handleCloseTab', {tabId}); + + const tab = ServerManager.getTab(tabId); + if (!tab) { + return; + } + ServerManager.setTabIsOpen(tabId, false); + const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id); + ViewManager.showById(nextTab.id); +}; + +export const handleOpenTab = (event: IpcMainEvent, tabId: string) => { + log.debug('handleOpenTab', {tabId}); + + ServerManager.setTabIsOpen(tabId, true); + ViewManager.showById(tabId); +}; + +export const selectNextTab = () => { + selectTab((order) => order + 1); +}; + +export const selectPreviousTab = () => { + selectTab((order, length) => (length + (order - 1))); +}; + +export const handleGetOrderedTabsForServer = (event: IpcMainInvokeEvent, serverId: string) => { + return ServerManager.getOrderedTabsForServer(serverId).map((tab) => tab.toMattermostTab()); +}; + +export const handleGetLastActive = () => { + const server = ServerManager.getCurrentServer(); + const tab = ServerManager.getLastActiveTabForServer(server.id); + return {server: server.id, tab: tab.id}; +}; + +const selectTab = (fn: (order: number, length: number) => number) => { + const currentView = ViewManager.getCurrentView(); + if (!currentView) { + return; + } + + const currentTeamTabs = ServerManager.getOrderedTabsForServer(currentView.tab.server.id).map((tab, index) => ({tab, index})); + const filteredTabs = currentTeamTabs?.filter((tab) => tab.tab.isOpen); + const currentTab = currentTeamTabs?.find((tab) => tab.tab.type === currentView.tab.type); + if (!currentTeamTabs || !currentTab || !filteredTabs) { + return; + } + + let currentOrder = currentTab.index; + let nextIndex = -1; + while (nextIndex === -1) { + const nextOrder = (fn(currentOrder, currentTeamTabs.length) % currentTeamTabs.length); + nextIndex = filteredTabs.findIndex((tab) => tab.index === nextOrder); + currentOrder = nextOrder; + } + + const newTab = filteredTabs[nextIndex].tab; + ViewManager.showById(newTab.id); +}; diff --git a/src/main/app/utils.test.js b/src/main/app/utils.test.js index f432d4f2..6b431c88 100644 --- a/src/main/app/utils.test.js +++ b/src/main/app/utils.test.js @@ -52,7 +52,6 @@ jest.mock('main/menus/tray', () => ({})); jest.mock('main/tray/tray', () => ({})); jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/mainWindow', () => ({})); -jest.mock('main/windows/windowManager', () => ({})); jest.mock('./initialize', () => ({ mainProtocol: 'mattermost', diff --git a/src/main/app/utils.ts b/src/main/app/utils.ts index 04f0bce0..07dcd38d 100644 --- a/src/main/app/utils.ts +++ b/src/main/app/utils.ts @@ -27,7 +27,6 @@ import {createMenu as createTrayMenu} from 'main/menus/tray'; import {ServerInfo} from 'main/server/serverInfo'; import {setTrayMenu} from 'main/tray/tray'; import ViewManager from 'main/views/viewManager'; -import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; import {mainProtocol} from './initialize'; @@ -39,7 +38,8 @@ const log = new Logger('App.Utils'); export function openDeepLink(deeplinkingUrl: string) { try { - WindowManager.showMainWindow(deeplinkingUrl); + MainWindow.show(); + ViewManager.handleDeepLink(deeplinkingUrl); } catch (err) { log.error(`There was an error opening the deeplinking url: ${err}`); } @@ -58,7 +58,7 @@ export function handleUpdateMenuEvent() { Menu.setApplicationMenu(aMenu); aMenu.addListener('menu-will-close', () => { ViewManager.focusCurrentView(); - WindowManager.sendToRenderer(APP_MENU_WILL_CLOSE); + MainWindow.sendToRenderer(APP_MENU_WILL_CLOSE); }); // set up context menu for tray icon diff --git a/src/main/app/windows.ts b/src/main/app/windows.ts new file mode 100644 index 00000000..806e4c32 --- /dev/null +++ b/src/main/app/windows.ts @@ -0,0 +1,57 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {BrowserWindow, IpcMainEvent, systemPreferences} from 'electron'; + +import {Logger} from 'common/log'; +import Config from 'common/config'; + +const log = new Logger('App.Windows'); + +export const handleGetDarkMode = () => { + return Config.darkMode; +}; + +export const handleClose = (event: IpcMainEvent) => BrowserWindow.fromWebContents(event.sender)?.close(); +export const handleMaximize = (event: IpcMainEvent) => BrowserWindow.fromWebContents(event.sender)?.maximize(); +export const handleMinimize = (event: IpcMainEvent) => BrowserWindow.fromWebContents(event.sender)?.minimize(); +export const handleRestore = (event: IpcMainEvent) => { + const window = BrowserWindow.fromWebContents(event.sender); + if (!window) { + return; + } + window.restore(); + if (window.isFullScreen()) { + window.setFullScreen(false); + } +}; + +export const handleDoubleClick = (event: IpcMainEvent, windowType?: string) => { + log.debug('handleDoubleClick', windowType); + + let action = 'Maximize'; + if (process.platform === 'darwin') { + action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); + } + const win = BrowserWindow.fromWebContents(event.sender); + if (!win) { + return; + } + switch (action) { + case 'Minimize': + if (win.isMinimized()) { + win.restore(); + } else { + win.minimize(); + } + break; + case 'Maximize': + default: + if (win.isMaximized()) { + win.unmaximize(); + } else { + win.maximize(); + } + break; + } +}; diff --git a/src/main/authManager.test.js b/src/main/authManager.test.js index 9a691090..b9add966 100644 --- a/src/main/authManager.test.js +++ b/src/main/authManager.test.js @@ -4,8 +4,8 @@ import {AuthManager} from 'main/authManager'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; import ModalManager from 'main/views/modalManager'; +import ViewManager from 'main/views/viewManager'; jest.mock('common/config', () => ({ teams: [{ @@ -89,8 +89,8 @@ jest.mock('main/windows/mainWindow', () => ({ get: jest.fn().mockImplementation(() => ({})), })); -jest.mock('main/windows/windowManager', () => ({ - getServerURLFromWebContentsId: jest.fn(), +jest.mock('main/views/viewManager', () => ({ + getViewByWebContentsId: jest.fn(), })); jest.mock('main/views/modalManager', () => ({ @@ -109,42 +109,42 @@ describe('main/authManager', () => { authManager.popPermissionModal = jest.fn(); it('should not pop any modal on a missing server', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(undefined); + ViewManager.getViewByWebContentsId.mockReturnValue(undefined); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 0}, {url: 'http://badurl.com/'}, null, jest.fn()); expect(authManager.popLoginModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled(); }); it('should popLoginModal when isTrustedURL', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://trustedurl.com/')); + ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://trustedurl.com/')}}}); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://trustedurl.com/'}, null, jest.fn()); expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled(); }); it('should popLoginModal when isCustomLoginURL', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://customloginurl.com/')); + ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://customloginurl.com/')}}}); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://customloginurl.com/'}, null, jest.fn()); expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled(); }); it('should popLoginModal when has permission', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://haspermissionurl.com/')); + ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://haspermissionurl.com/')}}}); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://haspermissionurl.com/'}, null, jest.fn()); expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled(); }); it('should popPermissionModal when anything else is true', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://someotherurl.com/')); + ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://someotherurl.com/')}}}); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, jest.fn()); expect(authManager.popLoginModal).not.toBeCalled(); expect(authManager.popPermissionModal).toBeCalled(); }); it('should set login callback when logging in', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(new URL('http://someotherurl.com/')); + ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://someotherurl.com/')}}}); const callback = jest.fn(); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback); expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback); diff --git a/src/main/authManager.ts b/src/main/authManager.ts index 8d509111..0b5cceea 100644 --- a/src/main/authManager.ts +++ b/src/main/authManager.ts @@ -12,8 +12,8 @@ import urlUtils from 'common/utils/url'; import modalManager from 'main/views/modalManager'; import TrustedOriginsStore from 'main/trustedOrigins'; import {getLocalURLString, getLocalPreload} from 'main/utils'; -import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; +import ViewManager from 'main/views/viewManager'; const log = new Logger('AuthManager'); const preload = getLocalPreload('desktopAPI.js'); @@ -40,7 +40,7 @@ export class AuthManager { if (!parsedURL) { return; } - const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id); + const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url; if (!serverURL) { return; } diff --git a/src/main/autoUpdater.test.js b/src/main/autoUpdater.test.js index ccbfb88e..fc883ce3 100644 --- a/src/main/autoUpdater.test.js +++ b/src/main/autoUpdater.test.js @@ -44,7 +44,7 @@ jest.mock('main/notifications', () => ({ displayUpgrade: jest.fn(), displayRestartToUpgrade: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ +jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), })); diff --git a/src/main/badge.test.js b/src/main/badge.test.js index 633f6521..6ca69787 100644 --- a/src/main/badge.test.js +++ b/src/main/badge.test.js @@ -25,9 +25,6 @@ jest.mock('common/appState', () => ({ jest.mock('./windows/mainWindow', () => ({ get: jest.fn(), })); -jest.mock('./windows/windowManager', () => ({ - setOverlayIcon: jest.fn(), -})); jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn().mockReturnValue(''), diff --git a/src/main/downloadsManager.test.js b/src/main/downloadsManager.test.js index 16b0b7db..6f5d03a2 100644 --- a/src/main/downloadsManager.test.js +++ b/src/main/downloadsManager.test.js @@ -77,7 +77,7 @@ jest.mock('macos-notification-state', () => ({ getDoNotDisturb: jest.fn(), })); jest.mock('main/notifications', () => ({})); -jest.mock('main/windows/windowManager', () => ({ +jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), })); jest.mock('main/views/viewManager', () => ({})); diff --git a/src/main/downloadsManager.ts b/src/main/downloadsManager.ts index 6ae64ea3..f7cfd7eb 100644 --- a/src/main/downloadsManager.ts +++ b/src/main/downloadsManager.ts @@ -32,7 +32,7 @@ import * as Validator from 'common/Validator'; import {localizeMessage} from 'main/i18nManager'; import {displayDownloadCompleted} from 'main/notifications'; import ViewManager from 'main/views/viewManager'; -import WindowManager from 'main/windows/windowManager'; +import MainWindow from 'main/windows/mainWindow'; import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils'; import appVersionManager from './AppVersionManager'; @@ -335,7 +335,7 @@ export class DownloadsManager extends JsonFileManager { onOpen = () => { this.open = true; - WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); + MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); }; onClose = () => { @@ -361,7 +361,7 @@ export class DownloadsManager extends JsonFileManager { log.debug('openDownloadsDropdown'); this.open = true; ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN); - WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); + MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); }; hasUpdate = () => { @@ -394,7 +394,7 @@ export class DownloadsManager extends JsonFileManager { this.downloads = downloads; this.setJson(downloads); ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads); - WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads); + MainWindow.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads); }; private save = (key: string, item: DownloadedItem) => { @@ -402,7 +402,7 @@ export class DownloadsManager extends JsonFileManager { this.downloads[key] = item; this.setValue(key, item); ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads); - WindowManager?.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads); + MainWindow.sendToRenderer(UPDATE_DOWNLOADS_DROPDOWN, this.downloads); }; private handleDownloadItemEvents = (item: DownloadItem, webContents: WebContents) => { @@ -485,9 +485,9 @@ export class DownloadsManager extends JsonFileManager { log.debug('shouldShowBadge'); if (this.open === true) { - WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); + MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); } else { - WindowManager.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE); + MainWindow.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE); } }; diff --git a/src/main/menus/app.test.js b/src/main/menus/app.test.js index 25edc7cb..bc73dcf1 100644 --- a/src/main/menus/app.test.js +++ b/src/main/menus/app.test.js @@ -55,12 +55,15 @@ jest.mock('common/servers/serverManager', () => ({ getOrderedServers: jest.fn(), getOrderedTabsForServer: jest.fn(), })); +jest.mock('main/app/servers', () => ({ + switchServer: jest.fn(), +})); jest.mock('main/diagnostics', () => ({})); jest.mock('main/downloadsManager', () => ({ hasDownloads: jest.fn(), })); jest.mock('main/views/viewManager', () => ({})); -jest.mock('main/windows/windowManager', () => ({ +jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), })); jest.mock('main/windows/settingsWindow', () => ({})); diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index b52f2448..fbd66709 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -13,12 +13,13 @@ import {Config} from 'common/config'; import {localizeMessage} from 'main/i18nManager'; import ServerManager from 'common/servers/serverManager'; -import WindowManager from 'main/windows/windowManager'; import {UpdateManager} from 'main/autoUpdater'; import downloadsManager from 'main/downloadsManager'; import Diagnostics from 'main/diagnostics'; import ViewManager from 'main/views/viewManager'; import SettingsWindow from 'main/windows/settingsWindow'; +import {selectNextTab, selectPreviousTab} from 'main/app/tabs'; +import {switchServer} from 'main/app/servers'; export function createTemplate(config: Config, updateManager: UpdateManager) { const separatorItem: MenuItemConstructorOptions = { @@ -265,7 +266,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: team.name, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, click() { - WindowManager.switchServer(team.id); + switchServer(team.id); }, }); if (ServerManager.getCurrentServer().id === team.id) { @@ -274,7 +275,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: ` ${localizeMessage(`common.tabs.${tab.type}`, getTabDisplayName(tab.type as TabType))}`, accelerator: `CmdOrCtrl+${i + 1}`, click() { - WindowManager.switchTab(tab.id); + ViewManager.showById(tab.id); }, }); }); @@ -284,14 +285,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'), accelerator: 'Ctrl+Tab', click() { - WindowManager.selectNextTab(); + selectNextTab(); }, enabled: (teams.length > 1), }, { label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'), accelerator: 'Ctrl+Shift+Tab', click() { - WindowManager.selectPreviousTab(); + selectPreviousTab(); }, enabled: (teams.length > 1), }, ...(isMac ? [separatorItem, { diff --git a/src/main/menus/tray.test.js b/src/main/menus/tray.test.js index 2d2ae6c7..288aff6b 100644 --- a/src/main/menus/tray.test.js +++ b/src/main/menus/tray.test.js @@ -14,9 +14,10 @@ jest.mock('main/i18nManager', () => ({ jest.mock('common/servers/serverManager', () => ({ getOrderedServers: jest.fn(), })); - +jest.mock('main/app/servers', () => ({ + switchServer: jest.fn(), +})); jest.mock('main/windows/settingsWindow', () => ({})); -jest.mock('main/windows/windowManager', () => ({})); describe('main/menus/tray', () => { it('should show the first 9 servers (using order)', () => { diff --git a/src/main/menus/tray.ts b/src/main/menus/tray.ts index 79bb2591..275299af 100644 --- a/src/main/menus/tray.ts +++ b/src/main/menus/tray.ts @@ -5,10 +5,11 @@ import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron'; -import WindowManager from 'main/windows/windowManager'; -import {localizeMessage} from 'main/i18nManager'; import ServerManager from 'common/servers/serverManager'; + +import {localizeMessage} from 'main/i18nManager'; import SettingsWindow from 'main/windows/settingsWindow'; +import {switchServer} from 'main/app/servers'; export function createTemplate() { const teams = ServerManager.getOrderedServers(); @@ -17,7 +18,7 @@ export function createTemplate() { return { label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name, click: () => { - WindowManager.switchServer(team.id); + switchServer(team.id); }, }; }), { diff --git a/src/main/notifications/index.test.js b/src/main/notifications/index.test.js index 4a99d567..8784aa75 100644 --- a/src/main/notifications/index.test.js +++ b/src/main/notifications/index.test.js @@ -13,9 +13,8 @@ import {PLAY_SOUND} from 'common/communication'; import Config from 'common/config'; import {localizeMessage} from 'main/i18nManager'; - -import MainWindow from '../windows/mainWindow'; -import WindowManager from '../windows/windowManager'; +import MainWindow from 'main/windows/mainWindow'; +import ViewManager from 'main/views/viewManager'; import getLinuxDoNotDisturb from './dnd-linux'; @@ -83,14 +82,11 @@ jest.mock('../views/viewManager', () => ({ }, }, }), + showById: jest.fn(), })); jest.mock('../windows/mainWindow', () => ({ get: jest.fn(), -})); -jest.mock('../windows/windowManager', () => ({ sendToRenderer: jest.fn(), - flashFrame: jest.fn(), - switchTab: jest.fn(), })); jest.mock('main/i18nManager', () => ({ @@ -192,7 +188,7 @@ describe('main/notifications', () => { {id: 1}, {soundName: 'test_sound'}, ); - expect(WindowManager.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound'); + expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(PLAY_SOUND, 'test_sound'); }); it('should remove existing notification from the same channel/team on windows', () => { @@ -248,7 +244,7 @@ describe('main/notifications', () => { ); const mention = mentions.find((m) => m.body === 'mention_click_body'); mention.value.click(); - expect(WindowManager.switchTab).toHaveBeenCalledWith('server_id'); + expect(ViewManager.showById).toHaveBeenCalledWith('server_id'); }); it('linux/windows - should not flash frame when config item is not set', () => { diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 0a4e2001..315e8bb4 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -13,7 +13,6 @@ import {Logger} from 'common/log'; import ViewManager from '../views/viewManager'; import MainWindow from '../windows/mainWindow'; -import WindowManager from '../windows/windowManager'; import {Mention} from './Mention'; import {DownloadNotification} from './Download'; @@ -67,7 +66,7 @@ export function displayMention(title: string, body: string, channel: {id: string } const notificationSound = mention.getNotificationSound(); if (notificationSound) { - WindowManager.sendToRenderer(PLAY_SOUND, notificationSound); + MainWindow.sendToRenderer(PLAY_SOUND, notificationSound); } flashFrame(true); }); @@ -75,7 +74,7 @@ export function displayMention(title: string, body: string, channel: {id: string mention.on('click', () => { log.debug('notification click', serverName, mention); if (serverName) { - WindowManager.switchTab(view.id); + ViewManager.showById(view.id); webcontents.send('notification-clicked', {channel, teamId, url}); } }); diff --git a/src/main/tray/tray.test.js b/src/main/tray/tray.test.js index 20cd47d5..53b3d2b1 100644 --- a/src/main/tray/tray.test.js +++ b/src/main/tray/tray.test.js @@ -59,10 +59,9 @@ jest.mock('main/AutoLauncher', () => ({ jest.mock('main/badge', () => ({ setUnreadBadgeSetting: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ - handleUpdateConfig: jest.fn(), +jest.mock('main/windows/mainWindow', () => ({ sendToRenderer: jest.fn(), - initializeCurrentServerName: jest.fn(), + on: jest.fn(), })); describe('main/tray', () => { diff --git a/src/main/tray/tray.ts b/src/main/tray/tray.ts index 4e1df657..afd6281a 100644 --- a/src/main/tray/tray.ts +++ b/src/main/tray/tray.ts @@ -7,12 +7,14 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron'; import AppState from 'common/appState'; import {UPDATE_APPSTATE_TOTALS} from 'common/communication'; +import {Logger} from 'common/log'; import {localizeMessage} from 'main/i18nManager'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; +import SettingsWindow from 'main/windows/settingsWindow'; const assetsDir = path.resolve(app.getAppPath(), 'assets'); +const log = new Logger('Tray'); let trayImages: Record; let trayIcon: Tray; @@ -85,12 +87,12 @@ export function setupTray(iconTheme: string) { trayIcon.setToolTip(app.name); trayIcon.on('click', () => { - const mainWindow = MainWindow.get(true)!; - if (mainWindow.isVisible()) { + const mainWindow = MainWindow.get(); + if (mainWindow && mainWindow.isVisible()) { mainWindow.blur(); // To move focus to the next top-level window in Windows mainWindow.hide(); } else { - WindowManager.restoreMain(); + restoreMain(); } }); @@ -98,7 +100,7 @@ export function setupTray(iconTheme: string) { trayIcon.popUpContextMenu(); }); trayIcon.on('balloon-click', () => { - WindowManager.restoreMain(); + restoreMain(); }); AppState.on(UPDATE_APPSTATE_TOTALS, (anyExpired: boolean, anyMentions: number, anyUnreads: boolean) => { @@ -114,6 +116,32 @@ export function setupTray(iconTheme: string) { }); } +const restoreMain = () => { + log.info('restoreMain'); + MainWindow.show(); + const mainWindow = MainWindow.get(); + if (!mainWindow) { + throw new Error('Main window does not exist'); + } + if (!mainWindow.isVisible() || mainWindow.isMinimized()) { + if (mainWindow.isMinimized()) { + mainWindow.restore(); + } else { + mainWindow.show(); + } + const settingsWindow = SettingsWindow.get(); + if (settingsWindow) { + settingsWindow.focus(); + } else { + mainWindow.focus(); + } + } else if (SettingsWindow.get()) { + SettingsWindow.get()?.focus(); + } else { + mainWindow.focus(); + } +}; + function setTray(status: string, message: string) { if (trayIcon.isDestroyed()) { return; diff --git a/src/main/views/MattermostView.test.js b/src/main/views/MattermostView.test.js index 13c16536..cb577945 100644 --- a/src/main/views/MattermostView.test.js +++ b/src/main/views/MattermostView.test.js @@ -9,7 +9,6 @@ import {MattermostServer} from 'common/servers/MattermostServer'; import MessagingTabView from 'common/tabs/MessagingTabView'; import MainWindow from '../windows/mainWindow'; -import * as WindowManager from '../windows/windowManager'; import ContextMenu from '../contextMenu'; import Utils from '../utils'; @@ -41,8 +40,6 @@ jest.mock('electron', () => ({ jest.mock('../windows/mainWindow', () => ({ focusThreeDotMenu: jest.fn(), get: jest.fn(), -})); -jest.mock('../windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); jest.mock('common/appState', () => ({ @@ -181,7 +178,7 @@ describe('main/views/MattermostView', () => { await expect(promise).rejects.toThrow(error); expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object)); expect(mattermostView.loadRetry).not.toBeCalled(); - expect(WindowManager.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.id, expect.any(String), expect.any(String)); + expect(MainWindow.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.id, expect.any(String), expect.any(String)); expect(mattermostView.status).toBe(-1); jest.runAllTimers(); expect(retryInBackgroundFn).toBeCalled(); @@ -426,13 +423,13 @@ describe('main/views/MattermostView', () => { it('should hide back button on internal url', () => { Utils.shouldHaveBackBar.mockReturnValue(false); mattermostView.handleDidNavigate(null, 'http://server-1.com/path/to/channels'); - expect(WindowManager.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, false); + expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, false); }); it('should show back button on external url', () => { Utils.shouldHaveBackBar.mockReturnValue(true); mattermostView.handleDidNavigate(null, 'http://server-2.com/some/other/path'); - expect(WindowManager.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, true); + expect(MainWindow.sendToRenderer).toHaveBeenCalledWith(TOGGLE_BACK_BUTTON, true); }); }); diff --git a/src/main/views/MattermostView.ts b/src/main/views/MattermostView.ts index f5d4aebb..114035c7 100644 --- a/src/main/views/MattermostView.ts +++ b/src/main/views/MattermostView.ts @@ -26,7 +26,6 @@ import {Logger} from 'common/log'; import {TabView} from 'common/tabs/TabView'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; import ContextMenu from '../contextMenu'; import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils'; @@ -180,7 +179,7 @@ export class MattermostView extends EventEmitter { const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); loading.then(this.loadSuccess(loadURL)).catch((err) => { if (err.code && err.code.startsWith('ERR_CERT')) { - WindowManager.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); + MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); this.emit(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); this.log.info(`Invalid certificate, stop retrying until the user decides what to do: ${err}.`); this.status = Status.ERROR; @@ -394,7 +393,7 @@ export class MattermostView extends EventEmitter { if (this.maxRetries-- > 0) { this.loadRetry(loadURL, err); } else { - WindowManager.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); + MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); this.emit(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); this.log.info(`Couldn't establish a connection with ${loadURL}, will continue to retry in the background`, err); this.status = Status.ERROR; @@ -419,14 +418,14 @@ export class MattermostView extends EventEmitter { private loadRetry = (loadURL: string, err: Error) => { this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL); - WindowManager.sendToRenderer(LOAD_RETRY, this.id, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString()); + MainWindow.sendToRenderer(LOAD_RETRY, this.id, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString()); this.log.info(`failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`); } private loadSuccess = (loadURL: string) => { return () => { this.log.verbose(`finished loading ${loadURL}`); - WindowManager.sendToRenderer(LOAD_SUCCESS, this.id); + MainWindow.sendToRenderer(LOAD_SUCCESS, this.id); this.maxRetries = MAX_SERVER_RETRIES; if (this.status === Status.LOADING) { this.updateMentionsFromTitle(this.view.webContents.getTitle()); @@ -476,11 +475,11 @@ export class MattermostView extends EventEmitter { if (shouldHaveBackBar(this.tab.url || '', url)) { this.setBounds(getWindowBoundaries(mainWindow, true)); - WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true); + MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true); this.log.debug('show back button'); } else { this.setBounds(getWindowBoundaries(mainWindow)); - WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); + MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, false); this.log.debug('hide back button'); } } diff --git a/src/main/views/downloadsDropdownMenuView.test.js b/src/main/views/downloadsDropdownMenuView.test.js index 37a13b5c..9eb2147c 100644 --- a/src/main/views/downloadsDropdownMenuView.test.js +++ b/src/main/views/downloadsDropdownMenuView.test.js @@ -54,10 +54,9 @@ jest.mock('macos-notification-state', () => ({ })); jest.mock('main/downloadsManager', () => ({})); jest.mock('main/windows/mainWindow', () => ({ + on: jest.fn(), get: jest.fn(), getBounds: jest.fn(), -})); -jest.mock('main/windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); jest.mock('fs', () => ({ diff --git a/src/main/views/downloadsDropdownMenuView.ts b/src/main/views/downloadsDropdownMenuView.ts index 513437ac..f8a44edf 100644 --- a/src/main/views/downloadsDropdownMenuView.ts +++ b/src/main/views/downloadsDropdownMenuView.ts @@ -11,6 +11,8 @@ import { DOWNLOADS_DROPDOWN_MENU_OPEN_FILE, DOWNLOADS_DROPDOWN_MENU_SHOW_FILE_IN_FOLDER, EMIT_CONFIGURATION, + MAIN_WINDOW_CREATED, + MAIN_WINDOW_RESIZED, OPEN_DOWNLOADS_DROPDOWN_MENU, REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO, TOGGLE_DOWNLOADS_DROPDOWN_MENU, @@ -28,7 +30,6 @@ import { import {getLocalPreload, getLocalURLString} from 'main/utils'; import downloadsManager from 'main/downloadsManager'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; const log = new Logger('DownloadsDropdownMenuView'); @@ -43,6 +44,8 @@ export class DownloadsDropdownMenuView { constructor() { this.open = false; + MainWindow.on(MAIN_WINDOW_CREATED, this.init); + MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds); ipcMain.on(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose); ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle); @@ -55,7 +58,7 @@ export class DownloadsDropdownMenuView { ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem); } - init = () => { + private init = () => { this.windowBounds = MainWindow.getBounds(); if (!this.windowBounds) { throw new Error('Cannot initialize downloadsDropdownMenuView, missing MainWindow'); @@ -79,10 +82,10 @@ export class DownloadsDropdownMenuView { * This is called every time the "window" is resized so that we can position * the downloads dropdown at the correct position */ - updateWindowBounds = () => { + private updateWindowBounds = (newBounds: Electron.Rectangle) => { log.debug('updateWindowBounds'); - this.windowBounds = MainWindow.getBounds(); + this.windowBounds = newBounds; this.updateDownloadsDropdownMenu(); this.repositionDownloadsDropdownMenu(); } @@ -134,7 +137,7 @@ export class DownloadsDropdownMenuView { this.item = undefined; ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM); this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0)); - WindowManager.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN_MENU); + MainWindow.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN_MENU); } private handleToggle = (event: IpcMainEvent, payload: DownloadsMenuOpenEventPayload) => { diff --git a/src/main/views/downloadsDropdownView.test.js b/src/main/views/downloadsDropdownView.test.js index a7a24154..baf2700c 100644 --- a/src/main/views/downloadsDropdownView.test.js +++ b/src/main/views/downloadsDropdownView.test.js @@ -67,10 +67,9 @@ jest.mock('main/downloadsManager', () => ({ onClose: jest.fn(), })); jest.mock('main/windows/mainWindow', () => ({ + on: jest.fn(), get: jest.fn(), getBounds: jest.fn(), -})); -jest.mock('main/windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); diff --git a/src/main/views/downloadsDropdownView.ts b/src/main/views/downloadsDropdownView.ts index bf1fb3a1..d4fe6d4f 100644 --- a/src/main/views/downloadsDropdownView.ts +++ b/src/main/views/downloadsDropdownView.ts @@ -16,6 +16,8 @@ import { UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, DOWNLOADS_DROPDOWN_OPEN_FILE, + MAIN_WINDOW_CREATED, + MAIN_WINDOW_RESIZED, } from 'common/communication'; import {Logger} from 'common/log'; import Config from 'common/config'; @@ -23,7 +25,6 @@ import {TAB_BAR_HEIGHT, DOWNLOADS_DROPDOWN_WIDTH, DOWNLOADS_DROPDOWN_HEIGHT, DOW import {getLocalPreload, getLocalURLString} from 'main/utils'; import downloadsManager from 'main/downloadsManager'; -import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; const log = new Logger('DownloadsDropdownView'); @@ -35,6 +36,8 @@ export class DownloadsDropdownView { private view?: BrowserView; constructor() { + MainWindow.on(MAIN_WINDOW_CREATED, this.init); + MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds); ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose); ipcMain.on(EMIT_CONFIGURATION, this.updateDownloadsDropdown); @@ -73,10 +76,10 @@ export class DownloadsDropdownView { * This is called every time the "window" is resized so that we can position * the downloads dropdown at the correct position */ - updateWindowBounds = () => { + private updateWindowBounds = (newBounds: Electron.Rectangle) => { log.debug('updateWindowBounds'); - this.windowBounds = MainWindow.getBounds(); + this.windowBounds = newBounds; this.updateDownloadsDropdown(); this.repositionDownloadsDropdown(); } @@ -110,7 +113,7 @@ export class DownloadsDropdownView { MainWindow.get()?.setTopBrowserView(this.view); this.view.webContents.focus(); downloadsManager.onOpen(); - WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN); + MainWindow.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN); } private handleClose = () => { @@ -118,7 +121,7 @@ export class DownloadsDropdownView { this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0)); downloadsManager.onClose(); - WindowManager.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN); + MainWindow.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN); } private clearDownloads = () => { diff --git a/src/main/views/loadingScreen.test.js b/src/main/views/loadingScreen.test.js index 7822d729..e0c4b7c2 100644 --- a/src/main/views/loadingScreen.test.js +++ b/src/main/views/loadingScreen.test.js @@ -13,6 +13,7 @@ jest.mock('electron', () => ({ jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), + on: jest.fn(), })); describe('main/views/loadingScreen', () => { diff --git a/src/main/views/loadingScreen.ts b/src/main/views/loadingScreen.ts index 8ec919fc..32b95c66 100644 --- a/src/main/views/loadingScreen.ts +++ b/src/main/views/loadingScreen.ts @@ -3,7 +3,7 @@ import {BrowserView, app, ipcMain} from 'electron'; -import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication'; +import {DARK_MODE_CHANGE, LOADING_SCREEN_ANIMATION_FINISHED, MAIN_WINDOW_RESIZED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication'; import {Logger} from 'common/log'; import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils'; @@ -24,6 +24,7 @@ export class LoadingScreen { constructor() { this.state = LoadingScreenState.HIDDEN; + MainWindow.on(MAIN_WINDOW_RESIZED, this.setBounds); ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleAnimationFinished); } @@ -31,16 +32,6 @@ export class LoadingScreen { * 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); } @@ -111,6 +102,16 @@ export class LoadingScreen { app.emit('e2e-app-loaded'); } } + + private setBounds = () => { + if (this.view) { + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + this.view.setBounds(getWindowBoundaries(mainWindow)); + } + } } const loadingScreen = new LoadingScreen(); diff --git a/src/main/views/modalManager.test.js b/src/main/views/modalManager.test.js index c2dfde0c..9aaa190b 100644 --- a/src/main/views/modalManager.test.js +++ b/src/main/views/modalManager.test.js @@ -25,7 +25,8 @@ jest.mock('./modalView', () => ({ jest.mock('main/views/viewManager', () => ({ focusCurrentView: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ +jest.mock('main/windows/mainWindow', () => ({ + on: jest.fn(), sendToRenderer: jest.fn(), })); jest.mock('process', () => ({ diff --git a/src/main/views/modalManager.ts b/src/main/views/modalManager.ts index a361a3c6..5a718597 100644 --- a/src/main/views/modalManager.ts +++ b/src/main/views/modalManager.ts @@ -15,14 +15,14 @@ import { EMIT_CONFIGURATION, DARK_MODE_CHANGE, GET_MODAL_UNCLOSEABLE, - RESIZE_MODAL, + MAIN_WINDOW_RESIZED, } from 'common/communication'; import {Logger} from 'common/log'; import {getAdjustedWindowBoundaries} from 'main/utils'; +import MainWindow from 'main/windows/mainWindow'; import WebContentsEventManager from 'main/views/webContentEvents'; import ViewManager from 'main/views/viewManager'; -import WindowManager from 'main/windows/windowManager'; import {ModalView} from './modalView'; @@ -40,7 +40,7 @@ export class ModalManager { ipcMain.handle(RETRIEVE_MODAL_INFO, this.handleInfoRequest); ipcMain.on(MODAL_RESULT, this.handleModalResult); ipcMain.on(MODAL_CANCEL, this.handleModalCancel); - ipcMain.on(RESIZE_MODAL, this.handleResizeModal); + MainWindow.on(MAIN_WINDOW_RESIZED, this.handleResizeModal); ipcMain.on(EMIT_CONFIGURATION, this.handleEmitConfiguration); } @@ -88,11 +88,11 @@ export class ModalManager { const withDevTools = process.env.MM_DEBUG_MODALS || false; this.modalQueue.forEach((modal, index) => { if (index === 0) { - WindowManager.sendToRenderer(MODAL_OPEN); + MainWindow.sendToRenderer(MODAL_OPEN); modal.show(undefined, Boolean(withDevTools)); WebContentsEventManager.addWebContentsEventListeners(modal.view.webContents); } else { - WindowManager.sendToRenderer(MODAL_CLOSE); + MainWindow.sendToRenderer(MODAL_CLOSE); modal.hide(); } }); @@ -114,7 +114,7 @@ export class ModalManager { if (this.modalQueue.length) { this.showModal(); } else { - WindowManager.sendToRenderer(MODAL_CLOSE); + MainWindow.sendToRenderer(MODAL_CLOSE); ViewManager.focusCurrentView(); } } @@ -131,7 +131,7 @@ export class ModalManager { return this.modalQueue.some((modal) => modal.isActive()); } - handleResizeModal = (event: IpcMainEvent, bounds: Electron.Rectangle) => { + handleResizeModal = (bounds: Electron.Rectangle) => { log.debug('handleResizeModal', {bounds, modalQueueLength: this.modalQueue.length}); if (this.modalQueue.length) { diff --git a/src/main/views/teamDropdownView.test.js b/src/main/views/teamDropdownView.test.js index d3322588..ac3888fd 100644 --- a/src/main/views/teamDropdownView.test.js +++ b/src/main/views/teamDropdownView.test.js @@ -27,12 +27,9 @@ jest.mock('electron', () => ({ }, })); jest.mock('main/windows/mainWindow', () => ({ + on: jest.fn(), get: jest.fn(), getBounds: jest.fn(), - addBrowserView: jest.fn(), - setTopBrowserView: jest.fn(), -})); -jest.mock('../windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); diff --git a/src/main/views/teamDropdownView.ts b/src/main/views/teamDropdownView.ts index b0d9426a..8e36e179 100644 --- a/src/main/views/teamDropdownView.ts +++ b/src/main/views/teamDropdownView.ts @@ -15,6 +15,8 @@ import { REQUEST_TEAMS_DROPDOWN_INFO, RECEIVE_DROPDOWN_MENU_SIZE, SERVERS_UPDATE, + MAIN_WINDOW_CREATED, + MAIN_WINDOW_RESIZED, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -23,7 +25,6 @@ import ServerManager from 'common/servers/serverManager'; import {getLocalPreload, getLocalURLString} from 'main/utils'; -import WindowManager from '../windows/windowManager'; import MainWindow from '../windows/mainWindow'; const log = new Logger('TeamDropdownView'); @@ -51,6 +52,9 @@ export class TeamDropdownView { this.mentions = new Map(); this.expired = new Map(); + MainWindow.on(MAIN_WINDOW_CREATED, this.init); + MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds); + ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen); ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose); ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); @@ -62,7 +66,13 @@ export class TeamDropdownView { ServerManager.on(SERVERS_UPDATE, this.updateServers); } - init = () => { + private updateWindowBounds = (newBounds: Electron.Rectangle) => { + this.windowBounds = newBounds; + this.updateDropdown(); + } + + private init = () => { + log.info('init'); const preload = getLocalPreload('desktopAPI.js'); this.view = new BrowserView({webPreferences: { preload, @@ -80,11 +90,6 @@ export class TeamDropdownView { MainWindow.get()?.addBrowserView(this.view); } - updateWindowBounds = () => { - this.windowBounds = MainWindow.getBounds(); - this.updateDropdown(); - } - private updateDropdown = () => { log.silly('updateDropdown'); @@ -132,7 +137,7 @@ export class TeamDropdownView { this.view.setBounds(this.bounds); MainWindow.get()?.setTopBrowserView(this.view); this.view.webContents.focus(); - WindowManager.sendToRenderer(OPEN_TEAMS_DROPDOWN); + MainWindow.sendToRenderer(OPEN_TEAMS_DROPDOWN); this.isOpen = true; } @@ -140,7 +145,7 @@ export class TeamDropdownView { log.debug('handleClose'); this.view?.setBounds(this.getBounds(0, 0)); - WindowManager.sendToRenderer(CLOSE_TEAMS_DROPDOWN); + MainWindow.sendToRenderer(CLOSE_TEAMS_DROPDOWN); this.isOpen = false; } diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index b58c9f78..a1d57b48 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -69,6 +69,7 @@ jest.mock('main/views/loadingScreen', () => ({ })); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), + on: jest.fn(), })); jest.mock('common/servers/serverManager', () => ({ getCurrentServer: jest.fn(), diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 6c98d98c..ecc1697b 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -24,6 +24,8 @@ import { HISTORY, GET_VIEW_INFO_FOR_TEST, SESSION_EXPIRED, + MAIN_WINDOW_CREATED, + MAIN_WINDOW_RESIZED, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -36,7 +38,7 @@ import {TabView, TAB_MESSAGING} from 'common/tabs/TabView'; import {localizeMessage} from 'main/i18nManager'; import MainWindow from 'main/windows/mainWindow'; -import {getLocalURLString, getLocalPreload} from '../utils'; +import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; import {MattermostView} from './MattermostView'; import modalManager from './modalManager'; @@ -57,6 +59,8 @@ export class ViewManager { 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(); + MainWindow.on(MAIN_WINDOW_CREATED, this.init); + MainWindow.on(MAIN_WINDOW_RESIZED, this.handleSetCurrentViewBounds); ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest); ipcMain.on(HISTORY, this.handleHistory); ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); @@ -71,7 +75,9 @@ export class ViewManager { ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration); } - init = () => { + private init = () => { + MainWindow.onBrowserWindow?.('focus', this.focusCurrentView); + LoadingScreen.show(); ServerManager.getAllServers().forEach((server) => this.loadServer(server)); this.showInitial(); @@ -323,7 +329,7 @@ export class ViewManager { const localURL = getLocalURLString('urlView.html', query); urlView.webContents.loadURL(localURL); MainWindow.get()?.addBrowserView(urlView); - const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? mainWindow.getBounds(); + const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? MainWindow.getBounds(); const hideView = () => { delete this.urlViewCancel; @@ -343,6 +349,10 @@ export class ViewManager { const adjustWidth = (event: IpcMainEvent, width: number) => { log.silly('showURLView.adjustWidth', width); + if (!boundaries) { + return; + } + const bounds = { x: 0, y: (boundaries.height + TAB_BAR_HEIGHT) - URL_VIEW_HEIGHT, @@ -526,6 +536,16 @@ export class ViewManager { AppState.updateExpired(viewId, isExpired); } + private handleSetCurrentViewBounds = (newBounds: Electron.Rectangle) => { + log.debug('handleSetCurrentViewBounds', newBounds); + + const currentView = this.getCurrentView(); + if (currentView) { + const adjustedBounds = getAdjustedWindowBoundaries(newBounds.width, newBounds.height, shouldHaveBackBar(currentView.tab.url, currentView.currentURL)); + currentView.setBounds(adjustedBounds); + } + } + /** * Helper functions */ diff --git a/src/main/views/webContentEvents.test.js b/src/main/views/webContentEvents.test.js index 3ca9a515..5935e79a 100644 --- a/src/main/views/webContentEvents.test.js +++ b/src/main/views/webContentEvents.test.js @@ -8,8 +8,8 @@ import {shell, BrowserWindow} from 'electron'; import urlUtils from 'common/utils/url'; import ContextMenu from 'main/contextMenu'; +import ViewManager from 'main/views/viewManager'; -import * as WindowManager from '../windows/windowManager'; import allowProtocolDialog from '../allowProtocolDialog'; import {WebContentsEventManager} from './webContentEvents'; @@ -30,10 +30,7 @@ jest.mock('../allowProtocolDialog', () => ({})); jest.mock('main/windows/callsWidgetWindow', () => ({})); jest.mock('main/views/viewManager', () => ({ getViewByWebContentsId: jest.fn(), -})); -jest.mock('../windows/windowManager', () => ({ - getServerURLFromWebContentsId: jest.fn(), - showMainWindow: jest.fn(), + handleDeepLink: jest.fn(), })); jest.mock('../utils', () => ({ composeUserAgent: jest.fn(), @@ -231,7 +228,7 @@ describe('main/views/webContentsEvents', () => { }); it('should open in the browser when there is no server matching', () => { - WindowManager.getServerURLFromWebContentsId.mockReturnValue(undefined); + ViewManager.getViewByWebContentsId.mockReturnValue(undefined); expect(newWindow({url: 'http://server-2.com/subpath'})).toStrictEqual({action: 'deny'}); expect(shell.openExternal).toBeCalledWith('http://server-2.com/subpath'); }); @@ -248,7 +245,7 @@ describe('main/views/webContentsEvents', () => { it('should open team links in the app', () => { expect(newWindow({url: 'http://server-1.com/myteam/channels/mychannel'})).toStrictEqual({action: 'deny'}); - expect(WindowManager.showMainWindow).toBeCalledWith(new URL('http://server-1.com/myteam/channels/mychannel')); + expect(ViewManager.handleDeepLink).toBeCalledWith(new URL('http://server-1.com/myteam/channels/mychannel')); }); it('should prevent admin links from opening in a new window', () => { diff --git a/src/main/views/webContentEvents.ts b/src/main/views/webContentEvents.ts index aee0e531..189d6abf 100644 --- a/src/main/views/webContentEvents.ts +++ b/src/main/views/webContentEvents.ts @@ -12,7 +12,6 @@ import ContextMenu from 'main/contextMenu'; import ServerManager from 'common/servers/serverManager'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; import ViewManager from 'main/views/viewManager'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; @@ -63,7 +62,11 @@ export class WebContentsEventManager { return this.popupWindow.serverURL; } - return WindowManager.getServerURLFromWebContentsId(webContentsId); + if (CallsWidgetWindow.isCallsWidget(webContentsId)) { + return CallsWidgetWindow.getURL(); + } + + return ViewManager.getViewByWebContentsId(webContentsId)?.tab.server.url; } private generateWillNavigate = (webContentsId: number) => { @@ -182,7 +185,7 @@ export class WebContentsEventManager { } if (urlUtils.isTeamUrl(serverURL, parsedURL, true)) { - WindowManager.showMainWindow(parsedURL); + ViewManager.handleDeepLink(parsedURL); return {action: 'deny'}; } if (urlUtils.isAdminUrl(serverURL, parsedURL)) { @@ -259,7 +262,7 @@ export class WebContentsEventManager { const otherServerURL = ServerManager.lookupTabByURL(parsedURL); if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) { - WindowManager.showMainWindow(parsedURL); + ViewManager.handleDeepLink(parsedURL); return {action: 'deny'}; } diff --git a/src/main/windows/callsWidgetWindow.test.js b/src/main/windows/callsWidgetWindow.test.js index ab8bf1f0..187676b9 100644 --- a/src/main/windows/callsWidgetWindow.test.js +++ b/src/main/windows/callsWidgetWindow.test.js @@ -12,9 +12,10 @@ import { CALLS_PLUGIN_ID, } from 'common/utils/constants'; import urlUtils from 'common/utils/url'; + +import {switchServer} from 'main/app/servers'; import MainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; -import WindowManager from 'main/windows/windowManager'; import { resetScreensharePermissionsMacOS, openScreensharePermissionsSettingsMacOS, @@ -55,7 +56,7 @@ jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), focus: jest.fn(), })); -jest.mock('main/windows/windowManager', () => ({ +jest.mock('main/app/servers', () => ({ switchServer: jest.fn(), })); jest.mock('main/views/viewManager', () => ({ @@ -797,7 +798,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should switch server', () => { callsWidgetWindow.handleDesktopSourcesModalRequest(); - expect(WindowManager.switchServer).toHaveBeenCalledWith('server-1'); + expect(switchServer).toHaveBeenCalledWith('server-1'); }); }); @@ -864,7 +865,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should switch server', () => { callsWidgetWindow.handleCallsWidgetChannelLinkClick(); - expect(WindowManager.switchServer).toHaveBeenCalledWith('server-2'); + expect(switchServer).toHaveBeenCalledWith('server-2'); }); }); @@ -890,7 +891,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should focus view and propagate error to main view', () => { callsWidgetWindow.handleCallsError('', {err: 'client-error'}); - expect(WindowManager.switchServer).toHaveBeenCalledWith('server-2'); + expect(switchServer).toHaveBeenCalledWith('server-2'); expect(focus).toHaveBeenCalled(); expect(callsWidgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'}); }); @@ -918,7 +919,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should pass through the click link to browser history push', () => { callsWidgetWindow.handleCallsLinkClick('', {link: '/other/subpath'}); - expect(WindowManager.switchServer).toHaveBeenCalledWith('server-1'); + expect(switchServer).toHaveBeenCalledWith('server-1'); expect(view.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath'); }); }); diff --git a/src/main/windows/callsWidgetWindow.ts b/src/main/windows/callsWidgetWindow.ts index 6019b419..16081b55 100644 --- a/src/main/windows/callsWidgetWindow.ts +++ b/src/main/windows/callsWidgetWindow.ts @@ -37,9 +37,10 @@ import { DESKTOP_SOURCES_RESULT, DISPATCH_GET_DESKTOP_SOURCES, } from 'common/communication'; + +import {switchServer} from 'main/app/servers'; import webContentsEventManager from 'main/views/webContentEvents'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; import ViewManager from 'main/views/viewManager'; const log = new Logger('CallsWidgetWindow'); @@ -454,7 +455,7 @@ export class CallsWidgetWindow { return; } - WindowManager.switchServer(this.serverID); + switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); } @@ -472,7 +473,7 @@ export class CallsWidgetWindow { return; } - WindowManager.switchServer(this.serverID); + switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); } @@ -484,7 +485,7 @@ export class CallsWidgetWindow { return; } - WindowManager.switchServer(this.serverID); + switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(CALLS_ERROR, msg); } @@ -496,7 +497,7 @@ export class CallsWidgetWindow { return; } - WindowManager.switchServer(this.serverID); + switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); } diff --git a/src/main/windows/mainWindow.test.js b/src/main/windows/mainWindow.test.js index 1583b55c..ca7e3e40 100644 --- a/src/main/windows/mainWindow.test.js +++ b/src/main/windows/mainWindow.test.js @@ -36,6 +36,7 @@ jest.mock('electron', () => ({ BrowserWindow: jest.fn(), ipcMain: { handle: jest.fn(), + on: jest.fn(), }, screen: { getDisplayMatching: jest.fn(), @@ -469,6 +470,39 @@ describe('main/windows/mainWindow', () => { }); }); + describe('show', () => { + const mainWindow = new MainWindow(); + mainWindow.win = { + visible: false, + isVisible: () => mainWindow.visible, + show: jest.fn(), + focus: jest.fn(), + on: jest.fn(), + once: jest.fn(), + webContents: { + setWindowOpenHandler: jest.fn(), + }, + }; + + beforeEach(() => { + mainWindow.win.show.mockImplementation(() => { + mainWindow.visible = true; + }); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + it('should show main window if it exists and focus it if it is already visible', () => { + mainWindow.show(); + expect(mainWindow.win.show).toHaveBeenCalled(); + + mainWindow.show(); + expect(mainWindow.win.focus).toHaveBeenCalled(); + }); + }); + describe('onUnresponsive', () => { const mainWindow = new MainWindow(); diff --git a/src/main/windows/mainWindow.ts b/src/main/windows/mainWindow.ts index 09505659..ac7a94d9 100644 --- a/src/main/windows/mainWindow.ts +++ b/src/main/windows/mainWindow.ts @@ -7,16 +7,30 @@ import path from 'path'; import os from 'os'; +import {EventEmitter} from 'events'; + import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, globalShortcut, Input, ipcMain, screen} from 'electron'; import {SavedWindowState} from 'types/mainWindow'; import AppState from 'common/appState'; -import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, FOCUS_THREE_DOT_MENU, SERVERS_UPDATE, UPDATE_APPSTATE_FOR_VIEW_ID, UPDATE_MENTIONS} from 'common/communication'; +import { + SELECT_NEXT_TAB, + SELECT_PREVIOUS_TAB, + GET_FULL_SCREEN_STATUS, + FOCUS_THREE_DOT_MENU, + SERVERS_UPDATE, + UPDATE_APPSTATE_FOR_VIEW_ID, + UPDATE_MENTIONS, + MAXIMIZE_CHANGE, + MAIN_WINDOW_CREATED, + MAIN_WINDOW_RESIZED, + VIEW_FINISHED_RESIZING, +} from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; -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, SECOND} from 'common/utils/constants'; import Utils from 'common/utils/util'; import * as Validator from 'common/Validator'; @@ -29,18 +43,23 @@ import {getLocalPreload, getLocalURLString, isInsideRectangle} from '../utils'; const log = new Logger('MainWindow'); const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P']; -export class MainWindow { +export class MainWindow extends EventEmitter { private win?: BrowserWindow; private savedWindowState: SavedWindowState; private ready: boolean; + private isResizing: boolean; constructor() { + super(); + // Create the browser window. this.ready = false; + this.isResizing = false; this.savedWindowState = this.getSavedWindowState(); ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen()); + ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); ServerManager.on(SERVERS_UPDATE, this.handleUpdateConfig); @@ -108,6 +127,15 @@ export class MainWindow { this.win.on('focus', this.onFocus); this.win.on('blur', this.onBlur); this.win.on('unresponsive', this.onUnresponsive); + this.win.on('maximize', this.onMaximize); + this.win.on('unmaximize', this.onUnmaximize); + this.win.on('enter-full-screen', () => this.win?.webContents.send('enter-full-screen')); + this.win.on('leave-full-screen', () => this.win?.webContents.send('leave-full-screen')); + this.win.on('will-resize', this.onWillResize); + this.win.on('resized', this.onResized); + if (process.platform !== 'darwin') { + mainWindow.on('resize', this.onResize); + } this.win.webContents.on('before-input-event', this.onBeforeInputEvent); @@ -119,21 +147,45 @@ export class MainWindow { const contextMenu = new ContextMenu({}, this.win); contextMenu.reload(); + + this.emit(MAIN_WINDOW_CREATED); } get isReady() { return this.ready; } - get = (ensureCreated?: boolean) => { - if (ensureCreated && !this.win) { - this.init(); - } + get = () => { return this.win; } - getBounds = () => { - return this.win?.getContentBounds(); + show = () => { + if (this.win) { + if (this.win.isVisible()) { + this.win.focus(); + } else { + this.win.show(); + } + } else { + this.init(); + this.show(); + } + } + + getBounds = (): Electron.Rectangle | undefined => { + if (!this.win) { + return undefined; + } + + // 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.win.getSize(); + return {...this.win.getContentBounds(), width: size[0], height: size[1]}; + } + + return this.win.getContentBounds(); } focusThreeDotMenu = () => { @@ -143,6 +195,27 @@ export class MainWindow { } } + onBrowserWindow = this.win?.on; + + sendToRenderer = (channel: string, ...args: unknown[]) => { + this.sendToRendererWithRetry(3, channel, ...args); + } + + private sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => { + if (!this.win || !this.isReady) { + if (maxRetries > 0) { + log.debug(`Can't send ${channel}, will retry`); + setTimeout(() => { + this.sendToRendererWithRetry(maxRetries - 1, channel, ...args); + }, SECOND); + } else { + log.error(`Unable to send the message to the main window for message type ${channel}`); + } + return; + } + this.win.webContents.send(channel, ...args); + } + private shouldStartFullScreen = () => { if (global?.args?.fullscreen !== undefined) { return global.args.fullscreen; @@ -328,6 +401,64 @@ export class MainWindow { }); } + private onMaximize = () => { + this.win?.webContents.send(MAXIMIZE_CHANGE, true); + this.emit(MAIN_WINDOW_RESIZED, this.getBounds()); + } + + private onUnmaximize = () => { + this.win?.webContents.send(MAXIMIZE_CHANGE, false); + this.emit(MAIN_WINDOW_RESIZED, this.getBounds()); + } + + /** + * Resizing code + */ + + private onWillResize = (event: Event, newBounds: Electron.Rectangle) => { + log.silly('onWillResize', newBounds); + + /** + * Fixes an issue on win11 related to Snap where the first "will-resize" event would return the same bounds + * causing the "resize" event to not fire + */ + const prevBounds = this.getBounds(); + if (prevBounds?.height === newBounds.height && prevBounds?.width === newBounds.width) { + log.debug('prevented resize'); + event.preventDefault(); + return; + } + + if (this.isResizing) { + log.debug('prevented resize'); + event.preventDefault(); + return; + } + + this.isResizing = true; + this.emit(MAIN_WINDOW_RESIZED, newBounds); + } + + private onResize = () => { + log.silly('onResize'); + + if (this.isResizing) { + return; + } + this.emit(MAIN_WINDOW_RESIZED, this.getBounds()); + } + + private onResized = () => { + log.debug('onResized'); + + this.emit(MAIN_WINDOW_RESIZED, this.getBounds()); + this.isResizing = false; + } + + private handleViewFinishedResizing = () => { + this.isResizing = false; + } + /** * Server Manager update handler */ diff --git a/src/main/windows/settingsWindow.ts b/src/main/windows/settingsWindow.ts index f02ac97e..eb7481cc 100644 --- a/src/main/windows/settingsWindow.ts +++ b/src/main/windows/settingsWindow.ts @@ -7,6 +7,8 @@ import {SHOW_SETTINGS_WINDOW} from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; +import ViewManager from 'main/views/viewManager'; + import ContextMenu from '../contextMenu'; import {getLocalPreload, getLocalURLString} from '../utils'; @@ -33,8 +35,12 @@ export class SettingsWindow { return this.win; } + sendToRenderer = (channel: string, ...args: any[]) => { + this.win?.webContents.send(channel, ...args); + } + private create = () => { - const mainWindow = MainWindow.get(true); + const mainWindow = MainWindow.get(); if (!mainWindow) { return; } @@ -67,6 +73,8 @@ export class SettingsWindow { this.win.on('closed', () => { delete this.win; + + ViewManager.focusCurrentView(); }); } } diff --git a/src/main/windows/windowManager.test.js b/src/main/windows/windowManager.test.js deleted file mode 100644 index fcb7920b..00000000 --- a/src/main/windows/windowManager.test.js +++ /dev/null @@ -1,637 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/* eslint-disable max-lines */ -'use strict'; - -import {systemPreferences} from 'electron'; - -import {getTabViewName} from 'common/tabs/TabView'; -import ServerManager from 'common/servers/serverManager'; - -import {getAdjustedWindowBoundaries} from 'main/utils'; - -import ViewManager from '../views/viewManager'; -import LoadingScreen from '../views/loadingScreen'; -import TeamDropdownView from '../views/teamDropdownView'; - -import {WindowManager} from './windowManager'; -import MainWindow from './mainWindow'; -import SettingsWindow from './settingsWindow'; -import CallsWidgetWindow from './callsWidgetWindow'; - -jest.mock('path', () => ({ - resolve: jest.fn(), - join: jest.fn(), -})); - -jest.mock('electron', () => ({ - ipcMain: { - handle: jest.fn(), - on: jest.fn(), - emit: jest.fn(), - }, - app: { - getAppPath: jest.fn(), - quit: jest.fn(), - dock: { - show: jest.fn(), - bounce: jest.fn(), - }, - }, - systemPreferences: { - getUserDefault: jest.fn(), - }, -})); - -jest.mock('common/config', () => ({})); - -jest.mock('common/tabs/TabView', () => ({ - getTabViewName: jest.fn(), - TAB_MESSAGING: 'tab-messaging', -})); -jest.mock('../utils', () => ({ - getAdjustedWindowBoundaries: jest.fn(), - shouldHaveBackBar: jest.fn(), - openScreensharePermissionsSettingsMacOS: jest.fn(), - resetScreensharePermissionsMacOS: jest.fn(), -})); -jest.mock('../views/viewManager', () => ({ - reloadConfiguration: jest.fn(), - showById: jest.fn(), - getCurrentView: jest.fn(), - getView: jest.fn(), - isViewClosed: jest.fn(), - openClosedTab: jest.fn(), - handleDeepLink: jest.fn(), - setLoadingScreenBounds: jest.fn(), - showByName: jest.fn(), - updateMainWindow: jest.fn(), -})); -jest.mock('../CriticalErrorHandler', () => jest.fn()); -jest.mock('../views/loadingScreen', () => ({ - isHidden: jest.fn(), - setBounds: jest.fn(), -})); -jest.mock('../views/teamDropdownView', () => ({ - updateWindowBounds: jest.fn(), -})); -jest.mock('../views/downloadsDropdownView', () => ({ - updateWindowBounds: jest.fn(), -})); -jest.mock('../views/downloadsDropdownMenuView', () => ({ - updateWindowBounds: jest.fn(), -})); -jest.mock('./settingsWindow', () => ({ - show: jest.fn(), - get: jest.fn(), -})); -jest.mock('./mainWindow', () => ({ - get: jest.fn(), -})); -jest.mock('../downloadsManager', () => ({ - getDownloads: () => {}, -})); - -jest.mock('common/servers/serverManager', () => ({ - getAllServers: jest.fn(), - getServer: jest.fn(), - getCurrentServer: jest.fn(), - on: jest.fn(), - lookupTabByURL: jest.fn(), - getOrderedTabsForServer: jest.fn(), - getLastActiveTabForServer: jest.fn(), - getServerLog: () => ({ - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - verbose: jest.fn(), - debug: jest.fn(), - silly: jest.fn(), - }), - getViewLog: () => ({ - error: jest.fn(), - warn: jest.fn(), - info: jest.fn(), - verbose: jest.fn(), - debug: jest.fn(), - silly: jest.fn(), - }), -})); -jest.mock('./callsWidgetWindow', () => ({ - isCallsWidget: jest.fn(), - getURL: jest.fn(), -})); -jest.mock('main/views/webContentEvents', () => ({})); - -describe('main/windows/windowManager', () => { - describe('showMainWindow', () => { - const windowManager = new WindowManager(); - 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(() => { - jest.resetAllMocks(); - }); - - it('should show main window if it exists and focus it if it is already visible', () => { - windowManager.showMainWindow(); - expect(mainWindow.show).toHaveBeenCalled(); - - windowManager.showMainWindow(); - expect(mainWindow.focus).toHaveBeenCalled(); - }); - - it('should open deep link when provided', () => { - windowManager.showMainWindow('mattermost://server-1.com/subpath'); - expect(ViewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath'); - }); - }); - - describe('handleResizeMainWindow', () => { - const windowManager = new WindowManager(); - const view = { - setBounds: jest.fn(), - tab: { - url: 'http://server-1.com', - }, - view: { - webContents: { - getURL: jest.fn(), - }, - }, - }; - const mainWindow = { - getContentBounds: () => ({width: 800, height: 600}), - getSize: () => [1000, 900], - }; - windowManager.teamDropdown = { - updateWindowBounds: jest.fn(), - }; - - beforeEach(() => { - MainWindow.get.mockReturnValue(mainWindow); - jest.useFakeTimers(); - MainWindow.get.mockReturnValue(mainWindow); - ViewManager.getCurrentView.mockReturnValue(view); - getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); - }); - - afterEach(() => { - jest.runAllTimers(); - jest.resetAllMocks(); - jest.runOnlyPendingTimers(); - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - it('should update loading screen and team dropdown bounds', () => { - windowManager.handleResizeMainWindow(); - expect(LoadingScreen.setBounds).toHaveBeenCalled(); - expect(TeamDropdownView.updateWindowBounds).toHaveBeenCalled(); - }); - - 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(), - }, - }, - }; - const mainWindow = { - getContentBounds: () => ({width: 1000, height: 900}), - getSize: () => [1000, 900], - }; - - beforeEach(() => { - MainWindow.get.mockReturnValue(mainWindow); - LoadingScreen.isHidden.mockReturnValue(true); - ViewManager.getCurrentView.mockReturnValue(view); - getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); - }); - - afterEach(() => { - windowManager.isResizing = false; - jest.clearAllMocks(); - }); - - it('should update loading screen and team dropdown bounds', () => { - const event = {preventDefault: jest.fn()}; - windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); - expect(LoadingScreen.setBounds).toHaveBeenCalled(); - expect(TeamDropdownView.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(event.preventDefault).toHaveBeenCalled(); - 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('handleResizedMainWindow', () => { - const windowManager = new WindowManager(); - const view = { - setBounds: jest.fn(), - tab: { - url: 'http://server-1.com', - }, - view: { - webContents: { - getURL: jest.fn(), - }, - }, - }; - const mainWindow = { - getContentBounds: () => ({width: 800, height: 600}), - getSize: () => [1000, 900], - }; - - beforeEach(() => { - ViewManager.getCurrentView.mockReturnValue(view); - MainWindow.get.mockReturnValue(mainWindow); - getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); - }); - - afterEach(() => { - windowManager.isResizing = true; - jest.resetAllMocks(); - }); - - it('should use getContentBounds when the platform is different to linux', () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { - value: 'windows', - }); - - windowManager.handleResizedMainWindow(); - - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); - - expect(windowManager.isResizing).toBe(false); - 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.handleResizedMainWindow(); - - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); - - expect(windowManager.isResizing).toBe(false); - expect(view.setBounds).toHaveBeenCalledWith({width: 1000, height: 900}); - }); - }); - - describe('restoreMain', () => { - const windowManager = new WindowManager(); - const mainWindow = { - isVisible: jest.fn(), - isMinimized: jest.fn(), - restore: jest.fn(), - show: jest.fn(), - focus: jest.fn(), - }; - - beforeEach(() => { - MainWindow.get.mockReturnValue(mainWindow); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should restore main window if minimized', () => { - mainWindow.isMinimized.mockReturnValue(true); - windowManager.restoreMain(); - expect(mainWindow.restore).toHaveBeenCalled(); - }); - - it('should show main window if not visible or minimized', () => { - mainWindow.isVisible.mockReturnValue(false); - mainWindow.isMinimized.mockReturnValue(false); - windowManager.restoreMain(); - expect(mainWindow.show).toHaveBeenCalled(); - }); - - it('should focus main window if visible and not minimized', () => { - mainWindow.isVisible.mockReturnValue(true); - mainWindow.isMinimized.mockReturnValue(false); - windowManager.restoreMain(); - expect(mainWindow.focus).toHaveBeenCalled(); - }); - - it('should focus settings window regardless of main window state if it exists', () => { - const settingsWindow = {focus: jest.fn()}; - SettingsWindow.get.mockReturnValue(settingsWindow); - - mainWindow.isVisible.mockReturnValue(false); - mainWindow.isMinimized.mockReturnValue(false); - windowManager.restoreMain(); - expect(settingsWindow.focus).toHaveBeenCalled(); - settingsWindow.focus.mockClear(); - - mainWindow.isVisible.mockReturnValue(true); - mainWindow.isMinimized.mockReturnValue(false); - windowManager.restoreMain(); - expect(settingsWindow.focus).toHaveBeenCalled(); - settingsWindow.focus.mockClear(); - - mainWindow.isVisible.mockReturnValue(false); - mainWindow.isMinimized.mockReturnValue(true); - windowManager.restoreMain(); - expect(settingsWindow.focus).toHaveBeenCalled(); - settingsWindow.focus.mockClear(); - - mainWindow.isVisible.mockReturnValue(true); - mainWindow.isMinimized.mockReturnValue(true); - windowManager.restoreMain(); - expect(settingsWindow.focus).toHaveBeenCalled(); - settingsWindow.focus.mockClear(); - }); - }); - - describe('handleDoubleClick', () => { - const windowManager = new WindowManager(); - const mainWindow = { - isMinimized: jest.fn(), - restore: jest.fn(), - minimize: jest.fn(), - isMaximized: jest.fn(), - unmaximize: jest.fn(), - maximize: jest.fn(), - }; - const settingsWindow = { - isMinimized: jest.fn(), - restore: jest.fn(), - minimize: jest.fn(), - isMaximized: jest.fn(), - unmaximize: jest.fn(), - maximize: jest.fn(), - }; - - beforeEach(() => { - systemPreferences.getUserDefault.mockReturnValue('Maximize'); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should do nothing when the windows arent set', () => { - windowManager.handleDoubleClick(null, 'settings'); - expect(settingsWindow.isMaximized).not.toHaveBeenCalled(); - - windowManager.handleDoubleClick(); - expect(mainWindow.isMaximized).not.toHaveBeenCalled(); - }); - - it('should maximize when not maximized and vice versa', () => { - MainWindow.get.mockReturnValue(mainWindow); - - mainWindow.isMaximized.mockReturnValue(false); - windowManager.handleDoubleClick(); - expect(mainWindow.maximize).toHaveBeenCalled(); - - mainWindow.isMaximized.mockReturnValue(true); - windowManager.handleDoubleClick(); - expect(mainWindow.unmaximize).toHaveBeenCalled(); - }); - - it('mac - should minimize when not minimized and vice versa when setting is set', () => { - const originalPlatform = process.platform; - Object.defineProperty(process, 'platform', { - value: 'darwin', - }); - - systemPreferences.getUserDefault.mockReturnValue('Minimize'); - SettingsWindow.get.mockReturnValue(settingsWindow); - - settingsWindow.isMinimized.mockReturnValue(false); - windowManager.handleDoubleClick(null, 'settings'); - expect(settingsWindow.minimize).toHaveBeenCalled(); - - settingsWindow.isMinimized.mockReturnValue(true); - windowManager.handleDoubleClick(null, 'settings'); - expect(settingsWindow.restore).toHaveBeenCalled(); - - Object.defineProperty(process, 'platform', { - value: originalPlatform, - }); - }); - }); - - describe('switchServer', () => { - const windowManager = new WindowManager(); - const views = new Map([ - ['tab-1', {id: 'tab-1'}], - ['tab-2', {id: 'tab-2'}], - ['tab-3', {id: 'tab-3'}], - ]); - - beforeEach(() => { - jest.useFakeTimers(); - const server1 = { - id: 'server-1', - }; - const server2 = { - id: 'server-2', - }; - ServerManager.getServer.mockImplementation((name) => { - switch (name) { - case 'server-1': - return server1; - case 'server-2': - return server2; - default: - return undefined; - } - }); - ViewManager.getView.mockImplementation((viewId) => views.get(viewId)); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - afterAll(() => { - jest.runOnlyPendingTimers(); - jest.clearAllTimers(); - jest.useRealTimers(); - }); - - it('should do nothing if cannot find the server', () => { - windowManager.switchServer('server-3'); - expect(getTabViewName).not.toBeCalled(); - expect(ViewManager.showById).not.toBeCalled(); - }); - - it('should show first open tab in order when last active not defined', () => { - ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); - windowManager.switchServer('server-1'); - expect(ViewManager.showById).toHaveBeenCalledWith('tab-3'); - }); - - it('should show last active tab of chosen server', () => { - ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'}); - windowManager.switchServer('server-2'); - expect(ViewManager.showById).toHaveBeenCalledWith('tab-2'); - }); - - it('should wait for view to exist if specified', () => { - ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); - views.delete('tab-3'); - windowManager.switchServer('server-1', true); - expect(ViewManager.showById).not.toBeCalled(); - - jest.advanceTimersByTime(200); - expect(ViewManager.showById).not.toBeCalled(); - - views.set('tab-3', {}); - jest.advanceTimersByTime(200); - expect(ViewManager.showById).toBeCalledWith('tab-3'); - }); - }); - - describe('selectTab', () => { - const windowManager = new WindowManager(); - windowManager.switchTab = jest.fn(); - - beforeEach(() => { - const tabs = [ - { - id: 'tab-1', - type: 'tab-1', - isOpen: false, - }, - { - id: 'tab-2', - type: 'tab-2', - isOpen: true, - }, - { - id: 'tab-3', - type: 'tab-3', - isOpen: true, - }, - ]; - ServerManager.getOrderedTabsForServer.mockReturnValue(tabs); - }); - - afterEach(() => { - jest.resetAllMocks(); - }); - - it('should select next server when open', () => { - ViewManager.getCurrentView.mockReturnValue({ - tab: { - server: { - id: 'server-1', - }, - type: 'tab-3', - }, - }); - - windowManager.selectTab((order) => order + 1); - expect(windowManager.switchTab).toBeCalledWith('tab-2'); - }); - - it('should select previous server when open', () => { - ViewManager.getCurrentView.mockReturnValue({ - tab: { - server: { - id: 'server-1', - }, - type: 'tab-2', - }, - }); - - windowManager.selectTab((order, length) => (length + (order - 1))); - expect(windowManager.switchTab).toBeCalledWith('tab-3'); - }); - - it('should skip over closed tab', () => { - ViewManager.getCurrentView.mockReturnValue({ - tab: { - server: { - id: 'server-1', - }, - type: 'tab-2', - }, - }); - windowManager.selectTab((order) => order + 1); - expect(windowManager.switchTab).toBeCalledWith('tab-3'); - }); - }); - - describe('getServerURLFromWebContentsId', () => { - const windowManager = new WindowManager(); - - it('should return calls widget URL', () => { - ViewManager.getView.mockReturnValue({name: 'server-1_tab-messaging'}); - CallsWidgetWindow.getURL.mockReturnValue('http://server-1.com'); - CallsWidgetWindow.isCallsWidget.mockReturnValue(true); - expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe('http://server-1.com'); - }); - }); -}); diff --git a/src/main/windows/windowManager.ts b/src/main/windows/windowManager.ts deleted file mode 100644 index f751f33e..00000000 --- a/src/main/windows/windowManager.ts +++ /dev/null @@ -1,426 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -/* eslint-disable max-lines */ - -import {BrowserWindow, systemPreferences, ipcMain, IpcMainEvent} from 'electron'; - -import { - MAXIMIZE_CHANGE, - GET_DARK_MODE, - UPDATE_SHORTCUT_MENU, - RESIZE_MODAL, - VIEW_FINISHED_RESIZING, - WINDOW_CLOSE, - WINDOW_MAXIMIZE, - WINDOW_MINIMIZE, - WINDOW_RESTORE, - DOUBLE_CLICK_ON_WINDOW, -} from 'common/communication'; -import {Logger} from 'common/log'; -import {SECOND} from 'common/utils/constants'; -import Config from 'common/config'; - -import ServerManager from 'common/servers/serverManager'; - -import { - getAdjustedWindowBoundaries, - shouldHaveBackBar, -} from '../utils'; - -import ViewManager from '../views/viewManager'; -import LoadingScreen from '../views/loadingScreen'; -import {MattermostView} from '../views/MattermostView'; -import TeamDropdownView from '../views/teamDropdownView'; -import DownloadsDropdownView from '../views/downloadsDropdownView'; -import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView'; - -import MainWindow from './mainWindow'; -import CallsWidgetWindow from './callsWidgetWindow'; -import SettingsWindow from './settingsWindow'; - -// singleton module to manage application's windows - -const log = new Logger('WindowManager'); - -export class WindowManager { - private isResizing: boolean; - - constructor() { - this.isResizing = false; - - ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode); - ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); - ipcMain.on(WINDOW_CLOSE, this.handleClose); - ipcMain.on(WINDOW_MAXIMIZE, this.handleMaximize); - ipcMain.on(WINDOW_MINIMIZE, this.handleMinimize); - ipcMain.on(WINDOW_RESTORE, this.handleRestore); - ipcMain.on(DOUBLE_CLICK_ON_WINDOW, this.handleDoubleClick); - } - - showMainWindow = (deeplinkingURL?: string | URL) => { - log.debug('showMainWindow', deeplinkingURL); - - const mainWindow = MainWindow.get(); - if (mainWindow) { - if (mainWindow.isVisible()) { - mainWindow.focus(); - } else { - mainWindow.show(); - } - } else { - this.createMainWindow(); - } - - if (deeplinkingURL) { - ViewManager.handleDeepLink(deeplinkingURL); - } - } - - private createMainWindow = () => { - const mainWindow = MainWindow.get(true); - if (!mainWindow) { - return; - } - - // 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', ViewManager.focusCurrentView); - mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen')); - mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen')); - - this.initializeViewManager(); - TeamDropdownView.init(); - DownloadsDropdownView.init(); - DownloadsDropdownMenuView.init(); - } - - // max retries allows the message to get to the renderer even if it is sent while the app is starting up. - private sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => { - const mainWindow = MainWindow.get(); - - if (!mainWindow || !MainWindow.isReady) { - if (maxRetries > 0) { - log.debug(`Can't send ${channel}, will retry`); - setTimeout(() => { - this.sendToRendererWithRetry(maxRetries - 1, channel, ...args); - }, SECOND); - } else { - log.error(`Unable to send the message to the main window for message type ${channel}`); - } - return; - } - mainWindow.webContents.send(channel, ...args); - SettingsWindow.get()?.webContents.send(channel, ...args); - } - - sendToRenderer = (channel: string, ...args: unknown[]) => { - this.sendToRendererWithRetry(3, channel, ...args); - } - - restoreMain = () => { - log.info('restoreMain'); - if (!MainWindow.get()) { - this.showMainWindow(); - } - const mainWindow = MainWindow.get(); - if (!mainWindow) { - throw new Error('Main window does not exist'); - } - if (!mainWindow.isVisible() || mainWindow.isMinimized()) { - if (mainWindow.isMinimized()) { - mainWindow.restore(); - } else { - mainWindow.show(); - } - const settingsWindow = SettingsWindow.get(); - if (settingsWindow) { - settingsWindow.focus(); - } else { - mainWindow.focus(); - } - } else if (SettingsWindow.get()) { - SettingsWindow.get()?.focus(); - } else { - mainWindow.focus(); - } - } - - private initializeViewManager = () => { - ViewManager.init(); - } - - switchServer = (serverId: string, waitForViewToExist = false) => { - ServerManager.getServerLog(serverId, 'WindowManager').debug('switchServer'); - this.showMainWindow(); - const server = ServerManager.getServer(serverId); - if (!server) { - ServerManager.getServerLog(serverId, 'WindowManager').error('Cannot find server in config'); - return; - } - const nextTab = ServerManager.getLastActiveTabForServer(serverId); - if (waitForViewToExist) { - const timeout = setInterval(() => { - if (ViewManager.getView(nextTab.id)) { - ViewManager.showById(nextTab.id); - clearTimeout(timeout); - } - }, 100); - } else { - ViewManager.showById(nextTab.id); - } - ipcMain.emit(UPDATE_SHORTCUT_MENU); - } - - switchTab = (tabId: string) => { - ViewManager.showById(tabId); - } - - /** - * ID fetching - */ - - getServerURLFromWebContentsId = (id: number) => { - if (CallsWidgetWindow.isCallsWidget(id)) { - return CallsWidgetWindow.getURL(); - } - - return ViewManager.getViewByWebContentsId(id)?.tab.server.url; - } - - /** - * Tab switching - */ - - selectNextTab = () => { - this.selectTab((order) => order + 1); - } - - selectPreviousTab = () => { - this.selectTab((order, length) => (length + (order - 1))); - } - - private selectTab = (fn: (order: number, length: number) => number) => { - const currentView = ViewManager.getCurrentView(); - if (!currentView) { - return; - } - - const currentTeamTabs = ServerManager.getOrderedTabsForServer(currentView.tab.server.id).map((tab, index) => ({tab, index})); - const filteredTabs = currentTeamTabs?.filter((tab) => tab.tab.isOpen); - const currentTab = currentTeamTabs?.find((tab) => tab.tab.type === currentView.tab.type); - if (!currentTeamTabs || !currentTab || !filteredTabs) { - return; - } - - let currentOrder = currentTab.index; - let nextIndex = -1; - while (nextIndex === -1) { - const nextOrder = (fn(currentOrder, currentTeamTabs.length) % currentTeamTabs.length); - nextIndex = filteredTabs.findIndex((tab) => tab.index === nextOrder); - currentOrder = nextOrder; - } - - const newTab = filteredTabs[nextIndex].tab; - this.switchTab(newTab.id); - } - - /***************** - * MAIN WINDOW EVENT HANDLERS - *****************/ - - private handleMaximizeMainWindow = () => { - DownloadsDropdownView.updateWindowBounds(); - DownloadsDropdownMenuView.updateWindowBounds(); - this.sendToRenderer(MAXIMIZE_CHANGE, true); - } - - private handleUnmaximizeMainWindow = () => { - DownloadsDropdownView.updateWindowBounds(); - DownloadsDropdownMenuView.updateWindowBounds(); - this.sendToRenderer(MAXIMIZE_CHANGE, false); - } - - private handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => { - log.silly('handleWillResizeMainWindow'); - - if (!MainWindow.get()) { - return; - } - - /** - * Fixes an issue on win11 related to Snap where the first "will-resize" event would return the same bounds - * causing the "resize" event to not fire - */ - const prevBounds = this.getBounds(); - if (prevBounds.height === newBounds.height && prevBounds.width === newBounds.width) { - return; - } - - if (this.isResizing && LoadingScreen.isHidden() && ViewManager.getCurrentView()) { - log.debug('prevented resize'); - event.preventDefault(); - return; - } - - this.throttledWillResize(newBounds); - LoadingScreen.setBounds(); - TeamDropdownView.updateWindowBounds(); - DownloadsDropdownView.updateWindowBounds(); - DownloadsDropdownMenuView.updateWindowBounds(); - ipcMain.emit(RESIZE_MODAL, null, newBounds); - } - - private handleResizedMainWindow = () => { - log.silly('handleResizedMainWindow'); - - const bounds = this.getBounds(); - this.throttledWillResize(bounds); - ipcMain.emit(RESIZE_MODAL, null, bounds); - TeamDropdownView.updateWindowBounds(); - DownloadsDropdownView.updateWindowBounds(); - DownloadsDropdownMenuView.updateWindowBounds(); - this.isResizing = false; - } - - private throttledWillResize = (newBounds: Electron.Rectangle) => { - log.silly('throttledWillResize', {newBounds}); - - this.isResizing = true; - this.setCurrentViewBounds(newBounds); - } - - private handleResizeMainWindow = () => { - log.silly('handleResizeMainWindow'); - - if (!MainWindow.get()) { - return; - } - if (this.isResizing) { - return; - } - - const bounds = this.getBounds(); - - // Another workaround since the window doesn't update properly under Linux for some reason - // See above comment - setTimeout(this.setCurrentViewBounds, 10, bounds); - - LoadingScreen.setBounds(); - TeamDropdownView.updateWindowBounds(); - DownloadsDropdownView.updateWindowBounds(); - DownloadsDropdownMenuView.updateWindowBounds(); - ipcMain.emit(RESIZE_MODAL, null, bounds); - }; - - private setCurrentViewBounds = (bounds: {width: number; height: number}) => { - log.debug('setCurrentViewBounds', {bounds}); - - const currentView = ViewManager.getCurrentView(); - if (currentView) { - const adjustedBounds = getAdjustedWindowBoundaries(bounds.width, bounds.height, shouldHaveBackBar(currentView.tab.url, currentView.currentURL)); - this.setBoundsFunction(currentView, adjustedBounds); - } - } - - private setBoundsFunction = (currentView: MattermostView, bounds: Electron.Rectangle) => { - log.silly('setBoundsFunction', bounds.width, bounds.height); - currentView.setBounds(bounds); - }; - - private getBounds = () => { - let bounds; - - const mainWindow = MainWindow.get(); - if (mainWindow) { - // 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 = mainWindow.getSize(); - bounds = {width: size[0], height: size[1]}; - } else { - bounds = mainWindow.getContentBounds(); - } - } - - return bounds as Electron.Rectangle; - } - - /***************** - * IPC EVENT HANDLERS - *****************/ - - private handleGetDarkMode = () => { - return Config.darkMode; - } - - private handleViewFinishedResizing = () => { - this.isResizing = false; - } - - private handleClose = () => { - const focused = BrowserWindow.getFocusedWindow(); - focused?.close(); - } - private handleMaximize = () => { - const focused = BrowserWindow.getFocusedWindow(); - if (focused) { - focused.maximize(); - } - } - private handleMinimize = () => { - const focused = BrowserWindow.getFocusedWindow(); - if (focused) { - focused.minimize(); - } - } - private handleRestore = () => { - const focused = BrowserWindow.getFocusedWindow(); - if (focused) { - focused.restore(); - } - if (focused?.isFullScreen()) { - focused.setFullScreen(false); - } - } - - handleDoubleClick = (e: IpcMainEvent, windowType?: string) => { - log.debug('handleDoubleClick', windowType); - - let action = 'Maximize'; - if (process.platform === 'darwin') { - action = systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string'); - } - const win = (windowType === 'settings') ? SettingsWindow.get() : MainWindow.get(); - if (!win) { - return; - } - switch (action) { - case 'Minimize': - if (win.isMinimized()) { - win.restore(); - } else { - win.minimize(); - } - break; - case 'Maximize': - default: - if (win.isMaximized()) { - win.unmaximize(); - } else { - win.maximize(); - } - break; - } - } -} - -const windowManager = new WindowManager(); -export default windowManager;