diff --git a/src/main/app/servers.test.js b/src/app/serverViewState.test.js similarity index 82% rename from src/main/app/servers.test.js rename to src/app/serverViewState.test.js index 46175dd9..d4aa0a48 100644 --- a/src/main/app/servers.test.js +++ b/src/app/serverViewState.test.js @@ -12,10 +12,12 @@ import {getLocalURLString, getLocalPreload} from 'main/utils'; import MainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; -import * as Servers from './servers'; +import {ServerViewState} from './serverViewState'; jest.mock('electron', () => ({ ipcMain: { + on: jest.fn(), + handle: jest.fn(), emit: jest.fn(), }, })); @@ -32,6 +34,7 @@ jest.mock('common/servers/serverManager', () => ({ getLastActiveTabForServer: jest.fn(), getServerLog: jest.fn(), lookupViewByURL: jest.fn(), + getOrderedServers: jest.fn(), })); jest.mock('common/servers/MattermostServer', () => ({ MattermostServer: jest.fn(), @@ -84,8 +87,9 @@ const servers = [ }, ]; -describe('main/app/servers', () => { +describe('app/serverViewState', () => { describe('switchServer', () => { + const serverViewState = new ServerViewState(); const views = new Map([ ['view-1', {id: 'view-1'}], ['view-2', {id: 'view-2'}], @@ -125,26 +129,26 @@ describe('main/app/servers', () => { }); it('should do nothing if cannot find the server', () => { - Servers.switchServer('server-3'); + serverViewState.switchServer('server-3'); expect(ViewManager.showById).not.toBeCalled(); }); it('should show first open view in order when last active not defined', () => { ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-3'}); - Servers.switchServer('server-1'); + serverViewState.switchServer('server-1'); expect(ViewManager.showById).toHaveBeenCalledWith('view-3'); }); it('should show last active view of chosen server', () => { ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-2'}); - Servers.switchServer('server-2'); + serverViewState.switchServer('server-2'); expect(ViewManager.showById).toHaveBeenCalledWith('view-2'); }); it('should wait for view to exist if specified', () => { ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-3'}); views.delete('view-3'); - Servers.switchServer('server-1', true); + serverViewState.switchServer('server-1', true); expect(ViewManager.showById).not.toBeCalled(); jest.advanceTimersByTime(200); @@ -156,7 +160,8 @@ describe('main/app/servers', () => { }); }); - describe('handleNewServerModal', () => { + describe('showNewServerModal', () => { + const serverViewState = new ServerViewState(); let serversCopy; beforeEach(() => { @@ -196,7 +201,7 @@ describe('main/app/servers', () => { const promise = Promise.resolve(data); ModalManager.addModal.mockReturnValue(promise); - Servers.handleNewServerModal(); + serverViewState.showNewServerModal(); await promise; expect(ServerManager.addServer).toHaveBeenCalledWith(data); @@ -213,6 +218,7 @@ describe('main/app/servers', () => { }); describe('handleEditServerModal', () => { + const serverViewState = new ServerViewState(); let serversCopy; beforeEach(() => { @@ -241,7 +247,7 @@ describe('main/app/servers', () => { }); it('should do nothing when the server cannot be found', () => { - Servers.handleEditServerModal(null, 'bad-server'); + serverViewState.showEditServerModal(null, 'bad-server'); expect(ModalManager.addModal).not.toBeCalled(); }); @@ -252,7 +258,7 @@ describe('main/app/servers', () => { }); ModalManager.addModal.mockReturnValue(promise); - Servers.handleEditServerModal(null, 'server-1'); + serverViewState.showEditServerModal(null, 'server-1'); await promise; expect(serversCopy).not.toContainEqual(expect.objectContaining({ id: 'server-1', @@ -270,6 +276,7 @@ describe('main/app/servers', () => { }); describe('handleRemoveServerModal', () => { + const serverViewState = new ServerViewState(); let serversCopy; beforeEach(() => { @@ -278,23 +285,20 @@ describe('main/app/servers', () => { MainWindow.get.mockReturnValue({}); serversCopy = JSON.parse(JSON.stringify(servers)); - ServerManager.getServer.mockImplementation((id) => { - if (id !== serversCopy[0].id) { - return undefined; - } + ServerManager.getServer.mockImplementation(() => { return serversCopy[0]; }); ServerManager.removeServer.mockImplementation(() => { serversCopy = []; }); - ServerManager.getAllServers.mockReturnValue(serversCopy); + ServerManager.getOrderedServers.mockReturnValue(serversCopy); }); it('should remove the existing server', async () => { const promise = Promise.resolve(true); ModalManager.addModal.mockReturnValue(promise); - Servers.handleRemoveServerModal(null, 'server-1'); + serverViewState.showRemoveServerModal(null, 'server-1'); await promise; expect(serversCopy).not.toContainEqual(expect.objectContaining({ id: 'server-1', @@ -315,7 +319,7 @@ describe('main/app/servers', () => { tabs, })); - Servers.handleRemoveServerModal(null, 'server-1'); + serverViewState.showRemoveServerModal(null, 'server-1'); await promise; expect(serversCopy).toContainEqual(expect.objectContaining({ id: 'server-1', @@ -327,6 +331,8 @@ describe('main/app/servers', () => { }); describe('handleServerURLValidation', () => { + const serverViewState = new ServerViewState(); + beforeEach(() => { MattermostServer.mockImplementation(({url}) => ({url})); ServerInfo.mockImplementation(({url}) => ({ @@ -343,43 +349,43 @@ describe('main/app/servers', () => { }); it('should return Missing when you get no URL', async () => { - const result = await Servers.handleServerURLValidation({}); + const result = await serverViewState.handleServerURLValidation({}); expect(result.status).toBe(URLValidationStatus.Missing); }); it('should return Invalid when you pass in invalid characters', async () => { - const result = await Servers.handleServerURLValidation({}, '!@#$%^&*()!@#$%^&*()'); + const result = await serverViewState.handleServerURLValidation({}, '!@#$%^&*()!@#$%^&*()'); expect(result.status).toBe(URLValidationStatus.Invalid); }); it('should include HTTPS when missing', async () => { - const result = await Servers.handleServerURLValidation({}, 'server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'server.com'); expect(result.status).toBe(URLValidationStatus.OK); expect(result.validatedURL).toBe('https://server.com/'); }); it('should correct typos in the protocol', async () => { - const result = await Servers.handleServerURLValidation({}, 'htpst://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'htpst://server.com'); expect(result.status).toBe(URLValidationStatus.OK); expect(result.validatedURL).toBe('https://server.com/'); }); it('should replace HTTP with HTTPS when applicable', async () => { - const result = await Servers.handleServerURLValidation({}, 'http://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'http://server.com'); expect(result.status).toBe(URLValidationStatus.OK); expect(result.validatedURL).toBe('https://server.com/'); }); it('should generate a warning when the server already exists', async () => { ServerManager.lookupViewByURL.mockReturnValue({server: {id: 'server-1', url: new URL('https://server.com')}}); - const result = await Servers.handleServerURLValidation({}, 'https://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com'); expect(result.status).toBe(URLValidationStatus.URLExists); expect(result.validatedURL).toBe('https://server.com/'); }); it('should generate a warning if the server exists when editing', async () => { ServerManager.lookupViewByURL.mockReturnValue({server: {name: 'Server 1', id: 'server-1', url: new URL('https://server.com')}}); - const result = await Servers.handleServerURLValidation({}, 'https://server.com', 'server-2'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com', 'server-2'); expect(result.status).toBe(URLValidationStatus.URLExists); expect(result.validatedURL).toBe('https://server.com/'); expect(result.existingServerName).toBe('Server 1'); @@ -387,7 +393,7 @@ describe('main/app/servers', () => { it('should not generate a warning if editing the same server', async () => { ServerManager.lookupViewByURL.mockReturnValue({server: {name: 'Server 1', id: 'server-1', url: new URL('https://server.com')}}); - const result = await Servers.handleServerURLValidation({}, 'https://server.com', 'server-1'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com', 'server-1'); expect(result.status).toBe(URLValidationStatus.OK); expect(result.validatedURL).toBe('https://server.com/'); }); @@ -407,7 +413,7 @@ describe('main/app/servers', () => { }), })); - const result = await Servers.handleServerURLValidation({}, 'http://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'http://server.com'); expect(result.status).toBe(URLValidationStatus.Insecure); expect(result.validatedURL).toBe('http://server.com/'); }); @@ -419,7 +425,7 @@ describe('main/app/servers', () => { }), })); - const result = await Servers.handleServerURLValidation({}, 'https://not-server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'https://not-server.com'); expect(result.status).toBe(URLValidationStatus.NotMattermost); expect(result.validatedURL).toBe('https://not-server.com/'); }); @@ -435,7 +441,7 @@ describe('main/app/servers', () => { }), })); - const result = await Servers.handleServerURLValidation({}, 'https://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com'); expect(result.status).toBe(URLValidationStatus.URLUpdated); expect(result.validatedURL).toBe('https://mainserver.com/'); }); @@ -454,7 +460,7 @@ describe('main/app/servers', () => { }), })); - const result = await Servers.handleServerURLValidation({}, 'https://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com'); expect(result.status).toBe(URLValidationStatus.URLNotMatched); expect(result.validatedURL).toBe('https://server.com/'); }); @@ -471,10 +477,31 @@ describe('main/app/servers', () => { }), })); - const result = await Servers.handleServerURLValidation({}, 'https://server.com'); + const result = await serverViewState.handleServerURLValidation({}, 'https://server.com'); expect(result.status).toBe(URLValidationStatus.URLExists); expect(result.validatedURL).toBe('https://mainserver.com/'); expect(result.existingServerName).toBe('Server 1'); }); }); + + describe('handleCloseView', () => { + const serverViewState = new ServerViewState(); + + it('should close the specified view and switch to the next open view', () => { + ServerManager.getView.mockReturnValue({server: {id: 'server-1'}}); + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-2'}); + serverViewState.handleCloseView(null, 'view-3'); + expect(ServerManager.setViewIsOpen).toBeCalledWith('view-3', false); + expect(ViewManager.showById).toBeCalledWith('view-2'); + }); + }); + + describe('handleOpenView', () => { + const serverViewState = new ServerViewState(); + + it('should open the specified view', () => { + serverViewState.handleOpenView(null, 'view-1'); + expect(ViewManager.showById).toBeCalledWith('view-1'); + }); + }); }); diff --git a/src/app/serverViewState.ts b/src/app/serverViewState.ts new file mode 100644 index 00000000..71ea3a2b --- /dev/null +++ b/src/app/serverViewState.ts @@ -0,0 +1,380 @@ +// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. +// See LICENSE.txt for license information. + +import {IpcMainEvent, IpcMainInvokeEvent, ipcMain} from 'electron'; + +import {UniqueServer, Server} from 'types/config'; +import {URLValidationResult} from 'types/server'; + +import { + CLOSE_VIEW, + GET_LAST_ACTIVE, + GET_ORDERED_SERVERS, + GET_ORDERED_TABS_FOR_SERVER, + OPEN_VIEW, + SHOW_EDIT_SERVER_MODAL, + SHOW_NEW_SERVER_MODAL, + SHOW_REMOVE_SERVER_MODAL, + SWITCH_SERVER, + UPDATE_SERVER_ORDER, + UPDATE_SHORTCUT_MENU, + UPDATE_TAB_ORDER, + VALIDATE_SERVER_URL, +} from 'common/communication'; +import {Logger} from 'common/log'; +import ServerManager from 'common/servers/serverManager'; +import {MattermostServer} from 'common/servers/MattermostServer'; +import {isValidURI, isValidURL, parseURL} from 'common/utils/url'; +import {URLValidationStatus} from 'common/utils/constants'; +import Config from 'common/config'; + +import ViewManager from 'main/views/viewManager'; +import ModalManager from 'main/views/modalManager'; +import MainWindow from 'main/windows/mainWindow'; +import {getLocalPreload, getLocalURLString} from 'main/utils'; +import {ServerInfo} from 'main/server/serverInfo'; + +const log = new Logger('App', 'ServerViewState'); + +export class ServerViewState { + private currentServerId?: string; + + constructor() { + ipcMain.on(SWITCH_SERVER, (event, serverId) => this.switchServer(serverId)); + ipcMain.on(SHOW_NEW_SERVER_MODAL, this.showNewServerModal); + ipcMain.on(SHOW_EDIT_SERVER_MODAL, this.showEditServerModal); + ipcMain.on(SHOW_REMOVE_SERVER_MODAL, this.showRemoveServerModal); + ipcMain.handle(VALIDATE_SERVER_URL, this.handleServerURLValidation); + ipcMain.handle(GET_ORDERED_SERVERS, this.handleGetOrderedServers); + ipcMain.on(UPDATE_SERVER_ORDER, this.updateServerOrder); + + ipcMain.on(CLOSE_VIEW, this.handleCloseView); + ipcMain.on(OPEN_VIEW, this.handleOpenView); + ipcMain.handle(GET_LAST_ACTIVE, this.handleGetLastActive); + ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, this.handleGetOrderedViewsForServer); + ipcMain.on(UPDATE_TAB_ORDER, this.updateTabOrder); + } + + init = () => { + const orderedServers = ServerManager.getOrderedServers(); + if (orderedServers.length) { + if (Config.lastActiveServer && orderedServers[Config.lastActiveServer]) { + this.currentServerId = orderedServers[Config.lastActiveServer].id; + } else { + this.currentServerId = orderedServers[0].id; + } + } + } + + getCurrentServer = () => { + log.debug('getCurrentServer'); + + if (!this.currentServerId) { + throw new Error('No server set as current'); + } + const server = ServerManager.getServer(this.currentServerId); + if (!server) { + throw new Error('Current server does not exist'); + } + return server; + } + + 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; + } + this.currentServerId = serverId; + const nextView = ServerManager.getLastActiveTabForServer(serverId); + if (waitForViewToExist) { + const timeout = setInterval(() => { + if (ViewManager.getView(nextView.id)) { + ViewManager.showById(nextView.id); + clearInterval(timeout); + } + }, 100); + } else { + ViewManager.showById(nextView.id); + } + ipcMain.emit(UPDATE_SHORTCUT_MENU); + } + + selectNextView = () => { + this.selectView((order) => order + 1); + }; + + selectPreviousView = () => { + this.selectView((order, length) => (length + (order - 1))); + }; + + updateCurrentView = (serverId: string, viewId: string) => { + this.currentServerId = serverId; + ServerManager.updateLastActive(viewId); + } + + /** + * Server Modals + */ + + showNewServerModal = () => { + log.debug('showNewServerModal'); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + + const modalPromise = ModalManager.addModal( + 'newServer', + getLocalURLString('newServer.html'), + getLocalPreload('desktopAPI.js'), + null, + mainWindow, + !ServerManager.hasServers(), + ); + + modalPromise.then((data) => { + const newServer = ServerManager.addServer(data); + this.switchServer(newServer.id, true); + }).catch((e) => { + // e is undefined for user cancellation + if (e) { + log.error(`there was an error in the new server modal: ${e}`); + } + }); + }; + + private showEditServerModal = (e: IpcMainEvent, id: string) => { + log.debug('showEditServerModal', id); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + const server = ServerManager.getServer(id); + if (!server) { + return; + } + + const modalPromise = ModalManager.addModal( + 'editServer', + getLocalURLString('editServer.html'), + getLocalPreload('desktopAPI.js'), + server.toUniqueServer(), + mainWindow); + + 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}`); + } + }); + }; + + private showRemoveServerModal = (e: IpcMainEvent, id: string) => { + log.debug('handleRemoveServerModal', id); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + const server = ServerManager.getServer(id); + if (!server) { + return; + } + + const modalPromise = ModalManager.addModal( + 'removeServer', + getLocalURLString('removeServer.html'), + getLocalPreload('desktopAPI.js'), + server.name, + mainWindow, + ); + + modalPromise.then((remove) => { + if (remove) { + const remainingServers = ServerManager.getOrderedServers().filter((orderedServer) => server.id !== orderedServer.id); + if (this.currentServerId === server.id && remainingServers.length) { + this.currentServerId = remainingServers[0].id; + } + + if (!remainingServers.length) { + delete this.currentServerId; + } + + 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}`); + } + }); + }; + + /** + * IPC Handlers + */ + + private handleServerURLValidation = async (e: IpcMainInvokeEvent, url?: string, currentId?: string): Promise => { + log.debug('handleServerURLValidation', url, currentId); + + // If the URL is missing or null, reject + if (!url) { + return {status: URLValidationStatus.Missing}; + } + + let httpUrl = url; + if (!isValidURL(url)) { + // If it already includes the protocol, tell them it's invalid + if (isValidURI(url)) { + httpUrl = url.replace(/^(.+):/, 'https:'); + } else { + // Otherwise add HTTPS for them + httpUrl = `https://${url}`; + } + } + + // Make sure the final URL is valid + const parsedURL = parseURL(httpUrl); + if (!parsedURL) { + return {status: URLValidationStatus.Invalid}; + } + + // Try and add HTTPS to see if we can get a more secure URL + let secureURL = parsedURL; + if (parsedURL.protocol === 'http:') { + secureURL = parseURL(parsedURL.toString().replace(/^http:/, 'https:')) ?? parsedURL; + } + + // Tell the user if they already have a server for this URL + const existingServer = ServerManager.lookupViewByURL(secureURL, true); + if (existingServer && existingServer.server.id !== currentId) { + return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()}; + } + + // Try and get remote info from the most secure URL, otherwise use the insecure one + let remoteURL = secureURL; + let remoteInfo = await this.testRemoteServer(secureURL); + if (!remoteInfo) { + if (secureURL.toString() !== parsedURL.toString()) { + remoteURL = parsedURL; + remoteInfo = await this.testRemoteServer(parsedURL); + } + } + + // If we can't get the remote info, warn the user that this might not be the right URL + // If the original URL was invalid, don't replace that as they probably have a typo somewhere + if (!remoteInfo) { + return {status: URLValidationStatus.NotMattermost, validatedURL: parsedURL.toString()}; + } + + // If we were only able to connect via HTTP, warn the user that the connection is not secure + if (remoteURL.protocol === 'http:') { + return {status: URLValidationStatus.Insecure, serverVersion: remoteInfo.serverVersion, validatedURL: remoteURL.toString()}; + } + + // If the URL doesn't match the Site URL, set the URL to the correct one + if (remoteInfo.siteURL && remoteURL.toString() !== new URL(remoteInfo.siteURL).toString()) { + const parsedSiteURL = parseURL(remoteInfo.siteURL); + if (parsedSiteURL) { + // Check the Site URL as well to see if it's already pre-configured + const existingServer = ServerManager.lookupViewByURL(parsedSiteURL, true); + if (existingServer && existingServer.server.id !== currentId) { + return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()}; + } + + // If we can't reach the remote Site URL, there's probably a configuration issue + const remoteSiteURLInfo = await this.testRemoteServer(parsedSiteURL); + if (!remoteSiteURLInfo) { + return {status: URLValidationStatus.URLNotMatched, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteURL.toString()}; + } + } + + // Otherwise fix it for them and return + return {status: URLValidationStatus.URLUpdated, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL}; + } + + return {status: URLValidationStatus.OK, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL}; + }; + + private handleCloseView = (event: IpcMainEvent, viewId: string) => { + log.debug('handleCloseView', {viewId}); + + const view = ServerManager.getView(viewId); + if (!view) { + return; + } + ServerManager.setViewIsOpen(viewId, false); + const nextView = ServerManager.getLastActiveTabForServer(view.server.id); + ViewManager.showById(nextView.id); + }; + + private handleOpenView = (event: IpcMainEvent, viewId: string) => { + log.debug('handleOpenView', {viewId}); + + ServerManager.setViewIsOpen(viewId, true); + ViewManager.showById(viewId); + }; + + private handleGetOrderedViewsForServer = (event: IpcMainInvokeEvent, serverId: string) => { + return ServerManager.getOrderedTabsForServer(serverId).map((view) => view.toUniqueView()); + }; + + private handleGetLastActive = () => { + const server = this.getCurrentServer(); + const view = ServerManager.getLastActiveTabForServer(server.id); + return {server: server.id, view: view.id}; + }; + + private updateServerOrder = (event: IpcMainEvent, serverOrder: string[]) => ServerManager.updateServerOrder(serverOrder); + private updateTabOrder = (event: IpcMainEvent, serverId: string, viewOrder: string[]) => ServerManager.updateTabOrder(serverId, viewOrder); + + private handleGetOrderedServers = () => ServerManager.getOrderedServers().map((srv) => srv.toUniqueServer()); + + /** + * Helper functions + */ + + private testRemoteServer = async (parsedURL: URL) => { + const server = new MattermostServer({name: 'temp', url: parsedURL.toString()}, false); + const serverInfo = new ServerInfo(server); + try { + const remoteInfo = await serverInfo.fetchRemoteInfo(); + return remoteInfo; + } catch (error) { + return undefined; + } + }; + + private selectView = (fn: (order: number, length: number) => number) => { + const currentView = ViewManager.getCurrentView(); + if (!currentView) { + return; + } + + const currentServerViews = ServerManager.getOrderedTabsForServer(currentView.view.server.id).map((view, index) => ({view, index})); + const filteredViews = currentServerViews?.filter((view) => view.view.isOpen); + const currentServerView = currentServerViews?.find((view) => view.view.type === currentView.view.type); + if (!currentServerViews || !currentServerView || !filteredViews) { + return; + } + + let currentOrder = currentServerView.index; + let nextIndex = -1; + while (nextIndex === -1) { + const nextOrder = (fn(currentOrder, currentServerViews.length) % currentServerViews.length); + nextIndex = filteredViews.findIndex((view) => view.index === nextOrder); + currentOrder = nextOrder; + } + + const newView = filteredViews[nextIndex].view; + ViewManager.showById(newView.id); + }; +} + +const serverViewState = new ServerViewState(); +export default serverViewState; diff --git a/src/common/servers/serverManager.ts b/src/common/servers/serverManager.ts index 1820d034..3d225a8f 100644 --- a/src/common/servers/serverManager.ts +++ b/src/common/servers/serverManager.ts @@ -26,7 +26,6 @@ export class ServerManager extends EventEmitter { private servers: Map; private remoteInfo: Map; private serverOrder: string[]; - private currentServerId?: string; private views: Map; private viewOrder: Map; @@ -71,19 +70,6 @@ export class ServerManager extends EventEmitter { }, [] as MattermostServer[]); } - getCurrentServer = () => { - log.debug('getCurrentServer'); - - if (!this.currentServerId) { - throw new Error('No server set as current'); - } - const server = this.servers.get(this.currentServerId); - if (!server) { - throw new Error('Current server does not exist'); - } - return server; - } - getLastActiveTabForServer = (serverId: string) => { log.withPrefix(serverId).debug('getLastActiveTabForServer'); @@ -187,10 +173,6 @@ export class ServerManager extends EventEmitter { }); this.viewOrder.set(newServer.id, viewOrder); - if (!this.currentServerId) { - this.currentServerId = newServer.id; - } - // Emit this event whenever we update a server URL to ensure remote info is fetched this.emit(SERVERS_URL_MODIFIED, [newServer.id]); this.persistServers(); @@ -234,14 +216,6 @@ export class ServerManager extends EventEmitter { this.remoteInfo.delete(serverId); this.servers.delete(serverId); - if (this.currentServerId === serverId && this.hasServers()) { - this.currentServerId = this.serverOrder[0]; - } - - if (!this.hasServers()) { - delete this.currentServerId; - } - this.persistServers(); } @@ -262,8 +236,6 @@ export class ServerManager extends EventEmitter { } this.lastActiveView.set(view.server.id, viewId); - this.currentServerId = view.server.id; - const serverOrder = this.serverOrder.findIndex((srv) => srv === view.server.id); if (serverOrder < 0) { throw new Error('Server order corrupt, ID not found.'); @@ -286,11 +258,6 @@ export class ServerManager extends EventEmitter { } this.filterOutDuplicateServers(); this.serverOrder = serverOrder; - if (Config.lastActiveServer && this.serverOrder[Config.lastActiveServer]) { - this.currentServerId = this.serverOrder[Config.lastActiveServer]; - } else { - this.currentServerId = this.serverOrder[0]; - } } private filterOutDuplicateServers = () => { diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index cd764cca..39ed95a6 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -97,6 +97,9 @@ jest.mock('../../../electron-builder.json', () => ([ }, ])); +jest.mock('app/serverViewState', () => ({ + init: jest.fn(), +})); jest.mock('common/config', () => ({ once: jest.fn(), on: jest.fn(), @@ -120,7 +123,6 @@ 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(), diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index c029c2f4..de9f7fb9 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -8,16 +8,9 @@ import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-install import isDev from 'electron-is-dev'; import { - SWITCH_SERVER, FOCUS_BROWSERVIEW, QUIT, - SHOW_NEW_SERVER_MODAL, NOTIFY_MENTION, - SWITCH_TAB, - CLOSE_VIEW, - OPEN_VIEW, - SHOW_EDIT_SERVER_MODAL, - SHOW_REMOVE_SERVER_MODAL, UPDATE_SHORTCUT_MENU, GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, USER_ACTIVITY_UPDATE, @@ -29,11 +22,6 @@ import { GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, UPDATE_PATHS, - UPDATE_SERVER_ORDER, - UPDATE_TAB_ORDER, - GET_LAST_ACTIVE, - GET_ORDERED_SERVERS, - GET_ORDERED_TABS_FOR_SERVER, SERVERS_URL_MODIFIED, GET_DARK_MODE, WINDOW_CLOSE, @@ -41,7 +29,6 @@ import { WINDOW_MINIMIZE, WINDOW_RESTORE, DOUBLE_CLICK_ON_WINDOW, - VALIDATE_SERVER_URL, } from 'common/communication'; import Config from 'common/config'; import {isTrustedURL, parseURL} from 'common/utils/url'; @@ -94,16 +81,6 @@ import { handleQuit, handlePingDomain, } from './intercom'; -import { - handleEditServerModal, - handleNewServerModal, - handleRemoveServerModal, - handleServerURLValidation, - switchServer, -} from './servers'; -import { - handleCloseView, handleGetLastActive, handleGetOrderedViewsForServer, handleOpenView, -} from './views'; import { clearAppCache, getDeeplinkingURL, @@ -279,17 +256,8 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu); } - ipcMain.on(SWITCH_SERVER, (event, serverId) => switchServer(serverId)); - ipcMain.on(SWITCH_TAB, (event, viewId) => ViewManager.showById(viewId)); - ipcMain.on(CLOSE_VIEW, handleCloseView); - ipcMain.on(OPEN_VIEW, handleOpenView); - ipcMain.on(QUIT, handleQuit); - ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal); - ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal); - ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal); - ipcMain.handle(VALIDATE_SERVER_URL, handleServerURLValidation); ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages); ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload); ipcMain.on(START_UPGRADE, handleStartUpgrade); @@ -298,12 +266,6 @@ function initializeInterCommunicationEventListeners() { ipcMain.handle(GET_LOCAL_CONFIGURATION, handleGetLocalConfiguration); ipcMain.on(UPDATE_CONFIGURATION, updateConfiguration); - ipcMain.on(UPDATE_SERVER_ORDER, (event, serverOrder) => ServerManager.updateServerOrder(serverOrder)); - ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, viewOrder) => ServerManager.updateTabOrder(serverId, viewOrder)); - ipcMain.handle(GET_LAST_ACTIVE, handleGetLastActive); - ipcMain.handle(GET_ORDERED_SERVERS, () => ServerManager.getOrderedServers().map((srv) => srv.toUniqueServer())); - ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, handleGetOrderedViewsForServer); - ipcMain.handle(GET_DARK_MODE, handleGetDarkMode); ipcMain.on(WINDOW_CLOSE, handleClose); ipcMain.on(WINDOW_MAXIMIZE, handleMaximize); diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index 7ce84648..b18e7884 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -11,6 +11,7 @@ import { handleMainWindowIsShown, } from './intercom'; +jest.mock('app/serverViewState', () => ({})); jest.mock('common/config', () => ({ setServers: jest.fn(), })); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index 0e5ebb6e..6625764f 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -6,6 +6,8 @@ import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; import {UniqueServer} from 'types/config'; import {MentionData} from 'types/notification'; +import ServerViewState from 'app/serverViewState'; + import {Logger} from 'common/log'; import ServerManager from 'common/servers/serverManager'; import {ping} from 'common/utils/requests'; @@ -16,7 +18,6 @@ import ModalManager from 'main/views/modalManager'; import MainWindow from 'main/windows/mainWindow'; import {handleAppBeforeQuit} from './app'; -import {handleNewServerModal, switchServer} from './servers'; const log = new Logger('App.Intercom'); @@ -58,7 +59,7 @@ function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerMo return; } if (showNewServerModal) { - handleNewServerModal(); + ServerViewState.showNewServerModal(); } } @@ -101,7 +102,7 @@ export function handleWelcomeScreenModal() { if (modalPromise) { modalPromise.then((data) => { const newServer = ServerManager.addServer(data); - switchServer(newServer.id, true); + ServerViewState.switchServer(newServer.id, true); }).catch((e) => { // e is undefined for user cancellation if (e) { diff --git a/src/main/app/servers.ts b/src/main/app/servers.ts deleted file mode 100644 index 7d2ac05c..00000000 --- a/src/main/app/servers.ts +++ /dev/null @@ -1,229 +0,0 @@ -// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. -// See LICENSE.txt for license information. - -import {IpcMainEvent, IpcMainInvokeEvent, ipcMain} from 'electron'; - -import {UniqueServer, Server} from 'types/config'; -import {URLValidationResult} from 'types/server'; - -import {UPDATE_SHORTCUT_MENU} from 'common/communication'; -import {Logger} from 'common/log'; -import ServerManager from 'common/servers/serverManager'; -import {MattermostServer} from 'common/servers/MattermostServer'; -import {isValidURI, isValidURL, parseURL} from 'common/utils/url'; -import {URLValidationStatus} from 'common/utils/constants'; - -import ViewManager from 'main/views/viewManager'; -import ModalManager from 'main/views/modalManager'; -import MainWindow from 'main/windows/mainWindow'; -import {getLocalPreload, getLocalURLString} from 'main/utils'; -import {ServerInfo} from 'main/server/serverInfo'; - -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 nextView = ServerManager.getLastActiveTabForServer(serverId); - if (waitForViewToExist) { - const timeout = setInterval(() => { - if (ViewManager.getView(nextView.id)) { - ViewManager.showById(nextView.id); - clearInterval(timeout); - } - }, 100); - } else { - ViewManager.showById(nextView.id); - } - ipcMain.emit(UPDATE_SHORTCUT_MENU); -}; - -export const handleNewServerModal = () => { - log.debug('handleNewServerModal'); - - const html = getLocalURLString('newServer.html'); - - const preload = getLocalPreload('desktopAPI.js'); - - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - const modalPromise = ModalManager.addModal('newServer', html, preload, null, mainWindow, !ServerManager.hasServers()); - if (modalPromise) { - modalPromise.then((data) => { - const newServer = ServerManager.addServer(data); - switchServer(newServer.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( - 'editServer', - html, - preload, - server.toUniqueServer(), - mainWindow); - if (modalPromise) { - modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => { - // e is undefined for user cancellation - if (e) { - log.error(`there was an error in the edit server modal: ${e}`); - } - }); - } else { - log.warn('There is already an edit server modal'); - } -}; - -export const handleRemoveServerModal = (e: IpcMainEvent, id: string) => { - log.debug('handleRemoveServerModal', id); - - const html = getLocalURLString('removeServer.html'); - - const preload = getLocalPreload('desktopAPI.js'); - - const server = ServerManager.getServer(id); - if (!server) { - return; - } - const mainWindow = MainWindow.get(); - if (!mainWindow) { - return; - } - const modalPromise = ModalManager.addModal('removeServer', html, preload, server.name, mainWindow); - if (modalPromise) { - modalPromise.then((remove) => { - if (remove) { - ServerManager.removeServer(server.id); - } - }).catch((e) => { - // e is undefined for user cancellation - if (e) { - log.error(`there was an error in the edit server modal: ${e}`); - } - }); - } else { - log.warn('There is already an edit server modal'); - } -}; - -export const handleServerURLValidation = async (e: IpcMainInvokeEvent, url?: string, currentId?: string): Promise => { - log.debug('handleServerURLValidation', url, currentId); - - // If the URL is missing or null, reject - if (!url) { - return {status: URLValidationStatus.Missing}; - } - - let httpUrl = url; - if (!isValidURL(url)) { - // If it already includes the protocol, tell them it's invalid - if (isValidURI(url)) { - httpUrl = url.replace(/^(.+):/, 'https:'); - } else { - // Otherwise add HTTPS for them - httpUrl = `https://${url}`; - } - } - - // Make sure the final URL is valid - const parsedURL = parseURL(httpUrl); - if (!parsedURL) { - return {status: URLValidationStatus.Invalid}; - } - - // Try and add HTTPS to see if we can get a more secure URL - let secureURL = parsedURL; - if (parsedURL.protocol === 'http:') { - secureURL = parseURL(parsedURL.toString().replace(/^http:/, 'https:')) ?? parsedURL; - } - - // Tell the user if they already have a server for this URL - const existingServer = ServerManager.lookupViewByURL(secureURL, true); - if (existingServer && existingServer.server.id !== currentId) { - return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()}; - } - - // Try and get remote info from the most secure URL, otherwise use the insecure one - let remoteURL = secureURL; - let remoteInfo = await testRemoteServer(secureURL); - if (!remoteInfo) { - if (secureURL.toString() !== parsedURL.toString()) { - remoteURL = parsedURL; - remoteInfo = await testRemoteServer(parsedURL); - } - } - - // If we can't get the remote info, warn the user that this might not be the right URL - // If the original URL was invalid, don't replace that as they probably have a typo somewhere - if (!remoteInfo) { - return {status: URLValidationStatus.NotMattermost, validatedURL: parsedURL.toString()}; - } - - // If we were only able to connect via HTTP, warn the user that the connection is not secure - if (remoteURL.protocol === 'http:') { - return {status: URLValidationStatus.Insecure, serverVersion: remoteInfo.serverVersion, validatedURL: remoteURL.toString()}; - } - - // If the URL doesn't match the Site URL, set the URL to the correct one - if (remoteInfo.siteURL && remoteURL.toString() !== new URL(remoteInfo.siteURL).toString()) { - const parsedSiteURL = parseURL(remoteInfo.siteURL); - if (parsedSiteURL) { - // Check the Site URL as well to see if it's already pre-configured - const existingServer = ServerManager.lookupViewByURL(parsedSiteURL, true); - if (existingServer && existingServer.server.id !== currentId) { - return {status: URLValidationStatus.URLExists, existingServerName: existingServer.server.name, validatedURL: existingServer.server.url.toString()}; - } - - // If we can't reach the remote Site URL, there's probably a configuration issue - const remoteSiteURLInfo = await testRemoteServer(parsedSiteURL); - if (!remoteSiteURLInfo) { - return {status: URLValidationStatus.URLNotMatched, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteURL.toString()}; - } - } - - // Otherwise fix it for them and return - return {status: URLValidationStatus.URLUpdated, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL}; - } - - return {status: URLValidationStatus.OK, serverVersion: remoteInfo.serverVersion, serverName: remoteInfo.siteName, validatedURL: remoteInfo.siteURL}; -}; - -const testRemoteServer = async (parsedURL: URL) => { - const server = new MattermostServer({name: 'temp', url: parsedURL.toString()}, false); - const serverInfo = new ServerInfo(server); - try { - const remoteInfo = await serverInfo.fetchRemoteInfo(); - return remoteInfo; - } catch (error) { - return undefined; - } -}; diff --git a/src/main/app/views.test.js b/src/main/app/views.test.js deleted file mode 100644 index bccd3420..00000000 --- a/src/main/app/views.test.js +++ /dev/null @@ -1,39 +0,0 @@ -// 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 { - handleCloseView, - handleOpenView, -} from './views'; - -jest.mock('common/servers/serverManager', () => ({ - setViewIsOpen: jest.fn(), - getView: jest.fn(), - getLastActiveTabForServer: jest.fn(), -})); - -jest.mock('main/views/viewManager', () => ({ - showById: jest.fn(), -})); - -describe('main/app/views', () => { - describe('handleCloseView', () => { - it('should close the specified view and switch to the next open view', () => { - ServerManager.getView.mockReturnValue({server: {id: 'server-1'}}); - ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-2'}); - handleCloseView(null, 'view-3'); - expect(ServerManager.setViewIsOpen).toBeCalledWith('view-3', false); - expect(ViewManager.showById).toBeCalledWith('view-2'); - }); - }); - - describe('handleOpenView', () => { - it('should open the specified view', () => { - handleOpenView(null, 'view-1'); - expect(ViewManager.showById).toBeCalledWith('view-1'); - }); - }); -}); diff --git a/src/main/app/views.ts b/src/main/app/views.ts deleted file mode 100644 index 477d0bf7..00000000 --- a/src/main/app/views.ts +++ /dev/null @@ -1,73 +0,0 @@ -// 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.Views'); - -export const handleCloseView = (event: IpcMainEvent, viewId: string) => { - log.debug('handleCloseView', {viewId}); - - const view = ServerManager.getView(viewId); - if (!view) { - return; - } - ServerManager.setViewIsOpen(viewId, false); - const nextView = ServerManager.getLastActiveTabForServer(view.server.id); - ViewManager.showById(nextView.id); -}; - -export const handleOpenView = (event: IpcMainEvent, viewId: string) => { - log.debug('handleOpenView', {viewId}); - - ServerManager.setViewIsOpen(viewId, true); - ViewManager.showById(viewId); -}; - -export const selectNextView = () => { - selectView((order) => order + 1); -}; - -export const selectPreviousView = () => { - selectView((order, length) => (length + (order - 1))); -}; - -export const handleGetOrderedViewsForServer = (event: IpcMainInvokeEvent, serverId: string) => { - return ServerManager.getOrderedTabsForServer(serverId).map((view) => view.toUniqueView()); -}; - -export const handleGetLastActive = () => { - const server = ServerManager.getCurrentServer(); - const view = ServerManager.getLastActiveTabForServer(server.id); - return {server: server.id, view: view.id}; -}; - -const selectView = (fn: (order: number, length: number) => number) => { - const currentView = ViewManager.getCurrentView(); - if (!currentView) { - return; - } - - const currentServerViews = ServerManager.getOrderedTabsForServer(currentView.view.server.id).map((view, index) => ({view, index})); - const filteredViews = currentServerViews?.filter((view) => view.view.isOpen); - const currentServerView = currentServerViews?.find((view) => view.view.type === currentView.view.type); - if (!currentServerViews || !currentServerView || !filteredViews) { - return; - } - - let currentOrder = currentServerView.index; - let nextIndex = -1; - while (nextIndex === -1) { - const nextOrder = (fn(currentOrder, currentServerViews.length) % currentServerViews.length); - nextIndex = filteredViews.findIndex((view) => view.index === nextOrder); - currentOrder = nextOrder; - } - - const newView = filteredViews[nextIndex].view; - ViewManager.showById(newView.id); -}; diff --git a/src/main/menus/app.test.js b/src/main/menus/app.test.js index 067ae0f6..5b3a931b 100644 --- a/src/main/menus/app.test.js +++ b/src/main/menus/app.test.js @@ -8,6 +8,8 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state import {localizeMessage} from 'main/i18nManager'; import ServerManager from 'common/servers/serverManager'; +import ServerViewState from 'app/serverViewState'; + import {createTemplate} from './app'; jest.mock('electron', () => { @@ -51,12 +53,12 @@ jest.mock('main/i18nManager', () => ({ })); jest.mock('common/servers/serverManager', () => ({ hasServers: jest.fn(), - getCurrentServer: jest.fn(), getOrderedServers: jest.fn(), getOrderedTabsForServer: jest.fn(), })); -jest.mock('main/app/servers', () => ({ +jest.mock('app/serverViewState', () => ({ switchServer: jest.fn(), + getCurrentServer: jest.fn(), })); jest.mock('main/diagnostics', () => ({})); jest.mock('main/downloadsManager', () => ({ @@ -107,7 +109,7 @@ describe('main/menus/app', () => { ]; beforeEach(() => { - ServerManager.getCurrentServer.mockReturnValue(servers[0]); + ServerViewState.getCurrentServer.mockReturnValue(servers[0]); ServerManager.getOrderedServers.mockReturnValue(servers); ServerManager.getOrderedTabsForServer.mockReturnValue(views); getDarwinDoNotDisturb.mockReturnValue(false); @@ -278,7 +280,7 @@ describe('main/menus/app', () => { } return id; }); - ServerManager.getCurrentServer.mockImplementation(() => ({id: servers[0].id})); + ServerViewState.getCurrentServer.mockImplementation(() => ({id: servers[0].id})); const modifiedViews = [...Array(15).keys()].map((key) => ({ id: `view-${key}`, diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index 92daa825..f4ce963b 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -6,6 +6,8 @@ import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron'; import log from 'electron-log'; +import ServerViewState from 'app/serverViewState'; + import {OPEN_SERVERS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication'; import {t} from 'common/utils/util'; import {getViewDisplayName, ViewType} from 'common/views/View'; @@ -18,8 +20,6 @@ import downloadsManager from 'main/downloadsManager'; import Diagnostics from 'main/diagnostics'; import ViewManager from 'main/views/viewManager'; import SettingsWindow from 'main/windows/settingsWindow'; -import {selectNextView, selectPreviousView} from 'main/app/views'; -import {switchServer} from 'main/app/servers'; export function createTemplate(config: Config, updateManager: UpdateManager) { const separatorItem: MenuItemConstructorOptions = { @@ -266,10 +266,10 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: server.name, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, click() { - switchServer(server.id); + ServerViewState.switchServer(server.id); }, }); - if (ServerManager.getCurrentServer().id === server.id) { + if (ServerViewState.getCurrentServer().id === server.id) { ServerManager.getOrderedTabsForServer(server.id).slice(0, 9).forEach((view, i) => { items.push({ label: ` ${localizeMessage(`common.views.${view.type}`, getViewDisplayName(view.type as ViewType))}`, @@ -285,14 +285,14 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'), accelerator: 'Ctrl+Tab', click() { - selectNextView(); + ServerViewState.selectNextView(); }, enabled: (servers.length > 1), }, { label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'), accelerator: 'Ctrl+Shift+Tab', click() { - selectPreviousView(); + ServerViewState.selectPreviousView(); }, enabled: (servers.length > 1), }, ...(isMac ? [separatorItem, { diff --git a/src/main/menus/tray.test.js b/src/main/menus/tray.test.js index 288aff6b..a58559cc 100644 --- a/src/main/menus/tray.test.js +++ b/src/main/menus/tray.test.js @@ -14,7 +14,7 @@ jest.mock('main/i18nManager', () => ({ jest.mock('common/servers/serverManager', () => ({ getOrderedServers: jest.fn(), })); -jest.mock('main/app/servers', () => ({ +jest.mock('app/serverViewState', () => ({ switchServer: jest.fn(), })); jest.mock('main/windows/settingsWindow', () => ({})); diff --git a/src/main/menus/tray.ts b/src/main/menus/tray.ts index 748cadcf..cc552b49 100644 --- a/src/main/menus/tray.ts +++ b/src/main/menus/tray.ts @@ -5,11 +5,12 @@ import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron'; +import ServerViewState from 'app/serverViewState'; + import ServerManager from 'common/servers/serverManager'; import {localizeMessage} from 'main/i18nManager'; import SettingsWindow from 'main/windows/settingsWindow'; -import {switchServer} from 'main/app/servers'; export function createTemplate() { const servers = ServerManager.getOrderedServers(); @@ -18,7 +19,7 @@ export function createTemplate() { return { label: server.name.length > 50 ? `${server.name.slice(0, 50)}...` : server.name, click: () => { - switchServer(server.id); + ServerViewState.switchServer(server.id); }, }; }), { diff --git a/src/main/views/serverDropdownView.test.js b/src/main/views/serverDropdownView.test.js index 1ee96eb7..011bcdcc 100644 --- a/src/main/views/serverDropdownView.test.js +++ b/src/main/views/serverDropdownView.test.js @@ -9,6 +9,8 @@ import MainWindow from 'main/windows/mainWindow'; import {ServerDropdownView} from './serverDropdownView'; +jest.mock('app/serverViewState', () => ({})); + jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, getLocalURLString: (file) => file, diff --git a/src/main/views/serverDropdownView.ts b/src/main/views/serverDropdownView.ts index 92d5bf02..f3d9751f 100644 --- a/src/main/views/serverDropdownView.ts +++ b/src/main/views/serverDropdownView.ts @@ -5,6 +5,8 @@ import {BrowserView, ipcMain, IpcMainEvent} from 'electron'; import {UniqueServer} from 'types/config'; +import ServerViewState from 'app/serverViewState'; + import AppState from 'common/appState'; import { CLOSE_SERVERS_DROPDOWN, @@ -98,7 +100,7 @@ export class ServerDropdownView { this.servers, Config.darkMode, this.windowBounds, - ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined, + ServerManager.hasServers() ? ServerViewState.getCurrentServer().id : undefined, Config.enableServerManagement, this.hasGPOServers, this.expired, diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index cbdc9657..f2f01f6e 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -6,6 +6,8 @@ import {dialog} from 'electron'; +import ServerViewState from 'app/serverViewState'; + import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication'; import {TAB_MESSAGING} from 'common/views/View'; import ServerManager from 'common/servers/serverManager'; @@ -30,6 +32,11 @@ jest.mock('electron', () => ({ handle: jest.fn(), }, })); +jest.mock('app/serverViewState', () => ({ + getCurrentServer: jest.fn(), + updateCurrentView: jest.fn(), + init: jest.fn(), +})); jest.mock('common/views/View', () => ({ getViewName: jest.fn((a, b) => `${a}-${b}`), TAB_MESSAGING: 'view', @@ -70,13 +77,11 @@ jest.mock('main/windows/mainWindow', () => ({ on: jest.fn(), })); jest.mock('common/servers/serverManager', () => ({ - getCurrentServer: jest.fn(), getOrderedTabsForServer: jest.fn(), getAllServers: jest.fn(), hasServers: jest.fn(), getLastActiveServer: jest.fn(), getLastActiveTabForServer: jest.fn(), - updateLastActive: jest.fn(), lookupViewByURL: jest.fn(), getRemoteInfo: jest.fn(), on: jest.fn(), @@ -351,7 +356,7 @@ describe('main/views/viewManager', () => { viewManager.showById = jest.fn(); MainWindow.get.mockReturnValue(window); ServerManager.hasServers.mockReturnValue(true); - ServerManager.getCurrentServer.mockReturnValue({id: 'server-0'}); + ServerViewState.getCurrentServer.mockReturnValue({id: 'server-0'}); }); afterEach(() => { @@ -440,7 +445,7 @@ describe('main/views/viewManager', () => { beforeEach(() => { ServerManager.getAllServers.mockReturnValue(servers); - ServerManager.getCurrentServer.mockReturnValue(servers[0]); + ServerViewState.getCurrentServer.mockReturnValue(servers[0]); urlUtils.cleanPathName.mockImplementation((base, path) => path); }); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 4b853ce2..4c8aa62c 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -3,6 +3,8 @@ import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron'; +import ServerViewState from 'app/serverViewState'; + import AppState from 'common/appState'; import {SECOND, TAB_BAR_HEIGHT} from 'common/utils/constants'; import { @@ -27,6 +29,7 @@ import { MAIN_WINDOW_CREATED, MAIN_WINDOW_RESIZED, MAIN_WINDOW_FOCUSED, + SWITCH_TAB, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -74,6 +77,8 @@ export class ViewManager { ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); ipcMain.on(SESSION_EXPIRED, this.handleSessionExpired); + ipcMain.on(SWITCH_TAB, (event, viewId) => this.showById(viewId)); + ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration); } @@ -127,7 +132,7 @@ export class ViewManager { } hidePrevious?.(); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.view.server.id, newView.view.id); - ServerManager.updateLastActive(newView.view.id); + ServerViewState.updateCurrentView(newView.view.server.id, newView.view.id); } else { this.getViewLogger(viewId).warn(`Couldn't find a view with name: ${viewId}`); } @@ -263,8 +268,10 @@ export class ViewManager { private showInitial = () => { log.verbose('showInitial'); + // TODO: This init should be happening elsewhere, future refactor will fix this + ServerViewState.init(); if (ServerManager.hasServers()) { - const lastActiveServer = ServerManager.getCurrentServer(); + const lastActiveServer = ServerViewState.getCurrentServer(); const lastActiveView = ServerManager.getLastActiveTabForServer(lastActiveServer.id); this.showById(lastActiveView.id); } else { @@ -485,7 +492,7 @@ export class ViewManager { return; } let redirectedView = this.getView(redirectedviewId) || currentView; - if (redirectedView !== currentView && redirectedView?.view.server.id === ServerManager.getCurrentServer().id && redirectedView?.isLoggedIn) { + if (redirectedView !== currentView && redirectedView?.view.server.id === ServerViewState.getCurrentServer().id && redirectedView?.isLoggedIn) { log.info('redirecting to a new view', redirectedView?.id || viewId); this.showById(redirectedView?.id || viewId); } else { diff --git a/src/main/windows/callsWidgetWindow.test.js b/src/main/windows/callsWidgetWindow.test.js index bf50aea3..84e585c8 100644 --- a/src/main/windows/callsWidgetWindow.test.js +++ b/src/main/windows/callsWidgetWindow.test.js @@ -5,6 +5,8 @@ import {BrowserWindow, desktopCapturer, systemPreferences} from 'electron'; +import ServerViewState from 'app/serverViewState'; + import {CALLS_WIDGET_SHARE_SCREEN, CALLS_JOINED_CALL} from 'common/communication'; import { MINIMUM_CALLS_WIDGET_WIDTH, @@ -13,7 +15,6 @@ import { } from 'common/utils/constants'; import urlUtils from 'common/utils/url'; -import {switchServer} from 'main/app/servers'; import MainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; import { @@ -56,7 +57,7 @@ jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), focus: jest.fn(), })); -jest.mock('main/app/servers', () => ({ +jest.mock('app/serverViewState', () => ({ switchServer: jest.fn(), })); jest.mock('main/views/viewManager', () => ({ @@ -810,7 +811,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should switch server', () => { callsWidgetWindow.handleDesktopSourcesModalRequest(); - expect(switchServer).toHaveBeenCalledWith('server-1'); + expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); }); }); @@ -877,7 +878,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should switch server', () => { callsWidgetWindow.handleCallsWidgetChannelLinkClick(); - expect(switchServer).toHaveBeenCalledWith('server-2'); + expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2'); }); }); @@ -903,7 +904,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should focus view and propagate error to main view', () => { callsWidgetWindow.handleCallsError('', {err: 'client-error'}); - expect(switchServer).toHaveBeenCalledWith('server-2'); + expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-2'); expect(focus).toHaveBeenCalled(); expect(callsWidgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'}); }); @@ -931,7 +932,7 @@ describe('main/windows/callsWidgetWindow', () => { it('should pass through the click link to browser history push', () => { callsWidgetWindow.handleCallsLinkClick('', {link: '/other/subpath'}); - expect(switchServer).toHaveBeenCalledWith('server-1'); + expect(ServerViewState.switchServer).toHaveBeenCalledWith('server-1'); expect(view.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath'); }); }); diff --git a/src/main/windows/callsWidgetWindow.ts b/src/main/windows/callsWidgetWindow.ts index d005366f..157c2521 100644 --- a/src/main/windows/callsWidgetWindow.ts +++ b/src/main/windows/callsWidgetWindow.ts @@ -14,14 +14,7 @@ import { CallsWidgetWindowConfig, } from 'types/calls'; -import {MattermostBrowserView} from 'main/views/MattermostBrowserView'; - -import { - composeUserAgent, - getLocalPreload, - openScreensharePermissionsSettingsMacOS, - resetScreensharePermissionsMacOS, -} from 'main/utils'; +import ServerViewState from 'app/serverViewState'; import {Logger} from 'common/log'; import {CALLS_PLUGIN_ID, MINIMUM_CALLS_WIDGET_HEIGHT, MINIMUM_CALLS_WIDGET_WIDTH} from 'common/utils/constants'; @@ -43,7 +36,13 @@ import { DISPATCH_GET_DESKTOP_SOURCES, } from 'common/communication'; -import {switchServer} from 'main/app/servers'; +import {MattermostBrowserView} from 'main/views/MattermostBrowserView'; +import { + composeUserAgent, + getLocalPreload, + openScreensharePermissionsSettingsMacOS, + resetScreensharePermissionsMacOS, +} from 'main/utils'; import webContentsEventManager from 'main/views/webContentEvents'; import MainWindow from 'main/windows/mainWindow'; import ViewManager from 'main/views/viewManager'; @@ -485,7 +484,7 @@ export class CallsWidgetWindow { return; } - switchServer(this.serverID); + ServerViewState.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); } @@ -503,7 +502,7 @@ export class CallsWidgetWindow { return; } - switchServer(this.serverID); + ServerViewState.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); } @@ -515,7 +514,7 @@ export class CallsWidgetWindow { return; } - switchServer(this.serverID); + ServerViewState.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(CALLS_ERROR, msg); } @@ -527,7 +526,7 @@ export class CallsWidgetWindow { return; } - switchServer(this.serverID); + ServerViewState.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); } diff --git a/webpack.config.base.js b/webpack.config.base.js index 604f6340..c1f7d133 100644 --- a/webpack.config.base.js +++ b/webpack.config.base.js @@ -43,6 +43,7 @@ module.exports = { alias: { renderer: path.resolve(__dirname, 'src/renderer'), main: path.resolve(__dirname, './src/main'), + app: path.resolve(__dirname, './src/app'), common: path.resolve(__dirname, './src/common'), static: path.resolve(__dirname, './src/assets'), },