[MM-51964] Clean up MattermostView, remove tuple in preparation for id (#2668)

undefined
This commit is contained in:
Devin Binnie 2023-04-06 11:17:33 -04:00 committed by GitHub
parent 53fb8c8fd3
commit 88eb2e2c70
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 470 additions and 541 deletions

13
package-lock.json generated
View file

@ -21,7 +21,6 @@
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/register": "7.17.7",
"@bloomberg/record-tuple-polyfill": "^0.0.4",
"@electron/fuses": "1.6.0",
"@electron/universal": "1.3.1",
"@mattermost/compass-icons": "0.1.32",
@ -2108,12 +2107,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"node_modules/@bloomberg/record-tuple-polyfill": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@bloomberg/record-tuple-polyfill/-/record-tuple-polyfill-0.0.4.tgz",
"integrity": "sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==",
"dev": true
},
"node_modules/@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",
@ -34773,12 +34766,6 @@
"integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==",
"dev": true
},
"@bloomberg/record-tuple-polyfill": {
"version": "0.0.4",
"resolved": "https://registry.npmjs.org/@bloomberg/record-tuple-polyfill/-/record-tuple-polyfill-0.0.4.tgz",
"integrity": "sha512-h0OYmPR3A5Dfbetra/GzxBAzQk8sH7LhRkRUTdagX6nrtlUgJGYCTv4bBK33jsTQw9HDd8PE2x1Ma+iRKEDUsw==",
"dev": true
},
"@develar/schema-utils": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@develar/schema-utils/-/schema-utils-2.6.5.tgz",

View file

@ -139,7 +139,6 @@
"@babel/preset-env": "7.16.11",
"@babel/preset-react": "7.16.7",
"@babel/register": "7.17.7",
"@bloomberg/record-tuple-polyfill": "^0.0.4",
"@electron/fuses": "1.6.0",
"@electron/universal": "1.3.1",
"@mattermost/compass-icons": "0.1.32",

View file

