[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
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
110 changed files with 1698 additions and 1757 deletions

View file

@ -26,7 +26,8 @@
"no-var": 2,
"react/no-find-dom-node": 2,
"multiline-ternary": 0,
"max-lines": ["warn", 650]
"max-lines": ["warn", 650],
"no-underscore-dangle": ["warn"]
},
"overrides": [
{
@ -120,7 +121,7 @@
"src/renderer/components/Button/Button.stories.tsx",
"src/renderer/components/TeamList.tsx",
"src/renderer/components/LoginModal.tsx",
"src/renderer/components/NewTeamModal.tsx",
"src/renderer/components/NewServerModal.tsx",
"src/renderer/settings.tsx",
"src/renderer/index.tsx"
],

View file

@ -11,7 +11,7 @@ Originally created as "electron-mattermost" by Yuya Ochiai.
## Features
### Desktop integration
* Server dropdown for access to multiple teams
* Server dropdown for access to multiple servers
* Dedicated tabs for Channels, Boards and Playbooks
* Desktop Notifications
* Badges for unread channels and mentions

View file

@ -26,7 +26,7 @@ To contribute to the process of testing the Mattermost Desktop App:
- Post a message with information on what you're testing, for example: `Testing Mattermost Desktop App 5.0.2 on Windows 10 64-bit`.
- Reply to the post by clicking on "..." then "Reply" with This is a comment including files and upload five (5) files including at least one image, one sound file and one video clip from your Desktop App.
- Search for the word "Desktop" and click "Jump" on the search result of your own post in Step 3.1. Click into the preview of the files you uploaded and try to download each one.
- Verify [Team Management works as documented](https://docs.mattermost.com/messaging/managing-desktop-app-servers.html).
- Verify [Server Management works as documented](https://docs.mattermost.com/messaging/managing-desktop-app-servers.html).
- Verify [App Options work as documented](https://docs.mattermost.com/messaging/managing-desktop-app-options.html).
- Verify Menu Bar options work as documented.
- Use the desktop app for another 15 minutes, trying different features and functionality on the user interface.

View file

@ -40,7 +40,7 @@ if (process.platform === 'win32') {
robot.mouseClick();
}
const exampleTeam = {
const exampleServer = {
name: 'example',
url: exampleURL,
order: 0,
@ -61,7 +61,7 @@ const exampleTeam = {
],
lastActiveTab: 0,
};
const githubTeam = {
const githubServer = {
name: 'github',
url: 'https://github.com/',
order: 1,
@ -87,7 +87,7 @@ const githubTeam = {
const demoConfig = {
version: 3,
teams: [exampleTeam, githubTeam],
teams: [exampleServer, githubServer],
showTrayIcon: false,
trayIconTheme: 'light',
minimizeToTray: false,
@ -113,9 +113,9 @@ const demoConfig = {
const demoMattermostConfig = {
...demoConfig,
teams: [{
...exampleTeam,
...exampleServer,
url: mattermostURL,
}, githubTeam],
}, githubServer],
};
const cmdOrCtrl = process.platform === 'darwin' ? 'command' : 'control';
@ -235,7 +235,7 @@ module.exports = {
return null;
}
const info = await window.testHelper.getViewInfoForTest();
return {viewName: `${info.serverName}___${info.tabType}`, webContentsId: info.webContentsId};
return {viewName: `${info.serverName}___${info.viewType}`, webContentsId: info.webContentsId};
}).then((result) => {
if (result) {
map[result.viewName] = {win, webContentsId: result.webContentsId};

View file

@ -46,7 +46,7 @@ describe('application', function desc() {
return window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getURL();
}, webContentsId);
isActive.should.equal('https://github.com/test/url');
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
dropdownButtonText.should.equal('github');
await this.app.close();
});

View file

@ -98,8 +98,8 @@ describe('focus', function desc() {
it('MM-T1316 should return focus to the message box when closing the settings window', async () => {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer');
await mainView.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
const newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'),
});
@ -121,15 +121,15 @@ describe('focus', function desc() {
it('MM-T1317 should return focus to the focused box when switching servers', async () => {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:has-text("community")');
await mainView.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
// eslint-disable-next-line dot-notation
const secondServer = this.serverMap['community___TAB_MESSAGING'].win;
await secondServer.waitForSelector('#input_loginId');
await secondServer.focus('#input_loginId');
await mainView.click('.TeamDropdownButton');
await dropdownView.click(`.TeamDropdown .TeamDropdown__button:has-text("${config.teams[0].name}")`);
await mainView.click('.ServerDropdownButton');
await dropdownView.click(`.ServerDropdown .ServerDropdown__button:has-text("${config.teams[0].name}")`);
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
isTextboxFocused.should.be.true;
@ -141,8 +141,8 @@ describe('focus', function desc() {
const textboxString = await firstServer.inputValue('#post_textbox');
textboxString.should.equal('Mattermost');
await mainView.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:has-text("community")');
await mainView.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
const isLoginFocused = await secondServer.$eval('#input_loginId', (el) => el === document.activeElement);
isLoginFocused.should.be.true;

View file

@ -32,9 +32,9 @@ describe('menu_bar/dropdown', function desc() {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton');
const firstMenuItem = await dropdownView.innerText('.TeamDropdown button.TeamDropdown__button:nth-child(1) span');
const secondMenuItem = await dropdownView.innerText('.TeamDropdown button.TeamDropdown__button:nth-child(2) span');
await mainWindow.click('.ServerDropdownButton');
const firstMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(1) span');
const secondMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(2) span');
firstMenuItem.should.equal(config.teams[0].name);
secondMenuItem.should.equal(config.teams[1].name);
@ -57,7 +57,7 @@ describe('menu_bar/dropdown', function desc() {
let dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.equal(0);
await mainWindow.click('.TeamDropdownButton');
await mainWindow.click('.ServerDropdownButton');
dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.be.greaterThan(0);
});
@ -74,8 +74,8 @@ describe('menu_bar/dropdown', function desc() {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown__button.addServer');
await mainWindow.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown__button.addServer');
const newServerModal = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'),
@ -107,8 +107,8 @@ describe('menu_bar/dropdown', function desc() {
});
it('MM-T4408_2 should show the second view after clicking the menu item', async () => {
await mainWindow.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown button.TeamDropdown__button:nth-child(2)');
await mainWindow.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown button.ServerDropdown__button:nth-child(2)');
const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
firstViewIsAttached.should.be.false;

View file

@ -72,23 +72,23 @@ describe('Menu/window_menu', function desc() {
after(afterFunc);
it('MM-T826_1 should show the second server', async () => {
let dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton');
let dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
dropdownButtonText.should.equal('google');
robot.keyTap('2', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton:has-text("github")');
dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("github")');
dropdownButtonText.should.equal('github');
});
it('MM-T826_2 should show the third server', async () => {
robot.keyTap('3', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton:has-text("google")');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("google")');
dropdownButtonText.should.equal('google');
});
it('MM-T826_3 should show the first server', async () => {
robot.keyTap('1', ['control', process.platform === 'darwin' ? 'command' : 'shift']);
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton:has-text("example")');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("example")');
dropdownButtonText.should.equal('example');
});
});

View file

@ -21,8 +21,8 @@ describe('Add Server Modal', function desc() {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer');
await mainView.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'),
});
@ -41,7 +41,7 @@ describe('Add Server Modal', function desc() {
let newServerView;
it('MM-T1312 should focus the first text input', async () => {
const isFocused = await newServerView.$eval('#teamUrlInput', (el) => el.isSameNode(document.activeElement));
const isFocused = await newServerView.$eval('#serverUrlInput', (el) => el.isSameNode(document.activeElement));
isFocused.should.be.true;
});
@ -53,39 +53,39 @@ describe('Add Server Modal', function desc() {
});
describe('MM-T4389 Invalid messages', () => {
it('MM-T4389_1 should not be valid if no team name or URL has been set', async () => {
it('MM-T4389_1 should not be valid if no server name or URL has been set', async () => {
await newServerView.click('#saveNewServerModal');
const existingName = await newServerView.isVisible('#teamNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid');
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
existingName.should.be.true;
existingUrl.should.be.true;
});
it('should not be valid if a server with the same name exists', async () => {
await newServerView.type('#teamNameInput', config.teams[0].name);
await newServerView.type('#teamUrlInput', 'http://example.org');
await newServerView.type('#serverNameInput', config.teams[0].name);
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamNameInput.is-invalid');
const existing = await newServerView.isVisible('#serverNameInput.is-invalid');
existing.should.be.true;
});
it('should not be valid if a server with the same URL exists', async () => {
await newServerView.type('#teamNameInput', 'some-new-server');
await newServerView.type('#teamUrlInput', config.teams[0].url);
await newServerView.type('#serverNameInput', 'some-new-server');
await newServerView.type('#serverUrlInput', config.teams[0].url);
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamUrlInput.is-invalid');
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true;
});
describe('Valid server name', async () => {
beforeEach(async () => {
await newServerView.type('#teamNameInput', 'TestTeam');
await newServerView.type('#serverNameInput', 'TestServer');
await newServerView.click('#saveNewServerModal');
});
it('MM-T4389_2 Name should not be marked invalid, URL should be marked invalid', async () => {
const existingName = await newServerView.isVisible('#teamNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid');
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.false;
existingUrl.should.be.true;
@ -95,13 +95,13 @@ describe('Add Server Modal', function desc() {
describe('Valid server url', () => {
beforeEach(async () => {
await newServerView.type('#teamUrlInput', 'http://example.org');
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal');
});
it('MM-T4389_3 URL should not be marked invalid, name should be marked invalid', async () => {
const existingName = await newServerView.isVisible('#teamNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid');
const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.true;
existingUrl.should.be.false;
@ -111,16 +111,16 @@ describe('Add Server Modal', function desc() {
});
it('MM-T2826_1 should not be valid if an invalid server address has been set', async () => {
await newServerView.type('#teamUrlInput', 'superInvalid url');
await newServerView.type('#serverUrlInput', 'superInvalid url');
await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamUrlInput.is-invalid');
const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true;
});
describe('Valid Team Settings', () => {
beforeEach(async () => {
await newServerView.type('#teamUrlInput', 'http://example.org');
await newServerView.type('#teamNameInput', 'TestTeam');
await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.type('#serverNameInput', 'TestServer');
});
it('should be possible to click add', async () => {
@ -128,7 +128,7 @@ describe('Add Server Modal', function desc() {
(disabled === null).should.be.true;
});
it('MM-T2826_2 should add the team to the config file', async () => {
it('MM-T2826_2 should add the server to the config file', async () => {
await newServerView.click('#saveNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('newServer')));
@ -136,7 +136,7 @@ describe('Add Server Modal', function desc() {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({
name: 'TestTeam',
name: 'TestServer',
url: 'http://example.org/',
order: 2,
lastActiveTab: 0,

View file

@ -48,7 +48,7 @@ describe('Configure Server Modal', function desc() {
});
it('MM-T5117 should be valid if display name and URL are set', async () => {
await configureServerModal.type('#input_name', 'TestTeam');
await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'http://example.org');
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled');
@ -56,7 +56,7 @@ describe('Configure Server Modal', function desc() {
});
it('MM-T5118 should not be valid if an invalid URL has been set', async () => {
await configureServerModal.type('#input_name', 'TestTeam');
await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'lorem.ipsum.dolor.sit.amet');
await configureServerModal.click('#connectConfigureServer');
@ -70,8 +70,8 @@ describe('Configure Server Modal', function desc() {
(connectButtonDisabled === '').should.be.true;
});
it('MM-T5119 should add the team to the config file', async () => {
await configureServerModal.type('#input_name', 'TestTeam');
it('MM-T5119 should add the server to the config file', async () => {
await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'http://example.org');
await configureServerModal.click('#connectConfigureServer');
@ -84,7 +84,7 @@ describe('Configure Server Modal', function desc() {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({
url: 'http://example.org/',
name: 'TestTeam',
name: 'TestServer',
order: 0,
lastActiveTab: 0,
tabs: [

View file

@ -64,41 +64,41 @@ describe('server_management/drag_and_drop', function desc() {
await beforeFunc();
mainWindow = this.app.windows().find((window) => window.url().includes('index'));
dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton');
await mainWindow.click('.ServerDropdownButton');
});
after(afterFunc);
it('MM-T2634_1 should appear the original order', async () => {
const firstMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(1) .TeamDropdown__draggable-handle');
const firstMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
const firstMenuItemText = await firstMenuItem.innerText();
firstMenuItemText.should.equal('example');
const secondMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(2) .TeamDropdown__draggable-handle');
const secondMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(2) .ServerDropdown__draggable-handle');
const secondMenuItemText = await secondMenuItem.innerText();
secondMenuItemText.should.equal('github');
const thirdMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(3) .TeamDropdown__draggable-handle');
const thirdMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(3) .ServerDropdown__draggable-handle');
const thirdMenuItemText = await thirdMenuItem.innerText();
thirdMenuItemText.should.equal('google');
});
it('MM-T2634_2 after dragging the server down, should appear in the new order', async () => {
// Move the first server down, then re-open the dropdown
const initialMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(1) .TeamDropdown__draggable-handle');
const initialMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
await initialMenuItem.focus();
await dropdownView.keyboard.down(' ');
await dropdownView.keyboard.down('ArrowDown');
await dropdownView.keyboard.down(' ');
await asyncSleep(1000);
await mainWindow.keyboard.press('Escape');
await mainWindow.click('.TeamDropdownButton');
await mainWindow.click('.ServerDropdownButton');
// Verify that the new order persists
const firstMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(1) .TeamDropdown__draggable-handle');
const firstMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(1) .ServerDropdown__draggable-handle');
const firstMenuItemText = await firstMenuItem.innerText();
firstMenuItemText.should.equal('github');
const secondMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(2) .TeamDropdown__draggable-handle');
const secondMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(2) .ServerDropdown__draggable-handle');
const secondMenuItemText = await secondMenuItem.innerText();
secondMenuItemText.should.equal('example');
const thirdMenuItem = await dropdownView.waitForSelector('.TeamDropdown button.TeamDropdown__button:nth-child(3) .TeamDropdown__draggable-handle');
const thirdMenuItem = await dropdownView.waitForSelector('.ServerDropdown button.ServerDropdown__button:nth-child(3) .ServerDropdown__draggable-handle');
const thirdMenuItemText = await thirdMenuItem.innerText();
thirdMenuItemText.should.equal('google');
});
@ -125,20 +125,20 @@ describe('server_management/drag_and_drop', function desc() {
it('MM-T2635_1 should be in the original order', async () => {
// Verify the original order
const firstTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(1)');
const firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
const firstTabText = await firstTab.innerText();
firstTabText.should.equal('Channels');
const secondTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(2)');
const secondTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(2)');
const secondTabText = await secondTab.innerText();
secondTabText.should.equal('Boards');
const thirdTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(3)');
const thirdTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(3)');
const thirdTabText = await thirdTab.innerText();
thirdTabText.should.equal('Playbooks');
});
it('MM-T2635_2 after moving the tab to the right, the tab should be in the new order', async () => {
// Move the first tab to the right
let firstTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(1)');
let firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
await firstTab.focus();
await mainWindow.keyboard.down(' ');
await mainWindow.keyboard.down('ArrowRight');
@ -146,13 +146,13 @@ describe('server_management/drag_and_drop', function desc() {
await asyncSleep(1000);
// Verify that the new order is visible
firstTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(1)');
firstTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(1)');
const firstTabText = await firstTab.innerText();
firstTabText.should.equal('Boards');
const secondTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(2)');
const secondTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(2)');
const secondTabText = await secondTab.innerText();
secondTabText.should.equal('Channels');
const thirdTab = await mainWindow.waitForSelector('.TabBar li.teamTabItem:nth-child(3)');
const thirdTab = await mainWindow.waitForSelector('.TabBar li.serverTabItem:nth-child(3)');
const thirdTabText = await thirdTab.innerText();
thirdTabText.should.equal('Playbooks');
});

View file

@ -21,9 +21,9 @@ describe('EditServerModal', function desc() {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.hover('.TeamDropdown .TeamDropdown__button:nth-child(1)');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:nth-child(1) button.TeamDropdown__button-edit');
await mainView.click('.ServerDropdownButton');
await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-edit');
editServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('editServer'),
@ -39,7 +39,7 @@ describe('EditServerModal', function desc() {
let editServerView;
it('should not edit team when Cancel is pressed', async () => {
it('should not edit server when Cancel is pressed', async () => {
await editServerView.click('#cancelNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -69,7 +69,7 @@ describe('EditServerModal', function desc() {
});
});
it('MM-T4391_1 should not edit team when Save is pressed but nothing edited', async () => {
it('MM-T4391_1 should not edit server when Save is pressed but nothing edited', async () => {
await editServerView.click('#saveNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -99,27 +99,27 @@ describe('EditServerModal', function desc() {
});
});
it('MM-T2826_3 should not edit team if an invalid server address has been set', async () => {
await editServerView.type('#teamUrlInput', 'superInvalid url');
it('MM-T2826_3 should not edit server if an invalid server address has been set', async () => {
await editServerView.type('#serverUrlInput', 'superInvalid url');
await editServerView.click('#saveNewServerModal');
const existing = await editServerView.isVisible('#teamUrlInput.is-invalid');
const existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true;
});
it('should not edit team if another server with the same name or URL exists', async () => {
await editServerView.fill('#teamNameInput', config.teams[1].name);
it('should not edit server if another server with the same name or URL exists', async () => {
await editServerView.fill('#serverNameInput', config.teams[1].name);
await editServerView.click('#saveNewServerModal');
let existing = await editServerView.isVisible('#teamNameInput.is-invalid');
let existing = await editServerView.isVisible('#serverNameInput.is-invalid');
existing.should.be.true;
await editServerView.fill('#teamNameInput', 'NewTestTeam');
await editServerView.fill('#teamUrlInput', config.teams[1].url);
existing = await editServerView.isVisible('#teamUrlInput.is-invalid');
await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.fill('#serverUrlInput', config.teams[1].url);
existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true;
});
it('MM-T4391_2 should edit team when Save is pressed and name edited', async () => {
await editServerView.fill('#teamNameInput', 'NewTestTeam');
it('MM-T4391_2 should edit server when Save is pressed and name edited', async () => {
await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.click('#saveNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -148,7 +148,7 @@ describe('EditServerModal', function desc() {
lastActiveTab: 0,
});
savedConfig.teams.should.deep.contain({
name: 'NewTestTeam',
name: 'NewTestServer',
url: env.exampleURL,
order: 0,
tabs: [
@ -170,8 +170,8 @@ describe('EditServerModal', function desc() {
});
});
it('MM-T4391_3 should edit team when Save is pressed and URL edited', async () => {
await editServerView.fill('#teamUrlInput', 'http://google.com');
it('MM-T4391_3 should edit server when Save is pressed and URL edited', async () => {
await editServerView.fill('#serverUrlInput', 'http://google.com');
await editServerView.click('#saveNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -222,9 +222,9 @@ describe('EditServerModal', function desc() {
});
});
it('MM-T4391_4 should edit team when Save is pressed and both edited', async () => {
await editServerView.fill('#teamNameInput', 'NewTestTeam');
await editServerView.fill('#teamUrlInput', 'http://google.com');
it('MM-T4391_4 should edit server when Save is pressed and both edited', async () => {
await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.fill('#serverUrlInput', 'http://google.com');
await editServerView.click('#saveNewServerModal');
await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -253,7 +253,7 @@ describe('EditServerModal', function desc() {
lastActiveTab: 0,
});
savedConfig.teams.should.deep.contain({
name: 'NewTestTeam',
name: 'NewTestServer',
url: 'http://google.com/',
order: 0,
tabs: [

View file

@ -24,8 +24,8 @@ describe('LongServerName', function desc() {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer');
await mainView.click('.ServerDropdownButton');
await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'),
});
@ -44,8 +44,8 @@ describe('LongServerName', function desc() {
let newServerView;
it('MM-T4050 Long server name', async () => {
await newServerView.type('#teamNameInput', longServerName);
await newServerView.type('#teamUrlInput', longServerUrl);
await newServerView.type('#serverNameInput', longServerName);
await newServerView.type('#serverUrlInput', longServerUrl);
await newServerView.click('#saveNewServerModal');
await asyncSleep(1000);

View file

@ -21,9 +21,9 @@ describe('RemoveServerModal', function desc() {
const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton');
await dropdownView.hover('.TeamDropdown .TeamDropdown__button:nth-child(1)');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:nth-child(1) button.TeamDropdown__button-remove');
await mainView.click('.ServerDropdownButton');
await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-remove');
removeServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('removeServer'),
@ -42,7 +42,7 @@ describe('RemoveServerModal', function desc() {
let removeServerView;
it('MM-T4390_1 should remove existing team on click Remove', async () => {
it('MM-T4390_1 should remove existing server on click Remove', async () => {
await removeServerView.click('button:has-text("Remove")');
await asyncSleep(1000);
@ -55,7 +55,7 @@ describe('RemoveServerModal', function desc() {
savedConfig.teams.should.deep.equal(expectedConfig);
});
it('MM-T4390_2 should NOT remove existing team on click Cancel', async () => {
it('MM-T4390_2 should NOT remove existing server on click Cancel', async () => {
await removeServerView.click('button:has-text("Cancel")');
await asyncSleep(1000);

View file

@ -44,7 +44,7 @@ describe('config', function desc() {
it('MM-T4401_1 should show correct server in the dropdown button', async () => {
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
dropdownButtonText.should.equal('example');
});
@ -66,8 +66,8 @@ describe('config', function desc() {
fs.writeFileSync(env.configFilePath, JSON.stringify(oldConfig));
this.app = await env.getApp();
const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton:has-text("Primary team")');
dropdownButtonText.should.equal('Primary team');
const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("Primary server")');
dropdownButtonText.should.equal('Primary server');
const str = fs.readFileSync(env.configFilePath, 'utf8');
const upgradedConfig = JSON.parse(str);

View file

@ -133,7 +133,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Zertifikatsinformation",
"renderer.dropdown.servers": "Server",
"renderer.dropdown.addAServer": "Server hinzufügen",
"renderer.components.teamDropdownButton.noServersConfigured": "Keine Server konfiguriert",
"renderer.components.serverDropdownButton.noServersConfigured": "Keine Server konfiguriert",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Seriennummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Öffentlicher Schlüssel Info",
@ -201,18 +201,18 @@
"renderer.components.removeServerModal.title": "Server entfernen",
"renderer.components.removeServerModal.confirm": "Bestätige, dass du den {serverName} Server entfernen möchtest?",
"renderer.components.removeServerModal.body": "Dies wird den Server aus deiner Desktop-App entfernen aber keine seiner Daten löschen - Du kannst den Server jederzeit wieder zur App hinzufügen.",
"renderer.components.newTeamModal.title.edit": "Server bearbeiten",
"renderer.components.newTeamModal.title.add": "Server hinzufügen",
"renderer.components.newTeamModal.serverURL.description": "Die URL deines Mattermost Servers. Muss mit http:// oder https:// beginnen.",
"renderer.components.newTeamModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Der Name des Server, der auf der Desktop App Tab-Leiste angezeigt wird.",
"renderer.components.newTeamModal.serverDisplayName": "Server-Anzeigename",
"renderer.components.newTeamModal.error.urlRequired": "URL wird benötigt.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL sollte mit http:// oder https:// beginnen.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Die URL ist nicht korrekt formatiert.",
"renderer.components.newTeamModal.error.serverUrlExists": "Ein Server mit der gleichen URL existiert schon.",
"renderer.components.newTeamModal.error.serverNameExists": "Ein Server mit diesem Namen existiert schon.",
"renderer.components.newTeamModal.error.nameRequired": "Name wird benötigt.",
"renderer.components.newServerModal.title.edit": "Server bearbeiten",
"renderer.components.newServerModal.title.add": "Server hinzufügen",
"renderer.components.newServerModal.serverURL.description": "Die URL deines Mattermost Servers. Muss mit http:// oder https:// beginnen.",
"renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newServerModal.serverDisplayName.description": "Der Name des Server, der auf der Desktop App Tab-Leiste angezeigt wird.",
"renderer.components.newServerModal.serverDisplayName": "Server-Anzeigename",
"renderer.components.newServerModal.error.urlRequired": "URL wird benötigt.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL sollte mit http:// oder https:// beginnen.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Die URL ist nicht korrekt formatiert.",
"renderer.components.newServerModal.error.serverUrlExists": "Ein Server mit der gleichen URL existiert schon.",
"renderer.components.newServerModal.error.serverNameExists": "Ein Server mit diesem Namen existiert schon.",
"renderer.components.newServerModal.error.nameRequired": "Name wird benötigt.",
"renderer.components.mainPage.updateReady": "Update bereit zur Installation",
"renderer.components.mainPage.updateAvailable": "Update verfügbar",
"renderer.components.mainPage.downloadingUpdate": "Lade Update runter. {percentDone}% von {total} @ {speed}/s",

View file

@ -138,23 +138,24 @@
"renderer.components.input.required": "This field is required",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newTeamModal.error.urlRequired": "URL is required.",
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name",
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newTeamModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newTeamModal.title.add": "Add Server",
"renderer.components.newTeamModal.title.edit": "Edit Server",
"renderer.components.newServerModal.error.nameRequired": "Name is required.",
"renderer.components.newServerModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newServerModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newServerModal.error.urlRequired": "URL is required.",
"renderer.components.newServerModal.serverDisplayName": "Server Display Name",
"renderer.components.newServerModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newServerModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newServerModal.title.add": "Add Server",
"renderer.components.newServerModal.title.edit": "Edit Server",
"renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.",
"renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove Server",
"renderer.components.saveButton.save": "Save",
"renderer.components.saveButton.saving": "Saving",
"renderer.components.serverDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.settingsPage.afterRestart": "Setting takes effect after restarting the app.",
"renderer.components.settingsPage.appLanguage": "Set app language (beta)",
"renderer.components.settingsPage.appLanguage.description": "Chooses the language that the Desktop App will use for menu items and popups. Still in beta, some languages will be missing translation strings.",
@ -219,7 +220,6 @@
"renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info",
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.welcomeScreen.button.getStarted": "Get Started",
"renderer.components.welcomeScreen.slides.boards.subtitle": "Ship on time, every time, with a project and task management solution built for digital operations.",
"renderer.components.welcomeScreen.slides.boards.title": "Boards",

View file

@ -56,7 +56,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",
"renderer.dropdown.servers": "Servers",
"renderer.dropdown.addAServer": "Add a server",
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.serverDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info",
@ -124,18 +124,18 @@
"renderer.components.removeServerModal.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove Server",
"renderer.components.removeServerModal.body": "This will remove the server from your Desktop App but will not delete any of its data - you can add the server back to the app at any time.",
"renderer.components.newTeamModal.title.edit": "Edit Server",
"renderer.components.newTeamModal.title.add": "Add Server",
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newTeamModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name",
"renderer.components.newTeamModal.error.urlRequired": "URL is required.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.",
"renderer.components.newServerModal.title.edit": "Edit Server",
"renderer.components.newServerModal.title.add": "Add Server",
"renderer.components.newServerModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newServerModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newServerModal.serverDisplayName": "Server Display Name",
"renderer.components.newServerModal.error.urlRequired": "URL is required.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newServerModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newServerModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newServerModal.error.nameRequired": "Name is required.",
"renderer.components.mainPage.updateReady": "Update ready to install",
"renderer.components.mainPage.updateAvailable": "Update available",
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s",

View file

@ -36,15 +36,15 @@
"common.tabs.TAB_MESSAGING": "Canales",
"common.tabs.TAB_FOCALBOARD": "Tableros",
"common.permissions.canBasicAuth": "Autenticación Web",
"renderer.components.newTeamModal.serverURL": "URL del servidor",
"renderer.components.newTeamModal.serverDisplayName.description": "El nombre del servidor tal y como aparecerá en la barra de pestañas.",
"renderer.components.newTeamModal.serverDisplayName": "Nombre del servidor",
"renderer.components.newTeamModal.error.urlRequired": "El campo de URL es obligatorio.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "La URL debería comenzar con http:// o https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "La URL no está tiene un formato correcto.",
"renderer.components.newTeamModal.error.serverUrlExists": "Ya existe un servidor con la misma URL.",
"renderer.components.newTeamModal.error.serverNameExists": "Ya existe un servidor con ese mismo nombre.",
"renderer.components.newTeamModal.error.nameRequired": "El campo nombre es obligatorio.",
"renderer.components.newServerModal.serverURL": "URL del servidor",
"renderer.components.newServerModal.serverDisplayName.description": "El nombre del servidor tal y como aparecerá en la barra de pestañas.",
"renderer.components.newServerModal.serverDisplayName": "Nombre del servidor",
"renderer.components.newServerModal.error.urlRequired": "El campo de URL es obligatorio.",
"renderer.components.newServerModal.error.urlNeedsHttp": "La URL debería comenzar con http:// o https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "La URL no está tiene un formato correcto.",
"renderer.components.newServerModal.error.serverUrlExists": "Ya existe un servidor con la misma URL.",
"renderer.components.newServerModal.error.serverNameExists": "Ya existe un servidor con ese mismo nombre.",
"renderer.components.newServerModal.error.nameRequired": "El campo nombre es obligatorio.",
"renderer.components.mainPage.updateReady": "Actualización lista para instalar",
"renderer.components.mainPage.updateAvailable": "Actualización disponible",
"renderer.components.mainPage.downloadingUpdate": "Descargando actualización. {percentDone}% de {total} @ {speed}/s",
@ -169,7 +169,7 @@
"renderer.components.welcomeScreen.slides.channels.subtitle": "Todas las comunicaciones de tu equipo en un solo sitio.<br></br>Colaboración segura a la medida de los desarrolladores.",
"renderer.components.welcomeScreen.slides.boards.title": "Boards",
"renderer.components.welcomeScreen.button.getStarted": "Comenzar",
"renderer.components.teamDropdownButton.noServersConfigured": "No hay servidores configurados",
"renderer.components.serverDropdownButton.noServersConfigured": "No hay servidores configurados",
"renderer.components.showCertificateModal.subjectName": "Nombre del firmante",
"renderer.components.showCertificateModal.serialNumber": "Número de serie",
"renderer.components.showCertificateModal.publicKeyInfo": "Información de clave pública",
@ -237,9 +237,9 @@
"renderer.components.removeServerModal.title": "Eliminar servidor",
"renderer.components.removeServerModal.confirm": "¿Deseas eliminar el servidor {serverName}?",
"renderer.components.removeServerModal.body": "Se eliminará el servidor de tu aplicación, pero no borrara sus datos - puedes volver a añadirlo en cualquier momento.",
"renderer.components.newTeamModal.title.edit": "Editar servidor",
"renderer.components.newTeamModal.title.add": "Añadir servidor",
"renderer.components.newTeamModal.serverURL.description": "La URL de tu servidor Mattermost debe comenzar con http:// o https://.",
"renderer.components.newServerModal.title.edit": "Editar servidor",
"renderer.components.newServerModal.title.add": "Añadir servidor",
"renderer.components.newServerModal.serverURL.description": "La URL de tu servidor Mattermost debe comenzar con http:// o https://.",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.configureServer.subtitle": "Configura tu primer servidor para conectar tu<br></br>hub de comunicación del equipo",
"renderer.downloadsDropdown.remaining": "restante",

View file

@ -39,7 +39,7 @@
"label.accept": "قبول",
"common.tabs.TAB_MESSAGING": "کانال‌ها",
"common.permissions.canBasicAuth": "احراز هویت تحت وب",
"renderer.components.newTeamModal.error.urlNeedsHttp": "آدرس باید با http:// یا https:// شروع شود.",
"renderer.components.newServerModal.error.urlNeedsHttp": "آدرس باید با http:// یا https:// شروع شود.",
"renderer.components.settingsPage.loggingLevel.level.warn": "اخطارها و هشدارها (warn)",
"renderer.components.settingsPage.loggingLevel.level.verbose": "تمام‌جزئیات (verbose)",
"renderer.components.settingsPage.loggingLevel.level.silly": "مختصرترین (silly)",
@ -84,7 +84,7 @@
"renderer.components.welcomeScreen.slides.channels.title": "کانال‌ها",
"renderer.components.welcomeScreen.slides.boards.title": "تابلوها",
"renderer.components.welcomeScreen.button.getStarted": "آغاز کردن",
"renderer.components.teamDropdownButton.noServersConfigured": "هیچ سروری پیکربندی نشده",
"renderer.components.serverDropdownButton.noServersConfigured": "هیچ سروری پیکربندی نشده",
"renderer.components.showCertificateModal.subjectName": "نام نهاد",
"renderer.components.showCertificateModal.serialNumber": "شماره سریال",
"renderer.components.showCertificateModal.publicKeyInfo": "اطلاعات کلید عمومی",
@ -139,16 +139,16 @@
"renderer.components.removeServerModal.title": "حذف سرور",
"renderer.components.removeServerModal.confirm": "تایید کنید که می‌خواهید سرور {serverName} را حذف کنید؟",
"renderer.components.removeServerModal.body": "این کار سرور را از برنامه میزکار شما حذف می‌کند ولی هیچ اطلاعاتی از سرور پاک نمی‌شود - هر وقتی که تصمیم گرفتید، دوباره می‌توانید همین سرور را اضافه کنید.",
"renderer.components.newTeamModal.title.edit": "ویرایش سرور",
"renderer.components.newTeamModal.title.add": "افزودن سرور",
"renderer.components.newTeamModal.serverURL.description": "آدرس سرور مترموست. حتما باید با http:// یا https:// شروع شود.",
"renderer.components.newTeamModal.serverURL": "آدرس سرور",
"renderer.components.newTeamModal.serverDisplayName.description": "نام سرور در برنامه میزکار در قسمت نوار بالایی نمایش داده می‌شود.",
"renderer.components.newTeamModal.serverDisplayName": "نام نمایشی سرور",
"renderer.components.newTeamModal.error.urlRequired": "آدرس اجباری است.",
"renderer.components.newTeamModal.error.serverUrlExists": "سروری با همین آدرس موجود است.",
"renderer.components.newTeamModal.error.serverNameExists": "سروری با همین نام موجود است.",
"renderer.components.newTeamModal.error.nameRequired": "نام اجباری است.",
"renderer.components.newServerModal.title.edit": "ویرایش سرور",
"renderer.components.newServerModal.title.add": "افزودن سرور",
"renderer.components.newServerModal.serverURL.description": "آدرس سرور مترموست. حتما باید با http:// یا https:// شروع شود.",
"renderer.components.newServerModal.serverURL": "آدرس سرور",
"renderer.components.newServerModal.serverDisplayName.description": "نام سرور در برنامه میزکار در قسمت نوار بالایی نمایش داده می‌شود.",
"renderer.components.newServerModal.serverDisplayName": "نام نمایشی سرور",
"renderer.components.newServerModal.error.urlRequired": "آدرس اجباری است.",
"renderer.components.newServerModal.error.serverUrlExists": "سروری با همین آدرس موجود است.",
"renderer.components.newServerModal.error.serverNameExists": "سروری با همین نام موجود است.",
"renderer.components.newServerModal.error.nameRequired": "نام اجباری است.",
"renderer.components.mainPage.titleBar": "مترموست",
"renderer.components.mainPage.contextMenu.ariaLabel": "فهرست دسترسی",
"renderer.components.input.required": "این قسمت اجباری است",
@ -165,7 +165,7 @@
"renderer.components.configureServer.connect.saving": "در حال اتصال…",
"renderer.components.configureServer.connect.default": "اتصال",
"main.tray.tray.mention": "در گفت‌وگویی به شما اشاره شده است",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "آدرس به درستی وارد نشده است.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "آدرس به درستی وارد نشده است.",
"renderer.components.configureServer.cardtitle": "اطلاعات سرور خود را وارد کنید",
"renderer.components.autoSaveIndicator.saving": "در حال ذخیره...",
"renderer.components.autoSaveIndicator.saved": "ذخیره شد",

View file

@ -17,7 +17,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Informations sur le certificat",
"renderer.dropdown.servers": "Serveurs",
"renderer.dropdown.addAServer": "Ajouter un serveur",
"renderer.components.teamDropdownButton.noServersConfigured": "Aucun serveur configuré",
"renderer.components.serverDropdownButton.noServersConfigured": "Aucun serveur configuré",
"renderer.components.showCertificateModal.subjectName": "Nom du sujet",
"renderer.components.showCertificateModal.serialNumber": "Numéro de série",
"renderer.components.showCertificateModal.publicKeyInfo": "Informations sur les clés publiques",
@ -85,18 +85,18 @@
"renderer.components.removeServerModal.title": "Retirer le serveur",
"renderer.components.removeServerModal.confirm": "Confirmez que vous souhaitez supprimer le serveur {serverName} ?",
"renderer.components.removeServerModal.body": "Cette opération supprime le serveur de votre application de bureau mais ne supprime aucune de ses données. Vous pouvez réintégrer le serveur dans l'application à tout moment.",
"renderer.components.newTeamModal.title.edit": "Éditer le serveur",
"renderer.components.newTeamModal.title.add": "Ajouter un serveur",
"renderer.components.newTeamModal.serverURL.description": "L'URL de votre serveur Mattermost. Doit commencer par http:// ou https://.",
"renderer.components.newTeamModal.serverURL": "URL du serveur",
"renderer.components.newTeamModal.serverDisplayName.description": "Le nom du serveur affiché dans la barre d'onglets de votre application de bureau.",
"renderer.components.newTeamModal.serverDisplayName": "Nom d'affichage du serveur",
"renderer.components.newTeamModal.error.urlRequired": "URL requise.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "L'URL doit commencer par http:// ou https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "L'URL n'est pas formatée correctement.",
"renderer.components.newTeamModal.error.serverUrlExists": "Un serveur avec la même URL existe déjà.",
"renderer.components.newTeamModal.error.serverNameExists": "Un serveur portant le même nom existe déjà.",
"renderer.components.newTeamModal.error.nameRequired": "Le nom est obligatoire.",
"renderer.components.newServerModal.title.edit": "Éditer le serveur",
"renderer.components.newServerModal.title.add": "Ajouter un serveur",
"renderer.components.newServerModal.serverURL.description": "L'URL de votre serveur Mattermost. Doit commencer par http:// ou https://.",
"renderer.components.newServerModal.serverURL": "URL du serveur",
"renderer.components.newServerModal.serverDisplayName.description": "Le nom du serveur affiché dans la barre d'onglets de votre application de bureau.",
"renderer.components.newServerModal.serverDisplayName": "Nom d'affichage du serveur",
"renderer.components.newServerModal.error.urlRequired": "URL requise.",
"renderer.components.newServerModal.error.urlNeedsHttp": "L'URL doit commencer par http:// ou https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "L'URL n'est pas formatée correctement.",
"renderer.components.newServerModal.error.serverUrlExists": "Un serveur avec la même URL existe déjà.",
"renderer.components.newServerModal.error.serverNameExists": "Un serveur portant le même nom existe déjà.",
"renderer.components.newServerModal.error.nameRequired": "Le nom est obligatoire.",
"renderer.components.mainPage.updateReady": "Mise à jour prête à être installée",
"renderer.components.mainPage.updateAvailable": "Mise à jour disponible",
"renderer.components.mainPage.downloadingUpdate": "Téléchargement de la mise à jour. {percentDone}% de {total} @ {speed}/s",

View file

@ -17,7 +17,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Tanúsítvány információk",
"renderer.dropdown.servers": "Kiszolgálók",
"renderer.dropdown.addAServer": "Kiszogáló hozzáadása",
"renderer.components.teamDropdownButton.noServersConfigured": "Nincs kiszolgáló beállítva",
"renderer.components.serverDropdownButton.noServersConfigured": "Nincs kiszolgáló beállítva",
"renderer.components.showCertificateModal.subjectName": "Tárgy neve",
"renderer.components.showCertificateModal.serialNumber": "Sorozatszám",
"renderer.components.showCertificateModal.publicKeyInfo": "Nyilvános kulcs információk",
@ -85,18 +85,18 @@
"renderer.components.removeServerModal.title": "Kiszolgáló eltávolítása",
"renderer.components.removeServerModal.confirm": "Megerősíti, hogy el kívánja távolítani a {serverName} kiszolgálót?",
"renderer.components.removeServerModal.body": "Ez eltávolítja a kiszolgálót az asztali alkalmazásból, de nem törli annak adatait - a kiszolgálót bármikor visszaadhatja az alkalmazáshoz.",
"renderer.components.newTeamModal.serverURL.description": "A Mattermost kiszolgáló URL-címe. Az URL-nek http:// vagy https:// -el kell kezdődnie.",
"renderer.components.newTeamModal.title.edit": "Kiszolgáló szerkesztése",
"renderer.components.newTeamModal.title.add": "Kiszolgáló hozzáadása",
"renderer.components.newTeamModal.serverDisplayName.description": "A kiszolgáló neve, amely az asztali alkalmazás fülsávján lesz látható.",
"renderer.components.newTeamModal.serverURL": "Kiszolgáló URL",
"renderer.components.newTeamModal.serverDisplayName": "Kiszolgáló megjelenítési neve",
"renderer.components.newTeamModal.error.urlRequired": "URL kötelező.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Az URL-nek http:// vagy https:// -el kell kezdődnie.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Az URL nincs jól formázva.",
"renderer.components.newTeamModal.error.serverUrlExists": "Egy ilyen URL-ű kiszolgáló már létezik.",
"renderer.components.newTeamModal.error.serverNameExists": "Egy ilyen nevű kiszolgáló már létezik.",
"renderer.components.newTeamModal.error.nameRequired": "Név kötelező.",
"renderer.components.newServerModal.serverURL.description": "A Mattermost kiszolgáló URL-címe. Az URL-nek http:// vagy https:// -el kell kezdődnie.",
"renderer.components.newServerModal.title.edit": "Kiszolgáló szerkesztése",
"renderer.components.newServerModal.title.add": "Kiszolgáló hozzáadása",
"renderer.components.newServerModal.serverDisplayName.description": "A kiszolgáló neve, amely az asztali alkalmazás fülsávján lesz látható.",
"renderer.components.newServerModal.serverURL": "Kiszolgáló URL",
"renderer.components.newServerModal.serverDisplayName": "Kiszolgáló megjelenítési neve",
"renderer.components.newServerModal.error.urlRequired": "URL kötelező.",
"renderer.components.newServerModal.error.urlNeedsHttp": "Az URL-nek http:// vagy https:// -el kell kezdődnie.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Az URL nincs jól formázva.",
"renderer.components.newServerModal.error.serverUrlExists": "Egy ilyen URL-ű kiszolgáló már létezik.",
"renderer.components.newServerModal.error.serverNameExists": "Egy ilyen nevű kiszolgáló már létezik.",
"renderer.components.newServerModal.error.nameRequired": "Név kötelező.",
"renderer.components.mainPage.updateReady": "Frissítés készen áll a telepítésre",
"renderer.components.mainPage.updateAvailable": "Frissítés érhető el",
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "A {appName} alkalmazás URL <link>{url}</link> helyes",

View file

@ -33,14 +33,14 @@
"renderer.components.settingsPage.flashWindow.description.note": "NOTA: ",
"renderer.components.settingsPage.enableHardwareAcceleration": "Usa l'accelerazione hardware della GPU",
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Usa il default di sistema",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL deve iniziare con http:// o https://.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL deve iniziare con http:// o https://.",
"renderer.components.removeServerModal.title": "Rimuovi server",
"renderer.components.settingsPage.appOptions": "Opzioni dell'app",
"renderer.components.saveButton.save": "Salva",
"renderer.components.newTeamModal.title.edit": "Modifica server",
"renderer.components.newTeamModal.title.add": "Aggiungi server",
"renderer.components.newServerModal.title.edit": "Modifica server",
"renderer.components.newServerModal.title.add": "Aggiungi server",
"renderer.components.input.required": "Questo campo è richiesto",
"renderer.components.newTeamModal.serverURL": "URL del server",
"renderer.components.newServerModal.serverURL": "URL del server",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.extraBar.back": "Indietro",
"renderer.components.configureServer.url.placeholder": "URL del server",

View file

@ -20,7 +20,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "証明書情報",
"renderer.dropdown.servers": "サーバー",
"renderer.dropdown.addAServer": "サーバーを追加",
"renderer.components.teamDropdownButton.noServersConfigured": "サーバーが設定されていません",
"renderer.components.serverDropdownButton.noServersConfigured": "サーバーが設定されていません",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.publicKeyInfo": "公開鍵情報",
@ -88,18 +88,18 @@
"renderer.components.removeServerModal.title": "サーバーを削除",
"renderer.components.removeServerModal.confirm": "本当に {serverName} サーバーを削除しますか?",
"renderer.components.removeServerModal.body": "この操作によりデスクトップアプリからサーバーが削除されますが、データは削除されません - いつでもアプリにサーバーを追加し直すことができます。",
"renderer.components.newTeamModal.title.edit": "サーバーを編集",
"renderer.components.newTeamModal.title.add": "サーバーを追加",
"renderer.components.newTeamModal.serverURL.description": "あなたのMattermostサーバーのURLです。http:// または https:// で始まる必要があります。",
"renderer.components.newTeamModal.serverURL": "サーバーURL",
"renderer.components.newTeamModal.serverDisplayName.description": "デスクトップアプリのタブバーに表示されるサーバーの名前です。",
"renderer.components.newTeamModal.serverDisplayName": "サーバー表示名",
"renderer.components.newTeamModal.error.urlRequired": "URLは必須です。",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URLは http:// または https:// で始まる必要があります。",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URLの形式が正しくありません。",
"renderer.components.newTeamModal.error.serverUrlExists": "同じURLのサーバーが既に存在しています。",
"renderer.components.newTeamModal.error.serverNameExists": "同名のサーバーが既に存在しています。",
"renderer.components.newTeamModal.error.nameRequired": "名前は必須です。",
"renderer.components.newServerModal.title.edit": "サーバーを編集",
"renderer.components.newServerModal.title.add": "サーバーを追加",
"renderer.components.newServerModal.serverURL.description": "あなたのMattermostサーバーのURLです。http:// または https:// で始まる必要があります。",
"renderer.components.newServerModal.serverURL": "サーバーURL",
"renderer.components.newServerModal.serverDisplayName.description": "デスクトップアプリのタブバーに表示されるサーバーの名前です。",
"renderer.components.newServerModal.serverDisplayName": "サーバー表示名",
"renderer.components.newServerModal.error.urlRequired": "URLは必須です。",
"renderer.components.newServerModal.error.urlNeedsHttp": "URLは http:// または https:// で始まる必要があります。",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URLの形式が正しくありません。",
"renderer.components.newServerModal.error.serverUrlExists": "同じURLのサーバーが既に存在しています。",
"renderer.components.newServerModal.error.serverNameExists": "同名のサーバーが既に存在しています。",
"renderer.components.newServerModal.error.nameRequired": "名前は必須です。",
"renderer.components.mainPage.updateReady": "更新をインストールする準備ができました",
"renderer.components.mainPage.updateAvailable": "更新が利用可能です",
"renderer.components.mainPage.downloadingUpdate": "更新をダウンロード中です。{total} 中 {percentDone}% 完了@ {speed}/s",

View file

@ -156,7 +156,7 @@
"renderer.components.welcomeScreen.slides.boards.title": "보드",
"renderer.components.welcomeScreen.slides.boards.subtitle": "디지털 오퍼레이션에 최적화된 프로젝트 및 작업 관리 솔루션을 사용해 항상 기한 내에 제품을 출시하세요.",
"renderer.components.welcomeScreen.button.getStarted": "시작하기",
"renderer.components.teamDropdownButton.noServersConfigured": "구성된 서버 없음",
"renderer.components.serverDropdownButton.noServersConfigured": "구성된 서버 없음",
"renderer.components.showCertificateModal.subjectName": "주체 이름(Subject Name)",
"renderer.components.showCertificateModal.serialNumber": "일련번호",
"renderer.components.showCertificateModal.publicKeyInfo": "공개 키 정보",
@ -225,18 +225,18 @@
"renderer.components.removeServerModal.title": "서버 제거",
"renderer.components.removeServerModal.confirm": "{serverName} 서버를 제거하시겠습니까?",
"renderer.components.removeServerModal.body": "이렇게 하면 데스크톱 앱에서 서버가 제거되지만 해당 데이터는 삭제되지 않습니다. 언제든지 앱에 서버를 다시 추가할 수 있습니다.",
"renderer.components.newTeamModal.title.edit": "서버 수정",
"renderer.components.newTeamModal.title.add": "서버 추가",
"renderer.components.newTeamModal.serverURL.description": "Mattermost 서버 URL입니다. http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newTeamModal.serverURL": "서버 URL",
"renderer.components.newTeamModal.serverDisplayName.description": "데스크톱 앱 탭 표시줄에 표시되는 서버 이름입니다.",
"renderer.components.newTeamModal.serverDisplayName": "서버 표시 이름",
"renderer.components.newTeamModal.error.urlRequired": "URL이 필요합니다.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL은 http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL 형식이 올바르지 않습니다.",
"renderer.components.newTeamModal.error.serverUrlExists": "동일한 URL을 가진 서버가 이미 존재합니다.",
"renderer.components.newTeamModal.error.serverNameExists": "같은 이름의 서버가 이미 존재합니다.",
"renderer.components.newTeamModal.error.nameRequired": "이름이 필요합니다.",
"renderer.components.newServerModal.title.edit": "서버 수정",
"renderer.components.newServerModal.title.add": "서버 추가",
"renderer.components.newServerModal.serverURL.description": "Mattermost 서버 URL입니다. http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newServerModal.serverURL": "서버 URL",
"renderer.components.newServerModal.serverDisplayName.description": "데스크톱 앱 탭 표시줄에 표시되는 서버 이름입니다.",
"renderer.components.newServerModal.serverDisplayName": "서버 표시 이름",
"renderer.components.newServerModal.error.urlRequired": "URL이 필요합니다.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL은 http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL 형식이 올바르지 않습니다.",
"renderer.components.newServerModal.error.serverUrlExists": "동일한 URL을 가진 서버가 이미 존재합니다.",
"renderer.components.newServerModal.error.serverNameExists": "같은 이름의 서버가 이미 존재합니다.",
"renderer.components.newServerModal.error.nameRequired": "이름이 필요합니다.",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "컨텍스트 메뉴",
"renderer.components.input.required": "이 필드는 필수입니다",

View file

@ -48,7 +48,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Informatie over het certificaat",
"renderer.dropdown.servers": "Servers",
"renderer.dropdown.addAServer": "Voeg een server toe",
"renderer.components.teamDropdownButton.noServersConfigured": "Geen geconfigureerde servers",
"renderer.components.serverDropdownButton.noServersConfigured": "Geen geconfigureerde servers",
"renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serienummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Info publieke sleutel",
@ -116,18 +116,18 @@
"renderer.components.removeServerModal.title": "Server verwijderen",
"renderer.components.removeServerModal.confirm": "Bevestig dat je de {serverNaam} server wil verwijderen?",
"renderer.components.removeServerModal.body": "Dit verwijdert de server van je desktop-applicatie maar verwijdert geen gegevens - je kan de server op elk moment opnieuw toevoegen aan de applicatie.",
"renderer.components.newTeamModal.title.edit": "Server bewerken",
"renderer.components.newTeamModal.title.add": "Server toevoegen",
"renderer.components.newTeamModal.serverURL.description": "De URL van je Mattermost server. Moet beginnen met http:// of https://.",
"renderer.components.newTeamModal.serverURL": "Server-URL",
"renderer.components.newTeamModal.serverDisplayName.description": "De naam van de server die op de tabbalk van jouw desktop app wordt weergegeven.",
"renderer.components.newTeamModal.serverDisplayName": "Weergave naam server",
"renderer.components.newTeamModal.error.urlRequired": "URL is verplicht.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL moet beginnen met http:// of https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL heeft een ongeldig formaat.",
"renderer.components.newTeamModal.error.serverUrlExists": "Een server met dezelfde URL bestaat reeds.",
"renderer.components.newTeamModal.error.serverNameExists": "Een server met dezelfde naam bestaat reeds.",
"renderer.components.newTeamModal.error.nameRequired": "Naam is verplicht.",
"renderer.components.newServerModal.title.edit": "Server bewerken",
"renderer.components.newServerModal.title.add": "Server toevoegen",
"renderer.components.newServerModal.serverURL.description": "De URL van je Mattermost server. Moet beginnen met http:// of https://.",
"renderer.components.newServerModal.serverURL": "Server-URL",
"renderer.components.newServerModal.serverDisplayName.description": "De naam van de server die op de tabbalk van jouw desktop app wordt weergegeven.",
"renderer.components.newServerModal.serverDisplayName": "Weergave naam server",
"renderer.components.newServerModal.error.urlRequired": "URL is verplicht.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL moet beginnen met http:// of https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL heeft een ongeldig formaat.",
"renderer.components.newServerModal.error.serverUrlExists": "Een server met dezelfde URL bestaat reeds.",
"renderer.components.newServerModal.error.serverNameExists": "Een server met dezelfde naam bestaat reeds.",
"renderer.components.newServerModal.error.nameRequired": "Naam is verplicht.",
"renderer.components.mainPage.updateReady": "Update klaar om geïnstalleerd te worden",
"renderer.components.mainPage.updateAvailable": "Update beschikbaar",
"renderer.components.mainPage.downloadingUpdate": "Update wordt gedownload. {percentDone}% van {total} @ {speed}/s",

View file

@ -76,18 +76,18 @@
"renderer.components.removeServerModal.title": "Usuń Serwer",
"renderer.components.removeServerModal.confirm": "Potwierdzasz, że chcesz usunąć serwer {serverName}?",
"renderer.components.removeServerModal.body": "To usunie serwer z Twojej aplikacji Desktop App, ale nie usunie żadnych jego danych - możesz dodać serwer z powrotem do aplikacji w dowolnym momencie.",
"renderer.components.newTeamModal.title.edit": "Edytuj Serwer",
"renderer.components.newTeamModal.title.add": "Dodaj Serwer",
"renderer.components.newTeamModal.serverURL.description": "Adres URL Twojego serwera Mattermost. Musi zaczynać się od http:// lub https://.",
"renderer.components.newTeamModal.serverURL": "URL Serwera",
"renderer.components.newTeamModal.serverDisplayName.description": "Nazwa serwera wyświetlana na pasku zakładek aplikacji desktopowej.",
"renderer.components.newTeamModal.serverDisplayName": "Wyświetlana Nazwa Serwera",
"renderer.components.newTeamModal.error.urlRequired": "Wymagany jest adres URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Adres URL powinien zaczynać się od http:// lub https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Adres URL nie jest poprawnie sformatowany.",
"renderer.components.newTeamModal.error.serverUrlExists": "Serwer o tym samym adresie URL już istnieje.",
"renderer.components.newTeamModal.error.serverNameExists": "Serwer o tej samej nazwie już istnieje.",
"renderer.components.newTeamModal.error.nameRequired": "Nazwa jest wymagana.",
"renderer.components.newServerModal.title.edit": "Edytuj Serwer",
"renderer.components.newServerModal.title.add": "Dodaj Serwer",
"renderer.components.newServerModal.serverURL.description": "Adres URL Twojego serwera Mattermost. Musi zaczynać się od http:// lub https://.",
"renderer.components.newServerModal.serverURL": "URL Serwera",
"renderer.components.newServerModal.serverDisplayName.description": "Nazwa serwera wyświetlana na pasku zakładek aplikacji desktopowej.",
"renderer.components.newServerModal.serverDisplayName": "Wyświetlana Nazwa Serwera",
"renderer.components.newServerModal.error.urlRequired": "Wymagany jest adres URL.",
"renderer.components.newServerModal.error.urlNeedsHttp": "Adres URL powinien zaczynać się od http:// lub https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Adres URL nie jest poprawnie sformatowany.",
"renderer.components.newServerModal.error.serverUrlExists": "Serwer o tym samym adresie URL już istnieje.",
"renderer.components.newServerModal.error.serverNameExists": "Serwer o tej samej nazwie już istnieje.",
"renderer.components.newServerModal.error.nameRequired": "Nazwa jest wymagana.",
"renderer.components.mainPage.updateReady": "Aktualizacja gotowa do zainstalowania",
"renderer.components.mainPage.updateAvailable": "Dostępna aktualizacja",
"renderer.components.mainPage.downloadingUpdate": "Pobieranie aktualizacji. {percentDone}% z {total} @ {speed}/s",
@ -192,7 +192,7 @@
"renderer.components.settingsPage.loggingLevel.level.debug": "Debuguj (debug)",
"renderer.components.settingsPage.loggingLevel.description.subtitle": "Zwiększenie poziomu dziennika zwiększa zużycie miejsca na dysku i może wpłynąć na wydajność. Zalecamy zwiększenie poziomu dziennika tylko w przypadku wystąpienia problemów.",
"renderer.components.welcomeScreen.button.getStarted": "Rozpocznij",
"renderer.components.teamDropdownButton.noServersConfigured": "Brak skonfigurowanych serwerów",
"renderer.components.serverDropdownButton.noServersConfigured": "Brak skonfigurowanych serwerów",
"renderer.components.showCertificateModal.subjectName": "Nazwa tematu",
"renderer.components.showCertificateModal.serialNumber": "Numer seryjny",
"renderer.components.showCertificateModal.publicKeyInfo": "Informacje o kluczach publicznych",

View file

@ -128,10 +128,10 @@
"main.menus.app.view.downloads": "Downloads",
"renderer.components.saveButton.save": "Salvar",
"renderer.components.removeServerModal.title": "Remover Servidor",
"renderer.components.newTeamModal.title.edit": "Editar Servidor",
"renderer.components.newTeamModal.title.add": "Adicionar Servidor",
"renderer.components.newTeamModal.serverURL": "URL do Servidor",
"renderer.components.newTeamModal.error.urlRequired": "URL é necessária.",
"renderer.components.newServerModal.title.edit": "Editar Servidor",
"renderer.components.newServerModal.title.add": "Adicionar Servidor",
"renderer.components.newServerModal.serverURL": "URL do Servidor",
"renderer.components.newServerModal.error.urlRequired": "URL é necessária.",
"renderer.components.mainPage.contextMenu.ariaLabel": "Menu de contexto",
"renderer.components.extraBar.back": "Voltar",
"renderer.components.errorView.troubleshooting.computerIsConnected": "Seu computador está conectado à internet.",

View file

@ -48,7 +48,7 @@
"renderer.components.welcomeScreen.slides.boards.title": "Boards",
"renderer.components.welcomeScreen.slides.boards.subtitle": "Успевайте каждый раз вовремя выполнить задачи, с решением для управления проектами и задачами, созданным для цифровых операций.",
"renderer.components.welcomeScreen.button.getStarted": "Начать",
"renderer.components.teamDropdownButton.noServersConfigured": "Серверы не настроены",
"renderer.components.serverDropdownButton.noServersConfigured": "Серверы не настроены",
"renderer.components.showCertificateModal.subjectName": "Имя субъекта (SN)",
"renderer.components.showCertificateModal.serialNumber": "Серийный номер",
"renderer.components.showCertificateModal.publicKeyInfo": "Информация о публичном ключе",
@ -118,19 +118,19 @@
"renderer.components.removeServerModal.title": "Удалить сервер",
"renderer.components.removeServerModal.confirm": "Подтвердите, что вы хотите удалить сервер {serverName}?",
"renderer.components.removeServerModal.body": "Это действие удалит сервер из вашего приложения, но не удалит его данные - вы можете добавить сервер обратно в приложение в любое время.",
"renderer.components.newTeamModal.title.edit": "Изменение настроек сервера",
"renderer.components.newTeamModal.title.add": "Добавить сервер",
"renderer.components.newTeamModal.serverURL.description": "URL вашего сервера Mattermost. Должен начинаться с http:// или https://.",
"renderer.components.newTeamModal.serverURL": "URL сервера",
"renderer.components.newTeamModal.serverDisplayName.description": "Имя сервера, отображаемое на панели вкладок вашего приложения.",
"renderer.components.newTeamModal.serverDisplayName": "Отображаемое имя сервера",
"renderer.components.newServerModal.title.edit": "Изменение настроек сервера",
"renderer.components.newServerModal.title.add": "Добавить сервер",
"renderer.components.newServerModal.serverURL.description": "URL вашего сервера Mattermost. Должен начинаться с http:// или https://.",
"renderer.components.newServerModal.serverURL": "URL сервера",
"renderer.components.newServerModal.serverDisplayName.description": "Имя сервера, отображаемое на панели вкладок вашего приложения.",
"renderer.components.newServerModal.serverDisplayName": "Отображаемое имя сервера",
"common.tabs.TAB_MESSAGING": "Channels",
"renderer.components.newTeamModal.error.nameRequired": "Укажите имя сервера.",
"renderer.components.newTeamModal.error.urlRequired": "Укажите URL-адрес.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL-адрес должен начинаться с http:// или https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL-адрес неверно отформатирован.",
"renderer.components.newTeamModal.error.serverUrlExists": "Сервер с таким URL уже существует.",
"renderer.components.newTeamModal.error.serverNameExists": "Сервер с таким же именем уже существует.",
"renderer.components.newServerModal.error.nameRequired": "Укажите имя сервера.",
"renderer.components.newServerModal.error.urlRequired": "Укажите URL-адрес.",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL-адрес должен начинаться с http:// или https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL-адрес неверно отформатирован.",
"renderer.components.newServerModal.error.serverUrlExists": "Сервер с таким URL уже существует.",
"renderer.components.newServerModal.error.serverNameExists": "Сервер с таким же именем уже существует.",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "Контекстное меню",
"renderer.components.input.required": "Обязательное поле",

View file

@ -24,18 +24,18 @@
"renderer.components.removeServerModal.title": "Ta bort servern",
"renderer.components.removeServerModal.confirm": "Bekräftar du att du vill ta bort anslutningen till servern {serverName}?",
"renderer.components.removeServerModal.body": "Detta tar bort servern från din skrivbordsapplikation, men raderar inte dess data - du kan när som helst lägga till servern i appen igen.",
"renderer.components.newTeamModal.title.edit": "Redigera server",
"renderer.components.newTeamModal.title.add": "Lägg till server",
"renderer.components.newTeamModal.serverURL.description": "URL för din Mattermost-server. Måste börja med http:// eller https://.",
"renderer.components.newTeamModal.serverURL": "Server-URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Namnet på servern som visas på fliken i skrivbordsappen.",
"renderer.components.newTeamModal.serverDisplayName": "Serverns visningsnamn",
"renderer.components.newTeamModal.error.urlRequired": "Du måste ange en URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Måste börja med http:// eller https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL-adressen är inte korrekt formaterad.",
"renderer.components.newTeamModal.error.serverUrlExists": "Det finns redan en server med samma URL.",
"renderer.components.newTeamModal.error.serverNameExists": "Det finns redan en server med samma namn.",
"renderer.components.newTeamModal.error.nameRequired": "Du måste ange Namn.",
"renderer.components.newServerModal.title.edit": "Redigera server",
"renderer.components.newServerModal.title.add": "Lägg till server",
"renderer.components.newServerModal.serverURL.description": "URL för din Mattermost-server. Måste börja med http:// eller https://.",
"renderer.components.newServerModal.serverURL": "Server-URL",
"renderer.components.newServerModal.serverDisplayName.description": "Namnet på servern som visas på fliken i skrivbordsappen.",
"renderer.components.newServerModal.serverDisplayName": "Serverns visningsnamn",
"renderer.components.newServerModal.error.urlRequired": "Du måste ange en URL.",
"renderer.components.newServerModal.error.urlNeedsHttp": "Måste börja med http:// eller https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL-adressen är inte korrekt formaterad.",
"renderer.components.newServerModal.error.serverUrlExists": "Det finns redan en server med samma URL.",
"renderer.components.newServerModal.error.serverNameExists": "Det finns redan en server med samma namn.",
"renderer.components.newServerModal.error.nameRequired": "Du måste ange Namn.",
"renderer.components.mainPage.updateReady": "Uppdatering klar att installera",
"renderer.components.mainPage.updateAvailable": "Uppdatering tillgänglig",
"renderer.components.mainPage.downloadingUpdate": "Hämtar uppdatering. {percentDone}% av {total} @ {speed}/s",
@ -189,7 +189,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Information om certifikat",
"renderer.dropdown.servers": "Servrar",
"renderer.dropdown.addAServer": "Lägg till en server",
"renderer.components.teamDropdownButton.noServersConfigured": "Inga servrar konfigurerade",
"renderer.components.serverDropdownButton.noServersConfigured": "Inga servrar konfigurerade",
"renderer.components.showCertificateModal.subjectName": "Ämnesnamn",
"renderer.components.showCertificateModal.serialNumber": "Serienummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Offentlig nyckel",

View file

@ -17,7 +17,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Sertifika bilgileri",
"renderer.dropdown.servers": "Sunucular",
"renderer.dropdown.addAServer": "Bir sunucu ekle",
"renderer.components.teamDropdownButton.noServersConfigured": "Herhangi bir sunucu yapılandırılmamış",
"renderer.components.serverDropdownButton.noServersConfigured": "Herhangi bir sunucu yapılandırılmamış",
"renderer.components.showCertificateModal.subjectName": "Özne adı",
"renderer.components.showCertificateModal.serialNumber": "Seri numarası",
"renderer.components.showCertificateModal.publicKeyInfo": "Herkese açık anahtar bilgileri",
@ -85,18 +85,18 @@
"renderer.components.removeServerModal.title": "Sunucuyu kaldır",
"renderer.components.removeServerModal.confirm": "{serverName} sunucunu kaldırmak istediğinizi onaylıyor musunuz?",
"renderer.components.removeServerModal.body": "Bu işlem sunucuyu masaüstü uygulamanızdan kaldırır ancak hiçbir verisini silmez. İstediğiniz zaman sunucuyu uygulamaya yeniden ekleyebilirsiniz.",
"renderer.components.newTeamModal.title.edit": "Sunucuyu düzenle",
"renderer.components.newTeamModal.title.add": "Sunucu ekle",
"renderer.components.newTeamModal.serverURL.description": "Mattermost sunucunuzun adresi. http:// ya da https:// ile başlamalıdır.",
"renderer.components.newTeamModal.serverURL": "Sunucu adresi",
"renderer.components.newTeamModal.serverDisplayName.description": "Sunucunun masaüstü uygulama sekmesi çubuğunda görüntülenecek adı.",
"renderer.components.newTeamModal.serverDisplayName": "Sunucunun görüntülenecek adı",
"renderer.components.newTeamModal.error.urlRequired": "Adresin yazılması zorunludur.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Adres http:// ya da https:// ile başlamalıdır.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Adresin biçimi doğru değil.",
"renderer.components.newTeamModal.error.serverUrlExists": "Aynı adresi kullanan bir sunucu zaten var.",
"renderer.components.newTeamModal.error.serverNameExists": "Aynı adlı bir sunucu zaten var.",
"renderer.components.newTeamModal.error.nameRequired": "Ad yazılması zorunludur.",
"renderer.components.newServerModal.title.edit": "Sunucuyu düzenle",
"renderer.components.newServerModal.title.add": "Sunucu ekle",
"renderer.components.newServerModal.serverURL.description": "Mattermost sunucunuzun adresi. http:// ya da https:// ile başlamalıdır.",
"renderer.components.newServerModal.serverURL": "Sunucu adresi",
"renderer.components.newServerModal.serverDisplayName.description": "Sunucunun masaüstü uygulama sekmesi çubuğunda görüntülenecek adı.",
"renderer.components.newServerModal.serverDisplayName": "Sunucunun görüntülenecek adı",
"renderer.components.newServerModal.error.urlRequired": "Adresin yazılması zorunludur.",
"renderer.components.newServerModal.error.urlNeedsHttp": "Adres http:// ya da https:// ile başlamalıdır.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Adresin biçimi doğru değil.",
"renderer.components.newServerModal.error.serverUrlExists": "Aynı adresi kullanan bir sunucu zaten var.",
"renderer.components.newServerModal.error.serverNameExists": "Aynı adlı bir sunucu zaten var.",
"renderer.components.newServerModal.error.nameRequired": "Ad yazılması zorunludur.",
"renderer.components.mainPage.updateReady": "Güncelleme kurulmaya hazır",
"renderer.components.mainPage.updateAvailable": "Güncelleme yayınlanmış",
"renderer.components.mainPage.downloadingUpdate": "Güncelleme indiriliyor. %{percentDone}/{total} indirildi. Hız: {speed}/s",

View file

@ -10,18 +10,18 @@
"renderer.components.removeServerModal.title": "Видалити сервер",
"renderer.components.removeServerModal.confirm": "Ви дійсно хочете прибрати цей {serverName} сервер?",
"renderer.components.removeServerModal.body": "Це прибере сервер із вашого десктопного додатку, але не видалить його даних ви можете повернути сервер до застосунку у будь–який час.",
"renderer.components.newTeamModal.title.edit": "Редагувати сервер",
"renderer.components.newTeamModal.title.add": "Додати сервер",
"renderer.components.newTeamModal.serverURL.description": "Посилання URL на сервер Mattermost повинно починатись із with http:// or https://.",
"renderer.components.newTeamModal.serverURL": "Посилання URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Ім'я серверу відображатиметься на панелі вкладок у десктопному додатку.",
"renderer.components.newTeamModal.serverDisplayName": "Ім'я серверу, що відображатиметься",
"renderer.components.newTeamModal.error.urlRequired": "Необхідне посилання URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Посилання URL має починатись із http:// or https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Посилання URL має неправильний формат.",
"renderer.components.newTeamModal.error.serverUrlExists": "Сервер з таким посиланням URL вже існує.",
"renderer.components.newTeamModal.error.serverNameExists": "Сервер з таким ім'ям вже існує.",
"renderer.components.newTeamModal.error.nameRequired": "Необхідне ім'я.",
"renderer.components.newServerModal.title.edit": "Редагувати сервер",
"renderer.components.newServerModal.title.add": "Додати сервер",
"renderer.components.newServerModal.serverURL.description": "Посилання URL на сервер Mattermost повинно починатись із with http:// or https://.",
"renderer.components.newServerModal.serverURL": "Посилання URL",
"renderer.components.newServerModal.serverDisplayName.description": "Ім'я серверу відображатиметься на панелі вкладок у десктопному додатку.",
"renderer.components.newServerModal.serverDisplayName": "Ім'я серверу, що відображатиметься",
"renderer.components.newServerModal.error.urlRequired": "Необхідне посилання URL.",
"renderer.components.newServerModal.error.urlNeedsHttp": "Посилання URL має починатись із http:// or https://.",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "Посилання URL має неправильний формат.",
"renderer.components.newServerModal.error.serverUrlExists": "Сервер з таким посиланням URL вже існує.",
"renderer.components.newServerModal.error.serverNameExists": "Сервер з таким ім'ям вже існує.",
"renderer.components.newServerModal.error.nameRequired": "Необхідне ім'я.",
"renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "Контекстне меню",
"renderer.components.input.required": "Це поле є обов'язковим",
@ -180,7 +180,7 @@
"common.tabs.TAB_FOCALBOARD": "Дошки",
"renderer.components.welcomeScreen.slides.boards.title": "Дошки",
"renderer.components.welcomeScreen.button.getStarted": "Розпочнемо",
"renderer.components.teamDropdownButton.noServersConfigured": "Немає налаштованих серверів",
"renderer.components.serverDropdownButton.noServersConfigured": "Немає налаштованих серверів",
"renderer.components.showCertificateModal.serialNumber": "Серійний номер",
"renderer.components.showCertificateModal.notValidBefore": "Недійсний до",
"renderer.components.showCertificateModal.notValidAfter": "Недійсний після",

View file

@ -170,7 +170,7 @@
"renderer.components.welcomeScreen.slides.boards.title": "公告板",
"renderer.components.welcomeScreen.slides.boards.subtitle": "通过一个为数字运营而建立的项目和任务管理解决方案,每次都要按时交付。",
"renderer.components.welcomeScreen.button.getStarted": "开始吧",
"renderer.components.teamDropdownButton.noServersConfigured": "没有配置服务器",
"renderer.components.serverDropdownButton.noServersConfigured": "没有配置服务器",
"renderer.components.showCertificateModal.subjectName": "主题",
"renderer.components.showCertificateModal.serialNumber": "序列号",
"renderer.components.showCertificateModal.publicKeyInfo": "公钥信息",
@ -236,18 +236,18 @@
"renderer.components.removeServerModal.title": "移除服务器",
"renderer.components.removeServerModal.confirm": "确认要删除{serverName}服务器?",
"renderer.components.removeServerModal.body": "这将从您的桌面应用程序中删除服务器,但不会删除其任何数据-您可以随时将服务器添加回应用程序。",
"renderer.components.newTeamModal.title.edit": "编辑服务器",
"renderer.components.newTeamModal.title.add": "添加服务器",
"renderer.components.newTeamModal.serverURL.description": "您Mattermost服务器的URL。必须以http://或https://开头。",
"renderer.components.newTeamModal.serverURL": "服务器URL",
"renderer.components.newTeamModal.serverDisplayName.description": "显示在桌面应用程序选项卡上的服务器名称。",
"renderer.components.newTeamModal.serverDisplayName": "服务器显示的名称",
"renderer.components.newTeamModal.error.urlRequired": "URL是必填项。",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL应以http://或https://开头。",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL格式不正确。",
"renderer.components.newTeamModal.error.serverUrlExists": "已经存在具有相同URL的服务器。",
"renderer.components.newTeamModal.error.serverNameExists": "已经存在同名服务器。",
"renderer.components.newTeamModal.error.nameRequired": "需要填写姓名。",
"renderer.components.newServerModal.title.edit": "编辑服务器",
"renderer.components.newServerModal.title.add": "添加服务器",
"renderer.components.newServerModal.serverURL.description": "您Mattermost服务器的URL。必须以http://或https://开头。",
"renderer.components.newServerModal.serverURL": "服务器URL",
"renderer.components.newServerModal.serverDisplayName.description": "显示在桌面应用程序选项卡上的服务器名称。",
"renderer.components.newServerModal.serverDisplayName": "服务器显示的名称",
"renderer.components.newServerModal.error.urlRequired": "URL是必填项。",
"renderer.components.newServerModal.error.urlNeedsHttp": "URL应以http://或https://开头。",
"renderer.components.newServerModal.error.urlIncorrectFormatting": "URL格式不正确。",
"renderer.components.newServerModal.error.serverUrlExists": "已经存在具有相同URL的服务器。",
"renderer.components.newServerModal.error.serverNameExists": "已经存在同名服务器。",
"renderer.components.newServerModal.error.nameRequired": "需要填写姓名。",
"renderer.components.mainPage.contextMenu.ariaLabel": "快捷菜单",
"renderer.components.input.required": "此栏为必填项",
"renderer.components.extraBar.back": "返回",

View file

@ -1,6 +1,6 @@
# Developer guide for using Group Policy Objects (GPO) (Windows 10 Pro)
GPOs are used to pre-configure servers/teams, autoUpdater and server management
GPOs are used to pre-configure servers, autoUpdater and server management
You can read more about GPOs [here](https://learn.microsoft.com/en-us/previous-versions/windows/desktop/policy/group-policy-objects)
@ -18,7 +18,7 @@ Example:
| Value Name | Value |
|------------|----------------------------------|
| Community | <https://community.mattermost.com> |
8. Now if you open your Mattermost desktop application you should be able to see the server in the server dropdown (team dropdown)
8. Now if you open your Mattermost desktop application you should be able to see the server in the server dropdown
---

View file

@ -4,10 +4,10 @@
const buildConfig = require('../dist/buildConfig');
function validateBuildConfig(config) {
if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) {
if (config.enableServerManagement === false && config.defaultServers && config.defaultServers.length === 0) {
return {
result: false,
message: `Specify at least one server for "defaultTeams" in buildConfig.js when "enableServerManagement is set to false.\n${JSON.stringify(config, null, 2)}`,
message: `Specify at least one server for "defaultServers" in buildConfig.js when "enableServerManagement is set to false.\n${JSON.stringify(config, null, 2)}`,
};
}
return {result: true};

View file

@ -48,10 +48,10 @@ You're now all set! See the [User Guide](#user-guide) below for instructions.
## User Guide
After launching, you need to configure the application to interact with your team.
After launching, you need to configure the application to interact with your server.
1. If you don't see a page titled "Settings", select **File** > **Settings...** from the menu bar.
2. Click **Add new team** next to the right of Team Management section.
2. Click **Add new server** next to the right of Server Management section.
3. Enter **Name** and a valid **URL**, which begins with either `http://` or `https://`.
4. Click **Add**.
5. Click **Save**.

View file

@ -153,7 +153,7 @@ describe('common/Validator', () => {
version: 3,
};
it('should ensure messaging tab is open', () => {
it('should ensure messaging view is open', () => {
const modifiedConfig = {
...config,
teams: [

View file

@ -12,7 +12,7 @@ import {ComparableCertificate} from 'types/certificate';
import {PermissionType, TrustedOrigin} from 'types/trustedOrigin';
import {Logger} from 'common/log';
import {TAB_MESSAGING} from 'common/tabs/TabView';
import {TAB_MESSAGING} from 'common/views/View';
import {isValidURL} from 'common/utils/url';
const log = new Logger('Validator');
@ -206,45 +206,45 @@ function cleanURL(url: string): string {
return updatedURL;
}
function cleanTeam<T extends {name: string; url: string}>(team: T) {
function cleanServer<T extends {name: string; url: string}>(server: T) {
return {
...team,
url: cleanURL(team.url),
...server,
url: cleanURL(server.url),
};
}
function cleanTeamWithTabs(team: ConfigServer) {
function cleanServerWithViews(server: ConfigServer) {
return {
...cleanTeam(team),
tabs: team.tabs.map((tab) => {
...cleanServer(server),
tabs: server.tabs.map((view) => {
return {
...tab,
isOpen: tab.name === TAB_MESSAGING ? true : tab.isOpen,
...view,
isOpen: view.name === TAB_MESSAGING ? true : view.isOpen,
};
}),
};
}
function cleanTeams<T extends {name: string; url: string}>(teams: T[], func: (team: T) => T) {
let newTeams = teams;
if (Array.isArray(newTeams) && newTeams.length) {
function cleanServers<T extends {name: string; url: string}>(servers: T[], func: (server: T) => T) {
let newServers = servers;
if (Array.isArray(newServers) && newServers.length) {
// first replace possible backslashes with forward slashes
newTeams = newTeams.map((team) => func(team));
newServers = newServers.map((server) => func(server));
// next filter out urls that are still invalid so all is not lost
newTeams = newTeams.filter(({url}) => isValidURL(url));
newServers = newServers.filter(({url}) => isValidURL(url));
}
return newTeams;
return newServers;
}
// validate v.1 config.json
export function validateV1ConfigData(data: ConfigV1) {
data.teams = cleanTeams(data.teams, cleanTeam);
data.teams = cleanServers(data.teams, cleanServer);
return validateAgainstSchema(data, configDataSchemaV1);
}
export function validateV2ConfigData(data: ConfigV2) {
data.teams = cleanTeams(data.teams, cleanTeam);
data.teams = cleanServers(data.teams, cleanServer);
if (data.spellCheckerURL && !isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;
@ -253,7 +253,7 @@ export function validateV2ConfigData(data: ConfigV2) {
}
export function validateV3ConfigData(data: ConfigV3) {
data.teams = cleanTeams(data.teams, cleanTeamWithTabs);
data.teams = cleanServers(data.teams, cleanServerWithViews);
if (data.spellCheckerURL && !isValidURL(data.spellCheckerURL)) {
log.error('Invalid download location for spellchecker dictionary, removing from config');
delete data.spellCheckerURL;

View file

@ -3,8 +3,8 @@
export const SWITCH_SERVER = 'switch-server';
export const SWITCH_TAB = 'switch-tab';
export const CLOSE_TAB = 'close-tab';
export const OPEN_TAB = 'open-tab';
export const CLOSE_VIEW = 'close-view';
export const OPEN_VIEW = 'open-view';
export const SET_ACTIVE_VIEW = 'set-active-view';
export const FOCUS_BROWSERVIEW = 'focus-browserview';
export const HISTORY = 'history';
@ -76,10 +76,10 @@ export const FOCUS_THREE_DOT_MENU = 'focus-three-dot-menu';
export const LOADSCREEN_END = 'loadscreen-end';
export const OPEN_TEAMS_DROPDOWN = 'open-teams-dropdown';
export const CLOSE_TEAMS_DROPDOWN = 'close-teams-dropdown';
export const UPDATE_TEAMS_DROPDOWN = 'update-teams-dropdown';
export const REQUEST_TEAMS_DROPDOWN_INFO = 'request-teams-dropdown-info';
export const OPEN_SERVERS_DROPDOWN = 'open-servers-dropdown';
export const CLOSE_SERVERS_DROPDOWN = 'close-servers-dropdown';
export const UPDATE_SERVERS_DROPDOWN = 'update-servers-dropdown';
export const REQUEST_SERVERS_DROPDOWN_INFO = 'request-servers-dropdown-info';
export const RECEIVE_DROPDOWN_MENU_SIZE = 'receive-dropdown-menu-size';
export const UPDATE_AVAILABLE = 'update-available';

View file

@ -64,7 +64,7 @@ describe('common/config/RegistryConfig', () => {
Object.defineProperty(process, 'platform', {
value: originalPlatform,
});
expect(registryConfig.data.teams).toContainEqual({
expect(registryConfig.data.servers).toContainEqual({
name: 'server-1',
url: 'http://server-1.com',
});

View file

@ -6,7 +6,7 @@ import {EventEmitter} from 'events';
import WindowsRegistry from 'winreg';
import WindowsRegistryUTF8 from 'winreg-utf8';
import {RegistryConfig as RegistryConfigType, Team} from 'types/config';
import {RegistryConfig as RegistryConfigType, Server} from 'types/config';
import {Logger} from 'common/log';
@ -26,7 +26,7 @@ export default class RegistryConfig extends EventEmitter {
super();
this.initialized = false;
this.data = {
teams: [],
servers: [],
};
}
@ -41,7 +41,7 @@ export default class RegistryConfig extends EventEmitter {
try {
const servers = await this.getServersListFromRegistry();
if (servers.length) {
this.data.teams!.push(...servers);
this.data.servers!.push(...servers);
}
} catch (error) {
log.warn('Nothing retrieved for \'DefaultServerList\'', error);
@ -78,7 +78,7 @@ export default class RegistryConfig extends EventEmitter {
*/
async getServersListFromRegistry() {
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`);
return defaultServers.flat(2).reduce((servers: Team[], server) => {
return defaultServers.flat(2).reduce((servers: Server[], server) => {
if (server) {
servers.push({
name: (server as WindowsRegistry.RegistryItem).name,

View file

@ -8,20 +8,20 @@ import {BuildConfig} from 'types/config';
/**
* Build-time configuration. End-users can't change these parameters.
* @prop {Object[]} defaultTeams
* @prop {string} defaultTeams[].name - The tab name for default team.
* @prop {string} defaultTeams[].url - The URL for default team.
* @prop {string} defaultTeams[].order - Sort order for team tabs (0, 1, 2)
* @prop {Object[]} defaultServers
* @prop {string} defaultServers[].name - The view name for default server.
* @prop {string} defaultServers[].url - The URL for default server.
* @prop {string} defaultServers[].order - Sort order for server views (0, 1, 2)
* @prop {string} helpLink - The URL for "Help->Learn More..." menu item.
* If null is specified, the menu disappears.
* @prop {boolean} enableServerManagement - Whether users can edit servers configuration.
* Specify at least one server for "defaultTeams"
* Specify at least one server for "defaultServers"
* when "enableServerManagement is set to false
* @prop {[]} managedResources - Defines which paths are managed
* @prop {[]} allowedProtocols - Defines which protocols should be automatically allowed
*/
const buildConfig: BuildConfig = {
defaultTeams: [/*
defaultServers: [/*
{
name: 'example',
url: 'https://example.com'

View file

@ -24,54 +24,54 @@ jest.mock('common/Validator', () => ({
validateConfigData: (configData) => (configData.version === 3 ? configData : null),
}));
jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: (value) => ({
jest.mock('common/views/View', () => ({
getDefaultViewsForConfigServer: (value) => ({
...value,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
}),
}));
const buildTeam = {
name: 'build-team-1',
const buildServer = {
name: 'build-server-1',
order: 0,
url: 'http://build-team-1.com',
url: 'http://build-server-1.com',
};
const buildTeamWithTabs = {
...buildTeam,
const buildServerWithViews = {
...buildServer,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
};
const registryTeam = {
name: 'registry-team-1',
const registryServer = {
name: 'registry-server-1',
order: 0,
url: 'http://registry-team-1.com',
url: 'http://registry-server-1.com',
};
const team = {
name: 'team-1',
const server = {
name: 'server-1',
order: 0,
url: 'http://team-1.com',
url: 'http://server-1.com',
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
};
@ -86,7 +86,7 @@ jest.mock('common/config/migrationPreferences', () => jest.fn());
jest.mock('common/config/buildConfig', () => {
return {
defaultTeams: [buildTeam],
defaultServers: [buildServer],
};
});
@ -99,7 +99,7 @@ describe('common/config', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs);
expect(config.predefinedServers).toContainEqual(buildServerWithViews);
});
describe('loadRegistry', () => {
@ -107,16 +107,16 @@ describe('common/config', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.onLoadRegistry({teams: [registryTeam]});
config.onLoadRegistry({servers: [registryServer]});
expect(config.reload).toHaveBeenCalled();
expect(config.predefinedTeams).toContainEqual({
...registryTeam,
expect(config.predefinedServers).toContainEqual({
...registryServer,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
});
@ -159,19 +159,19 @@ describe('common/config', () => {
expect(config.saveLocalConfigData).toHaveBeenCalled();
});
it('should not allow teams to be set using this method', () => {
it('should not allow servers to be set using this method', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
config.localConfigData = {teams: [team]};
config.localConfigData = {teams: [server]};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData};
});
config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}]);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team);
config.set('teams', [{...buildServerWithViews, name: 'build-team-2'}]);
expect(config.localConfigData.teams).not.toContainEqual({...buildServerWithViews, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(server);
});
});
@ -186,8 +186,8 @@ describe('common/config', () => {
});
config.saveLocalConfigData = jest.fn();
config.setServers([{...buildTeamWithTabs, name: 'build-team-2'}, team], 0);
expect(config.localConfigData.teams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'});
config.setServers([{...buildServerWithViews, name: 'build-server-2'}, server], 0);
expect(config.localConfigData.teams).toContainEqual({...buildServerWithViews, name: 'build-server-2'});
expect(config.localConfigData.lastActiveTeam).toBe(0);
expect(config.regenerateCombinedConfigData).toHaveBeenCalled();
expect(config.saveLocalConfigData).toHaveBeenCalled();
@ -341,7 +341,7 @@ describe('common/config', () => {
});
});
it('should not include any teams in the combined config', () => {
it('should not include any servers in the combined config', () => {
const config = new Config();
config.reload = jest.fn();
config.init(configPath, appName, appPath);
@ -349,20 +349,20 @@ describe('common/config', () => {
config.localConfigData = {};
config.buildConfigData = {enableServerManagement: true};
config.registryConfigData = {};
config.predefinedTeams.push(team, team);
config.predefinedServers.push(server, server);
config.useNativeWindow = false;
config.localConfigData = {teams: [
team,
server,
{
...team,
name: 'local-team-2',
url: 'http://local-team-2.com',
...server,
name: 'local-server-2',
url: 'http://local-server-2.com',
},
{
...team,
name: 'local-team-1',
...server,
name: 'local-server-1',
order: 1,
url: 'http://local-team-1.com',
url: 'http://local-server-1.com',
},
]};

View file

@ -18,7 +18,7 @@ import {
} from 'types/config';
import {Logger} from 'common/log';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getDefaultViewsForConfigServer} from 'common/views/View';
import Utils, {copy} from 'common/utils/util';
import * as Validator from 'common/Validator';
@ -36,7 +36,7 @@ export class Config extends EventEmitter {
private appPath?: string;
private registryConfig: RegistryConfig;
private predefinedServers: ConfigServer[];
private _predefinedServers: ConfigServer[];
private useNativeWindow: boolean;
private combinedData?: CombinedConfig;
@ -49,9 +49,9 @@ export class Config extends EventEmitter {
constructor() {
super();
this.registryConfig = new RegistryConfig();
this.predefinedServers = [];
if (buildConfig.defaultTeams) {
this.predefinedServers.push(...buildConfig.defaultTeams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
this._predefinedServers = [];
if (buildConfig.defaultServers) {
this._predefinedServers.push(...buildConfig.defaultServers.map((server, index) => getDefaultViewsForConfigServer({...server, order: index})));
}
try {
this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2');
@ -138,10 +138,10 @@ export class Config extends EventEmitter {
this.saveLocalConfigData();
}
setServers = (servers: ConfigServer[], lastActiveTeam?: number) => {
log.debug('setServers', servers, lastActiveTeam);
setServers = (servers: ConfigServer[], lastActiveServer?: number) => {
log.debug('setServers', servers, lastActiveServer);
this.localConfigData = Object.assign({}, this.localConfigData, {teams: servers, lastActiveTeam: lastActiveTeam ?? this.localConfigData?.lastActiveTeam});
this.localConfigData = Object.assign({}, this.localConfigData, {teams: servers, lastActiveTeam: lastActiveServer ?? this.localConfigData?.lastActiveTeam});
this.regenerateCombinedConfigData();
this.saveLocalConfigData();
}
@ -172,11 +172,11 @@ export class Config extends EventEmitter {
get darkMode() {
return this.combinedData?.darkMode ?? defaultPreferences.darkMode;
}
get localTeams() {
get localServers() {
return this.localConfigData?.teams ?? defaultPreferences.teams;
}
get predefinedTeams() {
return this.predefinedServers;
get predefinedServers() {
return this._predefinedServers;
}
get enableHardwareAcceleration() {
return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration;
@ -229,7 +229,7 @@ export class Config extends EventEmitter {
get minimizeToTray() {
return this.combinedData?.minimizeToTray;
}
get lastActiveTeam() {
get lastActiveServer() {
return this.combinedData?.lastActiveTeam;
}
get alwaysClose() {
@ -252,17 +252,17 @@ export class Config extends EventEmitter {
}
/**
* Gets the teams from registry into the config object and reload
* Gets the servers from registry into the config object and reload
*
* @param {object} registryData Team configuration from the registry and if teams can be managed by user
* @param {object} registryData Server configuration from the registry and if servers can be managed by user
*/
private onLoadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.debug('loadRegistry', {registryData});
this.registryConfigData = registryData;
if (this.registryConfigData.teams) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index})));
if (this.registryConfigData.servers) {
this._predefinedServers.push(...this.registryConfigData.servers.map((server, index) => getDefaultViewsForConfigServer({...server, order: index})));
}
this.reload();
}
@ -378,7 +378,8 @@ export class Config extends EventEmitter {
// We don't want to include the servers in the combined config, they should only be accesible via the ServerManager
delete (this.combinedData as any).teams;
delete (this.combinedData as any).defaultTeams;
delete (this.combinedData as any).servers;
delete (this.combinedData as any).defaultServers;
if (this.combinedData) {
this.combinedData.appName = this.appName;

View file

@ -4,15 +4,15 @@
import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgradePreferences';
import pastDefaultPreferences from 'common/config/pastDefaultPreferences';
jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: (value) => ({
jest.mock('common/views/View', () => ({
getDefaultViewsForConfigServer: (value) => ({
...value,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
}),
@ -27,7 +27,7 @@ describe('common/config/upgradePreferences', () => {
version: 1,
teams: [
{
name: 'Primary team',
name: 'Primary server',
url: config.url,
},
],
@ -39,10 +39,10 @@ describe('common/config/upgradePreferences', () => {
const config = {
version: 1,
teams: [{
name: 'Primary team',
name: 'Primary server',
url: 'http://server-1.com',
}, {
name: 'Secondary team',
name: 'Secondary server',
url: 'http://server-2.com',
}],
showTrayIcon: true,
@ -64,11 +64,11 @@ describe('common/config/upgradePreferences', () => {
...config,
version: 2,
teams: [{
name: 'Primary team',
name: 'Primary server',
url: 'http://server-1.com',
order: 0,
}, {
name: 'Secondary team',
name: 'Secondary server',
url: 'http://server-2.com',
order: 1,
}],
@ -80,11 +80,11 @@ describe('common/config/upgradePreferences', () => {
const config = {
version: 2,
teams: [{
name: 'Primary team',
name: 'Primary server',
url: 'http://server-1.com',
order: 0,
}, {
name: 'Secondary team',
name: 'Secondary server',
url: 'http://server-2.com',
order: 1,
}],
@ -111,27 +111,27 @@ describe('common/config/upgradePreferences', () => {
...config,
version: 3,
teams: [{
name: 'Primary team',
name: 'Primary server',
url: 'http://server-1.com',
order: 0,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
}, {
name: 'Secondary team',
name: 'Secondary server',
url: 'http://server-2.com',
order: 1,
tabs: [
{
name: 'tab1',
name: 'view1',
},
{
name: 'tab2',
name: 'view2',
},
],
}],

View file

@ -4,7 +4,7 @@
import {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getDefaultViewsForConfigServer} from 'common/views/View';
import pastDefaultPreferences from './pastDefaultPreferences';
@ -15,7 +15,7 @@ function deepCopy<T>(object: T): T {
export function upgradeV0toV1(configV0: ConfigV0) {
const config = deepCopy(pastDefaultPreferences[1]);
config.teams.push({
name: 'Primary team',
name: 'Primary server',
url: configV0.url,
});
return config;
@ -37,7 +37,7 @@ export function upgradeV2toV3(configV2: ConfigV2) {
const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2);
config.version = 3;
config.teams = configV2.teams.map((value) => {
return getDefaultConfigTeamFromTeam(value);
return getDefaultViewsForConfigServer(value);
});
config.lastActiveTeam = 0;
config.spellCheckerLocales = [];

View file

@ -3,7 +3,7 @@
import {v4 as uuid} from 'uuid';
import {MattermostTeam, Team} from 'types/config';
import {UniqueServer, Server} from 'types/config';
import {parseURL} from 'common/utils/url';
@ -13,7 +13,7 @@ export class MattermostServer {
url!: URL;
isPredefined: boolean;
constructor(server: Team, isPredefined: boolean) {
constructor(server: Server, isPredefined: boolean) {
this.id = uuid();
this.name = server.name;
@ -29,7 +29,7 @@ export class MattermostServer {
}
}
toMattermostTeam = (): MattermostTeam => {
toUniqueServer = (): UniqueServer => {
return {
name: this.name,
url: this.url.toString(),

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {TAB_MESSAGING, TAB_FOCALBOARD, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import {TAB_MESSAGING, TAB_FOCALBOARD, TAB_PLAYBOOKS} from 'common/views/View';
import {parseURL, isInternalURL} from 'common/utils/url';
import Utils from 'common/utils/util';
@ -31,12 +31,12 @@ describe('common/servers/serverManager', () => {
server.url = new URL(url);
};
serverManager.servers = new Map([['server-1', server]]);
serverManager.tabs = new Map([
['tab-1', {id: 'tab-1', type: TAB_MESSAGING, isOpen: true, server}],
['tab-2', {id: 'tab-2', type: TAB_PLAYBOOKS, server}],
['tab-3', {id: 'tab-3', type: TAB_FOCALBOARD, server}],
serverManager.views = new Map([
['view-1', {id: 'view-1', type: TAB_MESSAGING, isOpen: true, server}],
['view-2', {id: 'view-2', type: TAB_PLAYBOOKS, server}],
['view-3', {id: 'view-3', type: TAB_FOCALBOARD, server}],
]);
serverManager.tabOrder = new Map([['server-1', ['tab-1', 'tab-2', 'tab-3']]]);
serverManager.viewOrder = new Map([['server-1', ['view-1', 'view-2', 'view-3']]]);
serverManager.persistServers = jest.fn();
Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0');
});
@ -52,7 +52,7 @@ describe('common/servers/serverManager', () => {
expect(serverManager.persistServers).not.toHaveBeenCalled();
});
it('should open all tabs', async () => {
it('should open all views', async () => {
serverManager.updateRemoteInfos(new Map([['server-1', {
siteURL: 'http://server-1.com',
serverVersion: '6.0.0',
@ -60,8 +60,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: true,
}]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBe(true);
expect(serverManager.tabs.get('tab-3').isOpen).toBe(true);
expect(serverManager.views.get('view-2').isOpen).toBe(true);
expect(serverManager.views.get('view-3').isOpen).toBe(true);
});
it('should open only playbooks', async () => {
@ -72,8 +72,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: false,
}]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBe(true);
expect(serverManager.tabs.get('tab-3').isOpen).toBeUndefined();
expect(serverManager.views.get('view-2').isOpen).toBe(true);
expect(serverManager.views.get('view-3').isOpen).toBeUndefined();
});
it('should open none when server version is too old', async () => {
@ -84,8 +84,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: true,
}]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBeUndefined();
expect(serverManager.tabs.get('tab-3').isOpen).toBeUndefined();
expect(serverManager.views.get('view-2').isOpen).toBeUndefined();
expect(serverManager.views.get('view-3').isOpen).toBeUndefined();
});
it('should update server URL using site URL', async () => {
@ -100,7 +100,7 @@ describe('common/servers/serverManager', () => {
});
});
describe('lookupTabByURL', () => {
describe('lookupViewByURL', () => {
const serverManager = new ServerManager();
serverManager.getAllServers = () => [
{id: 'server-1', url: new URL('http://server-1.com')},
@ -109,16 +109,16 @@ describe('common/servers/serverManager', () => {
serverManager.getOrderedTabsForServer = (serverId) => {
if (serverId === 'server-1') {
return [
{id: 'tab-1', url: new URL('http://server-1.com')},
{id: 'tab-1-type-1', url: new URL('http://server-1.com/type1')},
{id: 'tab-1-type-2', url: new URL('http://server-1.com/type2')},
{id: 'view-1', url: new URL('http://server-1.com')},
{id: 'view-1-type-1', url: new URL('http://server-1.com/type1')},
{id: 'view-1-type-2', url: new URL('http://server-1.com/type2')},
];
}
if (serverId === 'server-2') {
return [
{id: 'tab-2', url: new URL('http://server-2.com/subpath')},
{id: 'tab-2-type-1', url: new URL('http://server-2.com/subpath/type1')},
{id: 'tab-2-type-2', url: new URL('http://server-2.com/subpath/type2')},
{id: 'view-2', url: new URL('http://server-2.com/subpath')},
{id: 'view-2-type-1', url: new URL('http://server-2.com/subpath/type1')},
{id: 'view-2-type-2', url: new URL('http://server-2.com/subpath/type2')},
];
}
return [];
@ -135,47 +135,47 @@ describe('common/servers/serverManager', () => {
it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-1', url: new URL('http://server-1.com')});
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-1', url: new URL('http://server-1.com')});
});
it('should match the correct server - base tab', () => {
const inputURL = new URL('http://server-1.com/team');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-1', url: new URL('http://server-1.com')});
it('should match the correct server - base view', () => {
const inputURL = new URL('http://server-1.com/server');
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-1', url: new URL('http://server-1.com')});
});
it('should match the correct server - different tab', () => {
it('should match the correct server - different view', () => {
const inputURL = new URL('http://server-1.com/type1/app');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-1-type-1', url: new URL('http://server-1.com/type1')});
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-1-type-1', url: new URL('http://server-1.com/type1')});
});
it('should return undefined for server with subpath and URL without', () => {
const inputURL = new URL('http://server-2.com');
expect(serverManager.lookupTabByURL(inputURL)).toBe(undefined);
expect(serverManager.lookupViewByURL(inputURL)).toBe(undefined);
});
it('should return undefined for server with subpath and URL with wrong subpath', () => {
const inputURL = new URL('http://server-2.com/different/subpath');
expect(serverManager.lookupTabByURL(inputURL)).toBe(undefined);
expect(serverManager.lookupViewByURL(inputURL)).toBe(undefined);
});
it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-2', url: new URL('http://server-2.com/subpath')});
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-2', url: new URL('http://server-2.com/subpath')});
});
it('should match the correct server with a subpath - base tab', () => {
const inputURL = new URL('http://server-2.com/subpath/team');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-2', url: new URL('http://server-2.com/subpath')});
it('should match the correct server with a subpath - base view', () => {
const inputURL = new URL('http://server-2.com/subpath/server');
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-2', url: new URL('http://server-2.com/subpath')});
});
it('should match the correct server with a subpath - different tab', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/team');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-2-type-2', url: new URL('http://server-2.com/subpath/type2')});
it('should match the correct server with a subpath - different view', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/server');
expect(serverManager.lookupViewByURL(inputURL)).toStrictEqual({id: 'view-2-type-2', url: new URL('http://server-2.com/subpath/type2')});
});
it('should return undefined for wrong server', () => {
const inputURL = new URL('http://server-3.com');
expect(serverManager.lookupTabByURL(inputURL)).toBe(undefined);
expect(serverManager.lookupViewByURL(inputURL)).toBe(undefined);
});
});
});

View file

@ -3,7 +3,7 @@
import EventEmitter from 'events';
import {Team, ConfigServer, ConfigTab} from 'types/config';
import {Server, ConfigServer, ConfigView} from 'types/config';
import {RemoteInfo} from 'types/server';
import Config from 'common/config';
@ -13,10 +13,10 @@ import {
} from 'common/communication';
import {Logger, getLevel} from 'common/log';
import {MattermostServer} from 'common/servers/MattermostServer';
import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS, TabView, getDefaultTabs} from 'common/tabs/TabView';
import MessagingTabView from 'common/tabs/MessagingTabView';
import FocalboardTabView from 'common/tabs/FocalboardTabView';
import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS, MattermostView, getDefaultViews} from 'common/views/View';
import MessagingView from 'common/views/MessagingView';
import FocalboardView from 'common/views/FocalboardView';
import PlaybooksView from 'common/views/PlaybooksView';
import {isInternalURL, parseURL} from 'common/utils/url';
import Utils from 'common/utils/util';
@ -28,9 +28,9 @@ export class ServerManager extends EventEmitter {
private serverOrder: string[];
private currentServerId?: string;
private tabs: Map<string, TabView>;
private tabOrder: Map<string, string[]>;
private lastActiveTab: Map<string, string>;
private views: Map<string, MattermostView>;
private viewOrder: Map<string, string[]>;
private lastActiveView: Map<string, string>;
constructor() {
super();
@ -38,25 +38,25 @@ export class ServerManager extends EventEmitter {
this.servers = new Map();
this.remoteInfo = new Map();
this.serverOrder = [];
this.tabs = new Map();
this.tabOrder = new Map();
this.lastActiveTab = new Map();
this.views = new Map();
this.viewOrder = new Map();
this.lastActiveView = new Map();
}
getOrderedTabsForServer = (serverId: string) => {
log.withPrefix(serverId).debug('getOrderedTabsForServer');
const tabOrder = this.tabOrder.get(serverId);
if (!tabOrder) {
const viewOrder = this.viewOrder.get(serverId);
if (!viewOrder) {
return [];
}
return tabOrder.reduce((tabs, tabId) => {
const tab = this.tabs.get(tabId);
if (tab) {
tabs.push(tab);
return viewOrder.reduce((views, viewId) => {
const view = this.views.get(viewId);
if (view) {
views.push(view);
}
return tabs;
}, [] as TabView[]);
return views;
}, [] as MattermostView[]);
}
getOrderedServers = () => {
@ -87,22 +87,22 @@ export class ServerManager extends EventEmitter {
getLastActiveTabForServer = (serverId: string) => {
log.withPrefix(serverId).debug('getLastActiveTabForServer');
const lastActiveTab = this.lastActiveTab.get(serverId);
if (lastActiveTab) {
const tab = this.tabs.get(lastActiveTab);
if (tab && tab?.isOpen) {
return tab;
const lastActiveView = this.lastActiveView.get(serverId);
if (lastActiveView) {
const view = this.views.get(lastActiveView);
if (view && view?.isOpen) {
return view;
}
}
return this.getFirstOpenTabForServer(serverId);
return this.getFirstOpenViewForServer(serverId);
}
getServer = (id: string) => {
return this.servers.get(id);
}
getTab = (id: string) => {
return this.tabs.get(id);
getView = (id: string) => {
return this.views.get(id);
}
getAllServers = () => {
@ -122,7 +122,7 @@ export class ServerManager extends EventEmitter {
remoteInfos.forEach((remoteInfo, serverId) => {
this.remoteInfo.set(serverId, remoteInfo);
hasUpdates = this.updateServerURL(serverId) || hasUpdates;
hasUpdates = this.openExtraTabs(serverId) || hasUpdates;
hasUpdates = this.openExtraViews(serverId) || hasUpdates;
});
if (hasUpdates) {
@ -130,8 +130,8 @@ export class ServerManager extends EventEmitter {
}
}
lookupTabByURL = (inputURL: URL | string, ignoreScheme = false) => {
log.silly('lookupTabByURL', `${inputURL}`, ignoreScheme);
lookupViewByURL = (inputURL: URL | string, ignoreScheme = false) => {
log.silly('lookupViewByURL', `${inputURL}`, ignoreScheme);
const parsedURL = parseURL(inputURL);
if (!parsedURL) {
@ -143,17 +143,17 @@ export class ServerManager extends EventEmitter {
if (!server) {
return undefined;
}
const tabs = this.getOrderedTabsForServer(server.id);
const views = this.getOrderedTabsForServer(server.id);
let selectedTab = tabs.find((tab) => tab && tab.type === TAB_MESSAGING);
tabs.
filter((tab) => tab && tab.type !== TAB_MESSAGING).
forEach((tab) => {
if (parsedURL.pathname.match(new RegExp(`^${tab.url.pathname}(/(.+))?`))) {
selectedTab = tab;
let selectedView = views.find((view) => view && view.type === TAB_MESSAGING);
views.
filter((view) => view && view.type !== TAB_MESSAGING).
forEach((view) => {
if (parsedURL.pathname.match(new RegExp(`^${view.url.pathname}(/(.+))?`))) {
selectedView = view;
}
});
return selectedTab;
return selectedView;
}
updateServerOrder = (serverOrder: string[]) => {
@ -163,14 +163,14 @@ export class ServerManager extends EventEmitter {
this.persistServers();
}
updateTabOrder = (serverId: string, tabOrder: string[]) => {
log.withPrefix(serverId).debug('updateTabOrder', tabOrder);
updateTabOrder = (serverId: string, viewOrder: string[]) => {
log.withPrefix(serverId).debug('updateTabOrder', viewOrder);
this.tabOrder.set(serverId, tabOrder);
this.viewOrder.set(serverId, viewOrder);
this.persistServers();
}
addServer = (server: Team) => {
addServer = (server: Server) => {
const newServer = new MattermostServer(server, false);
if (this.servers.has(newServer.id)) {
@ -179,13 +179,13 @@ export class ServerManager extends EventEmitter {
this.servers.set(newServer.id, newServer);
this.serverOrder.push(newServer.id);
const tabOrder: string[] = [];
getDefaultTabs().forEach((tab) => {
const newTab = this.getTabView(newServer, tab.name, tab.isOpen);
this.tabs.set(newTab.id, newTab);
tabOrder.push(newTab.id);
const viewOrder: string[] = [];
getDefaultViews().forEach((view) => {
const newView = this.getNewView(newServer, view.name, view.isOpen);
this.views.set(newView.id, newView);
viewOrder.push(newView.id);
});
this.tabOrder.set(newServer.id, tabOrder);
this.viewOrder.set(newServer.id, viewOrder);
if (!this.currentServerId) {
this.currentServerId = newServer.id;
@ -197,7 +197,7 @@ export class ServerManager extends EventEmitter {
return newServer;
}
editServer = (serverId: string, server: Team) => {
editServer = (serverId: string, server: Server) => {
const existingServer = this.servers.get(serverId);
if (!existingServer) {
return;
@ -212,11 +212,11 @@ export class ServerManager extends EventEmitter {
existingServer.updateURL(server.url);
this.servers.set(serverId, existingServer);
this.tabOrder.get(serverId)?.forEach((tabId) => {
const tab = this.tabs.get(tabId);
if (tab) {
tab.server = existingServer;
this.tabs.set(tabId, tab);
this.viewOrder.get(serverId)?.forEach((viewId) => {
const view = this.views.get(viewId);
if (view) {
view.server = existingServer;
this.views.set(viewId, view);
}
});
@ -225,9 +225,9 @@ export class ServerManager extends EventEmitter {
}
removeServer = (serverId: string) => {
this.tabOrder.get(serverId)?.forEach((tabId) => this.tabs.delete(tabId));
this.tabOrder.delete(serverId);
this.lastActiveTab.delete(serverId);
this.viewOrder.get(serverId)?.forEach((viewId) => this.views.delete(viewId));
this.viewOrder.delete(serverId);
this.lastActiveView.delete(serverId);
const index = this.serverOrder.findIndex((id) => id === serverId);
this.serverOrder.splice(index, 1);
@ -241,26 +241,26 @@ export class ServerManager extends EventEmitter {
this.persistServers();
}
setTabIsOpen = (tabId: string, isOpen: boolean) => {
const tab = this.tabs.get(tabId);
if (!tab) {
setViewIsOpen = (viewId: string, isOpen: boolean) => {
const view = this.views.get(viewId);
if (!view) {
return;
}
tab.isOpen = isOpen;
view.isOpen = isOpen;
this.persistServers();
}
updateLastActive = (tabId: string) => {
const tab = this.tabs.get(tabId);
if (!tab) {
updateLastActive = (viewId: string) => {
const view = this.views.get(viewId);
if (!view) {
return;
}
this.lastActiveTab.set(tab.server.id, tabId);
this.lastActiveView.set(view.server.id, viewId);
this.currentServerId = tab.server.id;
this.currentServerId = view.server.id;
const serverOrder = this.serverOrder.findIndex((srv) => srv === tab.server.id);
const serverOrder = this.serverOrder.findIndex((srv) => srv === view.server.id);
if (serverOrder < 0) {
throw new Error('Server order corrupt, ID not found.');
}
@ -270,26 +270,26 @@ export class ServerManager extends EventEmitter {
reloadFromConfig = () => {
const serverOrder: string[] = [];
Config.predefinedTeams.forEach((team) => {
const id = this.initServer(team, true);
Config.predefinedServers.forEach((server) => {
const id = this.initServer(server, true);
serverOrder.push(id);
});
if (Config.enableServerManagement) {
Config.localTeams.sort((a, b) => a.order - b.order).forEach((team) => {
const id = this.initServer(team, false);
Config.localServers.sort((a, b) => a.order - b.order).forEach((server) => {
const id = this.initServer(server, false);
serverOrder.push(id);
});
}
this.filterOutDuplicateTeams();
this.filterOutDuplicateServers();
this.serverOrder = serverOrder;
if (Config.lastActiveTeam && this.serverOrder[Config.lastActiveTeam]) {
this.currentServerId = this.serverOrder[Config.lastActiveTeam];
if (Config.lastActiveServer && this.serverOrder[Config.lastActiveServer]) {
this.currentServerId = this.serverOrder[Config.lastActiveServer];
} else {
this.currentServerId = this.serverOrder[0];
}
}
private filterOutDuplicateTeams = () => {
private filterOutDuplicateServers = () => {
const servers = [...this.servers.keys()].map((key) => ({key, value: this.servers.get(key)!}));
const uniqueServers = new Set();
servers.forEach((server) => {
@ -301,38 +301,38 @@ export class ServerManager extends EventEmitter {
});
}
private initServer = (team: ConfigServer, isPredefined: boolean) => {
const server = new MattermostServer(team, isPredefined);
private initServer = (configServer: ConfigServer, isPredefined: boolean) => {
const server = new MattermostServer(configServer, isPredefined);
this.servers.set(server.id, server);
log.withPrefix(server.id).debug('initialized server');
const tabOrder: string[] = [];
team.tabs.sort((a, b) => a.order - b.order).forEach((tab) => {
const tabView = this.getTabView(server, tab.name, tab.isOpen);
log.withPrefix(tabView.id).debug('initialized tab');
const viewOrder: string[] = [];
configServer.tabs.sort((a, b) => a.order - b.order).forEach((view) => {
const mattermostView = this.getNewView(server, view.name, view.isOpen);
log.withPrefix(mattermostView.id).debug('initialized view');
this.tabs.set(tabView.id, tabView);
tabOrder.push(tabView.id);
this.views.set(mattermostView.id, mattermostView);
viewOrder.push(mattermostView.id);
});
this.tabOrder.set(server.id, tabOrder);
if (typeof team.lastActiveTab !== 'undefined') {
this.lastActiveTab.set(server.id, tabOrder[team.lastActiveTab]);
this.viewOrder.set(server.id, viewOrder);
if (typeof configServer.lastActiveTab !== 'undefined') {
this.lastActiveView.set(server.id, viewOrder[configServer.lastActiveTab]);
}
return server.id;
}
private getFirstOpenTabForServer = (serverId: string) => {
const tabOrder = this.getOrderedTabsForServer(serverId);
const openTabs = tabOrder.filter((tab) => tab.isOpen);
const firstTab = openTabs[0];
if (!firstTab) {
throw new Error(`No tabs open for server id ${serverId}`);
private getFirstOpenViewForServer = (serverId: string) => {
const viewOrder = this.getOrderedTabsForServer(serverId);
const openViews = viewOrder.filter((view) => view.isOpen);
const firstView = openViews[0];
if (!firstView) {
throw new Error(`No views open for server id ${serverId}`);
}
return firstTab;
return firstView;
}
private persistServers = async (lastActiveTeam?: number) => {
private persistServers = async (lastActiveServer?: number) => {
this.emit(SERVERS_UPDATE);
const localServers = [...this.servers.values()].
@ -343,18 +343,18 @@ export class ServerManager extends EventEmitter {
servers.push(this.toConfigServer(srv));
return servers;
}, [] as ConfigServer[]);
await Config.setServers(localServers, lastActiveTeam);
await Config.setServers(localServers, lastActiveServer);
}
private getLastActiveTab = (serverId: string) => {
let lastActiveTab: number | undefined;
if (this.lastActiveTab.has(serverId)) {
const index = this.tabOrder.get(serverId)?.indexOf(this.lastActiveTab.get(serverId)!);
private getLastActiveView = (serverId: string) => {
let lastActiveView: number | undefined;
if (this.lastActiveView.has(serverId)) {
const index = this.viewOrder.get(serverId)?.indexOf(this.lastActiveView.get(serverId)!);
if (typeof index !== 'undefined' && index >= 0) {
lastActiveTab = index;
lastActiveView = index;
}
}
return lastActiveTab;
return lastActiveView;
}
private toConfigServer = (server: MattermostServer): ConfigServer => {
@ -362,30 +362,30 @@ export class ServerManager extends EventEmitter {
name: server.name,
url: `${server.url}`,
order: this.serverOrder.indexOf(server.id),
lastActiveTab: this.getLastActiveTab(server.id),
tabs: this.tabOrder.get(server.id)?.reduce((tabs, tabId, index) => {
const tab = this.tabs.get(tabId);
if (!tab) {
return tabs;
lastActiveTab: this.getLastActiveView(server.id),
tabs: this.viewOrder.get(server.id)?.reduce((views, viewId, index) => {
const view = this.views.get(viewId);
if (!view) {
return views;
}
tabs.push({
name: tab?.type,
views.push({
name: view?.type,
order: index,
isOpen: tab.isOpen,
isOpen: view.isOpen,
});
return tabs;
}, [] as ConfigTab[]) ?? [],
return views;
}, [] as ConfigView[]) ?? [],
};
}
private getTabView = (srv: MattermostServer, tabName: string, isOpen?: boolean) => {
switch (tabName) {
private getNewView = (srv: MattermostServer, viewName: string, isOpen?: boolean) => {
switch (viewName) {
case TAB_MESSAGING:
return new MessagingTabView(srv, isOpen);
return new MessagingView(srv, isOpen);
case TAB_FOCALBOARD:
return new FocalboardTabView(srv, isOpen);
return new FocalboardView(srv, isOpen);
case TAB_PLAYBOOKS:
return new PlaybooksTabView(srv, isOpen);
return new PlaybooksView(srv, isOpen);
default:
throw new Error('Not implemeneted');
}
@ -407,7 +407,7 @@ export class ServerManager extends EventEmitter {
return false;
}
private openExtraTabs = (serverId: string) => {
private openExtraViews = (serverId: string) => {
const server = this.servers.get(serverId);
const remoteInfo = this.remoteInfo.get(serverId);
@ -420,21 +420,21 @@ export class ServerManager extends EventEmitter {
}
let hasUpdates = false;
const tabOrder = this.tabOrder.get(serverId);
if (tabOrder) {
tabOrder.forEach((tabId) => {
const tab = this.tabs.get(tabId);
if (tab) {
if (tab.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof tab.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Playbooks');
tab.isOpen = true;
this.tabs.set(tabId, tab);
const viewOrder = this.viewOrder.get(serverId);
if (viewOrder) {
viewOrder.forEach((viewId) => {
const view = this.views.get(viewId);
if (view) {
if (view.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof view.isOpen === 'undefined') {
log.withPrefix(view.id).verbose('opening Playbooks');
view.isOpen = true;
this.views.set(viewId, view);
hasUpdates = true;
}
if (tab.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof tab.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Boards');
tab.isOpen = true;
this.tabs.set(tabId, tab);
if (view.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof view.isOpen === 'undefined') {
log.withPrefix(view.id).verbose('opening Boards');
view.isOpen = true;
this.views.set(viewId, view);
hasUpdates = true;
}
}
@ -457,7 +457,7 @@ export class ServerManager extends EventEmitter {
};
getViewLog = (viewId: string, ...additionalPrefixes: string[]) => {
const view = this.getTab(viewId);
const view = this.getView(viewId);
if (!view) {
return new Logger(viewId);
}

View file

@ -14,11 +14,11 @@ import {
isTrustedURL,
} from 'common/utils/url';
jest.mock('common/tabs/TabView', () => ({
getServerView: (srv, tab) => {
jest.mock('common/views/View', () => ({
getServerView: (srv, view) => {
return {
name: `${srv.name}_${tab.name}`,
url: `${srv.url}${srv.url.toString().endsWith('/') ? '' : '/'}${tab.name.split('-')[1] || ''}`,
name: `${srv.name}_${view.name}`,
url: `${srv.url}${srv.url.toString().endsWith('/') ? '' : '/'}${view.name.split('-')[1] || ''}`,
};
},
}));

View file

@ -119,8 +119,8 @@ describe('common/utils/util', () => {
describe('escapeRegex', () => {
it('should escape special chars in string when used inside regex', () => {
const str = 'Language C++';
const regex = `${escapeRegex(str)}___TAB_[A-Z]+`;
expect(new RegExp(regex).test('Language C++___TAB_ABCDEF')).toBe(true);
const regex = `${escapeRegex(str)}___VIEW_[A-Z]+`;
expect(new RegExp(regex).test('Language C++___VIEW_ABCDEF')).toBe(true);
});
});
});

View file

@ -3,13 +3,13 @@
import {v4 as uuid} from 'uuid';
import {MattermostTab} from 'types/config';
import {UniqueView} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer';
import {TabType, TabView} from './TabView';
import {ViewType, MattermostView} from './View';
export default abstract class BaseTabView implements TabView {
export default abstract class BaseView implements MattermostView {
id: string;
server: MattermostServer;
isOpen?: boolean;
@ -22,14 +22,14 @@ export default abstract class BaseTabView implements TabView {
get url(): URL {
throw new Error('Not implemented');
}
get type(): TabType {
get type(): ViewType {
throw new Error('Not implemented');
}
get shouldNotify(): boolean {
return false;
}
toMattermostTab = (): MattermostTab => {
toUniqueView = (): UniqueView => {
return {
id: this.id,
name: this.type,

View file

@ -3,15 +3,15 @@
import {getFormattedPathName} from 'common/utils/url';
import BaseTabView from './BaseTabView';
import {TabType, TAB_FOCALBOARD} from './TabView';
import BaseView from './BaseView';
import {ViewType, TAB_FOCALBOARD} from './View';
export default class FocalboardTabView extends BaseTabView {
export default class FocalboardView extends BaseView {
get url(): URL {
return new URL(`${this.server.url.origin}${getFormattedPathName(this.server.url.pathname)}boards`);
}
get type(): TabType {
get type(): ViewType {
return TAB_FOCALBOARD;
}
}

View file

@ -1,15 +1,15 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import BaseTabView from './BaseTabView';
import {TabType, TAB_MESSAGING} from './TabView';
import BaseView from './BaseView';
import {ViewType, TAB_MESSAGING} from './View';
export default class MessagingTabView extends BaseTabView {
export default class MessagingView extends BaseView {
get url(): URL {
return this.server.url;
}
get type(): TabType {
get type(): ViewType {
return TAB_MESSAGING;
}

View file

@ -3,15 +3,15 @@
import {getFormattedPathName} from 'common/utils/url';
import BaseTabView from './BaseTabView';
import {TabType, TAB_PLAYBOOKS} from './TabView';
import BaseView from './BaseView';
import {ViewType, TAB_PLAYBOOKS} from './View';
export default class PlaybooksTabView extends BaseTabView {
export default class PlaybooksView extends BaseView {
get url(): URL {
return new URL(`${this.server.url.origin}${getFormattedPathName(this.server.url.pathname)}playbooks`);
}
get type(): TabType {
get type(): ViewType {
return TAB_PLAYBOOKS;
}
}

View file

@ -1,35 +1,35 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MattermostTab, Team} from 'types/config';
import {UniqueView, Server} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer';
export const TAB_MESSAGING = 'TAB_MESSAGING';
export const TAB_FOCALBOARD = 'TAB_FOCALBOARD';
export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS';
export type TabType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS;
export type ViewType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS;
export interface TabView {
export interface MattermostView {
id: string;
server: MattermostServer;
isOpen?: boolean;
get type(): TabType;
get type(): ViewType;
get url(): URL;
get shouldNotify(): boolean;
toMattermostTab(): MattermostTab;
toUniqueView(): UniqueView;
}
export function getDefaultConfigTeamFromTeam(team: Team & {order: number; lastActiveTab?: number}) {
export function getDefaultViewsForConfigServer(server: Server & {order: number; lastActiveView?: number}) {
return {
...team,
tabs: getDefaultTabs(),
...server,
tabs: getDefaultViews(),
};
}
export function getDefaultTabs() {
export function getDefaultViews() {
return [
{
name: TAB_MESSAGING,
@ -47,8 +47,8 @@ export function getDefaultTabs() {
];
}
export function getTabDisplayName(tabType: TabType) {
switch (tabType) {
export function getViewDisplayName(viewType: ViewType) {
switch (viewType) {
case TAB_MESSAGING:
return 'Channels';
case TAB_FOCALBOARD:
@ -60,6 +60,6 @@ export function getTabDisplayName(tabType: TabType) {
}
}
export function canCloseTab(tabType: TabType) {
return tabType !== TAB_MESSAGING;
export function canCloseView(viewType: ViewType) {
return viewType !== TAB_MESSAGING;
}

View file

@ -95,9 +95,9 @@ describe('main/app/app', () => {
const promise = Promise.resolve({});
const certificate = {};
const view = {
tab: {
view: {
server: {
name: 'test-team',
name: 'test-server',
url: new URL(testURL),
},
},
@ -163,7 +163,7 @@ describe('main/app/app', () => {
expect(CertificateStore.save).toHaveBeenCalled();
});
it('should load URL using MattermostView when trusting certificate', async () => {
it('should load URL using MattermostBrowserView when trusting certificate', async () => {
dialog.showMessageBox.mockResolvedValue({response: 0});
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
expect(callback).toHaveBeenCalledWith(true);

View file

@ -96,8 +96,8 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
const errorID = `${parsedURL.origin}:${error}`;
const view = ViewManager.getViewByWebContentsId(webContents.id);
if (view?.tab.server) {
const serverURL = parseURL(view.tab.server.url);
if (view?.view.server) {
const serverURL = parseURL(view.view.server.url);
if (serverURL && serverURL.origin !== parsedURL.origin) {
log.warn(`Ignoring certificate for unmatched origin ${parsedURL.origin}, will not trust`);
callback(false);

View file

@ -93,14 +93,14 @@ describe('main/app/config', () => {
});
});
it('should recheck teams after config update if registry data is pulled in', () => {
it('should recheck servers after config update if registry data is pulled in', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'win32',
});
Config.registryConfigData = {};
handleConfigUpdate({teams: []});
handleConfigUpdate({servers: []});
expect(handleMainWindowIsShown).toHaveBeenCalled();
Object.defineProperty(process, 'platform', {

View file

@ -6,7 +6,7 @@
import {initialize} from './initialize';
// TODO: Singletons, we need DI :D
import('main/views/teamDropdownView');
import('main/views/serverDropdownView');
import('main/views/downloadsDropdownMenuView');
import('main/views/downloadsDropdownView');

View file

@ -278,7 +278,7 @@ describe('main/app/initialize', () => {
it('should allow permission requests for supported types from trusted URLs', async () => {
ViewManager.getViewByWebContentsId.mockReturnValue({
tab: {
view: {
server: {
url: new URL('http://server-1.com'),
},

View file

@ -14,8 +14,8 @@ import {
SHOW_NEW_SERVER_MODAL,
NOTIFY_MENTION,
SWITCH_TAB,
CLOSE_TAB,
OPEN_TAB,
CLOSE_VIEW,
OPEN_VIEW,
SHOW_EDIT_SERVER_MODAL,
SHOW_REMOVE_SERVER_MODAL,
UPDATE_SHORTCUT_MENU,
@ -100,8 +100,8 @@ import {
switchServer,
} from './servers';
import {
handleCloseTab, handleGetLastActive, handleGetOrderedTabsForServer, handleOpenTab,
} from './tabs';
handleCloseView, handleGetLastActive, handleGetOrderedViewsForServer, handleOpenView,
} from './views';
import {
clearAppCache,
getDeeplinkingURL,
@ -279,8 +279,8 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(SWITCH_SERVER, (event, serverId) => switchServer(serverId));
ipcMain.on(SWITCH_TAB, (event, viewId) => ViewManager.showById(viewId));
ipcMain.on(CLOSE_TAB, handleCloseTab);
ipcMain.on(OPEN_TAB, handleOpenTab);
ipcMain.on(CLOSE_VIEW, handleCloseView);
ipcMain.on(OPEN_VIEW, handleOpenView);
ipcMain.on(QUIT, handleQuit);
@ -296,10 +296,10 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(UPDATE_CONFIGURATION, updateConfiguration);
ipcMain.on(UPDATE_SERVER_ORDER, (event, serverOrder) => ServerManager.updateServerOrder(serverOrder));
ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, tabOrder) => ServerManager.updateTabOrder(serverId, tabOrder));
ipcMain.on(UPDATE_TAB_ORDER, (event, serverId, viewOrder) => ServerManager.updateTabOrder(serverId, viewOrder));
ipcMain.handle(GET_LAST_ACTIVE, handleGetLastActive);
ipcMain.handle(GET_ORDERED_SERVERS, () => ServerManager.getOrderedServers().map((srv) => srv.toMattermostTeam()));
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, handleGetOrderedTabsForServer);
ipcMain.handle(GET_ORDERED_SERVERS, () => ServerManager.getOrderedServers().map((srv) => srv.toUniqueServer()));
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, handleGetOrderedViewsForServer);
ipcMain.handle(GET_DARK_MODE, handleGetDarkMode);
ipcMain.on(WINDOW_CLOSE, handleClose);
@ -453,7 +453,7 @@ async function initializeAfterAppReady() {
}
const requestingURL = webContents.getURL();
const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url;
const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.view.server.url;
if (!serverURL) {
callback(false);

View file

@ -16,14 +16,14 @@ jest.mock('common/config', () => ({
}));
jest.mock('main/notifications', () => ({}));
jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(),
setViewIsOpen: jest.fn(),
getAllServers: jest.fn(),
hasServers: jest.fn(),
addServer: jest.fn(),
editServer: jest.fn(),
removeServer: jest.fn(),
getServer: jest.fn(),
getTab: jest.fn(),
getView: jest.fn(),
getLastActiveTabForServer: jest.fn(),
}));
jest.mock('main/utils', () => ({

View file

@ -3,7 +3,7 @@
import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import {MentionData} from 'types/notification';
import {Logger} from 'common/log';
@ -93,11 +93,11 @@ export function handleWelcomeScreenModal() {
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal<MattermostTeam[], MattermostTeam>('welcomeScreen', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers());
const modalPromise = ModalManager.addModal<UniqueServer[], UniqueServer>('welcomeScreen', html, preload, ServerManager.getAllServers().map((server) => server.toUniqueServer()), mainWindow, !ServerManager.hasServers());
if (modalPromise) {
modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data);
switchServer(newTeam.id, true);
const newServer = ServerManager.addServer(data);
switchServer(newServer.id, true);
}).catch((e) => {
// e is undefined for user cancellation
if (e) {

View file

@ -2,7 +2,7 @@
// See LICENSE.txt for license information.
import ServerManager from 'common/servers/serverManager';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getDefaultViewsForConfigServer} from 'common/views/View';
import ModalManager from 'main/views/modalManager';
import {getLocalURLString, getLocalPreload} from 'main/utils';
@ -18,19 +18,19 @@ jest.mock('electron', () => ({
}));
jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(),
setViewIsOpen: jest.fn(),
getAllServers: jest.fn(),
hasServers: jest.fn(),
addServer: jest.fn(),
editServer: jest.fn(),
removeServer: jest.fn(),
getServer: jest.fn(),
getTab: jest.fn(),
getView: jest.fn(),
getLastActiveTabForServer: jest.fn(),
getServerLog: jest.fn(),
}));
jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: jest.fn(),
jest.mock('common/views/View', () => ({
getDefaultViewsForConfigServer: jest.fn(),
}));
jest.mock('main/views/modalManager', () => ({
addModal: jest.fn(),
@ -50,22 +50,22 @@ jest.mock('main/views/viewManager', () => ({
const tabs = [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
name: 'view-3',
order: 1,
isOpen: true,
},
];
const teams = [
const servers = [
{
id: 'server-1',
name: 'server-1',
@ -77,9 +77,9 @@ const teams = [
describe('main/app/servers', () => {
describe('switchServer', () => {
const views = new Map([
['tab-1', {id: 'tab-1'}],
['tab-2', {id: 'tab-2'}],
['tab-3', {id: 'tab-3'}],
['view-1', {id: 'view-1'}],
['view-2', {id: 'view-2'}],
['view-3', {id: 'view-3'}],
]);
beforeEach(() => {
@ -119,69 +119,69 @@ describe('main/app/servers', () => {
expect(ViewManager.showById).not.toBeCalled();
});
it('should show first open tab in order when last active not defined', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'});
it('should show first open view in order when last active not defined', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-3'});
Servers.switchServer('server-1');
expect(ViewManager.showById).toHaveBeenCalledWith('tab-3');
expect(ViewManager.showById).toHaveBeenCalledWith('view-3');
});
it('should show last active tab of chosen server', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'});
it('should show last active view of chosen server', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-2'});
Servers.switchServer('server-2');
expect(ViewManager.showById).toHaveBeenCalledWith('tab-2');
expect(ViewManager.showById).toHaveBeenCalledWith('view-2');
});
it('should wait for view to exist if specified', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'});
views.delete('tab-3');
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-3'});
views.delete('view-3');
Servers.switchServer('server-1', true);
expect(ViewManager.showById).not.toBeCalled();
jest.advanceTimersByTime(200);
expect(ViewManager.showById).not.toBeCalled();
views.set('tab-3', {});
views.set('view-3', {});
jest.advanceTimersByTime(200);
expect(ViewManager.showById).toBeCalledWith('tab-3');
expect(ViewManager.showById).toBeCalledWith('view-3');
});
});
describe('handleNewServerModal', () => {
let teamsCopy;
let serversCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
serversCopy = JSON.parse(JSON.stringify(servers));
ServerManager.getAllServers.mockReturnValue([]);
ServerManager.addServer.mockImplementation(() => {
const newTeam = {
const newServer = {
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
name: 'new-server',
url: 'http://new-server.com',
tabs,
};
teamsCopy = [
...teamsCopy,
newTeam,
serversCopy = [
...serversCopy,
newServer,
];
return newTeam;
return newServer;
});
ServerManager.hasServers.mockReturnValue(Boolean(teamsCopy.length));
ServerManager.hasServers.mockReturnValue(Boolean(serversCopy.length));
ServerManager.getServerLog.mockReturnValue({debug: jest.fn(), error: jest.fn()});
getDefaultConfigTeamFromTeam.mockImplementation((team) => ({
...team,
getDefaultViewsForConfigServer.mockImplementation((server) => ({
...server,
tabs,
}));
});
it('should add new team to the config', async () => {
it('should add new server to the config', async () => {
const data = {
name: 'new-team',
url: 'http://new-team.com',
name: 'new-server',
url: 'http://new-server.com',
};
const promise = Promise.resolve(data);
ModalManager.addModal.mockReturnValue(promise);
@ -190,10 +190,10 @@ describe('main/app/servers', () => {
await promise;
expect(ServerManager.addServer).toHaveBeenCalledWith(data);
expect(teamsCopy).toContainEqual(expect.objectContaining({
expect(serversCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
name: 'new-server',
url: 'http://new-server.com',
tabs,
}));
@ -203,31 +203,31 @@ describe('main/app/servers', () => {
});
describe('handleEditServerModal', () => {
let teamsCopy;
let serversCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
serversCopy = JSON.parse(JSON.stringify(servers));
ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
if (id !== serversCopy[0].id) {
return undefined;
}
return {...teamsCopy[0], toMattermostTeam: jest.fn()};
return {...serversCopy[0], toUniqueServer: jest.fn()};
});
ServerManager.editServer.mockImplementation((id, team) => {
if (id !== teamsCopy[0].id) {
ServerManager.editServer.mockImplementation((id, server) => {
if (id !== serversCopy[0].id) {
return;
}
const newTeam = {
...teamsCopy[0],
...team,
const newServer = {
...serversCopy[0],
...server,
};
teamsCopy = [newTeam];
serversCopy = [newServer];
});
ServerManager.getAllServers.mockReturnValue(teamsCopy.map((team) => ({...team, toMattermostTeam: jest.fn()})));
ServerManager.getAllServers.mockReturnValue(serversCopy.map((server) => ({...server, toUniqueServer: jest.fn()})));
});
it('should do nothing when the server cannot be found', () => {
@ -235,58 +235,58 @@ describe('main/app/servers', () => {
expect(ModalManager.addModal).not.toBeCalled();
});
it('should edit the existing team', async () => {
it('should edit the existing server', async () => {
const promise = Promise.resolve({
name: 'new-team',
url: 'http://new-team.com',
name: 'new-server',
url: 'http://new-server.com',
});
ModalManager.addModal.mockReturnValue(promise);
Servers.handleEditServerModal(null, 'server-1');
await promise;
expect(teamsCopy).not.toContainEqual(expect.objectContaining({
expect(serversCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
tabs,
}));
expect(teamsCopy).toContainEqual(expect.objectContaining({
expect(serversCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
name: 'new-server',
url: 'http://new-server.com',
tabs,
}));
});
});
describe('handleRemoveServerModal', () => {
let teamsCopy;
let serversCopy;
beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({});
teamsCopy = JSON.parse(JSON.stringify(teams));
serversCopy = JSON.parse(JSON.stringify(servers));
ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
if (id !== serversCopy[0].id) {
return undefined;
}
return teamsCopy[0];
return serversCopy[0];
});
ServerManager.removeServer.mockImplementation(() => {
teamsCopy = [];
serversCopy = [];
});
ServerManager.getAllServers.mockReturnValue(teamsCopy);
ServerManager.getAllServers.mockReturnValue(serversCopy);
});
it('should remove the existing team', async () => {
it('should remove the existing server', async () => {
const promise = Promise.resolve(true);
ModalManager.addModal.mockReturnValue(promise);
Servers.handleRemoveServerModal(null, 'server-1');
await promise;
expect(teamsCopy).not.toContainEqual(expect.objectContaining({
expect(serversCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
@ -294,11 +294,11 @@ describe('main/app/servers', () => {
}));
});
it('should not remove the existing team when clicking Cancel', async () => {
it('should not remove the existing server when clicking Cancel', async () => {
const promise = Promise.resolve(false);
ModalManager.addModal.mockReturnValue(promise);
expect(teamsCopy).toContainEqual(expect.objectContaining({
expect(serversCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',
@ -307,7 +307,7 @@ describe('main/app/servers', () => {
Servers.handleRemoveServerModal(null, 'server-1');
await promise;
expect(teamsCopy).toContainEqual(expect.objectContaining({
expect(serversCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1',
url: 'http://server-1.com',

View file

@ -3,7 +3,7 @@
import {IpcMainEvent, ipcMain} from 'electron';
import {MattermostTeam, Team} from 'types/config';
import {UniqueServer, Server} from 'types/config';
import {UPDATE_SHORTCUT_MENU} from 'common/communication';
import {Logger} from 'common/log';
@ -24,16 +24,16 @@ export const switchServer = (serverId: string, waitForViewToExist = false) => {
ServerManager.getServerLog(serverId, 'WindowManager').error('Cannot find server in config');
return;
}
const nextTab = ServerManager.getLastActiveTabForServer(serverId);
const nextView = ServerManager.getLastActiveTabForServer(serverId);
if (waitForViewToExist) {
const timeout = setInterval(() => {
if (ViewManager.getView(nextTab.id)) {
ViewManager.showById(nextTab.id);
if (ViewManager.getView(nextView.id)) {
ViewManager.showById(nextView.id);
clearInterval(timeout);
}
}, 100);
} else {
ViewManager.showById(nextTab.id);
ViewManager.showById(nextView.id);
}
ipcMain.emit(UPDATE_SHORTCUT_MENU);
};
@ -49,11 +49,11 @@ export const handleNewServerModal = () => {
if (!mainWindow) {
return;
}
const modalPromise = ModalManager.addModal<MattermostTeam[], Team>('newServer', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers());
const modalPromise = ModalManager.addModal<UniqueServer[], Server>('newServer', html, preload, ServerManager.getAllServers().map((server) => server.toUniqueServer()), mainWindow, !ServerManager.hasServers());
if (modalPromise) {
modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data);
switchServer(newTeam.id, true);
const newServer = ServerManager.addServer(data);
switchServer(newServer.id, true);
}).catch((e) => {
// e is undefined for user cancellation
if (e) {
@ -80,13 +80,13 @@ export const handleEditServerModal = (e: IpcMainEvent, id: string) => {
if (!server) {
return;
}
const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>(
const modalPromise = ModalManager.addModal<{currentServers: UniqueServer[]; server: UniqueServer}, Server>(
'editServer',
html,
preload,
{
currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()),
team: server.toMattermostTeam(),
currentServers: ServerManager.getAllServers().map((server) => server.toUniqueServer()),
server: server.toUniqueServer(),
},
mainWindow);
if (modalPromise) {

View file

@ -1,39 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ServerManager from 'common/servers/serverManager';
import ViewManager from 'main/views/viewManager';
import {
handleCloseTab,
handleOpenTab,
} from './tabs';
jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(),
getTab: jest.fn(),
getLastActiveTabForServer: jest.fn(),
}));
jest.mock('main/views/viewManager', () => ({
showById: jest.fn(),
}));
describe('main/app/tabs', () => {
describe('handleCloseTab', () => {
it('should close the specified tab and switch to the next open tab', () => {
ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'});
handleCloseTab(null, 'tab-3');
expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false);
expect(ViewManager.showById).toBeCalledWith('tab-2');
});
});
describe('handleOpenTab', () => {
it('should open the specified tab', () => {
handleOpenTab(null, 'tab-1');
expect(ViewManager.showById).toBeCalledWith('tab-1');
});
});
});

View file

@ -1,73 +0,0 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log';
import ViewManager from 'main/views/viewManager';
const log = new Logger('App.Tabs');
export const handleCloseTab = (event: IpcMainEvent, tabId: string) => {
log.debug('handleCloseTab', {tabId});
const tab = ServerManager.getTab(tabId);
if (!tab) {
return;
}
ServerManager.setTabIsOpen(tabId, false);
const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id);
ViewManager.showById(nextTab.id);
};
export const handleOpenTab = (event: IpcMainEvent, tabId: string) => {
log.debug('handleOpenTab', {tabId});
ServerManager.setTabIsOpen(tabId, true);
ViewManager.showById(tabId);
};
export const selectNextTab = () => {
selectTab((order) => order + 1);
};
export const selectPreviousTab = () => {
selectTab((order, length) => (length + (order - 1)));
};
export const handleGetOrderedTabsForServer = (event: IpcMainInvokeEvent, serverId: string) => {
return ServerManager.getOrderedTabsForServer(serverId).map((tab) => tab.toMattermostTab());
};
export const handleGetLastActive = () => {
const server = ServerManager.getCurrentServer();
const tab = ServerManager.getLastActiveTabForServer(server.id);
return {server: server.id, tab: tab.id};
};
const selectTab = (fn: (order: number, length: number) => number) => {
const currentView = ViewManager.getCurrentView();
if (!currentView) {
return;
}
const currentTeamTabs = ServerManager.getOrderedTabsForServer(currentView.tab.server.id).map((tab, index) => ({tab, index}));
const filteredTabs = currentTeamTabs?.filter((tab) => tab.tab.isOpen);
const currentTab = currentTeamTabs?.find((tab) => tab.tab.type === currentView.tab.type);
if (!currentTeamTabs || !currentTab || !filteredTabs) {
return;
}
let currentOrder = currentTab.index;
let nextIndex = -1;
while (nextIndex === -1) {
const nextOrder = (fn(currentOrder, currentTeamTabs.length) % currentTeamTabs.length);
nextIndex = filteredTabs.findIndex((tab) => tab.index === nextOrder);
currentOrder = nextOrder;
}
const newTab = filteredTabs[nextIndex].tab;
ViewManager.showById(newTab.id);
};

View file

@ -0,0 +1,39 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import ServerManager from 'common/servers/serverManager';
import ViewManager from 'main/views/viewManager';
import {
handleCloseView,
handleOpenView,
} from './views';
jest.mock('common/servers/serverManager', () => ({
setViewIsOpen: jest.fn(),
getView: jest.fn(),
getLastActiveTabForServer: jest.fn(),
}));
jest.mock('main/views/viewManager', () => ({
showById: jest.fn(),
}));
describe('main/app/views', () => {
describe('handleCloseView', () => {
it('should close the specified view and switch to the next open view', () => {
ServerManager.getView.mockReturnValue({server: {id: 'server-1'}});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-2'});
handleCloseView(null, 'view-3');
expect(ServerManager.setViewIsOpen).toBeCalledWith('view-3', false);
expect(ViewManager.showById).toBeCalledWith('view-2');
});
});
describe('handleOpenView', () => {
it('should open the specified view', () => {
handleOpenView(null, 'view-1');
expect(ViewManager.showById).toBeCalledWith('view-1');
});
});
});

73
src/main/app/views.ts Normal file
View file

@ -0,0 +1,73 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log';
import ViewManager from 'main/views/viewManager';
const log = new Logger('App.Views');
export const handleCloseView = (event: IpcMainEvent, viewId: string) => {
log.debug('handleCloseView', {viewId});
const view = ServerManager.getView(viewId);
if (!view) {
return;
}
ServerManager.setViewIsOpen(viewId, false);
const nextView = ServerManager.getLastActiveTabForServer(view.server.id);
ViewManager.showById(nextView.id);
};
export const handleOpenView = (event: IpcMainEvent, viewId: string) => {
log.debug('handleOpenView', {viewId});
ServerManager.setViewIsOpen(viewId, true);
ViewManager.showById(viewId);
};
export const selectNextView = () => {
selectView((order) => order + 1);
};
export const selectPreviousView = () => {
selectView((order, length) => (length + (order - 1)));
};
export const handleGetOrderedViewsForServer = (event: IpcMainInvokeEvent, serverId: string) => {
return ServerManager.getOrderedTabsForServer(serverId).map((view) => view.toUniqueView());
};
export const handleGetLastActive = () => {
const server = ServerManager.getCurrentServer();
const view = ServerManager.getLastActiveTabForServer(server.id);
return {server: server.id, view: view.id};
};
const selectView = (fn: (order: number, length: number) => number) => {
const currentView = ViewManager.getCurrentView();
if (!currentView) {
return;
}
const currentServerViews = ServerManager.getOrderedTabsForServer(currentView.view.server.id).map((view, index) => ({view, index}));
const filteredViews = currentServerViews?.filter((view) => view.view.isOpen);
const currentServerView = currentServerViews?.find((view) => view.view.type === currentView.view.type);
if (!currentServerViews || !currentServerView || !filteredViews) {
return;
}
let currentOrder = currentServerView.index;
let nextIndex = -1;
while (nextIndex === -1) {
const nextOrder = (fn(currentOrder, currentServerViews.length) % currentServerViews.length);
nextIndex = filteredViews.findIndex((view) => view.index === nextOrder);
currentOrder = nextOrder;
}
const newView = filteredViews[nextIndex].view;
ViewManager.showById(newView.id);
};

View file

@ -7,54 +7,6 @@ import MainWindow from 'main/windows/mainWindow';
import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager';
jest.mock('common/config', () => ({
teams: [{
name: 'example',
url: 'http://example.com',
order: 0,
tabs: [
{
name: 'TAB_MESSAGING',
order: 0,
isOpen: true,
},
{
name: 'TAB_FOCALBOARD',
order: 1,
isOpen: true,
},
{
name: 'TAB_PLAYBOOKS',
order: 2,
isOpen: true,
},
],
lastActiveTab: 0,
}, {
name: 'github',
url: 'https://github.com/',
order: 1,
tabs: [
{
name: 'TAB_MESSAGING',
order: 0,
isOpen: true,
},
{
name: 'TAB_FOCALBOARD',
order: 1,
isOpen: true,
},
{
name: 'TAB_PLAYBOOKS',
order: 2,
isOpen: true,
},
],
lastActiveTab: 0,
}],
}));
jest.mock('common/utils/url', () => {
const actualUrl = jest.requireActual('common/utils/url');
return {
@ -116,35 +68,35 @@ describe('main/authManager', () => {
});
it('should popLoginModal when isTrustedURL', () => {
ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://trustedurl.com/')}}});
ViewManager.getViewByWebContentsId.mockReturnValue({view: {server: {url: new URL('http://trustedurl.com/')}}});
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://trustedurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popLoginModal when isCustomLoginURL', () => {
ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://customloginurl.com/')}}});
ViewManager.getViewByWebContentsId.mockReturnValue({view: {server: {url: new URL('http://customloginurl.com/')}}});
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://customloginurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popLoginModal when has permission', () => {
ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://haspermissionurl.com/')}}});
ViewManager.getViewByWebContentsId.mockReturnValue({view: {server: {url: new URL('http://haspermissionurl.com/')}}});
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://haspermissionurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled();
});
it('should popPermissionModal when anything else is true', () => {
ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://someotherurl.com/')}}});
ViewManager.getViewByWebContentsId.mockReturnValue({view: {server: {url: new URL('http://someotherurl.com/')}}});
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).toBeCalled();
});
it('should set login callback when logging in', () => {
ViewManager.getViewByWebContentsId.mockReturnValue({tab: {server: {url: new URL('http://someotherurl.com/')}}});
ViewManager.getViewByWebContentsId.mockReturnValue({view: {server: {url: new URL('http://someotherurl.com/')}}});
const callback = jest.fn();
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback);
expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback);

View file

@ -40,7 +40,7 @@ export class AuthManager {
if (!parsedURL) {
return;
}
const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.url;
const serverURL = ViewManager.getViewByWebContentsId(webContents.id)?.view.server.url;
if (!serverURL) {
return;
}

View file

@ -15,26 +15,26 @@ const stepDescriptiveName = 'serverConnectivity';
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
try {
const teams = ServerManager.getAllServers();
const servers = ServerManager.getAllServers();
await Promise.all(teams.map(async (team) => {
logger.debug('Pinging server: ', team.url);
await Promise.all(servers.map(async (server) => {
logger.debug('Pinging server: ', server.url);
if (!team.name || !team.url) {
throw new Error(`Invalid server configuration. Team Url: ${team.url}, team name: ${team.name}`);
if (!server.name || !server.url) {
throw new Error(`Invalid server configuration. Server Url: ${server.url}, server name: ${server.name}`);
}
const serverOnline = await isOnline(logger, `${team.url}/api/v4/system/ping`);
const serverOnline = await isOnline(logger, `${server.url}/api/v4/system/ping`);
if (!serverOnline) {
throw new Error(`Server appears to be offline. Team url: ${team.url}`);
throw new Error(`Server appears to be offline. Server url: ${server.url}`);
}
}));
return {
message: `${stepName} finished successfully`,
succeeded: true,
payload: teams,
payload: servers,
};
} catch (error) {
logger.warn(`Diagnostics ${stepName} Failure`, {error});

View file

@ -554,7 +554,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
log.debug('doneEventController', {state});
if (state === 'completed' && !this.open) {
displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.tab.server.name ?? '');
displayDownloadCompleted(path.basename(item.savePath), item.savePath, ViewManager.getViewByWebContentsId(webContents.id)?.view.server.name ?? '');
}
const bookmark = this.bookmarks.get(this.getFileId(item));

View file

@ -67,8 +67,8 @@ jest.mock('main/windows/mainWindow', () => ({
sendToRenderer: jest.fn(),
}));
jest.mock('main/windows/settingsWindow', () => ({}));
jest.mock('common/tabs/TabView', () => ({
getTabDisplayName: (name) => name,
jest.mock('common/views/View', () => ({
getViewDisplayName: (name) => name,
}));
describe('main/menus/app', () => {
@ -88,19 +88,19 @@ describe('main/menus/app', () => {
url: 'https:/ /github.com/',
},
];
const tabs = [
const views = [
{
id: 'tab-1',
id: 'view-1',
name: 'TAB_MESSAGING',
isOpen: true,
},
{
id: 'tab-2',
id: 'view-2',
name: 'TAB_FOCALBOARD',
isOpen: true,
},
{
id: 'tab-3',
id: 'view-3',
name: 'TAB_PLAYBOOKS',
isOpen: true,
},
@ -109,7 +109,7 @@ describe('main/menus/app', () => {
beforeEach(() => {
ServerManager.getCurrentServer.mockReturnValue(servers[0]);
ServerManager.getOrderedServers.mockReturnValue(servers);
ServerManager.getOrderedTabsForServer.mockReturnValue(tabs);
ServerManager.getOrderedTabsForServer.mockReturnValue(views);
getDarwinDoNotDisturb.mockReturnValue(false);
});
@ -217,7 +217,7 @@ describe('main/menus/app', () => {
expect(signInOption).toBe(undefined);
});
it('should not show `Sign in to Another Server` if no teams are configured', () => {
it('should not show `Sign in to Another Server` if no servers are configured', () => {
localizeMessage.mockImplementation((id) => {
switch (id) {
case 'main.menus.app.file':
@ -247,15 +247,15 @@ describe('main/menus/app', () => {
name: `server-${key}`,
url: `http://server-${key}.com`,
}));
const modifiedTabs = [
const modifiedViews = [
{
id: 'tab-1',
id: 'view-1',
type: 'TAB_MESSAGING',
isOpen: true,
},
];
ServerManager.getOrderedServers.mockReturnValue(modifiedServers);
ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs);
ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedViews);
const menu = createTemplate(config);
const windowMenu = menu.find((item) => item.label === '&Window');
for (let i = 0; i < 9; i++) {
@ -268,32 +268,32 @@ describe('main/menus/app', () => {
}
});
it('should show the first 9 tabs (using order) in the Window menu', () => {
it('should show the first 9 views (using order) in the Window menu', () => {
localizeMessage.mockImplementation((id) => {
if (id === 'main.menus.app.window') {
return '&Window';
}
if (id.startsWith('common.tabs')) {
return id.replace('common.tabs.', '');
if (id.startsWith('common.views')) {
return id.replace('common.views.', '');
}
return id;
});
ServerManager.getCurrentServer.mockImplementation(() => ({id: servers[0].id}));
const modifiedTabs = [...Array(15).keys()].map((key) => ({
id: `tab-${key}`,
type: `tab-${key}`,
const modifiedViews = [...Array(15).keys()].map((key) => ({
id: `view-${key}`,
type: `view-${key}`,
isOpen: true,
}));
ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs);
ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedViews);
const menu = createTemplate(config);
const windowMenu = menu.find((item) => item.label === '&Window');
for (let i = 0; i < 9; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`);
const menuItem = windowMenu.submenu.find((item) => item.label === ` view-${i}`);
expect(menuItem).not.toBe(undefined);
}
for (let i = 9; i < 15; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`);
const menuItem = windowMenu.submenu.find((item) => item.label === ` view-${i}`);
expect(menuItem).toBe(undefined);
}
});

View file

@ -6,9 +6,9 @@
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron';
import log from 'electron-log';
import {OPEN_TEAMS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication';
import {OPEN_SERVERS_DROPDOWN, SHOW_NEW_SERVER_MODAL} from 'common/communication';
import {t} from 'common/utils/util';
import {getTabDisplayName, TabType} from 'common/tabs/TabView';
import {getViewDisplayName, ViewType} from 'common/views/View';
import {Config} from 'common/config';
import {localizeMessage} from 'main/i18nManager';
@ -18,7 +18,7 @@ import downloadsManager from 'main/downloadsManager';
import Diagnostics from 'main/diagnostics';
import ViewManager from 'main/views/viewManager';
import SettingsWindow from 'main/windows/settingsWindow';
import {selectNextTab, selectPreviousTab} from 'main/app/tabs';
import {selectNextView, selectPreviousView} from 'main/app/views';
import {switchServer} from 'main/app/servers';
export function createTemplate(config: Config, updateManager: UpdateManager) {
@ -233,7 +233,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}],
});
const teams = ServerManager.getOrderedServers();
const servers = ServerManager.getOrderedServers();
const windowMenu = {
id: 'window',
label: localizeMessage('main.menus.app.window', '&Window'),
@ -257,25 +257,25 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
click() {
ipcMain.emit(OPEN_TEAMS_DROPDOWN);
ipcMain.emit(OPEN_SERVERS_DROPDOWN);
},
}] : []),
...teams.slice(0, 9).map((team, i) => {
...servers.slice(0, 9).map((server, i) => {
const items = [];
items.push({
label: team.name,
label: server.name,
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`,
click() {
switchServer(team.id);
switchServer(server.id);
},
});
if (ServerManager.getCurrentServer().id === team.id) {
ServerManager.getOrderedTabsForServer(team.id).slice(0, 9).forEach((tab, i) => {
if (ServerManager.getCurrentServer().id === server.id) {
ServerManager.getOrderedTabsForServer(server.id).slice(0, 9).forEach((view, i) => {
items.push({
label: ` ${localizeMessage(`common.tabs.${tab.type}`, getTabDisplayName(tab.type as TabType))}`,
label: ` ${localizeMessage(`common.views.${view.type}`, getViewDisplayName(view.type as ViewType))}`,
accelerator: `CmdOrCtrl+${i + 1}`,
click() {
ViewManager.showById(tab.id);
ViewManager.showById(view.id);
},
});
});
@ -285,16 +285,16 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'),
accelerator: 'Ctrl+Tab',
click() {
selectNextTab();
selectNextView();
},
enabled: (teams.length > 1),
enabled: (servers.length > 1),
}, {
label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'),
accelerator: 'Ctrl+Shift+Tab',
click() {
selectPreviousTab();
selectPreviousView();
},
enabled: (teams.length > 1),
enabled: (servers.length > 1),
}, ...(isMac ? [separatorItem, {
role: 'front',
label: localizeMessage('main.menus.app.window.bringAllToFront', 'Bring All to Front'),

View file

@ -12,13 +12,13 @@ import SettingsWindow from 'main/windows/settingsWindow';
import {switchServer} from 'main/app/servers';
export function createTemplate() {
const teams = ServerManager.getOrderedServers();
const servers = ServerManager.getOrderedServers();
const template = [
...teams.slice(0, 9).map((team) => {
...servers.slice(0, 9).map((server) => {
return {
label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name,
label: server.name.length > 50 ? `${server.name.slice(0, 50)}...` : server.name,
click: () => {
switchServer(team.id);
switchServer(server.id);
},
};
}), {

View file

@ -76,7 +76,7 @@ jest.mock('macos-notification-state', () => ({
jest.mock('../views/viewManager', () => ({
getViewByWebContentsId: () => ({
id: 'server_id',
tab: {
view: {
server: {
name: 'server_name',
},
@ -231,7 +231,7 @@ describe('main/notifications', () => {
});
});
it('should switch tab when clicking on notification', () => {
it('should switch view when clicking on notification', () => {
displayMention(
'click_test',
'mention_click_body',

View file

@ -40,7 +40,7 @@ export function displayMention(title: string, body: string, channel: {id: string
if (!view) {
return;
}
const serverName = view.tab.server.name;
const serverName = view.view.server.name;
const options = {
title: `${serverName}: ${title}`,

View file

@ -10,10 +10,10 @@ import {
GET_LANGUAGE_INFORMATION,
QUIT,
OPEN_APP_MENU,
CLOSE_TEAMS_DROPDOWN,
OPEN_TEAMS_DROPDOWN,
CLOSE_SERVERS_DROPDOWN,
OPEN_SERVERS_DROPDOWN,
SWITCH_TAB,
CLOSE_TAB,
CLOSE_VIEW,
WINDOW_CLOSE,
WINDOW_MINIMIZE,
WINDOW_MAXIMIZE,
@ -59,8 +59,8 @@ import {
DOWNLOADS_DROPDOWN_MENU_OPEN_FILE,
UPDATE_DOWNLOADS_DROPDOWN_MENU,
REQUEST_DOWNLOADS_DROPDOWN_MENU_INFO,
UPDATE_TEAMS_DROPDOWN,
REQUEST_TEAMS_DROPDOWN_INFO,
UPDATE_SERVERS_DROPDOWN,
REQUEST_SERVERS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE,
SWITCH_SERVER,
SHOW_NEW_SERVER_MODAL,
@ -111,10 +111,10 @@ contextBridge.exposeInMainWorld('mas', {
contextBridge.exposeInMainWorld('desktop', {
quit: (reason, stack) => ipcRenderer.send(QUIT, reason, stack),
openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU),
closeTeamsDropdown: () => ipcRenderer.send(CLOSE_TEAMS_DROPDOWN),
openTeamsDropdown: () => ipcRenderer.send(OPEN_TEAMS_DROPDOWN),
switchTab: (tabId) => ipcRenderer.send(SWITCH_TAB, tabId),
closeTab: (tabId) => ipcRenderer.send(CLOSE_TAB, tabId),
closeServersDropdown: () => ipcRenderer.send(CLOSE_SERVERS_DROPDOWN),
openServersDropdown: () => ipcRenderer.send(OPEN_SERVERS_DROPDOWN),
switchTab: (viewId) => ipcRenderer.send(SWITCH_TAB, viewId),
closeView: (viewId) => ipcRenderer.send(CLOSE_VIEW, viewId),
closeWindow: () => ipcRenderer.send(WINDOW_CLOSE),
minimizeWindow: () => ipcRenderer.send(WINDOW_MINIMIZE),
maximizeWindow: () => ipcRenderer.send(WINDOW_MAXIMIZE),
@ -130,7 +130,7 @@ contextBridge.exposeInMainWorld('desktop', {
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
updateTabOrder: (serverId, tabOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, tabOrder),
updateTabOrder: (serverId, viewOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, viewOrder),
getLastActive: () => ipcRenderer.invoke(GET_LAST_ACTIVE),
getOrderedServers: () => ipcRenderer.invoke(GET_ORDERED_SERVERS),
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
@ -153,7 +153,7 @@ contextBridge.exposeInMainWorld('desktop', {
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)),
onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewId, err, loadUrl) => listener(viewId, err, loadUrl)),
onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverId, tabId) => listener(serverId, tabId)),
onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverId, viewId) => listener(serverId, viewId)),
onMaximizeChange: (listener) => ipcRenderer.on(MAXIMIZE_CHANGE, (_, maximize) => listener(maximize)),
onEnterFullScreen: (listener) => ipcRenderer.on('enter-full-screen', () => listener()),
onLeaveFullScreen: (listener) => ipcRenderer.on('leave-full-screen', () => listener()),
@ -162,8 +162,8 @@ contextBridge.exposeInMainWorld('desktop', {
onModalClose: (listener) => ipcRenderer.on(MODAL_CLOSE, () => listener()),
onToggleBackButton: (listener) => ipcRenderer.on(TOGGLE_BACK_BUTTON, (_, showExtraBar) => listener(showExtraBar)),
onUpdateMentions: (listener) => ipcRenderer.on(UPDATE_MENTIONS, (_event, view, mentions, unreads, isExpired) => listener(view, mentions, unreads, isExpired)),
onCloseTeamsDropdown: (listener) => ipcRenderer.on(CLOSE_TEAMS_DROPDOWN, () => listener()),
onOpenTeamsDropdown: (listener) => ipcRenderer.on(OPEN_TEAMS_DROPDOWN, () => listener()),
onCloseServersDropdown: (listener) => ipcRenderer.on(CLOSE_SERVERS_DROPDOWN, () => listener()),
onOpenServersDropdown: (listener) => ipcRenderer.on(OPEN_SERVERS_DROPDOWN, () => listener()),
onCloseDownloadsDropdown: (listener) => ipcRenderer.on(CLOSE_DOWNLOADS_DROPDOWN, () => listener()),
onOpenDownloadsDropdown: (listener) => ipcRenderer.on(OPEN_DOWNLOADS_DROPDOWN, () => listener()),
onShowDownloadsDropdownButtonBadge: (listener) => ipcRenderer.on(SHOW_DOWNLOADS_DROPDOWN_BUTTON_BADGE, () => listener()),
@ -195,29 +195,29 @@ contextBridge.exposeInMainWorld('desktop', {
},
serverDropdown: {
requestInfo: () => ipcRenderer.send(REQUEST_TEAMS_DROPDOWN_INFO),
requestInfo: () => ipcRenderer.send(REQUEST_SERVERS_DROPDOWN_INFO),
sendSize: (width, height) => ipcRenderer.send(RECEIVE_DROPDOWN_MENU_SIZE, width, height),
switchServer: (serverId) => ipcRenderer.send(SWITCH_SERVER, serverId),
showNewServerModal: () => ipcRenderer.send(SHOW_NEW_SERVER_MODAL),
showEditServerModal: (serverId) => ipcRenderer.send(SHOW_EDIT_SERVER_MODAL, serverId),
showRemoveServerModal: (serverId) => ipcRenderer.send(SHOW_REMOVE_SERVER_MODAL, serverId),
onUpdateServerDropdown: (listener) => ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (_,
teams,
activeTeam,
onUpdateServerDropdown: (listener) => ipcRenderer.on(UPDATE_SERVERS_DROPDOWN, (_,
servers,
activeServer,
darkMode,
enableServerManagement,
hasGPOTeams,
hasGPOServers,
expired,
mentions,
unreads,
windowBounds,
) => listener(
teams,
activeTeam,
servers,
activeServer,
darkMode,
enableServerManagement,
hasGPOTeams,
hasGPOServers,
expired,
mentions,
unreads,

View file

@ -20,7 +20,7 @@ import {
SET_VIEW_OPTIONS,
REACT_APP_INITIALIZED,
USER_ACTIVITY_UPDATE,
CLOSE_TEAMS_DROPDOWN,
CLOSE_SERVERS_DROPDOWN,
BROWSER_HISTORY_BUTTON,
BROWSER_HISTORY_PUSH,
APP_LOGGED_IN,
@ -272,7 +272,7 @@ function isDownloadLink(el) {
}
window.addEventListener('click', (e) => {
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
ipcRenderer.send(CLOSE_SERVERS_DROPDOWN);
const el = e.target;
if (!isDownloadLink(el)) {
ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN);

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

View file

@ -247,7 +247,7 @@ describe('main/windows/callsWidgetWindow', () => {
title: 'call test title #/&',
};
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
url: new URL('http://localhost:8065'),
},
@ -262,7 +262,7 @@ describe('main/windows/callsWidgetWindow', () => {
it('getWidgetURL - under subpath', () => {
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
url: new URL('http://localhost:8065/subpath'),
},
@ -339,7 +339,7 @@ describe('main/windows/callsWidgetWindow', () => {
beforeEach(() => {
callsWidgetWindow.options = {callID: 'id'};
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
url: new URL('http://localhost:8065'),
},
@ -491,7 +491,7 @@ describe('main/windows/callsWidgetWindow', () => {
callsWidgetWindow.close = jest.fn();
callsWidgetWindow.getWidgetURL = jest.fn();
const view = {
name: 'server-1_tab-messaging',
name: 'server-1_view-messaging',
serverInfo: {
server: {
url: new URL('http://server-1.com'),
@ -526,12 +526,12 @@ describe('main/windows/callsWidgetWindow', () => {
it('should create calls widget window', async () => {
expect(callsWidgetWindow.win).toBeUndefined();
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'});
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'});
expect(callsWidgetWindow.win).toBeDefined();
});
it('should create with correct initial configuration', async () => {
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'});
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'});
expect(BrowserWindow).toHaveBeenCalledWith(expect.objectContaining({
width: MINIMUM_CALLS_WIDGET_WIDTH,
height: MINIMUM_CALLS_WIDGET_HEIGHT,
@ -560,7 +560,7 @@ describe('main/windows/callsWidgetWindow', () => {
const window = {webContents: {id: 2}};
callsWidgetWindow.win = window;
callsWidgetWindow.options = {callID: 'test'};
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_tab-messaging', {callID: 'test'});
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test'});
expect(callsWidgetWindow.win).toEqual(window);
});
@ -568,7 +568,7 @@ describe('main/windows/callsWidgetWindow', () => {
const window = {webContents: {id: 2}};
callsWidgetWindow.win = window;
callsWidgetWindow.getCallID = jest.fn(() => 'test');
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_tab-messaging', {callID: 'test2'});
await callsWidgetWindow.handleCreateCallsWidgetWindow('server-1_view-messaging', {callID: 'test2'});
expect(callsWidgetWindow.win).not.toEqual(window);
});
});
@ -580,18 +580,18 @@ describe('main/windows/callsWidgetWindow', () => {
send: jest.fn(),
},
};
const teams = [
const servers = [
{
name: 'server-1',
order: 1,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
@ -599,24 +599,24 @@ describe('main/windows/callsWidgetWindow', () => {
}, {
name: 'server-2',
order: 0,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
],
lastActiveTab: 2,
lastActiveView: 2,
},
];
const map = teams.reduce((arr, item) => {
item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, {
const map = servers.reduce((arr, item) => {
item.views.forEach((view) => {
arr.push([`${item.name}_${view.name}`, {
sendToRenderer: jest.fn(),
}]);
});
@ -649,9 +649,9 @@ describe('main/windows/callsWidgetWindow', () => {
},
]);
await callsWidgetWindow.handleGetDesktopSources('server-1_tab-1', null);
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
expect(views.get('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('desktop-sources-result', [
{
id: 'screen0',
},
@ -663,11 +663,11 @@ describe('main/windows/callsWidgetWindow', () => {
it('should send error with no sources', async () => {
jest.spyOn(desktopCapturer, 'getSources').mockResolvedValue([]);
await callsWidgetWindow.handleGetDesktopSources('server-2_tab-1', null);
await callsWidgetWindow.handleGetDesktopSources('server-2_view-1', null);
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(views.get('server-2_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
expect(views.get('server-2_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
@ -684,16 +684,16 @@ describe('main/windows/callsWidgetWindow', () => {
]);
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
await callsWidgetWindow.handleGetDesktopSources('server-1_tab-1', null);
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
expect(systemPreferences.getMediaAccessStatus).toHaveBeenCalledWith('screen');
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(views.get('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(views.get('server-1_tab-1').sendToRenderer).toHaveBeenCalledTimes(1);
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledTimes(1);
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledTimes(1);
});
@ -713,7 +713,7 @@ describe('main/windows/callsWidgetWindow', () => {
]);
jest.spyOn(systemPreferences, 'getMediaAccessStatus').mockReturnValue('denied');
await callsWidgetWindow.handleGetDesktopSources('server-1_tab-1', null);
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
expect(callsWidgetWindow.missingScreensharePermissions).toBe(true);
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(1);
@ -721,11 +721,11 @@ describe('main/windows/callsWidgetWindow', () => {
expect(callsWidgetWindow.win.webContents.send).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
expect(views.get('server-1_tab-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
expect(views.get('server-1_view-1').sendToRenderer).toHaveBeenCalledWith('calls-error', {
err: 'screen-permissions',
});
await callsWidgetWindow.handleGetDesktopSources('server-1_tab-1', null);
await callsWidgetWindow.handleGetDesktopSources('server-1_view-1', null);
expect(resetScreensharePermissionsMacOS).toHaveBeenCalledTimes(2);
expect(openScreensharePermissionsSettingsMacOS).toHaveBeenCalledTimes(1);
@ -739,25 +739,25 @@ describe('main/windows/callsWidgetWindow', () => {
describe('handleDesktopSourcesModalRequest', () => {
const callsWidgetWindow = new CallsWidgetWindow();
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
id: 'server-1',
},
},
sendToRenderer: jest.fn(),
};
const teams = [
const servers = [
{
name: 'server-1',
order: 1,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
@ -765,24 +765,24 @@ describe('main/windows/callsWidgetWindow', () => {
}, {
name: 'server-2',
order: 0,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
],
lastActiveTab: 2,
lastActiveView: 2,
},
];
const map = teams.reduce((arr, item) => {
item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, {}]);
const map = servers.reduce((arr, item) => {
item.views.forEach((view) => {
arr.push([`${item.name}_${view.name}`, {}]);
});
return arr;
}, []);
@ -805,7 +805,7 @@ describe('main/windows/callsWidgetWindow', () => {
describe('handleCallsWidgetChannelLinkClick', () => {
const callsWidgetWindow = new CallsWidgetWindow();
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
id: 'server-2',
},
@ -813,18 +813,18 @@ describe('main/windows/callsWidgetWindow', () => {
sendToRenderer: jest.fn(),
};
callsWidgetWindow.getChannelURL = jest.fn();
const teams = [
const servers = [
{
name: 'server-1',
order: 1,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
@ -832,24 +832,24 @@ describe('main/windows/callsWidgetWindow', () => {
}, {
name: 'server-2',
order: 0,
tabs: [
views: [
{
name: 'tab-1',
name: 'view-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
name: 'view-2',
order: 2,
isOpen: true,
},
],
lastActiveTab: 2,
lastActiveView: 2,
},
];
const map = teams.reduce((arr, item) => {
item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, {}]);
const map = servers.reduce((arr, item) => {
item.views.forEach((view) => {
arr.push([`${item.name}_${view.name}`, {}]);
});
return arr;
}, []);
@ -872,7 +872,7 @@ describe('main/windows/callsWidgetWindow', () => {
describe('handleCallsError', () => {
const callsWidgetWindow = new CallsWidgetWindow();
callsWidgetWindow.mainView = {
tab: {
view: {
server: {
id: 'server-2',
},
@ -899,7 +899,7 @@ describe('main/windows/callsWidgetWindow', () => {
describe('handleCallsLinkClick', () => {
const view = {
tab: {
view: {
server: {
id: 'server-1',
},

View file

@ -14,7 +14,7 @@ import {
CallsWidgetWindowConfig,
} from 'types/calls';
import {MattermostView} from 'main/views/MattermostView';
import {MattermostBrowserView} from 'main/views/MattermostBrowserView';
import {getLocalPreload, openScreensharePermissionsSettingsMacOS, resetScreensharePermissionsMacOS} from 'main/utils';
@ -47,7 +47,7 @@ const log = new Logger('CallsWidgetWindow');
export class CallsWidgetWindow {
private win?: BrowserWindow;
private mainView?: MattermostView;
private mainView?: MattermostBrowserView;
private options?: CallsWidgetWindowConfig;
private missingScreensharePermissions?: boolean;
@ -82,7 +82,7 @@ export class CallsWidgetWindow {
}
private get serverID() {
return this.mainView?.tab.server.id;
return this.mainView?.view.server.id;
}
/**
@ -101,7 +101,7 @@ export class CallsWidgetWindow {
if (!this.mainView) {
return undefined;
}
const u = parseURL(this.mainView.tab.server.url.toString()) as URL;
const u = parseURL(this.mainView.view.server.url.toString()) as URL;
u.pathname = getFormattedPathName(u.pathname);
u.pathname += `plugins/${CALLS_PLUGIN_ID}/standalone/widget.html`;
@ -119,7 +119,7 @@ export class CallsWidgetWindow {
return u.toString();
}
private init = (view: MattermostView, options: CallsWidgetWindowConfig) => {
private init = (view: MattermostBrowserView, options: CallsWidgetWindowConfig) => {
this.win = new BrowserWindow({
width: MINIMUM_CALLS_WIDGET_WIDTH,
height: MINIMUM_CALLS_WIDGET_HEIGHT,
@ -271,7 +271,7 @@ export class CallsWidgetWindow {
if (!parsedURL) {
return {action: 'deny' as const};
}
if (isCallsPopOutURL(this.mainView?.tab.server.url, parsedURL, this.options?.callID)) {
if (isCallsPopOutURL(this.mainView?.view.server.url, parsedURL, this.options?.callID)) {
return {
action: 'allow' as const,
overrideBrowserWindowOptions: {

View file

@ -421,7 +421,7 @@ describe('main/windows/mainWindow', () => {
expect(window.setFullScreen).toHaveBeenCalledWith(false);
});
it('should select tabs using alt+cmd+arrow keys on Mac', () => {
it('should select views using alt+cmd+arrow keys on Mac', () => {
const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', {
value: 'darwin',

View file

@ -5,7 +5,7 @@ import React, {useState, useCallback, useEffect} from 'react';
import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import {isValidURL, parseURL} from 'common/utils/url';
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
@ -22,8 +22,8 @@ import 'renderer/css/components/ConfigureServer.scss';
import 'renderer/css/components/LoadingScreen.css';
type ConfigureServerProps = {
currentTeams: MattermostTeam[];
team?: MattermostTeam;
currentServers: UniqueServer[];
server?: UniqueServer;
mobileView?: boolean;
darkMode?: boolean;
messageTitle?: string;
@ -32,12 +32,12 @@ type ConfigureServerProps = {
alternateLinkMessage?: string;
alternateLinkText?: string;
alternateLinkURL?: string;
onConnect: (data: MattermostTeam) => void;
onConnect: (data: UniqueServer) => void;
};
function ConfigureServer({
currentTeams,
team,
currentServers,
server,
mobileView,
darkMode,
messageTitle,
@ -54,7 +54,7 @@ function ConfigureServer({
name: prevName,
url: prevURL,
id,
} = team || {};
} = server || {};
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
const [name, setName] = useState(prevName || '');
@ -92,14 +92,14 @@ function ConfigureServer({
if (!newName) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.nameRequired',
id: 'renderer.components.newServerModal.error.nameRequired',
defaultMessage: 'Name is required.',
});
}
if (currentTeams.find(({name: existingName}) => existingName === newName)) {
if (currentServers.find(({name: existingName}) => existingName === newName)) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.serverNameExists',
id: 'renderer.components.newServerModal.error.serverNameExists',
defaultMessage: 'A server with the same name already exists.',
});
}
@ -110,28 +110,28 @@ function ConfigureServer({
const validateURL = async (fullURL: string) => {
if (!fullURL) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.urlRequired',
id: 'renderer.components.newServerModal.error.urlRequired',
defaultMessage: 'URL is required.',
});
}
if (!parseURL(fullURL)) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.urlIncorrectFormatting',
id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
defaultMessage: 'URL is not formatted correctly.',
});
}
if (!isValidURL(fullURL)) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.urlNeedsHttp',
id: 'renderer.components.newServerModal.error.urlNeedsHttp',
defaultMessage: 'URL should start with http:// or https://.',
});
}
if (currentTeams.find(({url: existingURL}) => parseURL(existingURL)?.toString === parseURL(fullURL)?.toString())) {
if (currentServers.find(({url: existingURL}) => parseURL(existingURL)?.toString === parseURL(fullURL)?.toString())) {
return formatMessage({
id: 'renderer.components.newTeamModal.error.serverUrlExists',
id: 'renderer.components.newServerModal.error.serverUrlExists',
defaultMessage: 'A server with the same URL already exists.',
});
}

View file

@ -10,7 +10,7 @@ import {Container, Row} from 'react-bootstrap';
import {DropResult} from 'react-beautiful-dnd';
import {injectIntl, IntlShape} from 'react-intl';
import {MattermostTab, MattermostTeam} from 'types/config';
import {UniqueView, UniqueServer} from 'types/config';
import {DownloadedItems} from 'types/downloads';
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
@ -23,7 +23,7 @@ import {playSound} from '../notificationSounds';
import TabBar from './TabBar';
import ExtraBar from './ExtraBar';
import ErrorView from './ErrorView';
import TeamDropdownButton from './TeamDropdownButton';
import ServerDropdownButton from './ServerDropdownButton';
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
import '../css/components/UpgradeButton.scss';
@ -47,8 +47,8 @@ type Props = {
type State = {
activeServerId?: string;
activeTabId?: string;
servers: MattermostTeam[];
tabs: Map<string, MattermostTab[]>;
servers: UniqueServer[];
tabs: Map<string, UniqueView[]>;
sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>;
@ -147,7 +147,7 @@ class MainPage extends React.PureComponent<Props, State> {
setInitialActiveTab = async () => {
const lastActive = await window.desktop.getLastActive();
this.setActiveView(lastActive.server, lastActive.tab);
this.setActiveView(lastActive.server, lastActive.view);
}
updateServers = async () => {
@ -239,11 +239,11 @@ class MainPage extends React.PureComponent<Props, State> {
this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired});
});
window.desktop.onCloseTeamsDropdown(() => {
window.desktop.onCloseServersDropdown(() => {
this.setState({isMenuOpen: false});
});
window.desktop.onOpenTeamsDropdown(() => {
window.desktop.onOpenServersDropdown(() => {
this.setState({isMenuOpen: true});
});
@ -290,7 +290,7 @@ class MainPage extends React.PureComponent<Props, State> {
}
handleCloseDropdowns = () => {
window.desktop.closeTeamsDropdown();
window.desktop.closeServersDropdown();
this.closeDownloadsDropdown();
}
@ -307,7 +307,7 @@ class MainPage extends React.PureComponent<Props, State> {
}
handleCloseTab = (tabId: string) => {
window.desktop.closeTab(tabId);
window.desktop.closeView(tabId);
}
handleDragAndDrop = async (dropResult: DropResult) => {
@ -399,7 +399,7 @@ class MainPage extends React.PureComponent<Props, State> {
render() {
const {intl} = this.props;
let currentTabs: MattermostTab[] = [];
let currentTabs: UniqueView[] = [];
if (this.state.activeServerId) {
currentTabs = this.state.tabs.get(this.state.activeServerId) ?? [];
}
@ -538,7 +538,7 @@ class MainPage extends React.PureComponent<Props, State> {
/>
</button>
{activeServer && (
<TeamDropdownButton
<ServerDropdownButton
isDisabled={this.state.modalOpen}
activeServerName={activeServer.name}
totalMentionCount={totalMentionCount}

View file

@ -6,15 +6,15 @@ import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import {isValidURL} from 'common/utils/url';
type Props = {
onClose?: () => void;
onSave?: (team: MattermostTeam) => void;
team?: MattermostTeam;
currentTeams?: MattermostTeam[];
onSave?: (server: UniqueServer) => void;
server?: UniqueServer;
currentServers?: UniqueServer[];
editMode?: boolean;
show?: boolean;
restoreFocus?: boolean;
@ -24,16 +24,16 @@ type Props = {
};
type State = {
teamName: string;
teamUrl: string;
teamId?: string;
teamOrder: number;
serverName: string;
serverUrl: string;
serverId?: string;
serverOrder: number;
saveStarted: boolean;
}
class NewTeamModal extends React.PureComponent<Props, State> {
class NewServerModal extends React.PureComponent<Props, State> {
wasShown?: boolean;
teamUrlInputRef?: HTMLInputElement;
serverUrlInputRef?: HTMLInputElement;
static defaultProps = {
restoreFocus: true,
@ -44,90 +44,90 @@ class NewTeamModal extends React.PureComponent<Props, State> {
this.wasShown = false;
this.state = {
teamName: '',
teamUrl: '',
teamOrder: props.currentOrder || 0,
serverName: '',
serverUrl: '',
serverOrder: props.currentOrder || 0,
saveStarted: false,
};
}
initializeOnShow() {
this.setState({
teamName: this.props.team ? this.props.team.name : '',
teamUrl: this.props.team ? this.props.team.url : '',
teamId: this.props.team?.id,
serverName: this.props.server ? this.props.server.name : '',
serverUrl: this.props.server ? this.props.server.url : '',
serverId: this.props.server?.id,
saveStarted: false,
});
}
getTeamNameValidationError() {
getServerNameValidationError() {
if (!this.state.saveStarted) {
return null;
}
if (this.props.currentTeams) {
const currentTeams = [...this.props.currentTeams];
if (currentTeams.find((team) => team.id !== this.state.teamId && team.name === this.state.teamName)) {
if (this.props.currentServers) {
const currentServers = [...this.props.currentServers];
if (currentServers.find((server) => server.id !== this.state.serverId && server.name === this.state.serverName)) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.serverNameExists'
id='renderer.components.newServerModal.error.serverNameExists'
defaultMessage='A server with the same name already exists.'
/>
);
}
}
return this.state.teamName.length > 0 ? null : (
return this.state.serverName.length > 0 ? null : (
<FormattedMessage
id='renderer.components.newTeamModal.error.nameRequired'
id='renderer.components.newServerModal.error.nameRequired'
defaultMessage='Name is required.'
/>
);
}
getTeamNameValidationState() {
return this.getTeamNameValidationError() === null ? null : 'error';
getServerNameValidationState() {
return this.getServerNameValidationError() === null ? null : 'error';
}
handleTeamNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
handleServerNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({
teamName: e.target.value,
serverName: e.target.value,
});
}
getTeamUrlValidationError() {
getServerUrlValidationError() {
if (!this.state.saveStarted) {
return null;
}
if (this.props.currentTeams) {
const currentTeams = [...this.props.currentTeams];
if (currentTeams.find((team) => team.id !== this.state.teamId && team.url === this.state.teamUrl)) {
if (this.props.currentServers) {
const currentServers = [...this.props.currentServers];
if (currentServers.find((server) => server.id !== this.state.serverId && server.url === this.state.serverUrl)) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.serverUrlExists'
id='renderer.components.newServerModal.error.serverUrlExists'
defaultMessage='A server with the same URL already exists.'
/>
);
}
}
if (this.state.teamUrl.length === 0) {
if (this.state.serverUrl.length === 0) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlRequired'
id='renderer.components.newServerModal.error.urlRequired'
defaultMessage='URL is required.'
/>
);
}
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) {
if (!(/^https?:\/\/.*/).test(this.state.serverUrl.trim())) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlNeedsHttp'
id='renderer.components.newServerModal.error.urlNeedsHttp'
defaultMessage='URL should start with http:// or https://.'
/>
);
}
if (!isValidURL(this.state.teamUrl.trim())) {
if (!isValidURL(this.state.serverUrl.trim())) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.error.urlIncorrectFormatting'
id='renderer.components.newServerModal.error.urlIncorrectFormatting'
defaultMessage='URL is not formatted correctly.'
/>
);
@ -135,32 +135,32 @@ class NewTeamModal extends React.PureComponent<Props, State> {
return null;
}
getTeamUrlValidationState() {
return this.getTeamUrlValidationError() === null ? null : 'error';
getServerUrlValidationState() {
return this.getServerUrlValidationError() === null ? null : 'error';
}
handleTeamUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const teamUrl = e.target.value;
this.setState({teamUrl});
handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const serverUrl = e.target.value;
this.setState({serverUrl});
}
addProtocolToUrl = (teamUrl: string): Promise<void> => {
if (teamUrl.startsWith('http://') || teamUrl.startsWith('https://')) {
addProtocolToUrl = (serverUrl: string): Promise<void> => {
if (serverUrl.startsWith('http://') || serverUrl.startsWith('https://')) {
return Promise.resolve(undefined);
}
return window.desktop.modals.pingDomain(teamUrl).
return window.desktop.modals.pingDomain(serverUrl).
then((result: string) => {
this.setState({teamUrl: `${result}://${this.state.teamUrl}`});
this.setState({serverUrl: `${result}://${this.state.serverUrl}`});
}).
catch(() => {
console.error(`Could not ping url: ${teamUrl}`);
console.error(`Could not ping url: ${serverUrl}`);
});
}
getError() {
const nameError = this.getTeamNameValidationError();
const urlError = this.getTeamUrlValidationError();
const nameError = this.getServerNameValidationError();
const urlError = this.getServerUrlValidationError();
if (nameError && urlError) {
return (
@ -179,20 +179,20 @@ class NewTeamModal extends React.PureComponent<Props, State> {
}
validateForm() {
return this.getTeamNameValidationState() === null &&
this.getTeamUrlValidationState() === null;
return this.getServerNameValidationState() === null &&
this.getServerUrlValidationState() === null;
}
save = async () => {
await this.addProtocolToUrl(this.state.teamUrl);
await this.addProtocolToUrl(this.state.serverUrl);
this.setState({
saveStarted: true,
}, () => {
if (this.validateForm()) {
this.props.onSave?.({
url: this.state.teamUrl,
name: this.state.teamName,
id: this.state.teamId,
url: this.state.serverUrl,
name: this.state.serverName,
id: this.state.serverId,
});
}
});
@ -219,14 +219,14 @@ class NewTeamModal extends React.PureComponent<Props, State> {
if (this.props.editMode) {
return (
<FormattedMessage
id='renderer.components.newTeamModal.title.edit'
id='renderer.components.newServerModal.title.edit'
defaultMessage='Edit Server'
/>
);
}
return (
<FormattedMessage
id='renderer.components.newTeamModal.title.add'
id='renderer.components.newServerModal.title.add'
defaultMessage='Add Server'
/>
);
@ -241,11 +241,11 @@ class NewTeamModal extends React.PureComponent<Props, State> {
return (
<Modal
bsClass='modal'
className='NewTeamModal'
className='NewServerModal'
show={this.props.show}
id='newServerModal'
enforceFocus={true}
onEntered={() => this.teamUrlInputRef?.focus()}
onEntered={() => this.serverUrlInputRef?.focus()}
onHide={this.props.onClose}
restoreFocus={this.props.restoreFocus}
onKeyDown={(e: React.KeyboardEvent) => {
@ -272,58 +272,58 @@ class NewTeamModal extends React.PureComponent<Props, State> {
<FormGroup>
<FormLabel>
<FormattedMessage
id='renderer.components.newTeamModal.serverURL'
id='renderer.components.newServerModal.serverURL'
defaultMessage='Server URL'
/>
</FormLabel>
<FormControl
id='teamUrlInput'
id='serverUrlInput'
type='text'
value={this.state.teamUrl}
value={this.state.serverUrl}
placeholder='https://example.com'
onChange={this.handleTeamUrlChange}
onChange={this.handleServerUrlChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}}
ref={(ref: HTMLInputElement) => {
this.teamUrlInputRef = ref;
this.serverUrlInputRef = ref;
if (this.props.setInputRef) {
this.props.setInputRef(ref);
}
}}
isInvalid={Boolean(this.getTeamUrlValidationState())}
isInvalid={Boolean(this.getServerUrlValidationState())}
autoFocus={true}
/>
<FormControl.Feedback/>
<FormText>
<FormattedMessage
id='renderer.components.newTeamModal.serverURL.description'
id='renderer.components.newServerModal.serverURL.description'
defaultMessage='The URL of your Mattermost server. Must start with http:// or https://.'
/>
</FormText>
</FormGroup>
<FormGroup className='NewTeamModal-noBottomSpace'>
<FormGroup className='NewServerModal-noBottomSpace'>
<FormLabel>
<FormattedMessage
id='renderer.components.newTeamModal.serverDisplayName'
id='renderer.components.newServerModal.serverDisplayName'
defaultMessage='Server Display Name'
/>
</FormLabel>
<FormControl
id='teamNameInput'
id='serverNameInput'
type='text'
value={this.state.teamName}
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newTeamModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
onChange={this.handleTeamNameChange}
value={this.state.serverName}
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newServerModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
onChange={this.handleServerNameChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation();
}}
isInvalid={Boolean(this.getTeamNameValidationState())}
isInvalid={Boolean(this.getServerNameValidationState())}
/>
<FormControl.Feedback/>
<FormText className='NewTeamModal-noBottomSpace'>
<FormText className='NewServerModal-noBottomSpace'>
<FormattedMessage
id='renderer.components.newTeamModal.serverDisplayName.description'
id='renderer.components.newServerModal.serverDisplayName.description'
defaultMessage='The name of the server displayed on your desktop app tab bar.'
/>
</FormText>
@ -367,4 +367,4 @@ class NewTeamModal extends React.PureComponent<Props, State> {
}
}
export default injectIntl(NewTeamModal);
export default injectIntl(NewServerModal);

View file

@ -5,7 +5,7 @@ import classNames from 'classnames';
import React, {useEffect} from 'react';
import {FormattedMessage} from 'react-intl';
import '../css/components/TeamDropdownButton.scss';
import '../css/components/ServerDropdownButton.scss';
type Props = {
isDisabled?: boolean;
@ -16,7 +16,7 @@ type Props = {
darkMode: boolean;
}
const TeamDropdownButton: React.FC<Props> = (props: Props) => {
const ServerDropdownButton: React.FC<Props> = (props: Props) => {
const {isDisabled, activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props;
const buttonRef: React.RefObject<HTMLButtonElement> = React.createRef();
@ -30,22 +30,22 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
event.preventDefault();
event.stopPropagation();
if (isMenuOpen) {
window.desktop.closeTeamsDropdown();
window.desktop.closeServersDropdown();
} else {
window.desktop.openTeamsDropdown();
window.desktop.openServersDropdown();
}
};
let badgeDiv: React.ReactNode;
if (totalMentionCount > 0) {
badgeDiv = (
<div className='TeamDropdownButton__badge-count'>
<div className='ServerDropdownButton__badge-count'>
<span>{totalMentionCount > 99 ? '99+' : totalMentionCount}</span>
</div>
);
} else if (hasUnreads) {
badgeDiv = (
<div className='TeamDropdownButton__badge-unreads'/>
<div className='ServerDropdownButton__badge-unreads'/>
);
}
@ -53,7 +53,7 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
<button
ref={buttonRef}
disabled={isDisabled}
className={classNames('TeamDropdownButton', {
className={classNames('ServerDropdownButton', {
disabled: isDisabled,
isMenuOpen,
darkMode,
@ -63,14 +63,14 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
event.stopPropagation();
}}
>
<div className='TeamDropdownButton__badge'>
<div className='ServerDropdownButton__badge'>
<i className='icon-server-variant'/>
{badgeDiv}
</div>
{activeServerName && <span>{activeServerName}</span>}
{!activeServerName &&
<FormattedMessage
id='renderer.components.teamDropdownButton.noServersConfigured'
id='renderer.components.serverDropdownButton.noServersConfigured'
defaultMessage='No servers configured'
/>
}
@ -79,4 +79,4 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
);
};
export default TeamDropdownButton;
export default ServerDropdownButton;

View file

@ -8,9 +8,9 @@ import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDra
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classNames from 'classnames';
import {MattermostTab} from 'types/config';
import {UniqueView} from 'types/config';
import {TabType, canCloseTab, getTabDisplayName} from 'common/tabs/TabView';
import {ViewType, canCloseView, getViewDisplayName} from 'common/views/View';
type Props = {
activeTabId?: string;
@ -19,7 +19,7 @@ type Props = {
isDarkMode: boolean;
onSelect: (id: string) => void;
onCloseTab: (id: string) => void;
tabs: MattermostTab[];
tabs: UniqueView[];
sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>;
@ -80,7 +80,7 @@ class TabBar extends React.PureComponent<Props> {
return (
<Draggable
key={tab.id}
draggableId={`teamTabItem-${tab.id}`}
draggableId={`serverTabItem-${tab.id}`}
index={index}
>
{(provided, snapshot) => {
@ -98,10 +98,10 @@ class TabBar extends React.PureComponent<Props> {
<NavItem
ref={provided.innerRef}
as='li'
id={`teamTabItem${index}`}
id={`serverTabItem${index}`}
draggable={false}
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})}
className={classNames('teamTabItem', {
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getViewDisplayName(tab.name as ViewType)})}
className={classNames('serverTabItem', {
active: this.props.activeTabId === tab.id,
dragging: snapshot.isDragging,
})}
@ -121,12 +121,12 @@ class TabBar extends React.PureComponent<Props> {
<div className='TabBar-tabSeperator'>
<FormattedMessage
id={`common.tabs.${tab.name}`}
defaultMessage={getTabDisplayName(tab.name as TabType)}
defaultMessage={getViewDisplayName(tab.name as ViewType)}
/>
{ badgeDiv }
{canCloseTab(tab.name as TabType) &&
{canCloseView(tab.name as ViewType) &&
<button
className='teamTabItem__close'
className='serverTabItem__close'
onClick={this.onCloseTab(tab.id!)}
>
<i className='icon-close'/>

View file

@ -5,7 +5,7 @@ body {
min-height: 100%;
}
.NewTeamModal-noBottomSpace {
.NewServerModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}

View file

@ -1,4 +1,4 @@
.NewTeamModal-noBottomSpace {
.NewServerModal-noBottomSpace {
padding-bottom: 0px;
margin-bottom: 0px;
}

View file

@ -1,4 +1,4 @@
.TeamDropdownButton {
.ServerDropdownButton {
background-color: transparent;
border-width: 0 1px;
border-color: rgba(61, 60, 64, 0.08);
@ -17,7 +17,7 @@
&:not(.disabled):hover {
background-color: #f4f4f4;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-count, .ServerDropdownButton__badge-unreads {
border-color: #f4f4f4;
}
}
@ -25,7 +25,7 @@
&:not(.disabled):focus, &.isMenuOpen {
background-color: #fff;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-count, .ServerDropdownButton__badge-unreads {
border-color: #fff;
}
}
@ -49,11 +49,11 @@
}
}
.TeamDropdownButton__badge {
.ServerDropdownButton__badge {
position: relative;
}
.TeamDropdownButton__badge-count {
.ServerDropdownButton__badge-count {
background: #F74343;
border-radius: 8px;
display: flex;
@ -77,7 +77,7 @@
box-sizing: unset;
}
.TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-unreads {
background: #579eff;
border-radius: 100px;
width: 12px;
@ -88,13 +88,13 @@
border: 2px solid #efefef;
}
.TeamDropdownButton.darkMode {
.ServerDropdownButton.darkMode {
border-color: rgba(221, 221, 221, 0.08);
&:hover {
background-color: #292929;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-count, .ServerDropdownButton__badge-unreads {
border-color: #292929;
}
}
@ -102,7 +102,7 @@
&:focus, &.isMenuOpen {
background-color: #1f1f1f;
.TeamDropdownButton__badge-count, .TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-count, .ServerDropdownButton__badge-unreads {
border-color: #1f1f1f;
}
}
@ -115,15 +115,15 @@
color: rgba(221, 221, 221, 0.56);
}
.TeamDropdownButton__badge-count {
.ServerDropdownButton__badge-count {
border-color: #2e2e2e;
}
.TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-unreads {
border-color: #2e2e2e;
}
.TeamDropdownButton__badge-unreads {
.ServerDropdownButton__badge-unreads {
background: #196CAF;
}
}

View file

@ -14,7 +14,7 @@
-webkit-app-region: no-drag;
}
.TabBar .teamTabItem span {
.TabBar .serverTabItem span {
flex: 0 1 auto;
overflow: hidden;
text-overflow: ellipsis;
@ -64,7 +64,7 @@
text-decoration: none;
}
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close {
.TabBar>li.serverTabItem>a>div.TabBar-tabSeperator>.serverTabItem__close {
background: none;
border: none;
color: rgba(61,60,64,0.32);
@ -72,19 +72,19 @@
padding: 0;
}
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close:hover {
.TabBar>li.serverTabItem>a>div.TabBar-tabSeperator>.serverTabItem__close:hover {
color: #3d3c40;
}
.TabBar.darkMode>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close:hover {
.TabBar.darkMode>li.serverTabItem>a>div.TabBar-tabSeperator>.serverTabItem__close:hover {
color: #ddd;
}
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close>i::before {
.TabBar>li.serverTabItem>a>div.TabBar-tabSeperator>.serverTabItem__close>i::before {
margin: 0;
}
.TabBar.darkMode>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close {
.TabBar.darkMode>li.serverTabItem>a>div.TabBar-tabSeperator>.serverTabItem__close {
color: rgba(221,221,221,0.32);
}
@ -99,39 +99,39 @@
align-items: center;
}
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.dragging>a {
.TabBar>li.serverTabItem.active>a, .TabBar>li.serverTabItem.dragging>a {
border: none;
background-color: #fff;
color: #3d3c40;
z-index: 9;
}
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.dragging>a {
.TabBar.darkMode>li.serverTabItem.active>a, .TabBar.darkMode>li.serverTabItem.dragging>a {
background-color: #1f1f1f;
color: #ddd;
}
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
.TabBar>li.serverTabItem:not(.active)+li.serverTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(61,60,64,0.08);
margin-left: -1px;
}
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
.TabBar.darkMode>li.serverTabItem:not(.active)+li.serverTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: 1px solid rgba(221,221,221,0.08);
margin-left: -1px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
.TabBar>li.serverTabItem:not(.active):not(.disabled):hover+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled):hover+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
.TabBar>li.serverTabItem:not(.active):not(.disabled):hover+li.serverTabItem:not(.active)>a>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
.TabBar>li.teamTabItem:not(.active):not(.disabled)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
.TabBar>li.serverTabItem:not(.active):not(.disabled)+li.serverTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
border-left: none;
margin-left: 0px;
}
@ -194,6 +194,6 @@
border-radius: 50%;
}
.TabBar .teamTabItem-unread {
.TabBar .serverTabItem-unread {
font-weight: bold;
}

Some files were not shown because too many files have changed in this diff Show more