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

View file

@ -11,7 +11,7 @@ Originally created as "electron-mattermost" by Yuya Ochiai.
## Features ## Features
### Desktop integration ### Desktop integration
* Server dropdown for access to multiple teams * Server dropdown for access to multiple servers
* Dedicated tabs for Channels, Boards and Playbooks * Dedicated tabs for Channels, Boards and Playbooks
* Desktop Notifications * Desktop Notifications
* Badges for unread channels and mentions * 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`. - 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. - 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. - 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 [App Options work as documented](https://docs.mattermost.com/messaging/managing-desktop-app-options.html).
- Verify Menu Bar options work as documented. - Verify Menu Bar options work as documented.
- Use the desktop app for another 15 minutes, trying different features and functionality on the user interface. - 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(); robot.mouseClick();
} }
const exampleTeam = { const exampleServer = {
name: 'example', name: 'example',
url: exampleURL, url: exampleURL,
order: 0, order: 0,
@ -61,7 +61,7 @@ const exampleTeam = {
], ],
lastActiveTab: 0, lastActiveTab: 0,
}; };
const githubTeam = { const githubServer = {
name: 'github', name: 'github',
url: 'https://github.com/', url: 'https://github.com/',
order: 1, order: 1,
@ -87,7 +87,7 @@ const githubTeam = {
const demoConfig = { const demoConfig = {
version: 3, version: 3,
teams: [exampleTeam, githubTeam], teams: [exampleServer, githubServer],
showTrayIcon: false, showTrayIcon: false,
trayIconTheme: 'light', trayIconTheme: 'light',
minimizeToTray: false, minimizeToTray: false,
@ -113,9 +113,9 @@ const demoConfig = {
const demoMattermostConfig = { const demoMattermostConfig = {
...demoConfig, ...demoConfig,
teams: [{ teams: [{
...exampleTeam, ...exampleServer,
url: mattermostURL, url: mattermostURL,
}, githubTeam], }, githubServer],
}; };
const cmdOrCtrl = process.platform === 'darwin' ? 'command' : 'control'; const cmdOrCtrl = process.platform === 'darwin' ? 'command' : 'control';
@ -235,7 +235,7 @@ module.exports = {
return null; return null;
} }
const info = await window.testHelper.getViewInfoForTest(); 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) => { }).then((result) => {
if (result) { if (result) {
map[result.viewName] = {win, webContentsId: result.webContentsId}; 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(); return window.getBrowserViews().find((view) => view.webContents.id === id).webContents.getURL();
}, webContentsId); }, webContentsId);
isActive.should.equal('https://github.com/test/url'); isActive.should.equal('https://github.com/test/url');
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton'); const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton');
dropdownButtonText.should.equal('github'); dropdownButtonText.should.equal('github');
await this.app.close(); 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 () => { 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 mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer'); await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
const newServerView = await this.app.waitForEvent('window', { const newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'), 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 () => { 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 mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:has-text("community")'); await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
// eslint-disable-next-line dot-notation // eslint-disable-next-line dot-notation
const secondServer = this.serverMap['community___TAB_MESSAGING'].win; const secondServer = this.serverMap['community___TAB_MESSAGING'].win;
await secondServer.waitForSelector('#input_loginId'); await secondServer.waitForSelector('#input_loginId');
await secondServer.focus('#input_loginId'); await secondServer.focus('#input_loginId');
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click(`.TeamDropdown .TeamDropdown__button:has-text("${config.teams[0].name}")`); await dropdownView.click(`.ServerDropdown .ServerDropdown__button:has-text("${config.teams[0].name}")`);
const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement); const isTextboxFocused = await firstServer.$eval('#post_textbox', (el) => el === document.activeElement);
isTextboxFocused.should.be.true; isTextboxFocused.should.be.true;
@ -141,8 +141,8 @@ describe('focus', function desc() {
const textboxString = await firstServer.inputValue('#post_textbox'); const textboxString = await firstServer.inputValue('#post_textbox');
textboxString.should.equal('Mattermost'); textboxString.should.equal('Mattermost');
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:has-text("community")'); await dropdownView.click('.ServerDropdown .ServerDropdown__button:has-text("community")');
const isLoginFocused = await secondServer.$eval('#input_loginId', (el) => el === document.activeElement); const isLoginFocused = await secondServer.$eval('#input_loginId', (el) => el === document.activeElement);
isLoginFocused.should.be.true; 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 mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton'); await mainWindow.click('.ServerDropdownButton');
const firstMenuItem = await dropdownView.innerText('.TeamDropdown button.TeamDropdown__button:nth-child(1) span'); const firstMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(1) span');
const secondMenuItem = await dropdownView.innerText('.TeamDropdown button.TeamDropdown__button:nth-child(2) span'); const secondMenuItem = await dropdownView.innerText('.ServerDropdown button.ServerDropdown__button:nth-child(2) span');
firstMenuItem.should.equal(config.teams[0].name); firstMenuItem.should.equal(config.teams[0].name);
secondMenuItem.should.equal(config.teams[1].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); let dropdownHeight = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.equal(0); 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 = await browserWindow.evaluate((window) => window.getBrowserViews().find((view) => view.webContents.getURL().includes('dropdown')).getBounds().height);
dropdownHeight.should.be.greaterThan(0); 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 mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton'); await mainWindow.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown__button.addServer'); await dropdownView.click('.ServerDropdown__button.addServer');
const newServerModal = await this.app.waitForEvent('window', { const newServerModal = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'), 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 () => { it('MM-T4408_2 should show the second view after clicking the menu item', async () => {
await mainWindow.click('.TeamDropdownButton'); await mainWindow.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown button.TeamDropdown__button:nth-child(2)'); 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); const firstViewIsAttached = await browserWindow.evaluate((window, url) => Boolean(window.getBrowserViews().find((view) => view.webContents.getURL() === url)), env.exampleURL);
firstViewIsAttached.should.be.false; firstViewIsAttached.should.be.false;

View file

@ -72,23 +72,23 @@ describe('Menu/window_menu', function desc() {
after(afterFunc); after(afterFunc);
it('MM-T826_1 should show the second server', async () => { 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'); dropdownButtonText.should.equal('google');
robot.keyTap('2', ['control', process.platform === 'darwin' ? 'command' : 'shift']); 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'); dropdownButtonText.should.equal('github');
}); });
it('MM-T826_2 should show the third server', async () => { it('MM-T826_2 should show the third server', async () => {
robot.keyTap('3', ['control', process.platform === 'darwin' ? 'command' : 'shift']); 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'); dropdownButtonText.should.equal('google');
}); });
it('MM-T826_3 should show the first server', async () => { it('MM-T826_3 should show the first server', async () => {
robot.keyTap('1', ['control', process.platform === 'darwin' ? 'command' : 'shift']); 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'); 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 mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer'); await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
newServerView = await this.app.waitForEvent('window', { newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'), predicate: (window) => window.url().includes('newServer'),
}); });
@ -41,7 +41,7 @@ describe('Add Server Modal', function desc() {
let newServerView; let newServerView;
it('MM-T1312 should focus the first text input', async () => { 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; isFocused.should.be.true;
}); });
@ -53,39 +53,39 @@ describe('Add Server Modal', function desc() {
}); });
describe('MM-T4389 Invalid messages', () => { 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'); await newServerView.click('#saveNewServerModal');
const existingName = await newServerView.isVisible('#teamNameInput.is-invalid'); const existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid'); const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
existingName.should.be.true; existingName.should.be.true;
existingUrl.should.be.true; existingUrl.should.be.true;
}); });
it('should not be valid if a server with the same name exists', async () => { 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('#serverNameInput', config.teams[0].name);
await newServerView.type('#teamUrlInput', 'http://example.org'); await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal'); await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamNameInput.is-invalid'); const existing = await newServerView.isVisible('#serverNameInput.is-invalid');
existing.should.be.true; existing.should.be.true;
}); });
it('should not be valid if a server with the same URL exists', async () => { it('should not be valid if a server with the same URL exists', async () => {
await newServerView.type('#teamNameInput', 'some-new-server'); await newServerView.type('#serverNameInput', 'some-new-server');
await newServerView.type('#teamUrlInput', config.teams[0].url); await newServerView.type('#serverUrlInput', config.teams[0].url);
await newServerView.click('#saveNewServerModal'); await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamUrlInput.is-invalid'); const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true; existing.should.be.true;
}); });
describe('Valid server name', async () => { describe('Valid server name', async () => {
beforeEach(async () => { beforeEach(async () => {
await newServerView.type('#teamNameInput', 'TestTeam'); await newServerView.type('#serverNameInput', 'TestServer');
await newServerView.click('#saveNewServerModal'); await newServerView.click('#saveNewServerModal');
}); });
it('MM-T4389_2 Name should not be marked invalid, URL should be marked invalid', async () => { 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 existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid'); const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.false; existingName.should.be.false;
existingUrl.should.be.true; existingUrl.should.be.true;
@ -95,13 +95,13 @@ describe('Add Server Modal', function desc() {
describe('Valid server url', () => { describe('Valid server url', () => {
beforeEach(async () => { beforeEach(async () => {
await newServerView.type('#teamUrlInput', 'http://example.org'); await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.click('#saveNewServerModal'); await newServerView.click('#saveNewServerModal');
}); });
it('MM-T4389_3 URL should not be marked invalid, name should be marked invalid', async () => { 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 existingName = await newServerView.isVisible('#serverNameInput.is-invalid');
const existingUrl = await newServerView.isVisible('#teamUrlInput.is-invalid'); const existingUrl = await newServerView.isVisible('#serverUrlInput.is-invalid');
const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled'); const disabled = await newServerView.getAttribute('#saveNewServerModal', 'disabled');
existingName.should.be.true; existingName.should.be.true;
existingUrl.should.be.false; 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 () => { 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'); await newServerView.click('#saveNewServerModal');
const existing = await newServerView.isVisible('#teamUrlInput.is-invalid'); const existing = await newServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true; existing.should.be.true;
}); });
describe('Valid Team Settings', () => { describe('Valid Team Settings', () => {
beforeEach(async () => { beforeEach(async () => {
await newServerView.type('#teamUrlInput', 'http://example.org'); await newServerView.type('#serverUrlInput', 'http://example.org');
await newServerView.type('#teamNameInput', 'TestTeam'); await newServerView.type('#serverNameInput', 'TestServer');
}); });
it('should be possible to click add', async () => { it('should be possible to click add', async () => {
@ -128,7 +128,7 @@ describe('Add Server Modal', function desc() {
(disabled === null).should.be.true; (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 newServerView.click('#saveNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('newServer'))); 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')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'TestTeam', name: 'TestServer',
url: 'http://example.org/', url: 'http://example.org/',
order: 2, order: 2,
lastActiveTab: 0, 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 () => { 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'); await configureServerModal.type('#input_url', 'http://example.org');
const connectButtonDisabled = await configureServerModal.getAttribute('#connectConfigureServer', 'disabled'); 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 () => { 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.type('#input_url', 'lorem.ipsum.dolor.sit.amet');
await configureServerModal.click('#connectConfigureServer'); await configureServerModal.click('#connectConfigureServer');
@ -70,8 +70,8 @@ describe('Configure Server Modal', function desc() {
(connectButtonDisabled === '').should.be.true; (connectButtonDisabled === '').should.be.true;
}); });
it('MM-T5119 should add the team to the config file', async () => { it('MM-T5119 should add the server to the config file', async () => {
await configureServerModal.type('#input_name', 'TestTeam'); await configureServerModal.type('#input_name', 'TestServer');
await configureServerModal.type('#input_url', 'http://example.org'); await configureServerModal.type('#input_url', 'http://example.org');
await configureServerModal.click('#connectConfigureServer'); await configureServerModal.click('#connectConfigureServer');
@ -84,7 +84,7 @@ describe('Configure Server Modal', function desc() {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
url: 'http://example.org/', url: 'http://example.org/',
name: 'TestTeam', name: 'TestServer',
order: 0, order: 0,
lastActiveTab: 0, lastActiveTab: 0,
tabs: [ tabs: [

View file

@ -64,41 +64,41 @@ describe('server_management/drag_and_drop', function desc() {
await beforeFunc(); await beforeFunc();
mainWindow = this.app.windows().find((window) => window.url().includes('index')); mainWindow = this.app.windows().find((window) => window.url().includes('index'));
dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainWindow.click('.TeamDropdownButton'); await mainWindow.click('.ServerDropdownButton');
}); });
after(afterFunc); after(afterFunc);
it('MM-T2634_1 should appear the original order', async () => { 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(); const firstMenuItemText = await firstMenuItem.innerText();
firstMenuItemText.should.equal('example'); 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(); const secondMenuItemText = await secondMenuItem.innerText();
secondMenuItemText.should.equal('github'); 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(); const thirdMenuItemText = await thirdMenuItem.innerText();
thirdMenuItemText.should.equal('google'); thirdMenuItemText.should.equal('google');
}); });
it('MM-T2634_2 after dragging the server down, should appear in the new order', async () => { 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 // 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 initialMenuItem.focus();
await dropdownView.keyboard.down(' '); await dropdownView.keyboard.down(' ');
await dropdownView.keyboard.down('ArrowDown'); await dropdownView.keyboard.down('ArrowDown');
await dropdownView.keyboard.down(' '); await dropdownView.keyboard.down(' ');
await asyncSleep(1000); await asyncSleep(1000);
await mainWindow.keyboard.press('Escape'); await mainWindow.keyboard.press('Escape');
await mainWindow.click('.TeamDropdownButton'); await mainWindow.click('.ServerDropdownButton');
// Verify that the new order persists // 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(); const firstMenuItemText = await firstMenuItem.innerText();
firstMenuItemText.should.equal('github'); 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(); const secondMenuItemText = await secondMenuItem.innerText();
secondMenuItemText.should.equal('example'); 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(); const thirdMenuItemText = await thirdMenuItem.innerText();
thirdMenuItemText.should.equal('google'); 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 () => { it('MM-T2635_1 should be in the original order', async () => {
// Verify the original order // 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(); const firstTabText = await firstTab.innerText();
firstTabText.should.equal('Channels'); 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(); const secondTabText = await secondTab.innerText();
secondTabText.should.equal('Boards'); 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(); const thirdTabText = await thirdTab.innerText();
thirdTabText.should.equal('Playbooks'); thirdTabText.should.equal('Playbooks');
}); });
it('MM-T2635_2 after moving the tab to the right, the tab should be in the new order', async () => { 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 // 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 firstTab.focus();
await mainWindow.keyboard.down(' '); await mainWindow.keyboard.down(' ');
await mainWindow.keyboard.down('ArrowRight'); await mainWindow.keyboard.down('ArrowRight');
@ -146,13 +146,13 @@ describe('server_management/drag_and_drop', function desc() {
await asyncSleep(1000); await asyncSleep(1000);
// Verify that the new order is visible // 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(); const firstTabText = await firstTab.innerText();
firstTabText.should.equal('Boards'); 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(); const secondTabText = await secondTab.innerText();
secondTabText.should.equal('Channels'); 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(); const thirdTabText = await thirdTab.innerText();
thirdTabText.should.equal('Playbooks'); 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 mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.hover('.TeamDropdown .TeamDropdown__button:nth-child(1)'); await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:nth-child(1) button.TeamDropdown__button-edit'); await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-edit');
editServerView = await this.app.waitForEvent('window', { editServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('editServer'), predicate: (window) => window.url().includes('editServer'),
@ -39,7 +39,7 @@ describe('EditServerModal', function desc() {
let editServerView; 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 editServerView.click('#cancelNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); 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 editServerView.click('#saveNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); 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 () => { it('MM-T2826_3 should not edit server if an invalid server address has been set', async () => {
await editServerView.type('#teamUrlInput', 'superInvalid url'); await editServerView.type('#serverUrlInput', 'superInvalid url');
await editServerView.click('#saveNewServerModal'); await editServerView.click('#saveNewServerModal');
const existing = await editServerView.isVisible('#teamUrlInput.is-invalid'); const existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true; existing.should.be.true;
}); });
it('should not edit team if another server with the same name or URL exists', async () => { it('should not edit server if another server with the same name or URL exists', async () => {
await editServerView.fill('#teamNameInput', config.teams[1].name); await editServerView.fill('#serverNameInput', config.teams[1].name);
await editServerView.click('#saveNewServerModal'); await editServerView.click('#saveNewServerModal');
let existing = await editServerView.isVisible('#teamNameInput.is-invalid'); let existing = await editServerView.isVisible('#serverNameInput.is-invalid');
existing.should.be.true; existing.should.be.true;
await editServerView.fill('#teamNameInput', 'NewTestTeam'); await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.fill('#teamUrlInput', config.teams[1].url); await editServerView.fill('#serverUrlInput', config.teams[1].url);
existing = await editServerView.isVisible('#teamUrlInput.is-invalid'); existing = await editServerView.isVisible('#serverUrlInput.is-invalid');
existing.should.be.true; existing.should.be.true;
}); });
it('MM-T4391_2 should edit team when Save is pressed and name edited', async () => { it('MM-T4391_2 should edit server when Save is pressed and name edited', async () => {
await editServerView.fill('#teamNameInput', 'NewTestTeam'); await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.click('#saveNewServerModal'); await editServerView.click('#saveNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -148,7 +148,7 @@ describe('EditServerModal', function desc() {
lastActiveTab: 0, lastActiveTab: 0,
}); });
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'NewTestTeam', name: 'NewTestServer',
url: env.exampleURL, url: env.exampleURL,
order: 0, order: 0,
tabs: [ tabs: [
@ -170,8 +170,8 @@ describe('EditServerModal', function desc() {
}); });
}); });
it('MM-T4391_3 should edit team when Save is pressed and URL edited', async () => { it('MM-T4391_3 should edit server when Save is pressed and URL edited', async () => {
await editServerView.fill('#teamUrlInput', 'http://google.com'); await editServerView.fill('#serverUrlInput', 'http://google.com');
await editServerView.click('#saveNewServerModal'); await editServerView.click('#saveNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); 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 () => { it('MM-T4391_4 should edit server when Save is pressed and both edited', async () => {
await editServerView.fill('#teamNameInput', 'NewTestTeam'); await editServerView.fill('#serverNameInput', 'NewTestServer');
await editServerView.fill('#teamUrlInput', 'http://google.com'); await editServerView.fill('#serverUrlInput', 'http://google.com');
await editServerView.click('#saveNewServerModal'); await editServerView.click('#saveNewServerModal');
await asyncSleep(1000); await asyncSleep(1000);
const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer'))); const existing = Boolean(await this.app.windows().find((window) => window.url().includes('editServer')));
@ -253,7 +253,7 @@ describe('EditServerModal', function desc() {
lastActiveTab: 0, lastActiveTab: 0,
}); });
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'NewTestTeam', name: 'NewTestServer',
url: 'http://google.com/', url: 'http://google.com/',
order: 0, order: 0,
tabs: [ tabs: [

View file

@ -24,8 +24,8 @@ describe('LongServerName', function desc() {
const mainView = this.app.windows().find((window) => window.url().includes('index')); const mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.click('.TeamDropdown .TeamDropdown__button.addServer'); await dropdownView.click('.ServerDropdown .ServerDropdown__button.addServer');
newServerView = await this.app.waitForEvent('window', { newServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('newServer'), predicate: (window) => window.url().includes('newServer'),
}); });
@ -44,8 +44,8 @@ describe('LongServerName', function desc() {
let newServerView; let newServerView;
it('MM-T4050 Long server name', async () => { it('MM-T4050 Long server name', async () => {
await newServerView.type('#teamNameInput', longServerName); await newServerView.type('#serverNameInput', longServerName);
await newServerView.type('#teamUrlInput', longServerUrl); await newServerView.type('#serverUrlInput', longServerUrl);
await newServerView.click('#saveNewServerModal'); await newServerView.click('#saveNewServerModal');
await asyncSleep(1000); 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 mainView = this.app.windows().find((window) => window.url().includes('index'));
const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown')); const dropdownView = this.app.windows().find((window) => window.url().includes('dropdown'));
await mainView.click('.TeamDropdownButton'); await mainView.click('.ServerDropdownButton');
await dropdownView.hover('.TeamDropdown .TeamDropdown__button:nth-child(1)'); await dropdownView.hover('.ServerDropdown .ServerDropdown__button:nth-child(1)');
await dropdownView.click('.TeamDropdown .TeamDropdown__button:nth-child(1) button.TeamDropdown__button-remove'); await dropdownView.click('.ServerDropdown .ServerDropdown__button:nth-child(1) button.ServerDropdown__button-remove');
removeServerView = await this.app.waitForEvent('window', { removeServerView = await this.app.waitForEvent('window', {
predicate: (window) => window.url().includes('removeServer'), predicate: (window) => window.url().includes('removeServer'),
@ -42,7 +42,7 @@ describe('RemoveServerModal', function desc() {
let removeServerView; 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 removeServerView.click('button:has-text("Remove")');
await asyncSleep(1000); await asyncSleep(1000);
@ -55,7 +55,7 @@ describe('RemoveServerModal', function desc() {
savedConfig.teams.should.deep.equal(expectedConfig); 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 removeServerView.click('button:has-text("Cancel")');
await asyncSleep(1000); 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 () => { 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 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'); dropdownButtonText.should.equal('example');
}); });
@ -66,8 +66,8 @@ describe('config', function desc() {
fs.writeFileSync(env.configFilePath, JSON.stringify(oldConfig)); fs.writeFileSync(env.configFilePath, JSON.stringify(oldConfig));
this.app = await env.getApp(); this.app = await env.getApp();
const mainWindow = this.app.windows().find((window) => window.url().includes('index')); const mainWindow = this.app.windows().find((window) => window.url().includes('index'));
const dropdownButtonText = await mainWindow.innerText('.TeamDropdownButton:has-text("Primary team")'); const dropdownButtonText = await mainWindow.innerText('.ServerDropdownButton:has-text("Primary server")');
dropdownButtonText.should.equal('Primary team'); dropdownButtonText.should.equal('Primary server');
const str = fs.readFileSync(env.configFilePath, 'utf8'); const str = fs.readFileSync(env.configFilePath, 'utf8');
const upgradedConfig = JSON.parse(str); const upgradedConfig = JSON.parse(str);

View file

@ -133,7 +133,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Zertifikatsinformation", "renderer.modals.certificate.certificateModal.certInfoButton": "Zertifikatsinformation",
"renderer.dropdown.servers": "Server", "renderer.dropdown.servers": "Server",
"renderer.dropdown.addAServer": "Server hinzufügen", "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.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Seriennummer", "renderer.components.showCertificateModal.serialNumber": "Seriennummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Öffentlicher Schlüssel Info", "renderer.components.showCertificateModal.publicKeyInfo": "Öffentlicher Schlüssel Info",
@ -201,18 +201,18 @@
"renderer.components.removeServerModal.title": "Server entfernen", "renderer.components.removeServerModal.title": "Server entfernen",
"renderer.components.removeServerModal.confirm": "Bestätige, dass du den {serverName} Server entfernen möchtest?", "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.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.newServerModal.title.edit": "Server bearbeiten",
"renderer.components.newTeamModal.title.add": "Server hinzufügen", "renderer.components.newServerModal.title.add": "Server hinzufügen",
"renderer.components.newTeamModal.serverURL.description": "Die URL deines Mattermost Servers. Muss mit http:// oder https:// beginnen.", "renderer.components.newServerModal.serverURL.description": "Die URL deines Mattermost Servers. Muss mit http:// oder https:// beginnen.",
"renderer.components.newTeamModal.serverURL": "Server URL", "renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Der Name des Server, der auf der Desktop App Tab-Leiste angezeigt wird.", "renderer.components.newServerModal.serverDisplayName.description": "Der Name des Server, der auf der Desktop App Tab-Leiste angezeigt wird.",
"renderer.components.newTeamModal.serverDisplayName": "Server-Anzeigename", "renderer.components.newServerModal.serverDisplayName": "Server-Anzeigename",
"renderer.components.newTeamModal.error.urlRequired": "URL wird benötigt.", "renderer.components.newServerModal.error.urlRequired": "URL wird benötigt.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL sollte mit http:// oder https:// beginnen.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL sollte mit http:// oder https:// beginnen.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Die URL ist nicht korrekt formatiert.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "Die URL ist nicht korrekt formatiert.",
"renderer.components.newTeamModal.error.serverUrlExists": "Ein Server mit der gleichen URL existiert schon.", "renderer.components.newServerModal.error.serverUrlExists": "Ein Server mit der gleichen URL existiert schon.",
"renderer.components.newTeamModal.error.serverNameExists": "Ein Server mit diesem Namen existiert schon.", "renderer.components.newServerModal.error.serverNameExists": "Ein Server mit diesem Namen existiert schon.",
"renderer.components.newTeamModal.error.nameRequired": "Name wird benötigt.", "renderer.components.newServerModal.error.nameRequired": "Name wird benötigt.",
"renderer.components.mainPage.updateReady": "Update bereit zur Installation", "renderer.components.mainPage.updateReady": "Update bereit zur Installation",
"renderer.components.mainPage.updateAvailable": "Update verfügbar", "renderer.components.mainPage.updateAvailable": "Update verfügbar",
"renderer.components.mainPage.downloadingUpdate": "Lade Update runter. {percentDone}% von {total} @ {speed}/s", "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.input.required": "This field is required",
"renderer.components.mainPage.contextMenu.ariaLabel": "Context menu", "renderer.components.mainPage.contextMenu.ariaLabel": "Context menu",
"renderer.components.mainPage.titleBar": "Mattermost", "renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.", "renderer.components.newServerModal.error.nameRequired": "Name is required.",
"renderer.components.newTeamModal.error.serverNameExists": "A server with the same name already exists.", "renderer.components.newServerModal.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.newServerModal.error.serverUrlExists": "A server with the same URL already exists.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newTeamModal.error.urlRequired": "URL is required.", "renderer.components.newServerModal.error.urlRequired": "URL is required.",
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name", "renderer.components.newServerModal.serverDisplayName": "Server Display Name",
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.", "renderer.components.newServerModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newTeamModal.serverURL": "Server URL", "renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.", "renderer.components.newServerModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newTeamModal.title.add": "Add Server", "renderer.components.newServerModal.title.add": "Add Server",
"renderer.components.newTeamModal.title.edit": "Edit 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.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.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove Server", "renderer.components.removeServerModal.title": "Remove Server",
"renderer.components.saveButton.save": "Save", "renderer.components.saveButton.save": "Save",
"renderer.components.saveButton.saving": "Saving", "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.afterRestart": "Setting takes effect after restarting the app.",
"renderer.components.settingsPage.appLanguage": "Set app language (beta)", "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.", "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.publicKeyInfo": "Public Key Info",
"renderer.components.showCertificateModal.serialNumber": "Serial Number", "renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.subjectName": "Subject Name", "renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.teamDropdownButton.noServersConfigured": "No servers configured",
"renderer.components.welcomeScreen.button.getStarted": "Get Started", "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.subtitle": "Ship on time, every time, with a project and task management solution built for digital operations.",
"renderer.components.welcomeScreen.slides.boards.title": "Boards", "renderer.components.welcomeScreen.slides.boards.title": "Boards",

View file

@ -56,7 +56,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information", "renderer.modals.certificate.certificateModal.certInfoButton": "Certificate Information",
"renderer.dropdown.servers": "Servers", "renderer.dropdown.servers": "Servers",
"renderer.dropdown.addAServer": "Add a server", "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.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serial Number", "renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.publicKeyInfo": "Public Key Info", "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.confirm": "Confirm you wish to remove the {serverName} server?",
"renderer.components.removeServerModal.title": "Remove 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.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.newServerModal.title.edit": "Edit Server",
"renderer.components.newTeamModal.title.add": "Add Server", "renderer.components.newServerModal.title.add": "Add Server",
"renderer.components.newTeamModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.", "renderer.components.newServerModal.serverURL.description": "The URL of your Mattermost server. Must start with http:// or https://.",
"renderer.components.newTeamModal.serverURL": "Server URL", "renderer.components.newServerModal.serverURL": "Server URL",
"renderer.components.newTeamModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.", "renderer.components.newServerModal.serverDisplayName.description": "The name of the server displayed on your desktop app tab bar.",
"renderer.components.newTeamModal.serverDisplayName": "Server Display Name", "renderer.components.newServerModal.serverDisplayName": "Server Display Name",
"renderer.components.newTeamModal.error.urlRequired": "URL is required.", "renderer.components.newServerModal.error.urlRequired": "URL is required.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL should start with http:// or https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL should start with http:// or https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL is not formatted correctly.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL is not formatted correctly.",
"renderer.components.newTeamModal.error.serverUrlExists": "A server with the same URL already exists.", "renderer.components.newServerModal.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.newServerModal.error.serverNameExists": "A server with the same name already exists.",
"renderer.components.newTeamModal.error.nameRequired": "Name is required.", "renderer.components.newServerModal.error.nameRequired": "Name is required.",
"renderer.components.mainPage.updateReady": "Update ready to install", "renderer.components.mainPage.updateReady": "Update ready to install",
"renderer.components.mainPage.updateAvailable": "Update available", "renderer.components.mainPage.updateAvailable": "Update available",
"renderer.components.mainPage.downloadingUpdate": "Downloading update. {percentDone}% of {total} @ {speed}/s", "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_MESSAGING": "Canales",
"common.tabs.TAB_FOCALBOARD": "Tableros", "common.tabs.TAB_FOCALBOARD": "Tableros",
"common.permissions.canBasicAuth": "Autenticación Web", "common.permissions.canBasicAuth": "Autenticación Web",
"renderer.components.newTeamModal.serverURL": "URL del servidor", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.serverDisplayName": "Nombre del servidor",
"renderer.components.newTeamModal.error.urlRequired": "El campo de URL es obligatorio.", "renderer.components.newServerModal.error.urlRequired": "El campo de URL es obligatorio.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "La URL debería comenzar con http:// o https://.", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.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.newServerModal.error.serverNameExists": "Ya existe un servidor con ese mismo nombre.",
"renderer.components.newTeamModal.error.nameRequired": "El campo nombre es obligatorio.", "renderer.components.newServerModal.error.nameRequired": "El campo nombre es obligatorio.",
"renderer.components.mainPage.updateReady": "Actualización lista para instalar", "renderer.components.mainPage.updateReady": "Actualización lista para instalar",
"renderer.components.mainPage.updateAvailable": "Actualización disponible", "renderer.components.mainPage.updateAvailable": "Actualización disponible",
"renderer.components.mainPage.downloadingUpdate": "Descargando actualización. {percentDone}% de {total} @ {speed}/s", "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.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.slides.boards.title": "Boards",
"renderer.components.welcomeScreen.button.getStarted": "Comenzar", "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.subjectName": "Nombre del firmante",
"renderer.components.showCertificateModal.serialNumber": "Número de serie", "renderer.components.showCertificateModal.serialNumber": "Número de serie",
"renderer.components.showCertificateModal.publicKeyInfo": "Información de clave pública", "renderer.components.showCertificateModal.publicKeyInfo": "Información de clave pública",
@ -237,9 +237,9 @@
"renderer.components.removeServerModal.title": "Eliminar servidor", "renderer.components.removeServerModal.title": "Eliminar servidor",
"renderer.components.removeServerModal.confirm": "¿Deseas eliminar el servidor {serverName}?", "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.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.newServerModal.title.edit": "Editar servidor",
"renderer.components.newTeamModal.title.add": "Añadir servidor", "renderer.components.newServerModal.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.serverURL.description": "La URL de tu servidor Mattermost debe comenzar con http:// o https://.",
"renderer.components.mainPage.titleBar": "Mattermost", "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.components.configureServer.subtitle": "Configura tu primer servidor para conectar tu<br></br>hub de comunicación del equipo",
"renderer.downloadsDropdown.remaining": "restante", "renderer.downloadsDropdown.remaining": "restante",

View file

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

View file

@ -17,7 +17,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Informations sur le certificat", "renderer.modals.certificate.certificateModal.certInfoButton": "Informations sur le certificat",
"renderer.dropdown.servers": "Serveurs", "renderer.dropdown.servers": "Serveurs",
"renderer.dropdown.addAServer": "Ajouter un serveur", "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.subjectName": "Nom du sujet",
"renderer.components.showCertificateModal.serialNumber": "Numéro de série", "renderer.components.showCertificateModal.serialNumber": "Numéro de série",
"renderer.components.showCertificateModal.publicKeyInfo": "Informations sur les clés publiques", "renderer.components.showCertificateModal.publicKeyInfo": "Informations sur les clés publiques",
@ -85,18 +85,18 @@
"renderer.components.removeServerModal.title": "Retirer le serveur", "renderer.components.removeServerModal.title": "Retirer le serveur",
"renderer.components.removeServerModal.confirm": "Confirmez que vous souhaitez supprimer le serveur {serverName} ?", "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.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.newServerModal.title.edit": "Éditer le serveur",
"renderer.components.newTeamModal.title.add": "Ajouter un serveur", "renderer.components.newServerModal.title.add": "Ajouter un serveur",
"renderer.components.newTeamModal.serverURL.description": "L'URL de votre serveur Mattermost. Doit commencer par http:// ou https://.", "renderer.components.newServerModal.serverURL.description": "L'URL de votre serveur Mattermost. Doit commencer par http:// ou https://.",
"renderer.components.newTeamModal.serverURL": "URL du serveur", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.serverDisplayName": "Nom d'affichage du serveur",
"renderer.components.newTeamModal.error.urlRequired": "URL requise.", "renderer.components.newServerModal.error.urlRequired": "URL requise.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "L'URL doit commencer par http:// ou https://.", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.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.newServerModal.error.serverNameExists": "Un serveur portant le même nom existe déjà.",
"renderer.components.newTeamModal.error.nameRequired": "Le nom est obligatoire.", "renderer.components.newServerModal.error.nameRequired": "Le nom est obligatoire.",
"renderer.components.mainPage.updateReady": "Mise à jour prête à être installée", "renderer.components.mainPage.updateReady": "Mise à jour prête à être installée",
"renderer.components.mainPage.updateAvailable": "Mise à jour disponible", "renderer.components.mainPage.updateAvailable": "Mise à jour disponible",
"renderer.components.mainPage.downloadingUpdate": "Téléchargement de la mise à jour. {percentDone}% de {total} @ {speed}/s", "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.modals.certificate.certificateModal.certInfoButton": "Tanúsítvány információk",
"renderer.dropdown.servers": "Kiszolgálók", "renderer.dropdown.servers": "Kiszolgálók",
"renderer.dropdown.addAServer": "Kiszogáló hozzáadása", "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.subjectName": "Tárgy neve",
"renderer.components.showCertificateModal.serialNumber": "Sorozatszám", "renderer.components.showCertificateModal.serialNumber": "Sorozatszám",
"renderer.components.showCertificateModal.publicKeyInfo": "Nyilvános kulcs információk", "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.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.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.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.newServerModal.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.newServerModal.title.edit": "Kiszolgáló szerkesztése",
"renderer.components.newTeamModal.title.add": "Kiszolgáló hozzáadása", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.serverURL": "Kiszolgáló URL",
"renderer.components.newTeamModal.serverDisplayName": "Kiszolgáló megjelenítési neve", "renderer.components.newServerModal.serverDisplayName": "Kiszolgáló megjelenítési neve",
"renderer.components.newTeamModal.error.urlRequired": "URL kötelező.", "renderer.components.newServerModal.error.urlRequired": "URL kötelező.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Az URL-nek http:// vagy https:// -el kell kezdődnie.", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.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.newServerModal.error.serverNameExists": "Egy ilyen nevű kiszolgáló már létezik.",
"renderer.components.newTeamModal.error.nameRequired": "Név kötelező.", "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.updateReady": "Frissítés készen áll a telepítésre",
"renderer.components.mainPage.updateAvailable": "Frissítés érhető el", "renderer.components.mainPage.updateAvailable": "Frissítés érhető el",
"renderer.components.errorView.troubleshooting.urlIsCorrect.appNameIsCorrect": "A {appName} alkalmazás URL <link>{url}</link> helyes", "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.flashWindow.description.note": "NOTA: ",
"renderer.components.settingsPage.enableHardwareAcceleration": "Usa l'accelerazione hardware della GPU", "renderer.components.settingsPage.enableHardwareAcceleration": "Usa l'accelerazione hardware della GPU",
"renderer.components.settingsPage.appLanguage.useSystemDefault": "Usa il default di sistema", "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.removeServerModal.title": "Rimuovi server",
"renderer.components.settingsPage.appOptions": "Opzioni dell'app", "renderer.components.settingsPage.appOptions": "Opzioni dell'app",
"renderer.components.saveButton.save": "Salva", "renderer.components.saveButton.save": "Salva",
"renderer.components.newTeamModal.title.edit": "Modifica server", "renderer.components.newServerModal.title.edit": "Modifica server",
"renderer.components.newTeamModal.title.add": "Aggiungi server", "renderer.components.newServerModal.title.add": "Aggiungi server",
"renderer.components.input.required": "Questo campo è richiesto", "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.mainPage.titleBar": "Mattermost",
"renderer.components.extraBar.back": "Indietro", "renderer.components.extraBar.back": "Indietro",
"renderer.components.configureServer.url.placeholder": "URL del server", "renderer.components.configureServer.url.placeholder": "URL del server",

View file

@ -20,7 +20,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "証明書情報", "renderer.modals.certificate.certificateModal.certInfoButton": "証明書情報",
"renderer.dropdown.servers": "サーバー", "renderer.dropdown.servers": "サーバー",
"renderer.dropdown.addAServer": "サーバーを追加", "renderer.dropdown.addAServer": "サーバーを追加",
"renderer.components.teamDropdownButton.noServersConfigured": "サーバーが設定されていません", "renderer.components.serverDropdownButton.noServersConfigured": "サーバーが設定されていません",
"renderer.components.showCertificateModal.subjectName": "Subject Name", "renderer.components.showCertificateModal.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serial Number", "renderer.components.showCertificateModal.serialNumber": "Serial Number",
"renderer.components.showCertificateModal.publicKeyInfo": "公開鍵情報", "renderer.components.showCertificateModal.publicKeyInfo": "公開鍵情報",
@ -88,18 +88,18 @@
"renderer.components.removeServerModal.title": "サーバーを削除", "renderer.components.removeServerModal.title": "サーバーを削除",
"renderer.components.removeServerModal.confirm": "本当に {serverName} サーバーを削除しますか?", "renderer.components.removeServerModal.confirm": "本当に {serverName} サーバーを削除しますか?",
"renderer.components.removeServerModal.body": "この操作によりデスクトップアプリからサーバーが削除されますが、データは削除されません - いつでもアプリにサーバーを追加し直すことができます。", "renderer.components.removeServerModal.body": "この操作によりデスクトップアプリからサーバーが削除されますが、データは削除されません - いつでもアプリにサーバーを追加し直すことができます。",
"renderer.components.newTeamModal.title.edit": "サーバーを編集", "renderer.components.newServerModal.title.edit": "サーバーを編集",
"renderer.components.newTeamModal.title.add": "サーバーを追加", "renderer.components.newServerModal.title.add": "サーバーを追加",
"renderer.components.newTeamModal.serverURL.description": "あなたのMattermostサーバーのURLです。http:// または https:// で始まる必要があります。", "renderer.components.newServerModal.serverURL.description": "あなたのMattermostサーバーのURLです。http:// または https:// で始まる必要があります。",
"renderer.components.newTeamModal.serverURL": "サーバーURL", "renderer.components.newServerModal.serverURL": "サーバーURL",
"renderer.components.newTeamModal.serverDisplayName.description": "デスクトップアプリのタブバーに表示されるサーバーの名前です。", "renderer.components.newServerModal.serverDisplayName.description": "デスクトップアプリのタブバーに表示されるサーバーの名前です。",
"renderer.components.newTeamModal.serverDisplayName": "サーバー表示名", "renderer.components.newServerModal.serverDisplayName": "サーバー表示名",
"renderer.components.newTeamModal.error.urlRequired": "URLは必須です。", "renderer.components.newServerModal.error.urlRequired": "URLは必須です。",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URLは http:// または https:// で始まる必要があります。", "renderer.components.newServerModal.error.urlNeedsHttp": "URLは http:// または https:// で始まる必要があります。",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URLの形式が正しくありません。", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URLの形式が正しくありません。",
"renderer.components.newTeamModal.error.serverUrlExists": "同じURLのサーバーが既に存在しています。", "renderer.components.newServerModal.error.serverUrlExists": "同じURLのサーバーが既に存在しています。",
"renderer.components.newTeamModal.error.serverNameExists": "同名のサーバーが既に存在しています。", "renderer.components.newServerModal.error.serverNameExists": "同名のサーバーが既に存在しています。",
"renderer.components.newTeamModal.error.nameRequired": "名前は必須です。", "renderer.components.newServerModal.error.nameRequired": "名前は必須です。",
"renderer.components.mainPage.updateReady": "更新をインストールする準備ができました", "renderer.components.mainPage.updateReady": "更新をインストールする準備ができました",
"renderer.components.mainPage.updateAvailable": "更新が利用可能です", "renderer.components.mainPage.updateAvailable": "更新が利用可能です",
"renderer.components.mainPage.downloadingUpdate": "更新をダウンロード中です。{total} 中 {percentDone}% 完了@ {speed}/s", "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.title": "보드",
"renderer.components.welcomeScreen.slides.boards.subtitle": "디지털 오퍼레이션에 최적화된 프로젝트 및 작업 관리 솔루션을 사용해 항상 기한 내에 제품을 출시하세요.", "renderer.components.welcomeScreen.slides.boards.subtitle": "디지털 오퍼레이션에 최적화된 프로젝트 및 작업 관리 솔루션을 사용해 항상 기한 내에 제품을 출시하세요.",
"renderer.components.welcomeScreen.button.getStarted": "시작하기", "renderer.components.welcomeScreen.button.getStarted": "시작하기",
"renderer.components.teamDropdownButton.noServersConfigured": "구성된 서버 없음", "renderer.components.serverDropdownButton.noServersConfigured": "구성된 서버 없음",
"renderer.components.showCertificateModal.subjectName": "주체 이름(Subject Name)", "renderer.components.showCertificateModal.subjectName": "주체 이름(Subject Name)",
"renderer.components.showCertificateModal.serialNumber": "일련번호", "renderer.components.showCertificateModal.serialNumber": "일련번호",
"renderer.components.showCertificateModal.publicKeyInfo": "공개 키 정보", "renderer.components.showCertificateModal.publicKeyInfo": "공개 키 정보",
@ -225,18 +225,18 @@
"renderer.components.removeServerModal.title": "서버 제거", "renderer.components.removeServerModal.title": "서버 제거",
"renderer.components.removeServerModal.confirm": "{serverName} 서버를 제거하시겠습니까?", "renderer.components.removeServerModal.confirm": "{serverName} 서버를 제거하시겠습니까?",
"renderer.components.removeServerModal.body": "이렇게 하면 데스크톱 앱에서 서버가 제거되지만 해당 데이터는 삭제되지 않습니다. 언제든지 앱에 서버를 다시 추가할 수 있습니다.", "renderer.components.removeServerModal.body": "이렇게 하면 데스크톱 앱에서 서버가 제거되지만 해당 데이터는 삭제되지 않습니다. 언제든지 앱에 서버를 다시 추가할 수 있습니다.",
"renderer.components.newTeamModal.title.edit": "서버 수정", "renderer.components.newServerModal.title.edit": "서버 수정",
"renderer.components.newTeamModal.title.add": "서버 추가", "renderer.components.newServerModal.title.add": "서버 추가",
"renderer.components.newTeamModal.serverURL.description": "Mattermost 서버 URL입니다. http:// 또는 https://로 시작해야 합니다.", "renderer.components.newServerModal.serverURL.description": "Mattermost 서버 URL입니다. http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newTeamModal.serverURL": "서버 URL", "renderer.components.newServerModal.serverURL": "서버 URL",
"renderer.components.newTeamModal.serverDisplayName.description": "데스크톱 앱 탭 표시줄에 표시되는 서버 이름입니다.", "renderer.components.newServerModal.serverDisplayName.description": "데스크톱 앱 탭 표시줄에 표시되는 서버 이름입니다.",
"renderer.components.newTeamModal.serverDisplayName": "서버 표시 이름", "renderer.components.newServerModal.serverDisplayName": "서버 표시 이름",
"renderer.components.newTeamModal.error.urlRequired": "URL이 필요합니다.", "renderer.components.newServerModal.error.urlRequired": "URL이 필요합니다.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL은 http:// 또는 https://로 시작해야 합니다.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL은 http:// 또는 https://로 시작해야 합니다.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL 형식이 올바르지 않습니다.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL 형식이 올바르지 않습니다.",
"renderer.components.newTeamModal.error.serverUrlExists": "동일한 URL을 가진 서버가 이미 존재합니다.", "renderer.components.newServerModal.error.serverUrlExists": "동일한 URL을 가진 서버가 이미 존재합니다.",
"renderer.components.newTeamModal.error.serverNameExists": "같은 이름의 서버가 이미 존재합니다.", "renderer.components.newServerModal.error.serverNameExists": "같은 이름의 서버가 이미 존재합니다.",
"renderer.components.newTeamModal.error.nameRequired": "이름이 필요합니다.", "renderer.components.newServerModal.error.nameRequired": "이름이 필요합니다.",
"renderer.components.mainPage.titleBar": "Mattermost", "renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "컨텍스트 메뉴", "renderer.components.mainPage.contextMenu.ariaLabel": "컨텍스트 메뉴",
"renderer.components.input.required": "이 필드는 필수입니다", "renderer.components.input.required": "이 필드는 필수입니다",

View file

@ -48,7 +48,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Informatie over het certificaat", "renderer.modals.certificate.certificateModal.certInfoButton": "Informatie over het certificaat",
"renderer.dropdown.servers": "Servers", "renderer.dropdown.servers": "Servers",
"renderer.dropdown.addAServer": "Voeg een server toe", "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.subjectName": "Subject Name",
"renderer.components.showCertificateModal.serialNumber": "Serienummer", "renderer.components.showCertificateModal.serialNumber": "Serienummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Info publieke sleutel", "renderer.components.showCertificateModal.publicKeyInfo": "Info publieke sleutel",
@ -116,18 +116,18 @@
"renderer.components.removeServerModal.title": "Server verwijderen", "renderer.components.removeServerModal.title": "Server verwijderen",
"renderer.components.removeServerModal.confirm": "Bevestig dat je de {serverNaam} server wil 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.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.newServerModal.title.edit": "Server bewerken",
"renderer.components.newTeamModal.title.add": "Server toevoegen", "renderer.components.newServerModal.title.add": "Server toevoegen",
"renderer.components.newTeamModal.serverURL.description": "De URL van je Mattermost server. Moet beginnen met http:// of https://.", "renderer.components.newServerModal.serverURL.description": "De URL van je Mattermost server. Moet beginnen met http:// of https://.",
"renderer.components.newTeamModal.serverURL": "Server-URL", "renderer.components.newServerModal.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.newServerModal.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.newServerModal.serverDisplayName": "Weergave naam server",
"renderer.components.newTeamModal.error.urlRequired": "URL is verplicht.", "renderer.components.newServerModal.error.urlRequired": "URL is verplicht.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL moet beginnen met http:// of https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL moet beginnen met http:// of https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL heeft een ongeldig formaat.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL heeft een ongeldig formaat.",
"renderer.components.newTeamModal.error.serverUrlExists": "Een server met dezelfde URL bestaat reeds.", "renderer.components.newServerModal.error.serverUrlExists": "Een server met dezelfde URL bestaat reeds.",
"renderer.components.newTeamModal.error.serverNameExists": "Een server met dezelfde naam bestaat reeds.", "renderer.components.newServerModal.error.serverNameExists": "Een server met dezelfde naam bestaat reeds.",
"renderer.components.newTeamModal.error.nameRequired": "Naam is verplicht.", "renderer.components.newServerModal.error.nameRequired": "Naam is verplicht.",
"renderer.components.mainPage.updateReady": "Update klaar om geïnstalleerd te worden", "renderer.components.mainPage.updateReady": "Update klaar om geïnstalleerd te worden",
"renderer.components.mainPage.updateAvailable": "Update beschikbaar", "renderer.components.mainPage.updateAvailable": "Update beschikbaar",
"renderer.components.mainPage.downloadingUpdate": "Update wordt gedownload. {percentDone}% van {total} @ {speed}/s", "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.title": "Usuń Serwer",
"renderer.components.removeServerModal.confirm": "Potwierdzasz, że chcesz usunąć serwer {serverName}?", "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.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.newServerModal.title.edit": "Edytuj Serwer",
"renderer.components.newTeamModal.title.add": "Dodaj Serwer", "renderer.components.newServerModal.title.add": "Dodaj Serwer",
"renderer.components.newTeamModal.serverURL.description": "Adres URL Twojego serwera Mattermost. Musi zaczynać się od http:// lub https://.", "renderer.components.newServerModal.serverURL.description": "Adres URL Twojego serwera Mattermost. Musi zaczynać się od http:// lub https://.",
"renderer.components.newTeamModal.serverURL": "URL Serwera", "renderer.components.newServerModal.serverURL": "URL Serwera",
"renderer.components.newTeamModal.serverDisplayName.description": "Nazwa serwera wyświetlana na pasku zakładek aplikacji desktopowej.", "renderer.components.newServerModal.serverDisplayName.description": "Nazwa serwera wyświetlana na pasku zakładek aplikacji desktopowej.",
"renderer.components.newTeamModal.serverDisplayName": "Wyświetlana Nazwa Serwera", "renderer.components.newServerModal.serverDisplayName": "Wyświetlana Nazwa Serwera",
"renderer.components.newTeamModal.error.urlRequired": "Wymagany jest adres URL.", "renderer.components.newServerModal.error.urlRequired": "Wymagany jest adres URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Adres URL powinien zaczynać się od http:// lub https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "Adres URL powinien zaczynać się od http:// lub https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Adres URL nie jest poprawnie sformatowany.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "Adres URL nie jest poprawnie sformatowany.",
"renderer.components.newTeamModal.error.serverUrlExists": "Serwer o tym samym adresie URL już istnieje.", "renderer.components.newServerModal.error.serverUrlExists": "Serwer o tym samym adresie URL już istnieje.",
"renderer.components.newTeamModal.error.serverNameExists": "Serwer o tej samej nazwie już istnieje.", "renderer.components.newServerModal.error.serverNameExists": "Serwer o tej samej nazwie już istnieje.",
"renderer.components.newTeamModal.error.nameRequired": "Nazwa jest wymagana.", "renderer.components.newServerModal.error.nameRequired": "Nazwa jest wymagana.",
"renderer.components.mainPage.updateReady": "Aktualizacja gotowa do zainstalowania", "renderer.components.mainPage.updateReady": "Aktualizacja gotowa do zainstalowania",
"renderer.components.mainPage.updateAvailable": "Dostępna aktualizacja", "renderer.components.mainPage.updateAvailable": "Dostępna aktualizacja",
"renderer.components.mainPage.downloadingUpdate": "Pobieranie aktualizacji. {percentDone}% z {total} @ {speed}/s", "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.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.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.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.subjectName": "Nazwa tematu",
"renderer.components.showCertificateModal.serialNumber": "Numer seryjny", "renderer.components.showCertificateModal.serialNumber": "Numer seryjny",
"renderer.components.showCertificateModal.publicKeyInfo": "Informacje o kluczach publicznych", "renderer.components.showCertificateModal.publicKeyInfo": "Informacje o kluczach publicznych",

View file

@ -128,10 +128,10 @@
"main.menus.app.view.downloads": "Downloads", "main.menus.app.view.downloads": "Downloads",
"renderer.components.saveButton.save": "Salvar", "renderer.components.saveButton.save": "Salvar",
"renderer.components.removeServerModal.title": "Remover Servidor", "renderer.components.removeServerModal.title": "Remover Servidor",
"renderer.components.newTeamModal.title.edit": "Editar Servidor", "renderer.components.newServerModal.title.edit": "Editar Servidor",
"renderer.components.newTeamModal.title.add": "Adicionar Servidor", "renderer.components.newServerModal.title.add": "Adicionar Servidor",
"renderer.components.newTeamModal.serverURL": "URL do Servidor", "renderer.components.newServerModal.serverURL": "URL do Servidor",
"renderer.components.newTeamModal.error.urlRequired": "URL é necessária.", "renderer.components.newServerModal.error.urlRequired": "URL é necessária.",
"renderer.components.mainPage.contextMenu.ariaLabel": "Menu de contexto", "renderer.components.mainPage.contextMenu.ariaLabel": "Menu de contexto",
"renderer.components.extraBar.back": "Voltar", "renderer.components.extraBar.back": "Voltar",
"renderer.components.errorView.troubleshooting.computerIsConnected": "Seu computador está conectado à internet.", "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.title": "Boards",
"renderer.components.welcomeScreen.slides.boards.subtitle": "Успевайте каждый раз вовремя выполнить задачи, с решением для управления проектами и задачами, созданным для цифровых операций.", "renderer.components.welcomeScreen.slides.boards.subtitle": "Успевайте каждый раз вовремя выполнить задачи, с решением для управления проектами и задачами, созданным для цифровых операций.",
"renderer.components.welcomeScreen.button.getStarted": "Начать", "renderer.components.welcomeScreen.button.getStarted": "Начать",
"renderer.components.teamDropdownButton.noServersConfigured": "Серверы не настроены", "renderer.components.serverDropdownButton.noServersConfigured": "Серверы не настроены",
"renderer.components.showCertificateModal.subjectName": "Имя субъекта (SN)", "renderer.components.showCertificateModal.subjectName": "Имя субъекта (SN)",
"renderer.components.showCertificateModal.serialNumber": "Серийный номер", "renderer.components.showCertificateModal.serialNumber": "Серийный номер",
"renderer.components.showCertificateModal.publicKeyInfo": "Информация о публичном ключе", "renderer.components.showCertificateModal.publicKeyInfo": "Информация о публичном ключе",
@ -118,19 +118,19 @@
"renderer.components.removeServerModal.title": "Удалить сервер", "renderer.components.removeServerModal.title": "Удалить сервер",
"renderer.components.removeServerModal.confirm": "Подтвердите, что вы хотите удалить сервер {serverName}?", "renderer.components.removeServerModal.confirm": "Подтвердите, что вы хотите удалить сервер {serverName}?",
"renderer.components.removeServerModal.body": "Это действие удалит сервер из вашего приложения, но не удалит его данные - вы можете добавить сервер обратно в приложение в любое время.", "renderer.components.removeServerModal.body": "Это действие удалит сервер из вашего приложения, но не удалит его данные - вы можете добавить сервер обратно в приложение в любое время.",
"renderer.components.newTeamModal.title.edit": "Изменение настроек сервера", "renderer.components.newServerModal.title.edit": "Изменение настроек сервера",
"renderer.components.newTeamModal.title.add": "Добавить сервер", "renderer.components.newServerModal.title.add": "Добавить сервер",
"renderer.components.newTeamModal.serverURL.description": "URL вашего сервера Mattermost. Должен начинаться с http:// или https://.", "renderer.components.newServerModal.serverURL.description": "URL вашего сервера Mattermost. Должен начинаться с http:// или https://.",
"renderer.components.newTeamModal.serverURL": "URL сервера", "renderer.components.newServerModal.serverURL": "URL сервера",
"renderer.components.newTeamModal.serverDisplayName.description": "Имя сервера, отображаемое на панели вкладок вашего приложения.", "renderer.components.newServerModal.serverDisplayName.description": "Имя сервера, отображаемое на панели вкладок вашего приложения.",
"renderer.components.newTeamModal.serverDisplayName": "Отображаемое имя сервера", "renderer.components.newServerModal.serverDisplayName": "Отображаемое имя сервера",
"common.tabs.TAB_MESSAGING": "Channels", "common.tabs.TAB_MESSAGING": "Channels",
"renderer.components.newTeamModal.error.nameRequired": "Укажите имя сервера.", "renderer.components.newServerModal.error.nameRequired": "Укажите имя сервера.",
"renderer.components.newTeamModal.error.urlRequired": "Укажите URL-адрес.", "renderer.components.newServerModal.error.urlRequired": "Укажите URL-адрес.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "URL-адрес должен начинаться с http:// или https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "URL-адрес должен начинаться с http:// или https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL-адрес неверно отформатирован.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL-адрес неверно отформатирован.",
"renderer.components.newTeamModal.error.serverUrlExists": "Сервер с таким URL уже существует.", "renderer.components.newServerModal.error.serverUrlExists": "Сервер с таким URL уже существует.",
"renderer.components.newTeamModal.error.serverNameExists": "Сервер с таким же именем уже существует.", "renderer.components.newServerModal.error.serverNameExists": "Сервер с таким же именем уже существует.",
"renderer.components.mainPage.titleBar": "Mattermost", "renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "Контекстное меню", "renderer.components.mainPage.contextMenu.ariaLabel": "Контекстное меню",
"renderer.components.input.required": "Обязательное поле", "renderer.components.input.required": "Обязательное поле",

View file

@ -24,18 +24,18 @@
"renderer.components.removeServerModal.title": "Ta bort servern", "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.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.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.newServerModal.title.edit": "Redigera server",
"renderer.components.newTeamModal.title.add": "Lägg till server", "renderer.components.newServerModal.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.newServerModal.serverURL.description": "URL för din Mattermost-server. Måste börja med http:// eller https://.",
"renderer.components.newTeamModal.serverURL": "Server-URL", "renderer.components.newServerModal.serverURL": "Server-URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Namnet på servern som visas på fliken i skrivbordsappen.", "renderer.components.newServerModal.serverDisplayName.description": "Namnet på servern som visas på fliken i skrivbordsappen.",
"renderer.components.newTeamModal.serverDisplayName": "Serverns visningsnamn", "renderer.components.newServerModal.serverDisplayName": "Serverns visningsnamn",
"renderer.components.newTeamModal.error.urlRequired": "Du måste ange en URL.", "renderer.components.newServerModal.error.urlRequired": "Du måste ange en URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Måste börja med http:// eller https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "Måste börja med http:// eller https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "URL-adressen är inte korrekt formaterad.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "URL-adressen är inte korrekt formaterad.",
"renderer.components.newTeamModal.error.serverUrlExists": "Det finns redan en server med samma URL.", "renderer.components.newServerModal.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.newServerModal.error.serverNameExists": "Det finns redan en server med samma namn.",
"renderer.components.newTeamModal.error.nameRequired": "Du måste ange Namn.", "renderer.components.newServerModal.error.nameRequired": "Du måste ange Namn.",
"renderer.components.mainPage.updateReady": "Uppdatering klar att installera", "renderer.components.mainPage.updateReady": "Uppdatering klar att installera",
"renderer.components.mainPage.updateAvailable": "Uppdatering tillgänglig", "renderer.components.mainPage.updateAvailable": "Uppdatering tillgänglig",
"renderer.components.mainPage.downloadingUpdate": "Hämtar uppdatering. {percentDone}% av {total} @ {speed}/s", "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.modals.certificate.certificateModal.certInfoButton": "Information om certifikat",
"renderer.dropdown.servers": "Servrar", "renderer.dropdown.servers": "Servrar",
"renderer.dropdown.addAServer": "Lägg till en server", "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.subjectName": "Ämnesnamn",
"renderer.components.showCertificateModal.serialNumber": "Serienummer", "renderer.components.showCertificateModal.serialNumber": "Serienummer",
"renderer.components.showCertificateModal.publicKeyInfo": "Offentlig nyckel", "renderer.components.showCertificateModal.publicKeyInfo": "Offentlig nyckel",

View file

@ -17,7 +17,7 @@
"renderer.modals.certificate.certificateModal.certInfoButton": "Sertifika bilgileri", "renderer.modals.certificate.certificateModal.certInfoButton": "Sertifika bilgileri",
"renderer.dropdown.servers": "Sunucular", "renderer.dropdown.servers": "Sunucular",
"renderer.dropdown.addAServer": "Bir sunucu ekle", "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.subjectName": "Özne adı",
"renderer.components.showCertificateModal.serialNumber": "Seri numarası", "renderer.components.showCertificateModal.serialNumber": "Seri numarası",
"renderer.components.showCertificateModal.publicKeyInfo": "Herkese açık anahtar bilgileri", "renderer.components.showCertificateModal.publicKeyInfo": "Herkese açık anahtar bilgileri",
@ -85,18 +85,18 @@
"renderer.components.removeServerModal.title": "Sunucuyu kaldır", "renderer.components.removeServerModal.title": "Sunucuyu kaldır",
"renderer.components.removeServerModal.confirm": "{serverName} sunucunu kaldırmak istediğinizi onaylıyor musunuz?", "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.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.newServerModal.title.edit": "Sunucuyu düzenle",
"renderer.components.newTeamModal.title.add": "Sunucu ekle", "renderer.components.newServerModal.title.add": "Sunucu ekle",
"renderer.components.newTeamModal.serverURL.description": "Mattermost sunucunuzun adresi. http:// ya da https:// ile başlamalıdır.", "renderer.components.newServerModal.serverURL.description": "Mattermost sunucunuzun adresi. http:// ya da https:// ile başlamalıdır.",
"renderer.components.newTeamModal.serverURL": "Sunucu adresi", "renderer.components.newServerModal.serverURL": "Sunucu adresi",
"renderer.components.newTeamModal.serverDisplayName.description": "Sunucunun masaüstü uygulama sekmesi çubuğunda görüntülenecek adı.", "renderer.components.newServerModal.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.newServerModal.serverDisplayName": "Sunucunun görüntülenecek adı",
"renderer.components.newTeamModal.error.urlRequired": "Adresin yazılması zorunludur.", "renderer.components.newServerModal.error.urlRequired": "Adresin yazılması zorunludur.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Adres http:// ya da https:// ile başlamalıdır.", "renderer.components.newServerModal.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.newServerModal.error.urlIncorrectFormatting": "Adresin biçimi doğru değil.",
"renderer.components.newTeamModal.error.serverUrlExists": "Aynı adresi kullanan bir sunucu zaten var.", "renderer.components.newServerModal.error.serverUrlExists": "Aynı adresi kullanan bir sunucu zaten var.",
"renderer.components.newTeamModal.error.serverNameExists": "Aynı adlı bir sunucu zaten var.", "renderer.components.newServerModal.error.serverNameExists": "Aynı adlı bir sunucu zaten var.",
"renderer.components.newTeamModal.error.nameRequired": "Ad yazılması zorunludur.", "renderer.components.newServerModal.error.nameRequired": "Ad yazılması zorunludur.",
"renderer.components.mainPage.updateReady": "Güncelleme kurulmaya hazır", "renderer.components.mainPage.updateReady": "Güncelleme kurulmaya hazır",
"renderer.components.mainPage.updateAvailable": "Güncelleme yayınlanmış", "renderer.components.mainPage.updateAvailable": "Güncelleme yayınlanmış",
"renderer.components.mainPage.downloadingUpdate": "Güncelleme indiriliyor. %{percentDone}/{total} indirildi. Hız: {speed}/s", "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.title": "Видалити сервер",
"renderer.components.removeServerModal.confirm": "Ви дійсно хочете прибрати цей {serverName} сервер?", "renderer.components.removeServerModal.confirm": "Ви дійсно хочете прибрати цей {serverName} сервер?",
"renderer.components.removeServerModal.body": "Це прибере сервер із вашого десктопного додатку, але не видалить його даних ви можете повернути сервер до застосунку у будь–який час.", "renderer.components.removeServerModal.body": "Це прибере сервер із вашого десктопного додатку, але не видалить його даних ви можете повернути сервер до застосунку у будь–який час.",
"renderer.components.newTeamModal.title.edit": "Редагувати сервер", "renderer.components.newServerModal.title.edit": "Редагувати сервер",
"renderer.components.newTeamModal.title.add": "Додати сервер", "renderer.components.newServerModal.title.add": "Додати сервер",
"renderer.components.newTeamModal.serverURL.description": "Посилання URL на сервер Mattermost повинно починатись із with http:// or https://.", "renderer.components.newServerModal.serverURL.description": "Посилання URL на сервер Mattermost повинно починатись із with http:// or https://.",
"renderer.components.newTeamModal.serverURL": "Посилання URL", "renderer.components.newServerModal.serverURL": "Посилання URL",
"renderer.components.newTeamModal.serverDisplayName.description": "Ім'я серверу відображатиметься на панелі вкладок у десктопному додатку.", "renderer.components.newServerModal.serverDisplayName.description": "Ім'я серверу відображатиметься на панелі вкладок у десктопному додатку.",
"renderer.components.newTeamModal.serverDisplayName": "Ім'я серверу, що відображатиметься", "renderer.components.newServerModal.serverDisplayName": "Ім'я серверу, що відображатиметься",
"renderer.components.newTeamModal.error.urlRequired": "Необхідне посилання URL.", "renderer.components.newServerModal.error.urlRequired": "Необхідне посилання URL.",
"renderer.components.newTeamModal.error.urlNeedsHttp": "Посилання URL має починатись із http:// or https://.", "renderer.components.newServerModal.error.urlNeedsHttp": "Посилання URL має починатись із http:// or https://.",
"renderer.components.newTeamModal.error.urlIncorrectFormatting": "Посилання URL має неправильний формат.", "renderer.components.newServerModal.error.urlIncorrectFormatting": "Посилання URL має неправильний формат.",
"renderer.components.newTeamModal.error.serverUrlExists": "Сервер з таким посиланням URL вже існує.", "renderer.components.newServerModal.error.serverUrlExists": "Сервер з таким посиланням URL вже існує.",
"renderer.components.newTeamModal.error.serverNameExists": "Сервер з таким ім'ям вже існує.", "renderer.components.newServerModal.error.serverNameExists": "Сервер з таким ім'ям вже існує.",
"renderer.components.newTeamModal.error.nameRequired": "Необхідне ім'я.", "renderer.components.newServerModal.error.nameRequired": "Необхідне ім'я.",
"renderer.components.mainPage.titleBar": "Mattermost", "renderer.components.mainPage.titleBar": "Mattermost",
"renderer.components.mainPage.contextMenu.ariaLabel": "Контекстне меню", "renderer.components.mainPage.contextMenu.ariaLabel": "Контекстне меню",
"renderer.components.input.required": "Це поле є обов'язковим", "renderer.components.input.required": "Це поле є обов'язковим",
@ -180,7 +180,7 @@
"common.tabs.TAB_FOCALBOARD": "Дошки", "common.tabs.TAB_FOCALBOARD": "Дошки",
"renderer.components.welcomeScreen.slides.boards.title": "Дошки", "renderer.components.welcomeScreen.slides.boards.title": "Дошки",
"renderer.components.welcomeScreen.button.getStarted": "Розпочнемо", "renderer.components.welcomeScreen.button.getStarted": "Розпочнемо",
"renderer.components.teamDropdownButton.noServersConfigured": "Немає налаштованих серверів", "renderer.components.serverDropdownButton.noServersConfigured": "Немає налаштованих серверів",
"renderer.components.showCertificateModal.serialNumber": "Серійний номер", "renderer.components.showCertificateModal.serialNumber": "Серійний номер",
"renderer.components.showCertificateModal.notValidBefore": "Недійсний до", "renderer.components.showCertificateModal.notValidBefore": "Недійсний до",
"renderer.components.showCertificateModal.notValidAfter": "Недійсний після", "renderer.components.showCertificateModal.notValidAfter": "Недійсний після",

View file

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

View file

@ -1,6 +1,6 @@
# Developer guide for using Group Policy Objects (GPO) (Windows 10 Pro) # 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) 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 | | Value Name | Value |
|------------|----------------------------------| |------------|----------------------------------|
| Community | <https://community.mattermost.com> | | 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'); const buildConfig = require('../dist/buildConfig');
function validateBuildConfig(config) { function validateBuildConfig(config) {
if (config.enableServerManagement === false && config.defaultTeams && config.defaultTeams.length === 0) { if (config.enableServerManagement === false && config.defaultServers && config.defaultServers.length === 0) {
return { return {
result: false, 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}; 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 ## 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. 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://`. 3. Enter **Name** and a valid **URL**, which begins with either `http://` or `https://`.
4. Click **Add**. 4. Click **Add**.
5. Click **Save**. 5. Click **Save**.

