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

@@ -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);

View File

@@ -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<string, boolean>;
mentions?: Map<string, number>;
expired?: Map<string, boolean>;
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<string, boolean>;
private mentions: Map<string, number>;
private expired: Map<string, boolean>;
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 = <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 = () => {
@@ -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<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');
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 = <T>(inputMap: Map<string, 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;
}, 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(),
getLastActiveServer: jest.fn(),
getLastActiveTabForServer: jest.fn(),
updateLastActive: jest.fn(),
lookupTabByURL: jest.fn(),
getRemoteInfo: jest.fn(),
on: jest.fn(),

View File

@@ -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}`);
}