From b71322a682205d01a03cc25166e40a3cc81d82cb Mon Sep 17 00:00:00 2001 From: Devin Binnie <52460000+devinbinnie@users.noreply.github.com> Date: Fri, 14 Apr 2023 12:14:11 -0400 Subject: [PATCH] Migrate teamDropdownView to singleton (#2677) --- src/common/communication.ts | 1 - src/main/app/initialize.ts | 3 - src/main/app/intercom.ts | 6 - src/main/views/teamDropdownView.test.js | 8 +- src/main/views/teamDropdownView.ts | 180 ++++++++++++------------ src/main/views/viewManager.test.js | 1 + src/main/views/viewManager.ts | 8 +- src/main/windows/windowManager.test.js | 12 +- src/main/windows/windowManager.ts | 9 +- 9 files changed, 107 insertions(+), 121 deletions(-) diff --git a/src/common/communication.ts b/src/common/communication.ts index aa2f9023..7beb2bc6 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -6,7 +6,6 @@ export const SWITCH_TAB = 'switch-tab'; export const CLOSE_TAB = 'close-tab'; export const OPEN_TAB = 'open-tab'; export const SET_ACTIVE_VIEW = 'set-active-view'; -export const UPDATE_LAST_ACTIVE = 'update-last-active'; export const FOCUS_BROWSERVIEW = 'focus-browserview'; export const HISTORY = 'history'; diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index edd61a8b..f96c1243 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -20,7 +20,6 @@ import { SHOW_EDIT_SERVER_MODAL, SHOW_REMOVE_SERVER_MODAL, UPDATE_SHORTCUT_MENU, - UPDATE_LAST_ACTIVE, GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, USER_ACTIVITY_UPDATE, START_UPGRADE, @@ -96,7 +95,6 @@ import { handleSelectDownload, handleSwitchServer, handleSwitchTab, - handleUpdateLastActive, handlePingDomain, handleGetOrderedServers, handleGetOrderedTabsForServer, @@ -264,7 +262,6 @@ function initializeInterCommunicationEventListeners() { ipcMain.handle('get-app-version', handleAppVersion); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView); - ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive); if (process.platform !== 'darwin') { ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index becad495..281ed188 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -279,12 +279,6 @@ export async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: return result.filePaths[0]; } -export function handleUpdateLastActive(event: IpcMainEvent, tabId: string) { - log.debug('handleUpdateLastActive', {tabId}); - - ServerManager.updateLastActive(tabId); -} - export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise { return Promise.allSettled([ ping(new URL(`https://${url}`)), diff --git a/src/main/views/teamDropdownView.test.js b/src/main/views/teamDropdownView.test.js index 64308d07..d3322588 100644 --- a/src/main/views/teamDropdownView.test.js +++ b/src/main/views/teamDropdownView.test.js @@ -7,7 +7,7 @@ import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHA import MainWindow from 'main/windows/mainWindow'; -import TeamDropdownView from './teamDropdownView'; +import {TeamDropdownView} from './teamDropdownView'; jest.mock('main/utils', () => ({ getLocalPreload: (file) => file, @@ -61,6 +61,12 @@ describe('main/views/teamDropdownView', () => { it('should change the view bounds based on open/closed state', () => { const teamDropdownView = new TeamDropdownView(); + teamDropdownView.view = { + setBounds: jest.fn(), + webContents: { + focus: jest.fn(), + }, + }; teamDropdownView.bounds = {width: 400, height: 300}; teamDropdownView.handleOpen(); expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds); diff --git a/src/main/views/teamDropdownView.ts b/src/main/views/teamDropdownView.ts index 7dbe6261..b0d9426a 100644 --- a/src/main/views/teamDropdownView.ts +++ b/src/main/views/teamDropdownView.ts @@ -3,7 +3,7 @@ import {BrowserView, ipcMain, IpcMainEvent} from 'electron'; -import {CombinedConfig, MattermostTeam} from 'types/config'; +import {MattermostTeam} from 'types/config'; import AppState from 'common/appState'; import { @@ -14,7 +14,6 @@ import { UPDATE_APPSTATE, REQUEST_TEAMS_DROPDOWN_INFO, RECEIVE_DROPDOWN_MENU_SIZE, - SET_ACTIVE_VIEW, SERVERS_UPDATE, } from 'common/communication'; import Config from 'common/config'; @@ -29,29 +28,41 @@ import MainWindow from '../windows/mainWindow'; const log = new Logger('TeamDropdownView'); -export default class TeamDropdownView { - view: BrowserView; - bounds?: Electron.Rectangle; - teams: MattermostTeam[]; - activeTeam?: string; - darkMode: boolean; - enableServerManagement?: boolean; - hasGPOTeams?: boolean; - unreads?: Map; - mentions?: Map; - expired?: Map; - windowBounds?: Electron.Rectangle; - isOpen: boolean; +export class TeamDropdownView { + private view?: BrowserView; + private teams: MattermostTeam[]; + private hasGPOTeams: boolean; + private isOpen: boolean; + private bounds: Electron.Rectangle; + + private unreads: Map; + private mentions: Map; + private expired: Map; + + private windowBounds?: Electron.Rectangle; constructor() { - this.teams = this.getOrderedTeams(); - this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); - this.darkMode = Config.darkMode; - this.enableServerManagement = Config.enableServerManagement; + this.teams = []; + this.hasGPOTeams = false; this.isOpen = false; + this.bounds = this.getBounds(0, 0); - this.windowBounds = MainWindow.getBounds(); + this.unreads = new Map(); + this.mentions = new Map(); + this.expired = new Map(); + ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen); + ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose); + ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); + + ipcMain.on(EMIT_CONFIGURATION, this.updateDropdown); + ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown); + + AppState.on(UPDATE_APPSTATE, this.updateMentions); + ServerManager.on(SERVERS_UPDATE, this.updateServers); + } + + init = () => { const preload = getLocalPreload('desktopAPI.js'); this.view = new BrowserView({webPreferences: { preload, @@ -61,63 +72,12 @@ export default class TeamDropdownView { // @ts-ignore transparent: true, }}); - this.view.webContents.loadURL(getLocalURLString('dropdown.html')); + + this.setOrderedTeams(); + this.windowBounds = MainWindow.getBounds(); + this.updateDropdown(); MainWindow.get()?.addBrowserView(this.view); - - ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen); - ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose); - ipcMain.on(EMIT_CONFIGURATION, this.updateConfig); - ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown); - ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); - ipcMain.on(SET_ACTIVE_VIEW, this.updateActiveTeam); - AppState.on(UPDATE_APPSTATE, this.updateMentions); - - ServerManager.on(SERVERS_UPDATE, this.updateServers); - } - - private getOrderedTeams = () => { - return ServerManager.getOrderedServers().map((team) => team.toMattermostTeam()); - } - - updateServers = () => { - this.teams = this.getOrderedTeams(); - this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); - } - - updateConfig = (event: IpcMainEvent, config: CombinedConfig) => { - log.silly('config', {config}); - - this.darkMode = config.darkMode; - this.enableServerManagement = config.enableServerManagement; - this.updateDropdown(); - } - - updateActiveTeam = (event: IpcMainEvent, serverId: string) => { - log.silly('updateActiveTeam', {serverId}); - - this.activeTeam = serverId; - this.updateDropdown(); - } - - private reduceNotifications = (items: Map, modifier: (base?: T, value?: T) => T) => { - return [...items.keys()].reduce((map, key) => { - const view = ServerManager.getTab(key); - if (!view) { - return map; - } - map.set(view.server.id, modifier(map.get(view.server.id), items.get(key))); - return map; - }, new Map()); - } - - updateMentions = (expired: Map, mentions: Map, unreads: Map) => { - log.silly('updateMentions', {expired, mentions, unreads}); - - this.unreads = this.reduceNotifications(unreads, (base, value) => base || value || false); - this.mentions = this.reduceNotifications(mentions, (base, value) => (base ?? 0) + (value ?? 0)); - this.expired = this.reduceNotifications(expired, (base, value) => base || value || false); - this.updateDropdown(); } updateWindowBounds = () => { @@ -125,16 +85,16 @@ export default class TeamDropdownView { this.updateDropdown(); } - updateDropdown = () => { + private updateDropdown = () => { log.silly('updateDropdown'); - this.view.webContents.send( + this.view?.webContents.send( UPDATE_TEAMS_DROPDOWN, this.teams, - this.darkMode, + Config.darkMode, this.windowBounds, - this.activeTeam, - this.enableServerManagement, + ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined, + Config.enableServerManagement, this.hasGPOTeams, this.expired, this.mentions, @@ -142,12 +102,33 @@ export default class TeamDropdownView { ); } - handleOpen = () => { + private updateServers = () => { + this.setOrderedTeams(); + this.updateDropdown(); + } + + private updateMentions = (expired: Map, mentions: Map, unreads: Map) => { + log.silly('updateMentions', {expired, mentions, unreads}); + + this.unreads = this.reduceNotifications(this.unreads, unreads, (base, value) => base || value || false); + this.mentions = this.reduceNotifications(this.mentions, mentions, (base, value) => (base ?? 0) + (value ?? 0)); + this.expired = this.reduceNotifications(this.expired, expired, (base, value) => base || value || false); + this.updateDropdown(); + } + + /** + * Menu open/close/size handlers + */ + + private handleOpen = () => { log.debug('handleOpen'); if (!this.bounds) { return; } + if (!this.view) { + return; + } this.view.setBounds(this.bounds); MainWindow.get()?.setTopBrowserView(this.view); this.view.webContents.focus(); @@ -155,24 +136,28 @@ export default class TeamDropdownView { this.isOpen = true; } - handleClose = () => { + private handleClose = () => { log.debug('handleClose'); - this.view.setBounds(this.getBounds(0, 0)); + this.view?.setBounds(this.getBounds(0, 0)); WindowManager.sendToRenderer(CLOSE_TEAMS_DROPDOWN); this.isOpen = false; } - handleReceivedMenuSize = (event: IpcMainEvent, width: number, height: number) => { + private handleReceivedMenuSize = (event: IpcMainEvent, width: number, height: number) => { log.silly('handleReceivedMenuSize', {width, height}); this.bounds = this.getBounds(width, height); if (this.isOpen) { - this.view.setBounds(this.bounds); + this.view?.setBounds(this.bounds); } } - getBounds = (width: number, height: number) => { + /** + * Helpers + */ + + private getBounds = (width: number, height: number) => { return { x: (process.platform === 'darwin' ? THREE_DOT_MENU_WIDTH_MAC : THREE_DOT_MENU_WIDTH) - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, @@ -181,11 +166,22 @@ export default class TeamDropdownView { }; } - destroy = () => { - // workaround to eliminate zombie processes - // https://github.com/mattermost/desktop/pull/1519 - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore - this.view.webContents.destroy(); + private reduceNotifications = (inputMap: Map, items: Map, modifier: (base?: T, value?: T) => T) => { + return [...items.keys()].reduce((map, key) => { + const view = ServerManager.getTab(key); + if (!view) { + return map; + } + map.set(view.server.id, modifier(map.get(view.server.id), items.get(key))); + return map; + }, inputMap); + } + + private setOrderedTeams = () => { + this.teams = ServerManager.getOrderedServers().map((team) => team.toMattermostTeam()); + this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); } } + +const teamDropdownView = new TeamDropdownView(); +export default teamDropdownView; diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index 35b7db6c..b58c9f78 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -77,6 +77,7 @@ jest.mock('common/servers/serverManager', () => ({ hasServers: jest.fn(), getLastActiveServer: jest.fn(), getLastActiveTabForServer: jest.fn(), + updateLastActive: jest.fn(), lookupTabByURL: jest.fn(), getRemoteInfo: jest.fn(), on: jest.fn(), diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index e9d3f353..6c98d98c 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -13,7 +13,6 @@ import { SET_ACTIVE_VIEW, OPEN_TAB, BROWSER_HISTORY_PUSH, - UPDATE_LAST_ACTIVE, UPDATE_URL_VIEW_WIDTH, SERVERS_UPDATE, REACT_APP_INITIALIZED, @@ -122,12 +121,7 @@ export class ViewManager { } hidePrevious?.(); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.id, newView.tab.id); - ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.id, newView.tab.id); - if (newView.isReady()) { - ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.id); - } else { - this.getViewLogger(tabId).warn(`couldn't show ${tabId}, not ready`); - } + ServerManager.updateLastActive(newView.tab.id); } else { this.getViewLogger(tabId).warn(`Couldn't find a view with name: ${tabId}`); } diff --git a/src/main/windows/windowManager.test.js b/src/main/windows/windowManager.test.js index 7a53cf59..9b5310d3 100644 --- a/src/main/windows/windowManager.test.js +++ b/src/main/windows/windowManager.test.js @@ -13,6 +13,7 @@ import {getAdjustedWindowBoundaries} from 'main/utils'; import ViewManager from '../views/viewManager'; import LoadingScreen from '../views/loadingScreen'; +import TeamDropdownView from '../views/teamDropdownView'; import {WindowManager} from './windowManager'; import MainWindow from './mainWindow'; @@ -72,7 +73,9 @@ jest.mock('../views/loadingScreen', () => ({ isHidden: jest.fn(), setBounds: jest.fn(), })); -jest.mock('../views/teamDropdownView', () => jest.fn()); +jest.mock('../views/teamDropdownView', () => ({ + updateWindowBounds: jest.fn(), +})); jest.mock('../views/downloadsDropdownView', () => jest.fn()); jest.mock('../views/downloadsDropdownMenuView', () => jest.fn()); jest.mock('./settingsWindow', () => ({ @@ -199,7 +202,7 @@ describe('main/windows/windowManager', () => { it('should update loading screen and team dropdown bounds', () => { windowManager.handleResizeMainWindow(); expect(LoadingScreen.setBounds).toHaveBeenCalled(); - expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); + expect(TeamDropdownView.updateWindowBounds).toHaveBeenCalled(); }); it('should use getSize when the platform is linux', () => { @@ -237,9 +240,6 @@ describe('main/windows/windowManager', () => { getContentBounds: () => ({width: 1000, height: 900}), getSize: () => [1000, 900], }; - windowManager.teamDropdown = { - updateWindowBounds: jest.fn(), - }; beforeEach(() => { MainWindow.get.mockReturnValue(mainWindow); @@ -257,7 +257,7 @@ describe('main/windows/windowManager', () => { const event = {preventDefault: jest.fn()}; windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); expect(LoadingScreen.setBounds).toHaveBeenCalled(); - expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); + expect(TeamDropdownView.updateWindowBounds).toHaveBeenCalled(); }); it('should not resize if the app is already resizing', () => { diff --git a/src/main/windows/windowManager.ts b/src/main/windows/windowManager.ts index 4e203c9c..70cde426 100644 --- a/src/main/windows/windowManager.ts +++ b/src/main/windows/windowManager.ts @@ -44,7 +44,6 @@ import SettingsWindow from './settingsWindow'; const log = new Logger('WindowManager'); export class WindowManager { - private teamDropdown?: TeamDropdownView; private downloadsDropdown?: DownloadsDropdownView; private downloadsDropdownMenu?: DownloadsDropdownMenuView; @@ -99,11 +98,11 @@ export class WindowManager { mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen')); mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen')); - this.teamDropdown = new TeamDropdownView(); this.downloadsDropdown = new DownloadsDropdownView(); this.downloadsDropdownMenu = new DownloadsDropdownMenuView(); this.initializeViewManager(); + TeamDropdownView.init(); } // max retries allows the message to get to the renderer even if it is sent while the app is starting up. @@ -276,7 +275,7 @@ export class WindowManager { this.throttledWillResize(newBounds); LoadingScreen.setBounds(); - this.teamDropdown?.updateWindowBounds(); + TeamDropdownView.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds(); ipcMain.emit(RESIZE_MODAL, null, newBounds); @@ -288,7 +287,7 @@ export class WindowManager { const bounds = this.getBounds(); this.throttledWillResize(bounds); ipcMain.emit(RESIZE_MODAL, null, bounds); - this.teamDropdown?.updateWindowBounds(); + TeamDropdownView.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds(); this.isResizing = false; @@ -318,7 +317,7 @@ export class WindowManager { setTimeout(this.setCurrentViewBounds, 10, bounds); LoadingScreen.setBounds(); - this.teamDropdown?.updateWindowBounds(); + TeamDropdownView.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds(); ipcMain.emit(RESIZE_MODAL, null, bounds);