View file

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

View file

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

View file

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

View file

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

View file

@ -6,7 +6,7 @@ import {EventEmitter} from 'events';
import WindowsRegistry from 'winreg'; import WindowsRegistry from 'winreg';
import WindowsRegistryUTF8 from 'winreg-utf8'; 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'; import {Logger} from 'common/log';
@ -26,7 +26,7 @@ export default class RegistryConfig extends EventEmitter {
super(); super();
this.initialized = false; this.initialized = false;
this.data = { this.data = {
teams: [], servers: [],
}; };
} }
@ -41,7 +41,7 @@ export default class RegistryConfig extends EventEmitter {
try { try {
const servers = await this.getServersListFromRegistry(); const servers = await this.getServersListFromRegistry();
if (servers.length) { if (servers.length) {
this.data.teams!.push(...servers); this.data.servers!.push(...servers);
} }
} catch (error) { } catch (error) {
log.warn('Nothing retrieved for \'DefaultServerList\'', error); log.warn('Nothing retrieved for \'DefaultServerList\'', error);
@ -78,7 +78,7 @@ export default class RegistryConfig extends EventEmitter {
*/ */
async getServersListFromRegistry() { async getServersListFromRegistry() {
const defaultServers = await this.getRegistryEntry(`${BASE_REGISTRY_KEY_PATH}\\DefaultServerList`); 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) { if (server) {
servers.push({ servers.push({
name: (server as WindowsRegistry.RegistryItem).name, 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. * Build-time configuration. End-users can't change these parameters.
* @prop {Object[]} defaultTeams * @prop {Object[]} defaultServers
* @prop {string} defaultTeams[].name - The tab name for default team. * @prop {string} defaultServers[].name - The view name for default server.
* @prop {string} defaultTeams[].url - The URL for default team. * @prop {string} defaultServers[].url - The URL for default server.
* @prop {string} defaultTeams[].order - Sort order for team tabs (0, 1, 2) * @prop {string} defaultServers[].order - Sort order for server views (0, 1, 2)
* @prop {string} helpLink - The URL for "Help->Learn More..." menu item. * @prop {string} helpLink - The URL for "Help->Learn More..." menu item.
* If null is specified, the menu disappears. * If null is specified, the menu disappears.
* @prop {boolean} enableServerManagement - Whether users can edit servers configuration. * @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 * when "enableServerManagement is set to false
* @prop {[]} managedResources - Defines which paths are managed * @prop {[]} managedResources - Defines which paths are managed
* @prop {[]} allowedProtocols - Defines which protocols should be automatically allowed * @prop {[]} allowedProtocols - Defines which protocols should be automatically allowed
*/ */
const buildConfig: BuildConfig = { const buildConfig: BuildConfig = {
defaultTeams: [/* defaultServers: [/*
{ {
name: 'example', name: 'example',
url: 'https://example.com' url: 'https://example.com'

View file

@ -24,54 +24,54 @@ jest.mock('common/Validator', () => ({
validateConfigData: (configData) => (configData.version === 3 ? configData : null), validateConfigData: (configData) => (configData.version === 3 ? configData : null),
})); }));
jest.mock('common/tabs/TabView', () => ({ jest.mock('common/views/View', () => ({
getDefaultConfigTeamFromTeam: (value) => ({ getDefaultViewsForConfigServer: (value) => ({
...value, ...value,
tabs: [ tabs: [
{ {
name: 'tab1', name: 'view1',
}, },
{ {
name: 'tab2', name: 'view2',
}, },
], ],
}), }),
})); }));
const buildTeam = { const buildServer = {
name: 'build-team-1', name: 'build-server-1',
order: 0, order: 0,
url: 'http://build-team-1.com', url: 'http://build-server-1.com',
}; };
const buildTeamWithTabs = { const buildServerWithViews = {
...buildTeam, ...buildServer,
tabs: [ tabs: [
{ {
name: 'tab1', name: 'view1',
}, },
{ {
name: 'tab2', name: 'view2',
}, },
], ],
}; };
const registryTeam = { const registryServer = {
name: 'registry-team-1', name: 'registry-server-1',
order: 0, order: 0,
url: 'http://registry-team-1.com', url: 'http://registry-server-1.com',
}; };
const team = { const server = {
name: 'team-1', name: 'server-1',
order: 0, order: 0,
url: 'http://team-1.com', url: 'http://server-1.com',
tabs: [ 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', () => { jest.mock('common/config/buildConfig', () => {
return { return {
defaultTeams: [buildTeam], defaultServers: [buildServer],
}; };
}); });
@ -99,7 +99,7 @@ describe('common/config', () => {
const config = new Config(); const config = new Config();
config.reload = jest.fn(); config.reload = jest.fn();
config.init(configPath, appName, appPath); config.init(configPath, appName, appPath);
expect(config.predefinedTeams).toContainEqual(buildTeamWithTabs); expect(config.predefinedServers).toContainEqual(buildServerWithViews);
}); });
describe('loadRegistry', () => { describe('loadRegistry', () => {
@ -107,16 +107,16 @@ describe('common/config', () => {
const config = new Config(); const config = new Config();
config.reload = jest.fn(); config.reload = jest.fn();
config.init(configPath, appName, appPath); config.init(configPath, appName, appPath);
config.onLoadRegistry({teams: [registryTeam]}); config.onLoadRegistry({servers: [registryServer]});
expect(config.reload).toHaveBeenCalled(); expect(config.reload).toHaveBeenCalled();
expect(config.predefinedTeams).toContainEqual({ expect(config.predefinedServers).toContainEqual({
...registryTeam, ...registryServer,
tabs: [ tabs: [
{ {
name: 'tab1', name: 'view1',
}, },
{ {
name: 'tab2', name: 'view2',
}, },
], ],
}); });
@ -159,19 +159,19 @@ describe('common/config', () => {
expect(config.saveLocalConfigData).toHaveBeenCalled(); 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(); const config = new Config();
config.reload = jest.fn(); config.reload = jest.fn();
config.init(configPath, appName, appPath); config.init(configPath, appName, appPath);
config.localConfigData = {teams: [team]}; config.localConfigData = {teams: [server]};
config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => { config.regenerateCombinedConfigData = jest.fn().mockImplementation(() => {
config.combinedData = {...config.localConfigData}; config.combinedData = {...config.localConfigData};
}); });
config.saveLocalConfigData = jest.fn(); config.saveLocalConfigData = jest.fn();
config.set('teams', [{...buildTeamWithTabs, name: 'build-team-2'}]); config.set('teams', [{...buildServerWithViews, name: 'build-team-2'}]);
expect(config.localConfigData.teams).not.toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); expect(config.localConfigData.teams).not.toContainEqual({...buildServerWithViews, name: 'build-team-2'});
expect(config.localConfigData.teams).toContainEqual(team); expect(config.localConfigData.teams).toContainEqual(server);
}); });
}); });
@ -186,8 +186,8 @@ describe('common/config', () => {
}); });
config.saveLocalConfigData = jest.fn(); config.saveLocalConfigData = jest.fn();
config.setServers([{...buildTeamWithTabs, name: 'build-team-2'}, team], 0); config.setServers([{...buildServerWithViews, name: 'build-server-2'}, server], 0);
expect(config.localConfigData.teams).toContainEqual({...buildTeamWithTabs, name: 'build-team-2'}); expect(config.localConfigData.teams).toContainEqual({...buildServerWithViews, name: 'build-server-2'});
expect(config.localConfigData.lastActiveTeam).toBe(0); expect(config.localConfigData.lastActiveTeam).toBe(0);
expect(config.regenerateCombinedConfigData).toHaveBeenCalled(); expect(config.regenerateCombinedConfigData).toHaveBeenCalled();
expect(config.saveLocalConfigData).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(); const config = new Config();
config.reload = jest.fn(); config.reload = jest.fn();
config.init(configPath, appName, appPath); config.init(configPath, appName, appPath);
@ -349,20 +349,20 @@ describe('common/config', () => {
config.localConfigData = {}; config.localConfigData = {};
config.buildConfigData = {enableServerManagement: true}; config.buildConfigData = {enableServerManagement: true};
config.registryConfigData = {}; config.registryConfigData = {};
config.predefinedTeams.push(team, team); config.predefinedServers.push(server, server);
config.useNativeWindow = false; config.useNativeWindow = false;
config.localConfigData = {teams: [ config.localConfigData = {teams: [
team, server,
{ {
...team, ...server,
name: 'local-team-2', name: 'local-server-2',
url: 'http://local-team-2.com', url: 'http://local-server-2.com',
}, },
{ {
...team, ...server,
name: 'local-team-1', name: 'local-server-1',
order: 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'; } from 'types/config';
import {Logger} from 'common/log'; 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 Utils, {copy} from 'common/utils/util';
import * as Validator from 'common/Validator'; import * as Validator from 'common/Validator';
@ -36,7 +36,7 @@ export class Config extends EventEmitter {
private appPath?: string; private appPath?: string;
private registryConfig: RegistryConfig; private registryConfig: RegistryConfig;
private predefinedServers: ConfigServer[]; private _predefinedServers: ConfigServer[];
private useNativeWindow: boolean; private useNativeWindow: boolean;
private combinedData?: CombinedConfig; private combinedData?: CombinedConfig;
@ -49,9 +49,9 @@ export class Config extends EventEmitter {
constructor() { constructor() {
super(); super();
this.registryConfig = new RegistryConfig(); this.registryConfig = new RegistryConfig();
this.predefinedServers = []; this._predefinedServers = [];
if (buildConfig.defaultTeams) { if (buildConfig.defaultServers) {
this.predefinedServers.push(...buildConfig.defaultTeams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index}))); this._predefinedServers.push(...buildConfig.defaultServers.map((server, index) => getDefaultViewsForConfigServer({...server, order: index})));
} }
try { try {
this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2'); this.useNativeWindow = os.platform() === 'win32' && !Utils.isVersionGreaterThanOrEqualTo(os.release(), '6.2');
@ -138,10 +138,10 @@ export class Config extends EventEmitter {
this.saveLocalConfigData(); this.saveLocalConfigData();
} }
setServers = (servers: ConfigServer[], lastActiveTeam?: number) => { setServers = (servers: ConfigServer[], lastActiveServer?: number) => {
log.debug('setServers', servers, lastActiveTeam); 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.regenerateCombinedConfigData();
this.saveLocalConfigData(); this.saveLocalConfigData();
} }
@ -172,11 +172,11 @@ export class Config extends EventEmitter {
get darkMode() { get darkMode() {
return this.combinedData?.darkMode ?? defaultPreferences.darkMode; return this.combinedData?.darkMode ?? defaultPreferences.darkMode;
} }
get localTeams() { get localServers() {
return this.localConfigData?.teams ?? defaultPreferences.teams; return this.localConfigData?.teams ?? defaultPreferences.teams;
} }
get predefinedTeams() { get predefinedServers() {
return this.predefinedServers; return this._predefinedServers;
} }
get enableHardwareAcceleration() { get enableHardwareAcceleration() {
return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration; return this.combinedData?.enableHardwareAcceleration ?? defaultPreferences.enableHardwareAcceleration;
@ -229,7 +229,7 @@ export class Config extends EventEmitter {
get minimizeToTray() { get minimizeToTray() {
return this.combinedData?.minimizeToTray; return this.combinedData?.minimizeToTray;
} }
get lastActiveTeam() { get lastActiveServer() {
return this.combinedData?.lastActiveTeam; return this.combinedData?.lastActiveTeam;
} }
get alwaysClose() { 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 => { private onLoadRegistry = (registryData: Partial<RegistryConfigType>): void => {
log.debug('loadRegistry', {registryData}); log.debug('loadRegistry', {registryData});
this.registryConfigData = registryData; this.registryConfigData = registryData;
if (this.registryConfigData.teams) { if (this.registryConfigData.servers) {
this.predefinedTeams.push(...this.registryConfigData.teams.map((team, index) => getDefaultConfigTeamFromTeam({...team, order: index}))); this._predefinedServers.push(...this.registryConfigData.servers.map((server, index) => getDefaultViewsForConfigServer({...server, order: index})));
} }
this.reload(); 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 // 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).teams;
delete (this.combinedData as any).defaultTeams; delete (this.combinedData as any).servers;
delete (this.combinedData as any).defaultServers;
if (this.combinedData) { if (this.combinedData) {
this.combinedData.appName = this.appName; this.combinedData.appName = this.appName;

View file

@ -4,15 +4,15 @@
import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgradePreferences'; import {upgradeV0toV1, upgradeV1toV2, upgradeV2toV3} from 'common/config/upgradePreferences';
import pastDefaultPreferences from 'common/config/pastDefaultPreferences'; import pastDefaultPreferences from 'common/config/pastDefaultPreferences';
jest.mock('common/tabs/TabView', () => ({ jest.mock('common/views/View', () => ({
getDefaultConfigTeamFromTeam: (value) => ({ getDefaultViewsForConfigServer: (value) => ({
...value, ...value,
tabs: [ tabs: [
{ {
name: 'tab1', name: 'view1',
}, },
{ {
name: 'tab2', name: 'view2',
}, },
], ],
}), }),
@ -27,7 +27,7 @@ describe('common/config/upgradePreferences', () => {
version: 1, version: 1,
teams: [ teams: [
{ {
name: 'Primary team', name: 'Primary server',
url: config.url, url: config.url,
}, },
], ],
@ -39,10 +39,10 @@ describe('common/config/upgradePreferences', () => {
const config = { const config = {
version: 1, version: 1,
teams: [{ teams: [{
name: 'Primary team', name: 'Primary server',
url: 'http://server-1.com', url: 'http://server-1.com',
}, { }, {
name: 'Secondary team', name: 'Secondary server',
url: 'http://server-2.com', url: 'http://server-2.com',
}], }],
showTrayIcon: true, showTrayIcon: true,
@ -64,11 +64,11 @@ describe('common/config/upgradePreferences', () => {
...config, ...config,
version: 2, version: 2,
teams: [{ teams: [{
name: 'Primary team', name: 'Primary server',
url: 'http://server-1.com', url: 'http://server-1.com',
order: 0, order: 0,
}, { }, {
name: 'Secondary team', name: 'Secondary server',
url: 'http://server-2.com', url: 'http://server-2.com',
order: 1, order: 1,
}], }],
@ -80,11 +80,11 @@ describe('common/config/upgradePreferences', () => {
const config = { const config = {
version: 2, version: 2,
teams: [{ teams: [{
name: 'Primary team', name: 'Primary server',
url: 'http://server-1.com', url: 'http://server-1.com',
order: 0, order: 0,
}, { }, {
name: 'Secondary team', name: 'Secondary server',
url: 'http://server-2.com', url: 'http://server-2.com',
order: 1, order: 1,
}], }],
@ -111,27 +111,27 @@ describe('common/config/upgradePreferences', () => {
...config, ...config,
version: 3, version: 3,
teams: [{ teams: [{
name: 'Primary team', name: 'Primary server',
url: 'http://server-1.com', url: 'http://server-1.com',
order: 0, order: 0,
tabs: [ tabs: [
{ {
name: 'tab1', name: 'view1',
}, },
{ {
name: 'tab2', name: 'view2',
}, },
], ],
}, { }, {
name: 'Secondary team', name: 'Secondary server',
url: 'http://server-2.com', url: 'http://server-2.com',
order: 1, order: 1,
tabs: [ 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 {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; import {getDefaultViewsForConfigServer} from 'common/views/View';
import pastDefaultPreferences from './pastDefaultPreferences'; import pastDefaultPreferences from './pastDefaultPreferences';
@ -15,7 +15,7 @@ function deepCopy<T>(object: T): T {
export function upgradeV0toV1(configV0: ConfigV0) { export function upgradeV0toV1(configV0: ConfigV0) {
const config = deepCopy(pastDefaultPreferences[1]); const config = deepCopy(pastDefaultPreferences[1]);
config.teams.push({ config.teams.push({
name: 'Primary team', name: 'Primary server',
url: configV0.url, url: configV0.url,
}); });
return config; return config;
@ -37,7 +37,7 @@ export function upgradeV2toV3(configV2: ConfigV2) {
const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2); const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2);
config.version = 3; config.version = 3;
config.teams = configV2.teams.map((value) => { config.teams = configV2.teams.map((value) => {
return getDefaultConfigTeamFromTeam(value); return getDefaultViewsForConfigServer(value);
}); });
config.lastActiveTeam = 0; config.lastActiveTeam = 0;
config.spellCheckerLocales = []; config.spellCheckerLocales = [];

View file

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

View file

@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // 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 {parseURL, isInternalURL} from 'common/utils/url';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
@ -31,12 +31,12 @@ describe('common/servers/serverManager', () => {
server.url = new URL(url); server.url = new URL(url);
}; };
serverManager.servers = new Map([['server-1', server]]); serverManager.servers = new Map([['server-1', server]]);
serverManager.tabs = new Map([ serverManager.views = new Map([
['tab-1', {id: 'tab-1', type: TAB_MESSAGING, isOpen: true, server}], ['view-1', {id: 'view-1', type: TAB_MESSAGING, isOpen: true, server}],
['tab-2', {id: 'tab-2', type: TAB_PLAYBOOKS, server}], ['view-2', {id: 'view-2', type: TAB_PLAYBOOKS, server}],
['tab-3', {id: 'tab-3', type: TAB_FOCALBOARD, 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(); serverManager.persistServers = jest.fn();
Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0'); Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0');
}); });
@ -52,7 +52,7 @@ describe('common/servers/serverManager', () => {
expect(serverManager.persistServers).not.toHaveBeenCalled(); expect(serverManager.persistServers).not.toHaveBeenCalled();
}); });
it('should open all tabs', async () => { it('should open all views', async () => {
serverManager.updateRemoteInfos(new Map([['server-1', { serverManager.updateRemoteInfos(new Map([['server-1', {
siteURL: 'http://server-1.com', siteURL: 'http://server-1.com',
serverVersion: '6.0.0', serverVersion: '6.0.0',
@ -60,8 +60,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: true, hasFocalboard: true,
}]])); }]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBe(true); expect(serverManager.views.get('view-2').isOpen).toBe(true);
expect(serverManager.tabs.get('tab-3').isOpen).toBe(true); expect(serverManager.views.get('view-3').isOpen).toBe(true);
}); });
it('should open only playbooks', async () => { it('should open only playbooks', async () => {
@ -72,8 +72,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: false, hasFocalboard: false,
}]])); }]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBe(true); expect(serverManager.views.get('view-2').isOpen).toBe(true);
expect(serverManager.tabs.get('tab-3').isOpen).toBeUndefined(); expect(serverManager.views.get('view-3').isOpen).toBeUndefined();
}); });
it('should open none when server version is too old', async () => { it('should open none when server version is too old', async () => {
@ -84,8 +84,8 @@ describe('common/servers/serverManager', () => {
hasFocalboard: true, hasFocalboard: true,
}]])); }]]));
expect(serverManager.tabs.get('tab-2').isOpen).toBeUndefined(); expect(serverManager.views.get('view-2').isOpen).toBeUndefined();
expect(serverManager.tabs.get('tab-3').isOpen).toBeUndefined(); expect(serverManager.views.get('view-3').isOpen).toBeUndefined();
}); });
it('should update server URL using site URL', async () => { 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(); const serverManager = new ServerManager();
serverManager.getAllServers = () => [ serverManager.getAllServers = () => [
{id: 'server-1', url: new URL('http://server-1.com')}, {id: 'server-1', url: new URL('http://server-1.com')},
@ -109,16 +109,16 @@ describe('common/servers/serverManager', () => {
serverManager.getOrderedTabsForServer = (serverId) => { serverManager.getOrderedTabsForServer = (serverId) => {
if (serverId === 'server-1') { if (serverId === 'server-1') {
return [ return [
{id: 'tab-1', url: new URL('http://server-1.com')}, {id: 'view-1', url: new URL('http://server-1.com')},
{id: 'tab-1-type-1', url: new URL('http://server-1.com/type1')}, {id: 'view-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-type-2', url: new URL('http://server-1.com/type2')},
]; ];
} }
if (serverId === 'server-2') { if (serverId === 'server-2') {
return [ return [
{id: 'tab-2', url: new URL('http://server-2.com/subpath')}, {id: 'view-2', url: new URL('http://server-2.com/subpath')},
{id: 'tab-2-type-1', url: new URL('http://server-2.com/subpath/type1')}, {id: 'view-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-type-2', url: new URL('http://server-2.com/subpath/type2')},
]; ];
} }
return []; return [];
@ -135,47 +135,47 @@ describe('common/servers/serverManager', () => {
it('should match the correct server - base URL', () => { it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com'); 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', () => { it('should match the correct server - base view', () => {
const inputURL = new URL('http://server-1.com/team'); const inputURL = new URL('http://server-1.com/server');
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 - different tab', () => { it('should match the correct server - different view', () => {
const inputURL = new URL('http://server-1.com/type1/app'); 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', () => { it('should return undefined for server with subpath and URL without', () => {
const inputURL = new URL('http://server-2.com'); 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', () => { it('should return undefined for server with subpath and URL with wrong subpath', () => {
const inputURL = new URL('http://server-2.com/different/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', () => { it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath'); 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', () => { it('should match the correct server with a subpath - base view', () => {
const inputURL = new URL('http://server-2.com/subpath/team'); const inputURL = new URL('http://server-2.com/subpath/server');
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 - different tab', () => { it('should match the correct server with a subpath - different view', () => {
const inputURL = new URL('http://server-2.com/subpath/type2/team'); const inputURL = new URL('http://server-2.com/subpath/type2/server');
expect(serverManager.lookupTabByURL(inputURL)).toStrictEqual({id: 'tab-2-type-2', url: new URL('http://server-2.com/subpath/type2')}); 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', () => { it('should return undefined for wrong server', () => {
const inputURL = new URL('http://server-3.com'); 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 EventEmitter from 'events';
import {Team, ConfigServer, ConfigTab} from 'types/config'; import {Server, ConfigServer, ConfigView} from 'types/config';
import {RemoteInfo} from 'types/server'; import {RemoteInfo} from 'types/server';
import Config from 'common/config'; import Config from 'common/config';
@ -13,10 +13,10 @@ import {
} from 'common/communication'; } from 'common/communication';
import {Logger, getLevel} from 'common/log'; import {Logger, getLevel} from 'common/log';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS, TabView, getDefaultTabs} from 'common/tabs/TabView'; import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS, MattermostView, getDefaultViews} from 'common/views/View';
import MessagingTabView from 'common/tabs/MessagingTabView'; import MessagingView from 'common/views/MessagingView';
import FocalboardTabView from 'common/tabs/FocalboardTabView'; import FocalboardView from 'common/views/FocalboardView';
import PlaybooksTabView from 'common/tabs/PlaybooksTabView'; import PlaybooksView from 'common/views/PlaybooksView';
import {isInternalURL, parseURL} from 'common/utils/url'; import {isInternalURL, parseURL} from 'common/utils/url';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
@ -28,9 +28,9 @@ export class ServerManager extends EventEmitter {
private serverOrder: string[]; private serverOrder: string[];
private currentServerId?: string; private currentServerId?: string;
private tabs: Map<string, TabView>; private views: Map<string, MattermostView>;
private tabOrder: Map<string, string[]>; private viewOrder: Map<string, string[]>;
private lastActiveTab: Map<string, string>; private lastActiveView: Map<string, string>;
constructor() { constructor() {
super(); super();
@ -38,25 +38,25 @@ export class ServerManager extends EventEmitter {
this.servers = new Map(); this.servers = new Map();
this.remoteInfo = new Map(); this.remoteInfo = new Map();
this.serverOrder = []; this.serverOrder = [];
this.tabs = new Map(); this.views = new Map();
this.tabOrder = new Map(); this.viewOrder = new Map();
this.lastActiveTab = new Map(); this.lastActiveView = new Map();
} }
getOrderedTabsForServer = (serverId: string) => { getOrderedTabsForServer = (serverId: string) => {
log.withPrefix(serverId).debug('getOrderedTabsForServer'); log.withPrefix(serverId).debug('getOrderedTabsForServer');
const tabOrder = this.tabOrder.get(serverId); const viewOrder = this.viewOrder.get(serverId);
if (!tabOrder) { if (!viewOrder) {
return []; return [];
} }
return tabOrder.reduce((tabs, tabId) => { return viewOrder.reduce((views, viewId) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (tab) { if (view) {
tabs.push(tab); views.push(view);
} }
return tabs; return views;
}, [] as TabView[]); }, [] as MattermostView[]);
} }
getOrderedServers = () => { getOrderedServers = () => {
@ -87,22 +87,22 @@ export class ServerManager extends EventEmitter {
getLastActiveTabForServer = (serverId: string) => { getLastActiveTabForServer = (serverId: string) => {
log.withPrefix(serverId).debug('getLastActiveTabForServer'); log.withPrefix(serverId).debug('getLastActiveTabForServer');
const lastActiveTab = this.lastActiveTab.get(serverId); const lastActiveView = this.lastActiveView.get(serverId);
if (lastActiveTab) { if (lastActiveView) {
const tab = this.tabs.get(lastActiveTab); const view = this.views.get(lastActiveView);
if (tab && tab?.isOpen) { if (view && view?.isOpen) {
return tab; return view;
} }
} }
return this.getFirstOpenTabForServer(serverId); return this.getFirstOpenViewForServer(serverId);
} }
getServer = (id: string) => { getServer = (id: string) => {
return this.servers.get(id); return this.servers.get(id);
} }
getTab = (id: string) => { getView = (id: string) => {
return this.tabs.get(id); return this.views.get(id);
} }
getAllServers = () => { getAllServers = () => {
@ -122,7 +122,7 @@ export class ServerManager extends EventEmitter {
remoteInfos.forEach((remoteInfo, serverId) => { remoteInfos.forEach((remoteInfo, serverId) => {
this.remoteInfo.set(serverId, remoteInfo); this.remoteInfo.set(serverId, remoteInfo);
hasUpdates = this.updateServerURL(serverId) || hasUpdates; hasUpdates = this.updateServerURL(serverId) || hasUpdates;
hasUpdates = this.openExtraTabs(serverId) || hasUpdates; hasUpdates = this.openExtraViews(serverId) || hasUpdates;
}); });
if (hasUpdates) { if (hasUpdates) {
@ -130,8 +130,8 @@ export class ServerManager extends EventEmitter {
} }
} }
lookupTabByURL = (inputURL: URL | string, ignoreScheme = false) => { lookupViewByURL = (inputURL: URL | string, ignoreScheme = false) => {
log.silly('lookupTabByURL', `${inputURL}`, ignoreScheme); log.silly('lookupViewByURL', `${inputURL}`, ignoreScheme);
const parsedURL = parseURL(inputURL); const parsedURL = parseURL(inputURL);
if (!parsedURL) { if (!parsedURL) {
@ -143,17 +143,17 @@ export class ServerManager extends EventEmitter {
if (!server) { if (!server) {
return undefined; return undefined;
} }
const tabs = this.getOrderedTabsForServer(server.id); const views = this.getOrderedTabsForServer(server.id);
let selectedTab = tabs.find((tab) => tab && tab.type === TAB_MESSAGING); let selectedView = views.find((view) => view && view.type === TAB_MESSAGING);
tabs. views.
filter((tab) => tab && tab.type !== TAB_MESSAGING). filter((view) => view && view.type !== TAB_MESSAGING).
forEach((tab) => { forEach((view) => {
if (parsedURL.pathname.match(new RegExp(`^${tab.url.pathname}(/(.+))?`))) { if (parsedURL.pathname.match(new RegExp(`^${view.url.pathname}(/(.+))?`))) {
selectedTab = tab; selectedView = view;
} }
}); });
return selectedTab; return selectedView;
} }
updateServerOrder = (serverOrder: string[]) => { updateServerOrder = (serverOrder: string[]) => {
@ -163,14 +163,14 @@ export class ServerManager extends EventEmitter {
this.persistServers(); this.persistServers();
} }
updateTabOrder = (serverId: string, tabOrder: string[]) => { updateTabOrder = (serverId: string, viewOrder: string[]) => {
log.withPrefix(serverId).debug('updateTabOrder', tabOrder); log.withPrefix(serverId).debug('updateTabOrder', viewOrder);
this.tabOrder.set(serverId, tabOrder); this.viewOrder.set(serverId, viewOrder);
this.persistServers(); this.persistServers();
} }
addServer = (server: Team) => { addServer = (server: Server) => {
const newServer = new MattermostServer(server, false); const newServer = new MattermostServer(server, false);
if (this.servers.has(newServer.id)) { if (this.servers.has(newServer.id)) {
@ -179,13 +179,13 @@ export class ServerManager extends EventEmitter {
this.servers.set(newServer.id, newServer); this.servers.set(newServer.id, newServer);
this.serverOrder.push(newServer.id); this.serverOrder.push(newServer.id);
const tabOrder: string[] = []; const viewOrder: string[] = [];
getDefaultTabs().forEach((tab) => { getDefaultViews().forEach((view) => {
const newTab = this.getTabView(newServer, tab.name, tab.isOpen); const newView = this.getNewView(newServer, view.name, view.isOpen);
this.tabs.set(newTab.id, newTab); this.views.set(newView.id, newView);
tabOrder.push(newTab.id); viewOrder.push(newView.id);
}); });
this.tabOrder.set(newServer.id, tabOrder); this.viewOrder.set(newServer.id, viewOrder);
if (!this.currentServerId) { if (!this.currentServerId) {
this.currentServerId = newServer.id; this.currentServerId = newServer.id;
@ -197,7 +197,7 @@ export class ServerManager extends EventEmitter {
return newServer; return newServer;
} }
editServer = (serverId: string, server: Team) => { editServer = (serverId: string, server: Server) => {
const existingServer = this.servers.get(serverId); const existingServer = this.servers.get(serverId);
if (!existingServer) { if (!existingServer) {
return; return;
@ -212,11 +212,11 @@ export class ServerManager extends EventEmitter {
existingServer.updateURL(server.url); existingServer.updateURL(server.url);
this.servers.set(serverId, existingServer); this.servers.set(serverId, existingServer);
this.tabOrder.get(serverId)?.forEach((tabId) => { this.viewOrder.get(serverId)?.forEach((viewId) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (tab) { if (view) {
tab.server = existingServer; view.server = existingServer;
this.tabs.set(tabId, tab); this.views.set(viewId, view);
} }
}); });
@ -225,9 +225,9 @@ export class ServerManager extends EventEmitter {
} }
removeServer = (serverId: string) => { removeServer = (serverId: string) => {
this.tabOrder.get(serverId)?.forEach((tabId) => this.tabs.delete(tabId)); this.viewOrder.get(serverId)?.forEach((viewId) => this.views.delete(viewId));
this.tabOrder.delete(serverId); this.viewOrder.delete(serverId);
this.lastActiveTab.delete(serverId); this.lastActiveView.delete(serverId);
const index = this.serverOrder.findIndex((id) => id === serverId); const index = this.serverOrder.findIndex((id) => id === serverId);
this.serverOrder.splice(index, 1); this.serverOrder.splice(index, 1);
@ -241,26 +241,26 @@ export class ServerManager extends EventEmitter {
this.persistServers(); this.persistServers();
} }
setTabIsOpen = (tabId: string, isOpen: boolean) => { setViewIsOpen = (viewId: string, isOpen: boolean) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (!tab) { if (!view) {
return; return;
} }
tab.isOpen = isOpen; view.isOpen = isOpen;
this.persistServers(); this.persistServers();
} }
updateLastActive = (tabId: string) => { updateLastActive = (viewId: string) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (!tab) { if (!view) {
return; 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) { if (serverOrder < 0) {
throw new Error('Server order corrupt, ID not found.'); throw new Error('Server order corrupt, ID not found.');
} }
@ -270,26 +270,26 @@ export class ServerManager extends EventEmitter {
reloadFromConfig = () => { reloadFromConfig = () => {
const serverOrder: string[] = []; const serverOrder: string[] = [];
Config.predefinedTeams.forEach((team) => { Config.predefinedServers.forEach((server) => {
const id = this.initServer(team, true); const id = this.initServer(server, true);
serverOrder.push(id); serverOrder.push(id);
}); });
if (Config.enableServerManagement) { if (Config.enableServerManagement) {
Config.localTeams.sort((a, b) => a.order - b.order).forEach((team) => { Config.localServers.sort((a, b) => a.order - b.order).forEach((server) => {
const id = this.initServer(team, false); const id = this.initServer(server, false);
serverOrder.push(id); serverOrder.push(id);
}); });
} }
this.filterOutDuplicateTeams(); this.filterOutDuplicateServers();
this.serverOrder = serverOrder; this.serverOrder = serverOrder;
if (Config.lastActiveTeam && this.serverOrder[Config.lastActiveTeam]) { if (Config.lastActiveServer && this.serverOrder[Config.lastActiveServer]) {
this.currentServerId = this.serverOrder[Config.lastActiveTeam]; this.currentServerId = this.serverOrder[Config.lastActiveServer];
} else { } else {
this.currentServerId = this.serverOrder[0]; this.currentServerId = this.serverOrder[0];
} }
} }
private filterOutDuplicateTeams = () => { private filterOutDuplicateServers = () => {
const servers = [...this.servers.keys()].map((key) => ({key, value: this.servers.get(key)!})); const servers = [...this.servers.keys()].map((key) => ({key, value: this.servers.get(key)!}));
const uniqueServers = new Set(); const uniqueServers = new Set();
servers.forEach((server) => { servers.forEach((server) => {
@ -301,38 +301,38 @@ export class ServerManager extends EventEmitter {
}); });
} }
private initServer = (team: ConfigServer, isPredefined: boolean) => { private initServer = (configServer: ConfigServer, isPredefined: boolean) => {
const server = new MattermostServer(team, isPredefined); const server = new MattermostServer(configServer, isPredefined);
this.servers.set(server.id, server); this.servers.set(server.id, server);
log.withPrefix(server.id).debug('initialized server'); log.withPrefix(server.id).debug('initialized server');
const tabOrder: string[] = []; const viewOrder: string[] = [];
team.tabs.sort((a, b) => a.order - b.order).forEach((tab) => { configServer.tabs.sort((a, b) => a.order - b.order).forEach((view) => {
const tabView = this.getTabView(server, tab.name, tab.isOpen); const mattermostView = this.getNewView(server, view.name, view.isOpen);
log.withPrefix(tabView.id).debug('initialized tab'); log.withPrefix(mattermostView.id).debug('initialized view');
this.tabs.set(tabView.id, tabView); this.views.set(mattermostView.id, mattermostView);
tabOrder.push(tabView.id); viewOrder.push(mattermostView.id);
}); });
this.tabOrder.set(server.id, tabOrder); this.viewOrder.set(server.id, viewOrder);
if (typeof team.lastActiveTab !== 'undefined') { if (typeof configServer.lastActiveTab !== 'undefined') {
this.lastActiveTab.set(server.id, tabOrder[team.lastActiveTab]); this.lastActiveView.set(server.id, viewOrder[configServer.lastActiveTab]);
} }
return server.id; return server.id;
} }
private getFirstOpenTabForServer = (serverId: string) => { private getFirstOpenViewForServer = (serverId: string) => {
const tabOrder = this.getOrderedTabsForServer(serverId); const viewOrder = this.getOrderedTabsForServer(serverId);
const openTabs = tabOrder.filter((tab) => tab.isOpen); const openViews = viewOrder.filter((view) => view.isOpen);
const firstTab = openTabs[0]; const firstView = openViews[0];
if (!firstTab) { if (!firstView) {
throw new Error(`No tabs open for server id ${serverId}`); 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); this.emit(SERVERS_UPDATE);
const localServers = [...this.servers.values()]. const localServers = [...this.servers.values()].
@ -343,18 +343,18 @@ export class ServerManager extends EventEmitter {
servers.push(this.toConfigServer(srv)); servers.push(this.toConfigServer(srv));
return servers; return servers;
}, [] as ConfigServer[]); }, [] as ConfigServer[]);
await Config.setServers(localServers, lastActiveTeam); await Config.setServers(localServers, lastActiveServer);
} }
private getLastActiveTab = (serverId: string) => { private getLastActiveView = (serverId: string) => {
let lastActiveTab: number | undefined; let lastActiveView: number | undefined;
if (this.lastActiveTab.has(serverId)) { if (this.lastActiveView.has(serverId)) {
const index = this.tabOrder.get(serverId)?.indexOf(this.lastActiveTab.get(serverId)!); const index = this.viewOrder.get(serverId)?.indexOf(this.lastActiveView.get(serverId)!);
if (typeof index !== 'undefined' && index >= 0) { if (typeof index !== 'undefined' && index >= 0) {
lastActiveTab = index; lastActiveView = index;
} }
} }
return lastActiveTab; return lastActiveView;
} }
private toConfigServer = (server: MattermostServer): ConfigServer => { private toConfigServer = (server: MattermostServer): ConfigServer => {
@ -362,30 +362,30 @@ export class ServerManager extends EventEmitter {
name: server.name, name: server.name,
url: `${server.url}`, url: `${server.url}`,
order: this.serverOrder.indexOf(server.id), order: this.serverOrder.indexOf(server.id),
lastActiveTab: this.getLastActiveTab(server.id), lastActiveTab: this.getLastActiveView(server.id),
tabs: this.tabOrder.get(server.id)?.reduce((tabs, tabId, index) => { tabs: this.viewOrder.get(server.id)?.reduce((views, viewId, index) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (!tab) { if (!view) {
return tabs; return views;
} }
tabs.push({ views.push({
name: tab?.type, name: view?.type,
order: index, order: index,
isOpen: tab.isOpen, isOpen: view.isOpen,
}); });
return tabs; return views;
}, [] as ConfigTab[]) ?? [], }, [] as ConfigView[]) ?? [],
}; };
} }
private getTabView = (srv: MattermostServer, tabName: string, isOpen?: boolean) => { private getNewView = (srv: MattermostServer, viewName: string, isOpen?: boolean) => {
switch (tabName) { switch (viewName) {
case TAB_MESSAGING: case TAB_MESSAGING:
return new MessagingTabView(srv, isOpen); return new MessagingView(srv, isOpen);
case TAB_FOCALBOARD: case TAB_FOCALBOARD:
return new FocalboardTabView(srv, isOpen); return new FocalboardView(srv, isOpen);
case TAB_PLAYBOOKS: case TAB_PLAYBOOKS:
return new PlaybooksTabView(srv, isOpen); return new PlaybooksView(srv, isOpen);
default: default:
throw new Error('Not implemeneted'); throw new Error('Not implemeneted');
} }
@ -407,7 +407,7 @@ export class ServerManager extends EventEmitter {
return false; return false;
} }
private openExtraTabs = (serverId: string) => { private openExtraViews = (serverId: string) => {
const server = this.servers.get(serverId); const server = this.servers.get(serverId);
const remoteInfo = this.remoteInfo.get(serverId); const remoteInfo = this.remoteInfo.get(serverId);
@ -420,21 +420,21 @@ export class ServerManager extends EventEmitter {
} }
let hasUpdates = false; let hasUpdates = false;
const tabOrder = this.tabOrder.get(serverId); const viewOrder = this.viewOrder.get(serverId);
if (tabOrder) { if (viewOrder) {
tabOrder.forEach((tabId) => { viewOrder.forEach((viewId) => {
const tab = this.tabs.get(tabId); const view = this.views.get(viewId);
if (tab) { if (view) {
if (tab.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof tab.isOpen === 'undefined') { if (view.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof view.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Playbooks'); log.withPrefix(view.id).verbose('opening Playbooks');
tab.isOpen = true; view.isOpen = true;
this.tabs.set(tabId, tab); this.views.set(viewId, view);
hasUpdates = true; hasUpdates = true;
} }
if (tab.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof tab.isOpen === 'undefined') { if (view.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof view.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Boards'); log.withPrefix(view.id).verbose('opening Boards');
tab.isOpen = true; view.isOpen = true;
this.tabs.set(tabId, tab); this.views.set(viewId, view);
hasUpdates = true; hasUpdates = true;
} }
} }
@ -457,7 +457,7 @@ export class ServerManager extends EventEmitter {
}; };
getViewLog = (viewId: string, ...additionalPrefixes: string[]) => { getViewLog = (viewId: string, ...additionalPrefixes: string[]) => {
const view = this.getTab(viewId); const view = this.getView(viewId);
if (!view) { if (!view) {
return new Logger(viewId); return new Logger(viewId);
} }

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -95,9 +95,9 @@ describe('main/app/app', () => {
const promise = Promise.resolve({}); const promise = Promise.resolve({});
const certificate = {}; const certificate = {};
const view = { const view = {
tab: { view: {
server: { server: {
name: 'test-team', name: 'test-server',
url: new URL(testURL), url: new URL(testURL),
}, },
}, },
@ -163,7 +163,7 @@ describe('main/app/app', () => {
expect(CertificateStore.save).toHaveBeenCalled(); 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}); dialog.showMessageBox.mockResolvedValue({response: 0});
await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback); await handleAppCertificateError(event, webContents, testURL, 'error-1', certificate, callback);
expect(callback).toHaveBeenCalledWith(true); expect(callback).toHaveBeenCalledWith(true);

View file

@ -96,8 +96,8 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
const errorID = `${parsedURL.origin}:${error}`; const errorID = `${parsedURL.origin}:${error}`;
const view = ViewManager.getViewByWebContentsId(webContents.id); const view = ViewManager.getViewByWebContentsId(webContents.id);
if (view?.tab.server) { if (view?.view.server) {
const serverURL = parseURL(view.tab.server.url); const serverURL = parseURL(view.view.server.url);
if (serverURL && serverURL.origin !== parsedURL.origin) { if (serverURL && serverURL.origin !== parsedURL.origin) {
log.warn(`Ignoring certificate for unmatched origin ${parsedURL.origin}, will not trust`); log.warn(`Ignoring certificate for unmatched origin ${parsedURL.origin}, will not trust`);
callback(false); 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; const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'win32', value: 'win32',
}); });
Config.registryConfigData = {}; Config.registryConfigData = {};
handleConfigUpdate({teams: []}); handleConfigUpdate({servers: []});
expect(handleMainWindowIsShown).toHaveBeenCalled(); expect(handleMainWindowIsShown).toHaveBeenCalled();
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {

View file

@ -6,7 +6,7 @@
import {initialize} from './initialize'; import {initialize} from './initialize';
// TODO: Singletons, we need DI :D // TODO: Singletons, we need DI :D
import('main/views/teamDropdownView'); import('main/views/serverDropdownView');
import('main/views/downloadsDropdownMenuView'); import('main/views/downloadsDropdownMenuView');
import('main/views/downloadsDropdownView'); 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 () => { it('should allow permission requests for supported types from trusted URLs', async () => {
ViewManager.getViewByWebContentsId.mockReturnValue({ ViewManager.getViewByWebContentsId.mockReturnValue({
tab: { view: {
server: { server: {
url: new URL('http://server-1.com'), url: new URL('http://server-1.com'),
}, },

View file

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

View file

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

View file

@ -3,7 +3,7 @@
import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; import {app, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
import {MattermostTeam} from 'types/config'; import {UniqueServer} from 'types/config';
import {MentionData} from 'types/notification'; import {MentionData} from 'types/notification';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@ -93,11 +93,11 @@ export function handleWelcomeScreenModal() {
if (!mainWindow) { if (!mainWindow) {
return; 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) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data); const newServer = ServerManager.addServer(data);
switchServer(newTeam.id, true); switchServer(newServer.id, true);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {

View file

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

View file

@ -3,7 +3,7 @@
import {IpcMainEvent, ipcMain} from 'electron'; 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 {UPDATE_SHORTCUT_MENU} from 'common/communication';
import {Logger} from 'common/log'; 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'); ServerManager.getServerLog(serverId, 'WindowManager').error('Cannot find server in config');
return; return;
} }
const nextTab = ServerManager.getLastActiveTabForServer(serverId); const nextView = ServerManager.getLastActiveTabForServer(serverId);
if (waitForViewToExist) { if (waitForViewToExist) {
const timeout = setInterval(() => { const timeout = setInterval(() => {
if (ViewManager.getView(nextTab.id)) { if (ViewManager.getView(nextView.id)) {
ViewManager.showById(nextTab.id); ViewManager.showById(nextView.id);
clearInterval(timeout); clearInterval(timeout);
} }
}, 100); }, 100);
} else { } else {
ViewManager.showById(nextTab.id); ViewManager.showById(nextView.id);
} }
ipcMain.emit(UPDATE_SHORTCUT_MENU); ipcMain.emit(UPDATE_SHORTCUT_MENU);
}; };
@ -49,11 +49,11 @@ export const handleNewServerModal = () => {
if (!mainWindow) { if (!mainWindow) {
return; 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) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const newTeam = ServerManager.addServer(data); const newServer = ServerManager.addServer(data);
switchServer(newTeam.id, true); switchServer(newServer.id, true);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {
@ -80,13 +80,13 @@ export const handleEditServerModal = (e: IpcMainEvent, id: string) => {
if (!server) { if (!server) {
return; return;
} }
const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>( const modalPromise = ModalManager.addModal<{currentServers: UniqueServer[]; server: UniqueServer}, Server>(
'editServer', 'editServer',
html, html,
preload, preload,
{ {
currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()), currentServers: ServerManager.getAllServers().map((server) => server.toUniqueServer()),
team: server.toMattermostTeam(), server: server.toUniqueServer(),
}, },
mainWindow); mainWindow);
if (modalPromise) { 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 ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager'; 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', () => { jest.mock('common/utils/url', () => {
const actualUrl = jest.requireActual('common/utils/url'); const actualUrl = jest.requireActual('common/utils/url');
return { return {
@ -116,35 +68,35 @@ describe('main/authManager', () => {
}); });
it('should popLoginModal when isTrustedURL', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://trustedurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popLoginModal when isCustomLoginURL', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://customloginurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popLoginModal when has permission', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://haspermissionurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).toBeCalled(); expect(authManager.popLoginModal).toBeCalled();
expect(authManager.popPermissionModal).not.toBeCalled(); expect(authManager.popPermissionModal).not.toBeCalled();
}); });
it('should popPermissionModal when anything else is true', () => { 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()); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, jest.fn());
expect(authManager.popLoginModal).not.toBeCalled(); expect(authManager.popLoginModal).not.toBeCalled();
expect(authManager.popPermissionModal).toBeCalled(); expect(authManager.popPermissionModal).toBeCalled();
}); });
it('should set login callback when logging in', () => { 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(); const callback = jest.fn();
authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback); authManager.handleAppLogin({preventDefault: jest.fn()}, {id: 1}, {url: 'http://someotherurl.com/'}, null, callback);
expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback); expect(authManager.loginCallbackMap.get('http://someotherurl.com/')).toEqual(callback);

View file

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

View file

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

View file

@ -554,7 +554,7 @@ export class DownloadsManager extends JsonFileManager<DownloadedItems> {
log.debug('doneEventController', {state}); log.debug('doneEventController', {state});
if (state === 'completed' && !this.open) { 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)); const bookmark = this.bookmarks.get(this.getFileId(item));

View file

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

View file

@ -6,9 +6,9 @@
import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron'; import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, clipboard} from 'electron';
import log from 'electron-log'; 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 {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 {Config} from 'common/config';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
@ -18,7 +18,7 @@ import downloadsManager from 'main/downloadsManager';
import Diagnostics from 'main/diagnostics'; import Diagnostics from 'main/diagnostics';
import ViewManager from 'main/views/viewManager'; import ViewManager from 'main/views/viewManager';
import SettingsWindow from 'main/windows/settingsWindow'; 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'; import {switchServer} from 'main/app/servers';
export function createTemplate(config: Config, updateManager: UpdateManager) { 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 = { const windowMenu = {
id: 'window', id: 'window',
label: localizeMessage('main.menus.app.window', '&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'), label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
click() { 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 = []; const items = [];
items.push({ items.push({
label: team.name, label: server.name,
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`,
click() { click() {
switchServer(team.id); switchServer(server.id);
}, },
}); });
if (ServerManager.getCurrentServer().id === team.id) { if (ServerManager.getCurrentServer().id === server.id) {
ServerManager.getOrderedTabsForServer(team.id).slice(0, 9).forEach((tab, i) => { ServerManager.getOrderedTabsForServer(server.id).slice(0, 9).forEach((view, i) => {
items.push({ 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}`, accelerator: `CmdOrCtrl+${i + 1}`,
click() { 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'), label: localizeMessage('main.menus.app.window.selectNextTab', 'Select Next Tab'),
accelerator: 'Ctrl+Tab', accelerator: 'Ctrl+Tab',
click() { click() {
selectNextTab(); selectNextView();
}, },
enabled: (teams.length > 1), enabled: (servers.length > 1),
}, { }, {
label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'), label: localizeMessage('main.menus.app.window.selectPreviousTab', 'Select Previous Tab'),
accelerator: 'Ctrl+Shift+Tab', accelerator: 'Ctrl+Shift+Tab',
click() { click() {
selectPreviousTab(); selectPreviousView();
}, },
enabled: (teams.length > 1), enabled: (servers.length > 1),
}, ...(isMac ? [separatorItem, { }, ...(isMac ? [separatorItem, {
role: 'front', role: 'front',
label: localizeMessage('main.menus.app.window.bringAllToFront', 'Bring All to 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'; import {switchServer} from 'main/app/servers';
export function createTemplate() { export function createTemplate() {
const teams = ServerManager.getOrderedServers(); const servers = ServerManager.getOrderedServers();
const template = [ const template = [
...teams.slice(0, 9).map((team) => { ...servers.slice(0, 9).map((server) => {
return { 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: () => { click: () => {
switchServer(team.id); switchServer(server.id);
}, },
}; };
}), { }), {

View file

@ -76,7 +76,7 @@ jest.mock('macos-notification-state', () => ({
jest.mock('../views/viewManager', () => ({ jest.mock('../views/viewManager', () => ({
getViewByWebContentsId: () => ({ getViewByWebContentsId: () => ({
id: 'server_id', id: 'server_id',
tab: { view: {
server: { server: {
name: 'server_name', 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( displayMention(
'click_test', 'click_test',
'mention_click_body', 'mention_click_body',

View file

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

View file

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

View file

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

View file

@ -6,13 +6,13 @@
import AppState from 'common/appState'; import AppState from 'common/appState';
import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communication'; import {LOAD_FAILED, TOGGLE_BACK_BUTTON, UPDATE_TARGET_URL} from 'common/communication';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from 'common/tabs/MessagingTabView'; import MessagingView from 'common/views/MessagingView';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import Utils from '../utils'; import Utils from '../utils';
import {MattermostView} from './MattermostView'; import {MattermostBrowserView} from './MattermostBrowserView';
jest.mock('electron', () => ({ jest.mock('electron', () => ({
app: { app: {
@ -59,12 +59,12 @@ jest.mock('../utils', () => ({
})); }));
const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'}); const server = new MattermostServer({name: 'server_name', url: 'http://server-1.com'});
const tabView = new MessagingTabView(server, true); const view = new MessagingView(server, true);
describe('main/views/MattermostView', () => { describe('main/views/MattermostBrowserView', () => {
describe('load', () => { describe('load', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -74,38 +74,38 @@ describe('main/views/MattermostView', () => {
it('should load provided URL when provided', async () => { it('should load provided URL when provided', async () => {
const promise = Promise.resolve(); const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('http://server-2.com'); mattermostView.load('http://server-2.com');
await promise; 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/'); expect(mattermostView.loadSuccess).toBeCalledWith('http://server-2.com/');
}); });
it('should load server URL when not provided', async () => { it('should load server URL when not provided', async () => {
const promise = Promise.resolve(); const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load(); mattermostView.load();
await promise; 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/'); expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
}); });
it('should load server URL when bad url provided', async () => { it('should load server URL when bad url provided', async () => {
const promise = Promise.resolve(); const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url'); mattermostView.load('a-bad<url');
await promise; 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/'); expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com/');
}); });
it('should call retry when failing to load', async () => { it('should call retry when failing to load', async () => {
const error = new Error('test'); const error = new Error('test');
const promise = Promise.reject(error); const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url'); mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error); 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); expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com/', error);
}); });
@ -113,23 +113,23 @@ describe('main/views/MattermostView', () => {
const error = new Error('test'); const error = new Error('test');
error.code = 'ERR_CERT_ERROR'; error.code = 'ERR_CERT_ERROR';
const promise = Promise.reject(error); const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.load('a-bad<url'); mattermostView.load('a-bad<url');
await expect(promise).rejects.toThrow(error); 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(mattermostView.loadRetry).not.toBeCalled();
}); });
}); });
describe('retry', () => { describe('retry', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
const retryInBackgroundFn = jest.fn(); const retryInBackgroundFn = jest.fn();
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
mattermostView.view.webContents.loadURL.mockImplementation(() => Promise.resolve()); mattermostView.browserView.webContents.loadURL.mockImplementation(() => Promise.resolve());
mattermostView.loadSuccess = jest.fn(); mattermostView.loadSuccess = jest.fn();
mattermostView.loadRetry = jest.fn(); mattermostView.loadRetry = jest.fn();
mattermostView.emit = jest.fn(); mattermostView.emit = jest.fn();
@ -143,16 +143,16 @@ describe('main/views/MattermostView', () => {
}); });
it('should do nothing when webcontents are destroyed', () => { it('should do nothing when webcontents are destroyed', () => {
const webContents = mattermostView.view.webContents; const webContents = mattermostView.browserView.webContents;
mattermostView.view.webContents = null; mattermostView.browserView.webContents = null;
mattermostView.retry('http://server-1.com')(); mattermostView.retry('http://server-1.com')();
expect(mattermostView.loadSuccess).not.toBeCalled(); expect(mattermostView.loadSuccess).not.toBeCalled();
mattermostView.view.webContents = webContents; mattermostView.browserView.webContents = webContents;
}); });
it('should call loadSuccess on successful load', async () => { it('should call loadSuccess on successful load', async () => {
const promise = Promise.resolve(); const promise = Promise.resolve();
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')(); mattermostView.retry('http://server-1.com')();
await promise; await promise;
expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com'); expect(mattermostView.loadSuccess).toBeCalledWith('http://server-1.com');
@ -162,10 +162,10 @@ describe('main/views/MattermostView', () => {
mattermostView.maxRetries = 10; mattermostView.maxRetries = 10;
const error = new Error('test'); const error = new Error('test');
const promise = Promise.reject(error); const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')(); mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error); 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); expect(mattermostView.loadRetry).toBeCalledWith('http://server-1.com', error);
}); });
@ -173,12 +173,12 @@ describe('main/views/MattermostView', () => {
mattermostView.maxRetries = 0; mattermostView.maxRetries = 0;
const error = new Error('test'); const error = new Error('test');
const promise = Promise.reject(error); const promise = Promise.reject(error);
mattermostView.view.webContents.loadURL.mockImplementation(() => promise); mattermostView.browserView.webContents.loadURL.mockImplementation(() => promise);
mattermostView.retry('http://server-1.com')(); mattermostView.retry('http://server-1.com')();
await expect(promise).rejects.toThrow(error); 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(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); expect(mattermostView.status).toBe(-1);
jest.runAllTimers(); jest.runAllTimers();
expect(retryInBackgroundFn).toBeCalled(); expect(retryInBackgroundFn).toBeCalled();
@ -187,7 +187,7 @@ describe('main/views/MattermostView', () => {
describe('goToOffset', () => { describe('goToOffset', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.reload = jest.fn(); mattermostView.reload = jest.fn();
afterEach(() => { afterEach(() => {
@ -196,18 +196,18 @@ describe('main/views/MattermostView', () => {
}); });
it('should only go to offset if it can', () => { it('should only go to offset if it can', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(false); mattermostView.browserView.webContents.canGoToOffset.mockReturnValue(false);
mattermostView.goToOffset(1); 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); mattermostView.goToOffset(1);
expect(mattermostView.view.webContents.goToOffset).toBeCalled(); expect(mattermostView.browserView.webContents.goToOffset).toBeCalled();
}); });
it('should call reload if an error occurs', () => { it('should call reload if an error occurs', () => {
mattermostView.view.webContents.canGoToOffset.mockReturnValue(true); mattermostView.browserView.webContents.canGoToOffset.mockReturnValue(true);
mattermostView.view.webContents.goToOffset.mockImplementation(() => { mattermostView.browserView.webContents.goToOffset.mockImplementation(() => {
throw new Error('hi'); throw new Error('hi');
}); });
mattermostView.goToOffset(1); mattermostView.goToOffset(1);
@ -217,8 +217,8 @@ describe('main/views/MattermostView', () => {
describe('onLogin', () => { describe('onLogin', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.view.webContents.getURL = jest.fn(); mattermostView.browserView.webContents.getURL = jest.fn();
mattermostView.reload = jest.fn(); mattermostView.reload = jest.fn();
afterEach(() => { afterEach(() => {
@ -227,19 +227,19 @@ describe('main/views/MattermostView', () => {
}); });
it('should reload view when URL is not on subpath of original server URL', () => { 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); mattermostView.onLogin(true);
expect(mattermostView.reload).toHaveBeenCalled(); expect(mattermostView.reload).toHaveBeenCalled();
}); });
it('should not reload if URLs are matching', () => { 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); mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled(); expect(mattermostView.reload).not.toHaveBeenCalled();
}); });
it('should not reload if URL is subpath of server URL', () => { 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); mattermostView.onLogin(true);
expect(mattermostView.reload).not.toHaveBeenCalled(); expect(mattermostView.reload).not.toHaveBeenCalled();
}); });
@ -247,7 +247,7 @@ describe('main/views/MattermostView', () => {
describe('loadSuccess', () => { describe('loadSuccess', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
@ -275,7 +275,7 @@ describe('main/views/MattermostView', () => {
describe('show', () => { describe('show', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()}; 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(() => { beforeEach(() => {
jest.useFakeTimers(); 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', () => { it('should add browser view to window and set bounds when request is true and view not currently visible', () => {
mattermostView.isVisible = false; mattermostView.isVisible = false;
mattermostView.show(); mattermostView.show();
expect(window.addBrowserView).toBeCalledWith(mattermostView.view); expect(window.addBrowserView).toBeCalledWith(mattermostView.browserView);
expect(mattermostView.setBounds).toBeCalled(); expect(mattermostView.setBounds).toBeCalled();
expect(mattermostView.isVisible).toBe(true); expect(mattermostView.isVisible).toBe(true);
}); });
@ -314,7 +314,7 @@ describe('main/views/MattermostView', () => {
describe('hide', () => { describe('hide', () => {
const window = {addBrowserView: jest.fn(), removeBrowserView: jest.fn(), on: jest.fn(), setTopBrowserView: jest.fn()}; 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(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -323,7 +323,7 @@ describe('main/views/MattermostView', () => {
it('should remove browser view', () => { it('should remove browser view', () => {
mattermostView.isVisible = true; mattermostView.isVisible = true;
mattermostView.hide(); mattermostView.hide();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view); expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
expect(mattermostView.isVisible).toBe(false); expect(mattermostView.isVisible).toBe(false);
}); });
@ -336,7 +336,7 @@ describe('main/views/MattermostView', () => {
describe('updateHistoryButton', () => { describe('updateHistoryButton', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -345,7 +345,7 @@ describe('main/views/MattermostView', () => {
it('should erase history and set isAtRoot when navigating to root URL', () => { it('should erase history and set isAtRoot when navigating to root URL', () => {
mattermostView.atRoot = false; mattermostView.atRoot = false;
mattermostView.updateHistoryButton(); mattermostView.updateHistoryButton();
expect(mattermostView.view.webContents.clearHistory).toHaveBeenCalled(); expect(mattermostView.browserView.webContents.clearHistory).toHaveBeenCalled();
expect(mattermostView.isAtRoot).toBe(true); expect(mattermostView.isAtRoot).toBe(true);
}); });
}); });
@ -362,22 +362,22 @@ describe('main/views/MattermostView', () => {
}); });
it('should remove browser view from window', () => { it('should remove browser view from window', () => {
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.view.webContents.destroy = jest.fn(); mattermostView.browserView.webContents.destroy = jest.fn();
mattermostView.destroy(); mattermostView.destroy();
expect(window.removeBrowserView).toBeCalledWith(mattermostView.view); expect(window.removeBrowserView).toBeCalledWith(mattermostView.browserView);
}); });
it('should clear mentions', () => { it('should clear mentions', () => {
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.view.webContents.destroy = jest.fn(); mattermostView.browserView.webContents.destroy = jest.fn();
mattermostView.destroy(); mattermostView.destroy();
expect(AppState.clear).toBeCalledWith(mattermostView.tab.id); expect(AppState.clear).toBeCalledWith(mattermostView.view.id);
}); });
it('should clear outstanding timeouts', () => { it('should clear outstanding timeouts', () => {
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
mattermostView.view.webContents.destroy = jest.fn(); mattermostView.browserView.webContents.destroy = jest.fn();
const spy = jest.spyOn(global, 'clearTimeout'); const spy = jest.spyOn(global, 'clearTimeout');
mattermostView.retryLoad = 999; mattermostView.retryLoad = 999;
mattermostView.removeLoading = 1000; mattermostView.removeLoading = 1000;
@ -388,7 +388,7 @@ describe('main/views/MattermostView', () => {
describe('handleInputEvents', () => { describe('handleInputEvents', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
it('should open three dot menu on pressing Alt', () => { it('should open three dot menu on pressing Alt', () => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -413,7 +413,7 @@ describe('main/views/MattermostView', () => {
describe('handleDidNavigate', () => { describe('handleDidNavigate', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -435,7 +435,7 @@ describe('main/views/MattermostView', () => {
describe('handleUpdateTarget', () => { describe('handleUpdateTarget', () => {
const window = {on: jest.fn()}; const window = {on: jest.fn()};
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(window); MainWindow.get.mockReturnValue(window);
@ -466,16 +466,16 @@ describe('main/views/MattermostView', () => {
}); });
describe('updateMentionsFromTitle', () => { describe('updateMentionsFromTitle', () => {
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostBrowserView(view, {}, {});
it('should parse mentions from title', () => { it('should parse mentions from title', () => {
mattermostView.updateMentionsFromTitle('(7) Mattermost'); 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', () => { it('should parse unreads from title', () => {
mattermostView.updateMentionsFromTitle('* Mattermost'); 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 ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {isInternalURL, parseURL} from 'common/utils/url'; 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'; import MainWindow from 'main/windows/mainWindow';
@ -42,12 +42,12 @@ enum Status {
const MENTIONS_GROUP = 2; const MENTIONS_GROUP = 2;
const titleParser = /(\((\d+)\) )?(\* )?/g; const titleParser = /(\((\d+)\) )?(\* )?/g;
export class MattermostView extends EventEmitter { export class MattermostBrowserView extends EventEmitter {
tab: TabView; view: MattermostView;
isVisible: boolean; isVisible: boolean;
private log: Logger; private log: Logger;
private view: BrowserView; private browserView: BrowserView;
private loggedIn: boolean; private loggedIn: boolean;
private atRoot: boolean; private atRoot: boolean;
private options: BrowserViewConstructorOptions; private options: BrowserViewConstructorOptions;
@ -58,9 +58,9 @@ export class MattermostView extends EventEmitter {
private maxRetries: number; private maxRetries: number;
private altPressStatus: boolean; private altPressStatus: boolean;
constructor(tab: TabView, options: BrowserViewConstructorOptions) { constructor(view: MattermostView, options: BrowserViewConstructorOptions) {
super(); super();
this.tab = tab; this.view = view;
const preload = getLocalPreload('preload.js'); const preload = getLocalPreload('preload.js');
this.options = Object.assign({}, options); this.options = Object.assign({}, options);
@ -75,24 +75,24 @@ export class MattermostView extends EventEmitter {
this.isVisible = false; this.isVisible = false;
this.loggedIn = false; this.loggedIn = false;
this.atRoot = true; this.atRoot = true;
this.view = new BrowserView(this.options); this.browserView = new BrowserView(this.options);
this.resetLoadingStatus(); this.resetLoadingStatus();
this.log = ServerManager.getViewLog(this.id, 'MattermostView'); this.log = ServerManager.getViewLog(this.id, 'MattermostBrowserView');
this.log.verbose('View created'); this.log.verbose('View created');
this.view.webContents.on('did-finish-load', this.handleDidFinishLoad); this.browserView.webContents.on('did-finish-load', this.handleDidFinishLoad);
this.view.webContents.on('page-title-updated', this.handleTitleUpdate); this.browserView.webContents.on('page-title-updated', this.handleTitleUpdate);
this.view.webContents.on('page-favicon-updated', this.handleFaviconUpdate); this.browserView.webContents.on('page-favicon-updated', this.handleFaviconUpdate);
this.view.webContents.on('update-target-url', this.handleUpdateTarget); this.browserView.webContents.on('update-target-url', this.handleUpdateTarget);
this.view.webContents.on('did-navigate', this.handleDidNavigate); this.browserView.webContents.on('did-navigate', this.handleDidNavigate);
if (process.platform !== 'darwin') { 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.maxRetries = MAX_SERVER_RETRIES;
this.altPressStatus = false; this.altPressStatus = false;
@ -105,7 +105,7 @@ export class MattermostView extends EventEmitter {
} }
get id() { get id() {
return this.tab.id; return this.view.id;
} }
get isAtRoot() { get isAtRoot() {
return this.atRoot; return this.atRoot;
@ -114,10 +114,10 @@ export class MattermostView extends EventEmitter {
return this.loggedIn; return this.loggedIn;
} }
get currentURL() { get currentURL() {
return parseURL(this.view.webContents.getURL()); return parseURL(this.browserView.webContents.getURL());
} }
get webContentsId() { get webContentsId() {
return this.view.webContents.id; return this.browserView.webContents.id;
} }
onLogin = (loggedIn: boolean) => { onLogin = (loggedIn: boolean) => {
@ -127,19 +127,19 @@ export class MattermostView extends EventEmitter {
this.loggedIn = loggedIn; 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 && if (loggedIn &&
this.currentURL?.toString() !== this.tab.url.toString() && this.currentURL?.toString() !== this.view.url.toString() &&
!this.currentURL?.toString().startsWith(this.tab.url.toString()) !this.currentURL?.toString().startsWith(this.view.url.toString())
) { ) {
this.reload(); this.reload();
} }
} }
goToOffset = (offset: number) => { goToOffset = (offset: number) => {
if (this.view.webContents.canGoToOffset(offset)) { if (this.browserView.webContents.canGoToOffset(offset)) {
try { try {
this.view.webContents.goToOffset(offset); this.browserView.webContents.goToOffset(offset);
this.updateHistoryButton(); this.updateHistoryButton();
} catch (error) { } catch (error) {
this.log.error(error); this.log.error(error);
@ -149,17 +149,17 @@ export class MattermostView extends EventEmitter {
} }
updateHistoryButton = () => { updateHistoryButton = () => {
if (this.currentURL?.toString() === this.tab.url.toString()) { if (this.currentURL?.toString() === this.view.url.toString()) {
this.view.webContents.clearHistory(); this.browserView.webContents.clearHistory();
this.atRoot = true; this.atRoot = true;
} else { } else {
this.atRoot = false; 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) => { load = (someURL?: URL | string) => {
if (!this.tab) { if (!this.browserView) {
return; return;
} }
@ -170,13 +170,13 @@ export class MattermostView extends EventEmitter {
loadURL = parsedURL.toString(); loadURL = parsedURL.toString();
} else { } else {
this.log.error('Cannot parse provided url, using current server url', someURL); this.log.error('Cannot parse provided url, using current server url', someURL);
loadURL = this.tab.url.toString(); loadURL = this.view.url.toString();
} }
} else { } else {
loadURL = this.tab.url.toString(); loadURL = this.view.url.toString();
} }
this.log.verbose(`Loading ${loadURL}`); 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) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) { if (err.code && err.code.startsWith('ERR_CERT')) {
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
@ -205,9 +205,9 @@ export class MattermostView extends EventEmitter {
return; return;
} }
this.isVisible = true; this.isVisible = true;
mainWindow.addBrowserView(this.view); mainWindow.addBrowserView(this.browserView);
mainWindow.setTopBrowserView(this.view); mainWindow.setTopBrowserView(this.browserView);
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL))); this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.view.url || '', this.currentURL)));
if (this.status === Status.READY) { if (this.status === Status.READY) {
this.focus(); this.focus();
} }
@ -216,7 +216,7 @@ export class MattermostView extends EventEmitter {
hide = () => { hide = () => {
if (this.isVisible) { if (this.isVisible) {
this.isVisible = false; this.isVisible = false;
MainWindow.get()?.removeBrowserView(this.view); MainWindow.get()?.removeBrowserView(this.browserView);
} }
} }
@ -226,27 +226,27 @@ export class MattermostView extends EventEmitter {
} }
getBounds = () => { getBounds = () => {
return this.view.getBounds(); return this.browserView.getBounds();
} }
openFind = () => { 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) => { setBounds = (boundaries: Electron.Rectangle) => {
this.view.setBounds(boundaries); this.browserView.setBounds(boundaries);
} }
destroy = () => { destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.webContentsId); WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
AppState.clear(this.id); AppState.clear(this.id);
MainWindow.get()?.removeBrowserView(this.view); MainWindow.get()?.removeBrowserView(this.browserView);
// workaround to eliminate zombie processes // workaround to eliminate zombie processes
// https://github.com/mattermost/desktop/pull/1519 // https://github.com/mattermost/desktop/pull/1519
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
this.view.webContents.destroy(); this.browserView.webContents.destroy();
this.isVisible = false; this.isVisible = false;
if (this.retryLoad) { if (this.retryLoad) {
@ -293,7 +293,7 @@ export class MattermostView extends EventEmitter {
} }
openDevTools = () => { 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[]) => { sendToRenderer = (channel: string, ...args: any[]) => {
this.view.webContents.send(channel, ...args); this.browserView.webContents.send(channel, ...args);
} }
isDestroyed = () => { isDestroyed = () => {
return this.view.webContents.isDestroyed(); return this.browserView.webContents.isDestroyed();
} }
focus = () => { focus = () => {
if (this.view.webContents) { if (this.browserView.webContents) {
this.view.webContents.focus(); this.browserView.webContents.focus();
} else { } else {
this.log.warn('trying to focus the browserview, but it doesn\'t yet have webcontents.'); 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 // if favicon is null, it will affect appState, but won't be memoized
private findUnreadState = (favicon: string | null) => { private findUnreadState = (favicon: string | null) => {
try { try {
this.view.webContents.send(IS_UNREAD, favicon, this.id); this.browserView.webContents.send(IS_UNREAD, favicon, this.id);
} catch (err: any) { } catch (err: any) {
this.log.error('There was an error trying to request the unread state', err); 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) => { private retry = (loadURL: string) => {
return () => { return () => {
// window was closed while retrying // window was closed while retrying
if (!this.view || !this.view.webContents) { if (!this.browserView || !this.browserView.webContents) {
return; 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) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (this.maxRetries-- > 0) { if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err); this.loadRetry(loadURL, err);
} else { } else {
MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); MainWindow.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.emit(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.status = Status.ERROR;
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL); this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
} }
@ -409,10 +409,10 @@ export class MattermostView extends EventEmitter {
private retryInBackground = (loadURL: string) => { private retryInBackground = (loadURL: string) => {
return () => { return () => {
// window was closed while retrying // window was closed while retrying
if (!this.view || !this.view.webContents) { if (!this.browserView || !this.browserView.webContents) {
return; 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(() => { loading.then(this.loadSuccess(loadURL)).catch(() => {
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL); this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
}); });
@ -431,7 +431,7 @@ export class MattermostView extends EventEmitter {
MainWindow.sendToRenderer(LOAD_SUCCESS, this.id); MainWindow.sendToRenderer(LOAD_SUCCESS, this.id);
this.maxRetries = MAX_SERVER_RETRIES; this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) { if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle()); this.updateMentionsFromTitle(this.browserView.webContents.getTitle());
this.findUnreadState(null); this.findUnreadState(null);
} }
this.status = Status.WAITING_MM; this.status = Status.WAITING_MM;
@ -439,7 +439,7 @@ export class MattermostView extends EventEmitter {
this.emit(LOAD_SUCCESS, this.id, loadURL); this.emit(LOAD_SUCCESS, this.id, loadURL);
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
if (mainWindow && this.currentURL) { 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 // wait for screen to truly finish loading before sending the message down
const timeout = setInterval(() => { const timeout = setInterval(() => {
if (!this.view.webContents) { if (!this.browserView.webContents) {
return; return;
} }
if (!this.view.webContents.isLoading()) { if (!this.browserView.webContents.isLoading()) {
try { 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); clearTimeout(timeout);
} catch (e) { } catch (e) {
this.log.error('failed to send view options to view'); this.log.error('failed to send view options to view');
@ -480,7 +480,7 @@ export class MattermostView extends EventEmitter {
return; return;
} }
if (shouldHaveBackBar(this.tab.url || '', parsedURL)) { if (shouldHaveBackBar(this.view.url || '', parsedURL)) {
this.setBounds(getWindowBoundaries(mainWindow, true)); this.setBounds(getWindowBoundaries(mainWindow, true));
MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true); MainWindow.sendToRenderer(TOGGLE_BACK_BUTTON, true);
this.log.debug('show back button'); this.log.debug('show back button');
@ -494,7 +494,7 @@ export class MattermostView extends EventEmitter {
private handleUpdateTarget = (e: Event, url: string) => { private handleUpdateTarget = (e: Event, url: string) => {
this.log.silly('handleUpdateTarget', url); this.log.silly('handleUpdateTarget', url);
const parsedURL = parseURL(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); this.emit(UPDATE_TARGET_URL);
} else { } else {
this.emit(UPDATE_TARGET_URL, url); this.emit(UPDATE_TARGET_URL, url);
@ -502,7 +502,7 @@ export class MattermostView extends EventEmitter {
} }
private handleServerWasModified = (serverIds: string) => { private handleServerWasModified = (serverIds: string) => {
if (serverIds.includes(this.tab.server.id)) { if (serverIds.includes(this.view.server.id)) {
this.reload(); 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 MainWindow from 'main/windows/mainWindow';
import {TeamDropdownView} from './teamDropdownView'; import {ServerDropdownView} from './serverDropdownView';
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: (file) => file, getLocalPreload: (file) => file,
@ -38,36 +38,36 @@ jest.mock('common/servers/serverManager', () => ({
getOrderedServers: jest.fn().mockReturnValue([]), getOrderedServers: jest.fn().mockReturnValue([]),
})); }));
describe('main/views/teamDropdownView', () => { describe('main/views/serverDropdownView', () => {
describe('getBounds', () => { describe('getBounds', () => {
beforeEach(() => { beforeEach(() => {
MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0}); MainWindow.getBounds.mockReturnValue({width: 500, height: 400, x: 0, y: 0});
}); });
const teamDropdownView = new TeamDropdownView(); const serverDropdownView = new ServerDropdownView();
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
it('should account for three dot menu, tab bar and shadow', () => { 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 { } else {
it('should account for three dot menu, tab bar and shadow', () => { 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', () => { it('should change the view bounds based on open/closed state', () => {
const teamDropdownView = new TeamDropdownView(); const serverDropdownView = new ServerDropdownView();
teamDropdownView.view = { serverDropdownView.view = {
setBounds: jest.fn(), setBounds: jest.fn(),
webContents: { webContents: {
focus: jest.fn(), focus: jest.fn(),
}, },
}; };
teamDropdownView.bounds = {width: 400, height: 300}; serverDropdownView.bounds = {width: 400, height: 300};
teamDropdownView.handleOpen(); serverDropdownView.handleOpen();
expect(teamDropdownView.view.setBounds).toBeCalledWith(teamDropdownView.bounds); expect(serverDropdownView.view.setBounds).toBeCalledWith(serverDropdownView.bounds);
teamDropdownView.handleClose(); serverDropdownView.handleClose();
expect(teamDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)}); 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 {BrowserView, ipcMain, IpcMainEvent} from 'electron';
import {MattermostTeam} from 'types/config'; import {UniqueServer} from 'types/config';
import AppState from 'common/appState'; import AppState from 'common/appState';
import { import {
CLOSE_TEAMS_DROPDOWN, CLOSE_SERVERS_DROPDOWN,
EMIT_CONFIGURATION, EMIT_CONFIGURATION,
OPEN_TEAMS_DROPDOWN, OPEN_SERVERS_DROPDOWN,
UPDATE_TEAMS_DROPDOWN, UPDATE_SERVERS_DROPDOWN,
UPDATE_APPSTATE, UPDATE_APPSTATE,
REQUEST_TEAMS_DROPDOWN_INFO, REQUEST_SERVERS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SERVERS_UPDATE, SERVERS_UPDATE,
MAIN_WINDOW_CREATED, MAIN_WINDOW_CREATED,
@ -27,12 +27,12 @@ import {getLocalPreload, getLocalURLString} from 'main/utils';
import MainWindow from '../windows/mainWindow'; 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 view?: BrowserView;
private teams: MattermostTeam[]; private servers: UniqueServer[];
private hasGPOTeams: boolean; private hasGPOServers: boolean;
private isOpen: boolean; private isOpen: boolean;
private bounds: Electron.Rectangle; private bounds: Electron.Rectangle;
@ -43,8 +43,8 @@ export class TeamDropdownView {
private windowBounds?: Electron.Rectangle; private windowBounds?: Electron.Rectangle;
constructor() { constructor() {
this.teams = []; this.servers = [];
this.hasGPOTeams = false; this.hasGPOServers = false;
this.isOpen = false; this.isOpen = false;
this.bounds = this.getBounds(0, 0); this.bounds = this.getBounds(0, 0);
@ -55,12 +55,12 @@ export class TeamDropdownView {
MainWindow.on(MAIN_WINDOW_CREATED, this.init); MainWindow.on(MAIN_WINDOW_CREATED, this.init);
MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds); MainWindow.on(MAIN_WINDOW_RESIZED, this.updateWindowBounds);
ipcMain.on(OPEN_TEAMS_DROPDOWN, this.handleOpen); ipcMain.on(OPEN_SERVERS_DROPDOWN, this.handleOpen);
ipcMain.on(CLOSE_TEAMS_DROPDOWN, this.handleClose); ipcMain.on(CLOSE_SERVERS_DROPDOWN, this.handleClose);
ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize);
ipcMain.on(EMIT_CONFIGURATION, this.updateDropdown); 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); AppState.on(UPDATE_APPSTATE, this.updateMentions);
ServerManager.on(SERVERS_UPDATE, this.updateServers); ServerManager.on(SERVERS_UPDATE, this.updateServers);
@ -84,7 +84,7 @@ export class TeamDropdownView {
}}); }});
this.view.webContents.loadURL(getLocalURLString('dropdown.html')); this.view.webContents.loadURL(getLocalURLString('dropdown.html'));
this.setOrderedTeams(); this.setOrderedServers();
this.windowBounds = MainWindow.getBounds(); this.windowBounds = MainWindow.getBounds();
this.updateDropdown(); this.updateDropdown();
MainWindow.get()?.addBrowserView(this.view); MainWindow.get()?.addBrowserView(this.view);
@ -94,13 +94,13 @@ export class TeamDropdownView {
log.silly('updateDropdown'); log.silly('updateDropdown');
this.view?.webContents.send( this.view?.webContents.send(
UPDATE_TEAMS_DROPDOWN, UPDATE_SERVERS_DROPDOWN,
this.teams, this.servers,
Config.darkMode, Config.darkMode,
this.windowBounds, this.windowBounds,
ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined, ServerManager.hasServers() ? ServerManager.getCurrentServer().id : undefined,
Config.enableServerManagement, Config.enableServerManagement,
this.hasGPOTeams, this.hasGPOServers,
this.expired, this.expired,
this.mentions, this.mentions,
this.unreads, this.unreads,
@ -108,7 +108,7 @@ export class TeamDropdownView {
} }
private updateServers = () => { private updateServers = () => {
this.setOrderedTeams(); this.setOrderedServers();
this.updateDropdown(); this.updateDropdown();
} }
@ -137,7 +137,7 @@ export class TeamDropdownView {
this.view.setBounds(this.bounds); this.view.setBounds(this.bounds);
MainWindow.get()?.setTopBrowserView(this.view); MainWindow.get()?.setTopBrowserView(this.view);
this.view.webContents.focus(); this.view.webContents.focus();
MainWindow.sendToRenderer(OPEN_TEAMS_DROPDOWN); MainWindow.sendToRenderer(OPEN_SERVERS_DROPDOWN);
this.isOpen = true; this.isOpen = true;
} }
@ -145,7 +145,7 @@ export class TeamDropdownView {
log.debug('handleClose'); log.debug('handleClose');
this.view?.setBounds(this.getBounds(0, 0)); this.view?.setBounds(this.getBounds(0, 0));
MainWindow.sendToRenderer(CLOSE_TEAMS_DROPDOWN); MainWindow.sendToRenderer(CLOSE_SERVERS_DROPDOWN);
this.isOpen = false; 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) => { private reduceNotifications = <T>(inputMap: Map<string, T>, items: Map<string, T>, modifier: (base?: T, value?: T) => T) => {
inputMap.clear(); inputMap.clear();
return [...items.keys()].reduce((map, key) => { return [...items.keys()].reduce((map, key) => {
const view = ServerManager.getTab(key); const view = ServerManager.getView(key);
if (!view) { if (!view) {
return map; return map;
} }
@ -183,11 +183,11 @@ export class TeamDropdownView {
}, inputMap); }, inputMap);
} }
private setOrderedTeams = () => { private setOrderedServers = () => {
this.teams = ServerManager.getOrderedServers().map((team) => team.toMattermostTeam()); this.servers = ServerManager.getOrderedServers().map((server) => server.toUniqueServer());
this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); this.hasGPOServers = this.servers.some((srv) => srv.isPredefined);
} }
} }
const teamDropdownView = new TeamDropdownView(); const serverDropdownView = new ServerDropdownView();
export default teamDropdownView; export default serverDropdownView;

View file

@ -7,13 +7,13 @@
import {dialog} from 'electron'; import {dialog} from 'electron';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication'; 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 ServerManager from 'common/servers/serverManager';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {MattermostView} from './MattermostView'; import {MattermostBrowserView} from './MattermostBrowserView';
import {ViewManager} from './viewManager'; import {ViewManager} from './viewManager';
import LoadingScreen from './loadingScreen'; import LoadingScreen from './loadingScreen';
@ -30,12 +30,9 @@ jest.mock('electron', () => ({
handle: jest.fn(), handle: jest.fn(),
}, },
})); }));
jest.mock('common/config', () => ({ jest.mock('common/views/View', () => ({
teams: [], getViewName: jest.fn((a, b) => `${a}-${b}`),
})); TAB_MESSAGING: 'view',
jest.mock('common/tabs/TabView', () => ({
getTabViewName: jest.fn((a, b) => `${a}-${b}`),
TAB_MESSAGING: 'tab',
})); }));
jest.mock('common/servers/MattermostServer', () => ({ jest.mock('common/servers/MattermostServer', () => ({
@ -79,7 +76,7 @@ jest.mock('common/servers/serverManager', () => ({
getLastActiveServer: jest.fn(), getLastActiveServer: jest.fn(),
getLastActiveTabForServer: jest.fn(), getLastActiveTabForServer: jest.fn(),
updateLastActive: jest.fn(), updateLastActive: jest.fn(),
lookupTabByURL: jest.fn(), lookupViewByURL: jest.fn(),
getRemoteInfo: jest.fn(), getRemoteInfo: jest.fn(),
on: jest.fn(), on: jest.fn(),
getServerLog: () => ({ getServerLog: () => ({
@ -100,8 +97,8 @@ jest.mock('common/servers/serverManager', () => ({
}), }),
})); }));
jest.mock('./MattermostView', () => ({ jest.mock('./MattermostBrowserView', () => ({
MattermostView: jest.fn(), MattermostBrowserView: jest.fn(),
})); }));
jest.mock('./modalManager', () => ({ jest.mock('./modalManager', () => ({
@ -121,12 +118,12 @@ describe('main/views/viewManager', () => {
beforeEach(() => { beforeEach(() => {
viewManager.showById = jest.fn(); viewManager.showById = jest.fn();
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
MattermostView.mockImplementation((tab) => ({ MattermostBrowserView.mockImplementation((view) => ({
on: jest.fn(), on: jest.fn(),
load: loadFn, load: loadFn,
once: onceFn, once: onceFn,
destroy: destroyFn, destroy: destroyFn,
id: tab.id, id: view.id,
})); }));
}); });
@ -136,21 +133,21 @@ describe('main/views/viewManager', () => {
viewManager.views = new Map(); viewManager.views = new Map();
}); });
it('should add closed tabs to closedViews', () => { it('should add closed views to closedViews', () => {
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: false}); viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: false});
expect(viewManager.closedViews.has('tab1')).toBe(true); expect(viewManager.closedViews.has('view1')).toBe(true);
}); });
it('should remove from remove from closedViews when the tab is open', () => { it('should remove from remove from closedViews when the view is open', () => {
viewManager.closedViews.set('tab1', {}); viewManager.closedViews.set('view1', {});
expect(viewManager.closedViews.has('tab1')).toBe(true); expect(viewManager.closedViews.has('view1')).toBe(true);
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}); viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: true});
expect(viewManager.closedViews.has('tab1')).toBe(false); expect(viewManager.closedViews.has('view1')).toBe(false);
}); });
it('should add view to views map and add listeners', () => { it('should add view to views map and add listeners', () => {
viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}, 'http://server-1.com/subpath'); viewManager.loadView({id: 'server1'}, {id: 'view1', isOpen: true}, 'http://server-1.com/subpath');
expect(viewManager.views.has('tab1')).toBe(true); expect(viewManager.views.has('view1')).toBe(true);
expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView); expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView);
expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath'); expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath');
}); });
@ -173,14 +170,14 @@ describe('main/views/viewManager', () => {
const onceFn = jest.fn(); const onceFn = jest.fn();
const loadFn = jest.fn(); const loadFn = jest.fn();
const destroyFn = jest.fn(); const destroyFn = jest.fn();
MattermostView.mockImplementation((tab) => ({ MattermostBrowserView.mockImplementation((view) => ({
on: jest.fn(), on: jest.fn(),
load: loadFn, load: loadFn,
once: onceFn, once: onceFn,
destroy: destroyFn, destroy: destroyFn,
id: tab.id, id: view.id,
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
tab, view,
})); }));
}); });
@ -193,45 +190,45 @@ describe('main/views/viewManager', () => {
it('should recycle existing views', () => { it('should recycle existing views', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView'); const makeSpy = jest.spyOn(viewManager, 'makeView');
const view = new MattermostView({ const view = new MattermostBrowserView({
id: 'tab1', id: 'view1',
server: { server: {
id: 'server1', id: 'server1',
}, },
}); });
viewManager.views.set('tab1', view); viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server1', id: 'server1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
isOpen: true, isOpen: true,
}, },
]); ]);
viewManager.handleReloadConfiguration(); viewManager.handleReloadConfiguration();
expect(viewManager.views.get('tab1')).toBe(view); expect(viewManager.views.get('view1')).toBe(view);
expect(makeSpy).not.toHaveBeenCalled(); expect(makeSpy).not.toHaveBeenCalled();
makeSpy.mockRestore(); makeSpy.mockRestore();
}); });
it('should close tabs that arent open', () => { it('should close views that arent open', () => {
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server1', id: 'server1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
isOpen: false, isOpen: false,
}, },
]); ]);
viewManager.handleReloadConfiguration(); 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'); const makeSpy = jest.spyOn(viewManager, 'makeView');
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server1', id: 'server1',
@ -240,10 +237,10 @@ describe('main/views/viewManager', () => {
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
name: 'tab1', name: 'view1',
isOpen: true, isOpen: true,
url: new URL('http://server1.com/tab'), url: new URL('http://server1.com/view'),
}, },
]); ]);
viewManager.handleReloadConfiguration(); viewManager.handleReloadConfiguration();
@ -254,10 +251,10 @@ describe('main/views/viewManager', () => {
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
{ {
id: 'tab1', id: 'view1',
name: 'tab1', name: 'view1',
isOpen: true, isOpen: true,
url: new URL('http://server1.com/tab'), url: new URL('http://server1.com/view'),
}, },
); );
makeSpy.mockRestore(); makeSpy.mockRestore();
@ -265,27 +262,27 @@ describe('main/views/viewManager', () => {
it('should set focus to current view on reload', () => { it('should set focus to current view on reload', () => {
const view = { const view = {
id: 'tab1', id: 'view1',
tab: { view: {
server: { server: {
id: 'server-1', id: 'server-1',
}, },
id: 'tab1', id: 'view1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
focus: jest.fn(), focus: jest.fn(),
}; };
viewManager.currentView = 'tab1'; viewManager.currentView = 'view1';
viewManager.views.set('tab1', view); viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server1', id: 'server1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
isOpen: true, isOpen: true,
}, },
]); ]);
@ -295,23 +292,23 @@ describe('main/views/viewManager', () => {
it('should show initial if currentView has been removed', () => { it('should show initial if currentView has been removed', () => {
const view = { const view = {
id: 'tab1', id: 'view1',
tab: { view: {
id: 'tab1', id: 'view1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
}; };
viewManager.currentView = 'tab1'; viewManager.currentView = 'view1';
viewManager.views.set('tab1', view); viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server2', id: 'server2',
url: new URL('http://server2.com'), url: new URL('http://server2.com'),
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
isOpen: false, isOpen: false,
}, },
]); ]);
@ -321,21 +318,21 @@ describe('main/views/viewManager', () => {
it('should remove unused views', () => { it('should remove unused views', () => {
const view = { const view = {
name: 'tab1', name: 'view1',
tab: { view: {
name: 'tab1', name: 'view1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
}; };
viewManager.views.set('tab1', view); viewManager.views.set('view1', view);
ServerManager.getAllServers.mockReturnValue([{ ServerManager.getAllServers.mockReturnValue([{
id: 'server2', id: 'server2',
url: new URL('http://server2.com'), url: new URL('http://server2.com'),
}]); }]);
ServerManager.getOrderedTabsForServer.mockReturnValue([ ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
id: 'tab1', id: 'view1',
isOpen: false, isOpen: false,
}, },
]); ]);
@ -360,11 +357,11 @@ describe('main/views/viewManager', () => {
jest.resetAllMocks(); 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.getLastActiveServer.mockReturnValue({id: 'server-1'});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-1'}); ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'view-1'});
viewManager.showInitial(); viewManager.showInitial();
expect(viewManager.showById).toHaveBeenCalledWith('tab-1'); expect(viewManager.showById).toHaveBeenCalledWith('view-1');
}); });
it('should open new server modal when no servers exist', () => { it('should open new server modal when no servers exist', () => {
@ -385,7 +382,7 @@ describe('main/views/viewManager', () => {
order: 0, order: 0,
tabs: [ tabs: [
{ {
name: 'tab-messaging', name: 'view-messaging',
order: 0, order: 0,
isOpen: true, isOpen: true,
}, },
@ -403,9 +400,9 @@ describe('main/views/viewManager', () => {
}, },
]; ];
const view1 = { const view1 = {
id: 'server-1_tab-messaging', id: 'server-1_view-messaging',
isLoggedIn: true, isLoggedIn: true,
tab: { view: {
type: TAB_MESSAGING, type: TAB_MESSAGING,
server: { server: {
url: 'http://server-1.com', url: 'http://server-1.com',
@ -416,21 +413,21 @@ describe('main/views/viewManager', () => {
const view2 = { const view2 = {
...view1, ...view1,
id: 'server-1_other_type_1', id: 'server-1_other_type_1',
tab: { view: {
...view1.tab, ...view1.view,
type: 'other_type_1', type: 'other_type_1',
}, },
}; };
const view3 = { const view3 = {
...view1, ...view1,
id: 'server-1_other_type_2', id: 'server-1_other_type_2',
tab: { view: {
...view1.tab, ...view1.view,
type: 'other_type_2', type: 'other_type_2',
}, },
}; };
const views = new Map([ const views = new Map([
['server-1_tab-messaging', view1], ['server-1_view-messaging', view1],
['server-1_other_type_1', view2], ['server-1_other_type_1', view2],
]); ]);
const closedViews = new Map([ const closedViews = new Map([
@ -438,7 +435,7 @@ describe('main/views/viewManager', () => {
]); ]);
viewManager.getView = (viewId) => views.get(viewId); viewManager.getView = (viewId) => views.get(viewId);
viewManager.isViewClosed = (viewId) => closedViews.has(viewId); viewManager.isViewClosed = (viewId) => closedViews.has(viewId);
viewManager.openClosedTab = jest.fn(); viewManager.openClosedView = jest.fn();
beforeEach(() => { beforeEach(() => {
ServerManager.getAllServers.mockReturnValue(servers); ServerManager.getAllServers.mockReturnValue(servers);
@ -451,24 +448,24 @@ describe('main/views/viewManager', () => {
}); });
it('should open closed view if pushing to it', () => { it('should open closed view if pushing to it', () => {
viewManager.openClosedTab.mockImplementation((name) => { viewManager.openClosedView.mockImplementation((name) => {
const view = closedViews.get(name); const view = closedViews.get(name);
closedViews.delete(name); closedViews.delete(name);
views.set(name, view); views.set(name, view);
}); });
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_other_type_2'}); ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_2'});
viewManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_2/subpath'); viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_2/subpath');
expect(viewManager.openClosedTab).toBeCalledWith('server-1_other_type_2', 'http://server-1.com/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', () => { it('should open redirect view if different from current view', () => {
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_other_type_1'}); ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_other_type_1'});
viewManager.handleBrowserHistoryPush(null, 'server-1_tab-messaging', '/other_type_1/subpath'); viewManager.handleBrowserHistoryPush(null, 'server-1_view-messaging', '/other_type_1/subpath');
expect(viewManager.showById).toBeCalledWith('server-1_other_type_1'); expect(viewManager.showById).toBeCalledWith('server-1_other_type_1');
}); });
it('should ignore redirects to "/" to Messages from other tabs', () => { it('should ignore redirects to "/" to Messages from other views', () => {
ServerManager.lookupTabByURL.mockReturnValue({id: 'server-1_tab-messaging'}); ServerManager.lookupViewByURL.mockReturnValue({id: 'server-1_view-messaging'});
viewManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/'); viewManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/');
expect(view1.sendToRenderer).not.toBeCalled(); expect(view1.sendToRenderer).not.toBeCalled();
}); });
@ -487,11 +484,11 @@ describe('main/views/viewManager', () => {
send: jest.fn(), send: jest.fn(),
}, },
}, },
tab: { view: {
server: { server: {
name: 'server-1', name: 'server-1',
}, },
type: 'tab-1', type: 'view-1',
}, },
}; };
@ -510,9 +507,9 @@ describe('main/views/viewManager', () => {
...baseView, ...baseView,
isVisible: true, 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(viewManager.currentView).toBeUndefined();
expect(view.isReady).not.toBeCalled(); expect(view.isReady).not.toBeCalled();
expect(view.show).not.toBeCalled(); expect(view.show).not.toBeCalled();
@ -584,7 +581,7 @@ describe('main/views/viewManager', () => {
}; };
beforeEach(() => { beforeEach(() => {
viewManager.openClosedTab = jest.fn(); viewManager.openClosedView = jest.fn();
}); });
afterEach(() => { afterEach(() => {
@ -594,7 +591,7 @@ describe('main/views/viewManager', () => {
}); });
it('should load URL into matching view', () => { 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}; const view = {...baseView};
viewManager.views.set('view1', view); viewManager.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
@ -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', () => { 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'}); ServerManager.getRemoteInfo.mockReturnValue({serverVersion: '6.0.0'});
const view = { const view = {
...baseView, ...baseView,
tab: { view: {
server: { server: {
url: new URL('http://server-1.com'), url: new URL('http://server-1.com'),
}, },
@ -619,7 +616,7 @@ describe('main/views/viewManager', () => {
}); });
it('should throw error if view is missing', () => { 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}; const view = {...baseView};
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(view.load).not.toHaveBeenCalled(); expect(view.load).not.toHaveBeenCalled();
@ -632,11 +629,11 @@ describe('main/views/viewManager', () => {
expect(dialog.showErrorBox).toHaveBeenCalled(); expect(dialog.showErrorBox).toHaveBeenCalled();
}); });
it('should reopen closed tab if called upon', () => { it('should reopen closed view if called upon', () => {
ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')})); ServerManager.lookupViewByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
viewManager.closedViews.set('view1', {}); viewManager.closedViews.set('view1', {});
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); 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, LOAD_FAILED,
LOADSCREEN_END, LOADSCREEN_END,
SET_ACTIVE_VIEW, SET_ACTIVE_VIEW,
OPEN_TAB, OPEN_VIEW,
BROWSER_HISTORY_PUSH, BROWSER_HISTORY_PUSH,
UPDATE_URL_VIEW_WIDTH, UPDATE_URL_VIEW_WIDTH,
SERVERS_UPDATE, SERVERS_UPDATE,
@ -33,7 +33,7 @@ import {Logger} from 'common/log';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import ServerManager from 'common/servers/serverManager'; 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 {parseURL} from 'common/utils/url';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
@ -41,7 +41,7 @@ import MainWindow from 'main/windows/mainWindow';
import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils'; import {getLocalURLString, getLocalPreload, getAdjustedWindowBoundaries, shouldHaveBackBar} from '../utils';
import {MattermostView} from './MattermostView'; import {MattermostBrowserView} from './MattermostBrowserView';
import modalManager from './modalManager'; import modalManager from './modalManager';
import LoadingScreen from './loadingScreen'; import LoadingScreen from './loadingScreen';
@ -50,14 +50,14 @@ const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 20; const URL_VIEW_HEIGHT = 20;
export class ViewManager { export class ViewManager {
private closedViews: Map<string, {srv: MattermostServer; tab: TabView}>; private closedViews: Map<string, {srv: MattermostServer; view: MattermostView}>;
private views: Map<string, MattermostView>; private views: Map<string, MattermostBrowserView>;
private currentView?: string; private currentView?: string;
private urlViewCancel?: () => void; private urlViewCancel?: () => void;
constructor() { 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(); this.closedViews = new Map();
MainWindow.on(MAIN_WINDOW_CREATED, this.init); MainWindow.on(MAIN_WINDOW_CREATED, this.init);
@ -102,23 +102,23 @@ export class ViewManager {
return this.closedViews.has(viewId); return this.closedViews.has(viewId);
} }
showById = (tabId: string) => { showById = (viewId: string) => {
this.getViewLogger(tabId).debug('showById', tabId); this.getViewLogger(viewId).debug('showById', viewId);
const newView = this.views.get(tabId); const newView = this.views.get(viewId);
if (newView) { if (newView) {
if (newView.isVisible) { if (newView.isVisible) {
return; return;
} }
let hidePrevious; let hidePrevious;
if (this.currentView && this.currentView !== tabId) { if (this.currentView && this.currentView !== viewId) {
const previous = this.getCurrentView(); const previous = this.getCurrentView();
if (previous) { if (previous) {
hidePrevious = () => previous.hide(); hidePrevious = () => previous.hide();
} }
} }
this.currentView = tabId; this.currentView = viewId;
if (!newView.isErrored()) { if (!newView.isErrored()) {
newView.show(); newView.show();
if (newView.needsLoadingScreen()) { if (newView.needsLoadingScreen()) {
@ -126,10 +126,10 @@ export class ViewManager {
} }
} }
hidePrevious?.(); hidePrevious?.();
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.id, newView.tab.id); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.view.server.id, newView.view.id);
ServerManager.updateLastActive(newView.tab.id); ServerManager.updateLastActive(newView.view.id);
} else { } 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(); modalManager.showModal();
} }
@ -175,28 +175,28 @@ export class ViewManager {
handleDeepLink = (url: string | URL) => { handleDeepLink = (url: string | URL) => {
if (url) { if (url) {
const parsedURL = parseURL(url)!; const parsedURL = parseURL(url)!;
const tabView = ServerManager.lookupTabByURL(parsedURL, true); const view = ServerManager.lookupViewByURL(parsedURL, true);
if (tabView) { if (view) {
const urlWithSchema = `${tabView.url.origin}${parsedURL.pathname}${parsedURL.search}`; const urlWithSchema = `${view.url.origin}${parsedURL.pathname}${parsedURL.search}`;
if (this.closedViews.has(tabView.id)) { if (this.closedViews.has(view.id)) {
this.openClosedTab(tabView.id, urlWithSchema); this.openClosedView(view.id, urlWithSchema);
} else { } else {
const view = this.views.get(tabView.id); const browserView = this.views.get(view.id);
if (!view) { if (!browserView) {
log.error(`Couldn't find a view matching the id ${tabView.id}`); log.error(`Couldn't find a view matching the id ${view.id}`);
return; return;
} }
if (view.isReady() && ServerManager.getRemoteInfo(view.tab.server.id)?.serverVersion && Utils.isVersionGreaterThanOrEqualTo(ServerManager.getRemoteInfo(view.tab.server.id)?.serverVersion ?? '', '6.0.0')) { 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(view.tab.server.url.toString(), '')}`; const pathName = `/${urlWithSchema.replace(browserView.view.server.url.toString(), '')}`;
view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName); browserView.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(view.id); this.deeplinkSuccess(browserView.id);
} else { } else {
// attempting to change parsedURL protocol results in it not being modified. // attempting to change parsedURL protocol results in it not being modified.
view.resetLoadingStatus(); browserView.resetLoadingStatus();
view.load(urlWithSchema); browserView.load(urlWithSchema);
view.once(LOAD_SUCCESS, this.deeplinkSuccess); browserView.once(LOAD_SUCCESS, this.deeplinkSuccess);
view.once(LOAD_FAILED, this.deeplinkFailed); browserView.once(LOAD_FAILED, this.deeplinkFailed);
} }
} }
} else { } else {
@ -225,35 +225,35 @@ export class ViewManager {
*/ */
private loadServer = (server: MattermostServer) => { private loadServer = (server: MattermostServer) => {
const tabs = ServerManager.getOrderedTabsForServer(server.id); const views = ServerManager.getOrderedTabsForServer(server.id);
tabs.forEach((tab) => this.loadView(server, tab)); views.forEach((view) => this.loadView(server, view));
} }
private loadView = (srv: MattermostServer, tab: TabView, url?: string) => { private loadView = (srv: MattermostServer, view: MattermostView, url?: string) => {
if (!tab.isOpen) { if (!view.isOpen) {
this.closedViews.set(tab.id, {srv, tab}); this.closedViews.set(view.id, {srv, view});
return; return;
} }
const view = this.makeView(srv, tab, url); const browserView = this.makeView(srv, view, url);
this.addView(view); 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(); const mainWindow = MainWindow.get();
if (!mainWindow) { if (!mainWindow) {
throw new Error('Cannot create view, no main window present'); throw new Error('Cannot create view, no main window present');
} }
const view = new MattermostView(tab, {webPreferences: {spellcheck: Config.useSpellChecker}}); const browserView = new MattermostBrowserView(view, {webPreferences: {spellcheck: Config.useSpellChecker}});
view.once(LOAD_SUCCESS, this.activateView); browserView.once(LOAD_SUCCESS, this.activateView);
view.on(LOADSCREEN_END, this.finishLoading); browserView.on(LOADSCREEN_END, this.finishLoading);
view.on(LOAD_FAILED, this.failLoading); browserView.on(LOAD_FAILED, this.failLoading);
view.on(UPDATE_TARGET_URL, this.showURLView); browserView.on(UPDATE_TARGET_URL, this.showURLView);
view.load(url); browserView.load(url);
return view; return browserView;
} }
private addView = (view: MattermostView): void => { private addView = (view: MattermostBrowserView): void => {
this.views.set(view.id, view); this.views.set(view.id, view);
if (this.closedViews.has(view.id)) { if (this.closedViews.has(view.id)) {
this.closedViews.delete(view.id); this.closedViews.delete(view.id);
@ -265,8 +265,8 @@ export class ViewManager {
if (ServerManager.hasServers()) { if (ServerManager.hasServers()) {
const lastActiveServer = ServerManager.getCurrentServer(); const lastActiveServer = ServerManager.getCurrentServer();
const lastActiveTab = ServerManager.getLastActiveTabForServer(lastActiveServer.id); const lastActiveView = ServerManager.getLastActiveTabForServer(lastActiveServer.id);
this.showById(lastActiveTab.id); this.showById(lastActiveView.id);
} else { } else {
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW);
} }
@ -382,33 +382,33 @@ export class ViewManager {
*/ */
/** Called when a new configuration is received /** Called when a new configuration is received
* Servers or tabs have been added or edited. We need to * Servers or views have been added or edited. We need to
* close, open, or reload tabs, taking care to reuse tabs and * close, open, or reload views, taking care to reuse views and
* preserve focus on the currently selected tab. */ * preserve focus on the currently selected view. */
private handleReloadConfiguration = () => { private handleReloadConfiguration = () => {
log.debug('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()) { 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 views: Map<string, MattermostBrowserView> = new Map();
const closed: Map<string, {srv: MattermostServer; tab: TabView}> = new Map(); const closed: Map<string, {srv: MattermostServer; view: MattermostView}> = new Map();
const sortedTabs = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id). const sortedViews = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id).
map((t): [MattermostServer, TabView] => [x, t])); map((t): [MattermostServer, MattermostView] => [x, t]));
for (const [srv, tab] of sortedTabs) { for (const [srv, view] of sortedViews) {
const recycle = current.get(tab.id); const recycle = current.get(view.id);
if (!tab.isOpen) { if (!view.isOpen) {
closed.set(tab.id, {srv, tab}); closed.set(view.id, {srv, view});
} else if (recycle) { } else if (recycle) {
views.set(tab.id, recycle); views.set(view.id, recycle);
} else { } 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 // commit closed
for (const x of closed.values()) { 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()) { if (ServerManager.hasServers()) {
this.currentView = undefined; this.currentView = undefined;
this.showInitial(); this.showInitial();
@ -440,13 +440,13 @@ export class ViewManager {
} }
} }
// show the focused tab (or initial) // show the focused view (or initial)
if (currentTabId && views.has(currentTabId)) { if (currentViewId && views.has(currentViewId)) {
const view = views.get(currentTabId); const view = views.get(currentViewId);
if (view && view.id !== this.currentView) { if (view && view.id !== this.currentView) {
this.currentView = view.id; this.currentView = view.id;
this.showById(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 { } else {
this.focusCurrentView(); this.focusCurrentView();
} }
@ -475,17 +475,17 @@ export class ViewManager {
return; return;
} }
let cleanedPathName = pathName; let cleanedPathName = pathName;
if (currentView.tab.server.url.pathname !== '/' && pathName.startsWith(currentView.tab.server.url.pathname)) { if (currentView.view.server.url.pathname !== '/' && pathName.startsWith(currentView.view.server.url.pathname)) {
cleanedPathName = pathName.replace(currentView.tab.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 (this.isViewClosed(redirectedviewId)) {
// If it's a closed view, just open it and stop // 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; return;
} }
let redirectedView = this.getView(redirectedviewId) || currentView; 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); log.info('redirecting to a new view', redirectedView?.id || viewId);
this.showById(redirectedView?.id || viewId); this.showById(redirectedView?.id || viewId);
} else { } else {
@ -493,7 +493,7 @@ export class ViewManager {
} }
// Special case check for Channels to not force a redirect to "/", causing a refresh // Special case check for Channels to not force a redirect to "/", causing a refresh
if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) { if (!(redirectedView !== currentView && redirectedView?.view.type === TAB_MESSAGING && cleanedPathName === '/')) {
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName); redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
if (redirectedView) { if (redirectedView) {
this.handleBrowserHistoryButton(e, redirectedView.id); this.handleBrowserHistoryButton(e, redirectedView.id);
@ -547,7 +547,7 @@ export class ViewManager {
const currentView = this.getCurrentView(); const currentView = this.getCurrentView();
if (currentView && currentView.currentURL) { 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); currentView.setBounds(adjustedBounds);
} }
} }
@ -556,21 +556,21 @@ export class ViewManager {
* Helper functions * Helper functions
*/ */
private openClosedTab = (id: string, url?: string) => { private openClosedView = (id: string, url?: string) => {
if (!this.closedViews.has(id)) { if (!this.closedViews.has(id)) {
return; return;
} }
const {srv, tab} = this.closedViews.get(id)!; const {srv, view} = this.closedViews.get(id)!;
tab.isOpen = true; view.isOpen = true;
this.loadView(srv, tab, url); this.loadView(srv, view, url);
this.showById(id); this.showById(id);
const view = this.views.get(id)!; const browserView = this.views.get(id)!;
view.isVisible = true; browserView.isVisible = true;
view.on(LOAD_SUCCESS, () => { browserView.on(LOAD_SUCCESS, () => {
view.isVisible = false; browserView.isVisible = false;
this.showById(id); this.showById(id);
}); });
ipcMain.emit(OPEN_TAB, null, tab.id); ipcMain.emit(OPEN_VIEW, null, view.id);
} }
private getViewLogger = (viewId: string) => { private getViewLogger = (viewId: string) => {
@ -585,8 +585,8 @@ export class ViewManager {
return { return {
id: view.id, id: view.id,
webContentsId: view.webContentsId, webContentsId: view.webContentsId,
serverName: view.tab.server.name, serverName: view.view.server.name,
tabType: view.tab.type, viewType: view.view.type,
}; };
} }
} }

View file

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

View file

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

View file

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

View file

@ -421,7 +421,7 @@ describe('main/windows/mainWindow', () => {
expect(window.setFullScreen).toHaveBeenCalledWith(false); 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; const originalPlatform = process.platform;
Object.defineProperty(process, 'platform', { Object.defineProperty(process, 'platform', {
value: 'darwin', value: 'darwin',

View file

@ -5,7 +5,7 @@ import React, {useState, useCallback, useEffect} from 'react';
import {useIntl, FormattedMessage} from 'react-intl'; import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import {MattermostTeam} from 'types/config'; import {UniqueServer} from 'types/config';
import {isValidURL, parseURL} from 'common/utils/url'; import {isValidURL, parseURL} from 'common/utils/url';
import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants'; import {MODAL_TRANSITION_TIMEOUT} from 'common/utils/constants';
@ -22,8 +22,8 @@ import 'renderer/css/components/ConfigureServer.scss';
import 'renderer/css/components/LoadingScreen.css'; import 'renderer/css/components/LoadingScreen.css';
type ConfigureServerProps = { type ConfigureServerProps = {
currentTeams: MattermostTeam[]; currentServers: UniqueServer[];
team?: MattermostTeam; server?: UniqueServer;
mobileView?: boolean; mobileView?: boolean;
darkMode?: boolean; darkMode?: boolean;
messageTitle?: string; messageTitle?: string;
@ -32,12 +32,12 @@ type ConfigureServerProps = {
alternateLinkMessage?: string; alternateLinkMessage?: string;
alternateLinkText?: string; alternateLinkText?: string;
alternateLinkURL?: string; alternateLinkURL?: string;
onConnect: (data: MattermostTeam) => void; onConnect: (data: UniqueServer) => void;
}; };
function ConfigureServer({ function ConfigureServer({
currentTeams, currentServers,
team, server,
mobileView, mobileView,
darkMode, darkMode,
messageTitle, messageTitle,
@ -54,7 +54,7 @@ function ConfigureServer({
name: prevName, name: prevName,
url: prevURL, url: prevURL,
id, id,
} = team || {}; } = server || {};
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>(); const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
const [name, setName] = useState(prevName || ''); const [name, setName] = useState(prevName || '');
@ -92,14 +92,14 @@ function ConfigureServer({
if (!newName) { if (!newName) {
return formatMessage({ return formatMessage({
id: 'renderer.components.newTeamModal.error.nameRequired', id: 'renderer.components.newServerModal.error.nameRequired',
defaultMessage: 'Name is required.', defaultMessage: 'Name is required.',
}); });
} }
if (currentTeams.find(({name: existingName}) => existingName === newName)) { if (currentServers.find(({name: existingName}) => existingName === newName)) {
return formatMessage({ return formatMessage({
id: 'renderer.components.newTeamModal.error.serverNameExists', id: 'renderer.components.newServerModal.error.serverNameExists',
defaultMessage: 'A server with the same name already exists.', defaultMessage: 'A server with the same name already exists.',
}); });
} }
@ -110,28 +110,28 @@ function ConfigureServer({
const validateURL = async (fullURL: string) => { const validateURL = async (fullURL: string) => {
if (!fullURL) { if (!fullURL) {
return formatMessage({ return formatMessage({
id: 'renderer.components.newTeamModal.error.urlRequired', id: 'renderer.components.newServerModal.error.urlRequired',
defaultMessage: 'URL is required.', defaultMessage: 'URL is required.',
}); });
} }
if (!parseURL(fullURL)) { if (!parseURL(fullURL)) {
return formatMessage({ return formatMessage({
id: 'renderer.components.newTeamModal.error.urlIncorrectFormatting', id: 'renderer.components.newServerModal.error.urlIncorrectFormatting',
defaultMessage: 'URL is not formatted correctly.', defaultMessage: 'URL is not formatted correctly.',
}); });
} }
if (!isValidURL(fullURL)) { if (!isValidURL(fullURL)) {
return formatMessage({ return formatMessage({
id: 'renderer.components.newTeamModal.error.urlNeedsHttp', id: 'renderer.components.newServerModal.error.urlNeedsHttp',
defaultMessage: 'URL should start with http:// or https://.', 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({ return formatMessage({
id: 'renderer.components.newTeamModal.error.serverUrlExists', id: 'renderer.components.newServerModal.error.serverUrlExists',
defaultMessage: 'A server with the same URL already exists.', 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 {DropResult} from 'react-beautiful-dnd';
import {injectIntl, IntlShape} from 'react-intl'; import {injectIntl, IntlShape} from 'react-intl';
import {MattermostTab, MattermostTeam} from 'types/config'; import {UniqueView, UniqueServer} from 'types/config';
import {DownloadedItems} from 'types/downloads'; import {DownloadedItems} from 'types/downloads';
import restoreButton from '../../assets/titlebar/chrome-restore.svg'; import restoreButton from '../../assets/titlebar/chrome-restore.svg';
@ -23,7 +23,7 @@ import {playSound} from '../notificationSounds';
import TabBar from './TabBar'; import TabBar from './TabBar';
import ExtraBar from './ExtraBar'; import ExtraBar from './ExtraBar';
import ErrorView from './ErrorView'; import ErrorView from './ErrorView';
import TeamDropdownButton from './TeamDropdownButton'; import ServerDropdownButton from './ServerDropdownButton';
import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton'; import DownloadsDropdownButton from './DownloadsDropdown/DownloadsDropdownButton';
import '../css/components/UpgradeButton.scss'; import '../css/components/UpgradeButton.scss';
@ -47,8 +47,8 @@ type Props = {
type State = { type State = {
activeServerId?: string; activeServerId?: string;
activeTabId?: string; activeTabId?: string;
servers: MattermostTeam[]; servers: UniqueServer[];
tabs: Map<string, MattermostTab[]>; tabs: Map<string, UniqueView[]>;
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>; unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
@ -147,7 +147,7 @@ class MainPage extends React.PureComponent<Props, State> {
setInitialActiveTab = async () => { setInitialActiveTab = async () => {
const lastActive = await window.desktop.getLastActive(); const lastActive = await window.desktop.getLastActive();
this.setActiveView(lastActive.server, lastActive.tab); this.setActiveView(lastActive.server, lastActive.view);
} }
updateServers = async () => { updateServers = async () => {
@ -239,11 +239,11 @@ class MainPage extends React.PureComponent<Props, State> {
this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired}); this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired});
}); });
window.desktop.onCloseTeamsDropdown(() => { window.desktop.onCloseServersDropdown(() => {
this.setState({isMenuOpen: false}); this.setState({isMenuOpen: false});
}); });
window.desktop.onOpenTeamsDropdown(() => { window.desktop.onOpenServersDropdown(() => {
this.setState({isMenuOpen: true}); this.setState({isMenuOpen: true});
}); });
@ -290,7 +290,7 @@ class MainPage extends React.PureComponent<Props, State> {
} }
handleCloseDropdowns = () => { handleCloseDropdowns = () => {
window.desktop.closeTeamsDropdown(); window.desktop.closeServersDropdown();
this.closeDownloadsDropdown(); this.closeDownloadsDropdown();
} }
@ -307,7 +307,7 @@ class MainPage extends React.PureComponent<Props, State> {
} }
handleCloseTab = (tabId: string) => { handleCloseTab = (tabId: string) => {
window.desktop.closeTab(tabId); window.desktop.closeView(tabId);
} }
handleDragAndDrop = async (dropResult: DropResult) => { handleDragAndDrop = async (dropResult: DropResult) => {
@ -399,7 +399,7 @@ class MainPage extends React.PureComponent<Props, State> {
render() { render() {
const {intl} = this.props; const {intl} = this.props;
let currentTabs: MattermostTab[] = []; let currentTabs: UniqueView[] = [];
if (this.state.activeServerId) { if (this.state.activeServerId) {
currentTabs = this.state.tabs.get(this.state.activeServerId) ?? []; currentTabs = this.state.tabs.get(this.state.activeServerId) ?? [];
} }
@ -538,7 +538,7 @@ class MainPage extends React.PureComponent<Props, State> {
/> />
</button> </button>
{activeServer && ( {activeServer && (
<TeamDropdownButton <ServerDropdownButton
isDisabled={this.state.modalOpen} isDisabled={this.state.modalOpen}
activeServerName={activeServer.name} activeServerName={activeServer.name}
totalMentionCount={totalMentionCount} totalMentionCount={totalMentionCount}

View file

@ -6,15 +6,15 @@ import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap'; import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {MattermostTeam} from 'types/config'; import {UniqueServer} from 'types/config';
import {isValidURL} from 'common/utils/url'; import {isValidURL} from 'common/utils/url';
type Props = { type Props = {
onClose?: () => void; onClose?: () => void;
onSave?: (team: MattermostTeam) => void; onSave?: (server: UniqueServer) => void;
team?: MattermostTeam; server?: UniqueServer;
currentTeams?: MattermostTeam[]; currentServers?: UniqueServer[];
editMode?: boolean; editMode?: boolean;
show?: boolean; show?: boolean;
restoreFocus?: boolean; restoreFocus?: boolean;
@ -24,16 +24,16 @@ type Props = {
}; };
type State = { type State = {
teamName: string; serverName: string;
teamUrl: string; serverUrl: string;
teamId?: string; serverId?: string;
teamOrder: number; serverOrder: number;
saveStarted: boolean; saveStarted: boolean;
} }
class NewTeamModal extends React.PureComponent<Props, State> { class NewServerModal extends React.PureComponent<Props, State> {
wasShown?: boolean; wasShown?: boolean;
teamUrlInputRef?: HTMLInputElement; serverUrlInputRef?: HTMLInputElement;
static defaultProps = { static defaultProps = {
restoreFocus: true, restoreFocus: true,
@ -44,90 +44,90 @@ class NewTeamModal extends React.PureComponent<Props, State> {
this.wasShown = false; this.wasShown = false;
this.state = { this.state = {
teamName: '', serverName: '',
teamUrl: '', serverUrl: '',
teamOrder: props.currentOrder || 0, serverOrder: props.currentOrder || 0,
saveStarted: false, saveStarted: false,
}; };
} }
initializeOnShow() { initializeOnShow() {
this.setState({ this.setState({
teamName: this.props.team ? this.props.team.name : '', serverName: this.props.server ? this.props.server.name : '',
teamUrl: this.props.team ? this.props.team.url : '', serverUrl: this.props.server ? this.props.server.url : '',
teamId: this.props.team?.id, serverId: this.props.server?.id,
saveStarted: false, saveStarted: false,
}); });
} }
getTeamNameValidationError() { getServerNameValidationError() {
if (!this.state.saveStarted) { if (!this.state.saveStarted) {
return null; return null;
} }
if (this.props.currentTeams) { if (this.props.currentServers) {
const currentTeams = [...this.props.currentTeams]; const currentServers = [...this.props.currentServers];
if (currentTeams.find((team) => team.id !== this.state.teamId && team.name === this.state.teamName)) { if (currentServers.find((server) => server.id !== this.state.serverId && server.name === this.state.serverName)) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.serverNameExists' id='renderer.components.newServerModal.error.serverNameExists'
defaultMessage='A server with the same name already exists.' defaultMessage='A server with the same name already exists.'
/> />
); );
} }
} }
return this.state.teamName.length > 0 ? null : ( return this.state.serverName.length > 0 ? null : (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.nameRequired' id='renderer.components.newServerModal.error.nameRequired'
defaultMessage='Name is required.' defaultMessage='Name is required.'
/> />
); );
} }
getTeamNameValidationState() { getServerNameValidationState() {
return this.getTeamNameValidationError() === null ? null : 'error'; return this.getServerNameValidationError() === null ? null : 'error';
} }
handleTeamNameChange = (e: React.ChangeEvent<HTMLInputElement>) => { handleServerNameChange = (e: React.ChangeEvent<HTMLInputElement>) => {
this.setState({ this.setState({
teamName: e.target.value, serverName: e.target.value,
}); });
} }
getTeamUrlValidationError() { getServerUrlValidationError() {
if (!this.state.saveStarted) { if (!this.state.saveStarted) {
return null; return null;
} }
if (this.props.currentTeams) { if (this.props.currentServers) {
const currentTeams = [...this.props.currentTeams]; const currentServers = [...this.props.currentServers];
if (currentTeams.find((team) => team.id !== this.state.teamId && team.url === this.state.teamUrl)) { if (currentServers.find((server) => server.id !== this.state.serverId && server.url === this.state.serverUrl)) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.serverUrlExists' id='renderer.components.newServerModal.error.serverUrlExists'
defaultMessage='A server with the same URL already exists.' defaultMessage='A server with the same URL already exists.'
/> />
); );
} }
} }
if (this.state.teamUrl.length === 0) { if (this.state.serverUrl.length === 0) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.urlRequired' id='renderer.components.newServerModal.error.urlRequired'
defaultMessage='URL is required.' defaultMessage='URL is required.'
/> />
); );
} }
if (!(/^https?:\/\/.*/).test(this.state.teamUrl.trim())) { if (!(/^https?:\/\/.*/).test(this.state.serverUrl.trim())) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.urlNeedsHttp' id='renderer.components.newServerModal.error.urlNeedsHttp'
defaultMessage='URL should start with http:// or https://.' defaultMessage='URL should start with http:// or https://.'
/> />
); );
} }
if (!isValidURL(this.state.teamUrl.trim())) { if (!isValidURL(this.state.serverUrl.trim())) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.urlIncorrectFormatting' id='renderer.components.newServerModal.error.urlIncorrectFormatting'
defaultMessage='URL is not formatted correctly.' defaultMessage='URL is not formatted correctly.'
/> />
); );
@ -135,32 +135,32 @@ class NewTeamModal extends React.PureComponent<Props, State> {
return null; return null;
} }
getTeamUrlValidationState() { getServerUrlValidationState() {
return this.getTeamUrlValidationError() === null ? null : 'error'; return this.getServerUrlValidationError() === null ? null : 'error';
} }
handleTeamUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => { handleServerUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
const teamUrl = e.target.value; const serverUrl = e.target.value;
this.setState({teamUrl}); this.setState({serverUrl});
} }
addProtocolToUrl = (teamUrl: string): Promise<void> => { addProtocolToUrl = (serverUrl: string): Promise<void> => {
if (teamUrl.startsWith('http://') || teamUrl.startsWith('https://')) { if (serverUrl.startsWith('http://') || serverUrl.startsWith('https://')) {
return Promise.resolve(undefined); return Promise.resolve(undefined);
} }
return window.desktop.modals.pingDomain(teamUrl). return window.desktop.modals.pingDomain(serverUrl).
then((result: string) => { then((result: string) => {
this.setState({teamUrl: `${result}://${this.state.teamUrl}`}); this.setState({serverUrl: `${result}://${this.state.serverUrl}`});
}). }).
catch(() => { catch(() => {
console.error(`Could not ping url: ${teamUrl}`); console.error(`Could not ping url: ${serverUrl}`);
}); });
} }
getError() { getError() {
const nameError = this.getTeamNameValidationError(); const nameError = this.getServerNameValidationError();
const urlError = this.getTeamUrlValidationError(); const urlError = this.getServerUrlValidationError();
if (nameError && urlError) { if (nameError && urlError) {
return ( return (
@ -179,20 +179,20 @@ class NewTeamModal extends React.PureComponent<Props, State> {
} }
validateForm() { validateForm() {
return this.getTeamNameValidationState() === null && return this.getServerNameValidationState() === null &&
this.getTeamUrlValidationState() === null; this.getServerUrlValidationState() === null;
} }
save = async () => { save = async () => {
await this.addProtocolToUrl(this.state.teamUrl); await this.addProtocolToUrl(this.state.serverUrl);
this.setState({ this.setState({
saveStarted: true, saveStarted: true,
}, () => { }, () => {
if (this.validateForm()) { if (this.validateForm()) {
this.props.onSave?.({ this.props.onSave?.({
url: this.state.teamUrl, url: this.state.serverUrl,
name: this.state.teamName, name: this.state.serverName,
id: this.state.teamId, id: this.state.serverId,
}); });
} }
}); });
@ -219,14 +219,14 @@ class NewTeamModal extends React.PureComponent<Props, State> {
if (this.props.editMode) { if (this.props.editMode) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.title.edit' id='renderer.components.newServerModal.title.edit'
defaultMessage='Edit Server' defaultMessage='Edit Server'
/> />
); );
} }
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.title.add' id='renderer.components.newServerModal.title.add'
defaultMessage='Add Server' defaultMessage='Add Server'
/> />
); );
@ -241,11 +241,11 @@ class NewTeamModal extends React.PureComponent<Props, State> {
return ( return (
<Modal <Modal
bsClass='modal' bsClass='modal'
className='NewTeamModal' className='NewServerModal'
show={this.props.show} show={this.props.show}
id='newServerModal' id='newServerModal'
enforceFocus={true} enforceFocus={true}
onEntered={() => this.teamUrlInputRef?.focus()} onEntered={() => this.serverUrlInputRef?.focus()}
onHide={this.props.onClose} onHide={this.props.onClose}
restoreFocus={this.props.restoreFocus} restoreFocus={this.props.restoreFocus}
onKeyDown={(e: React.KeyboardEvent) => { onKeyDown={(e: React.KeyboardEvent) => {
@ -272,58 +272,58 @@ class NewTeamModal extends React.PureComponent<Props, State> {
<FormGroup> <FormGroup>
<FormLabel> <FormLabel>
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.serverURL' id='renderer.components.newServerModal.serverURL'
defaultMessage='Server URL' defaultMessage='Server URL'
/> />
</FormLabel> </FormLabel>
<FormControl <FormControl
id='teamUrlInput' id='serverUrlInput'
type='text' type='text'
value={this.state.teamUrl} value={this.state.serverUrl}
placeholder='https://example.com' placeholder='https://example.com'
onChange={this.handleTeamUrlChange} onChange={this.handleServerUrlChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => { onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation(); e.stopPropagation();
}} }}
ref={(ref: HTMLInputElement) => { ref={(ref: HTMLInputElement) => {
this.teamUrlInputRef = ref; this.serverUrlInputRef = ref;
if (this.props.setInputRef) { if (this.props.setInputRef) {
this.props.setInputRef(ref); this.props.setInputRef(ref);
} }
}} }}
isInvalid={Boolean(this.getTeamUrlValidationState())} isInvalid={Boolean(this.getServerUrlValidationState())}
autoFocus={true} autoFocus={true}
/> />
<FormControl.Feedback/> <FormControl.Feedback/>
<FormText> <FormText>
<FormattedMessage <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://.' defaultMessage='The URL of your Mattermost server. Must start with http:// or https://.'
/> />
</FormText> </FormText>
</FormGroup> </FormGroup>
<FormGroup className='NewTeamModal-noBottomSpace'> <FormGroup className='NewServerModal-noBottomSpace'>
<FormLabel> <FormLabel>
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.serverDisplayName' id='renderer.components.newServerModal.serverDisplayName'
defaultMessage='Server Display Name' defaultMessage='Server Display Name'
/> />
</FormLabel> </FormLabel>
<FormControl <FormControl
id='teamNameInput' id='serverNameInput'
type='text' type='text'
value={this.state.teamName} value={this.state.serverName}
placeholder={this.props.intl.formatMessage({id: 'renderer.components.newTeamModal.serverDisplayName', defaultMessage: 'Server Display Name'})} placeholder={this.props.intl.formatMessage({id: 'renderer.components.newServerModal.serverDisplayName', defaultMessage: 'Server Display Name'})}
onChange={this.handleTeamNameChange} onChange={this.handleServerNameChange}
onClick={(e: React.MouseEvent<HTMLInputElement>) => { onClick={(e: React.MouseEvent<HTMLInputElement>) => {
e.stopPropagation(); e.stopPropagation();
}} }}
isInvalid={Boolean(this.getTeamNameValidationState())} isInvalid={Boolean(this.getServerNameValidationState())}
/> />
<FormControl.Feedback/> <FormControl.Feedback/>
<FormText className='NewTeamModal-noBottomSpace'> <FormText className='NewServerModal-noBottomSpace'>
<FormattedMessage <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.' defaultMessage='The name of the server displayed on your desktop app tab bar.'
/> />
</FormText> </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 React, {useEffect} from 'react';
import {FormattedMessage} from 'react-intl'; import {FormattedMessage} from 'react-intl';
import '../css/components/TeamDropdownButton.scss'; import '../css/components/ServerDropdownButton.scss';
type Props = { type Props = {
isDisabled?: boolean; isDisabled?: boolean;
@ -16,7 +16,7 @@ type Props = {
darkMode: boolean; 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 {isDisabled, activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props;
const buttonRef: React.RefObject<HTMLButtonElement> = React.createRef(); const buttonRef: React.RefObject<HTMLButtonElement> = React.createRef();
@ -30,22 +30,22 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
if (isMenuOpen) { if (isMenuOpen) {
window.desktop.closeTeamsDropdown(); window.desktop.closeServersDropdown();
} else { } else {
window.desktop.openTeamsDropdown(); window.desktop.openServersDropdown();
} }
}; };
let badgeDiv: React.ReactNode; let badgeDiv: React.ReactNode;
if (totalMentionCount > 0) { if (totalMentionCount > 0) {
badgeDiv = ( badgeDiv = (
<div className='TeamDropdownButton__badge-count'> <div className='ServerDropdownButton__badge-count'>
<span>{totalMentionCount > 99 ? '99+' : totalMentionCount}</span> <span>{totalMentionCount > 99 ? '99+' : totalMentionCount}</span>
</div> </div>
); );
} else if (hasUnreads) { } else if (hasUnreads) {
badgeDiv = ( badgeDiv = (
<div className='TeamDropdownButton__badge-unreads'/> <div className='ServerDropdownButton__badge-unreads'/>
); );
} }
@ -53,7 +53,7 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
<button <button
ref={buttonRef} ref={buttonRef}
disabled={isDisabled} disabled={isDisabled}
className={classNames('TeamDropdownButton', { className={classNames('ServerDropdownButton', {
disabled: isDisabled, disabled: isDisabled,
isMenuOpen, isMenuOpen,
darkMode, darkMode,
@ -63,14 +63,14 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
event.stopPropagation(); event.stopPropagation();
}} }}
> >
<div className='TeamDropdownButton__badge'> <div className='ServerDropdownButton__badge'>
<i className='icon-server-variant'/> <i className='icon-server-variant'/>
{badgeDiv} {badgeDiv}
</div> </div>
{activeServerName && <span>{activeServerName}</span>} {activeServerName && <span>{activeServerName}</span>}
{!activeServerName && {!activeServerName &&
<FormattedMessage <FormattedMessage
id='renderer.components.teamDropdownButton.noServersConfigured' id='renderer.components.serverDropdownButton.noServersConfigured'
defaultMessage='No servers configured' 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 {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classNames from 'classnames'; 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 = { type Props = {
activeTabId?: string; activeTabId?: string;
@ -19,7 +19,7 @@ type Props = {
isDarkMode: boolean; isDarkMode: boolean;
onSelect: (id: string) => void; onSelect: (id: string) => void;
onCloseTab: (id: string) => void; onCloseTab: (id: string) => void;
tabs: MattermostTab[]; tabs: UniqueView[];
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>; unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
@ -80,7 +80,7 @@ class TabBar extends React.PureComponent<Props> {
return ( return (
<Draggable <Draggable
key={tab.id} key={tab.id}
draggableId={`teamTabItem-${tab.id}`} draggableId={`serverTabItem-${tab.id}`}
index={index} index={index}
> >
{(provided, snapshot) => { {(provided, snapshot) => {
@ -98,10 +98,10 @@ class TabBar extends React.PureComponent<Props> {
<NavItem <NavItem
ref={provided.innerRef} ref={provided.innerRef}
as='li' as='li'
id={`teamTabItem${index}`} id={`serverTabItem${index}`}
draggable={false} draggable={false}
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})} title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getViewDisplayName(tab.name as ViewType)})}
className={classNames('teamTabItem', { className={classNames('serverTabItem', {
active: this.props.activeTabId === tab.id, active: this.props.activeTabId === tab.id,
dragging: snapshot.isDragging, dragging: snapshot.isDragging,
})} })}
@ -121,12 +121,12 @@ class TabBar extends React.PureComponent<Props> {
<div className='TabBar-tabSeperator'> <div className='TabBar-tabSeperator'>
<FormattedMessage <FormattedMessage
id={`common.tabs.${tab.name}`} id={`common.tabs.${tab.name}`}
defaultMessage={getTabDisplayName(tab.name as TabType)} defaultMessage={getViewDisplayName(tab.name as ViewType)}
/> />
{ badgeDiv } { badgeDiv }
{canCloseTab(tab.name as TabType) && {canCloseView(tab.name as ViewType) &&
<button <button
className='teamTabItem__close' className='serverTabItem__close'
onClick={this.onCloseTab(tab.id!)} onClick={this.onCloseTab(tab.id!)}
> >
<i className='icon-close'/> <i className='icon-close'/>

View file

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

View file

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

View file

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

View file

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

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