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
57 changed files with 1089 additions and 1640 deletions

View File

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

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,11 @@
import {initialize} from './initialize';
// TODO: Singletons, we need DI :D
import('main/views/teamDropdownView');
import('main/views/downloadsDropdownMenuView');
import('main/views/downloadsDropdownView');
if (process.env.NODE_ENV !== 'production' && module.hot) {
module.hot.accept();
}

View File

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

View File

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

View File

@@ -1,20 +1,12 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getLocalURLString, getLocalPreload} from 'main/utils';
import ServerManager from 'common/servers/serverManager';
import MainWindow from 'main/windows/mainWindow';
import ModalManager from 'main/views/modalManager';
import WindowManager from 'main/windows/windowManager';
import {
handleOpenTab,
handleCloseTab,
handleNewServerModal,
handleEditServerModal,
handleRemoveServerModal,
handleWelcomeScreenModal,
handleMainWindowIsShown,
} from './intercom';
@@ -22,9 +14,6 @@ import {
jest.mock('common/config', () => ({
setServers: jest.fn(),
}));
jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: jest.fn(),
}));
jest.mock('main/notifications', () => ({}));
jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(),
@@ -45,224 +34,13 @@ jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/views/modalManager', () => ({
addModal: jest.fn(),
}));
jest.mock('main/windows/windowManager', () => ({
switchServer: jest.fn(),
switchTab: jest.fn(),
}));
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
jest.mock('./app', () => ({}));
const tabs = [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
];
const teams = [
{
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
},
];
describe('main/app/intercom', () => {
describe('handleCloseTab', () => {
it('should close the specified tab and switch to the next open tab', () => {
ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'});
handleCloseTab(null, 'tab-3');
expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false);
expect(WindowManager.switchTab).toBeCalledWith('tab-2');
});
});
describe('handleOpenTab', () => {
it('should open the specified tab', () => {
handleOpenTab(null, 'tab-1');
expect(WindowManager.switchTab).toBeCalledWith('tab-1');
});
});
describe('handleNewServerModal', () => {
let teamsCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
ServerManager.getAllServers.mockReturnValue([]);
ServerManager.addServer.mockImplementation(() => {
const newTeam = {
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
tabs,
};
teamsCopy = [
...teamsCopy,
newTeam,
];
return newTeam;
});
ServerManager.hasServers.mockReturnValue(Boolean(teamsCopy.length));
getDefaultConfigTeamFromTeam.mockImplementation((team) => ({
...team,
tabs,
}));
});
it('should add new team to the config', async () => {
const promise = Promise.resolve({
name: 'new-team',
url: 'http://new-team.com',
});
ModalManager.addModal.mockReturnValue(promise);
handleNewServerModal();
await promise;
expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
tabs,
}));
expect(WindowManager.switchServer).toBeCalledWith('server-1', true);
});
});
describe('handleEditServerModal', () => {
let teamsCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
return undefined;
}
return {...teamsCopy[0], toMattermostTeam: jest.fn()};
});
ServerManager.editServer.mockImplementation((id, team) => {
if (id !== teamsCopy[0].id) {
return;
}
const newTeam = {
...teamsCopy[0],
...team,
};
teamsCopy = [newTeam];
});
ServerManager.getAllServers.mockReturnValue(teamsCopy.map((team) => ({...team, toMattermostTeam: jest.fn()})));
});
it('should do nothing when the server cannot be found', () => {
handleEditServerModal(null, 'bad-server');
expect(ModalManager.addModal).not.toBeCalled();
});
it('should edit the existing team', async () => {
const promise = Promise.resolve({
name: 'new-team',
url: 'http://new-team.com',
});
ModalManager.addModal.mockReturnValue(promise);
handleEditServerModal(null, 'server-1');
await promise;
expect(teamsCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
}));
expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
tabs,
}));
});
});
describe('handleRemoveServerModal', () => {
let teamsCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
return undefined;
}
return teamsCopy[0];
});
ServerManager.removeServer.mockImplementation(() => {
teamsCopy = [];
});
ServerManager.getAllServers.mockReturnValue(teamsCopy);
});
it('should remove the existing team', async () => {
const promise = Promise.resolve(true);
ModalManager.addModal.mockReturnValue(promise);
handleRemoveServerModal(null, 'server-1');
await promise;
expect(teamsCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
}));
});
it('should not remove the existing team when clicking Cancel', async () => {
const promise = Promise.resolve(false);
ModalManager.addModal.mockReturnValue(promise);
expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
}));
handleRemoveServerModal(null, 'server-1');
await promise;
expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
}));
});
});
describe('handleWelcomeScreenModal', () => {
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');

View File

@@ -3,7 +3,7 @@
import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
import {Team, MattermostTeam} from 'types/config';
import {MattermostTeam} from 'types/config';
import {MentionData} from 'types/notification';
import Config from 'common/config';
@@ -14,10 +14,10 @@ import {displayMention} from 'main/notifications';
import {getLocalPreload, getLocalURLString} from 'main/utils';
import ServerManager from 'common/servers/serverManager';
import ModalManager from 'main/views/modalManager';
import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow';
import {handleAppBeforeQuit} from './app';
import {handleNewServerModal, switchServer} from './servers';
const log = new Logger('App.Intercom');
@@ -35,49 +35,6 @@ export function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
app.quit();
}
export function handleSwitchServer(event: IpcMainEvent, serverId: string) {
log.silly('handleSwitchServer', serverId);
WindowManager.switchServer(serverId);
}
export function handleSwitchTab(event: IpcMainEvent, tabId: string) {
log.silly('handleSwitchTab', {tabId});
WindowManager.switchTab(tabId);
}
export function handleCloseTab(event: IpcMainEvent, tabId: string) {
log.debug('handleCloseTab', {tabId});
const tab = ServerManager.getTab(tabId);
if (!tab) {
return;
}
ServerManager.setTabIsOpen(tabId, false);
const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id);
WindowManager.switchTab(nextTab.id);
}
export function handleOpenTab(event: IpcMainEvent, tabId: string) {
log.debug('handleOpenTab', {tabId});
ServerManager.setTabIsOpen(tabId, true);
WindowManager.switchTab(tabId);
}
export function handleGetOrderedServers() {
return ServerManager.getOrderedServers().map((srv) => srv.toMattermostTeam());
}
export function handleGetOrderedTabsForServer(event: IpcMainInvokeEvent, serverId: string) {
return ServerManager.getOrderedTabsForServer(serverId).map((tab) => tab.toMattermostTab());
}
export function handleGetLastActive() {
const server = ServerManager.getCurrentServer();
const tab = ServerManager.getLastActiveTabForServer(server.id);
return {server: server.id, tab: tab.id};
}
function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) {
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible});
@@ -126,101 +83,6 @@ export function handleMainWindowIsShown() {
}
}
export function handleNewServerModal() {
log.debug('handleNewServerModal');
const html = getLocalURLString('newServer.html');
const preload = getLocalPreload('desktopAPI.js');
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal<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() {
log.debug('handleWelcomeScreenModal');
@@ -236,7 +98,7 @@ export function handleWelcomeScreenModal() {
if (modalPromise) {
modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data);
WindowManager.switchServer(newTeam.id, true);
switchServer(newTeam.id, true);
}).catch((e) => {
// e is undefined for user cancellation
if (e) {

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/views/viewManager', () => ({}));
jest.mock('main/windows/mainWindow', () => ({}));
jest.mock('main/windows/windowManager', () => ({}));
jest.mock('./initialize', () => ({
mainProtocol: 'mattermost',

View File

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

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