@ -2,11 +2,10 @@
// See LICENSE.txt for license information.
import {v4 as uuid} from 'uuid';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getTabViewName, TabType, TabView, TabTuple} from './TabView';
import {getTabViewName, TabType, TabView} from './TabView';
export default abstract class BaseTabView implements TabView {
id: string;
@ -21,9 +20,6 @@ export default abstract class BaseTabView implements TabView {
get name(): string {
return getTabViewName(this.server.name, this.type);
}
get urlTypeTuple(): TabTuple {
return tuple(this.server.url.href, this.type) as TabTuple;
}
get url(): URL {
throw new Error('Not implemented');
}

View file

@ -9,7 +9,6 @@ export const TAB_MESSAGING = 'TAB_MESSAGING';
export const TAB_FOCALBOARD = 'TAB_FOCALBOARD';
export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS';
export type TabType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS;
export type TabTuple = [string, TabType];
export interface TabView {
id: string;
@ -20,7 +19,6 @@ export interface TabView {
get type(): TabType;
get url(): URL;
get shouldNotify(): boolean;
get urlTypeTuple(): TabTuple;
}
export function getDefaultTeamWithTabsFromTeam(team: FullTeam) {

View file

@ -9,6 +9,7 @@ import MessagingTabView from 'common/tabs/MessagingTabView';
import MainWindow from '../windows/mainWindow';
import * as WindowManager from '../windows/windowManager';
import ContextMenu from '../contextMenu';
import * as appState from '../appState';
import Utils from '../utils';
@ -24,6 +25,12 @@ jest.mock('electron', () => ({
on: jest.fn(),
getTitle: () => 'title',
getURL: () => 'http://server-1.com',
clearHistory: jest.fn(),
send: jest.fn(),
canGoBack: jest.fn(),
canGoForward: jest.fn(),
goToOffset: jest.fn(),
canGoToOffset: jest.fn(),
},
})),
ipcMain: {
@ -42,6 +49,7 @@ jest.mock('../appState', () => ({
updateMentions: jest.fn(),
}));
jest.mock('./webContentEvents', () => ({
addWebContentsEventListeners: jest.fn(),
removeWebContentsListeners: jest.fn(),
}));
jest.mock('../contextMenu', () => jest.fn());
@ -53,7 +61,7 @@ jest.mock('../utils', () => ({
}));
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
const tabView = new MessagingTabView(server);
const tabView = new MessagingTabView(server, true);
describe('main/views/MattermostView', () => {
describe('load', () => {
@ -179,6 +187,66 @@ describe('main/views/MattermostView', () => {
});
});
describe('goToOffset', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.reload = jest.fn();
afterEach(() => {
MainWindow.get.mockReturnValue(window);
jest.clearAllMocks();
});
it('should only go to offset if it can', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(false);
mattermostView.goToOffset(1);
expect(mattermostView.view.webContents.goToOffset).not.toBeCalled();
mattermostView.view.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.goToOffset(1);
expect(mattermostView.view.webContents.goToOffset).toBeCalled();
});
it('should call reload if an error occurs', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.view.webContents.goToOffset.mockImplementation(() => {
throw new Error('hi');
});
mattermostView.goToOffset(1);
expect(mattermostView.reload).toBeCalled();
});
});
describe('onLogin', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.getURL = jest.fn();
mattermostView.reload = jest.fn();
afterEach(() => {
MainWindow.get.mockReturnValue(window);
jest.clearAllMocks();
});
it('should reload view when URL is not on subpath of original server URL', () => {
mattermostView.view.webContents.getURL.mockReturnValue('http://server-2.com/subpath');
mattermostView.onLogin(true);
expect(mattermostView.reload).toHaveBeenCalled();
});
it('should not reload if URLs are matching', () => {
mattermostView.view.webContents.getURL.mockReturnValue('http://server-1.com');
mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled();
});
it('should not reload if URL is subpath of server URL', () => {
mattermostView.view.webContents.getURL.mockReturnValue('http://server-1.com/subpath');
mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled();
});
});
describe('loadSuccess', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
@ -208,7 +276,7 @@ describe('main/views/MattermostView', () => {
});
describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn()};
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
@ -226,59 +294,99 @@ describe('main/views/MattermostView', () => {
it('should add browser view to window and set bounds when request is true and view not currently visible', () => {
mattermostView.isVisible = false;
mattermostView.show(true);
mattermostView.show();
expect(window.addBrowserView).toBeCalledWith(mattermostView.view);
expect(mattermostView.setBounds).toBeCalled();
expect(mattermostView.isVisible).toBe(true);
});
it('should remove browser view when request is false', () => {
mattermostView.isVisible = true;
mattermostView.show(false);
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
expect(mattermostView.isVisible).toBe(false);
});
it('should do nothing when not toggling', () => {
mattermostView.isVisible = true;
mattermostView.show(true);
mattermostView.show();
expect(window.addBrowserView).not.toBeCalled();
expect(window.removeBrowserView).not.toBeCalled();
mattermostView.isVisible = false;
mattermostView.show(false);
expect(window.addBrowserView).not.toBeCalled();
expect(window.removeBrowserView).not.toBeCalled();
});
it('should focus view if view is ready', () => {
mattermostView.status = 1;
mattermostView.isVisible = false;
mattermostView.show(true);
mattermostView.show();
expect(mattermostView.focus).toBeCalled();
});
});
describe('hide', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
});
it('should remove browser view', () => {
mattermostView.isVisible = true;
mattermostView.hide();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
expect(mattermostView.isVisible).toBe(false);
});
it('should do nothing when not toggling', () => {
mattermostView.isVisible = false;
mattermostView.hide();
expect(window.removeBrowserView).not.toBeCalled();
});
});
describe('updateHistoryButton', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
});
it('should erase history and set isAtRoot when navigating to root URL', () => {
mattermostView.atRoot = false;
mattermostView.updateHistoryButton();
expect(mattermostView.view.webContents.clearHistory).toHaveBeenCalled();
expect(mattermostView.isAtRoot).toBe(true);
});
});
describe('destroy', () => {
const window = {removeBrowserView: jest.fn(), on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const contextMenu = {
dispose: jest.fn(),
};
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.destroy = jest.fn();
ContextMenu.mockReturnValue(contextMenu);
});
it('should remove browser view from window', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
});
it('should clear mentions', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.name, 0, false);
});
it('should destroy context menu', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(contextMenu.dispose).toBeCalled();
});
it('should clear outstanding timeouts', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
const spy = jest.spyOn(global, 'clearTimeout');
mattermostView.retryLoad = 999;
mattermostView.removeLoading = 1000;

View file

@ -6,7 +6,6 @@ import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
import {EventEmitter} from 'events';
import Util from 'common/utils/util';
import {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
import urlUtils from 'common/utils/url';
import {
@ -20,21 +19,21 @@ import {
LOADSCREEN_END,
BROWSER_HISTORY_BUTTON,
} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import {TabView, TabTuple} from 'common/tabs/TabView';
import {Logger} from 'common/log';
import {TabView} from 'common/tabs/TabView';
import {MattermostServer} from 'common/servers/MattermostServer';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils';
import WindowManager from '../windows/windowManager';
import * as appState from '../appState';
import WebContentsEventManager from './webContentEvents';
export enum Status {
enum Status {
LOADING,
READY,
WAITING_MM,
@ -42,27 +41,23 @@ export enum Status {
}
const MENTIONS_GROUP = 2;
const log = new Logger('MattermostView');
const titleParser = /(\((\d+)\) )?(\* )?/g;
export class MattermostView extends EventEmitter {
tab: TabView;
view: BrowserView;
isVisible: boolean;
isLoggedIn: boolean;
isAtRoot: boolean;
options: BrowserViewConstructorOptions;
serverInfo: ServerInfo;
isVisible: boolean;
removeLoading?: number;
currentFavicon?: string;
hasBeenShown: boolean;
contextMenu: ContextMenu;
status?: Status;
retryLoad?: NodeJS.Timeout;
maxRetries: number;
private log: Logger;
private view: BrowserView;
private loggedIn: boolean;
private atRoot: boolean;
private options: BrowserViewConstructorOptions;
private removeLoading?: number;
private contextMenu: ContextMenu;
private status?: Status;
private retryLoad?: NodeJS.Timeout;
private maxRetries: number;
private altPressStatus: boolean;
constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) {
@ -81,38 +76,24 @@ export class MattermostView extends EventEmitter {
...options.webPreferences,
};
this.isVisible = false;
this.isLoggedIn = false;
this.isAtRoot = true;
this.loggedIn = false;
this.atRoot = true;
this.view = new BrowserView(this.options);
this.resetLoadingStatus();
log.verbose(`BrowserView created for server ${this.tab.name}`);
this.hasBeenShown = false;
this.log = new Logger(this.name, 'MattermostView');
this.log.verbose('View created');
this.view.webContents.on('did-finish-load', this.handleDidFinishLoad);
this.view.webContents.on('page-title-updated', this.handleTitleUpdate);
this.view.webContents.on('page-favicon-updated', this.handleFaviconUpdate);
this.view.webContents.on('update-target-url', this.handleUpdateTarget);
this.view.webContents.on('did-navigate', this.handleDidNavigate);
if (process.platform !== 'darwin') {
this.view.webContents.on('before-input-event', this.handleInputEvents);
}
this.view.webContents.on('did-finish-load', () => {
log.debug('did-finish-load', this.tab.name);
// wait for screen to truly finish loading before sending the message down
const timeout = setInterval(() => {
if (!this.view.webContents) {
return;
}
if (!this.view.webContents.isLoading()) {
try {
this.view.webContents.send(SET_VIEW_OPTIONS, this.tab.name, this.tab.shouldNotify);
clearTimeout(timeout);
} catch (e) {
log.error('failed to send view options to view', this.tab.name);
}
}
}, 100);
});
WebContentsEventManager.addWebContentsEventListeners(this.view.webContents);
this.contextMenu = new ContextMenu({}, this.view);
this.maxRetries = MAX_SERVER_RETRIES;
@ -124,28 +105,79 @@ export class MattermostView extends EventEmitter {
});
}
// use the same name as the server
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
get name() {
return this.tab.name;
}
get urlTypeTuple(): TabTuple {
return this.tab.urlTypeTuple;
get isAtRoot() {
return this.atRoot;
}
get isLoggedIn() {
return this.loggedIn;
}
get currentURL() {
return this.view.webContents.getURL();
}
get webContentsId() {
return this.view.webContents.id;
}
updateServerInfo = (srv: MattermostServer) => {
let reload;
if (srv.url.toString() !== this.tab.server.url.toString()) {
reload = () => this.reload();
}
this.tab.server = srv;
this.serverInfo = new ServerInfo(srv);
this.view.webContents.send(SET_VIEW_OPTIONS, this.tab.name, this.tab.shouldNotify);
reload?.();
}
resetLoadingStatus = () => {
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything
delete this.retryLoad;
this.status = Status.LOADING;
this.maxRetries = MAX_SERVER_RETRIES;
onLogin = (loggedIn: boolean) => {
if (this.isLoggedIn === loggedIn) {
return;
}
this.loggedIn = loggedIn;
// If we're logging in from a different tab, force a reload
if (loggedIn &&
this.currentURL !== this.tab.url.toString() &&
!this.currentURL.startsWith(this.tab.url.toString())
) {
this.reload();
}
}
goToOffset = (offset: number) => {
if (this.view.webContents.canGoToOffset(offset)) {
try {
this.view.webContents.goToOffset(offset);
this.updateHistoryButton();
} catch (error) {
this.log.error(error);
this.reload();
}
}
}
updateHistoryButton = () => {
if (urlUtils.parseURL(this.currentURL)?.toString() === this.tab.url.toString()) {
this.view.webContents.clearHistory();
this.atRoot = true;
} else {
this.atRoot = false;
}
this.view.webContents.send(BROWSER_HISTORY_BUTTON, this.view.webContents.canGoBack(), this.view.webContents.canGoForward());
}
updateTabView = (tab: TabView) => {
let reload;
if (tab.url.toString() !== this.tab.url.toString()) {
reload = () => this.reload();
}
this.tab = tab;
this.view.webContents.send(SET_VIEW_OPTIONS, this.name, this.tab.shouldNotify);
reload?.();
}
load = (someURL?: URL | string) => {
@ -159,19 +191,19 @@ export class MattermostView extends EventEmitter {
if (parsedURL) {
loadURL = parsedURL.toString();
} else {
log.error('Cannot parse provided url, using current server url', someURL);
this.log.error('Cannot parse provided url, using current server url', someURL);
loadURL = this.tab.url.toString();
}
} else {
loadURL = this.tab.url.toString();
}
log.verbose(`[${Util.shorten(this.tab.name)}] Loading ${loadURL}`);
this.log.verbose(`Loading ${loadURL}`);
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) {
WindowManager.sendToRenderer(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.tab.name)}] Invalid certificate, stop retrying until the user decides what to do: ${err}.`);
WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
this.log.info('Invalid certificate, stop retrying until the user decides what to do.', err);
this.status = Status.ERROR;
return;
}
@ -183,85 +215,28 @@ export class MattermostView extends EventEmitter {
});
}
retry = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err);
} else {
WindowManager.sendToRenderer(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.tab.name)}] Couldn't establish a connection with ${loadURL}: ${err}. Will continue to retry in the background.`);
this.status = Status.ERROR;
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
}
});
};
}
retryInBackground = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch(() => {
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
});
};
}
loadRetry = (loadURL: string, err: Error) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
WindowManager.sendToRenderer(LOAD_RETRY, this.tab.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.tab.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
}
loadSuccess = (loadURL: string) => {
return () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
log.verbose(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle());
this.findUnreadState(null);
}
this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.tab.name, loadURL);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
};
}
show = (requestedVisibility?: boolean) => {
show = () => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
this.hasBeenShown = true;
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
if (request && !this.isVisible) {
mainWindow.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL())));
if (this.status === Status.READY) {
this.focus();
}
} else if (!request && this.isVisible) {
mainWindow.removeBrowserView(this.view);
if (this.isVisible) {
return;
}
this.isVisible = true;
mainWindow.addBrowserView(this.view);
mainWindow.setTopBrowserView(this.view);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
if (this.status === Status.READY) {
this.focus();
}
}
hide = () => {
if (this.isVisible) {
this.isVisible = false;
MainWindow.get()?.removeBrowserView(this.view);
}
this.isVisible = request;
}
reload = () => {
@ -269,41 +244,21 @@ export class MattermostView extends EventEmitter {
this.load();
}
hide = () => this.show(false);
getBounds = () => {
return this.view.getBounds();
}
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);
}
destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id);
appState.updateMentions(this.tab.name, 0, false);
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
appState.updateMentions(this.name, 0, false);
MainWindow.get()?.removeBrowserView(this.view);
// workaround to eliminate zombie processes
@ -319,13 +274,19 @@ export class MattermostView extends EventEmitter {
if (this.removeLoading) {
clearTimeout(this.removeLoading);
}
this.contextMenu.dispose();
}
focus = () => {
if (this.view.webContents) {
this.view.webContents.focus();
} else {
log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.');
/**
* Status hooks
*/
resetLoadingStatus = () => {
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything
delete this.retryLoad;
this.status = Status.LOADING;
this.maxRetries = MAX_SERVER_RETRIES;
}
}
@ -345,25 +306,41 @@ export class MattermostView extends EventEmitter {
this.status = Status.READY;
if (timedout) {
log.info(`${this.tab.name} timeout expired will show the browserview`);
this.emit(LOADSCREEN_END, this.tab.name);
this.log.verbose('timeout expired will show the browserview');
this.emit(LOADSCREEN_END, this.name);
}
clearTimeout(this.removeLoading);
delete this.removeLoading;
}
isInitialized = () => {
return this.status === Status.READY;
}
openDevTools = () => {
this.view.webContents.openDevTools({mode: 'detach'});
}
getWebContents = () => {
return this.view.webContents;
/**
* WebContents hooks
*/
sendToRenderer = (channel: string, ...args: any[]) => {
this.view.webContents.send(channel, ...args);
}
isDestroyed = () => {
return this.view.webContents.isDestroyed();
}
focus = () => {
if (this.view.webContents) {
this.view.webContents.focus();
} else {
this.log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.');
}
}
/**
* ALT key handling for the 3-dot menu (Windows/Linux)
*/
private registerAltKeyPressed = (input: Input) => {
const isAltPressed = input.key === 'Alt' && input.alt === true && input.control === false && input.shift === false && input.meta === false;
@ -380,8 +357,8 @@ export class MattermostView extends EventEmitter {
return input.type === 'keyUp' && this.altPressStatus === true;
};
handleInputEvents = (_: Event, input: Input) => {
log.silly('handleInputEvents', {tabName: this.tab.name, input});
private handleInputEvents = (_: Event, input: Input) => {
this.log.silly('handleInputEvents', input);
this.registerAltKeyPressed(input);
@ -390,61 +367,148 @@ export class MattermostView extends EventEmitter {
}
}
handleDidNavigate = (event: Event, url: string) => {
log.debug('handleDidNavigate', {tabName: this.tab.name, url});
/**
* Unreads/mentions handlers
*/
private updateMentionsFromTitle = (title: string) => {
const resultsIterator = title.matchAll(titleParser);
const results = resultsIterator.next(); // we are only interested in the first set
const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0;
appState.updateMentions(this.name, mentions);
}
// if favicon is null, it will affect appState, but won't be memoized
private findUnreadState = (favicon: string | null) => {
try {
this.view.webContents.send(IS_UNREAD, favicon, this.name);
} catch (err: any) {
this.log.error('There was an error trying to request the unread state', err);
}
}
private handleTitleUpdate = (e: Event, title: string) => {
this.log.debug('handleTitleUpdate', title);
this.updateMentionsFromTitle(title);
}
private handleFaviconUpdate = (e: Event, favicons: string[]) => {
this.log.silly('handleFaviconUpdate', favicons);
// if unread state is stored for that favicon, retrieve value.
// if not, get related info from preload and store it for future changes
this.findUnreadState(favicons[0]);
}
/**
* Loading/retry logic
*/
private retry = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err);
} else {
WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
this.log.info(`Couldn't establish a connection with ${loadURL}, will continue to retry in the background`, err);
this.status = Status.ERROR;
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
}
});
};
}
private retryInBackground = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch(() => {
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
});
};
}
private loadRetry = (loadURL: string, err: Error) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
WindowManager.sendToRenderer(LOAD_RETRY, this.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
this.log.info(`failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
}
private loadSuccess = (loadURL: string) => {
return () => {
this.log.verbose(`finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.name);
this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle());
this.findUnreadState(null);
}
this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.name, loadURL);
const mainWindow = MainWindow.get();
if (mainWindow) {
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
}
};
}
/**
* WebContents event handlers
*/
private handleDidFinishLoad = () => {
this.log.debug('did-finish-load', this.name);
// wait for screen to truly finish loading before sending the message down
const timeout = setInterval(() => {
if (!this.view.webContents) {
return;
}
if (!this.view.webContents.isLoading()) {
try {
this.view.webContents.send(SET_VIEW_OPTIONS, this.name, this.tab.shouldNotify);
clearTimeout(timeout);
} catch (e) {
this.log.error('failed to send view options to view');
}
}
}, 100);
}
private handleDidNavigate = (event: Event, url: string) => {
this.log.debug('handleDidNavigate', url);
if (shouldHaveBackBar(this.tab.url || '', url)) {
this.setBounds(getWindowBoundaries(MainWindow.get()!, true));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
log.info('show back button');
this.log.debug('show back button');
} else {
this.setBounds(getWindowBoundaries(MainWindow.get()!));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
log.info('hide back button');
this.log.debug('hide back button');
}
}
handleUpdateTarget = (e: Event, url: string) => {
log.silly('handleUpdateTarget', {tabName: this.tab.name, url});
private handleUpdateTarget = (e: Event, url: string) => {
this.log.silly('handleUpdateTarget', url);
if (url && !urlUtils.isInternalURL(urlUtils.parseURL(url), this.tab.server.url)) {
this.emit(UPDATE_TARGET_URL, url);
} else {
this.emit(UPDATE_TARGET_URL);
}
}
titleParser = /(\((\d+)\) )?(\* )?/g
handleTitleUpdate = (e: Event, title: string) => {
log.debug('handleTitleUpdate', {tabName: this.tab.name, title});
this.updateMentionsFromTitle(title);
}
updateMentionsFromTitle = (title: string) => {
const resultsIterator = title.matchAll(this.titleParser);
const results = resultsIterator.next(); // we are only interested in the first set
const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0;
appState.updateMentions(this.tab.name, mentions);
}
handleFaviconUpdate = (e: Event, favicons: string[]) => {
log.silly('handleFaviconUpdate', {tabName: this.tab.name, favicons});
// if unread state is stored for that favicon, retrieve value.
// if not, get related info from preload and store it for future changes
this.currentFavicon = favicons[0];
this.findUnreadState(favicons[0]);
}
// if favicon is null, it will affect appState, but won't be memoized
findUnreadState = (favicon: string | null) => {
try {
this.view.webContents.send(IS_UNREAD, favicon, this.tab.name);
} catch (err: any) {
log.error(`There was an error trying to request the unread state: ${err}`);
log.error(err.stack);
}
}
}

