Migrate teamDropdownView to singleton (#2677)

This commit is contained in:
Devin Binnie
2023-04-14 12:14:11 -04:00
committed by GitHub
parent 1428ba694b
commit b71322a682
9 changed files with 107 additions and 121 deletions

View File

@@ -6,7 +6,6 @@ export const SWITCH_TAB = 'switch-tab';
export const CLOSE_TAB = 'close-tab'; export const CLOSE_TAB = 'close-tab';
export const OPEN_TAB = 'open-tab'; export const OPEN_TAB = 'open-tab';
export const SET_ACTIVE_VIEW = 'set-active-view'; export const SET_ACTIVE_VIEW = 'set-active-view';
export const UPDATE_LAST_ACTIVE = 'update-last-active';
export const FOCUS_BROWSERVIEW = 'focus-browserview'; export const FOCUS_BROWSERVIEW = 'focus-browserview';
export const HISTORY = 'history'; export const HISTORY = 'history';

View File

@@ -20,7 +20,6 @@ import {
SHOW_EDIT_SERVER_MODAL, SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL, SHOW_REMOVE_SERVER_MODAL,
UPDATE_SHORTCUT_MENU, UPDATE_SHORTCUT_MENU,
UPDATE_LAST_ACTIVE,
GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, GET_AVAILABLE_SPELL_CHECKER_LANGUAGES,
USER_ACTIVITY_UPDATE, USER_ACTIVITY_UPDATE,
START_UPGRADE, START_UPGRADE,
@@ -96,7 +95,6 @@ import {
handleSelectDownload, handleSelectDownload,
handleSwitchServer, handleSwitchServer,
handleSwitchTab, handleSwitchTab,
handleUpdateLastActive,
handlePingDomain, handlePingDomain,
handleGetOrderedServers, handleGetOrderedServers,
handleGetOrderedTabsForServer, handleGetOrderedTabsForServer,
@@ -264,7 +262,6 @@ function initializeInterCommunicationEventListeners() {
ipcMain.handle('get-app-version', handleAppVersion); ipcMain.handle('get-app-version', handleAppVersion);
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent);
ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView); ipcMain.on(FOCUS_BROWSERVIEW, ViewManager.focusCurrentView);
ipcMain.on(UPDATE_LAST_ACTIVE, handleUpdateLastActive);
if (process.platform !== 'darwin') { if (process.platform !== 'darwin') {
ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu); ipcMain.on(OPEN_APP_MENU, handleOpenAppMenu);

View File

@@ -279,12 +279,6 @@ export async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom:
return result.filePaths[0]; 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<string> { export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise<string> {
return Promise.allSettled([ return Promise.allSettled([
ping(new URL(`https://${url}`)), ping(new URL(`https://${url}`)),

View File

@@ -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 MainWindow from 'main/windows/mainWindow';
import TeamDropdownView from './teamDropdownView'; import {TeamDropdownView} from './teamDropdownView';
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
@@ -61,6 +61,12 @@ describe('main/views/teamDropdownView', () => {
it('should change the view bounds based on open/closed state', () => { it('should change the view bounds based on open/closed state', () => {
const teamDropdownView = new TeamDropdownView(); const teamDropdownView = new TeamDropdownView();
teamDropdownView.view = {
setBounds: jest.fn(),
webContents: {
focus: jest.fn(),
},
};
teamDropdownView.bounds = {width: 400, height: 300}; teamDropdownView.bounds = {width: 400, height: 300};
teamDropdownView.handleOpen(); teamDropdownView.handleOpen();
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds); expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);

View File

@@ -3,7 +3,7 @@
import {BrowserView, ipcMain, IpcMainEvent} from 'electron'; import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
import {CombinedConfig, MattermostTeam} from 'types/config'; import {MattermostTeam} from 'types/config';
import AppState from 'common/appState'; import AppState from 'common/appState';
import { import {
@@ -14,7 +14,6 @@ import {
UPDATE_APPSTATE, UPDATE_APPSTATE,
REQUEST_TEAMS_DROPDOWN_INFO, REQUEST_TEAMS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SET_ACTIVE_VIEW,
SERVERS_UPDATE, SERVERS_UPDATE,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
@@ -29,29 +28,41 @@ import MainWindow from '../windows/mainWindow';
const log = new Logger('TeamDropdownView'); const log = new Logger('TeamDropdownView');
export default class TeamDropdownView { export class TeamDropdownView {
view: BrowserView; private view?: BrowserView;
bounds?: Electron.Rectangle; private teams: MattermostTeam[];
teams: MattermostTeam[]; private hasGPOTeams: boolean;
activeTeam?: string; private isOpen: boolean;
darkMode: boolean; private bounds: Electron.Rectangle;
enableServerManagement?: boolean;
hasGPOTeams?: boolean; private unreads: Map<string, boolean>;
unreads?: Map<string, boolean>; private mentions: Map<string, number>;
mentions?: Map<string, number>; private expired: Map<string, boolean>;
expired?: Map<string, boolean>;
windowBounds?: Electron.Rectangle; private windowBounds?: Electron.Rectangle;
isOpen: boolean;
constructor() { constructor() {
this.teams = this.getOrderedTeams(); this.teams = [];
this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); this.hasGPOTeams = false;
this.darkMode = Config.darkMode;
this.enableServerManagement = Config.enableServerManagement;
this.isOpen = 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'); const preload = getLocalPreload('desktopAPI.js');
this.view = new BrowserView({webPreferences: { this.view = new BrowserView({webPreferences: {
preload, preload,
@@ -61,63 +72,12 @@ export default class TeamDropdownView {
// @ts-ignore // @ts-ignore
transparent: true, transparent: true,
}}); }});
this.view.webContents.loadURL(getLocalURLString('dropdown.html')); this.view.webContents.loadURL(getLocalURLString('dropdown.html'));
this.setOrderedTeams();
this.windowBounds = MainWindow.getBounds();
this.updateDropdown();
MainWindow.get()?.addBrowserView(this.view); 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 = <T>(items: Map<string, T>, 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<string, boolean>, mentions: Map<string, number>, unreads: Map<string, boolean>) => {
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 = () => { updateWindowBounds = () => {
@@ -125,16 +85,16 @@ export default class TeamDropdownView {
this.updateDropdown(); this.updateDropdown();
} }
updateDropdown = () => { private updateDropdown = () => {
log.silly('updateDropdown'); log.silly('updateDropdown');
this.view.webContents.send( this.view?.webContents.send(
UPDATE_TEAMS_DROPDOWN, UPDATE_TEAMS_DROPDOWN,
this.teams, this.teams,
this.darkMode, Config.darkMode,
this.windowBounds, this.windowBounds,
this.activeTeam, ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined,
this.enableServerManagement, Config.enableServerManagement,
this.hasGPOTeams, this.hasGPOTeams,
this.expired, this.expired,
this.mentions, this.mentions,
@@ -142,12 +102,33 @@ export default class TeamDropdownView {
); );
} }
handleOpen = () => { private updateServers = () => {
this.setOrderedTeams();
this.updateDropdown();
}
private updateMentions = (expired: Map<string, boolean>, mentions: Map<string, number>, unreads: Map<string, boolean>) => {
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'); log.debug('handleOpen');
if (!this.bounds) { if (!this.bounds) {
return; return;
} }
if (!this.view) {
return;
}
this.view.setBounds(this.bounds); this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view); MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus(); this.view.webContents.focus();
@@ -155,24 +136,28 @@ export default class TeamDropdownView {
this.isOpen = true; this.isOpen = true;
} }
handleClose = () => { private handleClose = () => {
log.debug('handleClose'); log.debug('handleClose');
this.view.setBounds(this.getBounds(0, 0)); this.view?.setBounds(this.getBounds(0, 0));
WindowManager.sendToRenderer(CLOSE_TEAMS_DROPDOWN); WindowManager.sendToRenderer(CLOSE_TEAMS_DROPDOWN);
this.isOpen = false; this.isOpen = false;
} }
handleReceivedMenuSize = (event: IpcMainEvent, width: number, height: number) => { private handleReceivedMenuSize = (event: IpcMainEvent, width: number, height: number) => {
log.silly('handleReceivedMenuSize', {width, height}); log.silly('handleReceivedMenuSize', {width, height});
this.bounds = this.getBounds(width, height); this.bounds = this.getBounds(width, height);
if (this.isOpen) { 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 { return {
x: (process.platform === 'darwin' ? THREE_DOT_MENU_WIDTH_MAC : THREE_DOT_MENU_WIDTH) - MENU_SHADOW_WIDTH, x: (process.platform === 'darwin' ? THREE_DOT_MENU_WIDTH_MAC : THREE_DOT_MENU_WIDTH) - MENU_SHADOW_WIDTH,
y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH,
@@ -181,11 +166,22 @@ export default class TeamDropdownView {
}; };
} }
destroy = () => { private reduceNotifications = <T>(inputMap: Map<string, T>, items: Map<string, T>, modifier: (base?: T, value?: T) => T) => {
// workaround to eliminate zombie processes return [...items.keys()].reduce((map, key) => {
// https://github.com/mattermost/desktop/pull/1519 const view = ServerManager.getTab(key);
// eslint-disable-next-line @typescript-eslint/ban-ts-comment if (!view) {
// @ts-ignore return map;
this.view.webContents.destroy(); }
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;

View File

@@ -77,6 +77,7 @@ jest.mock('common/servers/serverManager', () => ({
hasServers: jest.fn(), hasServers: jest.fn(),
getLastActiveServer: jest.fn(), getLastActiveServer: jest.fn(),
getLastActiveTabForServer: jest.fn(), getLastActiveTabForServer: jest.fn(),
updateLastActive: jest.fn(),
lookupTabByURL: jest.fn(), lookupTabByURL: jest.fn(),
getRemoteInfo: jest.fn(), getRemoteInfo: jest.fn(),
on: jest.fn(), on: jest.fn(),

View File

@@ -13,7 +13,6 @@ import {
SET_ACTIVE_VIEW, SET_ACTIVE_VIEW,
OPEN_TAB, OPEN_TAB,
BROWSER_HISTORY_PUSH, BROWSER_HISTORY_PUSH,
UPDATE_LAST_ACTIVE,
UPDATE_URL_VIEW_WIDTH, UPDATE_URL_VIEW_WIDTH,
SERVERS_UPDATE, SERVERS_UPDATE,
REACT_APP_INITIALIZED, REACT_APP_INITIALIZED,
@@ -122,12 +121,7 @@ export class ViewManager {
} }
hidePrevious?.(); hidePrevious?.();
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.id, newView.tab.id); 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); ServerManager.updateLastActive(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`);
}
} else { } else {
this.getViewLogger(tabId).warn(`Couldn't find a view with name: ${tabId}`); this.getViewLogger(tabId).warn(`Couldn't find a view with name: ${tabId}`);
} }

View File

@@ -13,6 +13,7 @@ import {getAdjustedWindowBoundaries} from 'main/utils';
import ViewManager from '../views/viewManager'; import ViewManager from '../views/viewManager';
import LoadingScreen from '../views/loadingScreen'; import LoadingScreen from '../views/loadingScreen';
import TeamDropdownView from '../views/teamDropdownView';
import {WindowManager} from './windowManager'; import {WindowManager} from './windowManager';
import MainWindow from './mainWindow'; import MainWindow from './mainWindow';
@@ -72,7 +73,9 @@ jest.mock('../views/loadingScreen', () => ({
isHidden: jest.fn(), isHidden: jest.fn(),
setBounds: 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/downloadsDropdownView', () => jest.fn());
jest.mock('../views/downloadsDropdownMenuView', () => jest.fn()); jest.mock('../views/downloadsDropdownMenuView', () => jest.fn());
jest.mock('./settingsWindow', () => ({ jest.mock('./settingsWindow', () => ({
@@ -199,7 +202,7 @@ describe('main/windows/windowManager', () => {
it('should update loading screen and team dropdown bounds', () => { it('should update loading screen and team dropdown bounds', () => {
windowManager.handleResizeMainWindow(); windowManager.handleResizeMainWindow();
expect(LoadingScreen.setBounds).toHaveBeenCalled(); expect(LoadingScreen.setBounds).toHaveBeenCalled();
expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); expect(TeamDropdownView.updateWindowBounds).toHaveBeenCalled();
}); });
it('should use getSize when the platform is linux', () => { it('should use getSize when the platform is linux', () => {
@@ -237,9 +240,6 @@ describe('main/windows/windowManager', () => {
getContentBounds: () => ({width: 1000, height: 900}), getContentBounds: () => ({width: 1000, height: 900}),
getSize: () => [1000, 900], getSize: () => [1000, 900],
}; };
windowManager.teamDropdown = {
updateWindowBounds: jest.fn(),
};
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(mainWindow); MainWindow.get.mockReturnValue(mainWindow);
@@ -257,7 +257,7 @@ describe('main/windows/windowManager', () => {
const event = {preventDefault: jest.fn()}; const event = {preventDefault: jest.fn()};
windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600}); windowManager.handleWillResizeMainWindow(event, {width: 800, height: 600});
expect(LoadingScreen.setBounds).toHaveBeenCalled(); expect(LoadingScreen.setBounds).toHaveBeenCalled();
expect(windowManager.teamDropdown.updateWindowBounds).toHaveBeenCalled(); expect(TeamDropdownView.updateWindowBounds).toHaveBeenCalled();
}); });
it('should not resize if the app is already resizing', () => { it('should not resize if the app is already resizing', () => {

View File

@@ -44,7 +44,6 @@ import SettingsWindow from './settingsWindow';
const log = new Logger('WindowManager'); const log = new Logger('WindowManager');
export class WindowManager { export class WindowManager {
private teamDropdown?: TeamDropdownView;
private downloadsDropdown?: DownloadsDropdownView; private downloadsDropdown?: DownloadsDropdownView;
private downloadsDropdownMenu?: DownloadsDropdownMenuView; private downloadsDropdownMenu?: DownloadsDropdownMenuView;
@@ -99,11 +98,11 @@ export class WindowManager {
mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen')); mainWindow.on('enter-full-screen', () => this.sendToRenderer('enter-full-screen'));
mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen')); mainWindow.on('leave-full-screen', () => this.sendToRenderer('leave-full-screen'));
this.teamDropdown = new TeamDropdownView();
this.downloadsDropdown = new DownloadsDropdownView(); this.downloadsDropdown = new DownloadsDropdownView();
this.downloadsDropdownMenu = new DownloadsDropdownMenuView(); this.downloadsDropdownMenu = new DownloadsDropdownMenuView();
this.initializeViewManager(); 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. // 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); this.throttledWillResize(newBounds);
LoadingScreen.setBounds(); LoadingScreen.setBounds();
this.teamDropdown?.updateWindowBounds(); TeamDropdownView.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
ipcMain.emit(RESIZE_MODAL, null, newBounds); ipcMain.emit(RESIZE_MODAL, null, newBounds);
@@ -288,7 +287,7 @@ export class WindowManager {
const bounds = this.getBounds(); const bounds = this.getBounds();
this.throttledWillResize(bounds); this.throttledWillResize(bounds);
ipcMain.emit(RESIZE_MODAL, null, bounds); ipcMain.emit(RESIZE_MODAL, null, bounds);
this.teamDropdown?.updateWindowBounds(); TeamDropdownView.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
this.isResizing = false; this.isResizing = false;
@@ -318,7 +317,7 @@ export class WindowManager {
setTimeout(this.setCurrentViewBounds, 10, bounds); setTimeout(this.setCurrentViewBounds, 10, bounds);
LoadingScreen.setBounds(); LoadingScreen.setBounds();
this.teamDropdown?.updateWindowBounds(); TeamDropdownView.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
ipcMain.emit(RESIZE_MODAL, null, bounds); ipcMain.emit(RESIZE_MODAL, null, bounds);