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

View File

@@ -2,11 +2,10 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import {Tuple as tuple} from '@bloomberg/record-tuple-polyfill';
import {MattermostServer} from 'common/servers/MattermostServer'; 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 { export default abstract class BaseTabView implements TabView {
id: string; id: string;
@@ -21,9 +20,6 @@ export default abstract class BaseTabView implements TabView {
get name(): string { get name(): string {
return getTabViewName(this.server.name, this.type); return getTabViewName(this.server.name, this.type);
} }
get urlTypeTuple(): TabTuple {
return tuple(this.server.url.href, this.type) as TabTuple;
}
get url(): URL { get url(): URL {
throw new Error('Not implemented'); 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_FOCALBOARD = 'TAB_FOCALBOARD';
export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS'; export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS';
export type TabType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS; export type TabType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS;
export type TabTuple = [string, TabType];
export interface TabView { export interface TabView {
id: string; id: string;
@@ -20,7 +19,6 @@ export interface TabView {
get type(): TabType; get type(): TabType;
get url(): URL; get url(): URL;
get shouldNotify(): boolean; get shouldNotify(): boolean;
get urlTypeTuple(): TabTuple;
} }
export function getDefaultTeamWithTabsFromTeam(team: FullTeam) { export function getDefaultTeamWithTabsFromTeam(team: FullTeam) {

View File

@@ -9,6 +9,7 @@ import MessagingTabView from 'common/tabs/MessagingTabView';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
import * as WindowManager from '../windows/windowManager'; import * as WindowManager from '../windows/windowManager';
import ContextMenu from '../contextMenu';
import * as appState from '../appState'; import * as appState from '../appState';
import Utils from '../utils'; import Utils from '../utils';
@@ -24,6 +25,12 @@ jest.mock('electron', () => ({
on: jest.fn(), on: jest.fn(),
getTitle: () => 'title', getTitle: () => 'title',
getURL: () => 'http://server-1.com', getURL: () => 'http://server-1.com',
clearHistory: jest.fn(),
send: jest.fn(),
canGoBack: jest.fn(),
canGoForward: jest.fn(),
goToOffset: jest.fn(),
canGoToOffset: jest.fn(),
}, },
})), })),
ipcMain: { ipcMain: {
@@ -42,6 +49,7 @@ jest.mock('../appState', () => ({
updateMentions: jest.fn(), updateMentions: jest.fn(),
})); }));
jest.mock('./webContentEvents', () => ({ jest.mock('./webContentEvents', () => ({
addWebContentsEventListeners: jest.fn(),
removeWebContentsListeners: jest.fn(), removeWebContentsListeners: jest.fn(),
})); }));
jest.mock('../contextMenu', () => 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 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('main/views/MattermostView', () => {
describe('load', () => { 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', () => { describe('loadSuccess', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostView(tabView, {}, {});
@@ -208,7 +276,7 @@ describe('main/views/MattermostView', () => {
}); });
describe('show', () => { 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, {}, {}); const mattermostView = new MattermostView(tabView, {}, {});
beforeEach(() => { 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', () => { it('should add browser view to window and set bounds when request is true and view not currently visible', () => {
mattermostView.isVisible = false; mattermostView.isVisible = false;
mattermostView.show(true); mattermostView.show();
expect(window.addBrowserView).toBeCalledWith(mattermostView.view); expect(window.addBrowserView).toBeCalledWith(mattermostView.view);
expect(mattermostView.setBounds).toBeCalled(); expect(mattermostView.setBounds).toBeCalled();
expect(mattermostView.isVisible).toBe(true); 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', () => { it('should do nothing when not toggling', () => {
mattermostView.isVisible = true; mattermostView.isVisible = true;
mattermostView.show(true); mattermostView.show();
expect(window.addBrowserView).not.toBeCalled(); 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', () => { it('should focus view if view is ready', () => {
mattermostView.status = 1; mattermostView.status = 1;
mattermostView.isVisible = false; mattermostView.isVisible = false;
mattermostView.show(true); mattermostView.show();
expect(mattermostView.focus).toBeCalled(); 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', () => { describe('destroy', () => {
const window = {removeBrowserView: jest.fn(), on: jest.fn()}; const window = {removeBrowserView: jest.fn(), on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const contextMenu = {
dispose: jest.fn(),
};
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.destroy = jest.fn(); ContextMenu.mockReturnValue(contextMenu);
}); });
it('should remove browser view from window', () => { it('should remove browser view from window', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy(); mattermostView.destroy();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view); expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
}); });
it('should clear mentions', () => { it('should clear mentions', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy(); mattermostView.destroy();
expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.name, 0, false); 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', () => { it('should clear outstanding timeouts', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
const spy = jest.spyOn(global, 'clearTimeout'); const spy = jest.spyOn(global, 'clearTimeout');
mattermostView.retryLoad = 999; mattermostView.retryLoad = 999;
mattermostView.removeLoading = 1000; mattermostView.removeLoading = 1000;

View File

@@ -6,7 +6,6 @@ import {BrowserViewConstructorOptions, Event, Input} from 'electron/main';
import {EventEmitter} from 'events'; 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 {RELOAD_INTERVAL, MAX_SERVER_RETRIES, SECOND, MAX_LOADING_SCREEN_SECONDS} from 'common/utils/constants';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import { import {
@@ -20,21 +19,21 @@ import {
LOADSCREEN_END, LOADSCREEN_END,
BROWSER_HISTORY_BUTTON, BROWSER_HISTORY_BUTTON,
} from 'common/communication'; } from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import {TabView, TabTuple} from 'common/tabs/TabView';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TabView} from 'common/tabs/TabView';
import {MattermostServer} from 'common/servers/MattermostServer';
import {ServerInfo} from 'main/server/serverInfo'; import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils'; import {getWindowBoundaries, getLocalPreload, composeUserAgent, shouldHaveBackBar} from '../utils';
import WindowManager from '../windows/windowManager';
import * as appState from '../appState'; import * as appState from '../appState';
import WebContentsEventManager from './webContentEvents'; import WebContentsEventManager from './webContentEvents';
export enum Status { enum Status {
LOADING, LOADING,
READY, READY,
WAITING_MM, WAITING_MM,
@@ -42,27 +41,23 @@ export enum Status {
} }
const MENTIONS_GROUP = 2; const MENTIONS_GROUP = 2;
const log = new Logger('MattermostView'); const titleParser = /(\((\d+)\) )?(\* )?/g;
export class MattermostView extends EventEmitter { export class MattermostView extends EventEmitter {
tab: TabView; tab: TabView;
view: BrowserView;
isVisible: boolean;
isLoggedIn: boolean;
isAtRoot: boolean;
options: BrowserViewConstructorOptions;
serverInfo: ServerInfo; serverInfo: ServerInfo;
isVisible: boolean;
removeLoading?: number; private log: Logger;
private view: BrowserView;
currentFavicon?: string; private loggedIn: boolean;
hasBeenShown: boolean; private atRoot: boolean;
contextMenu: ContextMenu; private options: BrowserViewConstructorOptions;
private removeLoading?: number;
status?: Status; private contextMenu: ContextMenu;
retryLoad?: NodeJS.Timeout; private status?: Status;
maxRetries: number; private retryLoad?: NodeJS.Timeout;
private maxRetries: number;
private altPressStatus: boolean; private altPressStatus: boolean;
constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) { constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) {
@@ -81,38 +76,24 @@ export class MattermostView extends EventEmitter {
...options.webPreferences, ...options.webPreferences,
}; };
this.isVisible = false; this.isVisible = false;
this.isLoggedIn = false; this.loggedIn = false;
this.isAtRoot = true; this.atRoot = true;
this.view = new BrowserView(this.options); this.view = new BrowserView(this.options);
this.resetLoadingStatus(); this.resetLoadingStatus();
log.verbose(`BrowserView created for server ${this.tab.name}`); this.log = new Logger(this.name, 'MattermostView');
this.log.verbose('View created');
this.hasBeenShown = false;
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') { if (process.platform !== 'darwin') {
this.view.webContents.on('before-input-event', this.handleInputEvents); this.view.webContents.on('before-input-event', this.handleInputEvents);
} }
this.view.webContents.on('did-finish-load', () => { WebContentsEventManager.addWebContentsEventListeners(this.view.webContents);
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);
});
this.contextMenu = new ContextMenu({}, this.view); this.contextMenu = new ContextMenu({}, this.view);
this.maxRetries = MAX_SERVER_RETRIES; 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() { get name() {
return this.tab.name; return this.tab.name;
} }
get isAtRoot() {
get urlTypeTuple(): TabTuple { return this.atRoot;
return this.tab.urlTypeTuple; }
get isLoggedIn() {
return this.loggedIn;
}
get currentURL() {
return this.view.webContents.getURL();
}
get webContentsId() {
return this.view.webContents.id;
} }
updateServerInfo = (srv: MattermostServer) => { updateServerInfo = (srv: MattermostServer) => {
let reload;
if (srv.url.toString() !== this.tab.server.url.toString()) {
reload = () => this.reload();
}
this.tab.server = srv; this.tab.server = srv;
this.serverInfo = new ServerInfo(srv); this.serverInfo = new ServerInfo(srv);
this.view.webContents.send(SET_VIEW_OPTIONS, this.tab.name, this.tab.shouldNotify); this.view.webContents.send(SET_VIEW_OPTIONS, this.tab.name, this.tab.shouldNotify);
reload?.();
} }
resetLoadingStatus = () => { onLogin = (loggedIn: boolean) => {
if (this.status !== Status.LOADING) { // if it's already loading, don't touch anything if (this.isLoggedIn === loggedIn) {
delete this.retryLoad; return;
this.status = Status.LOADING;
this.maxRetries = MAX_SERVER_RETRIES;
} }
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) => { load = (someURL?: URL | string) => {
@@ -159,19 +191,19 @@ export class MattermostView extends EventEmitter {
if (parsedURL) { if (parsedURL) {
loadURL = parsedURL.toString(); loadURL = parsedURL.toString();
} else { } 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(); loadURL = this.tab.url.toString();
} }
} else { } else {
loadURL = this.tab.url.toString(); 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()}); const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) { if (err.code && err.code.startsWith('ERR_CERT')) {
WindowManager.sendToRenderer(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString()); this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.tab.name)}] Invalid certificate, stop retrying until the user decides what to do: ${err}.`); this.log.info('Invalid certificate, stop retrying until the user decides what to do.', err);
this.status = Status.ERROR; this.status = Status.ERROR;
return; return;
} }
@@ -183,85 +215,28 @@ export class MattermostView extends EventEmitter {
}); });
} }
retry = (loadURL: string) => { show = () => {
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) => {
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
if (this.isVisible) {
this.hasBeenShown = true; return;
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility; }
if (request && !this.isVisible) { this.isVisible = true;
mainWindow.addBrowserView(this.view); mainWindow.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.view.webContents.getURL()))); mainWindow.setTopBrowserView(this.view);
if (this.status === Status.READY) { this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
this.focus(); if (this.status === Status.READY) {
} this.focus();
} else if (!request && this.isVisible) { }
mainWindow.removeBrowserView(this.view); }
hide = () => {
if (this.isVisible) {
this.isVisible = false;
MainWindow.get()?.removeBrowserView(this.view);
} }
this.isVisible = request;
} }
reload = () => { reload = () => {
@@ -269,41 +244,21 @@ export class MattermostView extends EventEmitter {
this.load(); this.load();
} }
hide = () => this.show(false); getBounds = () => {
return this.view.getBounds();
}
openFind = () => { openFind = () => {
this.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']}); 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) => { setBounds = (boundaries: Electron.Rectangle) => {
this.view.setBounds(boundaries); this.view.setBounds(boundaries);
} }
destroy = () => { destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.view.webContents.id); WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
appState.updateMentions(this.tab.name, 0, false); appState.updateMentions(this.name, 0, false);
MainWindow.get()?.removeBrowserView(this.view); MainWindow.get()?.removeBrowserView(this.view);
// workaround to eliminate zombie processes // workaround to eliminate zombie processes
@@ -319,13 +274,19 @@ export class MattermostView extends EventEmitter {
if (this.removeLoading) { if (this.removeLoading) {
clearTimeout(this.removeLoading); clearTimeout(this.removeLoading);
} }
this.contextMenu.dispose();
} }
focus = () => { /**
if (this.view.webContents) { * Status hooks
this.view.webContents.focus(); */
} else {
log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.'); 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; this.status = Status.READY;
if (timedout) { if (timedout) {
log.info(`${this.tab.name} timeout expired will show the browserview`); this.log.verbose('timeout expired will show the browserview');
this.emit(LOADSCREEN_END, this.tab.name); this.emit(LOADSCREEN_END, this.name);
} }
clearTimeout(this.removeLoading); clearTimeout(this.removeLoading);
delete this.removeLoading; delete this.removeLoading;
} }
isInitialized = () => {
return this.status === Status.READY;
}
openDevTools = () => { openDevTools = () => {
this.view.webContents.openDevTools({mode: 'detach'}); 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) => { private registerAltKeyPressed = (input: Input) => {
const isAltPressed = input.key === 'Alt' && input.alt === true && input.control === false && input.shift === false && input.meta === false; 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; return input.type === 'keyUp' && this.altPressStatus === true;
}; };
handleInputEvents = (_: Event, input: Input) => { private handleInputEvents = (_: Event, input: Input) => {
log.silly('handleInputEvents', {tabName: this.tab.name, input}); this.log.silly('handleInputEvents', input);
this.registerAltKeyPressed(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)) { if (shouldHaveBackBar(this.tab.url || '', url)) {
this.setBounds(getWindowBoundaries(MainWindow.get()!, true)); this.setBounds(getWindowBoundaries(MainWindow.get()!, true));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
log.info('show back button'); this.log.debug('show back button');
} else { } else {
this.setBounds(getWindowBoundaries(MainWindow.get()!)); this.setBounds(getWindowBoundaries(MainWindow.get()!));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
log.info('hide back button'); this.log.debug('hide back button');
} }
} }
handleUpdateTarget = (e: Event, url: string) => { private handleUpdateTarget = (e: Event, url: string) => {
log.silly('handleUpdateTarget', {tabName: this.tab.name, url}); this.log.silly('handleUpdateTarget', url);
if (url && !urlUtils.isInternalURL(urlUtils.parseURL(url), this.tab.server.url)) { if (url && !urlUtils.isInternalURL(urlUtils.parseURL(url), this.tab.server.url)) {
this.emit(UPDATE_TARGET_URL, url); this.emit(UPDATE_TARGET_URL, url);
} else { } else {
this.emit(UPDATE_TARGET_URL); 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'; 'use strict';
import {dialog, ipcMain} from 'electron'; 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 {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication';
import Config from 'common/config'; 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', () => { describe('reloadConfiguration', () => {
const viewManager = new ViewManager(); const viewManager = new ViewManager();
@@ -203,7 +143,6 @@ describe('main/views/viewManager', () => {
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({ viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
name: `${srv.name}-${tabName}`, name: `${srv.name}-${tabName}`,
urlTypeTuple: tuple(`http://${srv.name}.com/`, tabName),
url: new URL(`http://${srv.name}.com`), url: new URL(`http://${srv.name}.com`),
})); }));
MattermostServer.mockImplementation((server) => ({ MattermostServer.mockImplementation((server) => ({
@@ -219,10 +158,10 @@ describe('main/views/viewManager', () => {
once: onceFn, once: onceFn,
destroy: destroyFn, destroy: destroyFn,
name: tab.name, name: tab.name,
urlTypeTuple: tab.urlTypeTuple,
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
tab, tab,
})); }));
getTabViewName.mockImplementation((a, b) => `${a}-${b}`);
}); });
afterEach(() => { afterEach(() => {
@@ -249,7 +188,6 @@ describe('main/views/viewManager', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView'); const makeSpy = jest.spyOn(viewManager, 'makeView');
const view = new MattermostView({ const view = new MattermostView({
name: 'server1-tab1', name: 'server1-tab1',
urlTypeTuple: tuple(new URL('http://server1.com').href, 'tab1'),
server: 'server1', server: 'server1',
}); });
viewManager.views.set('server1-tab1', view); viewManager.views.set('server1-tab1', view);
@@ -303,7 +241,7 @@ describe('main/views/viewManager', () => {
name: 'tab1', name: 'tab1',
isOpen: true, isOpen: true,
}, },
'http://server1.com/', 'http://server1.com',
); );
makeSpy.mockRestore(); makeSpy.mockRestore();
}); });
@@ -318,7 +256,6 @@ describe('main/views/viewManager', () => {
name: 'server1-tab1', name: 'server1-tab1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
urlTypeTuple: tuple('http://server1.com/', 'tab1'),
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
}; };
@@ -348,7 +285,6 @@ describe('main/views/viewManager', () => {
name: 'server1-tab1', name: 'server1-tab1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
urlTypeTuple: ['http://server.com/', 'tab1'],
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
}; };
@@ -772,12 +708,8 @@ describe('main/views/viewManager', () => {
resetLoadingStatus: jest.fn(), resetLoadingStatus: jest.fn(),
load: jest.fn(), load: jest.fn(),
once: jest.fn(), once: jest.fn(),
isInitialized: jest.fn(), isReady: jest.fn(),
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
serverInfo: { serverInfo: {
remoteInfo: { remoteInfo: {
serverVersion: '1.0.0', 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.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); 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', () => { it('should throw error if view is missing', () => {

View File

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

View File

@@ -17,7 +17,6 @@ import {protocols} from '../../../electron-builder.json';
import allowProtocolDialog from '../allowProtocolDialog'; import allowProtocolDialog from '../allowProtocolDialog';
import {composeUserAgent} from '../utils'; import {composeUserAgent} from '../utils';
import {MattermostView} from './MattermostView';
import ViewManager from './viewManager'; import ViewManager from './viewManager';
type CustomLogin = { 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 = ( addWebContentsEventListeners = (
contents: WebContents, contents: WebContents,
addListeners?: (contents: WebContents) => void, addListeners?: (contents: WebContents) => void,

View File

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

View File

@@ -190,7 +190,7 @@ export default class CallsWidgetWindow extends EventEmitter {
return; return;
} }
this.mainView.view.webContents.send(CALLS_JOINED_CALL, message); this.mainView.sendToRenderer(CALLS_JOINED_CALL, message);
} }
private setBounds(bounds: Rectangle) { 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 // Only allow events coming from either the widget window or the
// original Mattermost view that initiated it. // original Mattermost view that initiated it.
return event.sender.id === this.getWebContentsId() || 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', () => { describe('selectTab', () => {
const windowManager = new WindowManager(); const windowManager = new WindowManager();
windowManager.switchTab = jest.fn(); windowManager.switchTab = jest.fn();
@@ -801,11 +746,7 @@ describe('main/windows/windowManager', () => {
const map = Config.teams.reduce((arr, item) => { const map = Config.teams.reduce((arr, item) => {
item.tabs.forEach((tab) => { item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, { arr.push([`${item.name}_${tab.name}`, {
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
}]); }]);
}); });
return arr; return arr;
@@ -838,7 +779,7 @@ describe('main/windows/windowManager', () => {
await windowManager.handleGetDesktopSources('server-1_tab-1', null); 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', id: 'screen0',
}, },
@@ -854,7 +795,7 @@ describe('main/windows/windowManager', () => {
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions', 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', err: 'screen-permissions',
}); });
expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1); 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', { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions', 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', 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); 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', { expect(windowManager.callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions', 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', err: 'screen-permissions',
}); });
@@ -932,11 +873,7 @@ describe('main/windows/windowManager', () => {
return { return {
getServerName: () => 'server-1', getServerName: () => 'server-1',
getMainView: jest.fn().mockReturnValue({ getMainView: jest.fn().mockReturnValue({
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
}), }),
}; };
}); });
@@ -1007,11 +944,7 @@ describe('main/windows/windowManager', () => {
return { return {
getServerName: () => 'server-2', getServerName: () => 'server-2',
getMainView: jest.fn().mockReturnValue({ getMainView: jest.fn().mockReturnValue({
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
}), }),
getChannelURL: jest.fn(), getChannelURL: jest.fn(),
}; };
@@ -1086,11 +1019,7 @@ describe('main/windows/windowManager', () => {
return { return {
getServerName: () => 'server-2', getServerName: () => 'server-2',
getMainView: jest.fn().mockReturnValue({ getMainView: jest.fn().mockReturnValue({
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
}), }),
}; };
}); });
@@ -1107,7 +1036,7 @@ describe('main/windows/windowManager', () => {
windowManager.handleCallsError('', {err: 'client-error'}); windowManager.handleCallsError('', {err: 'client-error'});
expect(windowManager.switchServer).toHaveBeenCalledWith('server-2'); expect(windowManager.switchServer).toHaveBeenCalledWith('server-2');
expect(mainWindow.focus).toHaveBeenCalled(); 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(); const windowManager = new WindowManager();
windowManager.switchServer = jest.fn(); windowManager.switchServer = jest.fn();
const view1 = { const view1 = {
view: { sendToRenderer: jest.fn(),
webContents: {
send: jest.fn(),
},
},
}; };
beforeEach(() => { beforeEach(() => {
@@ -1141,7 +1066,7 @@ describe('main/windows/windowManager', () => {
windowManager.callsWidgetWindow = new CallsWidgetWindow(); windowManager.callsWidgetWindow = new CallsWidgetWindow();
windowManager.handleCallsLinkClick('', {link: '/other/subpath'}); windowManager.handleCallsLinkClick('', {link: '/other/subpath'});
expect(windowManager.switchServer).toHaveBeenCalledWith('server-1'); 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 { import {
MAXIMIZE_CHANGE, MAXIMIZE_CHANGE,
HISTORY,
FOCUS_THREE_DOT_MENU, FOCUS_THREE_DOT_MENU,
GET_DARK_MODE, GET_DARK_MODE,
UPDATE_SHORTCUT_MENU, UPDATE_SHORTCUT_MENU,
@@ -74,7 +73,6 @@ export class WindowManager {
constructor() { constructor() {
this.assetsDir = path.resolve(app.getAppPath(), 'assets'); this.assetsDir = path.resolve(app.getAppPath(), 'assets');
ipcMain.on(HISTORY, this.handleHistory);
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode); ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId); ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
@@ -133,7 +131,7 @@ export class WindowManager {
if (this.callsWidgetWindow) { if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName()); this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus(); 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) { if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName()); this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus(); 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) { if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName()); this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus(); 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) { if (this.callsWidgetWindow) {
this.switchServer(this.callsWidgetWindow.getServerName()); this.switchServer(this.callsWidgetWindow.getServerName());
MainWindow.get()?.focus(); 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(); const currentView = ViewManager.getCurrentView();
if (currentView) { 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); 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 = () => { selectNextTab = () => {
this.selectTab((order) => order + 1); this.selectTab((order) => order + 1);
} }
@@ -627,7 +604,7 @@ export class WindowManager {
if (!hasScreenPermissions || !sources.length) { if (!hasScreenPermissions || !sources.length) {
log.info('missing screen permissions'); 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); this.callsWidgetWindow?.win.webContents.send(CALLS_ERROR, screenPermissionsErrMsg);
return; return;
} }
@@ -641,12 +618,12 @@ export class WindowManager {
}); });
if (message.length > 0) { if (message.length > 0) {
view.view.webContents.send(DESKTOP_SOURCES_RESULT, message); view.sendToRenderer(DESKTOP_SOURCES_RESULT, message);
} }
}).catch((err) => { }).catch((err) => {
log.error('desktopCapturer.getSources failed', 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); 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};
}