Remove WindowManager, separate functionality into smaller modules (#2682)

* Move sendToRenderer to respective singletons

* Move to using ViewManager call for getting view by webContentsId

* Move show and create logic to main window, handle deep linking seperately

* Move resizing logic and event handing to mainWindow

* Move server switching logic to main/app

* Move tab switching logic to main/app, rely on showById for most usage

* Migrate remaining functions, remove windowManager objects, set up imports for self-contained singletons

* Fix E2E tests

* Update src/main/app/servers.ts

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>

---------

Co-authored-by: Elias Nahum <nahumhbl@gmail.com>
This commit is contained in:
Devin Binnie 2023-04-19 11:04:26 -04:00 committed by GitHub
parent a141d3cde4
commit f4f4511cc7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
57 changed files with 1089 additions and 1640 deletions

View file

@ -60,21 +60,12 @@ describe('file_menu/dropdown', function desc() {
}); });
} }
it('MM-T804 Preferences in Menu Bar open the Settings page', async () => { if (process.platform !== 'darwin') {
const mainWindow = this.app.windows().find((window) => window.url().includes('index')); it('MM-T804 Preferences in Menu Bar open the Settings page', async () => {
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]);
//Opening the menu bar //Opening the menu bar
robot.keyTap('alt'); const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
robot.keyTap('enter'); mainWindow.should.not.be.null;
await mainWindow.click('button.three-dot-menu');
robot.keyTap('f'); robot.keyTap('f');
robot.keyTap('s'); robot.keyTap('s');
robot.keyTap('enter'); robot.keyTap('enter');
@ -82,8 +73,8 @@ describe('file_menu/dropdown', function desc() {
predicate: (window) => window.url().includes('settings'), predicate: (window) => window.url().includes('settings'),
}); });
settingsWindowFromMenu.should.not.be.null; settingsWindowFromMenu.should.not.be.null;
} });
}); }
// TODO: Causes issues on Windows so skipping for Windows // TODO: Causes issues on Windows so skipping for Windows
if (process.platform !== 'win32') { if (process.platform !== 'win32') {

View file

@ -139,6 +139,7 @@ describe('Add Server Modal', function desc() {
name: 'TestTeam', name: 'TestTeam',
url: 'http://example.org/', url: 'http://example.org/',
order: 2, order: 2,
lastActiveTab: 0,
tabs: [ tabs: [
{ {
name: 'TAB_MESSAGING', name: 'TAB_MESSAGING',

View file

@ -86,6 +86,7 @@ describe('Configure Server Modal', function desc() {
url: 'http://example.org/', url: 'http://example.org/',
name: 'TestTeam', name: 'TestTeam',
order: 0, order: 0,
lastActiveTab: 0,
tabs: [ tabs: [
{ {
name: 'TAB_MESSAGING', name: 'TAB_MESSAGING',

View file

@ -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 = 'update-appstate';
export const UPDATE_APPSTATE_TOTALS = 'update-appstate-totals'; export const UPDATE_APPSTATE_TOTALS = 'update-appstate-totals';
export const UPDATE_APPSTATE_FOR_VIEW_ID = 'update-appstate-for-view-id'; 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';

View file

@ -38,19 +38,14 @@ jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(), localizeMessage: jest.fn(),
})); }));
jest.mock('main/tray/tray', () => ({})); jest.mock('main/tray/tray', () => ({}));
jest.mock('main/windows/windowManager', () => ({
showMainWindow: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
show: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({ jest.mock('main/views/viewManager', () => ({
getView: jest.fn(), getView: jest.fn(),
getViewByWebContentsId: jest.fn(), getViewByWebContentsId: jest.fn(),
})); }));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
describe('main/app/app', () => { describe('main/app/app', () => {
describe('handleAppWillFinishLaunching', () => { describe('handleAppWillFinishLaunching', () => {

View file

@ -10,7 +10,6 @@ import updateManager from 'main/autoUpdater';
import CertificateStore from 'main/certificateStore'; import CertificateStore from 'main/certificateStore';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {destroyTray} from 'main/tray/tray'; import {destroyTray} from 'main/tray/tray';
import WindowManager from 'main/windows/windowManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@ -30,8 +29,10 @@ export function handleAppSecondInstance(event: Event, argv: string[]) {
// Protocol handler for win32 // Protocol handler for win32
// argv: An array of the second instances (command line / deep linked) arguments // argv: An array of the second instances (command line / deep linked) arguments
const deeplinkingUrl = getDeeplinkingURL(argv); const deeplinkingURL = getDeeplinkingURL(argv);
WindowManager.showMainWindow(deeplinkingUrl); if (deeplinkingURL) {
openDeepLink(deeplinkingURL);
}
} }
export function handleAppWindowAllClosed() { export function handleAppWindowAllClosed() {

View file

@ -10,7 +10,7 @@ import {setLoggingLevel} from 'common/log';
import {handleConfigUpdate} from 'main/app/config'; import {handleConfigUpdate} from 'main/app/config';
import {handleMainWindowIsShown} from 'main/app/intercom'; import {handleMainWindowIsShown} from 'main/app/intercom';
import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow';
import AutoLauncher from 'main/AutoLauncher'; import AutoLauncher from 'main/AutoLauncher';
jest.mock('electron', () => ({ jest.mock('electron', () => ({
@ -47,8 +47,7 @@ jest.mock('main/views/viewManager', () => ({
reloadConfiguration: jest.fn(), reloadConfiguration: jest.fn(),
})); }));
jest.mock('main/views/loadingScreen', () => ({})); jest.mock('main/views/loadingScreen', () => ({}));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
handleUpdateConfig: jest.fn(),
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
@ -65,11 +64,11 @@ describe('main/app/config', () => {
it('should reload renderer config only when app is ready', () => { it('should reload renderer config only when app is ready', () => {
handleConfigUpdate({}); handleConfigUpdate({});
expect(WindowManager.sendToRenderer).not.toBeCalled(); expect(MainWindow.sendToRenderer).not.toBeCalled();
app.isReady.mockReturnValue(true); app.isReady.mockReturnValue(true);
handleConfigUpdate({}); handleConfigUpdate({});
expect(WindowManager.sendToRenderer).toBeCalledWith(RELOAD_CONFIGURATION); expect(MainWindow.sendToRenderer).toBeCalledWith(RELOAD_CONFIGURATION);
}); });
it('should set download path if applicable', () => { it('should set download path if applicable', () => {

View file

@ -13,7 +13,8 @@ import AutoLauncher from 'main/AutoLauncher';
import {setUnreadBadgeSetting} from 'main/badge'; import {setUnreadBadgeSetting} from 'main/badge';
import {refreshTrayImages} from 'main/tray/tray'; import {refreshTrayImages} from 'main/tray/tray';
import LoadingScreen from 'main/views/loadingScreen'; 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 {handleMainWindowIsShown} from './intercom';
import {handleUpdateMenuEvent, updateSpellCheckerLocales} from './utils'; import {handleUpdateMenuEvent, updateSpellCheckerLocales} from './utils';
@ -72,7 +73,8 @@ export function handleConfigUpdate(newConfig: CombinedConfig) {
} }
if (app.isReady()) { if (app.isReady()) {
WindowManager.sendToRenderer(RELOAD_CONFIGURATION); MainWindow.sendToRenderer(RELOAD_CONFIGURATION);
SettingsWindow.sendToRenderer(RELOAD_CONFIGURATION);
} }
setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge); setUnreadBadgeSetting(newConfig && newConfig.showUnreadBadge);
@ -111,7 +113,8 @@ export function handleDarkModeChange(darkMode: boolean) {
log.debug('handleDarkModeChange', darkMode); log.debug('handleDarkModeChange', darkMode);
refreshTrayImages(Config.trayIconTheme); refreshTrayImages(Config.trayIconTheme);
WindowManager.sendToRenderer(DARK_MODE_CHANGE, darkMode); MainWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode);
SettingsWindow.sendToRenderer(DARK_MODE_CHANGE, darkMode);
LoadingScreen.setDarkMode(darkMode); LoadingScreen.setDarkMode(darkMode);
ipcMain.emit(EMIT_CONFIGURATION, true, Config.data); ipcMain.emit(EMIT_CONFIGURATION, true, Config.data);

View file

@ -5,6 +5,11 @@
import {initialize} from './initialize'; 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) { if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept(); module.hot.accept();
} }

View file

@ -9,8 +9,8 @@ import Config from 'common/config';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import parseArgs from 'main/ParseArgs'; import parseArgs from 'main/ParseArgs';
import ViewManager from 'main/views/viewManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import {initialize} from './initialize'; import {initialize} from './initialize';
import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils'; import {clearAppCache, getDeeplinkingURL, wasUpdated} from './utils';
@ -119,6 +119,7 @@ jest.mock('main/app/config', () => ({
jest.mock('main/app/intercom', () => ({ jest.mock('main/app/intercom', () => ({
handleMainWindowIsShown: jest.fn(), handleMainWindowIsShown: jest.fn(),
})); }));
jest.mock('main/app/servers', () => ({}));
jest.mock('main/app/utils', () => ({ jest.mock('main/app/utils', () => ({
clearAppCache: jest.fn(), clearAppCache: jest.fn(),
getDeeplinkingURL: jest.fn(), getDeeplinkingURL: jest.fn(),
@ -168,18 +169,17 @@ jest.mock('main/UserActivityMonitor', () => ({
jest.mock('main/windows/callsWidgetWindow', () => ({ jest.mock('main/windows/callsWidgetWindow', () => ({
isCallsWidget: jest.fn(), isCallsWidget: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/views/viewManager', () => ({
showMainWindow: jest.fn(), getViewByWebContentsId: jest.fn(),
sendToRenderer: jest.fn(), handleDeepLink: jest.fn(),
getServerNameByWebContentsId: jest.fn(),
getServerURLFromWebContentsId: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/windows/settingsWindow', () => ({ jest.mock('main/windows/settingsWindow', () => ({
show: jest.fn(), show: jest.fn(),
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
show: jest.fn(),
sendToRenderer: jest.fn(),
})); }));
const originalProcess = process; const originalProcess = process;
describe('main/app/initialize', () => { describe('main/app/initialize', () => {
@ -272,11 +272,17 @@ describe('main/app/initialize', () => {
value: originalPlatform, 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 () => { 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(); let callback = jest.fn();
session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => { session.defaultSession.setPermissionRequestHandler.mockImplementation((cb) => {
cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback); cb({id: 1, getURL: () => 'http://server-1.com'}, 'bad-permission', callback);

View file

@ -36,6 +36,12 @@ import {
GET_ORDERED_SERVERS, GET_ORDERED_SERVERS,
GET_ORDERED_TABS_FOR_SERVER, GET_ORDERED_TABS_FOR_SERVER,
SERVERS_URL_MODIFIED, SERVERS_URL_MODIFIED,
GET_DARK_MODE,
WINDOW_CLOSE,
WINDOW_MAXIMIZE,
WINDOW_MINIMIZE,
WINDOW_RESTORE,
DOUBLE_CLICK_ON_WINDOW,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@ -59,7 +65,6 @@ import {refreshTrayImages, setupTray} from 'main/tray/tray';
import UserActivityMonitor from 'main/UserActivityMonitor'; import UserActivityMonitor from 'main/UserActivityMonitor';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {protocols} from '../../../electron-builder.json'; import {protocols} from '../../../electron-builder.json';
@ -84,22 +89,21 @@ import {
import { import {
handleMainWindowIsShown, handleMainWindowIsShown,
handleAppVersion, handleAppVersion,
handleCloseTab,
handleEditServerModal,
handleMentionNotification, handleMentionNotification,
handleNewServerModal,
handleOpenAppMenu, handleOpenAppMenu,
handleOpenTab,
handleQuit, handleQuit,
handleRemoveServerModal,
handleSelectDownload, handleSelectDownload,
handleSwitchServer,
handleSwitchTab,
handlePingDomain, handlePingDomain,
handleGetOrderedServers,
handleGetOrderedTabsForServer,
handleGetLastActive,
} from './intercom'; } from './intercom';
import {
handleEditServerModal,
handleNewServerModal,
handleRemoveServerModal,
switchServer,
} from './servers';
import {
handleCloseTab, handleGetLastActive, handleGetOrderedTabsForServer, handleOpenTab,
} from './tabs';
import { import {
clearAppCache, clearAppCache,
getDeeplinkingURL, getDeeplinkingURL,
@ -111,6 +115,14 @@ import {
migrateMacAppStore, migrateMacAppStore,
updateServerInfos, updateServerInfos,
} from './utils'; } from './utils';
import {
handleClose,
handleDoubleClick,
handleGetDarkMode,
handleMaximize,
handleMinimize,
handleRestore,
} from './windows';
export const mainProtocol = protocols?.[0]?.schemes?.[0]; export const mainProtocol = protocols?.[0]?.schemes?.[0];
@ -203,7 +215,7 @@ function initializeAppEventListeners() {
app.on('second-instance', handleAppSecondInstance); app.on('second-instance', handleAppSecondInstance);
app.on('window-all-closed', handleAppWindowAllClosed); app.on('window-all-closed', handleAppWindowAllClosed);
app.on('browser-window-created', handleAppBrowserWindowCreated); app.on('browser-window-created', handleAppBrowserWindowCreated);
app.on('activate', () => WindowManager.showMainWindow()); app.on('activate', () => MainWindow.show());
app.on('before-quit', handleAppBeforeQuit); app.on('before-quit', handleAppBeforeQuit);
app.on('certificate-error', handleAppCertificateError); app.on('certificate-error', handleAppCertificateError);
app.on('select-client-certificate', CertificateManager.handleSelectCertificate); app.on('select-client-certificate', CertificateManager.handleSelectCertificate);
@ -267,8 +279,8 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu); ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu);
} }
ipcMain.on(SWITCH_SERVER, handleSwitchServer); ipcMain.on(SWITCH_SERVER, (event, serverId) => switchServer(serverId));
ipcMain.on(SWITCH_TAB, handleSwitchTab); ipcMain.on(SWITCH_TAB, (event, viewId) => ViewManager.showById(viewId));
ipcMain.on(CLOSE_TAB, handleCloseTab); ipcMain.on(CLOSE_TAB, handleCloseTab);
ipcMain.on(OPEN_TAB, handleOpenTab); ipcMain.on(OPEN_TAB, handleOpenTab);
@ -289,8 +301,15 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(UPDATE_SERVER_ORDER, (event, serverOrder) => ServerManager.updateServerOrder(serverOrder)); ipcMain.on(UPDATE_SERVER_ORDER, (event, serverOrder) => ServerManager.updateServerOrder(serverOrder));
ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, tabOrder) => ServerManager.updateTabOrder(serverId, tabOrder)); ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, tabOrder) => ServerManager.updateTabOrder(serverId, tabOrder));
ipcMain.handle(GET_LAST_ACTIVE, handleGetLastActive); 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_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() { async function initializeAfterAppReady() {
@ -364,6 +383,9 @@ async function initializeAfterAppReady() {
catch((err) => log.error('An error occurred: ', err)); catch((err) => log.error('An error occurred: ', err));
} }
initCookieManager(defaultSession);
MainWindow.show();
let deeplinkingURL; let deeplinkingURL;
// Protocol handler for win32 // Protocol handler for win32
@ -371,13 +393,12 @@ async function initializeAfterAppReady() {
const args = process.argv.slice(1); const args = process.argv.slice(1);
if (Array.isArray(args) && args.length > 0) { if (Array.isArray(args) && args.length > 0) {
deeplinkingURL = getDeeplinkingURL(args); deeplinkingURL = getDeeplinkingURL(args);
if (deeplinkingURL) {
ViewManager.handleDeepLink(deeplinkingURL);
}
} }
} }
initCookieManager(defaultSession);
WindowManager.showMainWindow(deeplinkingURL);
// listen for status updates and pass on to renderer // listen for status updates and pass on to renderer
UserActivityMonitor.on('status', (status) => { UserActivityMonitor.on('status', (status) => {
log.debug('UserActivityMonitor.on(status)', status); log.debug('UserActivityMonitor.on(status)', status);
@ -440,7 +461,7 @@ async function initializeAfterAppReady() {
} }
const requestingURL = webContents.getURL(); const requestingURL = webContents.getURL();
const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id); const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url;
if (!serverURL) { if (!serverURL) {
callback(false); callback(false);

View file

@ -1,20 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalURLString, getLocalPreload} from 'main/utils';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import WindowManager from 'main/windows/windowManager';
import { import {
handleOpenTab,
handleCloseTab,
handleNewServerModal,
handleEditServerModal,
handleRemoveServerModal,
handleWelcomeScreenModal, handleWelcomeScreenModal,
handleMainWindowIsShown, handleMainWindowIsShown,
} from './intercom'; } from './intercom';
@ -22,9 +14,6 @@ import {
jest.mock('common/config', () => ({ jest.mock('common/config', () => ({
setServers: jest.fn(), setServers: jest.fn(),
})); }));
jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: jest.fn(),
}));
jest.mock('main/notifications', () => ({})); jest.mock('main/notifications', () => ({}));
jest.mock('common/servers/serverManager', () => ({ jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(), setTabIsOpen: jest.fn(),
@ -45,224 +34,13 @@ jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/views/modalManager', () => ({ jest.mock('main/views/modalManager', () => ({
addModal: jest.fn(), addModal: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({
switchServer: jest.fn(),
switchTab: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
})); }));
jest.mock('./app', () => ({})); 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('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', () => { describe('handleWelcomeScreenModal', () => {
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html'); getLocalURLString.mockReturnValue('/some/index.html');

View file

@ -3,7 +3,7 @@
import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; 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 {MentionData} from 'types/notification';
import Config from 'common/config'; import Config from 'common/config';
@ -14,10 +14,10 @@ import {displayMention} from 'main/notifications';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {handleAppBeforeQuit} from './app'; import {handleAppBeforeQuit} from './app';
import {handleNewServerModal, switchServer} from './servers';
const log = new Logger('App.Intercom'); const log = new Logger('App.Intercom');
@ -35,49 +35,6 @@ export function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
app.quit(); 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) { function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) {
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible}); 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<MattermostTeam[], Team>('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<string, boolean>('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() { export function handleWelcomeScreenModal() {
log.debug('handleWelcomeScreenModal'); log.debug('handleWelcomeScreenModal');
@ -236,7 +98,7 @@ export function handleWelcomeScreenModal() {
if (modalPromise) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data); const newTeam = ServerManager.addServer(data);
WindowManager.switchServer(newTeam.id, true); switchServer(newTeam.id, true);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {

View file

@ -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,
}));
});
});
});

134
src/main/app/servers.ts Normal file
View file

@ -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<MattermostTeam[], Team>('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<string, boolean>('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');
}
};

39
src/main/app/tabs.test.js Normal file
View file

@ -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');
});
});
});

73
src/main/app/tabs.ts Normal file
View file

@ -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);
};

View file

@ -52,7 +52,6 @@ jest.mock('main/menus/tray', () => ({}));
jest.mock('main/tray/tray', () => ({})); jest.mock('main/tray/tray', () => ({}));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/windows/mainWindow', () => ({})); jest.mock('main/windows/mainWindow', () => ({}));
jest.mock('main/windows/windowManager', () => ({}));
jest.mock('./initialize', () => ({ jest.mock('./initialize', () => ({
mainProtocol: 'mattermost', mainProtocol: 'mattermost',

View file

@ -27,7 +27,6 @@ import {createMenu as createTrayMenu} from 'main/menus/tray';
import {ServerInfo} from 'main/server/serverInfo'; import {ServerInfo} from 'main/server/serverInfo';
import {setTrayMenu} from 'main/tray/tray'; import {setTrayMenu} from 'main/tray/tray';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {mainProtocol} from './initialize'; import {mainProtocol} from './initialize';
@ -39,7 +38,8 @@ const log = new Logger('App.Utils');
export function openDeepLink(deeplinkingUrl: string) { export function openDeepLink(deeplinkingUrl: string) {
try { try {
WindowManager.showMainWindow(deeplinkingUrl); MainWindow.show();
ViewManager.handleDeepLink(deeplinkingUrl);
} catch (err) { } catch (err) {
log.error(`There was an error opening the deeplinking url: ${err}`); log.error(`There was an error opening the deeplinking url: ${err}`);
} }
@ -58,7 +58,7 @@ export function handleUpdateMenuEvent() {
Menu.setApplicationMenu(aMenu); Menu.setApplicationMenu(aMenu);
aMenu.addListener('menu-will-close', () => { aMenu.addListener('menu-will-close', () => {
ViewManager.focusCurrentView(); ViewManager.focusCurrentView();
WindowManager.sendToRenderer(APP_MENU_WILL_CLOSE); MainWindow.sendToRenderer(APP_MENU_WILL_CLOSE);
}); });
// set up context menu for tray icon // set up context menu for tray icon

57
src/main/app/windows.ts Normal file
View file

@ -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;
}
};

View file

@ -4,8 +4,8 @@
import {AuthManager} from 'main/authManager'; import {AuthManager} from 'main/authManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager';
jest.mock('common/config', () => ({ jest.mock('common/config', () => ({
teams: [{ teams: [{
@ -89,8 +89,8 @@ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn().mockImplementation(() => ({})), get: jest.fn().mockImplementation(() => ({})),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/views/viewManager', () => ({
getServerURLFromWebContentsId: jest.fn(), getViewByWebContentsId: jest.fn(),
})); }));
jest.mock('main/views/modalManager', () => ({ jest.mock('main/views/modalManager', () => ({
@ -109,42 +109,42 @@ describe('main/authManager', () => {
authManager.popPermissionModal = jest.fn(); authManager.popPermissionModal = jest.fn();
it('should not pop any modal on a missing server', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 0}, {url: 'http://badurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled(); expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popLoginModal when isTrustedURL', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://trustedurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popLoginModal when isCustomLoginURL', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://customloginurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popLoginModal when has permission', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://haspermissionurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popPermissionModal when anything else is true', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled(); expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).toBeCalled(); expect(authManager.popPermissionModal).toBeCalled();
}); });
it('should set login callback when logging in', () => { 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(); const callback = jest.fn();
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback);
expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback); expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback);

View file

@ -12,8 +12,8 @@ import urlUtils from 'common/utils/url';
import modalManager from 'main/views/modalManager'; import modalManager from 'main/views/modalManager';
import TrustedOriginsStore from 'main/trustedOrigins'; import TrustedOriginsStore from 'main/trustedOrigins';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalURLString, getLocalPreload} from 'main/utils';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import ViewManager from 'main/views/viewManager';
const log = new Logger('AuthManager'); const log = new Logger('AuthManager');
const preload = getLocalPreload('desktopAPI.js'); const preload = getLocalPreload('desktopAPI.js');
@ -40,7 +40,7 @@ export class AuthManager {
if (!parsedURL) { if (!parsedURL) {
return; return;
} }
const serverURL = WindowManager.getServerURLFromWebContentsId(webContents.id); const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url;
if (!serverURL) { if (!serverURL) {
return; return;
} }

View file

@ -44,7 +44,7 @@ jest.mock('main/notifications', () => ({
displayUpgrade: jest.fn(), displayUpgrade: jest.fn(),
displayRestartToUpgrade: jest.fn(), displayRestartToUpgrade: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));

View file

@ -25,9 +25,6 @@ jest.mock('common/appState', () => ({
jest.mock('./windows/mainWindow', () => ({ jest.mock('./windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
})); }));
jest.mock('./windows/windowManager', () => ({
setOverlayIcon: jest.fn(),
}));
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn().mockReturnValue(''), localizeMessage: jest.fn().mockReturnValue(''),

View file

@ -77,7 +77,7 @@ jest.mock('macos-notification-state', () => ({
getDoNotDisturb: jest.fn(), getDoNotDisturb: jest.fn(),
})); }));
jest.mock('main/notifications', () => ({})); jest.mock('main/notifications', () => ({}));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));

View file

@ -32,7 +32,7 @@ import * as Validator from 'common/Validator';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {displayDownloadCompleted} from 'main/notifications'; import {displayDownloadCompleted} from 'main/notifications';
import ViewManager from 'main/views/viewManager'; 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 {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils';
import appVersionManager from './AppVersionManager'; import appVersionManager from './AppVersionManager';
@ -335,7 +335,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
onOpen = () => { onOpen = () => {
this.open = true; this.open = true;
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
}; };
onClose = () => { onClose = () => {
@ -361,7 +361,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
log.debug('openDownloadsDropdown'); log.debug('openDownloadsDropdown');
this.open = true; this.open = true;
ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN); ipcMain.emit(OPEN_DOWNLOADS_DROPDOWN);
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
}; };
hasUpdate = () => { hasUpdate = () => {
@ -394,7 +394,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
this.downloads = downloads; this.downloads = downloads;
this.setJson(downloads); this.setJson(downloads);
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.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) => { private save = (key: string, item: DownloadedItem) => {
@ -402,7 +402,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
this.downloads[key] = item; this.downloads[key] = item;
this.setValue(key, item); this.setValue(key, item);
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN, true, this.downloads); 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) => { private handleDownloadItemEvents = (item: DownloadItem, webContents: WebContents) => {
@ -485,9 +485,9 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
log.debug('shouldShowBadge'); log.debug('shouldShowBadge');
if (this.open === true) { if (this.open === true) {
WindowManager.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE); MainWindow.sendToRenderer(HIDE_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
} else { } else {
WindowManager.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE); MainWindow.sendToRenderer(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE);
} }
}; };

View file

@ -55,12 +55,15 @@ jest.mock('common/servers/serverManager', () => ({
getOrderedServers: jest.fn(), getOrderedServers: jest.fn(),
getOrderedTabsForServer: jest.fn(), getOrderedTabsForServer: jest.fn(),
})); }));
jest.mock('main/app/servers', () => ({
switchServer: jest.fn(),
}));
jest.mock('main/diagnostics', () => ({})); jest.mock('main/diagnostics', () => ({}));
jest.mock('main/downloadsManager', () => ({ jest.mock('main/downloadsManager', () => ({
hasDownloads: jest.fn(), hasDownloads: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('main/windows/settingsWindow', () => ({}));

View file

@ -13,12 +13,13 @@ import {Config} from 'common/config';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import WindowManager from 'main/windows/windowManager';
import {UpdateManager} from 'main/autoUpdater'; import {UpdateManager} from 'main/autoUpdater';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import Diagnostics from 'main/diagnostics'; import Diagnostics from 'main/diagnostics';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import SettingsWindow from 'main/windows/settingsWindow'; 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) { export function createTemplate(config: Config, updateManager: UpdateManager) {
const separatorItem: MenuItemConstructorOptions = { const separatorItem: MenuItemConstructorOptions = {
@ -265,7 +266,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
label: team.name, label: team.name,
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`,
click() { click() {
WindowManager.switchServer(team.id); switchServer(team.id);
}, },
}); });
if (ServerManager.getCurrentServer().id === 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))}`, label: ` ${localizeMessage(`common.tabs.${tab.type}`, getTabDisplayName(tab.type as TabType))}`,
accelerator: `CmdOrCtrl+${i + 1}`, accelerator: `CmdOrCtrl+${i + 1}`,
click() { 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'), label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'),
accelerator: 'Ctrl+Tab', accelerator: 'Ctrl+Tab',
click() { click() {
WindowManager.selectNextTab(); selectNextTab();
}, },
enabled: (teams.length > 1), enabled: (teams.length > 1),
}, { }, {
label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'), label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'),
accelerator: 'Ctrl+Shift+Tab', accelerator: 'Ctrl+Shift+Tab',
click() { click() {
WindowManager.selectPreviousTab(); selectPreviousTab();
}, },
enabled: (teams.length > 1), enabled: (teams.length > 1),
}, ...(isMac ? [separatorItem, { }, ...(isMac ? [separatorItem, {

View file

@ -14,9 +14,10 @@ jest.mock('main/i18nManager', () => ({
jest.mock('common/servers/serverManager', () => ({ jest.mock('common/servers/serverManager', () => ({
getOrderedServers: jest.fn(), getOrderedServers: jest.fn(),
})); }));
jest.mock('main/app/servers', () => ({
switchServer: jest.fn(),
}));
jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('main/windows/settingsWindow', () => ({}));
jest.mock('main/windows/windowManager', () => ({}));
describe('main/menus/tray', () => { describe('main/menus/tray', () => {
it('should show the first 9 servers (using order)', () => { it('should show the first 9 servers (using order)', () => {

View file

@ -5,10 +5,11 @@
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron'; import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
import WindowManager from 'main/windows/windowManager';
import {localizeMessage} from 'main/i18nManager';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import {localizeMessage} from 'main/i18nManager';
import SettingsWindow from 'main/windows/settingsWindow'; import SettingsWindow from 'main/windows/settingsWindow';
import {switchServer} from 'main/app/servers';
export function createTemplate() { export function createTemplate() {
const teams = ServerManager.getOrderedServers(); const teams = ServerManager.getOrderedServers();
@ -17,7 +18,7 @@ export function createTemplate() {
return { return {
label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name, label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name,
click: () => { click: () => {
WindowManager.switchServer(team.id); switchServer(team.id);
}, },
}; };
}), { }), {

View file

@ -13,9 +13,8 @@ import {PLAY_SOUND} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import MainWindow from 'main/windows/mainWindow';
import MainWindow from '../windows/mainWindow'; import ViewManager from 'main/views/viewManager';
import WindowManager from '../windows/windowManager';
import getLinuxDoNotDisturb from './dnd-linux'; import getLinuxDoNotDisturb from './dnd-linux';
@ -83,14 +82,11 @@ jest.mock('../views/viewManager', () => ({
}, },
}, },
}), }),
showById: jest.fn(),
})); }));
jest.mock('../windows/mainWindow', () => ({ jest.mock('../windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
}));
jest.mock('../windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
flashFrame: jest.fn(),
switchTab: jest.fn(),
})); }));
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({
@ -192,7 +188,7 @@ describe('main/notifications', () => {
{id: 1}, {id: 1},
{soundName: 'test_sound'}, {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', () => { 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'); const mention = mentions.find((m) => m.body === 'mention_click_body');
mention.value.click(); 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', () => { it('linux/windows - should not flash frame when config item is not set', () => {

View file

@ -13,7 +13,6 @@ import {Logger} from 'common/log';
import ViewManager from '../views/viewManager'; import ViewManager from '../views/viewManager';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
import WindowManager from '../windows/windowManager';
import {Mention} from './Mention'; import {Mention} from './Mention';
import {DownloadNotification} from './Download'; import {DownloadNotification} from './Download';
@ -67,7 +66,7 @@ export function displayMention(title: string, body: string, channel: {id: string
} }
const notificationSound = mention.getNotificationSound(); const notificationSound = mention.getNotificationSound();
if (notificationSound) { if (notificationSound) {
WindowManager.sendToRenderer(PLAY_SOUND, notificationSound); MainWindow.sendToRenderer(PLAY_SOUND, notificationSound);
} }
flashFrame(true); flashFrame(true);
}); });
@ -75,7 +74,7 @@ export function displayMention(title: string, body: string, channel: {id: string
mention.on('click', () => { mention.on('click', () => {
log.debug('notification click', serverName, mention); log.debug('notification click', serverName, mention);
if (serverName) { if (serverName) {
WindowManager.switchTab(view.id); ViewManager.showById(view.id);
webcontents.send('notification-clicked', {channel, teamId, url}); webcontents.send('notification-clicked', {channel, teamId, url});
} }
}); });

View file

@ -59,10 +59,9 @@ jest.mock('main/AutoLauncher', () => ({
jest.mock('main/badge', () => ({ jest.mock('main/badge', () => ({
setUnreadBadgeSetting: jest.fn(), setUnreadBadgeSetting: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
handleUpdateConfig: jest.fn(),
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
initializeCurrentServerName: jest.fn(), on: jest.fn(),
})); }));
describe('main/tray', () => { describe('main/tray', () => {

View file

@ -7,12 +7,14 @@ import {app, nativeImage, Tray, systemPreferences, nativeTheme} from 'electron';
import AppState from 'common/appState'; import AppState from 'common/appState';
import {UPDATE_APPSTATE_TOTALS} from 'common/communication'; import {UPDATE_APPSTATE_TOTALS} from 'common/communication';
import {Logger} from 'common/log';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import MainWindow from 'main/windows/mainWindow'; 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 assetsDir = path.resolve(app.getAppPath(), 'assets');
const log = new Logger('Tray');
let trayImages: Record<string, Electron.NativeImage>; let trayImages: Record<string, Electron.NativeImage>;
let trayIcon: Tray; let trayIcon: Tray;
@ -85,12 +87,12 @@ export function setupTray(iconTheme: string) {
trayIcon.setToolTip(app.name); trayIcon.setToolTip(app.name);
trayIcon.on('click', () => { trayIcon.on('click', () => {
const mainWindow = MainWindow.get(true)!; const mainWindow = MainWindow.get();
if (mainWindow.isVisible()) { if (mainWindow && mainWindow.isVisible()) {
mainWindow.blur(); // To move focus to the next top-level window in Windows mainWindow.blur(); // To move focus to the next top-level window in Windows
mainWindow.hide(); mainWindow.hide();
} else { } else {
WindowManager.restoreMain(); restoreMain();
} }
}); });
@ -98,7 +100,7 @@ export function setupTray(iconTheme: string) {
trayIcon.popUpContextMenu(); trayIcon.popUpContextMenu();
}); });
trayIcon.on('balloon-click', () => { trayIcon.on('balloon-click', () => {
WindowManager.restoreMain(); restoreMain();
}); });
AppState.on(UPDATE_APPSTATE_TOTALS, (anyExpired: boolean, anyMentions: number, anyUnreads: boolean) => { 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) { function setTray(status: string, message: string) {
if (trayIcon.isDestroyed()) { if (trayIcon.isDestroyed()) {
return; return;

View file

@ -9,7 +9,6 @@ import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from 'common/tabs/MessagingTabView'; import MessagingTabView from 'common/tabs/MessagingTabView';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
import * as WindowManager from '../windows/windowManager';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import Utils from '../utils'; import Utils from '../utils';
@ -41,8 +40,6 @@ jest.mock('electron', () => ({
jest.mock('../windows/mainWindow', () => ({ jest.mock('../windows/mainWindow', () => ({
focusThreeDotMenu: jest.fn(), focusThreeDotMenu: jest.fn(),
get: jest.fn(), get: jest.fn(),
}));
jest.mock('../windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('common/appState', () => ({ jest.mock('common/appState', () => ({
@ -181,7 +178,7 @@ describe('main/views/MattermostView', () => {
await expect(promise).rejects.toThrow(error); await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object)); expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled(); 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); expect(mattermostView.status).toBe(-1);
jest.runAllTimers(); jest.runAllTimers();
expect(retryInBackgroundFn).toBeCalled(); expect(retryInBackgroundFn).toBeCalled();
@ -426,13 +423,13 @@ describe('main/views/MattermostView', () => {
it('should hide back button on internal url', () => { it('should hide back button on internal url', () => {
Utils.shouldHaveBackBar.mockReturnValue(false); Utils.shouldHaveBackBar.mockReturnValue(false);
mattermostView.handleDidNavigate(null, 'http://server-1.com/path/to/channels'); 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', () => { it('should show back button on external url', () => {
Utils.shouldHaveBackBar.mockReturnValue(true); Utils.shouldHaveBackBar.mockReturnValue(true);
mattermostView.handleDidNavigate(null, 'http://server-2.com/some/other/path'); 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);
}); });
}); });

View file

@ -26,7 +26,6 @@ import {Logger} from 'common/log';
import {TabView} from 'common/tabs/TabView'; import {TabView} from 'common/tabs/TabView';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils'; 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()}); const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) { 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.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.log.info(`Invalid certificate, stop retrying until the user decides what to do: ${err}.`);
this.status = Status.ERROR; this.status = Status.ERROR;
@ -394,7 +393,7 @@ export class MattermostView extends EventEmitter {
if (this.maxRetries-- > 0) { if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err); this.loadRetry(loadURL, err);
} else { } 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.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.log.info(`Couldn't establish a connection with ${loadURL}, will continue to retry in the background`, err);
this.status = Status.ERROR; this.status = Status.ERROR;
@ -419,14 +418,14 @@ export class MattermostView extends EventEmitter {
private loadRetry = (loadURL: string, err: Error) => { private loadRetry = (loadURL: string, err: Error) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL); 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`); this.log.info(`failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
} }
private loadSuccess = (loadURL: string) => { private loadSuccess = (loadURL: string) => {
return () => { return () => {
this.log.verbose(`finished loading ${loadURL}`); this.log.verbose(`finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.id); MainWindow.sendToRenderer(LOAD_SUCCESS, this.id);
this.maxRetries = MAX_SERVER_RETRIES; this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) { if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle()); this.updateMentionsFromTitle(this.view.webContents.getTitle());
@ -476,11 +475,11 @@ export class MattermostView extends EventEmitter {
if (shouldHaveBackBar(this.tab.url || '', url)) { if (shouldHaveBackBar(this.tab.url || '', url)) {
this.setBounds(getWindowBoundaries(mainWindow, true)); this.setBounds(getWindowBoundaries(mainWindow, true));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true); MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true);
this.log.debug('show back button'); this.log.debug('show back button');
} else { } else {
this.setBounds(getWindowBoundaries(mainWindow)); this.setBounds(getWindowBoundaries(mainWindow));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, false);
this.log.debug('hide back button'); this.log.debug('hide back button');
} }
} }

View file

@ -54,10 +54,9 @@ jest.mock('macos-notification-state', () => ({
})); }));
jest.mock('main/downloadsManager', () => ({})); jest.mock('main/downloadsManager', () => ({}));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
on: jest.fn(),
get: jest.fn(), get: jest.fn(),
getBounds: jest.fn(), getBounds: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('fs', () => ({ jest.mock('fs', () => ({

View file

@ -11,6 +11,8 @@ import {
DOWNLOADS_DROPDOWN_MENU_OPEN_FILE, DOWNLOADS_DROPDOWN_MENU_OPEN_FILE,
DOWNLOADS_DROPDOWN_MENU_SHOW_FILE_IN_FOLDER, DOWNLOADS_DROPDOWN_MENU_SHOW_FILE_IN_FOLDER,
EMIT_CONFIGURATION, EMIT_CONFIGURATION,
MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED,
OPEN_DOWNLOADS_DROPDOWN_MENU, OPEN_DOWNLOADS_DROPDOWN_MENU,
REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO, REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO,
TOGGLE_DOWNLOADS_DROPDOWN_MENU, TOGGLE_DOWNLOADS_DROPDOWN_MENU,
@ -28,7 +30,6 @@ import {
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
const log = new Logger('DownloadsDropdownMenuView'); const log = new Logger('DownloadsDropdownMenuView');
@ -43,6 +44,8 @@ export class DownloadsDropdownMenuView {
constructor() { constructor() {
this.open = false; 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(OPEN_DOWNLOADS_DROPDOWN_MENU, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN_MENU, this.handleClose);
ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle); ipcMain.on(TOGGLE_DOWNLOADS_DROPDOWN_MENU, this.handleToggle);
@ -55,7 +58,7 @@ export class DownloadsDropdownMenuView {
ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem); ipcMain.on(UPDATE_DOWNLOADS_DROPDOWN_MENU, this.updateItem);
} }
init = () => { private init = () => {
this.windowBounds = MainWindow.getBounds(); this.windowBounds = MainWindow.getBounds();
if (!this.windowBounds) { if (!this.windowBounds) {
throw new Error('Cannot initialize downloadsDropdownMenuView, missing MainWindow'); 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 * This is called every time the "window" is resized so that we can position
* the downloads dropdown at the correct position * the downloads dropdown at the correct position
*/ */
updateWindowBounds = () => { private updateWindowBounds = (newBounds: Electron.Rectangle) => {
log.debug('updateWindowBounds'); log.debug('updateWindowBounds');
this.windowBounds = MainWindow.getBounds(); this.windowBounds = newBounds;
this.updateDownloadsDropdownMenu(); this.updateDownloadsDropdownMenu();
this.repositionDownloadsDropdownMenu(); this.repositionDownloadsDropdownMenu();
} }
@ -134,7 +137,7 @@ export class DownloadsDropdownMenuView {
this.item = undefined; this.item = undefined;
ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM); ipcMain.emit(UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM);
this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0)); 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) => { private handleToggle = (event: IpcMainEvent, payload: DownloadsMenuOpenEventPayload) => {

View file

@ -67,10 +67,9 @@ jest.mock('main/downloadsManager', () => ({
onClose: jest.fn(), onClose: jest.fn(),
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
on: jest.fn(),
get: jest.fn(), get: jest.fn(),
getBounds: jest.fn(), getBounds: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));

View file

@ -16,6 +16,8 @@ import {
UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM, UPDATE_DOWNLOADS_DROPDOWN_MENU_ITEM,
GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION, GET_DOWNLOADED_IMAGE_THUMBNAIL_LOCATION,
DOWNLOADS_DROPDOWN_OPEN_FILE, DOWNLOADS_DROPDOWN_OPEN_FILE,
MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED,
} from 'common/communication'; } from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import Config from 'common/config'; 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 {getLocalPreload, getLocalURLString} from 'main/utils';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
const log = new Logger('DownloadsDropdownView'); const log = new Logger('DownloadsDropdownView');
@ -35,6 +36,8 @@ export class DownloadsDropdownView {
private view?: BrowserView; private view?: BrowserView;
constructor() { constructor() {
MainWindow.on(MAIN_WINDOW_CREATED, this.init);
MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds);
ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen); ipcMain.on(OPEN_DOWNLOADS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose); ipcMain.on(CLOSE_DOWNLOADS_DROPDOWN, this.handleClose);
ipcMain.on(EMIT_CONFIGURATION, this.updateDownloadsDropdown); 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 * This is called every time the "window" is resized so that we can position
* the downloads dropdown at the correct position * the downloads dropdown at the correct position
*/ */
updateWindowBounds = () => { private updateWindowBounds = (newBounds: Electron.Rectangle) => {
log.debug('updateWindowBounds'); log.debug('updateWindowBounds');
this.windowBounds = MainWindow.getBounds(); this.windowBounds = newBounds;
this.updateDownloadsDropdown(); this.updateDownloadsDropdown();
this.repositionDownloadsDropdown(); this.repositionDownloadsDropdown();
} }
@ -110,7 +113,7 @@ export class DownloadsDropdownView {
MainWindow.get()?.setTopBrowserView(this.view); MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus(); this.view.webContents.focus();
downloadsManager.onOpen(); downloadsManager.onOpen();
WindowManager.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN); MainWindow.sendToRenderer(OPEN_DOWNLOADS_DROPDOWN);
} }
private handleClose = () => { private handleClose = () => {
@ -118,7 +121,7 @@ export class DownloadsDropdownView {
this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0)); this.view?.setBounds(this.getBounds(this.windowBounds?.width ?? 0, 0, 0));
downloadsManager.onClose(); downloadsManager.onClose();
WindowManager.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN); MainWindow.sendToRenderer(CLOSE_DOWNLOADS_DROPDOWN);
} }
private clearDownloads = () => { private clearDownloads = () => {

View file

@ -13,6 +13,7 @@ jest.mock('electron', () => ({
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
on: jest.fn(),
})); }));
describe('main/views/loadingScreen', () => { describe('main/views/loadingScreen', () => {

View file

@ -3,7 +3,7 @@
import {BrowserView, app, ipcMain} from 'electron'; 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 {Logger} from 'common/log';
import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils'; import {getLocalPreload, getLocalURLString, getWindowBoundaries} from 'main/utils';
@ -24,6 +24,7 @@ export class LoadingScreen {
constructor() { constructor() {
this.state = LoadingScreenState.HIDDEN; this.state = LoadingScreenState.HIDDEN;
MainWindow.on(MAIN_WINDOW_RESIZED, this.setBounds);
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleAnimationFinished); ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, this.handleAnimationFinished);
} }
@ -31,16 +32,6 @@ export class LoadingScreen {
* Loading Screen * Loading Screen
*/ */
setBounds = () => {
if (this.view) {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.view.setBounds(getWindowBoundaries(mainWindow));
}
}
setDarkMode = (darkMode: boolean) => { setDarkMode = (darkMode: boolean) => {
this.view?.webContents.send(DARK_MODE_CHANGE, darkMode); this.view?.webContents.send(DARK_MODE_CHANGE, darkMode);
} }
@ -111,6 +102,16 @@ export class LoadingScreen {
app.emit('e2e-app-loaded'); 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(); const loadingScreen = new LoadingScreen();

View file

@ -25,7 +25,8 @@ jest.mock('./modalView', () => ({
jest.mock('main/views/viewManager', () => ({ jest.mock('main/views/viewManager', () => ({
focusCurrentView: jest.fn(), focusCurrentView: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/mainWindow', () => ({
on: jest.fn(),
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('process', () => ({ jest.mock('process', () => ({

View file

@ -15,14 +15,14 @@ import {
EMIT_CONFIGURATION, EMIT_CONFIGURATION,
DARK_MODE_CHANGE, DARK_MODE_CHANGE,
GET_MODAL_UNCLOSEABLE, GET_MODAL_UNCLOSEABLE,
RESIZE_MODAL, MAIN_WINDOW_RESIZED,
} from 'common/communication'; } from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getAdjustedWindowBoundaries} from 'main/utils'; import {getAdjustedWindowBoundaries} from 'main/utils';
import MainWindow from 'main/windows/mainWindow';
import WebContentsEventManager from 'main/views/webContentEvents'; import WebContentsEventManager from 'main/views/webContentEvents';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import WindowManager from 'main/windows/windowManager';
import {ModalView} from './modalView'; import {ModalView} from './modalView';
@ -40,7 +40,7 @@ export class ModalManager {
ipcMain.handle(RETRIEVE_MODAL_INFO, this.handleInfoRequest); ipcMain.handle(RETRIEVE_MODAL_INFO, this.handleInfoRequest);
ipcMain.on(MODAL_RESULT, this.handleModalResult); ipcMain.on(MODAL_RESULT, this.handleModalResult);
ipcMain.on(MODAL_CANCEL, this.handleModalCancel); 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); ipcMain.on(EMIT_CONFIGURATION, this.handleEmitConfiguration);
} }
@ -88,11 +88,11 @@ export class ModalManager {
const withDevTools = process.env.MM_DEBUG_MODALS || false; const withDevTools = process.env.MM_DEBUG_MODALS || false;
this.modalQueue.forEach((modal, index) => { this.modalQueue.forEach((modal, index) => {
if (index === 0) { if (index === 0) {
WindowManager.sendToRenderer(MODAL_OPEN); MainWindow.sendToRenderer(MODAL_OPEN);
modal.show(undefined, Boolean(withDevTools)); modal.show(undefined, Boolean(withDevTools));
WebContentsEventManager.addWebContentsEventListeners(modal.view.webContents); WebContentsEventManager.addWebContentsEventListeners(modal.view.webContents);
} else { } else {
WindowManager.sendToRenderer(MODAL_CLOSE); MainWindow.sendToRenderer(MODAL_CLOSE);
modal.hide(); modal.hide();
} }
}); });
@ -114,7 +114,7 @@ export class ModalManager {
if (this.modalQueue.length) { if (this.modalQueue.length) {
this.showModal(); this.showModal();
} else { } else {
WindowManager.sendToRenderer(MODAL_CLOSE); MainWindow.sendToRenderer(MODAL_CLOSE);
ViewManager.focusCurrentView(); ViewManager.focusCurrentView();
} }
} }
@ -131,7 +131,7 @@ export class ModalManager {
return this.modalQueue.some((modal) => modal.isActive()); 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}); log.debug('handleResizeModal', {bounds, modalQueueLength: this.modalQueue.length});
if (this.modalQueue.length) { if (this.modalQueue.length) {

View file

@ -27,12 +27,9 @@ jest.mock('electron', () => ({
}, },
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
on: jest.fn(),
get: jest.fn(), get: jest.fn(),
getBounds: jest.fn(), getBounds: jest.fn(),
addBrowserView: jest.fn(),
setTopBrowserView: jest.fn(),
}));
jest.mock('../windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));

View file

@ -15,6 +15,8 @@ import {
REQUEST_TEAMS_DROPDOWN_INFO, REQUEST_TEAMS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SERVERS_UPDATE, SERVERS_UPDATE,
MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@ -23,7 +25,6 @@ import ServerManager from 'common/servers/serverManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import WindowManager from '../windows/windowManager';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
const log = new Logger('TeamDropdownView'); const log = new Logger('TeamDropdownView');
@ -51,6 +52,9 @@ export class TeamDropdownView {
this.mentions = new Map(); this.mentions = new Map();
this.expired = 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(OPEN_TEAMS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose); ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose);
ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize);
@ -62,7 +66,13 @@ export class TeamDropdownView {
ServerManager.on(SERVERS_UPDATE, this.updateServers); 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'); const preload = getLocalPreload('desktopAPI.js');
this.view = new BrowserView({webPreferences: { this.view = new BrowserView({webPreferences: {
preload, preload,
@ -80,11 +90,6 @@ export class TeamDropdownView {
MainWindow.get()?.addBrowserView(this.view); MainWindow.get()?.addBrowserView(this.view);
} }
updateWindowBounds = () => {
this.windowBounds = MainWindow.getBounds();
this.updateDropdown();
}
private updateDropdown = () => { private updateDropdown = () => {
log.silly('updateDropdown'); log.silly('updateDropdown');
@ -132,7 +137,7 @@ export class TeamDropdownView {
this.view.setBounds(this.bounds); this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view); MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus(); this.view.webContents.focus();
WindowManager.sendToRenderer(OPEN_TEAMS_DROPDOWN); MainWindow.sendToRenderer(OPEN_TEAMS_DROPDOWN);
this.isOpen = true; this.isOpen = true;
} }
@ -140,7 +145,7 @@ export class TeamDropdownView {
log.debug('handleClose'); log.debug('handleClose');
this.view?.setBounds(this.getBounds(0, 0)); this.view?.setBounds(this.getBounds(0, 0));
WindowManager.sendToRenderer(CLOSE_TEAMS_DROPDOWN); MainWindow.sendToRenderer(CLOSE_TEAMS_DROPDOWN);
this.isOpen = false; this.isOpen = false;
} }

View file

@ -69,6 +69,7 @@ jest.mock('main/views/loadingScreen', () => ({
})); }));
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
on: jest.fn(),
})); }));
jest.mock('common/servers/serverManager', () => ({ jest.mock('common/servers/serverManager', () => ({
getCurrentServer: jest.fn(), getCurrentServer: jest.fn(),

View file

@ -24,6 +24,8 @@ import {
HISTORY, HISTORY,
GET_VIEW_INFO_FOR_TEST, GET_VIEW_INFO_FOR_TEST,
SESSION_EXPIRED, SESSION_EXPIRED,
MAIN_WINDOW_CREATED,
MAIN_WINDOW_RESIZED,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@ -36,7 +38,7 @@ import {TabView, TAB_MESSAGING} from 'common/tabs/TabView';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {getLocalURLString, getLocalPreload} from '../utils'; import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils';
import {MattermostView} from './MattermostView'; import {MattermostView} from './MattermostView';
import modalManager from './modalManager'; 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.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
this.closedViews = new Map(); this.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.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest);
ipcMain.on(HISTORY, this.handleHistory); ipcMain.on(HISTORY, this.handleHistory);
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
@ -71,7 +75,9 @@ export class ViewManager {
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration); ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
} }
init = () => { private init = () => {
MainWindow.onBrowserWindow?.('focus', this.focusCurrentView);
LoadingScreen.show(); LoadingScreen.show();
ServerManager.getAllServers().forEach((server) => this.loadServer(server)); ServerManager.getAllServers().forEach((server) => this.loadServer(server));
this.showInitial(); this.showInitial();
@ -323,7 +329,7 @@ export class ViewManager {
const localURL = getLocalURLString('urlView.html', query); const localURL = getLocalURLString('urlView.html', query);
urlView.webContents.loadURL(localURL); urlView.webContents.loadURL(localURL);
MainWindow.get()?.addBrowserView(urlView); 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 = () => { const hideView = () => {
delete this.urlViewCancel; delete this.urlViewCancel;
@ -343,6 +349,10 @@ export class ViewManager {
const adjustWidth = (event: IpcMainEvent, width: number) => { const adjustWidth = (event: IpcMainEvent, width: number) => {
log.silly('showURLView.adjustWidth', width); log.silly('showURLView.adjustWidth', width);
if (!boundaries) {
return;
}
const bounds = { const bounds = {
x: 0, x: 0,
y: (boundaries.height + TAB_BAR_HEIGHT) - URL_VIEW_HEIGHT, y: (boundaries.height + TAB_BAR_HEIGHT) - URL_VIEW_HEIGHT,
@ -526,6 +536,16 @@ export class ViewManager {
AppState.updateExpired(viewId, isExpired); 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 * Helper functions
*/ */

View file

@ -8,8 +8,8 @@ import {shell, BrowserWindow} from 'electron';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import ContextMenu from 'main/contextMenu'; import ContextMenu from 'main/contextMenu';
import ViewManager from 'main/views/viewManager';
import * as WindowManager from '../windows/windowManager';
import allowProtocolDialog from '../allowProtocolDialog'; import allowProtocolDialog from '../allowProtocolDialog';
import {WebContentsEventManager} from './webContentEvents'; import {WebContentsEventManager} from './webContentEvents';
@ -30,10 +30,7 @@ jest.mock('../allowProtocolDialog', () => ({}));
jest.mock('main/windows/callsWidgetWindow', () => ({})); jest.mock('main/windows/callsWidgetWindow', () => ({}));
jest.mock('main/views/viewManager', () => ({ jest.mock('main/views/viewManager', () => ({
getViewByWebContentsId: jest.fn(), getViewByWebContentsId: jest.fn(),
})); handleDeepLink: jest.fn(),
jest.mock('../windows/windowManager', () => ({
getServerURLFromWebContentsId: jest.fn(),
showMainWindow: jest.fn(),
})); }));
jest.mock('../utils', () => ({ jest.mock('../utils', () => ({
composeUserAgent: jest.fn(), composeUserAgent: jest.fn(),
@ -231,7 +228,7 @@ describe('main/views/webContentsEvents', () => {
}); });
it('should open in the browser when there is no server matching', () => { 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(newWindow({url: 'http://server-2.com/subpath'})).toStrictEqual({action: 'deny'});
expect(shell.openExternal).toBeCalledWith('http://server-2.com/subpath'); 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', () => { it('should open team links in the app', () => {
expect(newWindow({url: 'http://server-1.com/myteam/channels/mychannel'})).toStrictEqual({action: 'deny'}); 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', () => { it('should prevent admin links from opening in a new window', () => {

View file

@ -12,7 +12,6 @@ import ContextMenu from 'main/contextMenu';
import ServerManager from 'common/servers/serverManager'; import ServerManager from 'common/servers/serverManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
@ -63,7 +62,11 @@ export class WebContentsEventManager {
return this.popupWindow.serverURL; 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) => { private generateWillNavigate = (webContentsId: number) => {
@ -182,7 +185,7 @@ export class WebContentsEventManager {
} }
if (urlUtils.isTeamUrl(serverURL, parsedURL, true)) { if (urlUtils.isTeamUrl(serverURL, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL); ViewManager.handleDeepLink(parsedURL);
return {action: 'deny'}; return {action: 'deny'};
} }
if (urlUtils.isAdminUrl(serverURL, parsedURL)) { if (urlUtils.isAdminUrl(serverURL, parsedURL)) {
@ -259,7 +262,7 @@ export class WebContentsEventManager {
const otherServerURL = ServerManager.lookupTabByURL(parsedURL); const otherServerURL = ServerManager.lookupTabByURL(parsedURL);
if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) { if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL); ViewManager.handleDeepLink(parsedURL);
return {action: 'deny'}; return {action: 'deny'};
} }

View file

@ -12,9 +12,10 @@ import {
CALLS_PLUGIN_ID, CALLS_PLUGIN_ID,
} from 'common/utils/constants'; } from 'common/utils/constants';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import {switchServer} from 'main/app/servers';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import WindowManager from 'main/windows/windowManager';
import { import {
resetScreensharePermissionsMacOS, resetScreensharePermissionsMacOS,
openScreensharePermissionsSettingsMacOS, openScreensharePermissionsSettingsMacOS,
@ -55,7 +56,7 @@ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
focus: jest.fn(), focus: jest.fn(),
})); }));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/app/servers', () => ({
switchServer: jest.fn(), switchServer: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({ jest.mock('main/views/viewManager', () => ({
@ -797,7 +798,7 @@ describe('main/windows/callsWidgetWindow', () => {
it('should switch server', () => { it('should switch server', () => {
callsWidgetWindow.handleDesktopSourcesModalRequest(); 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', () => { it('should switch server', () => {
callsWidgetWindow.handleCallsWidgetChannelLinkClick(); 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', () => { it('should focus view and propagate error to main view', () => {
callsWidgetWindow.handleCallsError('', {err: 'client-error'}); callsWidgetWindow.handleCallsError('', {err: 'client-error'});
expect(WindowManager.switchServer).toHaveBeenCalledWith('server-2'); expect(switchServer).toHaveBeenCalledWith('server-2');
expect(focus).toHaveBeenCalled(); expect(focus).toHaveBeenCalled();
expect(callsWidgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'}); 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', () => { it('should pass through the click link to browser history push', () => {
callsWidgetWindow.handleCallsLinkClick('', {link: '/other/subpath'}); 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'); expect(view.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath');
}); });
}); });

View file

@ -37,9 +37,10 @@ import {
DESKTOP_SOURCES_RESULT, DESKTOP_SOURCES_RESULT,
DISPATCH_GET_DESKTOP_SOURCES, DISPATCH_GET_DESKTOP_SOURCES,
} from 'common/communication'; } from 'common/communication';
import {switchServer} from 'main/app/servers';
import webContentsEventManager from 'main/views/webContentEvents'; import webContentsEventManager from 'main/views/webContentEvents';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
const log = new Logger('CallsWidgetWindow'); const log = new Logger('CallsWidgetWindow');
@ -454,7 +455,7 @@ export class CallsWidgetWindow {
return; return;
} }
WindowManager.switchServer(this.serverID); switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
} }
@ -472,7 +473,7 @@ export class CallsWidgetWindow {
return; return;
} }
WindowManager.switchServer(this.serverID); switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL);
} }
@ -484,7 +485,7 @@ export class CallsWidgetWindow {
return; return;
} }
WindowManager.switchServer(this.serverID); switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(CALLS_ERROR, msg); this.mainView?.sendToRenderer(CALLS_ERROR, msg);
} }
@ -496,7 +497,7 @@ export class CallsWidgetWindow {
return; return;
} }
WindowManager.switchServer(this.serverID); switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link);
} }

View file

@ -36,6 +36,7 @@ jest.mock('electron', () => ({
BrowserWindow: jest.fn(), BrowserWindow: jest.fn(),
ipcMain: { ipcMain: {
handle: jest.fn(), handle: jest.fn(),
on: jest.fn(),
}, },
screen: { screen: {
getDisplayMatching: jest.fn(), 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', () => { describe('onUnresponsive', () => {
const mainWindow = new MainWindow(); const mainWindow = new MainWindow();

View file

@ -7,16 +7,30 @@ import path from 'path';
import os from 'os'; import os from 'os';
import {EventEmitter} from 'events';
import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, globalShortcut, Input, ipcMain, screen} from 'electron'; import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, globalShortcut, Input, ipcMain, screen} from 'electron';
import {SavedWindowState} from 'types/mainWindow'; import {SavedWindowState} from 'types/mainWindow';
import AppState from 'common/appState'; 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 Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager'; 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 Utils from 'common/utils/util';
import * as Validator from 'common/Validator'; import * as Validator from 'common/Validator';
@ -29,18 +43,23 @@ import {getLocalPreload, getLocalURLString, isInsideRectangle} from '../utils';
const log = new Logger('MainWindow'); const log = new Logger('MainWindow');
const ALT_MENU_KEYS = ['Alt+F', 'Alt+E', 'Alt+V', 'Alt+H', 'Alt+W', 'Alt+P']; 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 win?: BrowserWindow;
private savedWindowState: SavedWindowState; private savedWindowState: SavedWindowState;
private ready: boolean; private ready: boolean;
private isResizing: boolean;
constructor() { constructor() {
super();
// Create the browser window. // Create the browser window.
this.ready = false; this.ready = false;
this.isResizing = false;
this.savedWindowState = this.getSavedWindowState(); this.savedWindowState = this.getSavedWindowState();
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen()); ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen());
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
ServerManager.on(SERVERS_UPDATE, this.handleUpdateConfig); ServerManager.on(SERVERS_UPDATE, this.handleUpdateConfig);
@ -108,6 +127,15 @@ export class MainWindow {
this.win.on('focus', this.onFocus); this.win.on('focus', this.onFocus);
this.win.on('blur', this.onBlur); this.win.on('blur', this.onBlur);
this.win.on('unresponsive', this.onUnresponsive); 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); this.win.webContents.on('before-input-event', this.onBeforeInputEvent);
@ -119,21 +147,45 @@ export class MainWindow {
const contextMenu = new ContextMenu({}, this.win); const contextMenu = new ContextMenu({}, this.win);
contextMenu.reload(); contextMenu.reload();
this.emit(MAIN_WINDOW_CREATED);
} }
get isReady() { get isReady() {
return this.ready; return this.ready;
} }
get = (ensureCreated?: boolean) => { get = () => {
if (ensureCreated && !this.win) {
this.init();
}
return this.win; return this.win;
} }
getBounds = () => { show = () => {
return this.win?.getContentBounds(); 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 = () => { 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 = () => { private shouldStartFullScreen = () => {
if (global?.args?.fullscreen !== undefined) { if (global?.args?.fullscreen !== undefined) {
return global.args.fullscreen; 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 * Server Manager update handler
*/ */

View file

@ -7,6 +7,8 @@ import {SHOW_SETTINGS_WINDOW} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import ViewManager from 'main/views/viewManager';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import {getLocalPreload, getLocalURLString} from '../utils'; import {getLocalPreload, getLocalURLString} from '../utils';
@ -33,8 +35,12 @@ export class SettingsWindow {
return this.win; return this.win;
} }
sendToRenderer = (channel: string, ...args: any[]) => {
this.win?.webContents.send(channel, ...args);
}
private create = () => { private create = () => {
const mainWindow = MainWindow.get(true); const mainWindow = MainWindow.get();
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
@ -67,6 +73,8 @@ export class SettingsWindow {
this.win.on('closed', () => { this.win.on('closed', () => {
delete this.win; delete this.win;
ViewManager.focusCurrentView();
}); });
} }
} }

View file

@ -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');
});
});
});

View file

@ -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;