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 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';

View File

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

View File

@@ -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<string> {
return Promise.allSettled([
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 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}`);
}

View File

@@ -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', () => {

View File

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