[MM-14093] Rename 'team' to 'server' and 'tab' to 'view' in most cases, some additional cleanup (#2711)

* Rename MattermostTeam -> UniqueServer, MattermostTab -> UniqueView

* Rename 'team' to 'server'

* Some further cleanup

* Rename weirdly named function

* Rename 'tab' to 'view' in most instances

* Fix i18n

* PR feedback
This commit is contained in:
Devin Binnie
2023-05-08 09:17:01 -04:00
committed by GitHub
parent 9f75ddcf0f
commit 316beba950
110 changed files with 1698 additions and 1757 deletions

View File

@@ -6,13 +6,13 @@
import AppState from 'common/appState';
import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from 'common/tabs/MessagingTabView';
import MessagingView from 'common/views/MessagingView';
import MainWindow from '../windows/mainWindow';
import ContextMenu from '../contextMenu';
import Utils from '../utils';
import {MattermostView} from './MattermostView';
import {MattermostBrowserView} from './MattermostBrowserView';
jest.mock('electron', () => ({
app: {
@@ -59,12 +59,12 @@ jest.mock('../utils', () => ({
}));
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
const tabView = new MessagingTabView(server, true);
const view = new MessagingView(server, true);
describe('main/views/MattermostView', () => {
describe('main/views/MattermostBrowserView', () => {
describe('load', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -74,38 +74,38 @@ describe('main/views/MattermostView', () => {
it('should load provided URL when provided', async () => {
const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('http://server-2.com');
await promise;
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-2.com/', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-2.com/', expect.any(Object));
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-2.com/');
});
it('should load server URL when not provided', async () => {
const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load();
await promise;
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
});
it('should load server URL when bad url provided', async () => {
const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await promise;
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
});
it('should call retry when failing to load', async () => {
const error = new Error('test');
const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com/', error);
});
@@ -113,23 +113,23 @@ describe('main/views/MattermostView', () => {
const error = new Error('test');
error.code = 'ERR_CERT_ERROR';
const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com/', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled();
});
});
describe('retry', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
const retryInBackgroundFn = jest.fn();
beforeEach(() => {
jest.useFakeTimers();
MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.browserView.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.loadSuccess = jest.fn();
mattermostView.loadRetry = jest.fn();
mattermostView.emit = jest.fn();
@@ -143,16 +143,16 @@ describe('main/views/MattermostView', () => {
});
it('should do nothing when webcontents are destroyed', () => {
const webContents = mattermostView.view.webContents;
mattermostView.view.webContents = null;
const webContents = mattermostView.browserView.webContents;
mattermostView.browserView.webContents = null;
mattermostView.retry('http://server-1.com')();
expect(mattermostView.loadSuccess).not.toBeCalled();
mattermostView.view.webContents = webContents;
mattermostView.browserView.webContents = webContents;
});
it('should call loadSuccess on successful load', async () => {
const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await promise;
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com');
@@ -162,10 +162,10 @@ describe('main/views/MattermostView', () => {
mattermostView.maxRetries = 10;
const error = new Error('test');
const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com', error);
});
@@ -173,12 +173,12 @@ describe('main/views/MattermostView', () => {
mattermostView.maxRetries = 0;
const error = new Error('test');
const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise);
mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.browserView.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled();
expect(MainWindow.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.id, expect.any(String), expect.any(String));
expect(MainWindow.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.view.id, expect.any(String), expect.any(String));
expect(mattermostView.status).toBe(-1);
jest.runAllTimers();
expect(retryInBackgroundFn).toBeCalled();
@@ -187,7 +187,7 @@ describe('main/views/MattermostView', () => {
describe('goToOffset', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.reload = jest.fn();
afterEach(() => {
@@ -196,18 +196,18 @@ describe('main/views/MattermostView', () => {
});
it('should only go to offset if it can', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(false);
mattermostView.browserView.webContents.canGoToOffset.mockReturnValue(false);
mattermostView.goToOffset(1);
expect(mattermostView.view.webContents.goToOffset).not.toBeCalled();
expect(mattermostView.browserView.webContents.goToOffset).not.toBeCalled();
mattermostView.view.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.browserView.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.goToOffset(1);
expect(mattermostView.view.webContents.goToOffset).toBeCalled();
expect(mattermostView.browserView.webContents.goToOffset).toBeCalled();
});
it('should call reload if an error occurs', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.view.webContents.goToOffset.mockImplementation(() => {
mattermostView.browserView.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.browserView.webContents.goToOffset.mockImplementation(() => {
throw new Error('hi');
});
mattermostView.goToOffset(1);
@@ -217,8 +217,8 @@ describe('main/views/MattermostView', () => {
describe('onLogin', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.getURL = jest.fn();
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.getURL = jest.fn();
mattermostView.reload = jest.fn();
afterEach(() => {
@@ -227,19 +227,19 @@ describe('main/views/MattermostView', () => {
});
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.browserView.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.browserView.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.browserView.webContents.getURL.mockReturnValue('http://server-1.com/subpath');
mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled();
});
@@ -247,7 +247,7 @@ describe('main/views/MattermostView', () => {
describe('loadSuccess', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
jest.useFakeTimers();
@@ -275,7 +275,7 @@ describe('main/views/MattermostView', () => {
describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
jest.useFakeTimers();
@@ -293,7 +293,7 @@ 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();
expect(window.addBrowserView).toBeCalledWith(mattermostView.view);
expect(window.addBrowserView).toBeCalledWith(mattermostView.browserView);
expect(mattermostView.setBounds).toBeCalled();
expect(mattermostView.isVisible).toBe(true);
});
@@ -314,7 +314,7 @@ describe('main/views/MattermostView', () => {
describe('hide', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -323,7 +323,7 @@ describe('main/views/MattermostView', () => {
it('should remove browser view', () => {
mattermostView.isVisible = true;
mattermostView.hide();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
expect(mattermostView.isVisible).toBe(false);
});
@@ -336,7 +336,7 @@ describe('main/views/MattermostView', () => {
describe('updateHistoryButton', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -345,7 +345,7 @@ describe('main/views/MattermostView', () => {
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.browserView.webContents.clearHistory).toHaveBeenCalled();
expect(mattermostView.isAtRoot).toBe(true);
});
});
@@ -362,22 +362,22 @@ describe('main/views/MattermostView', () => {
});
it('should remove browser view from window', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view);
expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
});
it('should clear mentions', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(AppState.clear).toBeCalledWith(mattermostView.tab.id);
expect(AppState.clear).toBeCalledWith(mattermostView.view.id);
});
it('should clear outstanding timeouts', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.browserView.webContents.destroy = jest.fn();
const spy = jest.spyOn(global, 'clearTimeout');
mattermostView.retryLoad = 999;
mattermostView.removeLoading = 1000;
@@ -388,7 +388,7 @@ describe('main/views/MattermostView', () => {
describe('handleInputEvents', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
it('should open three dot menu on pressing Alt', () => {
MainWindow.get.mockReturnValue(window);
@@ -413,7 +413,7 @@ describe('main/views/MattermostView', () => {
describe('handleDidNavigate', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -435,7 +435,7 @@ describe('main/views/MattermostView', () => {
describe('handleUpdateTarget', () => {
const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => {
MainWindow.get.mockReturnValue(window);
@@ -466,16 +466,16 @@ describe('main/views/MattermostView', () => {
});
describe('updateMentionsFromTitle', () => {
const mattermostView = new MattermostView(tabView, {}, {});
const mattermostView = new MattermostBrowserView(view, {}, {});
it('should parse mentions from title', () => {
mattermostView.updateMentionsFromTitle('(7) Mattermost');
expect(AppState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.id, 7);
expect(AppState.updateMentions).toHaveBeenCalledWith(mattermostView.view.id, 7);
});
it('should parse unreads from title', () => {
mattermostView.updateMentionsFromTitle('* Mattermost');
expect(AppState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.id, 0);
expect(AppState.updateMentions).toHaveBeenCalledWith(mattermostView.view.id, 0);
});
});
});

View File

@@ -23,7 +23,7 @@ import {
import ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log';
import {isInternalURL, parseURL} from 'common/utils/url';
import {TabView} from 'common/tabs/TabView';
import {MattermostView} from 'common/views/View';
import MainWindow from 'main/windows/mainWindow';
@@ -42,12 +42,12 @@ enum Status {
const MENTIONS_GROUP = 2;
const titleParser = /(\((\d+)\) )?(\* )?/g;
export class MattermostView extends EventEmitter {
tab: TabView;
export class MattermostBrowserView extends EventEmitter {
view: MattermostView;
isVisible: boolean;
private log: Logger;
private view: BrowserView;
private browserView: BrowserView;
private loggedIn: boolean;
private atRoot: boolean;
private options: BrowserViewConstructorOptions;
@@ -58,9 +58,9 @@ export class MattermostView extends EventEmitter {
private maxRetries: number;
private altPressStatus: boolean;
constructor(tab: TabView, options: BrowserViewConstructorOptions) {
constructor(view: MattermostView, options: BrowserViewConstructorOptions) {
super();
this.tab = tab;
this.view = view;
const preload = getLocalPreload('preload.js');
this.options = Object.assign({}, options);
@@ -75,24 +75,24 @@ export class MattermostView extends EventEmitter {
this.isVisible = false;
this.loggedIn = false;
this.atRoot = true;
this.view = new BrowserView(this.options);
this.browserView = new BrowserView(this.options);
this.resetLoadingStatus();
this.log = ServerManager.getViewLog(this.id, 'MattermostView');
this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView');
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);
this.browserView.webContents.on('did-finish-load', this.handleDidFinishLoad);
this.browserView.webContents.on('page-title-updated', this.handleTitleUpdate);
this.browserView.webContents.on('page-favicon-updated', this.handleFaviconUpdate);
this.browserView.webContents.on('update-target-url', this.handleUpdateTarget);
this.browserView.webContents.on('did-navigate', this.handleDidNavigate);
if (process.platform !== 'darwin') {
this.view.webContents.on('before-input-event', this.handleInputEvents);
this.browserView.webContents.on('before-input-event', this.handleInputEvents);
}
WebContentsEventManager.addWebContentsEventListeners(this.view.webContents);
WebContentsEventManager.addWebContentsEventListeners(this.browserView.webContents);
this.contextMenu = new ContextMenu({}, this.view);
this.contextMenu = new ContextMenu({}, this.browserView);
this.maxRetries = MAX_SERVER_RETRIES;
this.altPressStatus = false;
@@ -105,7 +105,7 @@ export class MattermostView extends EventEmitter {
}
get id() {
return this.tab.id;
return this.view.id;
}
get isAtRoot() {
return this.atRoot;
@@ -114,10 +114,10 @@ export class MattermostView extends EventEmitter {
return this.loggedIn;
}
get currentURL() {
return parseURL(this.view.webContents.getURL());
return parseURL(this.browserView.webContents.getURL());
}
get webContentsId() {
return this.view.webContents.id;
return this.browserView.webContents.id;
}
onLogin = (loggedIn: boolean) => {
@@ -127,19 +127,19 @@ export class MattermostView extends EventEmitter {
this.loggedIn = loggedIn;
// If we're logging in from a different tab, force a reload
// If we're logging in from a different view, force a reload
if (loggedIn &&
this.currentURL?.toString() !== this.tab.url.toString() &&
!this.currentURL?.toString().startsWith(this.tab.url.toString())
this.currentURL?.toString() !== this.view.url.toString() &&
!this.currentURL?.toString().startsWith(this.view.url.toString())
) {
this.reload();
}
}
goToOffset = (offset: number) => {
if (this.view.webContents.canGoToOffset(offset)) {
if (this.browserView.webContents.canGoToOffset(offset)) {
try {
this.view.webContents.goToOffset(offset);
this.browserView.webContents.goToOffset(offset);
this.updateHistoryButton();
} catch (error) {
this.log.error(error);
@@ -149,17 +149,17 @@ export class MattermostView extends EventEmitter {
}
updateHistoryButton = () => {
if (this.currentURL?.toString() === this.tab.url.toString()) {
this.view.webContents.clearHistory();
if (this.currentURL?.toString() === this.view.url.toString()) {
this.browserView.webContents.clearHistory();
this.atRoot = true;
} else {
this.atRoot = false;
}
this.view.webContents.send(BROWSER_HISTORY_BUTTON, this.view.webContents.canGoBack(), this.view.webContents.canGoForward());
this.browserView.webContents.send(BROWSER_HISTORY_BUTTON, this.browserView.webContents.canGoBack(), this.browserView.webContents.canGoForward());
}
load = (someURL?: URL | string) => {
if (!this.tab) {
if (!this.browserView) {
return;
}
@@ -170,13 +170,13 @@ export class MattermostView extends EventEmitter {
loadURL = parsedURL.toString();
} else {
this.log.error('Cannot parse provided url, using current server url', someURL);
loadURL = this.tab.url.toString();
loadURL = this.view.url.toString();
}
} else {
loadURL = this.tab.url.toString();
loadURL = this.view.url.toString();
}
this.log.verbose(`Loading ${loadURL}`);
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) {
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
@@ -205,9 +205,9 @@ export class MattermostView extends EventEmitter {
return;
}
this.isVisible = true;
mainWindow.addBrowserView(this.view);
mainWindow.setTopBrowserView(this.view);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
mainWindow.addBrowserView(this.browserView);
mainWindow.setTopBrowserView(this.browserView);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.view.url || '', this.currentURL)));
if (this.status === Status.READY) {
this.focus();
}
@@ -216,7 +216,7 @@ export class MattermostView extends EventEmitter {
hide = () => {
if (this.isVisible) {
this.isVisible = false;
MainWindow.get()?.removeBrowserView(this.view);
MainWindow.get()?.removeBrowserView(this.browserView);
}
}
@@ -226,27 +226,27 @@ export class MattermostView extends EventEmitter {
}
getBounds = () => {
return this.view.getBounds();
return this.browserView.getBounds();
}
openFind = () => {
this.view.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
this.browserView.webContents.sendInputEvent({type: 'keyDown', keyCode: 'F', modifiers: [process.platform === 'darwin' ? 'cmd' : 'ctrl', 'shift']});
}
setBounds = (boundaries: Electron.Rectangle) => {
this.view.setBounds(boundaries);
this.browserView.setBounds(boundaries);
}
destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
AppState.clear(this.id);
MainWindow.get()?.removeBrowserView(this.view);
MainWindow.get()?.removeBrowserView(this.browserView);
// workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
this.view.webContents.destroy();
this.browserView.webContents.destroy();
this.isVisible = false;
if (this.retryLoad) {
@@ -293,7 +293,7 @@ export class MattermostView extends EventEmitter {
}
openDevTools = () => {
this.view.webContents.openDevTools({mode: 'detach'});
this.browserView.webContents.openDevTools({mode: 'detach'});
}
/**
@@ -301,16 +301,16 @@ export class MattermostView extends EventEmitter {
*/
sendToRenderer = (channel: string, ...args: any[]) => {
this.view.webContents.send(channel, ...args);
this.browserView.webContents.send(channel, ...args);
}
isDestroyed = () => {
return this.view.webContents.isDestroyed();
return this.browserView.webContents.isDestroyed();
}
focus = () => {
if (this.view.webContents) {
this.view.webContents.focus();
if (this.browserView.webContents) {
this.browserView.webContents.focus();
} else {
this.log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.');
}
@@ -361,7 +361,7 @@ export class MattermostView extends EventEmitter {
// 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.id);
this.browserView.webContents.send(IS_UNREAD, favicon, this.id);
} catch (err: any) {
this.log.error('There was an error trying to request the unread state', err);
}
@@ -388,17 +388,17 @@ export class MattermostView extends EventEmitter {
private retry = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
if (!this.browserView || !this.browserView.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err);
} else {
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.log.info(`Couldn't establish a connection with ${loadURL}, will continue to retry in the background`, err);
this.log.info(`Couldn't esviewlish a connection with ${loadURL}, will continue to retry in the background`, err);
this.status = Status.ERROR;
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
}
@@ -409,10 +409,10 @@ export class MattermostView extends EventEmitter {
private retryInBackground = (loadURL: string) => {
return () => {
// window was closed while retrying
if (!this.view || !this.view.webContents) {
if (!this.browserView || !this.browserView.webContents) {
return;
}
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
const loading = this.browserView.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch(() => {
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
});
@@ -431,7 +431,7 @@ export class MattermostView extends EventEmitter {
MainWindow.sendToRenderer(LOAD_SUCCESS, this.id);
this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle());
this.updateMentionsFromTitle(this.browserView.webContents.getTitle());
this.findUnreadState(null);
}
this.status = Status.WAITING_MM;
@@ -439,7 +439,7 @@ export class MattermostView extends EventEmitter {
this.emit(LOAD_SUCCESS, this.id, loadURL);
const mainWindow = MainWindow.get();
if (mainWindow && this.currentURL) {
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.view.url || '', this.currentURL)));
}
};
}
@@ -453,13 +453,13 @@ export class MattermostView extends EventEmitter {
// wait for screen to truly finish loading before sending the message down
const timeout = setInterval(() => {
if (!this.view.webContents) {
if (!this.browserView.webContents) {
return;
}
if (!this.view.webContents.isLoading()) {
if (!this.browserView.webContents.isLoading()) {
try {
this.view.webContents.send(SET_VIEW_OPTIONS, this.id, this.tab.shouldNotify);
this.browserView.webContents.send(SET_VIEW_OPTIONS, this.id, this.view.shouldNotify);
clearTimeout(timeout);
} catch (e) {
this.log.error('failed to send view options to view');
@@ -480,7 +480,7 @@ export class MattermostView extends EventEmitter {
return;
}
if (shouldHaveBackBar(this.tab.url || '', parsedURL)) {
if (shouldHaveBackBar(this.view.url || '', parsedURL)) {
this.setBounds(getWindowBoundaries(mainWindow, true));
MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true);
this.log.debug('show back button');
@@ -494,7 +494,7 @@ export class MattermostView extends EventEmitter {
private handleUpdateTarget = (e: Event, url: string) => {
this.log.silly('handleUpdateTarget', url);
const parsedURL = parseURL(url);
if (parsedURL && isInternalURL(parsedURL, this.tab.server.url)) {
if (parsedURL && isInternalURL(parsedURL, this.view.server.url)) {
this.emit(UPDATE_TARGET_URL);
} else {
this.emit(UPDATE_TARGET_URL, url);
@@ -502,7 +502,7 @@ export class MattermostView extends EventEmitter {
}
private handleServerWasModified = (serverIds: string) => {
if (serverIds.includes(this.tab.server.id)) {
if (serverIds.includes(this.view.server.id)) {
this.reload();
}
}

View File

@@ -7,7 +7,7 @@ import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHA
import MainWindow from 'main/windows/mainWindow';
import {TeamDropdownView} from './teamDropdownView';
import {ServerDropdownView} from './serverDropdownView';
jest.mock('main/utils', () => ({
getLocalPreload: (file) => file,
@@ -38,36 +38,36 @@ jest.mock('common/servers/serverManager', () => ({
getOrderedServers: jest.fn().mockReturnValue([]),
}));
describe('main/views/teamDropdownView', () => {
describe('main/views/serverDropdownView', () => {
describe('getBounds', () => {
beforeEach(() => {
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
});
const teamDropdownView = new TeamDropdownView();
const serverDropdownView = new ServerDropdownView();
if (process.platform === 'darwin') {
it('should account for three dot menu, tab bar and shadow', () => {
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
expect(serverDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH_MAC - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
});
} else {
it('should account for three dot menu, tab bar and shadow', () => {
expect(teamDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
expect(serverDropdownView.getBounds(400, 300)).toStrictEqual({x: THREE_DOT_MENU_WIDTH - MENU_SHADOW_WIDTH, y: TAB_BAR_HEIGHT - MENU_SHADOW_WIDTH, width: 400, height: 300});
});
}
});
it('should change the view bounds based on open/closed state', () => {
const teamDropdownView = new TeamDropdownView();
teamDropdownView.view = {
const serverDropdownView = new ServerDropdownView();
serverDropdownView.view = {
setBounds: jest.fn(),
webContents: {
focus: jest.fn(),
},
};
teamDropdownView.bounds = {width: 400, height: 300};
teamDropdownView.handleOpen();
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds);
teamDropdownView.handleClose();
expect(teamDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
serverDropdownView.bounds = {width: 400, height: 300};
serverDropdownView.handleOpen();
expect(serverDropdownView.view.setBounds).toBeCalledWith(serverDropdownView.bounds);
serverDropdownView.handleClose();
expect(serverDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
});
});

View File

@@ -3,16 +3,16 @@
import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import AppState from 'common/appState';
import {
CLOSE_TEAMS_DROPDOWN,
CLOSE_SERVERS_DROPDOWN,
EMIT_CONFIGURATION,
OPEN_TEAMS_DROPDOWN,
UPDATE_TEAMS_DROPDOWN,
OPEN_SERVERS_DROPDOWN,
UPDATE_SERVERS_DROPDOWN,
UPDATE_APPSTATE,
REQUEST_TEAMS_DROPDOWN_INFO,
REQUEST_SERVERS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE,
SERVERS_UPDATE,
MAIN_WINDOW_CREATED,
@@ -27,12 +27,12 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
import MainWindow from '../windows/mainWindow';
const log = new Logger('TeamDropdownView');
const log = new Logger('ServerDropdownView');
export class TeamDropdownView {
export class ServerDropdownView {
private view?: BrowserView;
private teams: MattermostTeam[];
private hasGPOTeams: boolean;
private servers: UniqueServer[];
private hasGPOServers: boolean;
private isOpen: boolean;
private bounds: Electron.Rectangle;
@@ -43,8 +43,8 @@ export class TeamDropdownView {
private windowBounds?: Electron.Rectangle;
constructor() {
this.teams = [];
this.hasGPOTeams = false;
this.servers = [];
this.hasGPOServers = false;
this.isOpen = false;
this.bounds = this.getBounds(0, 0);
@@ -55,12 +55,12 @@ export class TeamDropdownView {
MainWindow.on(MAIN_WINDOW_CREATED, this.init);
MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds);
ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose);
ipcMain.on(OPEN_SERVERS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_SERVERS_DROPDOWN, this.handleClose);
ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize);
ipcMain.on(EMIT_CONFIGURATION, this.updateDropdown);
ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown);
ipcMain.on(REQUEST_SERVERS_DROPDOWN_INFO, this.updateDropdown);
AppState.on(UPDATE_APPSTATE, this.updateMentions);
ServerManager.on(SERVERS_UPDATE, this.updateServers);
@@ -84,7 +84,7 @@ export class TeamDropdownView {
}});
this.view.webContents.loadURL(getLocalURLString('dropdown.html'));
this.setOrderedTeams();
this.setOrderedServers();
this.windowBounds = MainWindow.getBounds();
this.updateDropdown();
MainWindow.get()?.addBrowserView(this.view);
@@ -94,13 +94,13 @@ export class TeamDropdownView {
log.silly('updateDropdown');
this.view?.webContents.send(
UPDATE_TEAMS_DROPDOWN,
this.teams,
UPDATE_SERVERS_DROPDOWN,
this.servers,
Config.darkMode,
this.windowBounds,
ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined,
Config.enableServerManagement,
this.hasGPOTeams,
this.hasGPOServers,
this.expired,
this.mentions,
this.unreads,
@@ -108,7 +108,7 @@ export class TeamDropdownView {
}
private updateServers = () => {
this.setOrderedTeams();
this.setOrderedServers();
this.updateDropdown();
}
@@ -137,7 +137,7 @@ export class TeamDropdownView {
this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus();
MainWindow.sendToRenderer(OPEN_TEAMS_DROPDOWN);
MainWindow.sendToRenderer(OPEN_SERVERS_DROPDOWN);
this.isOpen = true;
}
@@ -145,7 +145,7 @@ export class TeamDropdownView {
log.debug('handleClose');
this.view?.setBounds(this.getBounds(0, 0));
MainWindow.sendToRenderer(CLOSE_TEAMS_DROPDOWN);
MainWindow.sendToRenderer(CLOSE_SERVERS_DROPDOWN);
this.isOpen = false;
}
@@ -174,7 +174,7 @@ export class TeamDropdownView {
private reduceNotifications = <T>(inputMap: Map<string, T>, items: Map<string, T>, modifier: (base?: T, value?: T) => T) => {
inputMap.clear();
return [...items.keys()].reduce((map, key) => {
const view = ServerManager.getTab(key);
const view = ServerManager.getView(key);
if (!view) {
return map;
}
@@ -183,11 +183,11 @@ export class TeamDropdownView {
}, inputMap);
}
private setOrderedTeams = () => {
this.teams = ServerManager.getOrderedServers().map((team) => team.toMattermostTeam());
this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined);
private setOrderedServers = () => {
this.servers = ServerManager.getOrderedServers().map((server) => server.toUniqueServer());
this.hasGPOServers = this.servers.some((srv) => srv.isPredefined);
}
}
const teamDropdownView = new TeamDropdownView();
export default teamDropdownView;
const serverDropdownView = new ServerDropdownView();
export default serverDropdownView;

View File

@@ -7,13 +7,13 @@
import {dialog} from 'electron';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication';
import {TAB_MESSAGING} from 'common/tabs/TabView';
import {TAB_MESSAGING} from 'common/views/View';
import ServerManager from 'common/servers/serverManager';
import urlUtils from 'common/utils/url';
import MainWindow from 'main/windows/mainWindow';
import {MattermostView} from './MattermostView';
import {MattermostBrowserView} from './MattermostBrowserView';
import {ViewManager} from './viewManager';
import LoadingScreen from './loadingScreen';
@@ -30,12 +30,9 @@ jest.mock('electron', () => ({
handle: jest.fn(),
},
}));
jest.mock('common/config', () => ({
teams: [],
}));
jest.mock('common/tabs/TabView', () => ({
getTabViewName: jest.fn((a, b) => `${a}-${b}`),
TAB_MESSAGING: 'tab',
jest.mock('common/views/View', () => ({
getViewName: jest.fn((a, b) => `${a}-${b}`),
TAB_MESSAGING: 'view',
}));
jest.mock('common/servers/MattermostServer', () => ({
@@ -79,7 +76,7 @@ jest.mock('common/servers/serverManager', () => ({
getLastActiveServer: jest.fn(),
getLastActiveTabForServer: jest.fn(),
updateLastActive: jest.fn(),
lookupTabByURL: jest.fn(),
lookupViewByURL: jest.fn(),
getRemoteInfo: jest.fn(),
on: jest.fn(),
getServerLog: () => ({
@@ -100,8 +97,8 @@ jest.mock('common/servers/serverManager', () => ({
}),
}));
jest.mock('./MattermostView', () => ({
MattermostView: jest.fn(),
jest.mock('./MattermostBrowserView', () => ({
MattermostBrowserView: jest.fn(),
}));
jest.mock('./modalManager', () => ({
@@ -121,12 +118,12 @@ describe('main/views/viewManager', () => {
beforeEach(() => {
viewManager.showById = jest.fn();
MainWindow.get.mockReturnValue({});
MattermostView.mockImplementation((tab) => ({
MattermostBrowserView.mockImplementation((view) => ({
on: jest.fn(),
load: loadFn,
once: onceFn,
destroy: destroyFn,
id: tab.id,
id: view.id,
}));
});
@@ -136,21 +133,21 @@ describe('main/views/viewManager', () => {
viewManager.views = new Map();
});
it('should add closed tabs to closedViews', () => {
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: false});
expect(viewManager.closedViews.has('tab1')).toBe(true);
it('should add closed views to closedViews', () => {
viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: false});
expect(viewManager.closedViews.has('view1')).toBe(true);
});
it('should remove from remove from closedViews when the tab is open', () => {
viewManager.closedViews.set('tab1', {});
expect(viewManager.closedViews.has('tab1')).toBe(true);
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true});
expect(viewManager.closedViews.has('tab1')).toBe(false);
it('should remove from remove from closedViews when the view is open', () => {
viewManager.closedViews.set('view1', {});
expect(viewManager.closedViews.has('view1')).toBe(true);
viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: true});
expect(viewManager.closedViews.has('view1')).toBe(false);
});
it('should add view to views map and add listeners', () => {
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}, 'http://server-1.com/subpath');
expect(viewManager.views.has('tab1')).toBe(true);
viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: true}, 'http://server-1.com/subpath');
expect(viewManager.views.has('view1')).toBe(true);
expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView);
expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath');
});
@@ -173,14 +170,14 @@ describe('main/views/viewManager', () => {
const onceFn = jest.fn();
const loadFn = jest.fn();
const destroyFn = jest.fn();
MattermostView.mockImplementation((tab) => ({
MattermostBrowserView.mockImplementation((view) => ({
on: jest.fn(),
load: loadFn,
once: onceFn,
destroy: destroyFn,
id: tab.id,
id: view.id,
updateServerInfo: jest.fn(),
tab,
view,
}));
});
@@ -193,45 +190,45 @@ describe('main/views/viewManager', () => {
it('should recycle existing views', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView');
const view = new MattermostView({
id: 'tab1',
const view = new MattermostBrowserView({
id: 'view1',
server: {
id: 'server1',
},
});
viewManager.views.set('tab1', view);
viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
id: 'view1',
isOpen: true,
},
]);
viewManager.handleReloadConfiguration();
expect(viewManager.views.get('tab1')).toBe(view);
expect(viewManager.views.get('view1')).toBe(view);
expect(makeSpy).not.toHaveBeenCalled();
makeSpy.mockRestore();
});
it('should close tabs that arent open', () => {
it('should close views that arent open', () => {
ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
id: 'view1',
isOpen: false,
},
]);
viewManager.handleReloadConfiguration();
expect(viewManager.closedViews.has('tab1')).toBe(true);
expect(viewManager.closedViews.has('view1')).toBe(true);
});
it('should create new views for new tabs', () => {
it('should create new views for new views', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView');
ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
@@ -240,10 +237,10 @@ describe('main/views/viewManager', () => {
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
name: 'tab1',
id: 'view1',
name: 'view1',
isOpen: true,
url: new URL('http://server1.com/tab'),
url: new URL('http://server1.com/view'),
},
]);
viewManager.handleReloadConfiguration();
@@ -254,10 +251,10 @@ describe('main/views/viewManager', () => {
url: new URL('http://server1.com'),
},
{
id: 'tab1',
name: 'tab1',
id: 'view1',
name: 'view1',
isOpen: true,
url: new URL('http://server1.com/tab'),
url: new URL('http://server1.com/view'),
},
);
makeSpy.mockRestore();
@@ -265,27 +262,27 @@ describe('main/views/viewManager', () => {
it('should set focus to current view on reload', () => {
const view = {
id: 'tab1',
tab: {
id: 'view1',
view: {
server: {
id: 'server-1',
},
id: 'tab1',
id: 'view1',
url: new URL('http://server1.com'),
},
destroy: jest.fn(),
updateServerInfo: jest.fn(),
focus: jest.fn(),
};
viewManager.currentView = 'tab1';
viewManager.views.set('tab1', view);
viewManager.currentView = 'view1';
viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
id: 'view1',
isOpen: true,
},
]);
@@ -295,23 +292,23 @@ describe('main/views/viewManager', () => {
it('should show initial if currentView has been removed', () => {
const view = {
id: 'tab1',
tab: {
id: 'tab1',
id: 'view1',
view: {
id: 'view1',
url: new URL('http://server1.com'),
},
destroy: jest.fn(),
updateServerInfo: jest.fn(),
};
viewManager.currentView = 'tab1';
viewManager.views.set('tab1', view);
viewManager.currentView = 'view1';
viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{
id: 'server2',
url: new URL('http://server2.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
id: 'view1',
isOpen: false,
},
]);
@@ -321,21 +318,21 @@ describe('main/views/viewManager', () => {
it('should remove unused views', () => {
const view = {
name: 'tab1',
tab: {
name: 'tab1',
name: 'view1',
view: {
name: 'view1',
url: new URL('http://server1.com'),
},
destroy: jest.fn(),
};
viewManager.views.set('tab1', view);
viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{
id: 'server2',
url: new URL('http://server2.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
id: 'view1',
isOpen: false,
},
]);
@@ -360,11 +357,11 @@ describe('main/views/viewManager', () => {
jest.resetAllMocks();
});
it('should show last active tab and server', () => {
it('should show last active view and server', () => {
ServerManager.getLastActiveServer.mockReturnValue({id: 'server-1'});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-1'});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-1'});
viewManager.showInitial();
expect(viewManager.showById).toHaveBeenCalledWith('tab-1');
expect(viewManager.showById).toHaveBeenCalledWith('view-1');
});
it('should open new server modal when no servers exist', () => {
@@ -385,7 +382,7 @@ describe('main/views/viewManager', () => {
order: 0,
tabs: [
{
name: 'tab-messaging',
name: 'view-messaging',
order: 0,
isOpen: true,
},
@@ -403,9 +400,9 @@ describe('main/views/viewManager', () => {
},
];
const view1 = {
id: 'server-1_tab-messaging',
id: 'server-1_view-messaging',
isLoggedIn: true,
tab: {
view: {
type: TAB_MESSAGING,
server: {
url: 'http://server-1.com',
@@ -416,21 +413,21 @@ describe('main/views/viewManager', () => {
const view2 = {
...view1,
id: 'server-1_other_type_1',
tab: {
...view1.tab,
view: {
...view1.view,
type: 'other_type_1',
},
};
const view3 = {
...view1,
id: 'server-1_other_type_2',
tab: {
...view1.tab,
view: {
...view1.view,
type: 'other_type_2',
},
};
const views = new Map([
['server-1_tab-messaging', view1],
['server-1_view-messaging', view1],
['server-1_other_type_1', view2],
]);
const closedViews = new Map([
@@ -438,7 +435,7 @@ describe('main/views/viewManager', () => {
]);
viewManager.getView = (viewId) => views.get(viewId);
viewManager.isViewClosed = (viewId) => closedViews.has(viewId);
viewManager.openClosedTab = jest.fn();
viewManager.openClosedView = jest.fn();
beforeEach(() => {
ServerManager.getAllServers.mockReturnValue(servers);
@@ -451,24 +448,24 @@ describe('main/views/viewManager', () => {
});
it('should open closed view if pushing to it', () => {
viewManager.openClosedTab.mockImplementation((name) => {
viewManager.openClosedView.mockImplementation((name) => {
const view = closedViews.get(name);
closedViews.delete(name);
views.set(name, view);
});
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_other_type_2'});
viewManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_2/subpath');
expect(viewManager.openClosedTab).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/other_type_2/subpath');
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_2'});
viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_2/subpath');
expect(viewManager.openClosedView).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/other_type_2/subpath');
});
it('should open redirect view if different from current view', () => {
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_other_type_1'});
viewManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_1/subpath');
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_1'});
viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_1/subpath');
expect(viewManager.showById).toBeCalledWith('server-1_other_type_1');
});
it('should ignore redirects to "/" to Messages from other tabs', () => {
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_tab-messaging'});
it('should ignore redirects to "/" to Messages from other views', () => {
ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_view-messaging'});
viewManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/');
expect(view1.sendToRenderer).not.toBeCalled();
});
@@ -487,11 +484,11 @@ describe('main/views/viewManager', () => {
send: jest.fn(),
},
},
tab: {
view: {
server: {
name: 'server-1',
},
type: 'tab-1',
type: 'view-1',
},
};
@@ -510,9 +507,9 @@ describe('main/views/viewManager', () => {
...baseView,
isVisible: true,
};
viewManager.views.set('server1-tab1', view);
viewManager.views.set('server1-view1', view);
viewManager.showById('server1-tab1');
viewManager.showById('server1-view1');
expect(viewManager.currentView).toBeUndefined();
expect(view.isReady).not.toBeCalled();
expect(view.show).not.toBeCalled();
@@ -584,7 +581,7 @@ describe('main/views/viewManager', () => {
};
beforeEach(() => {
viewManager.openClosedTab = jest.fn();
viewManager.openClosedView = jest.fn();
});
afterEach(() => {
@@ -594,7 +591,7 @@ describe('main/views/viewManager', () => {
});
it('should load URL into matching view', () => {
ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
ServerManager.lookupViewByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
const view = {...baseView};
viewManager.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
@@ -602,11 +599,11 @@ describe('main/views/viewManager', () => {
});
it('should send the URL to the view if its already loaded on a 6.0 server', () => {
ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
ServerManager.lookupViewByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
ServerManager.getRemoteInfo.mockReturnValue({serverVersion: '6.0.0'});
const view = {
...baseView,
tab: {
view: {
server: {
url: new URL('http://server-1.com'),
},
@@ -619,7 +616,7 @@ describe('main/views/viewManager', () => {
});
it('should throw error if view is missing', () => {
ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
ServerManager.lookupViewByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
const view = {...baseView};
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(view.load).not.toHaveBeenCalled();
@@ -632,11 +629,11 @@ describe('main/views/viewManager', () => {
expect(dialog.showErrorBox).toHaveBeenCalled();
});
it('should reopen closed tab if called upon', () => {
ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
it('should reopen closed view if called upon', () => {
ServerManager.lookupViewByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
viewManager.closedViews.set('view1', {});
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(viewManager.openClosedTab).toHaveBeenCalledWith('view1', 'http://server-1.com/deep/link?thing=yes');
expect(viewManager.openClosedView).toHaveBeenCalledWith('view1', 'http://server-1.com/deep/link?thing=yes');
});
});
});

View File

@@ -11,7 +11,7 @@ import {
LOAD_FAILED,
LOADSCREEN_END,
SET_ACTIVE_VIEW,
OPEN_TAB,
OPEN_VIEW,
BROWSER_HISTORY_PUSH,
UPDATE_URL_VIEW_WIDTH,
SERVERS_UPDATE,
@@ -33,7 +33,7 @@ import {Logger} from 'common/log';
import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer';
import ServerManager from 'common/servers/serverManager';
import {TabView, TAB_MESSAGING} from 'common/tabs/TabView';
import {MattermostView, TAB_MESSAGING} from 'common/views/View';
import {parseURL} from 'common/utils/url';
import {localizeMessage} from 'main/i18nManager';
@@ -41,7 +41,7 @@ import MainWindow from 'main/windows/mainWindow';
import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils';
import {MattermostView} from './MattermostView';
import {MattermostBrowserView} from './MattermostBrowserView';
import modalManager from './modalManager';
import LoadingScreen from './loadingScreen';
@@ -50,14 +50,14 @@ const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 20;
export class ViewManager {
private closedViews: Map<string, {srv: MattermostServer; tab: TabView}>;
private views: Map<string, MattermostView>;
private closedViews: Map<string, {srv: MattermostServer; view: MattermostView}>;
private views: Map<string, MattermostBrowserView>;
private currentView?: string;
private urlViewCancel?: () => void;
constructor() {
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 views on the renderer need that.
this.closedViews = new Map();
MainWindow.on(MAIN_WINDOW_CREATED, this.init);
@@ -102,23 +102,23 @@ export class ViewManager {
return this.closedViews.has(viewId);
}
showById = (tabId: string) => {
this.getViewLogger(tabId).debug('showById', tabId);
showById = (viewId: string) => {
this.getViewLogger(viewId).debug('showById', viewId);
const newView = this.views.get(tabId);
const newView = this.views.get(viewId);
if (newView) {
if (newView.isVisible) {
return;
}
let hidePrevious;
if (this.currentView && this.currentView !== tabId) {
if (this.currentView && this.currentView !== viewId) {
const previous = this.getCurrentView();
if (previous) {
hidePrevious = () => previous.hide();
}
}
this.currentView = tabId;
this.currentView = viewId;
if (!newView.isErrored()) {
newView.show();
if (newView.needsLoadingScreen()) {
@@ -126,10 +126,10 @@ export class ViewManager {
}
}
hidePrevious?.();
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.id, newView.tab.id);
ServerManager.updateLastActive(newView.tab.id);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.view.server.id, newView.view.id);
ServerManager.updateLastActive(newView.view.id);
} else {
this.getViewLogger(tabId).warn(`Couldn't find a view with name: ${tabId}`);
this.getViewLogger(viewId).warn(`Couldn't find a view with name: ${viewId}`);
}
modalManager.showModal();
}
@@ -175,28 +175,28 @@ export class ViewManager {
handleDeepLink = (url: string | URL) => {
if (url) {
const parsedURL = parseURL(url)!;
const tabView = ServerManager.lookupTabByURL(parsedURL, true);
if (tabView) {
const urlWithSchema = `${tabView.url.origin}${parsedURL.pathname}${parsedURL.search}`;
if (this.closedViews.has(tabView.id)) {
this.openClosedTab(tabView.id, urlWithSchema);
const view = ServerManager.lookupViewByURL(parsedURL, true);
if (view) {
const urlWithSchema = `${view.url.origin}${parsedURL.pathname}${parsedURL.search}`;
if (this.closedViews.has(view.id)) {
this.openClosedView(view.id, urlWithSchema);
} else {
const view = this.views.get(tabView.id);
if (!view) {
log.error(`Couldn't find a view matching the id ${tabView.id}`);
const browserView = this.views.get(view.id);
if (!browserView) {
log.error(`Couldn't find a view matching the id ${view.id}`);
return;
}
if (view.isReady() && ServerManager.getRemoteInfo(view.tab.server.id)?.serverVersion && Utils.isVersionGreaterThanOrEqualTo(ServerManager.getRemoteInfo(view.tab.server.id)?.serverVersion ?? '', '6.0.0')) {
const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`;
view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(view.id);
if (browserView.isReady() && ServerManager.getRemoteInfo(browserView.view.server.id)?.serverVersion && Utils.isVersionGreaterThanOrEqualTo(ServerManager.getRemoteInfo(browserView.view.server.id)?.serverVersion ?? '', '6.0.0')) {
const pathName = `/${urlWithSchema.replace(browserView.view.server.url.toString(), '')}`;
browserView.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(browserView.id);
} else {
// attempting to change parsedURL protocol results in it not being modified.
view.resetLoadingStatus();
view.load(urlWithSchema);
view.once(LOAD_SUCCESS, this.deeplinkSuccess);
view.once(LOAD_FAILED, this.deeplinkFailed);
browserView.resetLoadingStatus();
browserView.load(urlWithSchema);
browserView.once(LOAD_SUCCESS, this.deeplinkSuccess);
browserView.once(LOAD_FAILED, this.deeplinkFailed);
}
}
} else {
@@ -225,35 +225,35 @@ export class ViewManager {
*/
private loadServer = (server: MattermostServer) => {
const tabs = ServerManager.getOrderedTabsForServer(server.id);
tabs.forEach((tab) => this.loadView(server, tab));
const views = ServerManager.getOrderedTabsForServer(server.id);
views.forEach((view) => this.loadView(server, view));
}
private loadView = (srv: MattermostServer, tab: TabView, url?: string) => {
if (!tab.isOpen) {
this.closedViews.set(tab.id, {srv, tab});
private loadView = (srv: MattermostServer, view: MattermostView, url?: string) => {
if (!view.isOpen) {
this.closedViews.set(view.id, {srv, view});
return;
}
const view = this.makeView(srv, tab, url);
this.addView(view);
const browserView = this.makeView(srv, view, url);
this.addView(browserView);
}
private makeView = (srv: MattermostServer, tab: TabView, url?: string): MattermostView => {
private makeView = (srv: MattermostServer, view: MattermostView, url?: string): MattermostBrowserView => {
const mainWindow = MainWindow.get();
if (!mainWindow) {
throw new Error('Cannot create view, no main window present');
}
const view = new MattermostView(tab, {webPreferences: {spellcheck: Config.useSpellChecker}});
view.once(LOAD_SUCCESS, this.activateView);
view.on(LOADSCREEN_END, this.finishLoading);
view.on(LOAD_FAILED, this.failLoading);
view.on(UPDATE_TARGET_URL, this.showURLView);
view.load(url);
return view;
const browserView = new MattermostBrowserView(view, {webPreferences: {spellcheck: Config.useSpellChecker}});
browserView.once(LOAD_SUCCESS, this.activateView);
browserView.on(LOADSCREEN_END, this.finishLoading);
browserView.on(LOAD_FAILED, this.failLoading);
browserView.on(UPDATE_TARGET_URL, this.showURLView);
browserView.load(url);
return browserView;
}
private addView = (view: MattermostView): void => {
private addView = (view: MattermostBrowserView): void => {
this.views.set(view.id, view);
if (this.closedViews.has(view.id)) {
this.closedViews.delete(view.id);
@@ -265,8 +265,8 @@ export class ViewManager {
if (ServerManager.hasServers()) {
const lastActiveServer = ServerManager.getCurrentServer();
const lastActiveTab = ServerManager.getLastActiveTabForServer(lastActiveServer.id);
this.showById(lastActiveTab.id);
const lastActiveView = ServerManager.getLastActiveTabForServer(lastActiveServer.id);
this.showById(lastActiveView.id);
} else {
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW);
}
@@ -382,33 +382,33 @@ export class ViewManager {
*/
/** Called when a new configuration is received
* Servers or tabs have been added or edited. We need to
* close, open, or reload tabs, taking care to reuse tabs and
* preserve focus on the currently selected tab. */
* Servers or views have been added or edited. We need to
* close, open, or reload views, taking care to reuse views and
* preserve focus on the currently selected view. */
private handleReloadConfiguration = () => {
log.debug('handleReloadConfiguration');
const currentTabId: string | undefined = this.views.get(this.currentView as string)?.tab.id;
const currentViewId: string | undefined = this.views.get(this.currentView as string)?.view.id;
const current: Map<string, MattermostView> = new Map();
const current: Map<string, MattermostBrowserView> = new Map();
for (const view of this.views.values()) {
current.set(view.tab.id, view);
current.set(view.view.id, view);
}
const views: Map<string, MattermostView> = new Map();
const closed: Map<string, {srv: MattermostServer; tab: TabView}> = new Map();
const views: Map<string, MattermostBrowserView> = new Map();
const closed: Map<string, {srv: MattermostServer; view: MattermostView}> = new Map();
const sortedTabs = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id).
map((t): [MattermostServer, TabView] => [x, t]));
const sortedViews = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id).
map((t): [MattermostServer, MattermostView] => [x, t]));
for (const [srv, tab] of sortedTabs) {
const recycle = current.get(tab.id);
if (!tab.isOpen) {
closed.set(tab.id, {srv, tab});
for (const [srv, view] of sortedViews) {
const recycle = current.get(view.id);
if (!view.isOpen) {
closed.set(view.id, {srv, view});
} else if (recycle) {
views.set(tab.id, recycle);
views.set(view.id, recycle);
} else {
views.set(tab.id, this.makeView(srv, tab));
views.set(view.id, this.makeView(srv, view));
}
}
@@ -428,10 +428,10 @@ export class ViewManager {
// commit closed
for (const x of closed.values()) {
this.closedViews.set(x.tab.id, {srv: x.srv, tab: x.tab});
this.closedViews.set(x.view.id, {srv: x.srv, view: x.view});
}
if ((currentTabId && closed.has(currentTabId)) || (this.currentView && this.closedViews.has(this.currentView))) {
if ((currentViewId && closed.has(currentViewId)) || (this.currentView && this.closedViews.has(this.currentView))) {
if (ServerManager.hasServers()) {
this.currentView = undefined;
this.showInitial();
@@ -440,13 +440,13 @@ export class ViewManager {
}
}
// show the focused tab (or initial)
if (currentTabId && views.has(currentTabId)) {
const view = views.get(currentTabId);
// show the focused view (or initial)
if (currentViewId && views.has(currentViewId)) {
const view = views.get(currentViewId);
if (view && view.id !== this.currentView) {
this.currentView = view.id;
this.showById(view.id);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.id, view.tab.id);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.view.server.id, view.view.id);
} else {
this.focusCurrentView();
}
@@ -475,17 +475,17 @@ export class ViewManager {
return;
}
let cleanedPathName = pathName;
if (currentView.tab.server.url.pathname !== '/' && pathName.startsWith(currentView.tab.server.url.pathname)) {
cleanedPathName = pathName.replace(currentView.tab.server.url.pathname, '');
if (currentView.view.server.url.pathname !== '/' && pathName.startsWith(currentView.view.server.url.pathname)) {
cleanedPathName = pathName.replace(currentView.view.server.url.pathname, '');
}
const redirectedviewId = ServerManager.lookupTabByURL(`${currentView.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId;
const redirectedviewId = ServerManager.lookupViewByURL(`${currentView.view.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId;
if (this.isViewClosed(redirectedviewId)) {
// If it's a closed view, just open it and stop
this.openClosedTab(redirectedviewId, `${currentView.tab.server.url}${cleanedPathName}`);
this.openClosedView(redirectedviewId, `${currentView.view.server.url}${cleanedPathName}`);
return;
}
let redirectedView = this.getView(redirectedviewId) || currentView;
if (redirectedView !== currentView && redirectedView?.tab.server.id === ServerManager.getCurrentServer().id && redirectedView?.isLoggedIn) {
if (redirectedView !== currentView && redirectedView?.view.server.id === ServerManager.getCurrentServer().id && redirectedView?.isLoggedIn) {
log.info('redirecting to a new view', redirectedView?.id || viewId);
this.showById(redirectedView?.id || viewId);
} else {
@@ -493,7 +493,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 === '/')) {
if (!(redirectedView !== currentView && redirectedView?.view.type === TAB_MESSAGING && cleanedPathName === '/')) {
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
if (redirectedView) {
this.handleBrowserHistoryButton(e, redirectedView.id);
@@ -547,7 +547,7 @@ export class ViewManager {
const currentView = this.getCurrentView();
if (currentView && currentView.currentURL) {
const adjustedBounds = getAdjustedWindowBoundaries(newBounds.width, newBounds.height, shouldHaveBackBar(currentView.tab.url, currentView.currentURL));
const adjustedBounds = getAdjustedWindowBoundaries(newBounds.width, newBounds.height, shouldHaveBackBar(currentView.view.url, currentView.currentURL));
currentView.setBounds(adjustedBounds);
}
}
@@ -556,21 +556,21 @@ export class ViewManager {
* Helper functions
*/
private openClosedTab = (id: string, url?: string) => {
private openClosedView = (id: string, url?: string) => {
if (!this.closedViews.has(id)) {
return;
}
const {srv, tab} = this.closedViews.get(id)!;
tab.isOpen = true;
this.loadView(srv, tab, url);
const {srv, view} = this.closedViews.get(id)!;
view.isOpen = true;
this.loadView(srv, view, url);
this.showById(id);
const view = this.views.get(id)!;
view.isVisible = true;
view.on(LOAD_SUCCESS, () => {
view.isVisible = false;
const browserView = this.views.get(id)!;
browserView.isVisible = true;
browserView.on(LOAD_SUCCESS, () => {
browserView.isVisible = false;
this.showById(id);
});
ipcMain.emit(OPEN_TAB, null, tab.id);
ipcMain.emit(OPEN_VIEW, null, view.id);
}
private getViewLogger = (viewId: string) => {
@@ -585,8 +585,8 @@ export class ViewManager {
return {
id: view.id,
webContentsId: view.webContentsId,
serverName: view.tab.server.name,
tabType: view.tab.type,
serverName: view.view.server.name,
viewType: view.view.type,
};
}
}

View File

@@ -81,7 +81,7 @@ export class WebContentsEventManager {
return CallsWidgetWindow.getURL();
}
return ViewManager.getViewByWebContentsId(webContentsId)?.tab.server.url;
return ViewManager.getViewByWebContentsId(webContentsId)?.view.server.url;
}
private generateWillNavigate = (webContentsId: number) => {
@@ -274,7 +274,7 @@ export class WebContentsEventManager {
return {action: 'deny'};
}
const otherServerURL = ServerManager.lookupTabByURL(parsedURL);
const otherServerURL = ServerManager.lookupViewByURL(parsedURL);
if (otherServerURL && isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
ViewManager.handleDeepLink(parsedURL);
return {action: 'deny'};