diff --git a/src/main/app/app.test.js b/src/main/app/app.test.js index cefa9651..350d2bb9 100644 --- a/src/main/app/app.test.js +++ b/src/main/app/app.test.js @@ -5,7 +5,7 @@ import {app, dialog} from 'electron'; import CertificateStore from 'main/certificateStore'; import MainWindow from 'main/windows/mainWindow'; -import WindowManager from 'main/windows/windowManager'; +import ViewManager from 'main/views/viewManager'; import {handleAppWillFinishLaunching, handleAppCertificateError, certificateErrorCallbacks} from 'main/app/app'; import {getDeeplinkingURL, openDeepLink} from 'main/app/utils'; @@ -21,13 +21,6 @@ jest.mock('electron', () => ({ }, })); -jest.mock('common/config', () => ({ - teams: [{ - name: 'test-team', - url: 'http://server-1.com', - }], -})); - jest.mock('main/app/utils', () => ({ getDeeplinkingURL: jest.fn(), openDeepLink: jest.fn(), @@ -46,11 +39,14 @@ jest.mock('main/i18nManager', () => ({ })); jest.mock('main/tray/tray', () => ({})); jest.mock('main/windows/windowManager', () => ({ - getViewNameByWebContentsId: jest.fn(), - getServerNameByWebContentsId: jest.fn(), - viewManager: { - views: new Map(), - }, + showMainWindow: jest.fn(), +})); +jest.mock('main/windows/mainWindow', () => ({ + get: jest.fn(), +})); +jest.mock('main/views/viewManager', () => ({ + getView: jest.fn(), + getViewByWebContentsId: jest.fn(), })); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), @@ -71,7 +67,6 @@ describe('main/app/app', () => { }); afterEach(() => { - WindowManager.viewManager.views.clear(); jest.resetAllMocks(); }); @@ -104,10 +99,19 @@ describe('main/app/app', () => { const mainWindow = {}; const promise = Promise.resolve({}); const certificate = {}; + const view = { + tab: { + server: { + name: 'test-team', + url: new URL(testURL), + }, + }, + load: jest.fn(), + }; beforeEach(() => { MainWindow.get.mockReturnValue(mainWindow); - WindowManager.getServerNameByWebContentsId.mockReturnValue('test-team'); + ViewManager.getViewByWebContentsId.mockReturnValue(view); }); afterEach(() => { @@ -166,12 +170,9 @@ describe('main/app/app', () => { it('should load URL using MattermostView when trusting certificate', async () => { dialog.showMessageBox.mockResolvedValue({response: 0}); - const load = jest.fn(); - WindowManager.viewManager.views.set('view-name', {load}); - WindowManager.getViewNameByWebContentsId.mockReturnValue('view-name'); await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback); expect(callback).toHaveBeenCalledWith(true); - expect(load).toHaveBeenCalledWith(testURL); + expect(view.load).toHaveBeenCalledWith(testURL); }); it('should explicitly untrust if user selects More Details and then cancel with the checkbox checked', async () => { diff --git a/src/main/app/app.ts b/src/main/app/app.ts index 394f0cee..784612f8 100644 --- a/src/main/app/app.ts +++ b/src/main/app/app.ts @@ -5,13 +5,13 @@ import {app, BrowserWindow, Event, dialog, WebContents, Certificate, Details} fr import {Logger} from 'common/log'; import urlUtils from 'common/utils/url'; -import Config from 'common/config'; 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'; import {getDeeplinkingURL, openDeepLink, resizeScreen} from './utils'; @@ -95,10 +95,9 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo // update the callback const errorID = `${origin}:${error}`; - const serverName = WindowManager.getServerNameByWebContentsId(webContents.id); - const server = Config.teams.find((team) => team.name === serverName); - if (server) { - const serverURL = urlUtils.parseURL(server.url); + const view = ViewManager.getViewByWebContentsId(webContents.id); + if (view?.tab.server) { + const serverURL = urlUtils.parseURL(view.tab.server.url); if (serverURL && serverURL.origin !== origin) { log.warn(`Ignoring certificate for unmatched origin ${origin}, will not trust`); callback(false); @@ -159,10 +158,8 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo CertificateStore.save(); certificateErrorCallbacks.get(errorID)(true); - const viewName = WindowManager.getViewNameByWebContentsId(webContents.id); - if (viewName) { - const view = WindowManager.viewManager?.views.get(viewName); - view?.load(url); + if (view) { + view.load(url); } else { webContents.loadURL(url); } diff --git a/src/main/app/config.test.js b/src/main/app/config.test.js index 36a953da..beb9d851 100644 --- a/src/main/app/config.test.js +++ b/src/main/app/config.test.js @@ -44,6 +44,9 @@ jest.mock('main/badge', () => ({ jest.mock('main/tray/tray', () => ({ refreshTrayImages: jest.fn(), })); +jest.mock('main/views/viewManager', () => ({ + reloadConfiguration: jest.fn(), +})); jest.mock('main/views/loadingScreen', () => ({})); jest.mock('main/windows/windowManager', () => ({ handleUpdateConfig: jest.fn(), diff --git a/src/main/app/config.ts b/src/main/app/config.ts index 46242650..dad25f6f 100644 --- a/src/main/app/config.ts +++ b/src/main/app/config.ts @@ -12,6 +12,7 @@ import {Logger, setLoggingLevel} from 'common/log'; import AutoLauncher from 'main/AutoLauncher'; import {setUnreadBadgeSetting} from 'main/badge'; import {refreshTrayImages} from 'main/tray/tray'; +import ViewManager from 'main/views/viewManager'; import LoadingScreen from 'main/views/loadingScreen'; import WindowManager from 'main/windows/windowManager'; @@ -36,8 +37,8 @@ export function handleConfigUpdate(newConfig: CombinedConfig) { return; } - WindowManager.handleUpdateConfig(); if (app.isReady()) { + ViewManager.reloadConfiguration(); WindowManager.sendToRenderer(RELOAD_CONFIGURATION); } diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index 47acad5d..7ab03ca2 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -162,6 +162,7 @@ jest.mock('main/windows/windowManager', () => ({ getServerNameByWebContentsId: jest.fn(), getServerURLFromWebContentsId: jest.fn(), })); +jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/settingsWindow', () => ({ show: jest.fn(), })); diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index f377d8a1..5ef7ce65 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -56,6 +56,7 @@ import SettingsWindow from 'main/windows/settingsWindow'; import TrustedOriginsStore from 'main/trustedOrigins'; import {refreshTrayImages, setupTray} from 'main/tray/tray'; import UserActivityMonitor from 'main/UserActivityMonitor'; +import ViewManager from 'main/views/viewManager'; import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; @@ -238,7 +239,7 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(NOTIFY_MENTION, handleMentionNotification); ipcMain.handle('get-app-version', handleAppVersion); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); - ipcMain.on(FOCUS_BROWSERVIEW, WindowManager.focusBrowserView); + ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView); ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive); if (process.platform !== 'darwin') { @@ -351,7 +352,7 @@ function initializeAfterAppReady() { // listen for status updates and pass on to renderer UserActivityMonitor.on('status', (status) => { log.debug('UserActivityMonitor.on(status)', status); - WindowManager.sendToMattermostViews(USER_ACTIVITY_UPDATE, status); + ViewManager.sendToAllViews(USER_ACTIVITY_UPDATE, status); }); // start monitoring user activity (needs to be started after the app is ready) diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index 90fef811..b82f087c 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -30,6 +30,7 @@ jest.mock('main/utils', () => ({ getLocalPreload: jest.fn(), getLocalURLString: jest.fn(), })); +jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/modalManager', () => ({ addModal: jest.fn(), })); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index dc358c97..8bbaebcf 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -14,6 +14,7 @@ import {ping} from 'common/utils/requests'; import {displayMention} from 'main/notifications'; import {getLocalPreload, getLocalURLString} from 'main/utils'; import ModalManager from 'main/views/modalManager'; +import ViewManager from 'main/views/viewManager'; import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; @@ -26,7 +27,7 @@ export function handleReloadConfig() { log.debug('handleReloadConfig'); Config.reload(); - WindowManager.handleUpdateConfig(); + ViewManager.reloadConfiguration(); } export function handleAppVersion() { diff --git a/src/main/app/utils.test.js b/src/main/app/utils.test.js index 376252c1..c321a40d 100644 --- a/src/main/app/utils.test.js +++ b/src/main/app/utils.test.js @@ -60,6 +60,7 @@ jest.mock('main/server/serverInfo', () => ({ ServerInfo: jest.fn(), })); jest.mock('main/tray/tray', () => ({})); +jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/mainWindow', () => ({})); jest.mock('main/windows/windowManager', () => ({})); diff --git a/src/main/app/utils.ts b/src/main/app/utils.ts index 1e577ed6..0c7c7904 100644 --- a/src/main/app/utils.ts +++ b/src/main/app/utils.ts @@ -27,6 +27,7 @@ import {createMenu as createAppMenu} from 'main/menus/app'; 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'; @@ -110,7 +111,7 @@ export function handleUpdateMenuEvent() { const aMenu = createAppMenu(Config, updateManager); Menu.setApplicationMenu(aMenu); aMenu.addListener('menu-will-close', () => { - WindowManager.focusBrowserView(); + ViewManager.focusCurrentView(); WindowManager.sendToRenderer(APP_MENU_WILL_CLOSE); }); diff --git a/src/main/downloadsManager.test.js b/src/main/downloadsManager.test.js index b75b5201..16b0b7db 100644 --- a/src/main/downloadsManager.test.js +++ b/src/main/downloadsManager.test.js @@ -80,6 +80,7 @@ jest.mock('main/notifications', () => ({})); jest.mock('main/windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); +jest.mock('main/views/viewManager', () => ({})); jest.mock('common/config', () => { const original = jest.requireActual('common/config'); return { diff --git a/src/main/downloadsManager.ts b/src/main/downloadsManager.ts index c275d662..6ae64ea3 100644 --- a/src/main/downloadsManager.ts +++ b/src/main/downloadsManager.ts @@ -31,6 +31,7 @@ import {DOWNLOADS_DROPDOWN_AUTOCLOSE_TIMEOUT, DOWNLOADS_DROPDOWN_MAX_ITEMS} from import * as Validator from 'common/Validator'; import {localizeMessage} from 'main/i18nManager'; import {displayDownloadCompleted} from 'main/notifications'; +import ViewManager from 'main/views/viewManager'; import WindowManager from 'main/windows/windowManager'; import {doubleSecToMs, getPercentage, isStringWithLength, readFilenameFromContentDispositionHeader, shouldIncrementFilename} from 'main/utils'; @@ -509,7 +510,7 @@ export class DownloadsManager extends JsonFileManager { log.debug('doneEventController', {state}); if (state === 'completed' && !this.open) { - displayDownloadCompleted(path.basename(item.savePath), item.savePath, WindowManager.getServerNameByWebContentsId(webContents.id) || ''); + displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.name ?? ''); } const bookmark = this.bookmarks.get(this.getFileId(item)); diff --git a/src/main/menus/app.test.js b/src/main/menus/app.test.js index 0c6cad72..87a4004c 100644 --- a/src/main/menus/app.test.js +++ b/src/main/menus/app.test.js @@ -49,10 +49,11 @@ jest.mock('macos-notification-state', () => ({ jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); +jest.mock('main/diagnostics', () => ({})); jest.mock('main/downloadsManager', () => ({ hasDownloads: jest.fn(), })); -jest.mock('main/diagnostics', () => ({})); +jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/windowManager', () => ({ getCurrentTeamName: jest.fn(), sendToRenderer: jest.fn(), diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index bb6dcc57..c6c6fa30 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -6,7 +6,7 @@ import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron'; import log from 'electron-log'; -import {BROWSER_HISTORY_BUTTON, OPEN_TEAMS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication'; +import {OPEN_TEAMS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication'; import {t} from 'common/utils/util'; import {getTabDisplayName, TabType} from 'common/tabs/TabView'; import {Config} from 'common/config'; @@ -16,6 +16,7 @@ import WindowManager from 'main/windows/windowManager'; import {UpdateManager} from 'main/autoUpdater'; import downloadsManager from 'main/downloadsManager'; import Diagnostics from 'main/diagnostics'; +import ViewManager from 'main/views/viewManager'; import SettingsWindow from 'main/windows/settingsWindow'; export function createTemplate(config: Config, updateManager: UpdateManager) { @@ -126,20 +127,20 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: localizeMessage('main.menus.app.view.find', 'Find..'), accelerator: 'CmdOrCtrl+F', click() { - WindowManager.sendToFind(); + ViewManager.sendToFind(); }, }, { label: localizeMessage('main.menus.app.view.reload', 'Reload'), accelerator: 'CmdOrCtrl+R', click() { - WindowManager.reload(); + ViewManager.reload(); }, }, { label: localizeMessage('main.menus.app.view.clearCacheAndReload', 'Clear Cache and Reload'), accelerator: 'Shift+CmdOrCtrl+R', click() { session.defaultSession.clearCache(); - WindowManager.reload(); + ViewManager.reload(); }, }, { role: 'togglefullscreen', @@ -193,7 +194,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { }, { label: localizeMessage('main.menus.app.view.devToolsCurrentServer', 'Developer Tools for Current Server'), click() { - WindowManager.openBrowserViewDevTools(); + ViewManager.getCurrentView()?.openDevTools(); }, }]; @@ -219,21 +220,13 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { label: localizeMessage('main.menus.app.history.back', 'Back'), accelerator: process.platform === 'darwin' ? 'Cmd+[' : 'Alt+Left', click: () => { - const view = WindowManager.viewManager?.getCurrentView(); - if (view && view.view.webContents.canGoBack() && !view.isAtRoot) { - view.view.webContents.goBack(); - ipcMain.emit(BROWSER_HISTORY_BUTTON, null, view.name); - } + ViewManager.getCurrentView()?.goToOffset(-1); }, }, { label: localizeMessage('main.menus.app.history.forward', 'Forward'), accelerator: process.platform === 'darwin' ? 'Cmd+]' : 'Alt+Right', click: () => { - const view = WindowManager.viewManager?.getCurrentView(); - if (view && view.view.webContents.canGoForward()) { - view.view.webContents.goForward(); - ipcMain.emit(BROWSER_HISTORY_BUTTON, null, view.name); - } + ViewManager.getCurrentView()?.goToOffset(1); }, }], }); diff --git a/src/main/notifications/index.test.js b/src/main/notifications/index.test.js index efef7ee8..16b412da 100644 --- a/src/main/notifications/index.test.js +++ b/src/main/notifications/index.test.js @@ -75,11 +75,20 @@ jest.mock('windows-focus-assist', () => ({ jest.mock('macos-notification-state', () => ({ getDoNotDisturb: jest.fn(), })); +jest.mock('../views/viewManager', () => ({ + getViewByWebContentsId: () => ({ + id: 'server_id', + tab: { + server: { + name: 'server_name', + }, + }, + }), +})); jest.mock('../windows/mainWindow', () => ({ get: jest.fn(), })); jest.mock('../windows/windowManager', () => ({ - getServerNameByWebContentsId: () => 'server_name', sendToRenderer: jest.fn(), flashFrame: jest.fn(), switchTab: jest.fn(), diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 8c4d7e77..4d0dd32c 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -12,6 +12,7 @@ import {PLAY_SOUND} from 'common/communication'; import {Logger} from 'common/log'; import {TAB_MESSAGING} from 'common/tabs/TabView'; +import ViewManager from '../views/viewManager'; import MainWindow from '../windows/mainWindow'; import WindowManager from '../windows/windowManager'; @@ -37,7 +38,11 @@ export function displayMention(title: string, body: string, channel: {id: string return; } - const serverName = WindowManager.getServerNameByWebContentsId(webcontents.id); + const view = ViewManager.getViewByWebContentsId(webcontents.id); + if (!view) { + return; + } + const serverName = view.tab.server.name; const options = { title: `${serverName}: ${title}`, diff --git a/src/main/views/MattermostView.ts b/src/main/views/MattermostView.ts index c18f2d92..e0631f04 100644 --- a/src/main/views/MattermostView.ts +++ b/src/main/views/MattermostView.ts @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {BrowserView, app, ipcMain} from 'electron'; +import {BrowserView, app} from 'electron'; import {BrowserViewConstructorOptions, Event, Input} from 'electron/main'; import {EventEmitter} from 'events'; @@ -15,10 +15,10 @@ import { LOAD_FAILED, UPDATE_TARGET_URL, IS_UNREAD, - UNREAD_RESULT, TOGGLE_BACK_BUTTON, SET_VIEW_OPTIONS, LOADSCREEN_END, + BROWSER_HISTORY_BUTTON, } from 'common/communication'; import {MattermostServer} from 'common/servers/MattermostServer'; import {TabView, TabTuple} from 'common/tabs/TabView'; @@ -234,7 +234,6 @@ export class MattermostView extends EventEmitter { WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name); this.maxRetries = MAX_SERVER_RETRIES; if (this.status === Status.LOADING) { - ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); this.updateMentionsFromTitle(this.view.webContents.getTitle()); this.findUnreadState(null); } @@ -272,6 +271,32 @@ export class MattermostView extends EventEmitter { hide = () => this.show(false); + openFind = () => { + this.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']}); + } + + goToOffset = (offset: number) => { + if (this.view.webContents.canGoToOffset(offset)) { + try { + this.view.webContents.goToOffset(offset); + this.updateHistoryButton(); + } catch (error) { + log.error(error); + this.reload(); + } + } + } + + updateHistoryButton = () => { + if (urlUtils.parseURL(this.view.webContents.getURL())?.toString() === this.tab.url.toString()) { + this.view.webContents.clearHistory(); + this.isAtRoot = true; + } else { + this.isAtRoot = false; + } + this.view.webContents.send(BROWSER_HISTORY_BUTTON, this.view.webContents.canGoBack(), this.view.webContents.canGoForward()); + } + setBounds = (boundaries: Electron.Rectangle) => { this.view.setBounds(boundaries); } @@ -422,14 +447,4 @@ export class MattermostView extends EventEmitter { log.error(err.stack); } } - - // if favicon is null, it means it is the initial load, - // so don't memoize as we don't have the favicons and there is no rush to find out. - handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => { - log.silly('handleFaviconIsUnread', {favicon, viewName, result}); - - if (this.tab && viewName === this.tab.name) { - appState.updateUnreads(viewName, result); - } - } } diff --git a/src/main/views/downloadsDropdownView.test.js b/src/main/views/downloadsDropdownView.test.js index b6957f3a..b5d445ac 100644 --- a/src/main/views/downloadsDropdownView.test.js +++ b/src/main/views/downloadsDropdownView.test.js @@ -61,6 +61,10 @@ jest.mock('electron', () => { Notification: NotificationMock, }; }); +jest.mock('main/downloadsManager', () => ({ + onOpen: jest.fn(), + onClose: jest.fn(), +})); jest.mock('main/windows/mainWindow', () => ({ get: jest.fn(), getBounds: jest.fn(), diff --git a/src/main/views/modalManager.test.js b/src/main/views/modalManager.test.js index ce33263d..02443315 100644 --- a/src/main/views/modalManager.test.js +++ b/src/main/views/modalManager.test.js @@ -3,7 +3,7 @@ 'use strict'; -import * as WindowManager from '../windows/windowManager'; +import ViewManager from 'main/views/viewManager'; import {ModalManager} from './modalManager'; @@ -26,9 +26,11 @@ jest.mock('main/views/webContentEvents', () => ({ jest.mock('./modalView', () => ({ ModalView: jest.fn(), })); -jest.mock('../windows/windowManager', () => ({ +jest.mock('main/views/viewManager', () => ({ + focusCurrentView: jest.fn(), +})); +jest.mock('main/windows/windowManager', () => ({ sendToRenderer: jest.fn(), - focusBrowserView: jest.fn(), })); jest.mock('process', () => ({ env: {}, @@ -161,7 +163,7 @@ describe('main/views/modalManager', () => { modalManager.modalQueue.pop(); modalManager.modalPromises.delete('test2'); modalManager.handleModalFinished('resolve', {sender: {id: 1}}, 'something'); - expect(WindowManager.focusBrowserView).toBeCalled(); + expect(ViewManager.focusCurrentView).toBeCalled(); }); }); }); diff --git a/src/main/views/modalManager.ts b/src/main/views/modalManager.ts index d850b028..64d20564 100644 --- a/src/main/views/modalManager.ts +++ b/src/main/views/modalManager.ts @@ -22,6 +22,7 @@ import {Logger} from 'common/log'; import {getAdjustedWindowBoundaries} from 'main/utils'; import WebContentsEventManager from 'main/views/webContentEvents'; +import ViewManager from 'main/views/viewManager'; import WindowManager from 'main/windows/windowManager'; import {ModalView} from './modalView'; @@ -115,7 +116,7 @@ export class ModalManager { this.showModal(); } else { WindowManager.sendToRenderer(MODAL_CLOSE); - WindowManager.focusBrowserView(); + ViewManager.focusCurrentView(); } } diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index fab45b22..8a08a02b 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -7,7 +7,8 @@ import {dialog, ipcMain} from 'electron'; import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill'; -import {LOAD_SUCCESS, MAIN_WINDOW_SHOWN, BROWSER_HISTORY_PUSH} from 'common/communication'; +import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication'; +import Config from 'common/config'; import {MattermostServer} from 'common/servers/MattermostServer'; import {getTabViewName} from 'common/tabs/TabView'; import {equalUrlsIgnoringSubpath} from 'common/utils/url'; @@ -28,9 +29,12 @@ jest.mock('electron', () => ({ ipcMain: { emit: jest.fn(), on: jest.fn(), + handle: jest.fn(), }, })); - +jest.mock('common/config', () => ({ + teams: [], +})); jest.mock('common/tabs/TabView', () => ({ getTabViewName: jest.fn((a, b) => `${a}-${b}`), TAB_MESSAGING: 'tab', @@ -41,6 +45,9 @@ jest.mock('common/servers/MattermostServer', () => ({ })); jest.mock('common/utils/url', () => ({ + isTeamUrl: jest.fn(), + isAdminUrl: jest.fn(), + cleanPathName: jest.fn(), parseURL: (url) => { try { return new URL(url); @@ -73,6 +80,7 @@ jest.mock('./modalManager', () => ({ showModal: jest.fn(), })); jest.mock('./webContentEvents', () => ({})); +jest.mock('../appState', () => ({})); describe('main/views/viewManager', () => { describe('loadView', () => { @@ -119,12 +127,11 @@ describe('main/views/viewManager', () => { }); }); - describe('reloadViewIfNeeded', () => { + describe('handleAppLoggedIn', () => { const viewManager = new ViewManager({}); afterEach(() => { jest.resetAllMocks(); - viewManager.views = new Map(); }); it('should reload view when URL is not on subpath of original server URL', () => { @@ -140,7 +147,7 @@ describe('main/views/viewManager', () => { }, }; viewManager.views.set('view1', view); - viewManager.reloadViewIfNeeded('view1'); + viewManager.handleAppLoggedIn({}, 'view1'); expect(view.load).toHaveBeenCalledWith(new URL('http://server-1.com/')); }); @@ -157,7 +164,7 @@ describe('main/views/viewManager', () => { }, }; viewManager.views.set('view1', view); - viewManager.reloadViewIfNeeded('view1'); + viewManager.handleAppLoggedIn({}, 'view1'); expect(view.load).not.toHaveBeenCalled(); }); @@ -174,7 +181,7 @@ describe('main/views/viewManager', () => { }, }; viewManager.views.set('view1', view); - viewManager.reloadViewIfNeeded('view1'); + viewManager.handleAppLoggedIn({}, 'view1'); expect(view.load).not.toHaveBeenCalled(); }); }); @@ -186,6 +193,7 @@ describe('main/views/viewManager', () => { viewManager.loadView = jest.fn(); viewManager.showByName = jest.fn(); viewManager.showInitial = jest.fn(); + const mainWindow = { webContents: { send: jest.fn(), @@ -225,14 +233,7 @@ describe('main/views/viewManager', () => { }); it('should recycle existing views', () => { - const makeSpy = jest.spyOn(viewManager, 'makeView'); - const view = new MattermostView({ - name: 'server1-tab1', - urlTypeTuple: tuple(new URL('http://server1.com').href, 'tab1'), - server: 'server1', - }); - viewManager.views.set('server1-tab1', view); - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server1', url: 'http://server1.com', @@ -244,14 +245,22 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + const makeSpy = jest.spyOn(viewManager, 'makeView'); + const view = new MattermostView({ + name: 'server1-tab1', + urlTypeTuple: tuple(new URL('http://server1.com').href, 'tab1'), + server: 'server1', + }); + viewManager.views.set('server1-tab1', view); + viewManager.reloadConfiguration(); expect(viewManager.views.get('server1-tab1')).toBe(view); expect(makeSpy).not.toHaveBeenCalled(); makeSpy.mockRestore(); }); it('should close tabs that arent open', () => { - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server1', url: 'http://server1.com', @@ -263,13 +272,14 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + viewManager.reloadConfiguration(); expect(viewManager.closedViews.has('server1-tab1')).toBe(true); }); it('should create new views for new tabs', () => { const makeSpy = jest.spyOn(viewManager, 'makeView'); - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server1', url: 'http://server1.com', @@ -281,7 +291,8 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + viewManager.reloadConfiguration(); expect(makeSpy).toHaveBeenCalledWith( { name: 'server1', @@ -313,7 +324,7 @@ describe('main/views/viewManager', () => { }; viewManager.currentView = 'server1-tab1'; viewManager.views.set('server1-tab1', view); - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server1', url: 'http://server1.com', @@ -325,7 +336,8 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + viewManager.reloadConfiguration(); expect(viewManager.showByName).toHaveBeenCalledWith('server1-tab1'); }); @@ -342,7 +354,7 @@ describe('main/views/viewManager', () => { }; viewManager.currentView = 'server1-tab1'; viewManager.views.set('server1-tab1', view); - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server2', url: 'http://server2.com', @@ -354,7 +366,8 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + viewManager.reloadConfiguration(); expect(viewManager.showInitial).toBeCalled(); }); @@ -368,7 +381,7 @@ describe('main/views/viewManager', () => { destroy: jest.fn(), }; viewManager.views.set('server1-tab1', view); - viewManager.reloadConfiguration([ + Config.teams = [ { name: 'server2', url: 'http://server2.com', @@ -380,65 +393,64 @@ describe('main/views/viewManager', () => { }, ], }, - ]); + ]; + viewManager.reloadConfiguration(); expect(view.destroy).toBeCalled(); expect(viewManager.showInitial).toBeCalled(); }); }); describe('showInitial', () => { - const teams = [{ - name: 'server-1', - order: 1, - tabs: [ - { - name: 'tab-1', - order: 0, - isOpen: false, - }, - { - name: 'tab-2', - order: 2, - isOpen: true, - }, - { - name: 'tab-3', - order: 1, - isOpen: true, - }, - ], - }, { - name: 'server-2', - order: 0, - tabs: [ - { - name: 'tab-1', - order: 0, - isOpen: false, - }, - { - name: 'tab-2', - order: 2, - isOpen: true, - }, - { - name: 'tab-3', - order: 1, - isOpen: true, - }, - ], - }]; const viewManager = new ViewManager({}); - viewManager.getServers = () => teams.concat(); beforeEach(() => { + Config.teams = [{ + name: 'server-1', + order: 1, + tabs: [ + { + name: 'tab-1', + order: 0, + isOpen: false, + }, + { + name: 'tab-2', + order: 2, + isOpen: true, + }, + { + name: 'tab-3', + order: 1, + isOpen: true, + }, + ], + }, { + name: 'server-2', + order: 0, + tabs: [ + { + name: 'tab-1', + order: 0, + isOpen: false, + }, + { + name: 'tab-2', + order: 2, + isOpen: true, + }, + { + name: 'tab-3', + order: 1, + isOpen: true, + }, + ], + }]; viewManager.showByName = jest.fn(); getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`); }); afterEach(() => { jest.resetAllMocks(); - viewManager.getServers = () => teams; delete viewManager.lastActiveServer; }); @@ -454,7 +466,7 @@ describe('main/views/viewManager', () => { }); it('should show last active tab of first server', () => { - viewManager.getServers = () => [{ + Config.teams = [{ name: 'server-1', order: 1, tabs: [ @@ -501,7 +513,7 @@ describe('main/views/viewManager', () => { }); it('should show next tab when last active tab is closed', () => { - viewManager.getServers = () => [{ + Config.teams = [{ name: 'server-1', order: 1, tabs: [ @@ -553,7 +565,7 @@ describe('main/views/viewManager', () => { send: jest.fn(), }, }; - viewManager.getServers = () => []; + Config.teams = []; viewManager.showInitial(); expect(ipcMain.emit).toHaveBeenCalledWith(MAIN_WINDOW_SHOWN); }); @@ -654,8 +666,8 @@ describe('main/views/viewManager', () => { }); describe('getViewByURL', () => { - const viewManager = new ViewManager({}); - viewManager.getServers = () => [ + const viewManager = new ViewManager(); + const servers = [ { name: 'server-1', url: 'http://server-1.com', @@ -696,6 +708,7 @@ describe('main/views/viewManager', () => { }; beforeEach(() => { + Config.teams = servers.concat(); MattermostServer.mockImplementation((name, url) => ({ name, url: new URL(url), diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index ef8e0306..4764e413 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -1,7 +1,6 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. - -import {BrowserView, dialog, ipcMain, IpcMainEvent} from 'electron'; +import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron'; import {BrowserViewConstructorOptions} from 'electron/main'; import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill'; @@ -20,6 +19,13 @@ import { UPDATE_LAST_ACTIVE, UPDATE_URL_VIEW_WIDTH, MAIN_WINDOW_SHOWN, + RELOAD_CURRENT_VIEW, + REACT_APP_INITIALIZED, + APP_LOGGED_IN, + BROWSER_HISTORY_BUTTON, + APP_LOGGED_OUT, + UNREAD_RESULT, + GET_VIEW_NAME, } from 'common/communication'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -35,6 +41,7 @@ import {localizeMessage} from 'main/i18nManager'; import {ServerInfo} from 'main/server/serverInfo'; import MainWindow from 'main/windows/mainWindow'; +import * as appState from '../appState'; import {getLocalURLString, getLocalPreload} from '../utils'; import {MattermostView} from './MattermostView'; @@ -47,32 +54,200 @@ const URL_VIEW_DURATION = 10 * SECOND; const URL_VIEW_HEIGHT = 20; export class ViewManager { - lastActiveServer?: number; - viewOptions: BrowserViewConstructorOptions; - closedViews: Map; - views: Map; - currentView?: string; - urlView?: BrowserView; - urlViewCancel?: () => void; + private closedViews: Map; + private views: Map; + private currentView?: string; + + private urlViewCancel?: () => void; + + private lastActiveServer?: number; + private viewOptions: BrowserViewConstructorOptions; constructor() { this.lastActiveServer = Config.lastActiveTeam; this.viewOptions = {webPreferences: {spellcheck: Config.useSpellChecker}}; this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that. this.closedViews = new Map(); + + ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); + ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush); + ipcMain.on(BROWSER_HISTORY_BUTTON, this.handleBrowserHistoryButton); + ipcMain.on(APP_LOGGED_IN, this.handleAppLoggedIn); + ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut); + ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); + ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); + ipcMain.handle(GET_VIEW_NAME, this.handleGetViewName); } - getServers = () => { - return Config.teams.concat(); + init = () => { + this.getServers().forEach((server) => this.loadServer(server)); + this.showInitial(); } - loadServer = (server: TeamWithTabs) => { + getView = (viewName: string) => { + return this.views.get(viewName); + } + + getCurrentView = () => { + if (this.currentView) { + return this.views.get(this.currentView); + } + + return undefined; + } + + getViewByWebContentsId = (webContentsId: number) => { + return [...this.views.values()].find((view) => view.view.webContents.id === webContentsId); + } + + showByName = (name: string) => { + log.debug('viewManager.showByName', name); + + const newView = this.views.get(name); + if (newView) { + if (newView.isVisible) { + return; + } + if (this.currentView && this.currentView !== name) { + const previous = this.getCurrentView(); + if (previous) { + previous.hide(); + } + } + + this.currentView = name; + if (!newView.isErrored()) { + newView.show(); + if (newView.needsLoadingScreen()) { + LoadingScreen.show(); + } + } + MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type); + ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type); + if (newView.isReady()) { + ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type); + } else { + log.warn(`couldn't show ${name}, not ready`); + } + } else { + log.warn(`Couldn't find a view with name: ${name}`); + } + modalManager.showModal(); + } + + focusCurrentView = () => { + if (modalManager.isModalDisplayed()) { + modalManager.focusCurrentModal(); + return; + } + + const view = this.getCurrentView(); + if (view) { + view.focus(); + } + } + + reload = () => { + const currentView = this.getCurrentView(); + if (currentView) { + LoadingScreen.show(); + currentView.reload(); + } + } + + sendToAllViews = (channel: string, ...args: unknown[]) => { + this.views.forEach((view) => { + if (!view.view.webContents.isDestroyed()) { + view.view.webContents.send(channel, ...args); + } + }); + } + + sendToFind = () => { + this.getCurrentView()?.openFind(); + } + + /** + * Deep linking + */ + + handleDeepLink = (url: string | URL) => { + // TODO: fix for new tabs + if (url) { + const parsedURL = urlUtils.parseURL(url)!; + const tabView = this.getViewByURL(parsedURL, true); + if (tabView) { + const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`; + if (this.closedViews.has(tabView.name)) { + this.openClosedTab(tabView.name, urlWithSchema); + } else { + const view = this.views.get(tabView.name); + if (!view) { + log.error(`Couldn't find a view matching the name ${tabView.name}`); + return; + } + + if (view.isInitialized() && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) { + const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`; + view.view.webContents.send(BROWSER_HISTORY_PUSH, pathName); + this.deeplinkSuccess(view.name); + } else { + // attempting to change parsedURL protocol results in it not being modified. + view.resetLoadingStatus(); + view.load(urlWithSchema); + view.once(LOAD_SUCCESS, this.deeplinkSuccess); + view.once(LOAD_FAILED, this.deeplinkFailed); + } + } + } else { + dialog.showErrorBox( + localizeMessage('main.views.viewManager.handleDeepLink.error.title', 'No matching server'), + localizeMessage('main.views.viewManager.handleDeepLink.error.body', 'There is no configured server in the app that matches the requested url: {url}', {url: parsedURL.toString()}), + ); + } + } + }; + + private deeplinkSuccess = (viewName: string) => { + log.debug('deeplinkSuccess', viewName); + + const view = this.views.get(viewName); + if (!view) { + return; + } + this.showByName(viewName); + view.removeListener(LOAD_FAILED, this.deeplinkFailed); + }; + + private deeplinkFailed = (viewName: string, err: string, url: string) => { + log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`); + const view = this.views.get(viewName); + if (!view) { + return; + } + view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess); + } + + /** + * View loading helpers + */ + + private loadServer = (server: TeamWithTabs) => { const srv = new MattermostServer(server.name, server.url); const serverInfo = new ServerInfo(srv); server.tabs.forEach((tab) => this.loadView(srv, serverInfo, tab)); } - makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => { + private loadView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string) => { + if (!tab.isOpen) { + this.closedViews.set(getTabViewName(srv.name, tab.name), {srv, tab}); + return; + } + const view = this.makeView(srv, serverInfo, tab, url); + this.addView(view); + } + + private makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => { const tabView = this.getServerView(srv, tab.name); const view = new MattermostView(tabView, serverInfo, this.viewOptions); view.once(LOAD_SUCCESS, this.activateView); @@ -83,38 +258,153 @@ export class ViewManager { return view; } - addView = (view: MattermostView): void => { + private addView = (view: MattermostView): void => { this.views.set(view.name, view); if (this.closedViews.has(view.name)) { this.closedViews.delete(view.name); } } - loadView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string) => { - if (!tab.isOpen) { - this.closedViews.set(getTabViewName(srv.name, tab.name), {srv, tab}); + private showInitial = () => { + log.verbose('showInitial'); + + const servers = this.getServers(); + if (servers.length) { + const element = servers.find((e) => e.order === this.lastActiveServer) || servers.find((e) => e.order === 0); + if (element && element.tabs.length) { + let tab = element.tabs.find((tab) => tab.order === element.lastActiveTab) || element.tabs.find((tab) => tab.order === 0); + if (!tab?.isOpen) { + const openTabs = element.tabs.filter((tab) => tab.isOpen); + tab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0]; + } + if (tab) { + const tabView = getTabViewName(element.name, tab.name); + this.showByName(tabView); + } + } + } else { + MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null); + ipcMain.emit(MAIN_WINDOW_SHOWN); + } + } + + /** + * Mattermost view event handlers + */ + + private activateView = (viewName: string) => { + log.debug('activateView', viewName); + + if (this.currentView === viewName) { + this.showByName(this.currentView); + } + const view = this.views.get(viewName); + if (!view) { + log.error(`Couldn't find a view with the name ${viewName}`); return; } - const view = this.makeView(srv, serverInfo, tab, url); - this.addView(view); + WebContentsEventManager.addMattermostViewEventListeners(view); } - reloadViewIfNeeded = (viewName: string) => { - const view = this.views.get(viewName); - if (view && view.view.webContents.getURL() !== view.tab.url.toString() && !view.view.webContents.getURL().startsWith(view.tab.url.toString())) { - view.load(view.tab.url); + private finishLoading = (server: string) => { + log.debug('finishLoading', server); + + const view = this.views.get(server); + if (view && this.getCurrentView() === view) { + this.showByName(this.currentView!); + LoadingScreen.fade(); } } - load = () => { - this.getServers().forEach((server) => this.loadServer(server)); + private failLoading = (tabName: string) => { + log.debug('failLoading', tabName); + + LoadingScreen.fade(); + if (this.currentView === tabName) { + this.getCurrentView()?.hide(); + } } + private showURLView = (url: URL | string) => { + log.silly('showURLView', url); + + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + + if (this.urlViewCancel) { + this.urlViewCancel(); + } + if (url && url !== '') { + const urlString = typeof url === 'string' ? url : url.toString(); + const preload = getLocalPreload('desktopAPI.js'); + const urlView = new BrowserView({ + webPreferences: { + preload, + + // Workaround for this issue: https://github.com/electron/electron/issues/30993 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + transparent: true, + }}); + const query = new Map([['url', urlString]]); + const localURL = getLocalURLString('urlView.html', query); + urlView.webContents.loadURL(localURL); + mainWindow.addBrowserView(urlView); + const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? mainWindow.getBounds(); + + const hideView = () => { + delete this.urlViewCancel; + try { + mainWindow.removeBrowserView(urlView); + } catch (e) { + log.error('Failed to remove URL view', e); + } + + // workaround to eliminate zombie processes + // https://github.com/mattermost/desktop/pull/1519 + // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore + urlView.webContents.destroy(); + }; + + const adjustWidth = (event: IpcMainEvent, width: number) => { + log.silly('showURLView.adjustWidth', width); + + const bounds = { + x: 0, + y: (boundaries.height + TAB_BAR_HEIGHT) - URL_VIEW_HEIGHT, + width: width + 5, // add some padding to ensure that we don't cut off the border + height: URL_VIEW_HEIGHT, + }; + + log.silly('showURLView setBounds', boundaries, bounds); + urlView.setBounds(bounds); + }; + + ipcMain.on(UPDATE_URL_VIEW_WIDTH, adjustWidth); + + const timeout = setTimeout(hideView, + URL_VIEW_DURATION); + + this.urlViewCancel = () => { + clearTimeout(timeout); + ipcMain.removeListener(UPDATE_URL_VIEW_WIDTH, adjustWidth); + hideView(); + }; + } + } + + /** + * Event Handlers + */ + /** Called when a new configuration is received * Servers or tabs have been added or edited. We need to * close, open, or reload tabs, taking care to reuse tabs and * preserve focus on the currently selected tab. */ - reloadConfiguration = (configServers: TeamWithTabs[]) => { + reloadConfiguration = () => { log.debug('reloadConfiguration'); const focusedTuple: TabTuple | undefined = this.views.get(this.currentView as string)?.urlTypeTuple; @@ -127,7 +417,7 @@ export class ViewManager { const views: Map = new Map(); const closed: Map = new Map(); - const sortedTabs = configServers.flatMap((x) => [...x.tabs]. + const sortedTabs = this.getServers().flatMap((x) => [...x.tabs]. sort((a, b) => a.order - b.order). map((t): [TeamWithTabs, Tab] => [x, t])); @@ -167,7 +457,7 @@ export class ViewManager { } if ((focusedTuple && closed.has(focusedTuple)) || (this.currentView && this.closedViews.has(this.currentView))) { - if (configServers.length) { + if (this.getServers().length) { this.currentView = undefined; this.showInitial(); } else { @@ -188,101 +478,97 @@ export class ViewManager { } } - showInitial = () => { - log.verbose('showInitial'); + private handleAppLoggedIn = (event: IpcMainEvent, viewName: string) => { + log.debug('handleAppLoggedIn', viewName); - const servers = this.getServers(); - if (servers.length) { - const element = servers.find((e) => e.order === this.lastActiveServer) || servers.find((e) => e.order === 0); - if (element && element.tabs.length) { - let tab = element.tabs.find((tab) => tab.order === element.lastActiveTab) || element.tabs.find((tab) => tab.order === 0); - if (!tab?.isOpen) { - const openTabs = element.tabs.filter((tab) => tab.isOpen); - tab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0]; - } - if (tab) { - const tabView = getTabViewName(element.name, tab.name); - this.showByName(tabView); - } + const view = this.views.get(viewName); + if (view && !view.isLoggedIn) { + view.isLoggedIn = true; + if (view.view.webContents.getURL() !== view.tab.url.toString() && !view.view.webContents.getURL().startsWith(view.tab.url.toString())) { + view.load(view.tab.url); } - } else { - MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null); - ipcMain.emit(MAIN_WINDOW_SHOWN); } } - showByName = (name: string) => { - log.debug('showByName', name); + private handleAppLoggedOut = (event: IpcMainEvent, viewName: string) => { + log.debug('handleAppLoggedOut', viewName); - const newView = this.views.get(name); - if (newView) { - if (newView.isVisible) { - return; - } - if (this.currentView && this.currentView !== name) { - const previous = this.getCurrentView(); - if (previous) { - previous.hide(); - } - } - - this.currentView = name; - if (!newView.isErrored()) { - newView.show(); - if (newView.needsLoadingScreen()) { - LoadingScreen.show(); - } - } - MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type); - ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type); - if (newView.isReady()) { - ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type); - } else { - log.warn(`couldn't show ${name}, not ready`); - } - } else { - log.warn(`Couldn't find a view with name: ${name}`); + const view = this.views.get(viewName); + if (view && view.isLoggedIn) { + view.isLoggedIn = false; } - modalManager.showModal(); } - focus = () => { - if (modalManager.isModalDisplayed()) { - modalManager.focusCurrentModal(); + private handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => { + log.debug('handleBrowserHistoryPush', {viewName, pathName}); + + const currentView = this.views.get(viewName); + const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName); + const redirectedViewName = this.getViewByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.name || viewName; + if (this.closedViews.has(redirectedViewName)) { + // If it's a closed view, just open it and stop + this.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${cleanedPathName}`); return; } + let redirectedView = this.views.get(redirectedViewName) || currentView; + if (redirectedView !== currentView && redirectedView?.tab.name === this.currentView && redirectedView?.isLoggedIn) { + log.info('redirecting to a new view', redirectedView?.name || viewName); + this.showByName(redirectedView?.name || viewName); + } else { + redirectedView = currentView; + } + + // Special case check for Channels to not force a redirect to "/", causing a refresh + if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) { + redirectedView?.view.webContents.send(BROWSER_HISTORY_PUSH, cleanedPathName); + if (redirectedView) { + this.handleBrowserHistoryButton(e, redirectedView.name); + } + } + } + + private handleBrowserHistoryButton = (e: IpcMainEvent, viewName: string) => { + log.debug('handleBrowserHistoryButton', viewName); + + this.getView(viewName)?.updateHistoryButton(); + } + + private handleReactAppInitialized = (e: IpcMainEvent, viewName: string) => { + log.debug('handleReactAppInitialized', viewName); + + const view = this.views.get(viewName); + if (view) { + view.setInitialized(); + if (this.getCurrentView() === view) { + LoadingScreen.fade(); + } + } + } + + private handleReloadCurrentView = () => { + log.debug('handleReloadCurrentView'); const view = this.getCurrentView(); - if (view) { - view.focus(); - } - } - - activateView = (viewName: string) => { - log.debug('activateView', viewName); - - if (this.currentView === viewName) { - this.showByName(this.currentView); - } - const view = this.views.get(viewName); if (!view) { - log.error(`Couldn't find a view with the name ${viewName}`); return; } - WebContentsEventManager.addMattermostViewEventListeners(view); + view?.reload(); + this.showByName(view?.name); } - finishLoading = (server: string) => { - log.debug('finishLoading', server); + // if favicon is null, it means it is the initial load, + // so don't memoize as we don't have the favicons and there is no rush to find out. + private handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => { + log.silly('handleFaviconIsUnread', {favicon, viewName, result}); - const view = this.views.get(server); - if (view && this.getCurrentView() === view) { - this.showByName(this.currentView!); - LoadingScreen.fade(); - } + appState.updateUnreads(viewName, result); } - openClosedTab = (name: string, url?: string) => { + /** + * Helper functions + */ + + private openClosedTab = (name: string, url?: string) => { if (!this.closedViews.has(name)) { return; } @@ -299,142 +585,6 @@ export class ViewManager { ipcMain.emit(OPEN_TAB, null, srv.name, tab.name); } - failLoading = (tabName: string) => { - log.debug('failLoading', tabName); - - LoadingScreen.fade(); - if (this.currentView === tabName) { - this.getCurrentView()?.hide(); - } - } - - getCurrentView() { - if (this.currentView) { - return this.views.get(this.currentView); - } - - return undefined; - } - - openViewDevTools = () => { - const view = this.getCurrentView(); - if (view) { - view.openDevTools(); - } else { - log.error(`couldn't find ${this.currentView}`); - } - } - - findViewByWebContent(webContentId: number) { - let found = null; - let view; - const entries = this.views.values(); - - for (view of entries) { - const wc = view.getWebContents(); - if (wc && wc.id === webContentId) { - found = view; - } - } - return found; - } - - showURLView = (url: URL | string) => { - log.silly('showURLView', url); - - if (this.urlViewCancel) { - this.urlViewCancel(); - } - if (url && url !== '') { - const urlString = typeof url === 'string' ? url : url.toString(); - const preload = getLocalPreload('desktopAPI.js'); - const urlView = new BrowserView({ - webPreferences: { - preload, - - // Workaround for this issue: https://github.com/electron/electron/issues/30993 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - transparent: true, - }}); - const query = new Map([['url', urlString]]); - const localURL = getLocalURLString('urlView.html', query); - urlView.webContents.loadURL(localURL); - MainWindow.get()?.addBrowserView(urlView); - const boundaries = this.views.get(this.currentView || '')?.view.getBounds() ?? MainWindow.get()!.getBounds(); - - const hideView = () => { - delete this.urlViewCancel; - try { - MainWindow.get()?.removeBrowserView(urlView); - } catch (e) { - log.error('Failed to remove URL view', e); - } - - // workaround to eliminate zombie processes - // https://github.com/mattermost/desktop/pull/1519 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - urlView.webContents.destroy(); - }; - - const adjustWidth = (event: IpcMainEvent, width: number) => { - log.silly('showURLView.adjustWidth', width); - - const bounds = { - x: 0, - y: (boundaries.height + TAB_BAR_HEIGHT) - URL_VIEW_HEIGHT, - width: width + 5, // add some padding to ensure that we don't cut off the border - height: URL_VIEW_HEIGHT, - }; - - log.silly('showURLView setBounds', boundaries, bounds); - urlView.setBounds(bounds); - }; - - ipcMain.on(UPDATE_URL_VIEW_WIDTH, adjustWidth); - - const timeout = setTimeout(hideView, - URL_VIEW_DURATION); - - this.urlViewCancel = () => { - clearTimeout(timeout); - ipcMain.removeListener(UPDATE_URL_VIEW_WIDTH, adjustWidth); - hideView(); - }; - } - } - - setServerInitialized = (server: string) => { - const view = this.views.get(server); - if (view) { - view.setInitialized(); - if (this.getCurrentView() === view) { - LoadingScreen.fade(); - } - } - } - - deeplinkSuccess = (viewName: string) => { - log.debug('deeplinkSuccess', viewName); - - const view = this.views.get(viewName); - if (!view) { - return; - } - this.showByName(viewName); - view.removeListener(LOAD_FAILED, this.deeplinkFailed); - }; - - deeplinkFailed = (viewName: string, err: string, url: string) => { - log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`); - const view = this.views.get(viewName); - if (!view) { - return; - } - view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess); - } - getViewByURL = (inputURL: URL | string, ignoreScheme = false) => { log.silly('getViewByURL', `${inputURL}`, ignoreScheme); @@ -449,6 +599,7 @@ export class ViewManager { if (!server) { return undefined; } + const mmServer = new MattermostServer(server.name, server.url); let selectedTab = this.getServerView(mmServer, TAB_MESSAGING); server.tabs. @@ -462,51 +613,6 @@ export class ViewManager { return selectedTab; } - handleDeepLink = (url: string | URL) => { - // TODO: fix for new tabs - if (url) { - const parsedURL = urlUtils.parseURL(url)!; - const tabView = this.getViewByURL(parsedURL, true); - if (tabView) { - const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`; - if (this.closedViews.has(tabView.name)) { - this.openClosedTab(tabView.name, urlWithSchema); - } else { - const view = this.views.get(tabView.name); - if (!view) { - log.error(`Couldn't find a view matching the name ${tabView.name}`); - return; - } - - if (view.isInitialized() && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) { - const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`; - view.view.webContents.send(BROWSER_HISTORY_PUSH, pathName); - this.deeplinkSuccess(view.name); - } else { - // attempting to change parsedURL protocol results in it not being modified. - view.resetLoadingStatus(); - view.load(urlWithSchema); - view.once(LOAD_SUCCESS, this.deeplinkSuccess); - view.once(LOAD_FAILED, this.deeplinkFailed); - } - } - } else { - dialog.showErrorBox( - localizeMessage('main.views.viewManager.handleDeepLink.error.title', 'No matching server'), - localizeMessage('main.views.viewManager.handleDeepLink.error.body', 'There is no configured server in the app that matches the requested url: {url}', {url: parsedURL.toString()}), - ); - } - } - }; - - sendToAllViews = (channel: string, ...args: unknown[]) => { - this.views.forEach((view) => { - if (!view.view.webContents.isDestroyed()) { - view.view.webContents.send(channel, ...args); - } - }); - } - private getServerView = (srv: MattermostServer, tabName: string) => { switch (tabName) { case TAB_MESSAGING: @@ -519,4 +625,25 @@ export class ViewManager { throw new Error('Not implemeneted'); } } + + private getServers = () => { + return Config.teams.concat(); + } + + handleGetViewName = (event: IpcMainInvokeEvent) => { + return this.getViewByWebContentsId(event.sender.id); + } + + setServerInitialized = (server: string) => { + const view = this.views.get(server); + if (view) { + view.setInitialized(); + if (this.getCurrentView() === view) { + LoadingScreen.fade(); + } + } + } } + +const viewManager = new ViewManager(); +export default viewManager; diff --git a/src/main/views/webContentEvents.test.js b/src/main/views/webContentEvents.test.js index 6b7461c6..375e843d 100644 --- a/src/main/views/webContentEvents.test.js +++ b/src/main/views/webContentEvents.test.js @@ -25,6 +25,10 @@ jest.mock('electron', () => ({ jest.mock('main/contextMenu', () => jest.fn()); jest.mock('../allowProtocolDialog', () => ({})); +jest.mock('main/views/viewManager', () => ({ + getViewByWebContentsId: jest.fn(), + getViewByURL: jest.fn(), +})); jest.mock('../windows/windowManager', () => ({ getServerURLFromWebContentsId: jest.fn(), showMainWindow: jest.fn(), diff --git a/src/main/views/webContentEvents.ts b/src/main/views/webContentEvents.ts index 1cc87885..ec3a370c 100644 --- a/src/main/views/webContentEvents.ts +++ b/src/main/views/webContentEvents.ts @@ -18,6 +18,7 @@ import allowProtocolDialog from '../allowProtocolDialog'; import {composeUserAgent} from '../utils'; import {MattermostView} from './MattermostView'; +import ViewManager from './viewManager'; type CustomLogin = { inProgress: boolean; @@ -249,7 +250,7 @@ export class WebContentsEventManager { return {action: 'deny'}; } - const otherServerURL = WindowManager.viewManager?.getViewByURL(parsedURL); + const otherServerURL = ViewManager.getViewByURL(parsedURL); if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) { WindowManager.showMainWindow(parsedURL); return {action: 'deny'}; diff --git a/src/main/windows/windowManager.test.js b/src/main/windows/windowManager.test.js index f4a415cf..4225dd94 100644 --- a/src/main/windows/windowManager.test.js +++ b/src/main/windows/windowManager.test.js @@ -7,8 +7,7 @@ import {systemPreferences, desktopCapturer} from 'electron'; import Config from 'common/config'; -import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView'; -import urlUtils from 'common/utils/url'; +import {getTabViewName} from 'common/tabs/TabView'; import { getAdjustedWindowBoundaries, @@ -17,6 +16,8 @@ import { } from 'main/utils'; import LoadingScreen from '../views/loadingScreen'; +import ViewManager from 'main/views/viewManager'; + import {WindowManager} from './windowManager'; import MainWindow from './mainWindow'; import SettingsWindow from './settingsWindow'; @@ -52,11 +53,6 @@ jest.mock('electron', () => ({ jest.mock('common/config', () => ({})); -jest.mock('common/utils/url', () => ({ - isTeamUrl: jest.fn(), - isAdminUrl: jest.fn(), - cleanPathName: jest.fn(), -})); jest.mock('common/tabs/TabView', () => ({ getTabViewName: jest.fn(), TAB_MESSAGING: 'tab-messaging', @@ -68,7 +64,16 @@ jest.mock('../utils', () => ({ resetScreensharePermissionsMacOS: jest.fn(), })); jest.mock('../views/viewManager', () => ({ - ViewManager: jest.fn(), + isLoadingScreenHidden: jest.fn(), + getView: jest.fn(), + getViewByWebContentsId: jest.fn(), + getCurrentView: jest.fn(), + isViewClosed: jest.fn(), + openClosedTab: jest.fn(), + handleDeepLink: jest.fn(), + setLoadingScreenBounds: jest.fn(), + showByName: jest.fn(), + updateMainWindow: jest.fn(), })); jest.mock('../CriticalErrorHandler', () => jest.fn()); jest.mock('../views/loadingScreen', () => ({ @@ -94,27 +99,8 @@ jest.mock('./callsWidgetWindow'); jest.mock('main/views/webContentEvents', () => ({})); describe('main/windows/windowManager', () => { - describe('handleUpdateConfig', () => { - const windowManager = new WindowManager(); - - beforeEach(() => { - windowManager.viewManager = { - reloadConfiguration: jest.fn(), - }; - }); - - it('should reload config', () => { - windowManager.handleUpdateConfig(); - expect(windowManager.viewManager.reloadConfiguration).toHaveBeenCalled(); - }); - }); - describe('showMainWindow', () => { const windowManager = new WindowManager(); - windowManager.viewManager = { - handleDeepLink: jest.fn(), - updateMainWindow: jest.fn(), - }; windowManager.initializeViewManager = jest.fn(); const mainWindow = { @@ -150,7 +136,7 @@ describe('main/windows/windowManager', () => { it('should open deep link when provided', () => { windowManager.showMainWindow('mattermost://server-1.com/subpath'); - expect(windowManager.viewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath'); + expect(ViewManager.handleDeepLink).toHaveBeenCalledWith('mattermost://server-1.com/subpath'); }); }); @@ -167,10 +153,6 @@ describe('main/windows/windowManager', () => { }, }, }; - windowManager.viewManager = { - getCurrentView: () => view, - setLoadingScreenBounds: jest.fn(), - }; const mainWindow = { getContentBounds: () => ({width: 800, height: 600}), getSize: () => [1000, 900], @@ -182,6 +164,7 @@ describe('main/windows/windowManager', () => { beforeEach(() => { MainWindow.get.mockReturnValue(mainWindow); jest.useFakeTimers(); + ViewManager.getCurrentView.mockReturnValue(view); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); }); @@ -230,11 +213,7 @@ describe('main/windows/windowManager', () => { }, }, }; - windowManager.viewManager = { - getCurrentView: () => view, - setLoadingScreenBounds: jest.fn(), - loadingScreenState: 3, - }; + windowManager.teamDropdown = { updateWindowBounds: jest.fn(), }; @@ -244,13 +223,15 @@ describe('main/windows/windowManager', () => { }; beforeEach(() => { + ViewManager.getCurrentView.mockReturnValue(view); + ViewManager.isLoadingScreenHidden.mockReturnValue(true); MainWindow.get.mockReturnValue(mainWindow); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); }); afterEach(() => { windowManager.isResizing = false; - jest.resetAllMocks(); + jest.clearAllMocks(); }); it('should update loading screen and team dropdown bounds', () => { @@ -265,6 +246,7 @@ describe('main/windows/windowManager', () => { LoadingScreen.isHidden.mockReturnValue(true); const event = {preventDefault: jest.fn()}; windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); + expect(event.preventDefault).toHaveBeenCalled(); expect(view.setBounds).not.toHaveBeenCalled(); }); @@ -295,6 +277,7 @@ describe('main/windows/windowManager', () => { }; beforeEach(() => { + ViewManager.getCurrentView.mockReturnValue(view); MainWindow.get.mockReturnValue(mainWindow); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); }); @@ -304,17 +287,7 @@ describe('main/windows/windowManager', () => { jest.resetAllMocks(); }); - it('should not handle bounds if no window available', () => { - windowManager.handleResizedMainWindow(); - expect(windowManager.isResizing).toBe(false); - expect(view.setBounds).not.toHaveBeenCalled(); - }); - it('should use getContentBounds when the platform is different to linux', () => { - windowManager.viewManager = { - getCurrentView: () => view, - }; - const originalPlatform = process.platform; Object.defineProperty(process, 'platform', { value: 'windows', @@ -490,66 +463,63 @@ describe('main/windows/windowManager', () => { describe('switchServer', () => { const windowManager = new WindowManager(); - windowManager.viewManager = { - showByName: jest.fn(), - }; + const servers = [ + { + name: 'server-1', + order: 1, + tabs: [ + { + name: 'tab-1', + order: 0, + isOpen: false, + }, + { + name: 'tab-2', + order: 2, + isOpen: true, + }, + { + name: 'tab-3', + order: 1, + isOpen: true, + }, + ], + }, { + name: 'server-2', + order: 0, + tabs: [ + { + name: 'tab-1', + order: 0, + isOpen: false, + }, + { + name: 'tab-2', + order: 2, + isOpen: true, + }, + { + name: 'tab-3', + order: 1, + isOpen: true, + }, + ], + lastActiveTab: 2, + }, + ]; + const map = servers.reduce((arr, item) => { + item.tabs.forEach((tab) => { + arr.push([`${item.name}_${tab.name}`, {}]); + }); + return arr; + }, []); + const views = new Map(map); beforeEach(() => { jest.useFakeTimers(); getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`); - - Config.teams = [ - { - name: 'server-1', - order: 1, - tabs: [ - { - name: 'tab-1', - order: 0, - isOpen: false, - }, - { - name: 'tab-2', - order: 2, - isOpen: true, - }, - { - name: 'tab-3', - order: 1, - isOpen: true, - }, - ], - }, { - name: 'server-2', - order: 0, - tabs: [ - { - name: 'tab-1', - order: 0, - isOpen: false, - }, - { - name: 'tab-2', - order: 2, - isOpen: true, - }, - { - name: 'tab-3', - order: 1, - isOpen: true, - }, - ], - lastActiveTab: 2, - }, - ]; - - const map = Config.teams.reduce((arr, item) => { - item.tabs.forEach((tab) => { - arr.push([`${item.name}_${tab.name}`, {}]); - }); - return arr; - }, []); - windowManager.viewManager.views = new Map(map); + Config.teams = servers.concat(); + ViewManager.getView.mockImplementation((name) => views.get(name)); }); afterEach(() => { @@ -566,38 +536,35 @@ describe('main/windows/windowManager', () => { it('should do nothing if cannot find the server', () => { windowManager.switchServer('server-3'); expect(getTabViewName).not.toBeCalled(); - expect(windowManager.viewManager.showByName).not.toBeCalled(); + expect(ViewManager.showByName).not.toBeCalled(); }); it('should show first open tab in order when last active not defined', () => { windowManager.switchServer('server-1'); - expect(windowManager.viewManager.showByName).toHaveBeenCalledWith('server-1_tab-3'); + expect(ViewManager.showByName).toHaveBeenCalledWith('server-1_tab-3'); }); it('should show last active tab of chosen server', () => { windowManager.switchServer('server-2'); - expect(windowManager.viewManager.showByName).toHaveBeenCalledWith('server-2_tab-2'); + expect(ViewManager.showByName).toHaveBeenCalledWith('server-2_tab-2'); }); it('should wait for view to exist if specified', () => { - windowManager.viewManager.views.delete('server-1_tab-3'); + views.delete('server-1_tab-3'); windowManager.switchServer('server-1', true); - expect(windowManager.viewManager.showByName).not.toBeCalled(); + expect(ViewManager.showByName).not.toBeCalled(); jest.advanceTimersByTime(200); - expect(windowManager.viewManager.showByName).not.toBeCalled(); + expect(ViewManager.showByName).not.toBeCalled(); - windowManager.viewManager.views.set('server-1_tab-3', {}); + views.set('server-1_tab-3', {}); jest.advanceTimersByTime(200); - expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_tab-3'); + expect(ViewManager.showByName).toBeCalledWith('server-1_tab-3'); }); }); describe('handleHistory', () => { const windowManager = new WindowManager(); - windowManager.viewManager = { - getCurrentView: jest.fn(), - }; it('should only go to offset if it can', () => { const view = { @@ -608,12 +575,12 @@ describe('main/windows/windowManager', () => { }, }, }; - windowManager.viewManager.getCurrentView.mockReturnValue(view); + ViewManager.getCurrentView.mockReturnValue(view); windowManager.handleHistory(null, 1); expect(view.view.webContents.goToOffset).not.toBeCalled(); - windowManager.viewManager.getCurrentView.mockReturnValue({ + ViewManager.getCurrentView.mockReturnValue({ ...view, view: { ...view.view, @@ -644,7 +611,7 @@ describe('main/windows/windowManager', () => { view.view.webContents.goToOffset.mockImplementation(() => { throw new Error('hi'); }); - windowManager.viewManager.getCurrentView.mockReturnValue(view); + ViewManager.getCurrentView.mockReturnValue(view); windowManager.handleHistory(null, 1); expect(view.load).toBeCalledWith('http://server-1.com'); @@ -653,9 +620,6 @@ describe('main/windows/windowManager', () => { describe('selectTab', () => { const windowManager = new WindowManager(); - windowManager.viewManager = { - getCurrentView: jest.fn(), - }; windowManager.switchTab = jest.fn(); beforeEach(() => { @@ -690,7 +654,7 @@ describe('main/windows/windowManager', () => { }); it('should select next server when open', () => { - windowManager.viewManager.getCurrentView.mockReturnValue({ + ViewManager.getCurrentView.mockReturnValue({ tab: { server: { name: 'server-1', @@ -704,7 +668,7 @@ describe('main/windows/windowManager', () => { }); it('should select previous server when open', () => { - windowManager.viewManager.getCurrentView.mockReturnValue({ + ViewManager.getCurrentView.mockReturnValue({ tab: { server: { name: 'server-1', @@ -718,7 +682,7 @@ describe('main/windows/windowManager', () => { }); it('should skip over closed tab', () => { - windowManager.viewManager.getCurrentView.mockReturnValue({ + ViewManager.getCurrentView.mockReturnValue({ tab: { server: { name: 'server-1', @@ -731,183 +695,8 @@ describe('main/windows/windowManager', () => { }); }); - describe('handleBrowserHistoryPush', () => { - const windowManager = new WindowManager(); - const view1 = { - name: 'server-1_tab-messaging', - isLoggedIn: true, - tab: { - type: TAB_MESSAGING, - server: { - url: 'http://server-1.com', - }, - }, - view: { - webContents: { - send: jest.fn(), - }, - }, - }; - const view2 = { - ...view1, - name: 'server-1_other_type_1', - tab: { - ...view1.tab, - type: 'other_type_1', - }, - view: { - webContents: { - send: jest.fn(), - }, - }, - }; - const view3 = { - ...view1, - name: 'server-1_other_type_2', - tab: { - ...view1.tab, - type: 'other_type_2', - }, - view: { - webContents: { - send: jest.fn(), - }, - }, - }; - windowManager.viewManager = { - views: new Map([ - ['server-1_tab-messaging', view1], - ['server-1_other_type_1', view2], - ]), - closedViews: new Map([ - ['server-1_other_type_2', view3], - ]), - openClosedTab: jest.fn(), - showByName: jest.fn(), - getViewByURL: jest.fn(), - }; - windowManager.handleBrowserHistoryButton = jest.fn(); - - beforeEach(() => { - Config.teams = [ - { - name: 'server-1', - url: 'http://server-1.com', - order: 0, - tabs: [ - { - name: 'tab-messaging', - order: 0, - isOpen: true, - }, - { - name: 'other_type_1', - order: 2, - isOpen: true, - }, - { - name: 'other_type_2', - order: 1, - isOpen: false, - }, - ], - }, - ]; - urlUtils.cleanPathName.mockImplementation((base, path) => path); - }); - - afterEach(() => { - jest.resetAllMocks(); - Config.teams = []; - }); - - it('should open closed view if pushing to it', () => { - windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_other_type_2'}); - windowManager.viewManager.openClosedTab.mockImplementation((name) => { - const view = windowManager.viewManager.closedViews.get(name); - windowManager.viewManager.closedViews.delete(name); - windowManager.viewManager.views.set(name, view); - }); - - windowManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_2/subpath'); - expect(windowManager.viewManager.openClosedTab).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/other_type_2/subpath'); - }); - - it('should open redirect view if different from current view', () => { - windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_other_type_1'}); - windowManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_1/subpath'); - expect(windowManager.viewManager.showByName).toBeCalledWith('server-1_other_type_1'); - }); - - it('should ignore redirects to "/" to Messages from other tabs', () => { - windowManager.viewManager.getViewByURL.mockReturnValue({name: 'server-1_tab-messaging'}); - windowManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/'); - expect(view1.view.webContents.send).not.toBeCalled(); - }); - }); - - describe('handleBrowserHistoryButton', () => { - const windowManager = new WindowManager(); - const view1 = { - name: 'server-1_tab-messaging', - isLoggedIn: true, - isAtRoot: true, - tab: { - type: TAB_MESSAGING, - server: { - url: 'http://server-1.com', - }, - url: new URL('http://server-1.com'), - }, - view: { - webContents: { - canGoBack: jest.fn(), - canGoForward: jest.fn(), - clearHistory: jest.fn(), - send: jest.fn(), - getURL: jest.fn(), - }, - }, - }; - windowManager.viewManager = { - views: new Map([ - ['server-1_tab-messaging', view1], - ]), - }; - - beforeEach(() => { - Config.teams = [ - { - name: 'server-1', - url: 'http://server-1.com', - order: 0, - tabs: [ - { - name: 'tab-messaging', - order: 0, - isOpen: true, - }, - ], - }, - ]; - }); - - afterEach(() => { - jest.resetAllMocks(); - Config.teams = []; - view1.isAtRoot = true; - }); - - it('should erase history and set isAtRoot when navigating to root URL', () => { - view1.isAtRoot = false; - view1.view.webContents.getURL.mockReturnValue(view1.tab.url.toString()); - windowManager.handleBrowserHistoryButton(null, 'server-1_tab-messaging'); - expect(view1.view.webContents.clearHistory).toHaveBeenCalled(); - expect(view1.isAtRoot).toBe(true); - }); - }); - describe('createCallsWidgetWindow', () => { + const windowManager = new WindowManager(); const view = { name: 'server-1_tab-messaging', serverInfo: { @@ -927,19 +716,13 @@ describe('main/windows/windowManager', () => { close: jest.fn(), }; }); + ViewManager.getView.mockReturnValue(view); }); afterEach(() => { jest.resetAllMocks(); }); - const windowManager = new WindowManager(); - windowManager.viewManager = { - views: new Map([ - ['server-1_tab-messaging', view], - ]), - }; - it('should create calls widget window', async () => { expect(windowManager.callsWidgetWindow).toBeUndefined(); await windowManager.createCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'}); @@ -965,10 +748,6 @@ describe('main/windows/windowManager', () => { describe('handleGetDesktopSources', () => { const windowManager = new WindowManager(); - windowManager.viewManager = { - showByName: jest.fn(), - getCurrentView: jest.fn(), - }; beforeEach(() => { CallsWidgetWindow.mockImplementation(() => { @@ -1031,7 +810,8 @@ describe('main/windows/windowManager', () => { }); return arr; }, []); - windowManager.viewManager.views = new Map(map); + const views = new Map(map); + ViewManager.getView.mockImplementation((name) => views.get(name)); }); afterEach(() => { @@ -1058,7 +838,7 @@ describe('main/windows/windowManager', () => { await windowManager.handleGetDesktopSources('server-1_tab-1', null); - expect(windowManager.viewManager.views.get('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('desktop-sources-result', [ + expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('desktop-sources-result', [ { id: 'screen0', }, @@ -1074,7 +854,7 @@ describe('main/windows/windowManager', () => { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); - expect(windowManager.viewManager.views.get('server-2_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { + expect(ViewManager.getView('server-2_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); @@ -1097,10 +877,10 @@ describe('main/windows/windowManager', () => { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); - expect(windowManager.viewManager.views.get('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { + expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); - expect(windowManager.viewManager.views.get('server-1_tab-1').view.webContents.send).toHaveBeenCalledTimes(1); + expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledTimes(1); expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); }); @@ -1128,7 +908,7 @@ describe('main/windows/windowManager', () => { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); - expect(windowManager.viewManager.views.get('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { + expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', { err: 'screen-permissions', }); @@ -1146,10 +926,6 @@ describe('main/windows/windowManager', () => { describe('handleDesktopSourcesModalRequest', () => { const windowManager = new WindowManager(); windowManager.switchServer = jest.fn(); - windowManager.viewManager = { - showByName: jest.fn(), - getCurrentView: jest.fn(), - }; beforeEach(() => { CallsWidgetWindow.mockImplementation(() => { @@ -1206,7 +982,8 @@ describe('main/windows/windowManager', () => { }); return arr; }, []); - windowManager.viewManager.views = new Map(map); + const views = new Map(map); + ViewManager.getView.mockImplementation((name) => views.get(name)); }); afterEach(() => { @@ -1224,10 +1001,6 @@ describe('main/windows/windowManager', () => { describe('handleCallsWidgetChannelLinkClick', () => { const windowManager = new WindowManager(); windowManager.switchServer = jest.fn(); - windowManager.viewManager = { - showByName: jest.fn(), - getCurrentView: jest.fn(), - }; beforeEach(() => { CallsWidgetWindow.mockImplementation(() => { @@ -1285,7 +1058,8 @@ describe('main/windows/windowManager', () => { }); return arr; }, []); - windowManager.viewManager.views = new Map(map); + const views = new Map(map); + ViewManager.getView.mockImplementation((name) => views.get(name)); }); afterEach(() => { @@ -1347,11 +1121,6 @@ describe('main/windows/windowManager', () => { }, }, }; - windowManager.viewManager = { - views: new Map([ - ['server-1_tab-messaging', view1], - ]), - }; beforeEach(() => { CallsWidgetWindow.mockImplementation(() => { @@ -1360,6 +1129,7 @@ describe('main/windows/windowManager', () => { getMainView: jest.fn().mockReturnValue(view1), }; }); + ViewManager.getView.mockReturnValue(view1); }); afterEach(() => { @@ -1376,23 +1146,10 @@ describe('main/windows/windowManager', () => { }); describe('getServerURLFromWebContentsId', () => { - const view = { - name: 'server-1_tab-messaging', - serverInfo: { - server: { - url: new URL('http://server-1.com'), - }, - }, - }; const windowManager = new WindowManager(); - windowManager.viewManager = { - views: new Map([ - ['server-1_tab-messaging', view], - ]), - findViewByWebContent: jest.fn(), - }; it('should return calls widget URL', () => { + ViewManager.getView.mockReturnValue({name: 'server-1_tab-messaging'}); CallsWidgetWindow.mockImplementation(() => { return { on: jest.fn(), diff --git a/src/main/windows/windowManager.ts b/src/main/windows/windowManager.ts index 85c48b59..2f26652a 100644 --- a/src/main/windows/windowManager.ts +++ b/src/main/windows/windowManager.ts @@ -16,20 +16,14 @@ import { import { MAXIMIZE_CHANGE, HISTORY, - REACT_APP_INITIALIZED, FOCUS_THREE_DOT_MENU, GET_DARK_MODE, UPDATE_SHORTCUT_MENU, BROWSER_HISTORY_PUSH, - APP_LOGGED_IN, - GET_VIEW_NAME, GET_VIEW_WEBCONTENTS_ID, RESIZE_MODAL, - APP_LOGGED_OUT, - BROWSER_HISTORY_BUTTON, DISPATCH_GET_DESKTOP_SOURCES, DESKTOP_SOURCES_RESULT, - RELOAD_CURRENT_VIEW, VIEW_FINISHED_RESIZING, CALLS_JOIN_CALL, CALLS_LEAVE_CALL, @@ -39,10 +33,9 @@ import { CALLS_LINK_CLICK, } from 'common/communication'; import {Logger} from 'common/log'; -import urlUtils from 'common/utils/url'; import {SECOND} from 'common/utils/constants'; import Config from 'common/config'; -import {getTabViewName, TAB_MESSAGING} from 'common/tabs/TabView'; +import {getTabViewName} from 'common/tabs/TabView'; import downloadsManager from 'main/downloadsManager'; import {MattermostView} from 'main/views/MattermostView'; @@ -54,7 +47,7 @@ import { openScreensharePermissionsSettingsMacOS, } from '../utils'; -import {ViewManager} from '../views/viewManager'; +import ViewManager from '../views/viewManager'; import LoadingScreen from '../views/loadingScreen'; import TeamDropdownView from '../views/teamDropdownView'; import DownloadsDropdownView from '../views/downloadsDropdownView'; @@ -72,7 +65,6 @@ export class WindowManager { assetsDir: string; callsWidgetWindow?: CallsWidgetWindow; - viewManager?: ViewManager; teamDropdown?: TeamDropdownView; downloadsDropdown?: DownloadsDropdownView; downloadsDropdownMenu?: DownloadsDropdownMenuView; @@ -84,14 +76,7 @@ export class WindowManager { ipcMain.on(HISTORY, this.handleHistory); ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode); - ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); - ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush); - ipcMain.on(BROWSER_HISTORY_BUTTON, this.handleBrowserHistoryButton); - ipcMain.on(APP_LOGGED_IN, this.handleAppLoggedIn); - ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut); - ipcMain.handle(GET_VIEW_NAME, this.handleGetViewName); ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId); - ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); // Calls handlers @@ -104,12 +89,6 @@ export class WindowManager { ipcMain.on(CALLS_LINK_CLICK, this.genCallsEventHandler(this.handleCallsLinkClick)); } - handleUpdateConfig = () => { - if (this.viewManager) { - this.viewManager.reloadConfiguration(Config.teams || []); - } - } - genCallsEventHandler = (handler: CallsEventHandler) => { return (event: IpcMainEvent, viewName: string, msg?: any) => { if (this.callsWidgetWindow && !this.callsWidgetWindow.isAllowedEvent(event)) { @@ -132,7 +111,7 @@ export class WindowManager { // window to be fully closed. await this.callsWidgetWindow.close(); } - const currentView = this.viewManager?.views.get(viewName); + const currentView = ViewManager.getView(viewName); if (!currentView) { log.error('unable to create calls widget window: currentView is missing'); return; @@ -209,7 +188,7 @@ export class WindowManager { } if (deeplinkingURL) { - this.viewManager?.handleDeepLink(deeplinkingURL); + ViewManager.handleDeepLink(deeplinkingURL); } } @@ -227,7 +206,7 @@ export class WindowManager { } mainWindow.on('will-resize', this.handleWillResizeMainWindow); mainWindow.on('resized', this.handleResizedMainWindow); - mainWindow.on('focus', this.focusBrowserView); + mainWindow.on('focus', ViewManager.focusCurrentView); mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen')); mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen')); @@ -255,7 +234,7 @@ export class WindowManager { handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => { log.silly('handleWillResizeMainWindow'); - if (!(this.viewManager && MainWindow.get())) { + if (!MainWindow.get()) { return; } @@ -268,7 +247,7 @@ export class WindowManager { return; } - if (this.isResizing && LoadingScreen.isHidden() && this.viewManager.getCurrentView()) { + if (this.isResizing && LoadingScreen.isHidden() && ViewManager.getCurrentView()) { log.debug('prevented resize'); event.preventDefault(); return; @@ -310,7 +289,7 @@ export class WindowManager { handleResizeMainWindow = () => { log.silly('handleResizeMainWindow'); - if (!(this.viewManager && MainWindow.get())) { + if (!MainWindow.get()) { return; } if (this.isResizing) { @@ -333,7 +312,7 @@ export class WindowManager { setCurrentViewBounds = (bounds: {width: number; height: number}) => { log.debug('setCurrentViewBounds', {bounds}); - const currentView = this.viewManager?.getCurrentView(); + const currentView = ViewManager.getCurrentView(); if (currentView) { const adjustedBounds = getAdjustedWindowBoundaries(bounds.width, bounds.height, shouldHaveBackBar(currentView.tab.url, currentView.view.webContents.getURL())); this.setBoundsFunction(currentView, adjustedBounds); @@ -393,12 +372,6 @@ export class WindowManager { // TODO: should we include popups? } - sendToMattermostViews = (channel: string, ...args: unknown[]) => { - if (this.viewManager) { - this.viewManager.sendToAllViews(channel, ...args); - } - } - restoreMain = () => { log.info('restoreMain'); @@ -457,12 +430,8 @@ export class WindowManager { } initializeViewManager = () => { - if (!this.viewManager && Config) { - this.viewManager = new ViewManager(); - this.viewManager.load(); - this.viewManager.showInitial(); - this.initializeCurrentServerName(); - } + ViewManager.init(); + this.initializeCurrentServerName(); } initializeCurrentServerName = () => { @@ -488,13 +457,13 @@ export class WindowManager { const tabViewName = getTabViewName(serverName, nextTab.name); if (waitForViewToExist) { const timeout = setInterval(() => { - if (this.viewManager?.views.has(tabViewName)) { - this.viewManager?.showByName(tabViewName); + if (ViewManager.getView(tabViewName)) { + ViewManager.showByName(tabViewName); clearTimeout(timeout); } }, 100); } else { - this.viewManager?.showByName(tabViewName); + ViewManager.showByName(tabViewName); } ipcMain.emit(UPDATE_SHORTCUT_MENU); } @@ -503,23 +472,7 @@ export class WindowManager { log.debug('switchTab'); this.showMainWindow(); const tabViewName = getTabViewName(serverName, tabName); - this.viewManager?.showByName(tabViewName); - } - - focusBrowserView = () => { - log.debug('focusBrowserView'); - - if (this.viewManager) { - this.viewManager.focus(); - } else { - log.error('Trying to call focus when the viewManager has not yet been initialized'); - } - } - - openBrowserViewDevTools = () => { - if (this.viewManager) { - this.viewManager.openViewDevTools(); - } + ViewManager.showByName(tabViewName); } focusThreeDotMenu = () => { @@ -533,24 +486,6 @@ export class WindowManager { }; } - handleReactAppInitialized = (e: IpcMainEvent, view: string) => { - log.debug('handleReactAppInitialized', view); - - if (this.viewManager) { - this.viewManager.setServerInitialized(view); - } - } - - getViewNameByWebContentsId = (webContentsId: number) => { - const view = this.viewManager?.findViewByWebContent(webContentsId); - return view?.name; - } - - getServerNameByWebContentsId = (webContentsId: number) => { - const view = this.viewManager?.findViewByWebContent(webContentsId); - return view?.tab.server.name; - } - close = () => { const focused = BrowserWindow.getFocusedWindow(); focused?.close(); @@ -578,7 +513,7 @@ export class WindowManager { } reload = () => { - const currentView = this.viewManager?.getCurrentView(); + const currentView = ViewManager.getCurrentView(); if (currentView) { LoadingScreen.show(); currentView.reload(); @@ -586,7 +521,7 @@ export class WindowManager { } sendToFind = () => { - const currentView = this.viewManager?.getCurrentView(); + const currentView = ViewManager.getCurrentView(); if (currentView) { currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']}); } @@ -595,15 +530,13 @@ export class WindowManager { handleHistory = (event: IpcMainEvent, offset: number) => { log.debug('handleHistory', offset); - if (this.viewManager) { - const activeView = this.viewManager.getCurrentView(); - if (activeView && activeView.view.webContents.canGoToOffset(offset)) { - try { - activeView.view.webContents.goToOffset(offset); - } catch (error) { - log.error(error); - activeView.load(activeView.tab.url); - } + const activeView = ViewManager.getCurrentView(); + if (activeView && activeView.view.webContents.canGoToOffset(offset)) { + try { + activeView.view.webContents.goToOffset(offset); + } catch (error) { + log.error(error); + activeView.load(activeView.tab.url); } } } @@ -617,7 +550,7 @@ export class WindowManager { } selectTab = (fn: (order: number, length: number) => number) => { - const currentView = this.viewManager?.getCurrentView(); + const currentView = ViewManager.getCurrentView(); if (!currentView) { return; } @@ -645,76 +578,10 @@ export class WindowManager { return Config.darkMode; } - handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => { - log.debug('handleBrowserHistoryPush', {viewName, pathName}); - - const currentView = this.viewManager?.views.get(viewName); - const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName); - const redirectedViewName = this.viewManager?.getViewByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.name || viewName; - if (this.viewManager?.closedViews.has(redirectedViewName)) { - // If it's a closed view, just open it and stop - this.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${cleanedPathName}`); - return; - } - let redirectedView = this.viewManager?.views.get(redirectedViewName) || currentView; - if (redirectedView !== currentView && redirectedView?.tab.server.name === this.currentServerName && redirectedView?.isLoggedIn) { - log.info('redirecting to a new view', redirectedView?.name || viewName); - this.viewManager?.showByName(redirectedView?.name || viewName); - } else { - redirectedView = currentView; - } - - // Special case check for Channels to not force a redirect to "/", causing a refresh - if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) { - redirectedView?.view.webContents.send(BROWSER_HISTORY_PUSH, cleanedPathName); - if (redirectedView) { - this.handleBrowserHistoryButton(e, redirectedView.name); - } - } - } - - handleBrowserHistoryButton = (e: IpcMainEvent, viewName: string) => { - log.debug('handleBrowserHistoryButton', viewName); - - const currentView = this.viewManager?.views.get(viewName); - if (currentView) { - if (currentView.view.webContents.getURL() === currentView.tab.url.toString()) { - currentView.view.webContents.clearHistory(); - currentView.isAtRoot = true; - } else { - currentView.isAtRoot = false; - } - currentView?.view.webContents.send(BROWSER_HISTORY_BUTTON, currentView.view.webContents.canGoBack(), currentView.view.webContents.canGoForward()); - } - } - getCurrentTeamName = () => { return this.currentServerName; } - handleAppLoggedIn = (event: IpcMainEvent, viewName: string) => { - log.debug('handleAppLoggedIn', viewName); - - const view = this.viewManager?.views.get(viewName); - if (view && !view.isLoggedIn) { - view.isLoggedIn = true; - this.viewManager?.reloadViewIfNeeded(viewName); - } - } - - handleAppLoggedOut = (event: IpcMainEvent, viewName: string) => { - log.debug('handleAppLoggedOut', viewName); - - const view = this.viewManager?.views.get(viewName); - if (view && view.isLoggedIn) { - view.isLoggedIn = false; - } - } - - handleGetViewName = (event: IpcMainInvokeEvent) => { - return this.getViewNameByWebContentsId(event.sender.id); - } - handleGetWebContentsId = (event: IpcMainInvokeEvent) => { return event.sender.id; } @@ -722,7 +589,7 @@ export class WindowManager { handleGetDesktopSources = async (viewName: string, opts: Electron.SourcesOptions) => { log.debug('handleGetDesktopSources', {viewName, opts}); - const view = this.viewManager?.views.get(viewName); + const view = ViewManager.getView(viewName); if (!view) { log.error('handleGetDesktopSources: view not found'); return Promise.resolve(); @@ -784,27 +651,12 @@ export class WindowManager { }); } - handleReloadCurrentView = () => { - log.debug('handleReloadCurrentView'); - - const view = this.viewManager?.getCurrentView(); - if (!view) { - return; - } - view?.reload(); - this.viewManager?.showByName(view?.name); - } - getServerURLFromWebContentsId = (id: number) => { if (this.callsWidgetWindow && (id === this.callsWidgetWindow.getWebContentsId() || id === this.callsWidgetWindow.getPopOutWebContentsId())) { return this.callsWidgetWindow.getURL(); } - const viewName = this.getViewNameByWebContentsId(id); - if (!viewName) { - return undefined; - } - return this.viewManager?.views.get(viewName)?.tab.server.url; + return ViewManager.getViewByWebContentsId(id)?.tab.server.url; } }