View file

@ -5,7 +5,6 @@
'use strict';
import {dialog, ipcMain} from 'electron';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication';
import Config from 'common/config';
@ -127,65 +126,6 @@ describe('main/views/viewManager', () => {
});
});
describe('handleAppLoggedIn', () => {
const viewManager = new ViewManager({});
afterEach(() => {
jest.resetAllMocks();
});
it('should reload view when URL is not on subpath of original server URL', () => {
const view = {
load: jest.fn(),
view: {
webContents: {
getURL: () => 'http://server-2.com/subpath',
},
},
tab: {
url: new URL('http://server-1.com/'),
},
};
viewManager.views.set('view1', view);
viewManager.handleAppLoggedIn({}, 'view1');
expect(view.load).toHaveBeenCalledWith(new URL('http://server-1.com/'));
});
it('should not reload if URLs are matching', () => {
const view = {
load: jest.fn(),
view: {
webContents: {
getURL: () => 'http://server-1.com/',
},
},
tab: {
url: new URL('http://server-1.com/'),
},
};
viewManager.views.set('view1', view);
viewManager.handleAppLoggedIn({}, 'view1');
expect(view.load).not.toHaveBeenCalled();
});
it('should not reload if URL is subpath of server URL', () => {
const view = {
load: jest.fn(),
view: {
webContents: {
getURL: () => 'http://server-1.com/subpath',
},
},
tab: {
url: new URL('http://server-1.com/'),
},
};
viewManager.views.set('view1', view);
viewManager.handleAppLoggedIn({}, 'view1');
expect(view.load).not.toHaveBeenCalled();
});
});
describe('reloadConfiguration', () => {
const viewManager = new ViewManager();
@ -203,7 +143,6 @@ describe('main/views/viewManager', () => {
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
name: `${srv.name}-${tabName}`,
urlTypeTuple: tuple(`http://${srv.name}.com/`, tabName),
url: new URL(`http://${srv.name}.com`),
}));
MattermostServer.mockImplementation((server) => ({
@ -219,10 +158,10 @@ describe('main/views/viewManager', () => {
once: onceFn,
destroy: destroyFn,
name: tab.name,
urlTypeTuple: tab.urlTypeTuple,
updateServerInfo: jest.fn(),
tab,
}));
getTabViewName.mockImplementation((a, b) => `${a}-${b}`);
});
afterEach(() => {
@ -249,7 +188,6 @@ 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);
@ -303,7 +241,7 @@ describe('main/views/viewManager', () => {
name: 'tab1',
isOpen: true,
},
'http://server1.com/',
'http://server1.com',
);
makeSpy.mockRestore();
});
@ -318,7 +256,6 @@ describe('main/views/viewManager', () => {
name: 'server1-tab1',
url: new URL('http://server1.com'),
},
urlTypeTuple: tuple('http://server1.com/', 'tab1'),
destroy: jest.fn(),
updateServerInfo: jest.fn(),
};
@ -348,7 +285,6 @@ describe('main/views/viewManager', () => {
name: 'server1-tab1',
url: new URL('http://server1.com'),
},
urlTypeTuple: ['http://server.com/', 'tab1'],
destroy: jest.fn(),
updateServerInfo: jest.fn(),
};
@ -772,12 +708,8 @@ describe('main/views/viewManager', () => {
resetLoadingStatus: jest.fn(),
load: jest.fn(),
once: jest.fn(),
isInitialized: jest.fn(),
view: {
webContents: {
send: jest.fn(),
},
},
isReady: jest.fn(),
sendToRenderer: jest.fn(),
serverInfo: {
remoteInfo: {
serverVersion: '1.0.0',
@ -819,10 +751,10 @@ describe('main/views/viewManager', () => {
},
},
};
view.isInitialized.mockImplementation(() => true);
view.isReady.mockImplementation(() => true);
viewManager.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(view.view.webContents.send).toHaveBeenCalledWith(BROWSER_HISTORY_PUSH, '/deep/link?thing=yes');
expect(view.sendToRenderer).toHaveBeenCalledWith(BROWSER_HISTORY_PUSH, '/deep/link?thing=yes');
});
it('should throw error if view is missing', () => {

View file

@ -3,8 +3,6 @@
import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import {BrowserViewConstructorOptions} from 'electron/main';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {Tab, TeamWithTabs} from 'types/config';
import {SECOND, TAB_BAR_HEIGHT} from 'common/utils/constants';
@ -26,13 +24,14 @@ import {
APP_LOGGED_OUT,
UNREAD_RESULT,
GET_VIEW_NAME,
HISTORY,
} from 'common/communication';
import Config from 'common/config';
import {Logger} from 'common/log';
import urlUtils, {equalUrlsIgnoringSubpath} from 'common/utils/url';
import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer';
import {getTabViewName, TabTuple, TabType, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import {getTabViewName, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import MessagingTabView from 'common/tabs/MessagingTabView';
import FocalboardTabView from 'common/tabs/FocalboardTabView';
import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
@ -46,7 +45,6 @@ import {getLocalURLString, getLocalPreload} from '../utils';
import {MattermostView} from './MattermostView';
import modalManager from './modalManager';
import WebContentsEventManager from './webContentEvents';
import LoadingScreen from './loadingScreen';
const log = new Logger('ViewManager');
@ -69,6 +67,7 @@ export class ViewManager {
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(HISTORY, this.handleHistory);
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
ipcMain.on(BROWSER_HISTORY_BUTTON, this.handleBrowserHistoryButton);
@ -97,7 +96,7 @@ export class ViewManager {
}
getViewByWebContentsId = (webContentsId: number) => {
return [...this.views.values()].find((view) => view.view.webContents.id === webContentsId);
return [...this.views.values()].find((view) => view.webContentsId === webContentsId);
}
showByName = (name: string) => {
@ -157,8 +156,8 @@ export class ViewManager {
sendToAllViews = (channel: string, ...args: unknown[]) => {
this.views.forEach((view) => {
if (!view.view.webContents.isDestroyed()) {
view.view.webContents.send(channel, ...args);
if (!view.isDestroyed()) {
view.sendToRenderer(channel, ...args);
}
});
}
@ -187,9 +186,9 @@ export class ViewManager {
return;
}
if (view.isInitialized() && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) {
if (view.isReady() && 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);
view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(view.name);
} else {
// attempting to change parsedURL protocol results in it not being modified.
@ -298,12 +297,6 @@ export class ViewManager {
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);
}
private finishLoading = (server: string) => {
@ -352,7 +345,7 @@ export class ViewManager {
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 boundaries = this.views.get(this.currentView || '')?.getBounds() ?? mainWindow.getBounds();
const hideView = () => {
delete this.urlViewCancel;
@ -407,15 +400,13 @@ export class ViewManager {
reloadConfiguration = () => {
log.debug('reloadConfiguration');
const focusedTuple: TabTuple | undefined = this.views.get(this.currentView as string)?.urlTypeTuple;
const current: Map<TabTuple, MattermostView> = new Map();
const current: Map<string, MattermostView> = new Map();
for (const view of this.views.values()) {
current.set(view.urlTypeTuple, view);
current.set(view.name, view);
}
const views: Map<TabTuple, MattermostView> = new Map();
const closed: Map<TabTuple, {srv: MattermostServer; tab: Tab; name: string}> = new Map();
const views: Map<string, MattermostView> = new Map();
const closed: Map<string, {srv: MattermostServer; tab: Tab; name: string}> = new Map();
const sortedTabs = this.getServers().flatMap((x) => [...x.tabs].
sort((a, b) => a.order - b.order).
@ -424,16 +415,16 @@ export class ViewManager {
for (const [team, tab] of sortedTabs) {
const srv = new MattermostServer(team);
const info = new ServerInfo(srv);
const tabTuple = tuple(new URL(team.url).href, tab.name as TabType);
const recycle = current.get(tabTuple);
const tabName = getTabViewName(team.name, tab.name);
const recycle = current.get(tabName);
if (!tab.isOpen) {
const view = this.getServerView(srv, tab.name);
closed.set(tabTuple, {srv, tab, name: view.name});
closed.set(tabName, {srv, tab, name: view.name});
} else if (recycle) {
recycle.updateServerInfo(srv);
views.set(tabTuple, recycle);
views.set(tabName, recycle);
} else {
views.set(tabTuple, this.makeView(srv, info, tab, tabTuple[0]));
views.set(tabName, this.makeView(srv, info, tab, team.url));
}
}
@ -456,7 +447,7 @@ export class ViewManager {
this.closedViews.set(x.name, {srv: x.srv, tab: x.tab});
}
if ((focusedTuple && closed.has(focusedTuple)) || (this.currentView && this.closedViews.has(this.currentView))) {
if ((this.currentView && closed.has(this.currentView)) || (this.currentView && this.closedViews.has(this.currentView))) {
if (this.getServers().length) {
this.currentView = undefined;
this.showInitial();
@ -466,8 +457,8 @@ export class ViewManager {
}
// show the focused tab (or initial)
if (focusedTuple && views.has(focusedTuple)) {
const view = views.get(focusedTuple);
if (this.currentView && views.has(this.currentView)) {
const view = views.get(this.currentView);
if (view) {
this.currentView = view.name;
this.showByName(view.name);
@ -478,25 +469,16 @@ export class ViewManager {
}
}
private handleAppLoggedIn = (event: IpcMainEvent, viewName: string) => {
log.debug('handleAppLoggedIn', viewName);
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);
}
}
private handleHistory = (event: IpcMainEvent, offset: number) => {
this.getCurrentView()?.goToOffset(offset);
}
private handleAppLoggedOut = (event: IpcMainEvent, viewName: string) => {
log.debug('handleAppLoggedOut', viewName);
private handleAppLoggedIn = (event: IpcMainEvent, viewId: string) => {
this.getView(viewId)?.onLogin(true);
}
const view = this.views.get(viewName);
if (view && view.isLoggedIn) {
view.isLoggedIn = false;
}
private handleAppLoggedOut = (event: IpcMainEvent, viewId: string) => {
this.getView(viewId)?.onLogin(false);
}
private handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => {
@ -520,7 +502,7 @@ export class ViewManager {
// 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);
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
if (redirectedView) {
this.handleBrowserHistoryButton(e, redirectedView.name);
}

View file

@ -17,7 +17,6 @@ import {protocols} from '../../../electron-builder.json';
import allowProtocolDialog from '../allowProtocolDialog';
import {composeUserAgent} from '../utils';
import {MattermostView} from './MattermostView';
import ViewManager from './viewManager';
type CustomLogin = {
@ -268,24 +267,6 @@ export class WebContentsEventManager {
}
};
addMattermostViewEventListeners = (mmview: MattermostView) => {
this.addWebContentsEventListeners(
mmview.view.webContents,
(contents: WebContents) => {
contents.on('page-title-updated', mmview.handleTitleUpdate);
contents.on('page-favicon-updated', mmview.handleFaviconUpdate);
contents.on('update-target-url', mmview.handleUpdateTarget);
contents.on('did-navigate', mmview.handleDidNavigate);
},
(contents: WebContents) => {
contents.removeListener('page-title-updated', mmview.handleTitleUpdate);
contents.removeListener('page-favicon-updated', mmview.handleFaviconUpdate);
contents.removeListener('update-target-url', mmview.handleUpdateTarget);
contents.removeListener('did-navigate', mmview.handleDidNavigate);
},
);
};
addWebContentsEventListeners = (
contents: WebContents,
addListeners?: (contents: WebContents) => void,

View file

@ -52,12 +52,8 @@ describe('main/windows/callsWidgetWindow', () => {
};
const mainView = {
view: {
webContents: {
send: jest.fn(),
id: 'mainViewID',
},
},
sendToRenderer: jest.fn(),
webContentsId: 'mainViewID',
serverInfo: {
server: {
name: 'test-server-name',
@ -434,12 +430,12 @@ describe('main/windows/callsWidgetWindow', () => {
widgetWindow.onJoinedCall({
sender: {id: 'badID'},
}, message);
expect(widgetWindow.mainView.view.webContents.send).not.toHaveBeenCalled();
expect(widgetWindow.mainView.sendToRenderer).not.toHaveBeenCalled();
widgetWindow.onJoinedCall({
sender: baseWindow.webContents,
}, 'widget', message);
expect(widgetWindow.mainView.view.webContents.send).toHaveBeenCalledWith(CALLS_JOINED_CALL, message);
expect(widgetWindow.mainView.sendToRenderer).toHaveBeenCalledWith(CALLS_JOINED_CALL, message);
});
it('menubar disabled on popout', () => {
@ -589,7 +585,7 @@ describe('main/windows/callsWidgetWindow', () => {
})).toEqual(false);
expect(widgetWindow.isAllowedEvent({
sender: widgetWindow.mainView.view.webContents,
sender: {id: 'mainViewID'},
})).toEqual(true);
expect(widgetWindow.isAllowedEvent({

View file

@ -190,7 +190,7 @@ export default class CallsWidgetWindow extends EventEmitter {
return;
}
this.mainView.view.webContents.send(CALLS_JOINED_CALL, message);
this.mainView.sendToRenderer(CALLS_JOINED_CALL, message);
}
private setBounds(bounds: Rectangle) {
@ -297,7 +297,7 @@ export default class CallsWidgetWindow extends EventEmitter {
// Only allow events coming from either the widget window or the
// original Mattermost view that initiated it.
return event.sender.id === this.getWebContentsId() ||
event.sender.id === this.getMainView().getWebContents().id;
event.sender.id === this.getMainView().webContentsId;
}
}

View file

@ -563,61 +563,6 @@ describe('main/windows/windowManager', () => {
});
});
describe('handleHistory', () => {
const windowManager = new WindowManager();
it('should only go to offset if it can', () => {
const view = {
view: {
webContents: {
goToOffset: jest.fn(),
canGoToOffset: () => false,
},
},
};
ViewManager.getCurrentView.mockReturnValue(view);
windowManager.handleHistory(null, 1);
expect(view.view.webContents.goToOffset).not.toBeCalled();
ViewManager.getCurrentView.mockReturnValue({
...view,
view: {
...view.view,
webContents: {
...view.view.webContents,
canGoToOffset: () => true,
},
},
});
windowManager.handleHistory(null, 1);
expect(view.view.webContents.goToOffset).toBeCalled();
});
it('should load base URL if an error occurs', () => {
const view = {
load: jest.fn(),
tab: {
url: 'http://server-1.com',
},
view: {
webContents: {
goToOffset: jest.fn(),
canGoToOffset: () => true,
},
},
};
view.view.webContents.goToOffset.mockImplementation(() => {
throw new Error('hi');
});
ViewManager.getCurrentView.mockReturnValue(view);
windowManager.handleHistory(null, 1);
expect(view.load).toBeCalledWith('http://server-1.com');
});
});
describe('selectTab', () => {
const windowManager = new WindowManager();
windowManager.switchTab = jest.fn();
@ -801,11 +746,7 @@ describe('main/windows/windowManager', () => {
const map = Config.teams.reduce((arr, item) => {
item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, {
view: {
webContents: {
send: jest.fn(),
},
},
sendToRenderer: jest.fn(),
}]);
});
return arr;
@ -838,7 +779,7 @@ describe('main/windows/windowManager', () => {
await windowManager.handleGetDesktopSources('server-1_tab-1', null);
expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('desktop-sources-result', [
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [
{
id: 'screen0',
},
@ -854,7 +795,7 @@ describe('main/windows/windowManager', () => {
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(ViewManager.getView('server-2_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', {
expect(ViewManager.getView('server-2_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
@ -877,10 +818,10 @@ describe('main/windows/windowManager', () => {
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', {
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledTimes(1);
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledTimes(1);
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
});
@ -908,7 +849,7 @@ describe('main/windows/windowManager', () => {
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(ViewManager.getView('server-1_tab-1').view.webContents.send).toHaveBeenCalledWith('calls-error', {
expect(ViewManager.getView('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
@ -932,11 +873,7 @@ describe('main/windows/windowManager', () => {
return {
getServerName: () => 'server-1',
getMainView: jest.fn().mockReturnValue({
view: {
webContents: {
send: jest.fn(),
},
},
sendToRenderer: jest.fn(),
}),
};
});
@ -1007,11 +944,7 @@ describe('main/windows/windowManager', () => {
return {
getServerName: () => 'server-2',
getMainView: jest.fn().mockReturnValue({
view: {
webContents: {
send: jest.fn(),
},
},
sendToRenderer: jest.fn(),
}),
getChannelURL: jest.fn(),
};
@ -1086,11 +1019,7 @@ describe('main/windows/windowManager', () => {
return {
getServerName: () => 'server-2',
getMainView: jest.fn().mockReturnValue({
view: {
webContents: {
send: jest.fn(),
},
},
sendToRenderer: jest.fn(),
}),
};
});
@ -1107,7 +1036,7 @@ describe('main/windows/windowManager', () => {
windowManager.handleCallsError('', {err: 'client-error'});
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
expect(mainWindow.focus).toHaveBeenCalled();
expect(windowManager.callsWidgetWindow.getMainView().view.webContents.send).toHaveBeenCalledWith('calls-error', {err: 'client-error'});
expect(windowManager.callsWidgetWindow.getMainView().sendToRenderer).toHaveBeenCalledWith('calls-error', {err: 'client-error'});
});
});
@ -1115,11 +1044,7 @@ describe('main/windows/windowManager', () => {
const windowManager = new WindowManager();
windowManager.switchServer = jest.fn();
const view1 = {
view: {
webContents: {
send: jest.fn(),
},
},
sendToRenderer: jest.fn(),
};
beforeEach(() => {
@ -1141,7 +1066,7 @@ describe('main/windows/windowManager', () => {
windowManager.callsWidgetWindow = new CallsWidgetWindow();
windowManager.handleCallsLinkClick('', {link: '/other/subpath'});
expect(windowManager.switchServer).toHaveBeenCalledWith('server-1');
expect(view1.view.webContents.send).toBeCalledWith('browser-history-push', '/other/subpath');
expect(view1.sendToRenderer).toBeCalledWith('browser-history-push', '/other/subpath');
});
});

View file

@ -15,7 +15,6 @@ import {
import {
MAXIMIZE_CHANGE,
HISTORY,
FOCUS_THREE_DOT_MENU,
GET_DARK_MODE,
UPDATE_SHORTCUT_MENU,
@ -74,7 +73,6 @@ export class WindowManager {
constructor() {
this.assetsDir = path.resolve(app.getAppPath(), 'assets');
ipcMain.on(HISTORY, this.handleHistory);
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
@ -133,7 +131,7 @@ export class WindowManager {
if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus();
this.callsWidgetWindow.getMainView().view.webContents.send(DESKTOP_SOURCES_MODAL_REQUEST);
this.callsWidgetWindow.getMainView().sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
}
}
@ -143,7 +141,7 @@ export class WindowManager {
if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus();
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, this.callsWidgetWindow.getChannelURL());
this.callsWidgetWindow.getMainView().sendToRenderer(BROWSER_HISTORY_PUSH, this.callsWidgetWindow.getChannelURL());
}
}
@ -153,7 +151,7 @@ export class WindowManager {
if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus();
this.callsWidgetWindow.getMainView().view.webContents.send(CALLS_ERROR, msg);
this.callsWidgetWindow.getMainView().sendToRenderer(CALLS_ERROR, msg);
}
}
@ -163,7 +161,7 @@ export class WindowManager {
if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus();
this.callsWidgetWindow.getMainView().view.webContents.send(BROWSER_HISTORY_PUSH, msg.link);
this.callsWidgetWindow.getMainView().sendToRenderer(BROWSER_HISTORY_PUSH, msg.link);
}
}
@ -314,7 +312,7 @@ export class WindowManager {
const currentView = ViewManager.getCurrentView();
if (currentView) {
const adjustedBounds = getAdjustedWindowBoundaries(bounds.width, bounds.height, shouldHaveBackBar(currentView.tab.url, currentView.view.webContents.getURL()));
const adjustedBounds = getAdjustedWindowBoundaries(bounds.width, bounds.height, shouldHaveBackBar(currentView.tab.url, currentView.currentURL));
this.setBoundsFunction(currentView, adjustedBounds);
}
}
@ -520,27 +518,6 @@ export class WindowManager {
}
}
sendToFind = () => {
const currentView = ViewManager.getCurrentView();
if (currentView) {
currentView.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
}
}
handleHistory = (event: IpcMainEvent, offset: number) => {
log.debug('handleHistory', offset);
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);
}
}
}
selectNextTab = () => {
this.selectTab((order) => order + 1);
}
@ -627,7 +604,7 @@ export class WindowManager {
if (!hasScreenPermissions || !sources.length) {
log.info('missing screen permissions');
view.view.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
this.callsWidgetWindow?.win.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
return;
}
@ -641,12 +618,12 @@ export class WindowManager {
});
if (message.length > 0) {
view.view.webContents.send(DESKTOP_SOURCES_RESULT, message);
view.sendToRenderer(DESKTOP_SOURCES_RESULT, message);
}
}).catch((err) => {
log.error('desktopCapturer.getSources failed', err);
view.view.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
view.sendToRenderer(CALLS_ERROR, screenPermissionsErrMsg);
this.callsWidgetWindow?.win.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
});
}

View file

@ -1,16 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
declare module '@bloomberg/record-tuple-polyfill' {
export function Tuple<A>(A): [A];
export function Tuple<A, B>(a: A, b: B): [A, B];
export function Tuple<A, B, C>(a: A, b: B, c: C): [A, B, C];
export function Tuple<A, B, C, D>(a: A, b: B, c: C, d: D): [A, B, C, D];
export function Tuple<A, B, C, D, E>(a: A, b: B, c: C, d: D, e: E): [A, B, C, D, E];
export function Tuple<A, B, C, D, E, F>(a: A, b: B, c: C, d: D, e: E, f: F): [A, B, C, D, E, F];
export function Tuple<A, B, C, D, E, F, G>(a: A, b: B, c: C, d: D, e: E, f: F, g: G): [A, B, C, D, E, F, G];
export function Tuple<A, B, C, D, E, F, G, H>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H): [A, B, C, D, E, F, G, H];
export function Tuple<A, B, C, D, E, F, G, H, I>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I): [A, B, C, D, E, F, G, H, I];
export function Tuple<A, B, C, D, E, F, G, H, I, J>(a: A, b: B, c: C, d: D, e: E, f: F, g: G, h: H, i: I, j: J): [A, B, C, D, E, F, G, H, I, J];
export function Record<T>(x: {[key: string]: T}): {[key: string]: T};
}