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

* Rename MattermostTeam -> UniqueServer, MattermostTab -> UniqueView

* Rename 'team' to 'server'

* Some further cleanup

* Rename weirdly named function

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

* Fix i18n

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,7 +2,7 @@
@import url("HoveringURL.css");
@import url("MainPage.css");
@import url("MattermostView.css");
@import url("NewTeamModal.css");
@import url("NewServerModal.css");
@import url("PermissionRequestDialog.css");
@import url("TabBar.css");
@import url("UpdaterPage.css");

View File

@@ -14,7 +14,7 @@ body {
max-width: 600px;
}
.TeamDropdown {
.ServerDropdown {
display: flex;
flex-direction: column;
align-items: flex-start;
@@ -26,7 +26,7 @@ body {
min-width: 180px;
}
.TeamDropdown__droppable {
.ServerDropdown__droppable {
width: 100%;
overflow-y: scroll;
@@ -40,21 +40,21 @@ body {
}
}
.TeamDropdown__header {
.ServerDropdown__header {
padding: 6px 20px;
display: flex;
align-items: center;
width: -webkit-fill-available;
user-select: none;
.TeamDropdown__servers {
.ServerDropdown__servers {
font-weight: 600;
font-size: 14px;
line-height: 20px;
color: #3D3C40;
}
.TeamDropdown__keyboardShortcut {
.ServerDropdown__keyboardShortcut {
font-weight: 400;
font-size: 12px;
line-height: 16px;
@@ -63,14 +63,14 @@ body {
}
}
.TeamDropdown__divider {
.ServerDropdown__divider {
border-top: 1px solid rgba(61, 60, 64, 0.08);
border-bottom: 0;
width: 100%;
margin: 8px 0;
}
.TeamDropdown__button {
.ServerDropdown__button {
background-color: transparent;
border: none;
padding: 8px 18px 8px 7px;
@@ -83,12 +83,12 @@ body {
&:not(.anyDragging):hover {
background-color: rgba(61, 60, 64, 0.08);
.TeamDropdown__button-edit, .TeamDropdown__button-remove {
.ServerDropdown__button-edit, .ServerDropdown__button-remove {
opacity: 1;
pointer-events: all;
}
.TeamDropdown__draggable-handle > i.icon-drag-vertical {
.ServerDropdown__draggable-handle > i.icon-drag-vertical {
opacity: 1;
}
}
@@ -97,17 +97,17 @@ body {
background-color: rgba(22, 109, 224, 0.08);
outline: none;
.TeamDropdown__button-edit, .TeamDropdown__button-remove {
.ServerDropdown__button-edit, .ServerDropdown__button-remove {
opacity: 1;
pointer-events: all;
}
.TeamDropdown__draggable-handle > i.icon-drag-vertical {
.ServerDropdown__draggable-handle > i.icon-drag-vertical {
opacity: 1;
}
}
&.dragging .TeamDropdown__draggable-handle {
&.dragging .ServerDropdown__draggable-handle {
opacity: 1;
pointer-events: all;
}
@@ -126,7 +126,7 @@ body {
color: #166de0;
}
> .TeamDropdown__draggable-handle > span, &.addServer > span {
> .ServerDropdown__draggable-handle > span, &.addServer > span {
font-size: 14px;
line-height: 20px;
color: #3D3C40;
@@ -147,7 +147,7 @@ body {
}
}
.TeamDropdown__draggable-handle {
.ServerDropdown__draggable-handle {
cursor: pointer !important;
display: flex;
overflow: hidden;
@@ -174,7 +174,7 @@ body {
}
}
.TeamDropdown__badge {
.ServerDropdown__badge {
display: flex;
justify-content: center;
align-items: center;
@@ -182,7 +182,7 @@ body {
min-width: 32px;
}
.TeamDropdown__badge-dot {
.ServerDropdown__badge-dot {
background: #579EFF;
height: 8px;
width: 8px;
@@ -190,7 +190,7 @@ body {
flex: 0 0 8px;
}
.TeamDropdown__badge-count {
.ServerDropdown__badge-count {
background: #F74343;
text-align: center;
border-radius: 8px;
@@ -211,7 +211,7 @@ body {
}
}
.TeamDropdown__indicators {
.ServerDropdown__indicators {
margin-left: auto;
display: flex;
@@ -229,7 +229,7 @@ body {
outline: none;
}
&.TeamDropdown__button-remove:hover, &.TeamDropdown__button-remove:focus {
&.ServerDropdown__button-remove:hover, &.ServerDropdown__button-remove:focus {
background: rgba(247, 67, 67, 0.16);
box-shadow: 0px 0px 0px 4px rgba(247, 67, 67, 0.16);
}
@@ -240,7 +240,7 @@ body {
}
}
.TeamDropdown__button-edit, .TeamDropdown__button-remove {
.ServerDropdown__button-edit, .ServerDropdown__button-remove {
opacity: 0;
pointer-events: none;
@@ -250,10 +250,10 @@ body {
}
}
.TeamDropdown__button-remove {
.ServerDropdown__button-remove {
margin-right: 7px;
+ .TeamDropdown__badge {
+ .ServerDropdown__badge {
margin-left: 3px;
}
@@ -262,22 +262,22 @@ body {
}
}
.TeamDropdown.darkMode {
.ServerDropdown.darkMode {
background: #1f1f1f;
.TeamDropdown__header > span {
.ServerDropdown__header > span {
color: #DDD;
&.TeamDropdown__keyboardShortcut {
&.ServerDropdown__keyboardShortcut {
color: rgba(221, 221, 221, 0.56);
}
}
.TeamDropdown__divider {
.ServerDropdown__divider {
border-color: rgba(221, 221, 221, 0.08);
}
.TeamDropdown__button {
.ServerDropdown__button {
&:not(.anyDragging):hover {
background-color: rgba(221, 221, 221, 0.08);
}
@@ -290,37 +290,37 @@ body {
color: rgba(221, 221, 221, 0.56);
}
> .TeamDropdown__draggable-handle > span, &.addServer > span {
> .ServerDropdown__draggable-handle > span, &.addServer > span {
color: #DDD;
}
.TeamDropdown__button-remove i {
.ServerDropdown__button-remove i {
color: #F74343;
}
.TeamDropdown__button-edit:hover, .TeamDropdown__button-edit:focus {
.ServerDropdown__button-edit:hover, .ServerDropdown__button-edit:focus {
background: rgba(221, 223, 228, 0.08);
box-shadow: 0px 0px 0px 4px rgba(221, 223, 228, 0.08);
}
}
.TeamDropdown__badge-expired i {
.ServerDropdown__badge-expired i {
color: rgba(221, 221, 221, 0.56);
}
.TeamDropdown__button-edit > i {
.ServerDropdown__button-edit > i {
color: rgba(221, 221, 221, 0.56);
}
.TeamDropdown__draggable-handle > i.icon-drag-vertical::before {
.ServerDropdown__draggable-handle > i.icon-drag-vertical::before {
color:rgba(221, 221, 221, 0.32);
}
.TeamDropdown__badge-count i {
.ServerDropdown__badge-count i {
color: #e81023;
}
.TeamDropdown__badge-dot {
.ServerDropdown__badge-dot {
background: #196CAF;
}
}

View File

@@ -1,6 +1,6 @@
@import '~bootstrap-dark/src/bootstrap-dark.css';
.TeamListItem:hover {
.ServerListItem:hover {
background: #242a30;
}

View File

@@ -7,7 +7,7 @@ import {FormattedMessage} from 'react-intl';
import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
@@ -16,16 +16,16 @@ import './css/dropdown.scss';
import IntlProvider from './intl_provider';
type State = {
teams?: MattermostTeam[];
teamOrder?: string[];
orderedTeams?: MattermostTeam[];
activeTeam?: string;
servers?: UniqueServer[];
serverOrder?: string[];
orderedServers?: UniqueServer[];
activeServer?: string;
darkMode?: boolean;
enableServerManagement?: boolean;
unreads?: Map<string, boolean>;
mentions?: Map<string, number>;
expired?: Map<string, boolean>;
hasGPOTeams?: boolean;
hasGPOServers?: boolean;
isAnyDragging: boolean;
windowBounds?: Electron.Rectangle;
}
@@ -40,7 +40,7 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
}
return style;
}
class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
class ServerDropdown extends React.PureComponent<Record<string, never>, State> {
buttonRefs: Map<number, HTMLButtonElement>;
addServerRef: React.RefObject<HTMLButtonElement>;
focusedIndex: number | null;
@@ -59,22 +59,22 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}
handleUpdate = (
teams: MattermostTeam[],
servers: UniqueServer[],
darkMode: boolean,
windowBounds: Electron.Rectangle,
activeTeam?: string,
activeServer?: string,
enableServerManagement?: boolean,
hasGPOTeams?: boolean,
hasGPOServers?: boolean,
expired?: Map<string, boolean>,
mentions?: Map<string, number>,
unreads?: Map<string, boolean>,
) => {
this.setState({
teams,
activeTeam,
servers,
activeServer,
darkMode,
enableServerManagement,
hasGPOTeams,
hasGPOServers,
unreads,
mentions,
expired,
@@ -82,12 +82,12 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
});
}
selectServer = (team: MattermostTeam) => {
selectServer = (server: UniqueServer) => {
return () => {
if (!team.id) {
if (!server.id) {
return;
}
window.desktop.serverDropdown.switchServer(team.id);
window.desktop.serverDropdown.switchServer(server.id);
this.closeMenu();
};
}
@@ -95,7 +95,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
closeMenu = () => {
if (!this.state.isAnyDragging) {
(document.activeElement as HTMLElement).blur();
window.desktop.closeTeamsDropdown();
window.desktop.closeServersDropdown();
}
}
@@ -108,8 +108,8 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
this.closeMenu();
}
isActiveTeam = (team: MattermostTeam) => {
return team.id === this.state.activeTeam;
isActiveServer = (server: UniqueServer) => {
return server.id === this.state.activeServer;
}
onDragStart = () => {
@@ -123,17 +123,17 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
this.setState({isAnyDragging: false});
return;
}
if (!this.state.teams) {
if (!this.state.servers) {
throw new Error('No config');
}
const teamsCopy = this.state.teams.concat();
const serversCopy = this.state.servers.concat();
const team = teamsCopy.splice(removedIndex, 1);
const newOrder = addedIndex < this.state.teams.length ? addedIndex : this.state.teams.length - 1;
teamsCopy.splice(newOrder, 0, team[0]);
const server = serversCopy.splice(removedIndex, 1);
const newOrder = addedIndex < this.state.servers.length ? addedIndex : this.state.servers.length - 1;
serversCopy.splice(newOrder, 0, server[0]);
this.setState({teams: teamsCopy, isAnyDragging: false});
window.desktop.updateServerOrder(teamsCopy.map((team) => team.id!));
this.setState({servers: serversCopy, isAnyDragging: false});
window.desktop.updateServerOrder(serversCopy.map((server) => server.id!));
}
componentDidMount() {
@@ -151,18 +151,18 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
window.removeEventListener('keydown', this.handleKeyboardShortcuts);
}
setButtonRef = (teamIndex: number, refMethod?: (element: HTMLButtonElement) => unknown) => {
setButtonRef = (serverIndex: number, refMethod?: (element: HTMLButtonElement) => unknown) => {
return (ref: HTMLButtonElement) => {
this.addButtonRef(teamIndex, ref);
this.addButtonRef(serverIndex, ref);
refMethod?.(ref);
};
}
addButtonRef = (teamIndex: number, ref: HTMLButtonElement | null) => {
addButtonRef = (serverIndex: number, ref: HTMLButtonElement | null) => {
if (ref) {
this.buttonRefs.set(teamIndex, ref);
this.buttonRefs.set(serverIndex, ref);
ref.addEventListener('focusin', () => {
this.focusedIndex = teamIndex;
this.focusedIndex = serverIndex;
});
ref.addEventListener('blur', () => {
this.focusedIndex = null;
@@ -203,30 +203,30 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}
}
editServer = (teamId: string) => {
if (this.teamIsPredefined(teamId)) {
editServer = (serverId: string) => {
if (this.serverIsPredefined(serverId)) {
return () => {};
}
return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
window.desktop.serverDropdown.showEditServerModal(teamId);
window.desktop.serverDropdown.showEditServerModal(serverId);
this.closeMenu();
};
}
removeServer = (teamId: string) => {
if (this.teamIsPredefined(teamId)) {
removeServer = (serverId: string) => {
if (this.serverIsPredefined(serverId)) {
return () => {};
}
return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation();
window.desktop.serverDropdown.showRemoveServerModal(teamId);
window.desktop.serverDropdown.showRemoveServerModal(serverId);
this.closeMenu();
};
}
teamIsPredefined = (teamId: string) => {
return this.state.teams?.some((team) => team.id === teamId && team.isPredefined);
serverIsPredefined = (serverId: string) => {
return this.state.servers?.some((server) => server.id === serverId && server.isPredefined);
}
render() {
@@ -234,7 +234,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
<IntlProvider>
<div
onClick={this.preventPropagation}
className={classNames('TeamDropdown', {
className={classNames('ServerDropdown', {
darkMode: this.state.darkMode,
})}
style={{
@@ -242,101 +242,101 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
maxWidth: this.state.windowBounds ? (this.state.windowBounds.width - THREE_DOT_MENU_WIDTH_MAC) : undefined,
}}
>
<div className='TeamDropdown__header'>
<span className='TeamDropdown__servers'>
<div className='ServerDropdown__header'>
<span className='ServerDropdown__servers'>
<FormattedMessage
id='renderer.dropdown.servers'
defaultMessage='Servers'
/>
</span>
<span className='TeamDropdown__keyboardShortcut'>
<span className='ServerDropdown__keyboardShortcut'>
{window.process.platform === 'darwin' ? '⌃⌘S' : 'Ctrl + Shift + S'}
</span>
</div>
<hr className='TeamDropdown__divider'/>
<hr className='ServerDropdown__divider'/>
<DragDropContext
onDragStart={this.onDragStart}
onDragEnd={this.onDragEnd}
>
<Droppable
isDropDisabled={this.state.hasGPOTeams}
droppableId='TeamDropdown__droppable'
isDropDisabled={this.state.hasGPOServers}
droppableId='ServerDropdown__droppable'
>
{(provided) => (
<div
className='TeamDropdown__droppable'
className='ServerDropdown__droppable'
ref={provided.innerRef}
{...provided.droppableProps}
>
{this.state.teams?.map((team, orderedIndex) => {
const index = this.state.teams?.indexOf(team);
const sessionExpired = this.state.expired?.get(team.id!);
const hasUnreads = this.state.unreads?.get(team.id!);
const mentionCount = this.state.mentions?.get(team.id!);
{this.state.servers?.map((server, orderedIndex) => {
const index = this.state.servers?.indexOf(server);
const sessionExpired = this.state.expired?.get(server.id!);
const hasUnreads = this.state.unreads?.get(server.id!);
const mentionCount = this.state.mentions?.get(server.id!);
let badgeDiv: React.ReactNode;
if (sessionExpired) {
badgeDiv = (
<div className='TeamDropdown__badge-expired'>
<div className='ServerDropdown__badge-expired'>
<i className='icon-alert-circle-outline'/>
</div>
);
} else if (mentionCount && mentionCount > 0) {
badgeDiv = (
<div className='TeamDropdown__badge-count'>
<div className='ServerDropdown__badge-count'>
<span>{mentionCount > 99 ? '99+' : mentionCount}</span>
</div>
);
} else if (hasUnreads) {
badgeDiv = (
<div className='TeamDropdown__badge-dot'/>
<div className='ServerDropdown__badge-dot'/>
);
}
return (
<Draggable
key={index}
draggableId={`TeamDropdown__draggable-${index}`}
draggableId={`ServerDropdown__draggable-${index}`}
index={orderedIndex}
disableInteractiveElementBlocking={true}
>
{(provided, snapshot) => (
<button
className={classNames('TeamDropdown__button', {
className={classNames('ServerDropdown__button', {
dragging: snapshot.isDragging,
anyDragging: this.state.isAnyDragging,
active: this.isActiveTeam(team),
active: this.isActiveServer(server),
})}
ref={this.setButtonRef(orderedIndex, provided.innerRef)}
{...provided.draggableProps}
onClick={this.selectServer(team)}
onClick={this.selectServer(server)}
style={getStyle(provided.draggableProps.style)}
>
<div
className={classNames('TeamDropdown__draggable-handle', {
className={classNames('ServerDropdown__draggable-handle', {
dragging: snapshot.isDragging,
})}
{...provided.dragHandleProps}
onClick={this.handleClickOnDragHandle}
>
<i className='icon-drag-vertical'/>
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
<span>{team.name}</span>
{this.isActiveServer(server) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
<span>{server.name}</span>
</div>
{!team.isPredefined && <div className='TeamDropdown__indicators'>
{!server.isPredefined && <div className='ServerDropdown__indicators'>
<button
className='TeamDropdown__button-edit'
onClick={this.editServer(team.id!)}
className='ServerDropdown__button-edit'
onClick={this.editServer(server.id!)}
>
<i className='icon-pencil-outline'/>
</button>
<button
className='TeamDropdown__button-remove'
onClick={this.removeServer(team.id!)}
className='ServerDropdown__button-remove'
onClick={this.removeServer(server.id!)}
>
<i className='icon-trash-can-outline'/>
</button>
{badgeDiv && <div className='TeamDropdown__badge'>
{badgeDiv && <div className='ServerDropdown__badge'>
{badgeDiv}
</div>}
</div>}
@@ -350,13 +350,13 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
)}
</Droppable>
</DragDropContext>
<hr className='TeamDropdown__divider'/>
<hr className='ServerDropdown__divider'/>
{this.state.enableServerManagement &&
<button
ref={(ref) => {
this.addButtonRef(this.state.teams?.length || 0, ref);
this.addButtonRef(this.state.servers?.length || 0, ref);
}}
className='TeamDropdown__button addServer'
className='ServerDropdown__button addServer'
onClick={this.addServer}
>
<i className='icon-plus'/>
@@ -373,6 +373,6 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}
ReactDOM.render(
<TeamDropdown/>,
<ServerDropdown/>,
document.getElementById('app'),
);

View File

@@ -7,49 +7,49 @@ import 'renderer/css/modals.css';
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import IntlProvider from 'renderer/intl_provider';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
import NewServerModal from '../../components/NewServerModal';
import setupDarkMode from '../darkMode';
setupDarkMode();
type ModalInfo = {
team: MattermostTeam;
currentTeams: MattermostTeam[];
server: UniqueServer;
currentServers: UniqueServer[];
};
const onClose = () => {
window.desktop.modals.cancelModal();
};
const onSave = (data: MattermostTeam) => {
const onSave = (data: UniqueServer) => {
window.desktop.modals.finishModal(data);
};
const EditServerModalWrapper: React.FC = () => {
const [server, setServer] = useState<MattermostTeam>();
const [currentTeams, setCurrentTeams] = useState<MattermostTeam[]>();
const [server, setServer] = useState<UniqueServer>();
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
useEffect(() => {
window.desktop.modals.getModalInfo<ModalInfo>().then(({team, currentTeams}) => {
setServer(team);
setCurrentTeams(currentTeams);
window.desktop.modals.getModalInfo<ModalInfo>().then(({server, currentServers}) => {
setServer(server);
setCurrentServers(currentServers);
});
}, []);
return (
<IntlProvider>
<NewTeamModal
<NewServerModal
onClose={onClose}
onSave={onSave}
editMode={true}
show={Boolean(server)}
team={server}
currentTeams={currentTeams}
server={server}
currentServers={currentServers}
/>
</IntlProvider>
);

View File

@@ -35,7 +35,7 @@ class LoadingScreenRoot extends React.PureComponent<Props, State> {
window.desktop.loadingScreen.onToggleLoadingScreenVisibility(this.onToggleLoadingScreenVisibility);
window.addEventListener('click', () => {
window.desktop.closeTeamsDropdown();
window.desktop.closeServersDropdown();
window.desktop.closeDownloadsDropdown();
});
}

View File

@@ -7,11 +7,11 @@ import 'renderer/css/modals.css';
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import IntlProvider from 'renderer/intl_provider';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
import NewServerModal from '../../components/NewServerModal';
import setupDarkMode from '../darkMode';
@@ -21,31 +21,31 @@ const onClose = () => {
window.desktop.modals.cancelModal();
};
const onSave = (data: MattermostTeam) => {
const onSave = (data: UniqueServer) => {
window.desktop.modals.finishModal(data);
};
const NewServerModalWrapper: React.FC = () => {
const [unremoveable, setUnremovable] = useState<boolean>();
const [currentTeams, setCurrentTeams] = useState<MattermostTeam[]>();
const [currentServers, setCurrentServers] = useState<UniqueServer[]>();
useEffect(() => {
window.desktop.modals.isModalUncloseable().then((uncloseable) => {
setUnremovable(uncloseable);
});
window.desktop.modals.getModalInfo<MattermostTeam[]>().then((teams) => {
setCurrentTeams(teams);
window.desktop.modals.getModalInfo<UniqueServer[]>().then((servers) => {
setCurrentServers(servers);
});
}, []);
return (
<IntlProvider>
<NewTeamModal
<NewServerModal
onClose={unremoveable ? undefined : onClose}
onSave={onSave}
editMode={false}
show={true}
currentTeams={currentTeams}
currentServers={currentServers}
/>
</IntlProvider>
);

View File

@@ -4,7 +4,7 @@
import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom';
import {MattermostTeam} from 'types/config';
import {UniqueServer} from 'types/config';
import IntlProvider from 'renderer/intl_provider';
@@ -15,7 +15,7 @@ import 'bootstrap/dist/css/bootstrap.min.css';
const MOBILE_SCREEN_WIDTH = 1200;
const onConnect = (data: MattermostTeam) => {
const onConnect = (data: UniqueServer) => {
window.desktop.modals.finishModal(data);
};
@@ -23,7 +23,7 @@ const WelcomeScreenModalWrapper = () => {
const [darkMode, setDarkMode] = useState(false);
const [getStarted, setGetStarted] = useState(false);
const [mobileView, setMobileView] = useState(false);
const [currentTeams, setCurrentTeams] = useState<MattermostTeam[]>([]);
const [currentServers, setCurrentServers] = useState<UniqueServer[]>([]);
const handleWindowResize = () => {
setMobileView(window.innerWidth < MOBILE_SCREEN_WIDTH);
@@ -38,8 +38,8 @@ const WelcomeScreenModalWrapper = () => {
setDarkMode(result);
});
window.desktop.modals.getModalInfo<MattermostTeam[]>().then((result) => {
setCurrentTeams(result);
window.desktop.modals.getModalInfo<UniqueServer[]>().then((result) => {
setCurrentServers(result);
});
handleWindowResize();
@@ -60,7 +60,7 @@ const WelcomeScreenModalWrapper = () => {
<ConfigureServer
mobileView={mobileView}
darkMode={darkMode}
currentTeams={currentTeams}
currentServers={currentServers}
onConnect={onConnect}
/>
) : (

View File

@@ -1,32 +1,32 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export type Tab = {
export type View = {
name: string;
isOpen?: boolean;
}
export type Team = {
export type Server = {
name: string;
url: string;
}
export type ConfigTab = Tab & {
export type ConfigView = View & {
order: number;
}
export type ConfigServer = Team & {
export type ConfigServer = Server & {
order: number;
lastActiveTab?: number;
tabs: ConfigTab[];
tabs: ConfigView[];
}
export type MattermostTeam = Team & {
export type UniqueServer = Server & {
id?: string;
isPredefined?: boolean;
}
export type MattermostTab = Tab & {
export type UniqueView = View & {
id?: string;
}
@@ -61,59 +61,49 @@ export type ConfigV3 = {
appLanguage?: string;
}
export type ConfigV2 = {
version: 2;
teams: Array<{
name: string;
url: string;
order: number;
}>;
showTrayIcon: boolean;
trayIconTheme: string;
minimizeToTray: boolean;
notifications: {
flashWindow: number;
bounceIcon: boolean;
bounceIconType: 'critical' | 'informational';
};
showUnreadBadge: boolean;
useSpellChecker: boolean;
enableHardwareAcceleration: boolean;
autostart: boolean;
spellCheckerLocale: string;
spellCheckerURL?: string;
darkMode: boolean;
downloadLocation?: string;
}
export type ConfigV2 =
Omit<ConfigV3,
'version' |
'teams' |
'hideOnStart' |
'spellCheckerLocales' |
'lastActiveTeam' |
'startInFullscreen' |
'autoCheckForUpdates' |
'alwaysMinimize' |
'alwaysClose' |
'logLevel' |
'appLanguage'
> & {
version: 2;
teams: Array<{
name: string;
url: string;
order: number;
}>;
spellCheckerLocale: string;
}
export type ConfigV1 = {
version: 1;
teams: Array<{
name: string;
url: string;
}>;
showTrayIcon: boolean;
trayIconTheme: string;
minimizeToTray: boolean;
notifications: {
flashWindow: number;
bounceIcon: boolean;
bounceIconType: 'critical' | 'informational';
};
showUnreadBadge: boolean;
useSpellChecker: boolean;
spellCheckerURL?: string;
enableHardwareAcceleration: boolean;
autostart: boolean;
spellCheckerLocale: string;
}
export type ConfigV1 =
Omit<ConfigV2,
'version' |
'teams' |
'darkMode' |
'downloadLocation'
> & {
version: 1;
teams: Array<{
name: string;
url: string;
}>;
}
export type ConfigV0 = {version: 0; url: string};
export type AnyConfig = ConfigV3 | ConfigV2 | ConfigV1 | ConfigV0;
export type BuildConfig = {
defaultTeams?: Team[];
defaultServers?: Server[];
helpLink: string;
enableServerManagement: boolean;
enableAutoUpdater: boolean;
@@ -122,12 +112,12 @@ export type BuildConfig = {
}
export type RegistryConfig = {
teams: Team[];
servers: Server[];
enableServerManagement: boolean;
enableAutoUpdater: boolean;
}
export type CombinedConfig = Omit<ConfigV3, 'teams'> & Omit<BuildConfig, 'defaultTeams'> & {
export type CombinedConfig = Omit<Config, 'teams'> & Omit<BuildConfig, 'defaultServers'> & {
appName: string;
useNativeWindow: boolean;
}

View File

@@ -5,7 +5,7 @@ import {ipcRenderer, Rectangle} from 'electron/renderer';
import {Language} from '../../i18n/i18n';
import {CombinedConfig, LocalConfiguration, MattermostTab, MattermostTeam} from './config';
import {CombinedConfig, LocalConfiguration, UniqueView, UniqueServer} from './config';
import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
import {SaveQueueItem} from './settings';
@@ -32,10 +32,10 @@ declare global {
desktop: {
quit: (reason: string, stack: string) => void;
openAppMenu: () => void;
closeTeamsDropdown: () => void;
openTeamsDropdown: () => void;
switchTab: (tabId: string) => void;
closeTab: (tabId: string) => void;
closeServersDropdown: () => void;
openServersDropdown: () => void;
switchTab: (viewId: string) => void;
closeView: (viewId: string) => void;
closeWindow: () => void;
minimizeWindow: () => void;
maximizeWindow: () => void;
@@ -51,10 +51,10 @@ declare global {
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
updateServerOrder: (serverOrder: string[]) => Promise<void>;
updateTabOrder: (serverId: string, tabOrder: string[]) => Promise<void>;
getLastActive: () => Promise<{server: string; tab: string}>;
getOrderedServers: () => Promise<MattermostTeam[]>;
getOrderedTabsForServer: (serverId: string) => Promise<MattermostTab[]>;
updateTabOrder: (serverId: string, viewOrder: string[]) => Promise<void>;
getLastActive: () => Promise<{server: string; view: string}>;
getOrderedServers: () => Promise<UniqueServer[]>;
getOrderedTabsForServer: (serverId: string) => Promise<UniqueView[]>;
onUpdateServers: (listener: () => void) => void;
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
@@ -74,7 +74,7 @@ declare global {
onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
onLoadSuccess: (listener: (viewId: string) => void) => void;
onLoadFailed: (listener: (viewId: string, err: string, loadUrl: string) => void) => void;
onSetActiveView: (listener: (serverId: string, tabId: string) => void) => void;
onSetActiveView: (listener: (serverId: string, viewId: string) => void) => void;
onMaximizeChange: (listener: (maximize: boolean) => void) => void;
onEnterFullScreen: (listener: () => void) => void;
onLeaveFullScreen: (listener: () => void) => void;
@@ -83,8 +83,8 @@ declare global {
onModalClose: (listener: () => void) => void;
onToggleBackButton: (listener: (showExtraBar: boolean) => void) => void;
onUpdateMentions: (listener: (view: string, mentions: number, unreads: boolean, isExpired: boolean) => void) => void;
onCloseTeamsDropdown: (listener: () => void) => void;
onOpenTeamsDropdown: (listener: () => void) => void;
onCloseServersDropdown: (listener: () => void) => void;
onOpenServersDropdown: (listener: () => void) => void;
onCloseDownloadsDropdown: (listener: () => void) => void;
onOpenDownloadsDropdown: (listener: () => void) => void;
onShowDownloadsDropdownButtonBadge: (listener: () => void) => void;
@@ -139,12 +139,12 @@ declare global {
showRemoveServerModal: (serverId: string) => void;
onUpdateServerDropdown: (listener: (
teams: MattermostTeam[],
servers: UniqueServer[],
darkMode: boolean,
windowBounds: Rectangle,
activeTeam?: string,
activeServer?: string,
enableServerManagement?: boolean,
hasGPOTeams?: boolean,
hasGPOServers?: boolean,
expired?: Map<string, boolean>,
mentions?: Map<string, number>,
unreads?: Map<string, boolean>,