[MM-51964] Clean up MattermostView, remove tuple in preparation for id (#2668)
undefined
This commit is contained in:
@@ -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;
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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);
|
||||
}
|
||||
|
@@ -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,
|
||||
|
Reference in New Issue
Block a user