[MM-50485] Migrate app to ServerManager, remove view names and replace with IDs (#2672)

* Migrate app to ServerManager, remove view names and replace with IDs

* Fixed a test

* Fixed a bug when adding the initial server

* Merge'd

* Bug fixes and PR feedback
This commit is contained in:
Devin Binnie
2023-04-12 12:52:34 -04:00
committed by GitHub
parent d87097b1eb
commit 686b4ac9f1
58 changed files with 1570 additions and 2175 deletions

View File

@@ -234,9 +234,8 @@ module.exports = {
if (!window.testHelper) { if (!window.testHelper) {
return null; return null;
} }
const name = await window.testHelper.getViewName(); const info = await window.testHelper.getViewInfoForTest();
const webContentsId = await window.testHelper.getWebContentsId(); return {viewName: `${info.serverName}___${info.tabType}`, webContentsId: info.webContentsId};
return {viewName: name, webContentsId};
}).then((result) => { }).then((result) => {
if (result) { if (result) {
map[result.viewName] = {win, webContentsId: result.webContentsId}; map[result.viewName] = {win, webContentsId: result.webContentsId};

View File

@@ -137,7 +137,7 @@ describe('Add Server Modal', function desc() {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'TestTeam', name: 'TestTeam',
url: 'http://example.org', url: 'http://example.org/',
order: 2, order: 2,
tabs: [ tabs: [
{ {

View File

@@ -83,9 +83,8 @@ describe('Configure Server Modal', function desc() {
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
url: 'http://example.org', url: 'http://example.org/',
name: 'TestTeam', name: 'TestTeam',
index: null,
order: 0, order: 0,
tabs: [ tabs: [
{ {

View File

@@ -201,7 +201,7 @@ describe('EditServerModal', function desc() {
}); });
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'example', name: 'example',
url: 'http://google.com', url: 'http://google.com/',
order: 0, order: 0,
tabs: [ tabs: [
{ {
@@ -254,7 +254,7 @@ describe('EditServerModal', function desc() {
}); });
savedConfig.teams.should.deep.contain({ savedConfig.teams.should.deep.contain({
name: 'NewTestTeam', name: 'NewTestTeam',
url: 'http://google.com', url: 'http://google.com/',
order: 0, order: 0,
tabs: [ tabs: [
{ {

View File

@@ -18,7 +18,6 @@ export const GET_LOCAL_CONFIGURATION = 'get-local-configuration';
export const RELOAD_CONFIGURATION = 'reload-config'; export const RELOAD_CONFIGURATION = 'reload-config';
export const EMIT_CONFIGURATION = 'emit-configuration'; export const EMIT_CONFIGURATION = 'emit-configuration';
export const UPDATE_TEAMS = 'update-teams';
export const DARK_MODE_CHANGE = 'dark_mode_change'; export const DARK_MODE_CHANGE = 'dark_mode_change';
export const GET_DARK_MODE = 'get-dark-mode'; export const GET_DARK_MODE = 'get-dark-mode';
export const USER_ACTIVITY_UPDATE = 'user-activity-update'; export const USER_ACTIVITY_UPDATE = 'user-activity-update';
@@ -38,7 +37,6 @@ export const DOUBLE_CLICK_ON_WINDOW = 'double_click';
export const SHOW_NEW_SERVER_MODAL = 'show_new_server_modal'; export const SHOW_NEW_SERVER_MODAL = 'show_new_server_modal';
export const SHOW_EDIT_SERVER_MODAL = 'show-edit-server-modal'; export const SHOW_EDIT_SERVER_MODAL = 'show-edit-server-modal';
export const SHOW_REMOVE_SERVER_MODAL = 'show-remove-server-modal'; export const SHOW_REMOVE_SERVER_MODAL = 'show-remove-server-modal';
export const MAIN_WINDOW_SHOWN = 'main-window-shown';
export const RETRIEVE_MODAL_INFO = 'retrieve-modal-info'; export const RETRIEVE_MODAL_INFO = 'retrieve-modal-info';
export const MODAL_CANCEL = 'modal-cancel'; export const MODAL_CANCEL = 'modal-cancel';
@@ -106,8 +104,7 @@ export const APP_LOGGED_OUT = 'app-logged-out';
export const GET_AVAILABLE_SPELL_CHECKER_LANGUAGES = 'get-available-spell-checker-languages'; export const GET_AVAILABLE_SPELL_CHECKER_LANGUAGES = 'get-available-spell-checker-languages';
export const GET_VIEW_NAME = 'get-view-name'; export const GET_VIEW_INFO_FOR_TEST = 'get-view-info-for-test';
export const GET_VIEW_WEBCONTENTS_ID = 'get-view-webcontents-id';
export const RESIZE_MODAL = 'resize-modal'; export const RESIZE_MODAL = 'resize-modal';
export const GET_MODAL_UNCLOSEABLE = 'get-modal-uncloseable'; export const GET_MODAL_UNCLOSEABLE = 'get-modal-uncloseable';
@@ -119,7 +116,6 @@ export const UPDATE_URL_VIEW_WIDTH = 'update-url-view-width';
export const RELOAD_CURRENT_VIEW = 'reload-current-view'; export const RELOAD_CURRENT_VIEW = 'reload-current-view';
export const PING_DOMAIN = 'ping-domain'; export const PING_DOMAIN = 'ping-domain';
export const PING_DOMAIN_RESPONSE = 'ping-domain-response';
export const GET_LANGUAGE_INFORMATION = 'get-language-information'; export const GET_LANGUAGE_INFORMATION = 'get-language-information';
export const GET_AVAILABLE_LANGUAGES = 'get-available-languages'; export const GET_AVAILABLE_LANGUAGES = 'get-available-languages';
@@ -166,3 +162,8 @@ export const DOWNLOADS_DROPDOWN_MENU_SHOW_FILE_IN_FOLDER = 'downloads-dropdown-m
export const SERVERS_URL_MODIFIED = 'servers-modified'; export const SERVERS_URL_MODIFIED = 'servers-modified';
export const SERVERS_UPDATE = 'servers-update'; export const SERVERS_UPDATE = 'servers-update';
export const UPDATE_SERVER_ORDER = 'update-server-order';
export const UPDATE_TAB_ORDER = 'update-tab-order';
export const GET_LAST_ACTIVE = 'get-last-active';
export const GET_ORDERED_SERVERS = 'get-ordered-servers';
export const GET_ORDERED_TABS_FOR_SERVER = 'get-ordered-tabs-for-server';

View File

@@ -314,67 +314,66 @@ describe('common/config', () => {
}); });
}); });
// TODO: Re-enable when we migrate to ServerManager fully describe('regenerateCombinedConfigData', () => {
// describe('regenerateCombinedConfigData', () => { it('should combine config from all sources', () => {
// it('should combine config from all sources', () => { const config = new Config();
// const config = new Config(); config.reload = jest.fn();
// config.reload = jest.fn(); config.init(configPath, appName, appPath);
// config.init(configPath, appName, appPath); config.useNativeWindow = false;
// config.useNativeWindow = false; config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'};
// config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'}; config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'};
// config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'}; config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'};
// config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'}; config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'};
// config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'};
// config.regenerateCombinedConfigData(); config.regenerateCombinedConfigData();
// config.combinedData.darkMode = false; config.combinedData.darkMode = false;
// expect(config.combinedData).toStrictEqual({ expect(config.combinedData).toStrictEqual({
// appName: 'app-name', appName: 'app-name',
// useNativeWindow: false, useNativeWindow: false,
// darkMode: false, darkMode: false,
// otherBuildSetting: 'registry', otherBuildSetting: 'registry',
// registrySetting: 'registry', registrySetting: 'registry',
// otherLocalSetting: 'build', otherLocalSetting: 'build',
// buildSetting: 'build', buildSetting: 'build',
// otherDefaultSetting: 'local', otherDefaultSetting: 'local',
// localSetting: 'local', localSetting: 'local',
// defaultSetting: 'default', defaultSetting: 'default',
// }); });
// }); });
// it('should not include any teams in the combined config', () => { it('should not include any teams in the combined config', () => {
// const config = new Config(); const config = new Config();
// config.reload = jest.fn(); config.reload = jest.fn();
// config.init(configPath, appName, appPath); config.init(configPath, appName, appPath);
// config.defaultConfigData = {}; config.defaultConfigData = {};
// config.localConfigData = {}; config.localConfigData = {};
// config.buildConfigData = {enableServerManagement: true}; config.buildConfigData = {enableServerManagement: true};
// config.registryConfigData = {}; config.registryConfigData = {};
// config.predefinedTeams.push(team, team); config.predefinedTeams.push(team, team);
// config.useNativeWindow = false; config.useNativeWindow = false;
// config.localConfigData = {teams: [ config.localConfigData = {teams: [
// team, team,
// { {
// ...team, ...team,
// name: 'local-team-2', name: 'local-team-2',
// url: 'http://local-team-2.com', url: 'http://local-team-2.com',
// }, },
// { {
// ...team, ...team,
// name: 'local-team-1', name: 'local-team-1',
// order: 1, order: 1,
// url: 'http://local-team-1.com', url: 'http://local-team-1.com',
// }, },
// ]}; ]};
// config.regenerateCombinedConfigData(); config.regenerateCombinedConfigData();
// config.combinedData.darkMode = false; config.combinedData.darkMode = false;
// expect(config.combinedData).toStrictEqual({ expect(config.combinedData).toStrictEqual({
// appName: 'app-name', appName: 'app-name',
// useNativeWindow: false, useNativeWindow: false,
// darkMode: false, darkMode: false,
// enableServerManagement: true, enableServerManagement: true,
// }); });
// }); });
// }); });
}); });

View File

@@ -169,9 +169,6 @@ export class Config extends EventEmitter {
get version() { get version() {
return this.combinedData?.version ?? defaultPreferences.version; return this.combinedData?.version ?? defaultPreferences.version;
} }
get teams() {
return this.combinedData?.teams ?? defaultPreferences.teams;
}
get darkMode() { get darkMode() {
return this.combinedData?.darkMode ?? defaultPreferences.darkMode; return this.combinedData?.darkMode ?? defaultPreferences.darkMode;
} }
@@ -380,71 +377,14 @@ export class Config extends EventEmitter {
); );
// We don't want to include the servers in the combined config, they should only be accesible via the ServerManager // We don't want to include the servers in the combined config, they should only be accesible via the ServerManager
//delete (this.combinedData as any).teams; delete (this.combinedData as any).teams;
delete (this.combinedData as any).defaultTeams; delete (this.combinedData as any).defaultTeams;
if (this.combinedData) { if (this.combinedData) {
// TODO: This can be removed after we fully migrate to ServerManager
let combinedTeams: ConfigServer[] = [];
combinedTeams.push(...this.predefinedTeams);
if (this.localConfigData && this.enableServerManagement) {
combinedTeams.push(...this.localConfigData.teams || []);
}
combinedTeams = this.filterOutDuplicateTeams(combinedTeams);
combinedTeams = this.sortUnorderedTeams(combinedTeams);
this.combinedData.teams = combinedTeams;
this.combinedData.appName = this.appName; this.combinedData.appName = this.appName;
} }
} }
/**
* Returns the provided list of teams with duplicates filtered out
* TODO: This can be removed after we fully migrate to ServerManager
* @param {array} teams array of teams to check for duplicates
*/
private filterOutDuplicateTeams = (teams: ConfigServer[]) => {
let newTeams = teams;
const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => {
return uniqueURLs.has(`${team.name}:${team.url}`) ? false : uniqueURLs.add(`${team.name}:${team.url}`);
});
return newTeams;
}
/**
* Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort
* TODO: This can be removed after we fully migrate to ServerManager
*/
private sortUnorderedTeams = (teams: ConfigServer[]) => {
// We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur
const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
// Make a best pass at interpreting sort order. If an order is not specified, assume it is 0.
//
const newTeams = mappedTeams.sort((x, y) => {
if (!x.team.order) {
x.team.order = 0;
}
if (!y.team.order) {
y.team.order = 0;
}
// once we ensured `order` exists, we can sort numerically
return x.team.order - y.team.order;
});
// Now re-number all items from 0 to (max), ensuring user's sort order is preserved. The
// new tabbed interface requires an item with order:0 in order to raise the first tab.
//
newTeams.forEach((mappedTeam, i) => {
mappedTeam.team.order = i;
});
return newTeams.sort((x, y) => x.originalOrder - y.originalOrder).map((mappedTeam) => mappedTeam.team);
}
// helper functions // helper functions
private writeFile = (filePath: string, configData: Partial<ConfigType>, callback?: fs.NoParamCallback) => { private writeFile = (filePath: string, configData: Partial<ConfigType>, callback?: fs.NoParamCallback) => {
if (!this.defaultConfigData) { if (!this.defaultConfigData) {

View File

@@ -3,7 +3,7 @@
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import {Team} from 'types/config'; import {MattermostTeam, Team} from 'types/config';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
@@ -13,14 +13,13 @@ export class MattermostServer {
url!: URL; url!: URL;
isPredefined: boolean; isPredefined: boolean;
constructor(server: Team, isPredefined = false) { constructor(server: Team, isPredefined: boolean) {
this.id = uuid(); this.id = uuid();
this.name = server.name; this.name = server.name;
this.updateURL(server.url); this.updateURL(server.url);
this.isPredefined = isPredefined; this.isPredefined = isPredefined;
if (!this.url) {
throw new Error('Invalid url for creating a server');
}
} }
updateURL = (url: string) => { updateURL = (url: string) => {
@@ -29,4 +28,13 @@ export class MattermostServer {
throw new Error('Invalid url for creating a server'); throw new Error('Invalid url for creating a server');
} }
} }
toMattermostTeam = (): MattermostTeam => {
return {
name: this.name,
url: this.url.toString(),
id: this.id,
isPredefined: this.isPredefined,
};
}
} }

View File

@@ -32,9 +32,9 @@ describe('common/servers/serverManager', () => {
}; };
serverManager.servers = new Map([['server-1', server]]); serverManager.servers = new Map([['server-1', server]]);
serverManager.tabs = new Map([ serverManager.tabs = new Map([
['tab-1', {id: 'tab-1', name: TAB_MESSAGING, isOpen: true, server}], ['tab-1', {id: 'tab-1', type: TAB_MESSAGING, isOpen: true, server}],
['tab-2', {id: 'tab-2', name: TAB_PLAYBOOKS, server}], ['tab-2', {id: 'tab-2', type: TAB_PLAYBOOKS, server}],
['tab-3', {id: 'tab-3', name: TAB_FOCALBOARD, server}], ['tab-3', {id: 'tab-3', type: TAB_FOCALBOARD, server}],
]); ]);
serverManager.tabOrder = new Map([['server-1', ['tab-1', 'tab-2', 'tab-3']]]); serverManager.tabOrder = new Map([['server-1', ['tab-1', 'tab-2', 'tab-3']]]);
serverManager.persistServers = jest.fn(); serverManager.persistServers = jest.fn();

View File

@@ -145,9 +145,9 @@ export class ServerManager extends EventEmitter {
} }
const tabs = this.getOrderedTabsForServer(server.id); const tabs = this.getOrderedTabsForServer(server.id);
let selectedTab = tabs.find((tab) => tab && tab.name === TAB_MESSAGING); let selectedTab = tabs.find((tab) => tab && tab.type === TAB_MESSAGING);
tabs. tabs.
filter((tab) => tab && tab.name !== TAB_MESSAGING). filter((tab) => tab && tab.type !== TAB_MESSAGING).
forEach((tab) => { forEach((tab) => {
if (parsedURL.pathname.match(new RegExp(`^${tab.url.pathname}(/(.+))?`))) { if (parsedURL.pathname.match(new RegExp(`^${tab.url.pathname}(/(.+))?`))) {
selectedTab = tab; selectedTab = tab;
@@ -187,6 +187,10 @@ export class ServerManager extends EventEmitter {
}); });
this.tabOrder.set(newServer.id, tabOrder); this.tabOrder.set(newServer.id, tabOrder);
if (!this.currentServerId) {
this.currentServerId = newServer.id;
}
// Emit this event whenever we update a server URL to ensure remote info is fetched // Emit this event whenever we update a server URL to ensure remote info is fetched
this.emit(SERVERS_URL_MODIFIED, [newServer.id]); this.emit(SERVERS_URL_MODIFIED, [newServer.id]);
this.persistServers(); this.persistServers();
@@ -230,6 +234,10 @@ export class ServerManager extends EventEmitter {
this.remoteInfo.delete(serverId); this.remoteInfo.delete(serverId);
this.servers.delete(serverId); this.servers.delete(serverId);
if (this.currentServerId === serverId && this.hasServers()) {
this.currentServerId = this.serverOrder[0];
}
this.persistServers(); this.persistServers();
} }
@@ -274,7 +282,7 @@ export class ServerManager extends EventEmitter {
} }
this.filterOutDuplicateTeams(); this.filterOutDuplicateTeams();
this.serverOrder = serverOrder; this.serverOrder = serverOrder;
if (Config.lastActiveTeam) { if (Config.lastActiveTeam && this.serverOrder[Config.lastActiveTeam]) {
this.currentServerId = this.serverOrder[Config.lastActiveTeam]; this.currentServerId = this.serverOrder[Config.lastActiveTeam];
} else { } else {
this.currentServerId = this.serverOrder[0]; this.currentServerId = this.serverOrder[0];
@@ -417,13 +425,13 @@ export class ServerManager extends EventEmitter {
tabOrder.forEach((tabId) => { tabOrder.forEach((tabId) => {
const tab = this.tabs.get(tabId); const tab = this.tabs.get(tabId);
if (tab) { if (tab) {
if (tab.name === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof tab.isOpen === 'undefined') { if (tab.type === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof tab.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Playbooks'); log.withPrefix(tab.id).verbose('opening Playbooks');
tab.isOpen = true; tab.isOpen = true;
this.tabs.set(tabId, tab); this.tabs.set(tabId, tab);
hasUpdates = true; hasUpdates = true;
} }
if (tab.name === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof tab.isOpen === 'undefined') { if (tab.type === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof tab.isOpen === 'undefined') {
log.withPrefix(tab.id).verbose('opening Boards'); log.withPrefix(tab.id).verbose('opening Boards');
tab.isOpen = true; tab.isOpen = true;
this.tabs.set(tabId, tab); this.tabs.set(tabId, tab);
@@ -453,7 +461,7 @@ export class ServerManager extends EventEmitter {
if (!view) { if (!view) {
return new Logger(viewId); return new Logger(viewId);
} }
return new Logger(...additionalPrefixes, ...this.includeId(viewId, view.server.name, view.name)); return new Logger(...additionalPrefixes, ...this.includeId(viewId, view.server.name, view.type));
}; };
} }

View File

@@ -3,9 +3,11 @@
import {v4 as uuid} from 'uuid'; import {v4 as uuid} from 'uuid';
import {MattermostTab} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import {getTabViewName, TabType, TabView} from './TabView'; import {TabType, TabView} from './TabView';
export default abstract class BaseTabView implements TabView { export default abstract class BaseTabView implements TabView {
id: string; id: string;
@@ -17,9 +19,6 @@ export default abstract class BaseTabView implements TabView {
this.server = server; this.server = server;
this.isOpen = isOpen; this.isOpen = isOpen;
} }
get name(): string {
return getTabViewName(this.server.name, this.type);
}
get url(): URL { get url(): URL {
throw new Error('Not implemented'); throw new Error('Not implemented');
} }
@@ -29,4 +28,12 @@ export default abstract class BaseTabView implements TabView {
get shouldNotify(): boolean { get shouldNotify(): boolean {
return false; return false;
} }
toMattermostTab = (): MattermostTab => {
return {
id: this.id,
name: this.type,
isOpen: this.isOpen,
};
}
} }

View File

@@ -1,7 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Team} from 'types/config'; import {MattermostTab, Team} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
@@ -15,10 +15,11 @@ export interface TabView {
server: MattermostServer; server: MattermostServer;
isOpen?: boolean; isOpen?: boolean;
get name(): string;
get type(): TabType; get type(): TabType;
get url(): URL; get url(): URL;
get shouldNotify(): boolean; get shouldNotify(): boolean;
toMattermostTab(): MattermostTab;
} }
export function getDefaultConfigTeamFromTeam(team: Team & {order: number; lastActiveTab?: number}) { export function getDefaultConfigTeamFromTeam(team: Team & {order: number; lastActiveTab?: number}) {
@@ -59,10 +60,6 @@ export function getTabDisplayName(tabType: TabType) {
} }
} }
export function getTabViewName(serverName: string, tabType: string) {
return `${serverName}___${tabType}`;
}
export function canCloseTab(tabType: TabType) { export function canCloseTab(tabType: TabType) {
return tabType !== TAB_MESSAGING; return tabType !== TAB_MESSAGING;
} }

View File

@@ -116,7 +116,6 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo
certificateErrorCallbacks.set(errorID, callback); certificateErrorCallbacks.set(errorID, callback);
// TODO: should we move this to window manager or provide a handler for dialogs?
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
if (!mainWindow) { if (!mainWindow) {
return; return;

View File

@@ -28,7 +28,6 @@ jest.mock('electron', () => ({
jest.mock('main/app/utils', () => ({ jest.mock('main/app/utils', () => ({
handleUpdateMenuEvent: jest.fn(), handleUpdateMenuEvent: jest.fn(),
updateSpellCheckerLocales: jest.fn(), updateSpellCheckerLocales: jest.fn(),
updateServerInfos: jest.fn(),
setLoggingLevel: jest.fn(), setLoggingLevel: jest.fn(),
})); }));
jest.mock('main/app/intercom', () => ({ jest.mock('main/app/intercom', () => ({
@@ -51,7 +50,6 @@ jest.mock('main/views/loadingScreen', () => ({}));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/windowManager', () => ({
handleUpdateConfig: jest.fn(), handleUpdateConfig: jest.fn(),
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
initializeCurrentServerName: jest.fn(),
})); }));
describe('main/app/config', () => { describe('main/app/config', () => {

View File

@@ -3,7 +3,7 @@
import {app, ipcMain, nativeTheme} from 'electron'; import {app, ipcMain, nativeTheme} from 'electron';
import {CombinedConfig, ConfigServer, Config as ConfigType} from 'types/config'; import {CombinedConfig, Config as ConfigType} from 'types/config';
import {DARK_MODE_CHANGE, EMIT_CONFIGURATION, RELOAD_CONFIGURATION} from 'common/communication'; import {DARK_MODE_CHANGE, EMIT_CONFIGURATION, RELOAD_CONFIGURATION} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
@@ -12,12 +12,11 @@ import {Logger, setLoggingLevel} from 'common/log';
import AutoLauncher from 'main/AutoLauncher'; import AutoLauncher from 'main/AutoLauncher';
import {setUnreadBadgeSetting} from 'main/badge'; import {setUnreadBadgeSetting} from 'main/badge';
import {refreshTrayImages} from 'main/tray/tray'; import {refreshTrayImages} from 'main/tray/tray';
import ViewManager from 'main/views/viewManager';
import LoadingScreen from 'main/views/loadingScreen'; import LoadingScreen from 'main/views/loadingScreen';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
import {handleMainWindowIsShown} from './intercom'; import {handleMainWindowIsShown} from './intercom';
import {handleUpdateMenuEvent, updateServerInfos, updateSpellCheckerLocales} from './utils'; import {handleUpdateMenuEvent, updateSpellCheckerLocales} from './utils';
const log = new Logger('App.Config'); const log = new Logger('App.Config');
@@ -60,14 +59,6 @@ export function handleUpdateTheme() {
Config.set('darkMode', nativeTheme.shouldUseDarkColors); Config.set('darkMode', nativeTheme.shouldUseDarkColors);
} }
export function handleUpdateTeams(event: Electron.IpcMainInvokeEvent, newTeams: ConfigServer[]) {
log.debug('Config.handleUpdateTeams');
log.silly('Config.handleUpdateTeams', newTeams);
Config.setServers(newTeams);
return Config.teams;
}
export function handleConfigUpdate(newConfig: CombinedConfig) { export function handleConfigUpdate(newConfig: CombinedConfig) {
if (newConfig.logLevel) { if (newConfig.logLevel) {
setLoggingLevel(newConfig.logLevel); setLoggingLevel(newConfig.logLevel);
@@ -81,7 +72,6 @@ export function handleConfigUpdate(newConfig: CombinedConfig) {
} }
if (app.isReady()) { if (app.isReady()) {
ViewManager.reloadConfiguration();
WindowManager.sendToRenderer(RELOAD_CONFIGURATION); WindowManager.sendToRenderer(RELOAD_CONFIGURATION);
} }
@@ -106,8 +96,6 @@ export function handleConfigUpdate(newConfig: CombinedConfig) {
} }
if (app.isReady()) { if (app.isReady()) {
updateServerInfos(newConfig.teams);
WindowManager.initializeCurrentServerName();
handleMainWindowIsShown(); handleMainWindowIsShown();
} }

View File

@@ -124,10 +124,10 @@ jest.mock('main/app/utils', () => ({
getDeeplinkingURL: jest.fn(), getDeeplinkingURL: jest.fn(),
handleUpdateMenuEvent: jest.fn(), handleUpdateMenuEvent: jest.fn(),
shouldShowTrayIcon: jest.fn(), shouldShowTrayIcon: jest.fn(),
updateServerInfos: jest.fn(),
updateSpellCheckerLocales: jest.fn(), updateSpellCheckerLocales: jest.fn(),
wasUpdated: jest.fn(), wasUpdated: jest.fn(),
initCookieManager: jest.fn(), initCookieManager: jest.fn(),
updateServerInfos: jest.fn(),
})); }));
jest.mock('main/appState', () => ({ jest.mock('main/appState', () => ({
on: jest.fn(), on: jest.fn(),
@@ -149,6 +149,11 @@ jest.mock('main/notifications', () => ({
displayDownloadCompleted: jest.fn(), displayDownloadCompleted: jest.fn(),
})); }));
jest.mock('main/ParseArgs', () => jest.fn()); jest.mock('main/ParseArgs', () => jest.fn());
jest.mock('common/servers/serverManager', () => ({
reloadFromConfig: jest.fn(),
getAllServers: jest.fn(),
on: jest.fn(),
}));
jest.mock('main/tray/tray', () => ({ jest.mock('main/tray/tray', () => ({
refreshTrayImages: jest.fn(), refreshTrayImages: jest.fn(),
setupTray: jest.fn(), setupTray: jest.fn(),
@@ -194,7 +199,6 @@ describe('main/app/initialize', () => {
} }
}); });
Config.data = {}; Config.data = {};
Config.teams = [];
app.whenReady.mockResolvedValue(); app.whenReady.mockResolvedValue();
app.requestSingleInstanceLock.mockReturnValue(true); app.requestSingleInstanceLock.mockReturnValue(true);
app.getPath.mockImplementation((p) => `/basedir/${p}`); app.getPath.mockImplementation((p) => `/basedir/${p}`);

View File

@@ -11,16 +11,9 @@ import {
SWITCH_SERVER, SWITCH_SERVER,
FOCUS_BROWSERVIEW, FOCUS_BROWSERVIEW,
QUIT, QUIT,
DOUBLE_CLICK_ON_WINDOW,
SHOW_NEW_SERVER_MODAL, SHOW_NEW_SERVER_MODAL,
WINDOW_CLOSE,
WINDOW_MAXIMIZE,
WINDOW_MINIMIZE,
WINDOW_RESTORE,
NOTIFY_MENTION, NOTIFY_MENTION,
GET_DOWNLOAD_LOCATION, GET_DOWNLOAD_LOCATION,
SHOW_SETTINGS_WINDOW,
RELOAD_CONFIGURATION,
SWITCH_TAB, SWITCH_TAB,
CLOSE_TAB, CLOSE_TAB,
OPEN_TAB, OPEN_TAB,
@@ -33,13 +26,17 @@ import {
START_UPGRADE, START_UPGRADE,
START_UPDATE_DOWNLOAD, START_UPDATE_DOWNLOAD,
PING_DOMAIN, PING_DOMAIN,
MAIN_WINDOW_SHOWN,
OPEN_APP_MENU, OPEN_APP_MENU,
GET_CONFIGURATION, GET_CONFIGURATION,
GET_LOCAL_CONFIGURATION, GET_LOCAL_CONFIGURATION,
UPDATE_CONFIGURATION, UPDATE_CONFIGURATION,
UPDATE_PATHS, UPDATE_PATHS,
UPDATE_TEAMS, UPDATE_SERVER_ORDER,
UPDATE_TAB_ORDER,
GET_LAST_ACTIVE,
GET_ORDERED_SERVERS,
GET_ORDERED_TABS_FOR_SERVER,
SERVERS_URL_MODIFIED,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
@@ -57,7 +54,7 @@ import CriticalErrorHandler from 'main/CriticalErrorHandler';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
import i18nManager from 'main/i18nManager'; import i18nManager from 'main/i18nManager';
import parseArgs from 'main/ParseArgs'; import parseArgs from 'main/ParseArgs';
import SettingsWindow from 'main/windows/settingsWindow'; import ServerManager from 'common/servers/serverManager';
import TrustedOriginsStore from 'main/trustedOrigins'; import TrustedOriginsStore from 'main/trustedOrigins';
import {refreshTrayImages, setupTray} from 'main/tray/tray'; import {refreshTrayImages, setupTray} from 'main/tray/tray';
import UserActivityMonitor from 'main/UserActivityMonitor'; import UserActivityMonitor from 'main/UserActivityMonitor';
@@ -84,7 +81,6 @@ import {
handleGetLocalConfiguration, handleGetLocalConfiguration,
handleUpdateTheme, handleUpdateTheme,
updateConfiguration, updateConfiguration,
handleUpdateTeams,
} from './config'; } from './config';
import { import {
handleMainWindowIsShown, handleMainWindowIsShown,
@@ -96,24 +92,26 @@ import {
handleOpenAppMenu, handleOpenAppMenu,
handleOpenTab, handleOpenTab,
handleQuit, handleQuit,
handleReloadConfig,
handleRemoveServerModal, handleRemoveServerModal,
handleSelectDownload, handleSelectDownload,
handleSwitchServer, handleSwitchServer,
handleSwitchTab, handleSwitchTab,
handleUpdateLastActive, handleUpdateLastActive,
handlePingDomain, handlePingDomain,
handleGetOrderedServers,
handleGetOrderedTabsForServer,
handleGetLastActive,
} from './intercom'; } from './intercom';
import { import {
clearAppCache, clearAppCache,
getDeeplinkingURL, getDeeplinkingURL,
handleUpdateMenuEvent, handleUpdateMenuEvent,
shouldShowTrayIcon, shouldShowTrayIcon,
updateServerInfos,
updateSpellCheckerLocales, updateSpellCheckerLocales,
wasUpdated, wasUpdated,
initCookieManager, initCookieManager,
migrateMacAppStore, migrateMacAppStore,
updateServerInfos,
} from './utils'; } from './utils';
export const mainProtocol = protocols?.[0]?.schemes?.[0]; export const mainProtocol = protocols?.[0]?.schemes?.[0];
@@ -153,7 +151,7 @@ export async function initialize() {
// initialization that should run once the app is ready // initialization that should run once the app is ready
initializeInterCommunicationEventListeners(); initializeInterCommunicationEventListeners();
initializeAfterAppReady(); await initializeAfterAppReady();
} }
// //
@@ -262,7 +260,6 @@ function initializeBeforeAppReady() {
} }
function initializeInterCommunicationEventListeners() { function initializeInterCommunicationEventListeners() {
ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig);
ipcMain.on(NOTIFY_MENTION, handleMentionNotification); ipcMain.on(NOTIFY_MENTION, handleMentionNotification);
ipcMain.handle('get-app-version', handleAppVersion); ipcMain.handle('get-app-version', handleAppVersion);
ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent);
@@ -280,17 +277,9 @@ function initializeInterCommunicationEventListeners() {
ipcMain.on(QUIT, handleQuit); ipcMain.on(QUIT, handleQuit);
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick);
ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal); ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal);
ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal); ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal);
ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal); ipcMain.on(SHOW_REMOVE_SERVER_MODAL, handleRemoveServerModal);
ipcMain.on(MAIN_WINDOW_SHOWN, handleMainWindowIsShown);
ipcMain.on(WINDOW_CLOSE, WindowManager.close);
ipcMain.on(WINDOW_MAXIMIZE, WindowManager.maximize);
ipcMain.on(WINDOW_MINIMIZE, WindowManager.minimize);
ipcMain.on(WINDOW_RESTORE, WindowManager.restore);
ipcMain.on(SHOW_SETTINGS_WINDOW, SettingsWindow.show);
ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages); ipcMain.handle(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES, () => session.defaultSession.availableSpellCheckerLanguages);
ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload); ipcMain.handle(GET_DOWNLOAD_LOCATION, handleSelectDownload);
ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload); ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload);
@@ -298,12 +287,24 @@ function initializeInterCommunicationEventListeners() {
ipcMain.handle(PING_DOMAIN, handlePingDomain); ipcMain.handle(PING_DOMAIN, handlePingDomain);
ipcMain.handle(GET_CONFIGURATION, handleGetConfiguration); ipcMain.handle(GET_CONFIGURATION, handleGetConfiguration);
ipcMain.handle(GET_LOCAL_CONFIGURATION, handleGetLocalConfiguration); ipcMain.handle(GET_LOCAL_CONFIGURATION, handleGetLocalConfiguration);
ipcMain.handle(UPDATE_TEAMS, handleUpdateTeams);
ipcMain.on(UPDATE_CONFIGURATION, updateConfiguration); 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.handle(GET_LAST_ACTIVE, handleGetLastActive);
ipcMain.handle(GET_ORDERED_SERVERS, handleGetOrderedServers);
ipcMain.handle(GET_ORDERED_TABS_FOR_SERVER, handleGetOrderedTabsForServer);
} }
function initializeAfterAppReady() { async function initializeAfterAppReady() {
updateServerInfos(Config.teams); ServerManager.reloadFromConfig();
updateServerInfos(ServerManager.getAllServers());
ServerManager.on(SERVERS_URL_MODIFIED, (serverIds?: string[]) => {
if (serverIds && serverIds.length) {
updateServerInfos(serverIds.map((srvId) => ServerManager.getServer(srvId)!));
}
});
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
const defaultSession = session.defaultSession; const defaultSession = session.defaultSession;

View File

@@ -1,10 +1,10 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import Config from 'common/config';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {getLocalURLString, getLocalPreload} from 'main/utils'; import {getLocalURLString, getLocalPreload} from 'main/utils';
import ServerManager from 'common/servers/serverManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
@@ -26,6 +26,17 @@ jest.mock('common/tabs/TabView', () => ({
getDefaultConfigTeamFromTeam: jest.fn(), getDefaultConfigTeamFromTeam: jest.fn(),
})); }));
jest.mock('main/notifications', () => ({})); jest.mock('main/notifications', () => ({}));
jest.mock('common/servers/serverManager', () => ({
setTabIsOpen: jest.fn(),
getAllServers: jest.fn(),
hasServers: jest.fn(),
addServer: jest.fn(),
editServer: jest.fn(),
removeServer: jest.fn(),
getServer: jest.fn(),
getTab: jest.fn(),
getLastActiveTabForServer: jest.fn(),
}));
jest.mock('main/utils', () => ({ jest.mock('main/utils', () => ({
getLocalPreload: jest.fn(), getLocalPreload: jest.fn(),
getLocalURLString: jest.fn(), getLocalURLString: jest.fn(),
@@ -43,9 +54,6 @@ jest.mock('main/windows/mainWindow', () => ({
})); }));
jest.mock('./app', () => ({})); jest.mock('./app', () => ({}));
jest.mock('./utils', () => ({
updateServerInfos: jest.fn(),
}));
const tabs = [ const tabs = [
{ {
@@ -66,6 +74,7 @@ const tabs = [
]; ];
const teams = [ const teams = [
{ {
id: 'server-1',
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
tabs, tabs,
@@ -74,53 +83,46 @@ const teams = [
describe('main/app/intercom', () => { describe('main/app/intercom', () => {
describe('handleCloseTab', () => { describe('handleCloseTab', () => {
beforeEach(() => {
Config.setServers.mockImplementation((value) => {
Config.teams = value;
});
Config.teams = JSON.parse(JSON.stringify(teams));
});
afterEach(() => {
delete Config.teams;
});
it('should close the specified tab and switch to the next open tab', () => { it('should close the specified tab and switch to the next open tab', () => {
handleCloseTab(null, 'server-1', 'tab-3'); ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}});
expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-2'); ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'});
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-3').isOpen).toBe(false); handleCloseTab(null, 'tab-3');
expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false);
expect(WindowManager.switchTab).toBeCalledWith('tab-2');
}); });
}); });
describe('handleOpenTab', () => { describe('handleOpenTab', () => {
beforeEach(() => {
Config.setServers.mockImplementation((value) => {
Config.teams = value;
});
Config.teams = JSON.parse(JSON.stringify(teams));
});
afterEach(() => {
delete Config.teams;
});
it('should open the specified tab', () => { it('should open the specified tab', () => {
handleOpenTab(null, 'server-1', 'tab-1'); handleOpenTab(null, 'tab-1');
expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-1'); expect(WindowManager.switchTab).toBeCalledWith('tab-1');
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-1').isOpen).toBe(true);
}); });
}); });
describe('handleNewServerModal', () => { describe('handleNewServerModal', () => {
let teamsCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html'); getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.setServers.mockImplementation((value) => { teamsCopy = JSON.parse(JSON.stringify(teams));
Config.teams = value; ServerManager.getAllServers.mockReturnValue([]);
ServerManager.addServer.mockImplementation(() => {
const newTeam = {
id: 'server-1',
name: 'new-team',
url: 'http://new-team.com',
tabs,
};
teamsCopy = [
...teamsCopy,
newTeam,
];
return newTeam;
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); ServerManager.hasServers.mockReturnValue(Boolean(teamsCopy.length));
getDefaultConfigTeamFromTeam.mockImplementation((team) => ({ getDefaultConfigTeamFromTeam.mockImplementation((team) => ({
...team, ...team,
@@ -128,10 +130,6 @@ describe('main/app/intercom', () => {
})); }));
}); });
afterEach(() => {
delete Config.teams;
});
it('should add new team to the config', async () => { it('should add new team to the config', async () => {
const promise = Promise.resolve({ const promise = Promise.resolve({
name: 'new-team', name: 'new-team',
@@ -141,29 +139,42 @@ describe('main/app/intercom', () => {
handleNewServerModal(); handleNewServerModal();
await promise; await promise;
expect(Config.teams).toContainEqual(expect.objectContaining({ expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team', name: 'new-team',
url: 'http://new-team.com', url: 'http://new-team.com',
tabs, tabs,
})); }));
expect(WindowManager.switchServer).toBeCalledWith('new-team', true); expect(WindowManager.switchServer).toBeCalledWith('server-1', true);
}); });
}); });
describe('handleEditServerModal', () => { describe('handleEditServerModal', () => {
let teamsCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html'); getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.setServers.mockImplementation((value) => { teamsCopy = JSON.parse(JSON.stringify(teams));
Config.teams = value; ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
return undefined;
}
return {...teamsCopy[0], toMattermostTeam: jest.fn()};
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); ServerManager.editServer.mockImplementation((id, team) => {
}); if (id !== teamsCopy[0].id) {
return;
afterEach(() => { }
delete Config.teams; const newTeam = {
...teamsCopy[0],
...team,
};
teamsCopy = [newTeam];
});
ServerManager.getAllServers.mockReturnValue(teamsCopy.map((team) => ({...team, toMattermostTeam: jest.fn()})));
}); });
it('should do nothing when the server cannot be found', () => { it('should do nothing when the server cannot be found', () => {
@@ -180,12 +191,14 @@ describe('main/app/intercom', () => {
handleEditServerModal(null, 'server-1'); handleEditServerModal(null, 'server-1');
await promise; await promise;
expect(Config.teams).not.toContainEqual(expect.objectContaining({ expect(teamsCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
tabs, tabs,
})); }));
expect(Config.teams).toContainEqual(expect.objectContaining({ expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'new-team', name: 'new-team',
url: 'http://new-team.com', url: 'http://new-team.com',
tabs, tabs,
@@ -194,19 +207,24 @@ describe('main/app/intercom', () => {
}); });
describe('handleRemoveServerModal', () => { describe('handleRemoveServerModal', () => {
let teamsCopy;
beforeEach(() => { beforeEach(() => {
getLocalURLString.mockReturnValue('/some/index.html'); getLocalURLString.mockReturnValue('/some/index.html');
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.setServers.mockImplementation((value) => { teamsCopy = JSON.parse(JSON.stringify(teams));
Config.teams = value; ServerManager.getServer.mockImplementation((id) => {
if (id !== teamsCopy[0].id) {
return undefined;
}
return teamsCopy[0];
}); });
Config.teams = JSON.parse(JSON.stringify(teams)); ServerManager.removeServer.mockImplementation(() => {
}); teamsCopy = [];
});
afterEach(() => { ServerManager.getAllServers.mockReturnValue(teamsCopy);
delete Config.teams;
}); });
it('should remove the existing team', async () => { it('should remove the existing team', async () => {
@@ -215,7 +233,8 @@ describe('main/app/intercom', () => {
handleRemoveServerModal(null, 'server-1'); handleRemoveServerModal(null, 'server-1');
await promise; await promise;
expect(Config.teams).not.toContainEqual(expect.objectContaining({ expect(teamsCopy).not.toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
tabs, tabs,
@@ -226,7 +245,8 @@ describe('main/app/intercom', () => {
const promise = Promise.resolve(false); const promise = Promise.resolve(false);
ModalManager.addModal.mockReturnValue(promise); ModalManager.addModal.mockReturnValue(promise);
expect(Config.teams).toContainEqual(expect.objectContaining({ expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
tabs, tabs,
@@ -234,7 +254,8 @@ describe('main/app/intercom', () => {
handleRemoveServerModal(null, 'server-1'); handleRemoveServerModal(null, 'server-1');
await promise; await promise;
expect(Config.teams).toContainEqual(expect.objectContaining({ expect(teamsCopy).toContainEqual(expect.objectContaining({
id: 'server-1',
name: 'server-1', name: 'server-1',
url: 'http://server-1.com', url: 'http://server-1.com',
tabs, tabs,
@@ -248,10 +269,8 @@ describe('main/app/intercom', () => {
getLocalPreload.mockReturnValue('/some/preload.js'); getLocalPreload.mockReturnValue('/some/preload.js');
MainWindow.get.mockReturnValue({}); MainWindow.get.mockReturnValue({});
Config.setServers.mockImplementation((value) => { ServerManager.getAllServers.mockReturnValue([]);
Config.teams = value; ServerManager.hasServers.mockReturnValue(false);
});
Config.teams = JSON.parse(JSON.stringify([]));
}); });
it('should show welcomeScreen modal', async () => { it('should show welcomeScreen modal', async () => {
@@ -270,18 +289,7 @@ describe('main/app/intercom', () => {
MainWindow.get.mockReturnValue({ MainWindow.get.mockReturnValue({
isVisible: () => true, isVisible: () => true,
}); });
ServerManager.hasServers.mockReturnValue(true);
Config.setServers.mockImplementation((value) => {
Config.teams = value;
});
Config.registryConfigData = {
teams: JSON.parse(JSON.stringify([{
name: 'test-team',
order: 0,
url: 'https://someurl.here',
}])),
};
Config.teams = JSON.parse(JSON.stringify(teams));
handleMainWindowIsShown(); handleMainWindowIsShown();
expect(ModalManager.addModal).not.toHaveBeenCalled(); expect(ModalManager.addModal).not.toHaveBeenCalled();

View File

@@ -3,33 +3,24 @@
import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron'; import {app, dialog, IpcMainEvent, IpcMainInvokeEvent, Menu} from 'electron';
import {Team, TeamWithIndex} from 'types/config'; import {Team, MattermostTeam} from 'types/config';
import {MentionData} from 'types/notification'; import {MentionData} from 'types/notification';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView';
import {ping} from 'common/utils/requests'; import {ping} from 'common/utils/requests';
import {displayMention} from 'main/notifications'; import {displayMention} from 'main/notifications';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
import ServerManager from 'common/servers/serverManager';
import ModalManager from 'main/views/modalManager'; import ModalManager from 'main/views/modalManager';
import ViewManager from 'main/views/viewManager';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import {handleAppBeforeQuit} from './app'; import {handleAppBeforeQuit} from './app';
import {updateServerInfos} from './utils';
const log = new Logger('App.Intercom'); const log = new Logger('App.Intercom');
export function handleReloadConfig() {
log.debug('handleReloadConfig');
Config.reload();
ViewManager.reloadConfiguration();
}
export function handleAppVersion() { export function handleAppVersion() {
return { return {
name: app.getName(), name: app.getName(),
@@ -44,52 +35,50 @@ export function handleQuit(e: IpcMainEvent, reason: string, stack: string) {
app.quit(); app.quit();
} }
export function handleSwitchServer(event: IpcMainEvent, serverName: string) { export function handleSwitchServer(event: IpcMainEvent, serverId: string) {
log.silly('handleSwitchServer', serverName); log.silly('handleSwitchServer', serverId);
WindowManager.switchServer(serverName); WindowManager.switchServer(serverId);
} }
export function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) { export function handleSwitchTab(event: IpcMainEvent, tabId: string) {
log.silly('handleSwitchTab', {serverName, tabName}); log.silly('handleSwitchTab', {tabId});
WindowManager.switchTab(serverName, tabName); WindowManager.switchTab(tabId);
} }
export function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) { export function handleCloseTab(event: IpcMainEvent, tabId: string) {
log.debug('handleCloseTab', {serverName, tabName}); log.debug('handleCloseTab', {tabId});
const teams = Config.teams; const tab = ServerManager.getTab(tabId);
teams.forEach((team) => { if (!tab) {
if (team.name === serverName) { return;
team.tabs.forEach((tab) => { }
if (tab.name === tabName) { ServerManager.setTabIsOpen(tabId, false);
tab.isOpen = false; const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id);
} WindowManager.switchTab(nextTab.id);
});
}
});
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name;
WindowManager.switchTab(serverName, nextTab);
Config.setServers(teams);
} }
export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) { export function handleOpenTab(event: IpcMainEvent, tabId: string) {
log.debug('handleOpenTab', {serverName, tabName}); log.debug('handleOpenTab', {tabId});
const teams = Config.teams; ServerManager.setTabIsOpen(tabId, true);
teams.forEach((team) => { WindowManager.switchTab(tabId);
if (team.name === serverName) {
team.tabs.forEach((tab) => {
if (tab.name === tabName) {
tab.isOpen = true;
}
});
}
});
WindowManager.switchTab(serverName, tabName);
Config.setServers(teams);
} }
export function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) { export function handleGetOrderedServers() {
return ServerManager.getOrderedServers().map((srv) => srv.toMattermostTeam());
}
export function handleGetOrderedTabsForServer(event: IpcMainInvokeEvent, serverId: string) {
return ServerManager.getOrderedTabsForServer(serverId).map((tab) => tab.toMattermostTab());
}
export function handleGetLastActive() {
const server = ServerManager.getCurrentServer();
const tab = ServerManager.getLastActiveTabForServer(server.id);
return {server: server.id, tab: tab.id};
}
function handleShowOnboardingScreens(showWelcomeScreen: boolean, showNewServerModal: boolean, mainWindowIsVisible: boolean) {
log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible}); log.debug('handleShowOnboardingScreens', {showWelcomeScreen, showNewServerModal, mainWindowIsVisible});
if (showWelcomeScreen) { if (showWelcomeScreen) {
@@ -117,8 +106,8 @@ export function handleMainWindowIsShown() {
// eslint-disable-next-line no-undef // eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment // eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore // @ts-ignore
const showWelcomeScreen = () => !(Boolean(__SKIP_ONBOARDING_SCREENS__) || Config.teams.length); const showWelcomeScreen = () => !(Boolean(__SKIP_ONBOARDING_SCREENS__) || ServerManager.hasServers());
const showNewServerModal = () => Config.teams.length === 0; const showNewServerModal = () => !ServerManager.hasServers();
/** /**
* The 2 lines above need to be functions, otherwise the mainWindow.once() callback from previous * The 2 lines above need to be functions, otherwise the mainWindow.once() callback from previous
@@ -127,7 +116,7 @@ export function handleMainWindowIsShown() {
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
log.debug('handleMainWindowIsShown', {configTeams: Config.teams, showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)}); log.debug('handleMainWindowIsShown', {showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)});
if (mainWindow?.isVisible()) { if (mainWindow?.isVisible()) {
handleShowOnboardingScreens(showWelcomeScreen(), showNewServerModal(), true); handleShowOnboardingScreens(showWelcomeScreen(), showNewServerModal(), true);
} else { } else {
@@ -148,16 +137,11 @@ export function handleNewServerModal() {
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
const modalPromise = ModalManager.addModal<TeamWithIndex[], Team>('newServer', html, preload, Config.teams.map((team, index) => ({...team, index})), mainWindow, Config.teams.length === 0); const modalPromise = ModalManager.addModal<MattermostTeam[], Team>('newServer', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers());
if (modalPromise) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = Config.teams; const newTeam = ServerManager.addServer(data);
const order = teams.length; WindowManager.switchServer(newTeam.id, true);
const newTeam = getDefaultConfigTeamFromTeam({...data, order});
teams.push(newTeam);
Config.setServers(teams);
updateServerInfos([newTeam]);
WindowManager.switchServer(newTeam.name, true);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {
@@ -169,8 +153,8 @@ export function handleNewServerModal() {
} }
} }
export function handleEditServerModal(e: IpcMainEvent, name: string) { export function handleEditServerModal(e: IpcMainEvent, id: string) {
log.debug('handleEditServerModal', name); log.debug('handleEditServerModal', id);
const html = getLocalURLString('editServer.html'); const html = getLocalURLString('editServer.html');
@@ -180,27 +164,21 @@ export function handleEditServerModal(e: IpcMainEvent, name: string) {
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
const serverIndex = Config.teams.findIndex((team) => team.name === name); const server = ServerManager.getServer(id);
if (serverIndex < 0) { if (!server) {
return; return;
} }
const modalPromise = ModalManager.addModal<{currentTeams: TeamWithIndex[]; team: TeamWithIndex}, Team>( const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>(
'editServer', 'editServer',
html, html,
preload, preload,
{ {
currentTeams: Config.teams.map((team, index) => ({...team, index})), currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()),
team: {...Config.teams[serverIndex], index: serverIndex}, team: server.toMattermostTeam(),
}, },
mainWindow); mainWindow);
if (modalPromise) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => {
const teams = Config.teams;
teams[serverIndex].name = data.name;
teams[serverIndex].url = data.url;
Config.setServers(teams);
updateServerInfos([teams[serverIndex]]);
}).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {
log.error(`there was an error in the edit server modal: ${e}`); log.error(`there was an error in the edit server modal: ${e}`);
@@ -211,34 +189,26 @@ export function handleEditServerModal(e: IpcMainEvent, name: string) {
} }
} }
export function handleRemoveServerModal(e: IpcMainEvent, name: string) { export function handleRemoveServerModal(e: IpcMainEvent, id: string) {
log.debug('handleRemoveServerModal', name); log.debug('handleRemoveServerModal', id);
const html = getLocalURLString('removeServer.html'); const html = getLocalURLString('removeServer.html');
const preload = getLocalPreload('desktopAPI.js'); const preload = getLocalPreload('desktopAPI.js');
const server = ServerManager.getServer(id);
if (!server) {
return;
}
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
const modalPromise = ModalManager.addModal<string, boolean>('removeServer', html, preload, name, mainWindow); const modalPromise = ModalManager.addModal<string, boolean>('removeServer', html, preload, server.name, mainWindow);
if (modalPromise) { if (modalPromise) {
modalPromise.then((remove) => { modalPromise.then((remove) => {
if (remove) { if (remove) {
const teams = Config.teams; ServerManager.removeServer(server.id);
const removedTeam = teams.findIndex((team) => team.name === name);
if (removedTeam < 0) {
return;
}
const removedOrder = teams[removedTeam].order;
teams.splice(removedTeam, 1);
teams.forEach((value) => {
if (value.order > removedOrder) {
value.order--;
}
});
Config.setServers(teams);
} }
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
@@ -262,16 +232,11 @@ export function handleWelcomeScreenModal() {
if (!mainWindow) { if (!mainWindow) {
return; return;
} }
const modalPromise = ModalManager.addModal<TeamWithIndex[], Team>('welcomeScreen', html, preload, Config.teams.map((team, index) => ({...team, index})), mainWindow, Config.teams.length === 0); const modalPromise = ModalManager.addModal<MattermostTeam[], MattermostTeam>('welcomeScreen', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers());
if (modalPromise) { if (modalPromise) {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = Config.teams; const newTeam = ServerManager.addServer(data);
const order = teams.length; WindowManager.switchServer(newTeam.id, true);
const newTeam = getDefaultConfigTeamFromTeam({...data, order});
teams.push(newTeam);
Config.setServers(teams);
updateServerInfos([newTeam]);
WindowManager.switchServer(newTeam.name, true);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
if (e) { if (e) {
@@ -314,17 +279,10 @@ export async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom:
return result.filePaths[0]; return result.filePaths[0];
} }
export function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewName: string) { export function handleUpdateLastActive(event: IpcMainEvent, tabId: string) {
log.debug('handleUpdateLastActive', {serverName, viewName}); log.debug('handleUpdateLastActive', {tabId});
const teams = Config.teams; ServerManager.updateLastActive(tabId);
teams.forEach((team) => {
if (team.name === serverName) {
const viewOrder = team?.tabs.find((tab) => tab.name === viewName)?.order || 0;
team.lastActiveTab = viewOrder;
}
});
Config.setServers(teams, teams.find((team) => team.name === serverName)?.order || 0);
} }
export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise<string> { export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise<string> {

View File

@@ -5,15 +5,11 @@ import fs from 'fs-extra';
import {dialog, screen} from 'electron'; import {dialog, screen} from 'electron';
import Config from 'common/config';
import JsonFileManager from 'common/JsonFileManager'; import JsonFileManager from 'common/JsonFileManager';
import {TAB_MESSAGING, TAB_FOCALBOARD, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import Utils from 'common/utils/util';
import {updatePaths} from 'main/constants'; import {updatePaths} from 'main/constants';
import {ServerInfo} from 'main/server/serverInfo';
import {getDeeplinkingURL, updateServerInfos, resizeScreen, migrateMacAppStore} from './utils'; import {getDeeplinkingURL, resizeScreen, migrateMacAppStore} from './utils';
jest.mock('fs-extra', () => ({ jest.mock('fs-extra', () => ({
readFileSync: jest.fn(), readFileSync: jest.fn(),
@@ -43,9 +39,6 @@ jest.mock('common/config', () => ({
setServers: jest.fn(), setServers: jest.fn(),
})); }));
jest.mock('common/JsonFileManager'); jest.mock('common/JsonFileManager');
jest.mock('common/utils/util', () => ({
isVersionGreaterThanOrEqualTo: jest.fn(),
}));
jest.mock('main/autoUpdater', () => ({})); jest.mock('main/autoUpdater', () => ({}));
jest.mock('main/constants', () => ({ jest.mock('main/constants', () => ({
@@ -56,9 +49,6 @@ jest.mock('main/i18nManager', () => ({
})); }));
jest.mock('main/menus/app', () => ({})); jest.mock('main/menus/app', () => ({}));
jest.mock('main/menus/tray', () => ({})); jest.mock('main/menus/tray', () => ({}));
jest.mock('main/server/serverInfo', () => ({
ServerInfo: jest.fn(),
}));
jest.mock('main/tray/tray', () => ({})); jest.mock('main/tray/tray', () => ({}));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/windows/mainWindow', () => ({})); jest.mock('main/windows/mainWindow', () => ({}));
@@ -69,107 +59,6 @@ jest.mock('./initialize', () => ({
})); }));
describe('main/app/utils', () => { describe('main/app/utils', () => {
describe('updateServerInfos', () => {
const tabs = [
{
name: TAB_MESSAGING,
order: 0,
isOpen: true,
},
{
name: TAB_FOCALBOARD,
order: 2,
},
{
name: TAB_PLAYBOOKS,
order: 1,
},
];
const teams = [
{
name: 'server-1',
url: 'http://server-1.com',
tabs,
},
];
beforeEach(() => {
Utils.isVersionGreaterThanOrEqualTo.mockImplementation((version) => version === '6.0.0');
Config.setServers.mockImplementation((value) => {
Config.teams = value;
});
const teamsCopy = JSON.parse(JSON.stringify(teams));
Config.teams = teamsCopy;
});
afterEach(() => {
delete Config.teams;
});
it('should open all tabs', async () => {
ServerInfo.mockReturnValue({promise: {
name: 'server-1',
siteURL: 'http://server-1.com',
serverVersion: '6.0.0',
hasPlaybooks: true,
hasFocalboard: true,
}});
updateServerInfos(Config.teams);
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBe(true);
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBe(true);
});
it('should open only playbooks', async () => {
ServerInfo.mockReturnValue({promise: {
name: 'server-1',
siteURL: 'http://server-1.com',
serverVersion: '6.0.0',
hasPlaybooks: true,
hasFocalboard: false,
}});
updateServerInfos(Config.teams);
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBe(true);
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBeUndefined();
});
it('should open none when server version is too old', async () => {
ServerInfo.mockReturnValue({promise: {
name: 'server-1',
siteURL: 'http://server-1.com',
serverVersion: '5.0.0',
hasPlaybooks: true,
hasFocalboard: true,
}});
updateServerInfos(Config.teams);
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_PLAYBOOKS).isOpen).toBeUndefined();
expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === TAB_FOCALBOARD).isOpen).toBeUndefined();
});
it('should update server URL using site URL', async () => {
ServerInfo.mockReturnValue({promise: {
name: 'server-1',
siteURL: 'http://server-2.com',
serverVersion: '6.0.0',
hasPlaybooks: true,
hasFocalboard: true,
}});
updateServerInfos(Config.teams);
await new Promise(setImmediate); // workaround since Promise.all seems to not let me wait here
expect(Config.teams.find((team) => team.name === 'server-1').url).toBe('http://server-2.com');
});
});
describe('getDeeplinkingURL', () => { describe('getDeeplinkingURL', () => {
it('should return undefined if deeplinking URL is not last argument', () => { it('should return undefined if deeplinking URL is not last argument', () => {
expect(getDeeplinkingURL(['mattermost', 'mattermost://server-1.com', '--oops'])).toBeUndefined(); expect(getDeeplinkingURL(['mattermost', 'mattermost://server-1.com', '--oops'])).toBeUndefined();

View File

@@ -7,17 +7,16 @@ import fs from 'fs-extra';
import {app, BrowserWindow, Menu, Rectangle, Session, session, dialog, nativeImage, screen} from 'electron'; import {app, BrowserWindow, Menu, Rectangle, Session, session, dialog, nativeImage, screen} from 'electron';
import {MigrationInfo, TeamWithTabs} from 'types/config'; import {MigrationInfo} from 'types/config';
import {RemoteInfo} from 'types/server'; import {RemoteInfo} from 'types/server';
import {Boundaries} from 'types/utils'; import {Boundaries} from 'types/utils';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import JsonFileManager from 'common/JsonFileManager'; import JsonFileManager from 'common/JsonFileManager';
import ServerManager from 'common/servers/serverManager';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import Utils from 'common/utils/util';
import {APP_MENU_WILL_CLOSE} from 'common/communication'; import {APP_MENU_WILL_CLOSE} from 'common/communication';
import updateManager from 'main/autoUpdater'; import updateManager from 'main/autoUpdater';
@@ -52,59 +51,6 @@ export function updateSpellCheckerLocales() {
} }
} }
export function updateServerInfos(teams: TeamWithTabs[]) {
log.silly('app.utils.updateServerInfos');
const serverInfos: Array<Promise<RemoteInfo | string | undefined>> = [];
teams.forEach((team) => {
const serverInfo = new ServerInfo(new MattermostServer(team));
serverInfos.push(serverInfo.promise);
});
Promise.all(serverInfos).then((data: Array<RemoteInfo | string | undefined>) => {
const teams = Config.teams;
let hasUpdates = false;
teams.forEach((team) => {
hasUpdates = hasUpdates || updateServerURL(data, team);
hasUpdates = hasUpdates || openExtraTabs(data, team);
});
if (hasUpdates) {
Config.setServers(teams);
}
}).catch((reason: any) => {
log.error('Error getting server infos', reason);
});
}
function updateServerURL(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
if (remoteInfo && remoteInfo.siteURL && team.url !== remoteInfo.siteURL) {
team.url = remoteInfo.siteURL;
return true;
}
return false;
}
function openExtraTabs(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
let hasUpdates = false;
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
if (remoteInfo) {
team.tabs.forEach((tab) => {
if (tab.name !== TAB_MESSAGING && remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0')) {
if (tab.name === TAB_PLAYBOOKS && remoteInfo.hasPlaybooks && typeof tab.isOpen === 'undefined') {
log.info(`opening ${team.name}___${tab.name} on hasPlaybooks`);
tab.isOpen = true;
hasUpdates = true;
}
if (tab.name === TAB_FOCALBOARD && remoteInfo.hasFocalboard && typeof tab.isOpen === 'undefined') {
log.info(`opening ${team.name}___${tab.name} on hasFocalboard`);
tab.isOpen = true;
hasUpdates = true;
}
}
});
}
return hasUpdates;
}
export function handleUpdateMenuEvent() { export function handleUpdateMenuEvent() {
log.debug('handleUpdateMenuEvent'); log.debug('handleUpdateMenuEvent');
@@ -117,7 +63,7 @@ export function handleUpdateMenuEvent() {
// set up context menu for tray icon // set up context menu for tray icon
if (shouldShowTrayIcon()) { if (shouldShowTrayIcon()) {
const tMenu = createTrayMenu(Config.data!); const tMenu = createTrayMenu();
setTrayMenu(tMenu); setTrayMenu(tMenu);
} }
} }
@@ -291,3 +237,18 @@ export function migrateMacAppStore() {
log.error('MAS: An error occurred importing the existing configuration', e); log.error('MAS: An error occurred importing the existing configuration', e);
} }
} }
export async function updateServerInfos(servers: MattermostServer[]) {
const map: Map<string, RemoteInfo> = new Map();
await Promise.all(servers.map((srv) => {
const serverInfo = new ServerInfo(srv);
return serverInfo.fetchRemoteInfo().
then((data) => {
map.set(srv.id, data);
}).
catch((error) => {
log.warn('Could not get server info for', srv.name, error);
});
}));
ServerManager.updateRemoteInfos(map);
}

View File

@@ -4,14 +4,11 @@
import events from 'events'; import events from 'events';
import {ipcMain} from 'electron'; import {ipcMain} from 'electron';
import {Logger} from 'common/log';
import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED, UPDATE_DROPDOWN_MENTIONS} from 'common/communication'; import {UPDATE_MENTIONS, UPDATE_TRAY, UPDATE_BADGE, SESSION_EXPIRED, UPDATE_DROPDOWN_MENTIONS} from 'common/communication';
import ServerManager from 'common/servers/serverManager';
import WindowManager from './windows/windowManager'; import WindowManager from './windows/windowManager';
const log = new Logger('AppState');
const status = { const status = {
unreads: new Map<string, boolean>(), unreads: new Map<string, boolean>(),
mentions: new Map<string, number>(), mentions: new Map<string, number>(),
@@ -19,13 +16,13 @@ const status = {
emitter: new events.EventEmitter(), emitter: new events.EventEmitter(),
}; };
const emitMentions = (serverName: string) => { const emitMentions = (viewId: string) => {
const newMentions = getMentions(serverName); const newMentions = getMentions(viewId);
const newUnreads = getUnreads(serverName); const newUnreads = getUnreads(viewId);
const isExpired = getIsExpired(serverName); const isExpired = getIsExpired(viewId);
WindowManager.sendToRenderer(UPDATE_MENTIONS, serverName, newMentions, newUnreads, isExpired); WindowManager.sendToRenderer(UPDATE_MENTIONS, viewId, newMentions, newUnreads, isExpired);
log.silly('emitMentions', {serverName, isExpired, newMentions, newUnreads}); ServerManager.getViewLog(viewId, 'AppState').silly('emitMentions', {isExpired, newMentions, newUnreads});
emitStatus(); emitStatus();
}; };
@@ -50,17 +47,17 @@ const emitStatus = () => {
emitDropdown(status.expired, status.mentions, status.unreads); emitDropdown(status.expired, status.mentions, status.unreads);
}; };
export const updateMentions = (serverName: string, mentions: number, unreads?: boolean) => { export const updateMentions = (viewId: string, mentions: number, unreads?: boolean) => {
if (typeof unreads !== 'undefined') { if (typeof unreads !== 'undefined') {
status.unreads.set(serverName, Boolean(unreads)); status.unreads.set(viewId, Boolean(unreads));
} }
status.mentions.set(serverName, mentions || 0); status.mentions.set(viewId, mentions || 0);
emitMentions(serverName); emitMentions(viewId);
}; };
export const updateUnreads = (serverName: string, unreads: boolean) => { export const updateUnreads = (viewId: string, unreads: boolean) => {
status.unreads.set(serverName, Boolean(unreads)); status.unreads.set(viewId, Boolean(unreads));
emitMentions(serverName); emitMentions(viewId);
}; };
export const updateBadge = () => { export const updateBadge = () => {
@@ -70,16 +67,16 @@ export const updateBadge = () => {
emitBadge(expired, mentions, unreads); emitBadge(expired, mentions, unreads);
}; };
const getUnreads = (serverName: string) => { const getUnreads = (viewId: string) => {
return status.unreads.get(serverName) || false; return status.unreads.get(viewId) || false;
}; };
const getMentions = (serverName: string) => { const getMentions = (viewId: string) => {
return status.mentions.get(serverName) || 0; // this might be undefined as a way to tell that we don't know as it might need to login still. return status.mentions.get(viewId) || 0; // this might be undefined as a way to tell that we don't know as it might need to login still.
}; };
const getIsExpired = (serverName: string) => { const getIsExpired = (viewId: string) => {
return status.expired.get(serverName) || false; return status.expired.get(viewId) || false;
}; };
const totalMentions = () => { const totalMentions = () => {
@@ -111,19 +108,19 @@ const anyExpired = () => {
// add any other event emitter methods if needed // add any other event emitter methods if needed
export const on = status.emitter.on.bind(status.emitter); export const on = status.emitter.on.bind(status.emitter);
const setSessionExpired = (serverName: string, expired: boolean) => { const setSessionExpired = (viewId: string, expired: boolean) => {
const isExpired = Boolean(expired); const isExpired = Boolean(expired);
const old = status.expired.get(serverName); const old = status.expired.get(viewId);
status.expired.set(serverName, isExpired); status.expired.set(viewId, isExpired);
if (typeof old !== 'undefined' && old !== isExpired) { if (typeof old !== 'undefined' && old !== isExpired) {
emitTray(); emitTray();
} }
emitMentions(serverName); emitMentions(viewId);
}; };
ipcMain.on(SESSION_EXPIRED, (event, isExpired, serverName) => { ipcMain.on(SESSION_EXPIRED, (event, isExpired, viewId) => {
if (isExpired) { if (isExpired) {
log.debug('SESSION_EXPIRED', {isExpired, serverName}); ServerManager.getViewLog(viewId, 'AppState').debug('SESSION_EXPIRED', isExpired);
} }
setSessionExpired(serverName, isExpired); setSessionExpired(viewId, isExpired);
}); });

View File

@@ -4,7 +4,7 @@
import {ElectronLog} from 'electron-log'; import {ElectronLog} from 'electron-log';
import {DiagnosticStepResponse} from 'types/diagnostics'; import {DiagnosticStepResponse} from 'types/diagnostics';
import Config from 'common/config'; import ServerManager from 'common/servers/serverManager';
import DiagnosticsStep from '../DiagnosticStep'; import DiagnosticsStep from '../DiagnosticStep';
@@ -15,7 +15,7 @@ const stepDescriptiveName = 'serverConnectivity';
const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => { const run = async (logger: ElectronLog): Promise<DiagnosticStepResponse> => {
try { try {
const teams = Config.teams || []; const teams = ServerManager.getAllServers();
await Promise.all(teams.map(async (team) => { await Promise.all(teams.map(async (team) => {
logger.debug('Pinging server: ', team.url); logger.debug('Pinging server: ', team.url);

View File

@@ -6,7 +6,7 @@
import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state'; import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import WindowManager from 'main/windows/windowManager'; import ServerManager from 'common/servers/serverManager';
import {createTemplate} from './app'; import {createTemplate} from './app';
@@ -49,13 +49,18 @@ jest.mock('macos-notification-state', () => ({
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(), localizeMessage: jest.fn(),
})); }));
jest.mock('common/servers/serverManager', () => ({
hasServers: jest.fn(),
getCurrentServer: jest.fn(),
getOrderedServers: jest.fn(),
getOrderedTabsForServer: jest.fn(),
}));
jest.mock('main/diagnostics', () => ({})); jest.mock('main/diagnostics', () => ({}));
jest.mock('main/downloadsManager', () => ({ jest.mock('main/downloadsManager', () => ({
hasDownloads: jest.fn(), hasDownloads: jest.fn(),
})); }));
jest.mock('main/views/viewManager', () => ({})); jest.mock('main/views/viewManager', () => ({}));
jest.mock('main/windows/windowManager', () => ({ jest.mock('main/windows/windowManager', () => ({
getCurrentTeamName: jest.fn(),
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('main/windows/settingsWindow', () => ({}));
@@ -66,54 +71,42 @@ jest.mock('common/tabs/TabView', () => ({
describe('main/menus/app', () => { describe('main/menus/app', () => {
const config = { const config = {
enableServerManagement: true, enableServerManagement: true,
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,
}],
helpLink: 'http://link-to-help.site.com', helpLink: 'http://link-to-help.site.com',
}; };
const servers = [
{
id: 'server-1',
name: 'example',
url: 'http://example.com',
},
{
id: 'server-2',
name: 'github',
url: 'https:/ /github.com/',
},
];
const tabs = [
{
id: 'tab-1',
name: 'TAB_MESSAGING',
isOpen: true,
},
{
id: 'tab-2',
name: 'TAB_FOCALBOARD',
isOpen: true,
},
{
id: 'tab-3',
name: 'TAB_PLAYBOOKS',
isOpen: true,
},
];
beforeEach(() => { beforeEach(() => {
ServerManager.getCurrentServer.mockReturnValue(servers[0]);
ServerManager.getOrderedServers.mockReturnValue(servers);
ServerManager.getOrderedTabsForServer.mockReturnValue(tabs);
getDarwinDoNotDisturb.mockReturnValue(false); getDarwinDoNotDisturb.mockReturnValue(false);
}); });
@@ -192,6 +185,7 @@ describe('main/menus/app', () => {
return id; return id;
} }
}); });
ServerManager.hasServers.mockReturnValue(true);
const menu = createTemplate(config); const menu = createTemplate(config);
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
@@ -209,6 +203,7 @@ describe('main/menus/app', () => {
return ''; return '';
} }
}); });
ServerManager.hasServers.mockReturnValue(true);
const modifiedConfig = { const modifiedConfig = {
...config, ...config,
enableServerManagement: false, enableServerManagement: false,
@@ -230,11 +225,8 @@ describe('main/menus/app', () => {
return ''; return '';
} }
}); });
const modifiedConfig = { ServerManager.hasServers.mockReturnValue(false);
...config, const menu = createTemplate(config);
teams: [],
};
const menu = createTemplate(modifiedConfig);
const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File');
const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server');
expect(signInOption).toBe(undefined); expect(signInOption).toBe(undefined);
@@ -247,31 +239,27 @@ describe('main/menus/app', () => {
} }
return id; return id;
}); });
const modifiedConfig = { const modifiedServers = [...Array(15).keys()].map((key) => ({
teams: [...Array(15).keys()].map((key) => ({ id: `server-${key}`,
name: `server-${key}`, name: `server-${key}`,
url: `http://server-${key}.com`, url: `http://server-${key}.com`,
order: (key + 5) % 15, }));
lastActiveTab: 0, const modifiedTabs = [
tab: [ {
{ id: 'tab-1',
name: 'TAB_MESSAGING', type: 'TAB_MESSAGING',
isOpen: true, isOpen: true,
}, },
], ];
})), ServerManager.getOrderedServers.mockReturnValue(modifiedServers);
}; ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs);
const menu = createTemplate(modifiedConfig); const menu = createTemplate(config);
const windowMenu = menu.find((item) => item.label === '&Window'); const windowMenu = menu.find((item) => item.label === '&Window');
for (let i = 10; i < 15; i++) { for (let i = 0; i < 9; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`); const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`);
expect(menuItem).not.toBe(undefined); expect(menuItem).not.toBe(undefined);
} }
for (let i = 0; i < 4; i++) { for (let i = 9; i < 15; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`);
expect(menuItem).not.toBe(undefined);
}
for (let i = 4; i < 10; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`); const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`);
expect(menuItem).toBe(undefined); expect(menuItem).toBe(undefined);
} }
@@ -287,31 +275,21 @@ describe('main/menus/app', () => {
} }
return id; return id;
}); });
WindowManager.getCurrentTeamName.mockImplementation(() => config.teams[0].name); ServerManager.getCurrentServer.mockImplementation(() => ({id: servers[0].id}));
const modifiedConfig = { const modifiedTabs = [...Array(15).keys()].map((key) => ({
teams: [ id: `tab-${key}`,
{ type: `tab-${key}`,
...config.teams[0], isOpen: true,
tabs: [...Array(15).keys()].map((key) => ({ }));
name: `tab-${key}`, ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs);
isOpen: true, const menu = createTemplate(config);
order: (key + 5) % 15,
})),
},
],
};
const menu = createTemplate(modifiedConfig);
const windowMenu = menu.find((item) => item.label === '&Window'); const windowMenu = menu.find((item) => item.label === '&Window');
for (let i = 10; i < 15; i++) { for (let i = 0; i < 9; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`); const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`);
expect(menuItem).not.toBe(undefined); expect(menuItem).not.toBe(undefined);
} }
for (let i = 0; i < 4; i++) { for (let i = 9; i < 15; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`);
expect(menuItem).not.toBe(undefined);
}
for (let i = 4; i < 10; i++) {
const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`); const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`);
expect(menuItem).toBe(undefined); expect(menuItem).toBe(undefined);
} }

View File

@@ -12,6 +12,7 @@ import {getTabDisplayName, TabType} from 'common/tabs/TabView';
import {Config} from 'common/config'; import {Config} from 'common/config';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import ServerManager from 'common/servers/serverManager';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
import {UpdateManager} from 'main/autoUpdater'; import {UpdateManager} from 'main/autoUpdater';
import downloadsManager from 'main/downloadsManager'; import downloadsManager from 'main/downloadsManager';
@@ -49,7 +50,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}, },
}); });
if (config.enableServerManagement === true && config.teams.length > 0) { if (config.enableServerManagement === true && ServerManager.hasServers()) {
platformAppMenu.push({ platformAppMenu.push({
label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'), label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'),
click() { click() {
@@ -231,7 +232,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
}], }],
}); });
const teams = config.teams || []; const teams = ServerManager.getOrderedServers();
const windowMenu = { const windowMenu = {
id: 'window', id: 'window',
label: localizeMessage('main.menus.app.window', '&Window'), label: localizeMessage('main.menus.app.window', '&Window'),
@@ -251,29 +252,29 @@ export function createTemplate(config: Config, updateManager: UpdateManager) {
label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'), label: isMac ? localizeMessage('main.menus.app.window.closeWindow', 'Close Window') : localizeMessage('main.menus.app.window.close', 'Close'),
accelerator: 'CmdOrCtrl+W', accelerator: 'CmdOrCtrl+W',
}, separatorItem, }, separatorItem,
...(config.teams.length ? [{ ...(ServerManager.hasServers() ? [{
label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'), label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'),
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`,
click() { click() {
ipcMain.emit(OPEN_TEAMS_DROPDOWN); ipcMain.emit(OPEN_TEAMS_DROPDOWN);
}, },
}] : []), }] : []),
...teams.sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).map((team, i) => { ...teams.slice(0, 9).map((team, i) => {
const items = []; const items = [];
items.push({ items.push({
label: team.name, label: team.name,
accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`,
click() { click() {
WindowManager.switchServer(team.name); WindowManager.switchServer(team.id);
}, },
}); });
if (WindowManager.getCurrentTeamName() === team.name) { if (ServerManager.getCurrentServer().id === team.id) {
team.tabs.filter((tab) => tab.isOpen).sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).forEach((tab, i) => { ServerManager.getOrderedTabsForServer(team.id).slice(0, 9).forEach((tab, i) => {
items.push({ items.push({
label: ` ${localizeMessage(`common.tabs.${tab.name}`, getTabDisplayName(tab.name as TabType))}`, label: ` ${localizeMessage(`common.tabs.${tab.type}`, getTabDisplayName(tab.type as TabType))}`,
accelerator: `CmdOrCtrl+${i + 1}`, accelerator: `CmdOrCtrl+${i + 1}`,
click() { click() {
WindowManager.switchTab(team.name, tab.name); WindowManager.switchTab(tab.id);
}, },
}); });
}); });

View File

@@ -3,41 +3,35 @@
'use strict'; 'use strict';
import ServerManager from 'common/servers/serverManager';
import {createTemplate} from './tray'; import {createTemplate} from './tray';
jest.mock('main/i18nManager', () => ({ jest.mock('main/i18nManager', () => ({
localizeMessage: jest.fn(), localizeMessage: jest.fn(),
})); }));
jest.mock('common/servers/serverManager', () => ({
getOrderedServers: jest.fn(),
}));
jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('main/windows/settingsWindow', () => ({}));
jest.mock('main/windows/windowManager', () => ({})); jest.mock('main/windows/windowManager', () => ({}));
describe('main/menus/tray', () => { describe('main/menus/tray', () => {
it('should show the first 9 servers (using order)', () => { it('should show the first 9 servers (using order)', () => {
const config = { const servers = [...Array(15).keys()].map((key) => ({
teams: [...Array(15).keys()].map((key) => ({ id: `server-${key}`,
name: `server-${key}`, name: `server-${key}`,
url: `http://server-${key}.com`, url: `http://server-${key}.com`,
order: (key + 5) % 15, }));
lastActiveTab: 0, ServerManager.getOrderedServers.mockReturnValue(servers);
tab: [ const menu = createTemplate();
{ for (let i = 0; i < 9; i++) {
name: 'TAB_MESSAGING',
isOpen: true,
},
],
})),
};
const menu = createTemplate(config);
for (let i = 10; i < 15; i++) {
const menuItem = menu.find((item) => item.label === `server-${i}`); const menuItem = menu.find((item) => item.label === `server-${i}`);
expect(menuItem).not.toBe(undefined); expect(menuItem).not.toBe(undefined);
} }
for (let i = 0; i < 4; i++) { for (let i = 9; i < 15; i++) {
const menuItem = menu.find((item) => item.label === `server-${i}`);
expect(menuItem).not.toBe(undefined);
}
for (let i = 4; i < 10; i++) {
const menuItem = menu.find((item) => item.label === `server-${i}`); const menuItem = menu.find((item) => item.label === `server-${i}`);
expect(menuItem).toBe(undefined); expect(menuItem).toBe(undefined);
} }

View File

@@ -4,20 +4,20 @@
'use strict'; 'use strict';
import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron'; import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron';
import {CombinedConfig} from 'types/config';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import ServerManager from 'common/servers/serverManager';
import SettingsWindow from 'main/windows/settingsWindow'; import SettingsWindow from 'main/windows/settingsWindow';
export function createTemplate(config: CombinedConfig) { export function createTemplate() {
const teams = config.teams; const teams = ServerManager.getOrderedServers();
const template = [ const template = [
...teams.sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).map((team) => { ...teams.slice(0, 9).map((team) => {
return { return {
label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name, label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name,
click: () => { click: () => {
WindowManager.switchServer(team.name); WindowManager.switchServer(team.id);
}, },
}; };
}), { }), {
@@ -36,7 +36,7 @@ export function createTemplate(config: CombinedConfig) {
return template; return template;
} }
export function createMenu(config: CombinedConfig) { export function createMenu() {
// Electron is enforcing certain variables that it doesn't need // Electron is enforcing certain variables that it doesn't need
return Menu.buildFromTemplate(createTemplate(config) as Array<MenuItemConstructorOptions | MenuItem>); return Menu.buildFromTemplate(createTemplate() as Array<MenuItemConstructorOptions | MenuItem>);
} }

View File

@@ -11,7 +11,6 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state
import {PLAY_SOUND} from 'common/communication'; import {PLAY_SOUND} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {TAB_MESSAGING} from 'common/tabs/TabView';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
@@ -249,7 +248,7 @@ describe('main/notifications', () => {
); );
const mention = mentions.find((m) => m.body === 'mention_click_body'); const mention = mentions.find((m) => m.body === 'mention_click_body');
mention.value.click(); mention.value.click();
expect(WindowManager.switchTab).toBeCalledWith('server_name', TAB_MESSAGING); expect(WindowManager.switchTab).toHaveBeenCalledWith('server_id');
}); });
it('linux/windows - should not flash frame when config item is not set', () => { it('linux/windows - should not flash frame when config item is not set', () => {

View File

@@ -10,7 +10,6 @@ import {MentionData} from 'types/notification';
import Config from 'common/config'; import Config from 'common/config';
import {PLAY_SOUND} from 'common/communication'; import {PLAY_SOUND} from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TAB_MESSAGING} from 'common/tabs/TabView';
import ViewManager from '../views/viewManager'; import ViewManager from '../views/viewManager';
import MainWindow from '../windows/mainWindow'; import MainWindow from '../windows/mainWindow';
@@ -76,7 +75,7 @@ export function displayMention(title: string, body: string, channel: {id: string
mention.on('click', () => { mention.on('click', () => {
log.debug('notification click', serverName, mention); log.debug('notification click', serverName, mention);
if (serverName) { if (serverName) {
WindowManager.switchTab(serverName, TAB_MESSAGING); WindowManager.switchTab(view.id);
webcontents.send('notification-clicked', {channel, teamId, url}); webcontents.send('notification-clicked', {channel, teamId, url});
} }
}); });

View File

@@ -9,8 +9,6 @@ import {ipcRenderer, contextBridge} from 'electron';
import { import {
GET_LANGUAGE_INFORMATION, GET_LANGUAGE_INFORMATION,
QUIT, QUIT,
GET_VIEW_NAME,
GET_VIEW_WEBCONTENTS_ID,
OPEN_APP_MENU, OPEN_APP_MENU,
CLOSE_TEAMS_DROPDOWN, CLOSE_TEAMS_DROPDOWN,
OPEN_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN,
@@ -29,7 +27,6 @@ import {
HISTORY, HISTORY,
CHECK_FOR_UPDATES, CHECK_FOR_UPDATES,
UPDATE_CONFIGURATION, UPDATE_CONFIGURATION,
UPDATE_TEAMS,
GET_CONFIGURATION, GET_CONFIGURATION,
GET_DARK_MODE, GET_DARK_MODE,
REQUEST_HAS_DOWNLOADS, REQUEST_HAS_DOWNLOADS,
@@ -85,17 +82,16 @@ import {
LOADING_SCREEN_ANIMATION_FINISHED, LOADING_SCREEN_ANIMATION_FINISHED,
TOGGLE_LOADING_SCREEN_VISIBILITY, TOGGLE_LOADING_SCREEN_VISIBILITY,
DOWNLOADS_DROPDOWN_FOCUSED, DOWNLOADS_DROPDOWN_FOCUSED,
UPDATE_SERVER_ORDER,
UPDATE_TAB_ORDER,
GET_LAST_ACTIVE,
GET_ORDERED_SERVERS,
GET_ORDERED_TABS_FOR_SERVER,
SERVERS_UPDATE,
} from 'common/communication'; } from 'common/communication';
console.log('Preload initialized'); console.log('Preload initialized');
if (process.env.NODE_ENV === 'test') {
contextBridge.exposeInMainWorld('testHelper', {
getViewName: () => ipcRenderer.invoke(GET_VIEW_NAME),
getWebContentsId: () => ipcRenderer.invoke(GET_VIEW_WEBCONTENTS_ID),
});
}
contextBridge.exposeInMainWorld('process', { contextBridge.exposeInMainWorld('process', {
platform: process.platform, platform: process.platform,
env: { env: {
@@ -117,14 +113,14 @@ contextBridge.exposeInMainWorld('desktop', {
openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU), openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU),
closeTeamsDropdown: () => ipcRenderer.send(CLOSE_TEAMS_DROPDOWN), closeTeamsDropdown: () => ipcRenderer.send(CLOSE_TEAMS_DROPDOWN),
openTeamsDropdown: () => ipcRenderer.send(OPEN_TEAMS_DROPDOWN), openTeamsDropdown: () => ipcRenderer.send(OPEN_TEAMS_DROPDOWN),
switchTab: (serverName, tabName) => ipcRenderer.send(SWITCH_TAB, serverName, tabName), switchTab: (tabId) => ipcRenderer.send(SWITCH_TAB, tabId),
closeTab: (serverName, tabName) => ipcRenderer.send(CLOSE_TAB, serverName, tabName), closeTab: (tabId) => ipcRenderer.send(CLOSE_TAB, tabId),
closeWindow: () => ipcRenderer.send(WINDOW_CLOSE), closeWindow: () => ipcRenderer.send(WINDOW_CLOSE),
minimizeWindow: () => ipcRenderer.send(WINDOW_MINIMIZE), minimizeWindow: () => ipcRenderer.send(WINDOW_MINIMIZE),
maximizeWindow: () => ipcRenderer.send(WINDOW_MAXIMIZE), maximizeWindow: () => ipcRenderer.send(WINDOW_MAXIMIZE),
restoreWindow: () => ipcRenderer.send(WINDOW_RESTORE), restoreWindow: () => ipcRenderer.send(WINDOW_RESTORE),
doubleClickOnWindow: (windowName) => ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, windowName), doubleClickOnWindow: (windowName) => ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, windowName),
focusBrowserView: () => ipcRenderer.send(FOCUS_BROWSERVIEW), focusCurrentView: () => ipcRenderer.send(FOCUS_BROWSERVIEW),
reloadCurrentView: () => ipcRenderer.send(RELOAD_CURRENT_VIEW), reloadCurrentView: () => ipcRenderer.send(RELOAD_CURRENT_VIEW),
closeDownloadsDropdown: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN), closeDownloadsDropdown: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN),
closeDownloadsDropdownMenu: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU), closeDownloadsDropdownMenu: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU),
@@ -133,25 +129,31 @@ contextBridge.exposeInMainWorld('desktop', {
checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES), checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES),
updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems), updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems),
updateTeams: (updatedTeams) => ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams), updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder),
getConfiguration: (option) => ipcRenderer.invoke(GET_CONFIGURATION, option), updateTabOrder: (serverId, tabOrder) => ipcRenderer.send(UPDATE_TAB_ORDER, serverId, tabOrder),
getLastActive: () => ipcRenderer.invoke(GET_LAST_ACTIVE),
getOrderedServers: () => ipcRenderer.invoke(GET_ORDERED_SERVERS),
getOrderedTabsForServer: (serverId) => ipcRenderer.invoke(GET_ORDERED_TABS_FOR_SERVER, serverId),
onUpdateServers: (listener) => ipcRenderer.on(SERVERS_UPDATE, () => listener()),
getConfiguration: () => ipcRenderer.invoke(GET_CONFIGURATION),
getVersion: () => ipcRenderer.invoke('get-app-version'), getVersion: () => ipcRenderer.invoke('get-app-version'),
getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE), getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE),
requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS), requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS),
getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS), getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS),
getAvailableSpellCheckerLanguages: () => ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES), getAvailableSpellCheckerLanguages: () => ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_LANGUAGES),
getAvailableLanguages: () => ipcRenderer.invoke(GET_AVAILABLE_LANGUAGES), getAvailableLanguages: () => ipcRenderer.invoke(GET_AVAILABLE_LANGUAGES),
getLocalConfiguration: (option) => ipcRenderer.invoke(GET_LOCAL_CONFIGURATION, option), getLocalConfiguration: () => ipcRenderer.invoke(GET_LOCAL_CONFIGURATION),
getDownloadLocation: (downloadLocation) => ipcRenderer.invoke(GET_DOWNLOAD_LOCATION, downloadLocation), getDownloadLocation: (downloadLocation) => ipcRenderer.invoke(GET_DOWNLOAD_LOCATION, downloadLocation),
getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION), getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION),
onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()), onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()),
onReloadConfiguration: (listener) => ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()), onReloadConfiguration: (listener) => ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()),
onDarkModeChange: (listener) => ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => listener(darkMode)), onDarkModeChange: (listener) => ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => listener(darkMode)),
onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewName, retry, err, loadUrl) => listener(viewName, retry, err, loadUrl)), onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)),
onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewName) => listener(viewName)), onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)),
onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewName, err, loadUrl) => listener(viewName, err, loadUrl)), onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewId, err, loadUrl) => listener(viewId, err, loadUrl)),
onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverName, tabName) => listener(serverName, tabName)), onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverId, tabId) => listener(serverId, tabId)),
onMaximizeChange: (listener) => ipcRenderer.on(MAXIMIZE_CHANGE, (_, maximize) => listener(maximize)), onMaximizeChange: (listener) => ipcRenderer.on(MAXIMIZE_CHANGE, (_, maximize) => listener(maximize)),
onEnterFullScreen: (listener) => ipcRenderer.on('enter-full-screen', () => listener()), onEnterFullScreen: (listener) => ipcRenderer.on('enter-full-screen', () => listener()),
onLeaveFullScreen: (listener) => ipcRenderer.on('leave-full-screen', () => listener()), onLeaveFullScreen: (listener) => ipcRenderer.on('leave-full-screen', () => listener()),
@@ -195,10 +197,10 @@ contextBridge.exposeInMainWorld('desktop', {
serverDropdown: { serverDropdown: {
requestInfo: () => ipcRenderer.send(REQUEST_TEAMS_DROPDOWN_INFO), requestInfo: () => ipcRenderer.send(REQUEST_TEAMS_DROPDOWN_INFO),
sendSize: (width, height) => ipcRenderer.send(RECEIVE_DROPDOWN_MENU_SIZE, width, height), sendSize: (width, height) => ipcRenderer.send(RECEIVE_DROPDOWN_MENU_SIZE, width, height),
switchServer: (server) => ipcRenderer.send(SWITCH_SERVER, server), switchServer: (serverId) => ipcRenderer.send(SWITCH_SERVER, serverId),
showNewServerModal: () => ipcRenderer.send(SHOW_NEW_SERVER_MODAL), showNewServerModal: () => ipcRenderer.send(SHOW_NEW_SERVER_MODAL),
showEditServerModal: (serverName) => ipcRenderer.send(SHOW_EDIT_SERVER_MODAL, serverName), showEditServerModal: (serverId) => ipcRenderer.send(SHOW_EDIT_SERVER_MODAL, serverId),
showRemoveServerModal: (serverName) => ipcRenderer.send(SHOW_REMOVE_SERVER_MODAL, serverName), showRemoveServerModal: (serverId) => ipcRenderer.send(SHOW_REMOVE_SERVER_MODAL, serverId),
onUpdateServerDropdown: (listener) => ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (_, onUpdateServerDropdown: (listener) => ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (_,
teams, teams,

View File

@@ -25,8 +25,7 @@ import {
BROWSER_HISTORY_PUSH, BROWSER_HISTORY_PUSH,
APP_LOGGED_IN, APP_LOGGED_IN,
APP_LOGGED_OUT, APP_LOGGED_OUT,
GET_VIEW_NAME, GET_VIEW_INFO_FOR_TEST,
GET_VIEW_WEBCONTENTS_ID,
DISPATCH_GET_DESKTOP_SOURCES, DISPATCH_GET_DESKTOP_SOURCES,
DESKTOP_SOURCES_RESULT, DESKTOP_SOURCES_RESULT,
VIEW_FINISHED_RESIZING, VIEW_FINISHED_RESIZING,
@@ -45,15 +44,14 @@ const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
let appVersion; let appVersion;
let appName; let appName;
let sessionExpired; let sessionExpired;
let viewName; let viewId;
let shouldSendNotifications; let shouldSendNotifications;
console.log('Preload initialized'); console.log('Preload initialized');
if (process.env.NODE_ENV === 'test') { if (process.env.NODE_ENV === 'test') {
contextBridge.exposeInMainWorld('testHelper', { contextBridge.exposeInMainWorld('testHelper', {
getViewName: () => ipcRenderer.invoke(GET_VIEW_NAME), getViewInfoForTest: () => ipcRenderer.invoke(GET_VIEW_INFO_FOR_TEST),
getWebContentsId: () => ipcRenderer.invoke(GET_VIEW_WEBCONTENTS_ID),
}); });
} }
@@ -92,8 +90,8 @@ window.addEventListener('load', () => {
return; return;
} }
watchReactAppUntilInitialized(() => { watchReactAppUntilInitialized(() => {
ipcRenderer.send(REACT_APP_INITIALIZED, viewName); ipcRenderer.send(REACT_APP_INITIALIZED, viewId);
ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewName); ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId);
}); });
}); });
@@ -152,27 +150,27 @@ window.addEventListener('message', ({origin, data = {}} = {}) => {
} }
case 'browser-history-push': { case 'browser-history-push': {
const {path} = message; const {path} = message;
ipcRenderer.send(BROWSER_HISTORY_PUSH, viewName, path); ipcRenderer.send(BROWSER_HISTORY_PUSH, viewId, path);
break; break;
} }
case 'history-button': { case 'history-button': {
ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewName); ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId);
break; break;
} }
case 'get-desktop-sources': { case 'get-desktop-sources': {
ipcRenderer.send(DISPATCH_GET_DESKTOP_SOURCES, viewName, message); ipcRenderer.send(DISPATCH_GET_DESKTOP_SOURCES, viewId, message);
break; break;
} }
case CALLS_JOIN_CALL: { case CALLS_JOIN_CALL: {
ipcRenderer.send(CALLS_JOIN_CALL, viewName, message); ipcRenderer.send(CALLS_JOIN_CALL, viewId, message);
break; break;
} }
case CALLS_WIDGET_SHARE_SCREEN: { case CALLS_WIDGET_SHARE_SCREEN: {
ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, viewName, message); ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, viewId, message);
break; break;
} }
case CALLS_LEAVE_CALL: { case CALLS_LEAVE_CALL: {
ipcRenderer.send(CALLS_LEAVE_CALL, viewName, message); ipcRenderer.send(CALLS_LEAVE_CALL, viewId, message);
break; break;
} }
} }
@@ -202,12 +200,12 @@ const findUnread = (favicon) => {
const result = document.getElementsByClassName(classPair); const result = document.getElementsByClassName(classPair);
return result && result.length > 0; return result && result.length > 0;
}); });
ipcRenderer.send(UNREAD_RESULT, favicon, viewName, isUnread); ipcRenderer.send(UNREAD_RESULT, favicon, viewId, isUnread);
}; };
ipcRenderer.on(IS_UNREAD, (event, favicon, server) => { ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
if (typeof viewName === 'undefined') { if (typeof viewId === 'undefined') {
viewName = server; viewId = server;
} }
if (isReactAppInitialized()) { if (isReactAppInitialized()) {
findUnread(favicon); findUnread(favicon);
@@ -219,13 +217,13 @@ ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
}); });
ipcRenderer.on(SET_VIEW_OPTIONS, (_, name, shouldNotify) => { ipcRenderer.on(SET_VIEW_OPTIONS, (_, name, shouldNotify) => {
viewName = name; viewId = name;
shouldSendNotifications = shouldNotify; shouldSendNotifications = shouldNotify;
}); });
function getUnreadCount() { function getUnreadCount() {
// LHS not found => Log out => Count should be 0, but session may be expired. // LHS not found => Log out => Count should be 0, but session may be expired.
if (typeof viewName !== 'undefined') { if (typeof viewId !== 'undefined') {
let isExpired; let isExpired;
if (document.getElementById('sidebar-left') === null) { if (document.getElementById('sidebar-left') === null) {
const extraParam = (new URLSearchParams(window.location.search)).get('extra'); const extraParam = (new URLSearchParams(window.location.search)).get('extra');
@@ -235,7 +233,7 @@ function getUnreadCount() {
} }
if (isExpired !== sessionExpired) { if (isExpired !== sessionExpired) {
sessionExpired = isExpired; sessionExpired = isExpired;
ipcRenderer.send(SESSION_EXPIRED, sessionExpired, viewName); ipcRenderer.send(SESSION_EXPIRED, sessionExpired, viewId);
} }
} }
} }
@@ -308,10 +306,10 @@ ipcRenderer.on(BROWSER_HISTORY_BUTTON, (event, enableBack, enableForward) => {
window.addEventListener('storage', (e) => { window.addEventListener('storage', (e) => {
if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) { if (e.key === '__login__' && e.storageArea === localStorage && e.newValue) {
ipcRenderer.send(APP_LOGGED_IN, viewName); ipcRenderer.send(APP_LOGGED_IN, viewId);
} }
if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) { if (e.key === '__logout__' && e.storageArea === localStorage && e.newValue) {
ipcRenderer.send(APP_LOGGED_OUT, viewName); ipcRenderer.send(APP_LOGGED_OUT, viewId);
} }
}); });

View File

@@ -4,72 +4,56 @@
import {ClientConfig, RemoteInfo} from 'types/server'; import {ClientConfig, RemoteInfo} from 'types/server';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import {Logger} from 'common/log';
import {getServerAPI} from './serverAPI'; import {getServerAPI} from './serverAPI';
const log = new Logger('ServerInfo');
export class ServerInfo { export class ServerInfo {
server: MattermostServer; private server: MattermostServer;
remoteInfo: RemoteInfo; private remoteInfo: RemoteInfo;
promise: Promise<RemoteInfo | string | undefined>;
onRetrievedRemoteInfo?: (result?: RemoteInfo | string) => void;
constructor(server: MattermostServer) { constructor(server: MattermostServer) {
this.server = server; this.server = server;
this.remoteInfo = {name: server.name}; this.remoteInfo = {};
this.promise = new Promise<RemoteInfo | string | undefined>((resolve) => {
this.onRetrievedRemoteInfo = resolve;
});
this.getRemoteInfo();
} }
getRemoteInfo = () => { fetchRemoteInfo = async () => {
getServerAPI<ClientConfig>( await this.getRemoteInfo<ClientConfig>(
new URL(`${this.server.url.toString()}/api/v4/config/client?format=old`), new URL(`${this.server.url.toString()}/api/v4/config/client?format=old`),
false,
this.onGetConfig, this.onGetConfig,
this.onRetrievedRemoteInfo, );
this.onRetrievedRemoteInfo); await this.getRemoteInfo<Array<{id: string; version: string}>>(
getServerAPI<Array<{id: string; version: string}>>(
new URL(`${this.server.url.toString()}/api/v4/plugins/webapp`), new URL(`${this.server.url.toString()}/api/v4/plugins/webapp`),
false,
this.onGetPlugins, this.onGetPlugins,
this.onRetrievedRemoteInfo, );
this.onRetrievedRemoteInfo);
return this.remoteInfo;
} }
onGetConfig = (data: ClientConfig) => { private getRemoteInfo = <T>(
url: URL,
callback: (data: T) => void,
) => {
return new Promise<void>((resolve, reject) => {
getServerAPI<T>(
url,
false,
(data: T) => {
callback(data);
resolve();
},
() => reject(new Error('Aborted')),
(error: Error) => reject(error));
});
}
private onGetConfig = (data: ClientConfig) => {
this.remoteInfo.serverVersion = data.Version; this.remoteInfo.serverVersion = data.Version;
this.remoteInfo.siteURL = data.SiteURL; this.remoteInfo.siteURL = data.SiteURL;
this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.BuildBoards === 'true'; this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.BuildBoards === 'true';
this.trySendRemoteInfo();
} }
onGetPlugins = (data: Array<{id: string; version: string}>) => { private onGetPlugins = (data: Array<{id: string; version: string}>) => {
this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.some((plugin) => plugin.id === 'focalboard'); this.remoteInfo.hasFocalboard = this.remoteInfo.hasFocalboard || data.some((plugin) => plugin.id === 'focalboard');
this.remoteInfo.hasPlaybooks = data.some((plugin) => plugin.id === 'playbooks'); this.remoteInfo.hasPlaybooks = data.some((plugin) => plugin.id === 'playbooks');
this.trySendRemoteInfo();
}
trySendRemoteInfo = () => {
log.debug('trySendRemoteInfo', this.server.name, this.remoteInfo);
if (this.isRemoteInfoRetrieved()) {
this.onRetrievedRemoteInfo?.(this.remoteInfo);
}
}
isRemoteInfoRetrieved = () => {
return !(
typeof this.remoteInfo.serverVersion === 'undefined' ||
typeof this.remoteInfo.hasFocalboard === 'undefined' ||
typeof this.remoteInfo.hasPlaybooks === 'undefined'
);
} }
} }

View File

@@ -180,7 +180,7 @@ describe('main/views/MattermostView', () => {
await expect(promise).rejects.toThrow(error); await expect(promise).rejects.toThrow(error);
expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object)); expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object));
expect(mattermostView.loadRetry).not.toBeCalled(); expect(mattermostView.loadRetry).not.toBeCalled();
expect(WindowManager.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.name, expect.any(String), expect.any(String)); expect(WindowManager.sendToRenderer).toBeCalledWith(LOAD_FAILED, mattermostView.tab.id, expect.any(String), expect.any(String));
expect(mattermostView.status).toBe(-1); expect(mattermostView.status).toBe(-1);
jest.runAllTimers(); jest.runAllTimers();
expect(retryInBackgroundFn).toBeCalled(); expect(retryInBackgroundFn).toBeCalled();
@@ -374,14 +374,7 @@ describe('main/views/MattermostView', () => {
const mattermostView = new MattermostView(tabView, {}, {}); const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn(); mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy(); mattermostView.destroy();
expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.name, 0, false); expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.id, 0, false);
});
it('should destroy context menu', () => {
const mattermostView = new MattermostView(tabView, {}, {});
mattermostView.view.webContents.destroy = jest.fn();
mattermostView.destroy();
expect(contextMenu.dispose).toBeCalled();
}); });
it('should clear outstanding timeouts', () => { it('should clear outstanding timeouts', () => {
@@ -479,12 +472,12 @@ describe('main/views/MattermostView', () => {
it('should parse mentions from title', () => { it('should parse mentions from title', () => {
mattermostView.updateMentionsFromTitle('(7) Mattermost'); mattermostView.updateMentionsFromTitle('(7) Mattermost');
expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.name, 7); expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.id, 7);
}); });
it('should parse unreads from title', () => { it('should parse unreads from title', () => {
mattermostView.updateMentionsFromTitle('* Mattermost'); mattermostView.updateMentionsFromTitle('* Mattermost');
expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.name, 0); expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.id, 0);
}); });
}); });
}); });

View File

@@ -18,12 +18,12 @@ import {
SET_VIEW_OPTIONS, SET_VIEW_OPTIONS,
LOADSCREEN_END, LOADSCREEN_END,
BROWSER_HISTORY_BUTTON, BROWSER_HISTORY_BUTTON,
SERVERS_URL_MODIFIED,
} from 'common/communication'; } from 'common/communication';
import ServerManager from 'common/servers/serverManager';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TabView} from 'common/tabs/TabView'; import {TabView} from 'common/tabs/TabView';
import {MattermostServer} from 'common/servers/MattermostServer';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
@@ -45,7 +45,6 @@ const titleParser = /(\((\d+)\) )?(\* )?/g;
export class MattermostView extends EventEmitter { export class MattermostView extends EventEmitter {
tab: TabView; tab: TabView;
serverInfo: ServerInfo;
isVisible: boolean; isVisible: boolean;
private log: Logger; private log: Logger;
@@ -60,10 +59,9 @@ export class MattermostView extends EventEmitter {
private maxRetries: number; private maxRetries: number;
private altPressStatus: boolean; private altPressStatus: boolean;
constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) { constructor(tab: TabView, options: BrowserViewConstructorOptions) {
super(); super();
this.tab = tab; this.tab = tab;
this.serverInfo = serverInfo;
const preload = getLocalPreload('preload.js'); const preload = getLocalPreload('preload.js');
this.options = Object.assign({}, options); this.options = Object.assign({}, options);
@@ -81,7 +79,7 @@ export class MattermostView extends EventEmitter {
this.view = new BrowserView(this.options); this.view = new BrowserView(this.options);
this.resetLoadingStatus(); this.resetLoadingStatus();
this.log = new Logger(this.name, 'MattermostView'); this.log = ServerManager.getViewLog(this.id, 'MattermostView');
this.log.verbose('View created'); this.log.verbose('View created');
this.view.webContents.on('did-finish-load', this.handleDidFinishLoad); this.view.webContents.on('did-finish-load', this.handleDidFinishLoad);
@@ -103,10 +101,12 @@ export class MattermostView extends EventEmitter {
MainWindow.get()?.on('blur', () => { MainWindow.get()?.on('blur', () => {
this.altPressStatus = false; this.altPressStatus = false;
}); });
ServerManager.on(SERVERS_URL_MODIFIED, this.handleServerWasModified);
} }
get name() { get id() {
return this.tab.name; return this.tab.id;
} }
get isAtRoot() { get isAtRoot() {
return this.atRoot; return this.atRoot;
@@ -121,17 +121,6 @@ export class MattermostView extends EventEmitter {
return this.view.webContents.id; return this.view.webContents.id;
} }
updateServerInfo = (srv: MattermostServer) => {
let reload;
if (srv.url.toString() !== this.tab.server.url.toString()) {
reload = () => this.reload();
}
this.tab.server = srv;
this.serverInfo = new ServerInfo(srv);
this.view.webContents.send(SET_VIEW_OPTIONS, this.tab.name, this.tab.shouldNotify);
reload?.();
}
onLogin = (loggedIn: boolean) => { onLogin = (loggedIn: boolean) => {
if (this.isLoggedIn === loggedIn) { if (this.isLoggedIn === loggedIn) {
return; return;
@@ -170,16 +159,6 @@ export class MattermostView extends EventEmitter {
this.view.webContents.send(BROWSER_HISTORY_BUTTON, this.view.webContents.canGoBack(), this.view.webContents.canGoForward()); this.view.webContents.send(BROWSER_HISTORY_BUTTON, this.view.webContents.canGoBack(), this.view.webContents.canGoForward());
} }
updateTabView = (tab: TabView) => {
let reload;
if (tab.url.toString() !== this.tab.url.toString()) {
reload = () => this.reload();
}
this.tab = tab;
this.view.webContents.send(SET_VIEW_OPTIONS, this.name, this.tab.shouldNotify);
reload?.();
}
load = (someURL?: URL | string) => { load = (someURL?: URL | string) => {
if (!this.tab) { if (!this.tab) {
return; return;
@@ -201,9 +180,9 @@ export class MattermostView extends EventEmitter {
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
if (err.code && err.code.startsWith('ERR_CERT')) { if (err.code && err.code.startsWith('ERR_CERT')) {
WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); this.emit(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.log.info('Invalid certificate, stop retrying until the user decides what to do.', err); this.log.info(`Invalid certificate, stop retrying until the user decides what to do: ${err}.`);
this.status = Status.ERROR; this.status = Status.ERROR;
return; return;
} }
@@ -258,7 +237,7 @@ export class MattermostView extends EventEmitter {
destroy = () => { destroy = () => {
WebContentsEventManager.removeWebContentsListeners(this.webContentsId); WebContentsEventManager.removeWebContentsListeners(this.webContentsId);
appState.updateMentions(this.name, 0, false); appState.updateMentions(this.id, 0, false);
MainWindow.get()?.removeBrowserView(this.view); MainWindow.get()?.removeBrowserView(this.view);
// workaround to eliminate zombie processes // workaround to eliminate zombie processes
@@ -274,8 +253,6 @@ export class MattermostView extends EventEmitter {
if (this.removeLoading) { if (this.removeLoading) {
clearTimeout(this.removeLoading); clearTimeout(this.removeLoading);
} }
this.contextMenu.dispose();
} }
/** /**
@@ -307,7 +284,7 @@ export class MattermostView extends EventEmitter {
if (timedout) { if (timedout) {
this.log.verbose('timeout expired will show the browserview'); this.log.verbose('timeout expired will show the browserview');
this.emit(LOADSCREEN_END, this.name); this.emit(LOADSCREEN_END, this.id);
} }
clearTimeout(this.removeLoading); clearTimeout(this.removeLoading);
delete this.removeLoading; delete this.removeLoading;
@@ -376,13 +353,13 @@ export class MattermostView extends EventEmitter {
const results = resultsIterator.next(); // we are only interested in the first set const results = resultsIterator.next(); // we are only interested in the first set
const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0; const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0;
appState.updateMentions(this.name, mentions); appState.updateMentions(this.id, mentions);
} }
// if favicon is null, it will affect appState, but won't be memoized // if favicon is null, it will affect appState, but won't be memoized
private findUnreadState = (favicon: string | null) => { private findUnreadState = (favicon: string | null) => {
try { try {
this.view.webContents.send(IS_UNREAD, favicon, this.name); this.view.webContents.send(IS_UNREAD, favicon, this.id);
} catch (err: any) { } catch (err: any) {
this.log.error('There was an error trying to request the unread state', err); this.log.error('There was an error trying to request the unread state', err);
} }
@@ -417,8 +394,8 @@ export class MattermostView extends EventEmitter {
if (this.maxRetries-- > 0) { if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err); this.loadRetry(loadURL, err);
} else { } else {
WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.name, 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 establish a connection with ${loadURL}, will continue to retry in the background`, err);
this.status = Status.ERROR; this.status = Status.ERROR;
this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL); this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL);
@@ -442,14 +419,14 @@ export class MattermostView extends EventEmitter {
private loadRetry = (loadURL: string, err: Error) => { private loadRetry = (loadURL: string, err: Error) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL); this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
WindowManager.sendToRenderer(LOAD_RETRY, this.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_RETRY, this.id, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
this.log.info(`failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`); this.log.info(`failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
} }
private loadSuccess = (loadURL: string) => { private loadSuccess = (loadURL: string) => {
return () => { return () => {
this.log.verbose(`finished loading ${loadURL}`); this.log.verbose(`finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.name); WindowManager.sendToRenderer(LOAD_SUCCESS, this.id);
this.maxRetries = MAX_SERVER_RETRIES; this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) { if (this.status === Status.LOADING) {
this.updateMentionsFromTitle(this.view.webContents.getTitle()); this.updateMentionsFromTitle(this.view.webContents.getTitle());
@@ -457,7 +434,7 @@ export class MattermostView extends EventEmitter {
} }
this.status = Status.WAITING_MM; this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true); this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.name, loadURL); this.emit(LOAD_SUCCESS, this.id, loadURL);
const mainWindow = MainWindow.get(); const mainWindow = MainWindow.get();
if (mainWindow) { if (mainWindow) {
this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL))); this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL)));
@@ -470,7 +447,7 @@ export class MattermostView extends EventEmitter {
*/ */
private handleDidFinishLoad = () => { private handleDidFinishLoad = () => {
this.log.debug('did-finish-load', this.name); this.log.debug('did-finish-load');
// wait for screen to truly finish loading before sending the message down // wait for screen to truly finish loading before sending the message down
const timeout = setInterval(() => { const timeout = setInterval(() => {
@@ -480,7 +457,7 @@ export class MattermostView extends EventEmitter {
if (!this.view.webContents.isLoading()) { if (!this.view.webContents.isLoading()) {
try { try {
this.view.webContents.send(SET_VIEW_OPTIONS, this.name, this.tab.shouldNotify); this.view.webContents.send(SET_VIEW_OPTIONS, this.id, this.tab.shouldNotify);
clearTimeout(timeout); clearTimeout(timeout);
} catch (e) { } catch (e) {
this.log.error('failed to send view options to view'); this.log.error('failed to send view options to view');
@@ -492,12 +469,17 @@ export class MattermostView extends EventEmitter {
private handleDidNavigate = (event: Event, url: string) => { private handleDidNavigate = (event: Event, url: string) => {
this.log.debug('handleDidNavigate', url); this.log.debug('handleDidNavigate', url);
const mainWindow = MainWindow.get();
if (!mainWindow) {
return;
}
if (shouldHaveBackBar(this.tab.url || '', url)) { if (shouldHaveBackBar(this.tab.url || '', url)) {
this.setBounds(getWindowBoundaries(MainWindow.get()!, true)); this.setBounds(getWindowBoundaries(mainWindow, true));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true);
this.log.debug('show back button'); this.log.debug('show back button');
} else { } else {
this.setBounds(getWindowBoundaries(MainWindow.get()!)); this.setBounds(getWindowBoundaries(mainWindow));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
this.log.debug('hide back button'); this.log.debug('hide back button');
} }
@@ -511,4 +493,10 @@ export class MattermostView extends EventEmitter {
this.emit(UPDATE_TARGET_URL); this.emit(UPDATE_TARGET_URL);
} }
} }
private handleServerWasModified = (serverIds: string) => {
if (serverIds.includes(this.tab.server.id)) {
this.reload();
}
}
} }

View File

@@ -36,6 +36,11 @@ jest.mock('../windows/windowManager', () => ({
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
})); }));
jest.mock('common/servers/serverManager', () => ({
on: jest.fn(),
getOrderedServers: jest.fn().mockReturnValue([]),
}));
describe('main/views/teamDropdownView', () => { describe('main/views/teamDropdownView', () => {
describe('getBounds', () => { describe('getBounds', () => {
beforeEach(() => { beforeEach(() => {
@@ -62,52 +67,4 @@ describe('main/views/teamDropdownView', () => {
teamDropdownView.handleClose(); teamDropdownView.handleClose();
expect(teamDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)}); expect(teamDropdownView.view.setBounds).toBeCalledWith({width: 0, height: 0, x: expect.any(Number), y: expect.any(Number)});
}); });
describe('addGpoToTeams', () => {
it('should return teams with "isGPO": false when no config.registryTeams exist', () => {
const teamDropdownView = new TeamDropdownView();
const teams = [{
name: 'team-1',
url: 'https://mattermost.team-1.com',
}, {
name: 'team-2',
url: 'https://mattermost.team-2.com',
}];
const registryTeams = [];
expect(teamDropdownView.addGpoToTeams(teams, registryTeams)).toStrictEqual([{
name: 'team-1',
url: 'https://mattermost.team-1.com',
isGpo: false,
}, {
name: 'team-2',
url: 'https://mattermost.team-2.com',
isGpo: false,
}]);
});
it('should return teams with "isGPO": true if they exist in config.registryTeams', () => {
const teamDropdownView = new TeamDropdownView();
const teams = [{
name: 'team-1',
url: 'https://mattermost.team-1.com',
}, {
name: 'team-2',
url: 'https://mattermost.team-2.com',
}];
const registryTeams = [{
name: 'team-1',
url: 'https://mattermost.team-1.com',
}];
expect(teamDropdownView.addGpoToTeams(teams, registryTeams)).toStrictEqual([{
name: 'team-1',
url: 'https://mattermost.team-1.com',
isGpo: true,
}, {
name: 'team-2',
url: 'https://mattermost.team-2.com',
isGpo: false,
}]);
});
});
}); });

View File

@@ -3,7 +3,7 @@
import {BrowserView, ipcMain, IpcMainEvent} from 'electron'; import {BrowserView, ipcMain, IpcMainEvent} from 'electron';
import {CombinedConfig, Team, TeamWithTabs, TeamWithTabsAndGpo} from 'types/config'; import {CombinedConfig, MattermostTeam} from 'types/config';
import { import {
CLOSE_TEAMS_DROPDOWN, CLOSE_TEAMS_DROPDOWN,
@@ -14,10 +14,12 @@ import {
REQUEST_TEAMS_DROPDOWN_INFO, REQUEST_TEAMS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SET_ACTIVE_VIEW, SET_ACTIVE_VIEW,
SERVERS_UPDATE,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
import ServerManager from 'common/servers/serverManager';
import {getLocalPreload, getLocalURLString} from 'main/utils'; import {getLocalPreload, getLocalURLString} from 'main/utils';
@@ -30,7 +32,7 @@ const log = new Logger('TeamDropdownView');
export default class TeamDropdownView { export default class TeamDropdownView {
view: BrowserView; view: BrowserView;
bounds?: Electron.Rectangle; bounds?: Electron.Rectangle;
teams: TeamWithTabsAndGpo[]; teams: MattermostTeam[];
activeTeam?: string; activeTeam?: string;
darkMode: boolean; darkMode: boolean;
enableServerManagement?: boolean; enableServerManagement?: boolean;
@@ -42,7 +44,8 @@ export default class TeamDropdownView {
isOpen: boolean; isOpen: boolean;
constructor() { constructor() {
this.teams = this.addGpoToTeams(Config.teams, []); this.teams = this.getOrderedTeams();
this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined);
this.darkMode = Config.darkMode; this.darkMode = Config.darkMode;
this.enableServerManagement = Config.enableServerManagement; this.enableServerManagement = Config.enableServerManagement;
this.isOpen = false; this.isOpen = false;
@@ -69,6 +72,17 @@ export default class TeamDropdownView {
ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize);
ipcMain.on(SET_ACTIVE_VIEW, this.updateActiveTeam); ipcMain.on(SET_ACTIVE_VIEW, this.updateActiveTeam);
AppState.on(UPDATE_DROPDOWN_MENTIONS, this.updateMentions); AppState.on(UPDATE_DROPDOWN_MENTIONS, this.updateMentions);
ServerManager.on(SERVERS_UPDATE, this.updateServers);
}
private getOrderedTeams = () => {
return ServerManager.getOrderedServers().map((team) => team.toMattermostTeam());
}
updateServers = () => {
this.teams = this.getOrderedTeams();
this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined);
} }
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => { updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
@@ -76,23 +90,33 @@ export default class TeamDropdownView {
this.darkMode = config.darkMode; this.darkMode = config.darkMode;
this.enableServerManagement = config.enableServerManagement; this.enableServerManagement = config.enableServerManagement;
this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0;
this.updateDropdown(); this.updateDropdown();
} }
updateActiveTeam = (event: IpcMainEvent, name: string) => { updateActiveTeam = (event: IpcMainEvent, serverId: string) => {
log.silly('updateActiveTeam', {name}); log.silly('updateActiveTeam', {serverId});
this.activeTeam = name; this.activeTeam = serverId;
this.updateDropdown(); this.updateDropdown();
} }
private reduceNotifications = <T>(items: Map<string, T>, modifier: (base?: T, value?: T) => T) => {
return [...items.keys()].reduce((map, key) => {
const view = ServerManager.getTab(key);
if (!view) {
return map;
}
map.set(view.server.id, modifier(map.get(view.server.id), items.get(key)));
return map;
}, new Map());
}
updateMentions = (expired: Map<string, boolean>, mentions: Map<string, number>, unreads: Map<string, boolean>) => { updateMentions = (expired: Map<string, boolean>, mentions: Map<string, number>, unreads: Map<string, boolean>) => {
log.silly('updateMentions', {expired, mentions, unreads}); log.silly('updateMentions', {expired, mentions, unreads});
this.unreads = unreads; this.unreads = this.reduceNotifications(unreads, (base, value) => base || value || false);
this.mentions = mentions; this.mentions = this.reduceNotifications(mentions, (base, value) => (base ?? 0) + (value ?? 0));
this.expired = expired; this.expired = this.reduceNotifications(expired, (base, value) => base || value || false);
this.updateDropdown(); this.updateDropdown();
} }
@@ -164,16 +188,4 @@ export default class TeamDropdownView {
// @ts-ignore // @ts-ignore
this.view.webContents.destroy(); this.view.webContents.destroy();
} }
addGpoToTeams = (teams: TeamWithTabs[], registryTeams: Team[]): TeamWithTabsAndGpo[] => {
if (!registryTeams || registryTeams.length === 0) {
return teams.map((team) => ({...team, isGpo: false}));
}
return teams.map((team) => {
return {
...team,
isGpo: registryTeams.some((regTeam) => regTeam!.url === team!.url),
};
});
}
} }

View File

@@ -4,13 +4,12 @@
/* eslint-disable max-lines */ /* eslint-disable max-lines */
'use strict'; 'use strict';
import {dialog, ipcMain} from 'electron'; import {dialog} from 'electron';
import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication'; import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication';
import Config from 'common/config'; import {TAB_MESSAGING} from 'common/tabs/TabView';
import {MattermostServer} from 'common/servers/MattermostServer'; import ServerManager from 'common/servers/serverManager';
import {getTabViewName} from 'common/tabs/TabView'; import urlUtils from 'common/utils/url';
import {equalUrlsIgnoringSubpath} from 'common/utils/url';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
@@ -71,32 +70,61 @@ jest.mock('main/views/loadingScreen', () => ({
jest.mock('main/windows/mainWindow', () => ({ jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
})); }));
jest.mock('common/servers/serverManager', () => ({
getCurrentServer: jest.fn(),
getOrderedTabsForServer: jest.fn(),
getAllServers: jest.fn(),
hasServers: jest.fn(),
getLastActiveServer: jest.fn(),
getLastActiveTabForServer: jest.fn(),
lookupTabByURL: jest.fn(),
getRemoteInfo: jest.fn(),
on: jest.fn(),
getServerLog: () => ({
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
}),
getViewLog: () => ({
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
}),
}));
jest.mock('./MattermostView', () => ({ jest.mock('./MattermostView', () => ({
MattermostView: jest.fn(), MattermostView: jest.fn(),
})); }));
jest.mock('./modalManager', () => ({ jest.mock('./modalManager', () => ({
showModal: jest.fn(), showModal: jest.fn(),
isModalDisplayed: jest.fn(),
})); }));
jest.mock('./webContentEvents', () => ({})); jest.mock('./webContentEvents', () => ({}));
jest.mock('../appState', () => ({})); jest.mock('../appState', () => ({}));
describe('main/views/viewManager', () => { describe('main/views/viewManager', () => {
describe('loadView', () => { describe('loadView', () => {
const viewManager = new ViewManager({}); const viewManager = new ViewManager();
const onceFn = jest.fn(); const onceFn = jest.fn();
const loadFn = jest.fn(); const loadFn = jest.fn();
const destroyFn = jest.fn(); const destroyFn = jest.fn();
beforeEach(() => { beforeEach(() => {
viewManager.showByName = jest.fn(); viewManager.showById = jest.fn();
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({name: `${srv.name}-${tabName}`})); MainWindow.get.mockReturnValue({});
MattermostView.mockImplementation((tab) => ({ MattermostView.mockImplementation((tab) => ({
on: jest.fn(), on: jest.fn(),
load: loadFn, load: loadFn,
once: onceFn, once: onceFn,
destroy: destroyFn, destroy: destroyFn,
name: tab.name, id: tab.id,
})); }));
}); });
@@ -107,48 +135,39 @@ describe('main/views/viewManager', () => {
}); });
it('should add closed tabs to closedViews', () => { it('should add closed tabs to closedViews', () => {
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: false}); viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: false});
expect(viewManager.closedViews.has('server1-tab1')).toBe(true); expect(viewManager.closedViews.has('tab1')).toBe(true);
}); });
it('should remove from remove from closedViews when the tab is open', () => { it('should remove from remove from closedViews when the tab is open', () => {
viewManager.closedViews.set('server1-tab1', {}); viewManager.closedViews.set('tab1', {});
expect(viewManager.closedViews.has('server1-tab1')).toBe(true); expect(viewManager.closedViews.has('tab1')).toBe(true);
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}); viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true});
expect(viewManager.closedViews.has('server1-tab1')).toBe(false); expect(viewManager.closedViews.has('tab1')).toBe(false);
}); });
it('should add view to views map and add listeners', () => { it('should add view to views map and add listeners', () => {
viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}, 'http://server-1.com/subpath'); viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}, 'http://server-1.com/subpath');
expect(viewManager.views.has('server1-tab1')).toBe(true); expect(viewManager.views.has('tab1')).toBe(true);
expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView); expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView);
expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath'); expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath');
}); });
}); });
describe('reloadConfiguration', () => { describe('handleReloadConfiguration', () => {
const viewManager = new ViewManager(); const viewManager = new ViewManager();
beforeEach(() => { beforeEach(() => {
viewManager.loadView = jest.fn(); viewManager.loadView = jest.fn();
viewManager.showByName = jest.fn(); viewManager.showById = jest.fn();
viewManager.showInitial = jest.fn(); viewManager.showInitial = jest.fn();
viewManager.focus = jest.fn();
const mainWindow = { MainWindow.get.mockReturnValue({
webContents: { webContents: {
send: jest.fn(), send: jest.fn(),
}, },
}; });
MainWindow.get.mockReturnValue(mainWindow);
viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({
name: `${srv.name}-${tabName}`,
url: new URL(`http://${srv.name}.com`),
}));
MattermostServer.mockImplementation((server) => ({
name: server.name,
url: new URL(server.url),
}));
const onceFn = jest.fn(); const onceFn = jest.fn();
const loadFn = jest.fn(); const loadFn = jest.fn();
const destroyFn = jest.fn(); const destroyFn = jest.fn();
@@ -157,11 +176,10 @@ describe('main/views/viewManager', () => {
load: loadFn, load: loadFn,
once: onceFn, once: onceFn,
destroy: destroyFn, destroy: destroyFn,
name: tab.name, id: tab.id,
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
tab, tab,
})); }));
getTabViewName.mockImplementation((a, b) => `${a}-${b}`);
}); });
afterEach(() => { afterEach(() => {
@@ -172,342 +190,289 @@ describe('main/views/viewManager', () => {
}); });
it('should recycle existing views', () => { it('should recycle existing views', () => {
Config.teams = [
{
name: 'server1',
url: 'http://server1.com',
order: 1,
tabs: [
{
name: 'tab1',
isOpen: true,
},
],
},
];
const makeSpy = jest.spyOn(viewManager, 'makeView'); const makeSpy = jest.spyOn(viewManager, 'makeView');
const view = new MattermostView({ const view = new MattermostView({
name: 'server1-tab1', id: 'tab1',
server: 'server1', server: {
id: 'server1',
},
}); });
viewManager.views.set('server1-tab1', view); viewManager.views.set('tab1', view);
viewManager.reloadConfiguration(); ServerManager.getAllServers.mockReturnValue([{
expect(viewManager.views.get('server1-tab1')).toBe(view); id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{
id: 'tab1',
isOpen: true,
},
]);
viewManager.handleReloadConfiguration();
expect(viewManager.views.get('tab1')).toBe(view);
expect(makeSpy).not.toHaveBeenCalled(); expect(makeSpy).not.toHaveBeenCalled();
makeSpy.mockRestore(); makeSpy.mockRestore();
}); });
it('should close tabs that arent open', () => { it('should close tabs that arent open', () => {
Config.teams = [ ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
name: 'server1', id: 'tab1',
url: 'http://server1.com', isOpen: false,
order: 1,
tabs: [
{
name: 'tab1',
isOpen: false,
},
],
}, },
]; ]);
viewManager.reloadConfiguration(); viewManager.handleReloadConfiguration();
expect(viewManager.closedViews.has('server1-tab1')).toBe(true); expect(viewManager.closedViews.has('tab1')).toBe(true);
}); });
it('should create new views for new tabs', () => { it('should create new views for new tabs', () => {
const makeSpy = jest.spyOn(viewManager, 'makeView'); const makeSpy = jest.spyOn(viewManager, 'makeView');
Config.teams = [ ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
name: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
name: 'server1', id: 'tab1',
url: 'http://server1.com', name: 'tab1',
order: 1, isOpen: true,
tabs: [ url: new URL('http://server1.com/tab'),
{
name: 'tab1',
isOpen: true,
},
],
}, },
]; ]);
viewManager.reloadConfiguration(); viewManager.handleReloadConfiguration();
expect(makeSpy).toHaveBeenCalledWith( expect(makeSpy).toHaveBeenCalledWith(
{ {
id: 'server1',
name: 'server1', name: 'server1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
expect.any(Object),
{ {
id: 'tab1',
name: 'tab1', name: 'tab1',
isOpen: true, isOpen: true,
url: new URL('http://server1.com/tab'),
}, },
'http://server1.com',
); );
makeSpy.mockRestore(); makeSpy.mockRestore();
}); });
it('should set focus to current view on reload', () => { it('should set focus to current view on reload', () => {
const view = { const view = {
name: 'server1-tab1', id: 'tab1',
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
name: 'server1-tab1', id: 'tab1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
focus: jest.fn(),
}; };
viewManager.currentView = 'server1-tab1'; viewManager.currentView = 'tab1';
viewManager.views.set('server1-tab1', view); viewManager.views.set('tab1', view);
Config.teams = [ ServerManager.getAllServers.mockReturnValue([{
id: 'server1',
url: new URL('http://server1.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
name: 'server1', id: 'tab1',
url: 'http://server1.com', isOpen: true,
order: 1,
tabs: [
{
name: 'tab1',
isOpen: true,
},
],
}, },
]; ]);
viewManager.reloadConfiguration(); viewManager.handleReloadConfiguration();
expect(viewManager.showByName).toHaveBeenCalledWith('server1-tab1'); expect(view.focus).toHaveBeenCalled();
}); });
it('should show initial if currentView has been removed', () => { it('should show initial if currentView has been removed', () => {
const view = { const view = {
name: 'server1-tab1', id: 'tab1',
tab: { tab: {
name: 'server1-tab1', id: 'tab1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
updateServerInfo: jest.fn(), updateServerInfo: jest.fn(),
}; };
viewManager.currentView = 'server1-tab1'; viewManager.currentView = 'tab1';
viewManager.views.set('server1-tab1', view); viewManager.views.set('tab1', view);
Config.teams = [ ServerManager.getAllServers.mockReturnValue([{
id: 'server2',
url: new URL('http://server2.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
name: 'server2', id: 'tab1',
url: 'http://server2.com', isOpen: false,
order: 1,
tabs: [
{
name: 'tab1',
isOpen: true,
},
],
}, },
]; ]);
viewManager.reloadConfiguration(); viewManager.handleReloadConfiguration();
expect(viewManager.showInitial).toBeCalled(); expect(viewManager.showInitial).toBeCalled();
}); });
it('should remove unused views', () => { it('should remove unused views', () => {
const view = { const view = {
name: 'server1-tab1', name: 'tab1',
tab: { tab: {
name: 'server1-tab1', name: 'tab1',
url: new URL('http://server1.com'), url: new URL('http://server1.com'),
}, },
destroy: jest.fn(), destroy: jest.fn(),
}; };
viewManager.views.set('server1-tab1', view); viewManager.views.set('tab1', view);
Config.teams = [ ServerManager.getAllServers.mockReturnValue([{
id: 'server2',
url: new URL('http://server2.com'),
}]);
ServerManager.getOrderedTabsForServer.mockReturnValue([
{ {
name: 'server2', id: 'tab1',
url: 'http://server2.com', isOpen: false,
order: 1,
tabs: [
{
name: 'tab1',
isOpen: true,
},
],
}, },
]; ]);
viewManager.reloadConfiguration(); viewManager.handleReloadConfiguration();
expect(view.destroy).toBeCalled(); expect(view.destroy).toBeCalled();
expect(viewManager.showInitial).toBeCalled(); expect(viewManager.showInitial).toBeCalled();
}); });
}); });
describe('showInitial', () => { describe('showInitial', () => {
const viewManager = new ViewManager({}); const viewManager = new ViewManager();
const window = {webContents: {send: jest.fn()}};
beforeEach(() => { beforeEach(() => {
Config.teams = [{ viewManager.showById = jest.fn();
name: 'server-1', MainWindow.get.mockReturnValue(window);
order: 1, ServerManager.hasServers.mockReturnValue(true);
tabs: [ ServerManager.getCurrentServer.mockReturnValue({id: 'server-0'});
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}, {
name: 'server-2',
order: 0,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}];
viewManager.showByName = jest.fn();
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`);
}); });
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
delete viewManager.lastActiveServer;
}); });
it('should show first server and first open tab in order when last active not defined', () => { it('should show last active tab and server', () => {
ServerManager.getLastActiveServer.mockReturnValue({id: 'server-1'});
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-1'});
viewManager.showInitial(); viewManager.showInitial();
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-3'); expect(viewManager.showById).toHaveBeenCalledWith('tab-1');
});
it('should show first tab in order of last active server', () => {
viewManager.lastActiveServer = 1;
viewManager.showInitial();
expect(viewManager.showByName).toHaveBeenCalledWith('server-1_tab-3');
});
it('should show last active tab of first server', () => {
Config.teams = [{
name: 'server-1',
order: 1,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}, {
name: 'server-2',
order: 0,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
lastActiveTab: 2,
}];
viewManager.showInitial();
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-2');
});
it('should show next tab when last active tab is closed', () => {
Config.teams = [{
name: 'server-1',
order: 1,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}, {
name: 'server-2',
order: 0,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: true,
},
{
name: 'tab-2',
order: 2,
isOpen: false,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
lastActiveTab: 2,
}];
viewManager.showInitial();
expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-1');
}); });
it('should open new server modal when no servers exist', () => { it('should open new server modal when no servers exist', () => {
viewManager.mainWindow = { ServerManager.hasServers.mockReturnValue(false);
webContents: {
send: jest.fn(),
},
};
Config.teams = [];
viewManager.showInitial(); viewManager.showInitial();
expect(ipcMain.emit).toHaveBeenCalledWith(MAIN_WINDOW_SHOWN); expect(window.webContents.send).toHaveBeenCalledWith(SET_ACTIVE_VIEW);
}); });
}); });
describe('showByName', () => { describe('handleBrowserHistoryPush', () => {
const viewManager = new ViewManager();
viewManager.handleBrowserHistoryButton = jest.fn();
viewManager.showById = jest.fn();
const servers = [
{
name: 'server-1',
url: 'http://server-1.com',
order: 0,
tabs: [
{
name: 'tab-messaging',
order: 0,
isOpen: true,
},
{
name: 'other_type_1',
order: 2,
isOpen: true,
},
{
name: 'other_type_2',
order: 1,
isOpen: false,
},
],
},
];
const view1 = {
id: 'server-1_tab-messaging',
isLoggedIn: true,
tab: {
type: TAB_MESSAGING,
server: {
url: 'http://server-1.com',
},
},
sendToRenderer: jest.fn(),
};
const view2 = {
...view1,
id: 'server-1_other_type_1',
tab: {
...view1.tab,
type: 'other_type_1',
},
};
const view3 = {
...view1,
id: 'server-1_other_type_2',
tab: {
...view1.tab,
type: 'other_type_2',
},
};
const views = new Map([
['server-1_tab-messaging', view1],
['server-1_other_type_1', view2],
]);
const closedViews = new Map([
['server-1_other_type_2', view3],
]);
viewManager.getView = (viewId) => views.get(viewId);
viewManager.isViewClosed = (viewId) => closedViews.has(viewId);
viewManager.openClosedTab = jest.fn();
beforeEach(() => {
ServerManager.getAllServers.mockReturnValue(servers);
ServerManager.getCurrentServer.mockReturnValue(servers[0]);
urlUtils.cleanPathName.mockImplementation((base, path) => path);
});
afterEach(() => {
jest.resetAllMocks();
});
it('should open closed view if pushing to it', () => {
viewManager.openClosedTab.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');
});
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');
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'});
viewManager.handleBrowserHistoryPush(null, 'server-1_other_type_1', '/');
expect(view1.sendToRenderer).not.toBeCalled();
});
});
describe('showById', () => {
const viewManager = new ViewManager({}); const viewManager = new ViewManager({});
const baseView = { const baseView = {
isReady: jest.fn(), isReady: jest.fn(),
@@ -545,12 +510,12 @@ describe('main/views/viewManager', () => {
}; };
viewManager.views.set('server1-tab1', view); viewManager.views.set('server1-tab1', view);
viewManager.showByName('server1-tab1'); viewManager.showById('server1-tab1');
expect(viewManager.currentView).toBeUndefined(); expect(viewManager.currentView).toBeUndefined();
expect(view.isReady).not.toBeCalled(); expect(view.isReady).not.toBeCalled();
expect(view.show).not.toBeCalled(); expect(view.show).not.toBeCalled();
viewManager.showByName('some-view-name'); viewManager.showById('some-view-name');
expect(viewManager.currentView).toBeUndefined(); expect(viewManager.currentView).toBeUndefined();
expect(view.isReady).not.toBeCalled(); expect(view.isReady).not.toBeCalled();
expect(view.show).not.toBeCalled(); expect(view.show).not.toBeCalled();
@@ -569,7 +534,7 @@ describe('main/views/viewManager', () => {
viewManager.views.set('oldView', oldView); viewManager.views.set('oldView', oldView);
viewManager.views.set('newView', newView); viewManager.views.set('newView', newView);
viewManager.currentView = 'oldView'; viewManager.currentView = 'oldView';
viewManager.showByName('newView'); viewManager.showById('newView');
expect(oldView.hide).toHaveBeenCalled(); expect(oldView.hide).toHaveBeenCalled();
}); });
@@ -577,7 +542,7 @@ describe('main/views/viewManager', () => {
const view = {...baseView}; const view = {...baseView};
view.isErrored.mockReturnValue(true); view.isErrored.mockReturnValue(true);
viewManager.views.set('view1', view); viewManager.views.set('view1', view);
viewManager.showByName('view1'); viewManager.showById('view1');
expect(view.show).not.toHaveBeenCalled(); expect(view.show).not.toHaveBeenCalled();
}); });
@@ -586,7 +551,7 @@ describe('main/views/viewManager', () => {
view.isErrored.mockReturnValue(false); view.isErrored.mockReturnValue(false);
view.needsLoadingScreen.mockImplementation(() => true); view.needsLoadingScreen.mockImplementation(() => true);
viewManager.views.set('view1', view); viewManager.views.set('view1', view);
viewManager.showByName('view1'); viewManager.showById('view1');
expect(LoadingScreen.show).toHaveBeenCalled(); expect(LoadingScreen.show).toHaveBeenCalled();
}); });
@@ -595,113 +560,12 @@ describe('main/views/viewManager', () => {
view.needsLoadingScreen.mockImplementation(() => false); view.needsLoadingScreen.mockImplementation(() => false);
view.isErrored.mockReturnValue(false); view.isErrored.mockReturnValue(false);
viewManager.views.set('view1', view); viewManager.views.set('view1', view);
viewManager.showByName('view1'); viewManager.showById('view1');
expect(viewManager.currentView).toBe('view1'); expect(viewManager.currentView).toBe('view1');
expect(view.show).toHaveBeenCalled(); expect(view.show).toHaveBeenCalled();
}); });
}); });
describe('getViewByURL', () => {
const viewManager = new ViewManager();
const servers = [
{
name: 'server-1',
url: 'http://server-1.com',
tabs: [
{
name: 'tab',
},
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
],
},
{
name: 'server-2',
url: 'http://server-2.com/subpath',
tabs: [
{
name: 'tab-type1',
},
{
name: 'tab-type2',
},
{
name: 'tab',
},
],
},
];
viewManager.getServerView = (srv, tabName) => {
const postfix = tabName.split('-')[1];
return {
name: `${srv.name}_${tabName}`,
url: new URL(`${srv.url.toString().replace(/\/$/, '')}${postfix ? `/${postfix}` : ''}`),
};
};
beforeEach(() => {
Config.teams = servers.concat();
MattermostServer.mockImplementation((server) => ({
name: server.name,
url: new URL(server.url),
}));
equalUrlsIgnoringSubpath.mockImplementation((url1, url2) => `${url1}`.startsWith(`${url2}`));
});
afterEach(() => {
jest.resetAllMocks();
});
it('should match the correct server - base URL', () => {
const inputURL = new URL('http://server-1.com');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab', 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(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab', url: new URL('http://server-1.com')});
});
it('should match the correct server - different tab', () => {
const inputURL = new URL('http://server-1.com/type1/app');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-1_tab-type1', 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(viewManager.getViewByURL(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(viewManager.getViewByURL(inputURL)).toBe(undefined);
});
it('should match the correct server with a subpath - base URL', () => {
const inputURL = new URL('http://server-2.com/subpath');
expect(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab', 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(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab', 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(viewManager.getViewByURL(inputURL)).toStrictEqual({name: 'server-2_tab-type2', 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(viewManager.getViewByURL(inputURL)).toBe(undefined);
});
});
describe('handleDeepLink', () => { describe('handleDeepLink', () => {
const viewManager = new ViewManager({}); const viewManager = new ViewManager({});
const baseView = { const baseView = {
@@ -719,7 +583,6 @@ describe('main/views/viewManager', () => {
beforeEach(() => { beforeEach(() => {
viewManager.openClosedTab = jest.fn(); viewManager.openClosedTab = jest.fn();
viewManager.getViewByURL = jest.fn();
}); });
afterEach(() => { afterEach(() => {
@@ -729,7 +592,7 @@ describe('main/views/viewManager', () => {
}); });
it('should load URL into matching view', () => { it('should load URL into matching view', () => {
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'})); ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
const view = {...baseView}; const view = {...baseView};
viewManager.views.set('view1', view); viewManager.views.set('view1', view);
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
@@ -737,14 +600,10 @@ describe('main/views/viewManager', () => {
}); });
it('should send the URL to the view if its already loaded on a 6.0 server', () => { it('should send the URL to the view if its already loaded on a 6.0 server', () => {
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'})); ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
ServerManager.getRemoteInfo.mockReturnValue({serverVersion: '6.0.0'});
const view = { const view = {
...baseView, ...baseView,
serverInfo: {
remoteInfo: {
serverVersion: '6.0.0',
},
},
tab: { tab: {
server: { server: {
url: new URL('http://server-1.com'), url: new URL('http://server-1.com'),
@@ -758,7 +617,7 @@ describe('main/views/viewManager', () => {
}); });
it('should throw error if view is missing', () => { it('should throw error if view is missing', () => {
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'http://server-1.com/'})); ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
const view = {...baseView}; const view = {...baseView};
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(view.load).not.toHaveBeenCalled(); expect(view.load).not.toHaveBeenCalled();
@@ -772,10 +631,10 @@ describe('main/views/viewManager', () => {
}); });
it('should reopen closed tab if called upon', () => { it('should reopen closed tab if called upon', () => {
viewManager.getViewByURL.mockImplementation(() => ({name: 'view1', url: 'https://server-1.com/'})); ServerManager.lookupTabByURL.mockImplementation(() => ({id: 'view1', url: new URL('http://server-1.com/')}));
viewManager.closedViews.set('view1', {}); viewManager.closedViews.set('view1', {});
viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes');
expect(viewManager.openClosedTab).toHaveBeenCalledWith('view1', 'https://server-1.com/deep/link?thing=yes'); expect(viewManager.openClosedTab).toHaveBeenCalledWith('view1', 'http://server-1.com/deep/link?thing=yes');
}); });
}); });
}); });

View File

@@ -1,9 +1,7 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import {BrowserViewConstructorOptions} from 'electron/main';
import {Tab, TeamWithTabs} from 'types/config'; import {BrowserView, dialog, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron';
import {SECOND, TAB_BAR_HEIGHT} from 'common/utils/constants'; import {SECOND, TAB_BAR_HEIGHT} from 'common/utils/constants';
import { import {
@@ -16,28 +14,25 @@ import {
BROWSER_HISTORY_PUSH, BROWSER_HISTORY_PUSH,
UPDATE_LAST_ACTIVE, UPDATE_LAST_ACTIVE,
UPDATE_URL_VIEW_WIDTH, UPDATE_URL_VIEW_WIDTH,
MAIN_WINDOW_SHOWN, SERVERS_UPDATE,
RELOAD_CURRENT_VIEW,
REACT_APP_INITIALIZED, REACT_APP_INITIALIZED,
APP_LOGGED_IN,
BROWSER_HISTORY_BUTTON, BROWSER_HISTORY_BUTTON,
APP_LOGGED_OUT, APP_LOGGED_OUT,
APP_LOGGED_IN,
RELOAD_CURRENT_VIEW,
UNREAD_RESULT, UNREAD_RESULT,
GET_VIEW_NAME,
HISTORY, HISTORY,
GET_VIEW_INFO_FOR_TEST,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import urlUtils, {equalUrlsIgnoringSubpath} from 'common/utils/url'; import urlUtils from 'common/utils/url';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
import {MattermostServer} from 'common/servers/MattermostServer'; import {MattermostServer} from 'common/servers/MattermostServer';
import {getTabViewName, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView'; import ServerManager from 'common/servers/serverManager';
import MessagingTabView from 'common/tabs/MessagingTabView'; import {TabView, TAB_MESSAGING} from 'common/tabs/TabView';
import FocalboardTabView from 'common/tabs/FocalboardTabView';
import PlaybooksTabView from 'common/tabs/PlaybooksTabView';
import {localizeMessage} from 'main/i18nManager'; import {localizeMessage} from 'main/i18nManager';
import {ServerInfo} from 'main/server/serverInfo';
import MainWindow from 'main/windows/mainWindow'; import MainWindow from 'main/windows/mainWindow';
import * as appState from '../appState'; import * as appState from '../appState';
@@ -52,21 +47,17 @@ const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 20; const URL_VIEW_HEIGHT = 20;
export class ViewManager { export class ViewManager {
private closedViews: Map<string, {srv: MattermostServer; tab: Tab}>; private closedViews: Map<string, {srv: MattermostServer; tab: TabView}>;
private views: Map<string, MattermostView>; private views: Map<string, MattermostView>;
private currentView?: string; private currentView?: string;
private urlViewCancel?: () => void; private urlViewCancel?: () => void;
private lastActiveServer?: number;
private viewOptions: BrowserViewConstructorOptions;
constructor() { constructor() {
this.lastActiveServer = Config.lastActiveTeam;
this.viewOptions = {webPreferences: {spellcheck: Config.useSpellChecker}};
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 tabs on the renderer need that.
this.closedViews = new Map(); this.closedViews = new Map();
ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest);
ipcMain.on(HISTORY, this.handleHistory); ipcMain.on(HISTORY, this.handleHistory);
ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized);
ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush); ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush);
@@ -75,23 +66,24 @@ export class ViewManager {
ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut); ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut);
ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView);
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
ipcMain.handle(GET_VIEW_NAME, this.handleGetViewName);
ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration);
} }
init = () => { init = () => {
this.getServers().forEach((server) => this.loadServer(server)); LoadingScreen.show();
ServerManager.getAllServers().forEach((server) => this.loadServer(server));
this.showInitial(); this.showInitial();
} }
getView = (viewName: string) => { getView = (viewId: string) => {
return this.views.get(viewName); return this.views.get(viewId);
} }
getCurrentView = () => { getCurrentView = () => {
if (this.currentView) { if (this.currentView) {
return this.views.get(this.currentView); return this.views.get(this.currentView);
} }
return undefined; return undefined;
} }
@@ -99,42 +91,50 @@ export class ViewManager {
return [...this.views.values()].find((view) => view.webContentsId === webContentsId); return [...this.views.values()].find((view) => view.webContentsId === webContentsId);
} }
showByName = (name: string) => { isViewClosed = (viewId: string) => {
log.debug('viewManager.showByName', name); return this.closedViews.has(viewId);
}
const newView = this.views.get(name); showById = (tabId: string) => {
this.getViewLogger(tabId).debug('showById', tabId);
const newView = this.views.get(tabId);
if (newView) { if (newView) {
if (newView.isVisible) { if (newView.isVisible) {
return; return;
} }
if (this.currentView && this.currentView !== name) { let hidePrevious;
if (this.currentView && this.currentView !== tabId) {
const previous = this.getCurrentView(); const previous = this.getCurrentView();
if (previous) { if (previous) {
previous.hide(); hidePrevious = () => previous.hide();
} }
} }
this.currentView = name; this.currentView = tabId;
if (!newView.isErrored()) { if (!newView.isErrored()) {
newView.show(); newView.show();
if (newView.needsLoadingScreen()) { if (newView.needsLoadingScreen()) {
LoadingScreen.show(); LoadingScreen.show();
} }
} }
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type); hidePrevious?.();
ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.id, newView.tab.id);
ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.id, newView.tab.id);
if (newView.isReady()) { if (newView.isReady()) {
ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type); ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.id);
} else { } else {
log.warn(`couldn't show ${name}, not ready`); this.getViewLogger(tabId).warn(`couldn't show ${tabId}, not ready`);
} }
} else { } else {
log.warn(`Couldn't find a view with name: ${name}`); this.getViewLogger(tabId).warn(`Couldn't find a view with name: ${tabId}`);
} }
modalManager.showModal(); modalManager.showModal();
} }
focusCurrentView = () => { focusCurrentView = () => {
log.debug('focusCurrentView');
if (modalManager.isModalDisplayed()) { if (modalManager.isModalDisplayed()) {
modalManager.focusCurrentModal(); modalManager.focusCurrentModal();
return; return;
@@ -171,25 +171,24 @@ export class ViewManager {
*/ */
handleDeepLink = (url: string | URL) => { handleDeepLink = (url: string | URL) => {
// TODO: fix for new tabs
if (url) { if (url) {
const parsedURL = urlUtils.parseURL(url)!; const parsedURL = urlUtils.parseURL(url)!;
const tabView = this.getViewByURL(parsedURL, true); const tabView = ServerManager.lookupTabByURL(parsedURL, true);
if (tabView) { if (tabView) {
const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`; const urlWithSchema = `${tabView.url.origin}${parsedURL.pathname}${parsedURL.search}`;
if (this.closedViews.has(tabView.name)) { if (this.closedViews.has(tabView.id)) {
this.openClosedTab(tabView.name, urlWithSchema); this.openClosedTab(tabView.id, urlWithSchema);
} else { } else {
const view = this.views.get(tabView.name); const view = this.views.get(tabView.id);
if (!view) { if (!view) {
log.error(`Couldn't find a view matching the name ${tabView.name}`); log.error(`Couldn't find a view matching the id ${tabView.id}`);
return; return;
} }
if (view.isReady() && view.serverInfo.remoteInfo.serverVersion && Utils.isVersionGreaterThanOrEqualTo(view.serverInfo.remoteInfo.serverVersion, '6.0.0')) { 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(), '')}`; const pathName = `/${urlWithSchema.replace(view.tab.server.url.toString(), '')}`;
view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName); view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName);
this.deeplinkSuccess(view.name); this.deeplinkSuccess(view.id);
} else { } else {
// attempting to change parsedURL protocol results in it not being modified. // attempting to change parsedURL protocol results in it not being modified.
view.resetLoadingStatus(); view.resetLoadingStatus();
@@ -207,83 +206,67 @@ export class ViewManager {
} }
}; };
private deeplinkSuccess = (viewName: string) => { private deeplinkSuccess = (viewId: string) => {
log.debug('deeplinkSuccess', viewName); this.getViewLogger(viewId).debug('deeplinkSuccess');
const view = this.views.get(viewName); this.showById(viewId);
if (!view) { this.views.get(viewId)?.removeListener(LOAD_FAILED, this.deeplinkFailed);
return;
}
this.showByName(viewName);
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
}; };
private deeplinkFailed = (viewName: string, err: string, url: string) => { private deeplinkFailed = (viewId: string, err: string, url: string) => {
log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`); this.getViewLogger(viewId).error(`failed to load deeplink ${url}`, err);
const view = this.views.get(viewName); this.views.get(viewId)?.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
if (!view) {
return;
}
view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess);
} }
/** /**
* View loading helpers * View loading helpers
*/ */
private loadServer = (server: TeamWithTabs) => { private loadServer = (server: MattermostServer) => {
const srv = new MattermostServer(server); const tabs = ServerManager.getOrderedTabsForServer(server.id);
const serverInfo = new ServerInfo(srv); tabs.forEach((tab) => this.loadView(server, tab));
server.tabs.forEach((tab) => this.loadView(srv, serverInfo, tab));
} }
private loadView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string) => { private loadView = (srv: MattermostServer, tab: TabView, url?: string) => {
if (!tab.isOpen) { if (!tab.isOpen) {
this.closedViews.set(getTabViewName(srv.name, tab.name), {srv, tab}); this.closedViews.set(tab.id, {srv, tab});
return; return;
} }
const view = this.makeView(srv, serverInfo, tab, url); const view = this.makeView(srv, tab, url);
this.addView(view); this.addView(view);
} }
private makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => { private makeView = (srv: MattermostServer, tab: TabView, url?: string): MattermostView => {
const tabView = this.getServerView(srv, tab.name); const mainWindow = MainWindow.get();
const view = new MattermostView(tabView, serverInfo, this.viewOptions); 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.once(LOAD_SUCCESS, this.activateView);
view.load(url);
view.on(UPDATE_TARGET_URL, this.showURLView);
view.on(LOADSCREEN_END, this.finishLoading); view.on(LOADSCREEN_END, this.finishLoading);
view.on(LOAD_FAILED, this.failLoading); view.on(LOAD_FAILED, this.failLoading);
view.on(UPDATE_TARGET_URL, this.showURLView);
view.load(url);
return view; return view;
} }
private addView = (view: MattermostView): void => { private addView = (view: MattermostView): void => {
this.views.set(view.name, view); this.views.set(view.id, view);
if (this.closedViews.has(view.name)) { if (this.closedViews.has(view.id)) {
this.closedViews.delete(view.name); this.closedViews.delete(view.id);
} }
} }
private showInitial = () => { private showInitial = () => {
log.verbose('showInitial'); log.verbose('showInitial');
const servers = this.getServers(); if (ServerManager.hasServers()) {
if (servers.length) { const lastActiveServer = ServerManager.getCurrentServer();
const element = servers.find((e) => e.order === this.lastActiveServer) || servers.find((e) => e.order === 0); const lastActiveTab = ServerManager.getLastActiveTabForServer(lastActiveServer.id);
if (element && element.tabs.length) { this.showById(lastActiveTab.id);
let tab = element.tabs.find((tab) => tab.order === element.lastActiveTab) || element.tabs.find((tab) => tab.order === 0);
if (!tab?.isOpen) {
const openTabs = element.tabs.filter((tab) => tab.isOpen);
tab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0];
}
if (tab) {
const tabView = getTabViewName(element.name, tab.name);
this.showByName(tabView);
}
}
} else { } else {
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW);
ipcMain.emit(MAIN_WINDOW_SHOWN);
} }
} }
@@ -291,29 +274,28 @@ export class ViewManager {
* Mattermost view event handlers * Mattermost view event handlers
*/ */
private activateView = (viewName: string) => { private activateView = (viewId: string) => {
log.debug('activateView', viewName); this.getViewLogger(viewId).debug('activateView');
if (this.currentView === viewName) { if (this.currentView === viewId) {
this.showByName(this.currentView); this.showById(this.currentView);
} }
} }
private finishLoading = (server: string) => { private finishLoading = (viewId: string) => {
log.debug('finishLoading', server); this.getViewLogger(viewId).debug('finishLoading');
const view = this.views.get(server); if (this.currentView === viewId) {
if (view && this.getCurrentView() === view) { this.showById(this.currentView);
this.showByName(this.currentView!);
LoadingScreen.fade(); LoadingScreen.fade();
} }
} }
private failLoading = (tabName: string) => { private failLoading = (viewId: string) => {
log.debug('failLoading', tabName); this.getViewLogger(viewId).debug('failLoading');
LoadingScreen.fade(); LoadingScreen.fade();
if (this.currentView === tabName) { if (this.currentView === viewId) {
this.getCurrentView()?.hide(); this.getCurrentView()?.hide();
} }
} }
@@ -344,7 +326,7 @@ export class ViewManager {
const query = new Map([['url', urlString]]); const query = new Map([['url', urlString]]);
const localURL = getLocalURLString('urlView.html', query); const localURL = getLocalURLString('urlView.html', query);
urlView.webContents.loadURL(localURL); urlView.webContents.loadURL(localURL);
mainWindow.addBrowserView(urlView); MainWindow.get()?.addBrowserView(urlView);
const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? mainWindow.getBounds(); const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? mainWindow.getBounds();
const hideView = () => { const hideView = () => {
@@ -372,7 +354,7 @@ export class ViewManager {
height: URL_VIEW_HEIGHT, height: URL_VIEW_HEIGHT,
}; };
log.silly('showURLView setBounds', boundaries, bounds); log.silly('showURLView.setBounds', boundaries, bounds);
urlView.setBounds(bounds); urlView.setBounds(bounds);
}; };
@@ -397,34 +379,30 @@ export class ViewManager {
* Servers or tabs have been added or edited. We need to * Servers or tabs have been added or edited. We need to
* close, open, or reload tabs, taking care to reuse tabs and * close, open, or reload tabs, taking care to reuse tabs and
* preserve focus on the currently selected tab. */ * preserve focus on the currently selected tab. */
reloadConfiguration = () => { private handleReloadConfiguration = () => {
log.debug('reloadConfiguration'); log.debug('handleReloadConfiguration');
const currentTabId: string | undefined = this.views.get(this.currentView as string)?.tab.id;
const current: Map<string, MattermostView> = new Map(); const current: Map<string, MattermostView> = new Map();
for (const view of this.views.values()) { for (const view of this.views.values()) {
current.set(view.name, view); current.set(view.tab.id, view);
} }
const views: Map<string, MattermostView> = new Map(); const views: Map<string, MattermostView> = new Map();
const closed: Map<string, {srv: MattermostServer; tab: Tab; name: string}> = new Map(); const closed: Map<string, {srv: MattermostServer; tab: TabView}> = new Map();
const sortedTabs = this.getServers().flatMap((x) => [...x.tabs]. const sortedTabs = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id).
sort((a, b) => a.order - b.order). map((t): [MattermostServer, TabView] => [x, t]));
map((t): [TeamWithTabs, Tab] => [x, t]));
for (const [team, tab] of sortedTabs) { for (const [srv, tab] of sortedTabs) {
const srv = new MattermostServer(team); const recycle = current.get(tab.id);
const info = new ServerInfo(srv);
const tabName = getTabViewName(team.name, tab.name);
const recycle = current.get(tabName);
if (!tab.isOpen) { if (!tab.isOpen) {
const view = this.getServerView(srv, tab.name); closed.set(tab.id, {srv, tab});
closed.set(tabName, {srv, tab, name: view.name});
} else if (recycle) { } else if (recycle) {
recycle.updateServerInfo(srv); views.set(tab.id, recycle);
views.set(tabName, recycle);
} else { } else {
views.set(tabName, this.makeView(srv, info, tab, team.url)); views.set(tab.id, this.makeView(srv, tab));
} }
} }
@@ -439,16 +417,16 @@ export class ViewManager {
// commit views // commit views
this.views = new Map(); this.views = new Map();
for (const x of views.values()) { for (const x of views.values()) {
this.views.set(x.name, x); this.views.set(x.id, x);
} }
// commit closed // commit closed
for (const x of closed.values()) { for (const x of closed.values()) {
this.closedViews.set(x.name, {srv: x.srv, tab: x.tab}); this.closedViews.set(x.tab.id, {srv: x.srv, tab: x.tab});
} }
if ((this.currentView && closed.has(this.currentView)) || (this.currentView && this.closedViews.has(this.currentView))) { if ((currentTabId && closed.has(currentTabId)) || (this.currentView && this.closedViews.has(this.currentView))) {
if (this.getServers().length) { if (ServerManager.hasServers()) {
this.currentView = undefined; this.currentView = undefined;
this.showInitial(); this.showInitial();
} else { } else {
@@ -457,12 +435,14 @@ export class ViewManager {
} }
// show the focused tab (or initial) // show the focused tab (or initial)
if (this.currentView && views.has(this.currentView)) { if (currentTabId && views.has(currentTabId)) {
const view = views.get(this.currentView); const view = views.get(currentTabId);
if (view) { if (view && view.id !== this.currentView) {
this.currentView = view.name; this.currentView = view.id;
this.showByName(view.name); this.showById(view.id);
MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.name, view.tab.type); MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.id, view.tab.id);
} else {
this.focusCurrentView();
} }
} else { } else {
this.showInitial(); this.showInitial();
@@ -481,21 +461,21 @@ export class ViewManager {
this.getView(viewId)?.onLogin(false); this.getView(viewId)?.onLogin(false);
} }
private handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => { private handleBrowserHistoryPush = (e: IpcMainEvent, viewId: string, pathName: string) => {
log.debug('handleBrowserHistoryPush', {viewName, pathName}); log.debug('handleBrowserHistoryPush', {viewId, pathName});
const currentView = this.views.get(viewName); const currentView = this.getView(viewId);
const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName); const cleanedPathName = urlUtils.cleanPathName(currentView?.tab.server.url.pathname || '', pathName);
const redirectedViewName = this.getViewByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.name || viewName; const redirectedviewId = ServerManager.lookupTabByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId;
if (this.closedViews.has(redirectedViewName)) { if (this.isViewClosed(redirectedviewId)) {
// If it's a closed view, just open it and stop // If it's a closed view, just open it and stop
this.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${cleanedPathName}`); this.openClosedTab(redirectedviewId, `${currentView?.tab.server.url}${cleanedPathName}`);
return; return;
} }
let redirectedView = this.views.get(redirectedViewName) || currentView; let redirectedView = this.getView(redirectedviewId) || currentView;
if (redirectedView !== currentView && redirectedView?.tab.name === this.currentView && redirectedView?.isLoggedIn) { if (redirectedView !== currentView && redirectedView?.tab.server.id === ServerManager.getCurrentServer().id && redirectedView?.isLoggedIn) {
log.info('redirecting to a new view', redirectedView?.name || viewName); log.info('redirecting to a new view', redirectedView?.id || viewId);
this.showByName(redirectedView?.name || viewName); this.showById(redirectedView?.id || viewId);
} else { } else {
redirectedView = currentView; redirectedView = currentView;
} }
@@ -504,21 +484,19 @@ export class ViewManager {
if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) { if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) {
redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName); redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName);
if (redirectedView) { if (redirectedView) {
this.handleBrowserHistoryButton(e, redirectedView.name); this.handleBrowserHistoryButton(e, redirectedView.id);
} }
} }
} }
private handleBrowserHistoryButton = (e: IpcMainEvent, viewName: string) => { private handleBrowserHistoryButton = (e: IpcMainEvent, viewId: string) => {
log.debug('handleBrowserHistoryButton', viewName); this.getView(viewId)?.updateHistoryButton();
this.getView(viewName)?.updateHistoryButton();
} }
private handleReactAppInitialized = (e: IpcMainEvent, viewName: string) => { private handleReactAppInitialized = (e: IpcMainEvent, viewId: string) => {
log.debug('handleReactAppInitialized', viewName); log.debug('handleReactAppInitialized', viewId);
const view = this.views.get(viewName); const view = this.views.get(viewId);
if (view) { if (view) {
view.setInitialized(); view.setInitialized();
if (this.getCurrentView() === view) { if (this.getCurrentView() === view) {
@@ -535,94 +513,53 @@ export class ViewManager {
return; return;
} }
view?.reload(); view?.reload();
this.showByName(view?.name); this.showById(view?.id);
} }
// if favicon is null, it means it is the initial load, // if favicon is null, it means it is the initial load,
// so don't memoize as we don't have the favicons and there is no rush to find out. // so don't memoize as we don't have the favicons and there is no rush to find out.
private handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => { private handleFaviconIsUnread = (e: Event, favicon: string, viewId: string, result: boolean) => {
log.silly('handleFaviconIsUnread', {favicon, viewName, result}); log.silly('handleFaviconIsUnread', {favicon, viewId, result});
appState.updateUnreads(viewName, result); appState.updateUnreads(viewId, result);
} }
/** /**
* Helper functions * Helper functions
*/ */
private openClosedTab = (name: string, url?: string) => { private openClosedTab = (id: string, url?: string) => {
if (!this.closedViews.has(name)) { if (!this.closedViews.has(id)) {
return; return;
} }
const {srv, tab} = this.closedViews.get(name)!; const {srv, tab} = this.closedViews.get(id)!;
tab.isOpen = true; tab.isOpen = true;
this.loadView(srv, new ServerInfo(srv), tab, url); this.loadView(srv, tab, url);
this.showByName(name); this.showById(id);
const view = this.views.get(name)!; const view = this.views.get(id)!;
view.isVisible = true; view.isVisible = true;
view.on(LOAD_SUCCESS, () => { view.on(LOAD_SUCCESS, () => {
view.isVisible = false; view.isVisible = false;
this.showByName(name); this.showById(id);
}); });
ipcMain.emit(OPEN_TAB, null, srv.name, tab.name); ipcMain.emit(OPEN_TAB, null, tab.id);
} }
getViewByURL = (inputURL: URL | string, ignoreScheme = false) => { private getViewLogger = (viewId: string) => {
log.silly('getViewByURL', `${inputURL}`, ignoreScheme); return ServerManager.getViewLog(viewId, 'ViewManager');
}
const parsedURL = urlUtils.parseURL(inputURL); private handleGetViewInfoForTest = (event: IpcMainInvokeEvent) => {
if (!parsedURL) { const view = this.getViewByWebContentsId(event.sender.id);
return undefined; if (!view) {
} return null;
const server = this.getServers().find((team) => {
const parsedServerUrl = urlUtils.parseURL(team.url)!;
return equalUrlsIgnoringSubpath(parsedURL, parsedServerUrl, ignoreScheme) && parsedURL.pathname.match(new RegExp(`^${parsedServerUrl.pathname}(.+)?(/(.+))?$`));
});
if (!server) {
return undefined;
}
const mmServer = new MattermostServer(server);
let selectedTab = this.getServerView(mmServer, TAB_MESSAGING);
server.tabs.
filter((tab) => tab.name !== TAB_MESSAGING).
forEach((tab) => {
const tabCandidate = this.getServerView(mmServer, tab.name);
if (parsedURL.pathname.match(new RegExp(`^${tabCandidate.url.pathname}(/(.+))?`))) {
selectedTab = tabCandidate;
}
});
return selectedTab;
}
private getServerView = (srv: MattermostServer, tabName: string) => {
switch (tabName) {
case TAB_MESSAGING:
return new MessagingTabView(srv);
case TAB_FOCALBOARD:
return new FocalboardTabView(srv);
case TAB_PLAYBOOKS:
return new PlaybooksTabView(srv);
default:
throw new Error('Not implemeneted');
}
}
private getServers = () => {
return Config.teams.concat();
}
handleGetViewName = (event: IpcMainInvokeEvent) => {
return this.getViewByWebContentsId(event.sender.id);
}
setServerInitialized = (server: string) => {
const view = this.views.get(server);
if (view) {
view.setInitialized();
if (this.getCurrentView() === view) {
LoadingScreen.fade();
}
} }
return {
id: view.id,
webContentsId: view.webContentsId,
serverName: view.tab.server.name,
tabType: view.tab.type,
};
} }
} }

View File

@@ -23,12 +23,13 @@ jest.mock('electron', () => ({
session: {}, session: {},
})); }));
jest.mock('main/contextMenu', () => jest.fn()); jest.mock('main/contextMenu', () => jest.fn());
jest.mock('main/windows/mainWindow', () => ({
get: jest.fn(),
}));
jest.mock('../allowProtocolDialog', () => ({})); jest.mock('../allowProtocolDialog', () => ({}));
jest.mock('main/windows/callsWidgetWindow', () => ({})); jest.mock('main/windows/callsWidgetWindow', () => ({}));
jest.mock('main/views/viewManager', () => ({ jest.mock('main/views/viewManager', () => ({
getViewByWebContentsId: jest.fn(), getViewByWebContentsId: jest.fn(),
getViewByURL: jest.fn(),
})); }));
jest.mock('../windows/windowManager', () => ({ jest.mock('../windows/windowManager', () => ({
getServerURLFromWebContentsId: jest.fn(), getServerURLFromWebContentsId: jest.fn(),

View File

@@ -9,17 +9,18 @@ import urlUtils from 'common/utils/url';
import {flushCookiesStore} from 'main/app/utils'; import {flushCookiesStore} from 'main/app/utils';
import ContextMenu from 'main/contextMenu'; import ContextMenu from 'main/contextMenu';
import ServerManager from 'common/servers/serverManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; import MainWindow from 'main/windows/mainWindow';
import WindowManager from 'main/windows/windowManager'; import WindowManager from 'main/windows/windowManager';
import ViewManager from 'main/views/viewManager';
import CallsWidgetWindow from 'main/windows/callsWidgetWindow';
import {protocols} from '../../../electron-builder.json'; import {protocols} from '../../../electron-builder.json';
import allowProtocolDialog from '../allowProtocolDialog'; import allowProtocolDialog from '../allowProtocolDialog';
import {composeUserAgent} from '../utils'; import {composeUserAgent} from '../utils';
import ViewManager from './viewManager';
type CustomLogin = { type CustomLogin = {
inProgress: boolean; inProgress: boolean;
} }
@@ -38,10 +39,16 @@ export class WebContentsEventManager {
} }
private log = (webContentsId?: number) => { private log = (webContentsId?: number) => {
if (webContentsId) { if (!webContentsId) {
return log.withPrefix(String(webContentsId)); return log;
} }
return log;
const view = ViewManager.getViewByWebContentsId(webContentsId);
if (!view) {
return log;
}
return ServerManager.getViewLog(view.id, 'WebContentsEventManager');
} }
private isTrustedPopupWindow = (webContentsId: number) => { private isTrustedPopupWindow = (webContentsId: number) => {
@@ -59,7 +66,7 @@ export class WebContentsEventManager {
return WindowManager.getServerURLFromWebContentsId(webContentsId); return WindowManager.getServerURLFromWebContentsId(webContentsId);
} }
generateWillNavigate = (webContentsId: number) => { private generateWillNavigate = (webContentsId: number) => {
return (event: Event, url: string) => { return (event: Event, url: string) => {
this.log(webContentsId).debug('will-navigate', url); this.log(webContentsId).debug('will-navigate', url);
@@ -95,9 +102,9 @@ export class WebContentsEventManager {
}; };
}; };
generateDidStartNavigation = (webContentsId: number) => { private generateDidStartNavigation = (webContentsId: number) => {
return (event: Event, url: string) => { return (event: Event, url: string) => {
this.log(webContentsId).debug('did-start-navigation', {webContentsId, url}); this.log(webContentsId).debug('did-start-navigation', url);
const parsedURL = urlUtils.parseURL(url)!; const parsedURL = urlUtils.parseURL(url)!;
const serverURL = this.getServerURLFromWebContentsId(webContentsId); const serverURL = this.getServerURLFromWebContentsId(webContentsId);
@@ -114,12 +121,12 @@ export class WebContentsEventManager {
}; };
}; };
denyNewWindow = (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => { private denyNewWindow = (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
this.log().warn(`Prevented popup window to open a new window to ${details.url}.`); this.log().warn(`Prevented popup window to open a new window to ${details.url}.`);
return {action: 'deny'}; return {action: 'deny'};
}; };
generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => { private generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => {
return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => { return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => {
this.log(webContentsId).debug('new-window', details.url); this.log(webContentsId).debug('new-window', details.url);
@@ -199,7 +206,7 @@ export class WebContentsEventManager {
this.popupWindow = { this.popupWindow = {
win: new BrowserWindow({ win: new BrowserWindow({
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
//parent: WindowManager.getMainWindow(), parent: MainWindow.get(),
show: false, show: false,
center: true, center: true,
webPreferences: { webPreferences: {
@@ -250,7 +257,7 @@ export class WebContentsEventManager {
return {action: 'deny'}; return {action: 'deny'};
} }
const otherServerURL = ViewManager.getViewByURL(parsedURL); const otherServerURL = ServerManager.lookupTabByURL(parsedURL);
if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) { if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) {
WindowManager.showMainWindow(parsedURL); WindowManager.showMainWindow(parsedURL);
return {action: 'deny'}; return {action: 'deny'};

View File

@@ -645,7 +645,6 @@ describe('main/windows/callsWidgetWindow', () => {
thumbnail: { thumbnail: {
toDataURL: jest.fn(), toDataURL: jest.fn(),
}, },
}, },
]); ]);
@@ -741,7 +740,7 @@ describe('main/windows/callsWidgetWindow', () => {
callsWidgetWindow.mainView = { callsWidgetWindow.mainView = {
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
}, },
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
@@ -807,7 +806,7 @@ describe('main/windows/callsWidgetWindow', () => {
callsWidgetWindow.mainView = { callsWidgetWindow.mainView = {
tab: { tab: {
server: { server: {
name: 'server-2', id: 'server-2',
}, },
}, },
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
@@ -874,7 +873,7 @@ describe('main/windows/callsWidgetWindow', () => {
callsWidgetWindow.mainView = { callsWidgetWindow.mainView = {
tab: { tab: {
server: { server: {
name: 'server-2', id: 'server-2',
}, },
}, },
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),
@@ -901,7 +900,7 @@ describe('main/windows/callsWidgetWindow', () => {
const view = { const view = {
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
}, },
sendToRenderer: jest.fn(), sendToRenderer: jest.fn(),

View File

@@ -80,8 +80,8 @@ export class CallsWidgetWindow {
return this.options?.callID; return this.options?.callID;
} }
private get serverName() { private get serverID() {
return this.mainView?.tab.server.name; return this.mainView?.tab.server.id;
} }
/** /**
@@ -450,11 +450,11 @@ export class CallsWidgetWindow {
private handleDesktopSourcesModalRequest = () => { private handleDesktopSourcesModalRequest = () => {
log.debug('handleDesktopSourcesModalRequest'); log.debug('handleDesktopSourcesModalRequest');
if (!this.serverName) { if (!this.serverID) {
return; return;
} }
WindowManager.switchServer(this.serverName); WindowManager.switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST);
} }
@@ -468,11 +468,11 @@ export class CallsWidgetWindow {
private handleCallsWidgetChannelLinkClick = () => { private handleCallsWidgetChannelLinkClick = () => {
log.debug('handleCallsWidgetChannelLinkClick'); log.debug('handleCallsWidgetChannelLinkClick');
if (!this.serverName) { if (!this.serverID) {
return; return;
} }
WindowManager.switchServer(this.serverName); WindowManager.switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL);
} }
@@ -480,11 +480,11 @@ export class CallsWidgetWindow {
private handleCallsError = (_: string, msg: CallsErrorMessage) => { private handleCallsError = (_: string, msg: CallsErrorMessage) => {
log.debug('handleCallsError', msg); log.debug('handleCallsError', msg);
if (!this.serverName) { if (!this.serverID) {
return; return;
} }
WindowManager.switchServer(this.serverName); WindowManager.switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(CALLS_ERROR, msg); this.mainView?.sendToRenderer(CALLS_ERROR, msg);
} }
@@ -492,11 +492,11 @@ export class CallsWidgetWindow {
private handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => { private handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => {
log.debug('handleCallsLinkClick with linkURL', msg.link); log.debug('handleCallsLinkClick with linkURL', msg.link);
if (!this.serverName) { if (!this.serverID) {
return; return;
} }
WindowManager.switchServer(this.serverName); WindowManager.switchServer(this.serverID);
MainWindow.get()?.focus(); MainWindow.get()?.focus();
this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link);
} }

View File

@@ -11,9 +11,10 @@ import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, glob
import {SavedWindowState} from 'types/mainWindow'; import {SavedWindowState} from 'types/mainWindow';
import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, FOCUS_THREE_DOT_MENU} from 'common/communication'; import {SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB, GET_FULL_SCREEN_STATUS, FOCUS_THREE_DOT_MENU, SERVERS_UPDATE} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import ServerManager from 'common/servers/serverManager';
import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants'; import {DEFAULT_WINDOW_HEIGHT, DEFAULT_WINDOW_WIDTH, MINIMUM_WINDOW_HEIGHT, MINIMUM_WINDOW_WIDTH} from 'common/utils/constants';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
import * as Validator from 'common/Validator'; import * as Validator from 'common/Validator';
@@ -39,6 +40,8 @@ export class MainWindow {
this.savedWindowState = this.getSavedWindowState(); this.savedWindowState = this.getSavedWindowState();
ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen()); ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen());
ServerManager.on(SERVERS_UPDATE, this.handleUpdateConfig);
} }
init = () => { init = () => {
@@ -321,6 +324,13 @@ export class MainWindow {
} }
}); });
} }
/**
* Server Manager update handler
*/
private handleUpdateConfig = () => {
this.win?.webContents.send(SERVERS_UPDATE);
}
} }
const mainWindow = new MainWindow(); const mainWindow = new MainWindow();

View File

@@ -6,13 +6,13 @@
import {systemPreferences} from 'electron'; import {systemPreferences} from 'electron';
import Config from 'common/config';
import {getTabViewName} from 'common/tabs/TabView'; import {getTabViewName} from 'common/tabs/TabView';
import ServerManager from 'common/servers/serverManager';
import {getAdjustedWindowBoundaries} from 'main/utils'; import {getAdjustedWindowBoundaries} from 'main/utils';
import LoadingScreen from '../views/loadingScreen';
import ViewManager from 'main/views/viewManager'; import ViewManager from '../views/viewManager';
import LoadingScreen from '../views/loadingScreen';
import {WindowManager} from './windowManager'; import {WindowManager} from './windowManager';
import MainWindow from './mainWindow'; import MainWindow from './mainWindow';
@@ -56,10 +56,10 @@ jest.mock('../utils', () => ({
resetScreensharePermissionsMacOS: jest.fn(), resetScreensharePermissionsMacOS: jest.fn(),
})); }));
jest.mock('../views/viewManager', () => ({ jest.mock('../views/viewManager', () => ({
isLoadingScreenHidden: jest.fn(), reloadConfiguration: jest.fn(),
getView: jest.fn(), showById: jest.fn(),
getViewByWebContentsId: jest.fn(),
getCurrentView: jest.fn(), getCurrentView: jest.fn(),
getView: jest.fn(),
isViewClosed: jest.fn(), isViewClosed: jest.fn(),
openClosedTab: jest.fn(), openClosedTab: jest.fn(),
handleDeepLink: jest.fn(), handleDeepLink: jest.fn(),
@@ -81,12 +81,36 @@ jest.mock('./settingsWindow', () => ({
})); }));
jest.mock('./mainWindow', () => ({ jest.mock('./mainWindow', () => ({
get: jest.fn(), get: jest.fn(),
focus: jest.fn(),
})); }));
jest.mock('../downloadsManager', () => ({ jest.mock('../downloadsManager', () => ({
getDownloads: () => {}, getDownloads: () => {},
})); }));
jest.mock('common/servers/serverManager', () => ({
getAllServers: jest.fn(),
getServer: jest.fn(),
getCurrentServer: jest.fn(),
on: jest.fn(),
lookupTabByURL: jest.fn(),
getOrderedTabsForServer: jest.fn(),
getLastActiveTabForServer: jest.fn(),
getServerLog: () => ({
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
}),
getViewLog: () => ({
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
verbose: jest.fn(),
debug: jest.fn(),
silly: jest.fn(),
}),
}));
jest.mock('./callsWidgetWindow', () => ({ jest.mock('./callsWidgetWindow', () => ({
isCallsWidget: jest.fn(), isCallsWidget: jest.fn(),
getURL: jest.fn(), getURL: jest.fn(),
@@ -159,6 +183,7 @@ describe('main/windows/windowManager', () => {
beforeEach(() => { beforeEach(() => {
MainWindow.get.mockReturnValue(mainWindow); MainWindow.get.mockReturnValue(mainWindow);
jest.useFakeTimers(); jest.useFakeTimers();
MainWindow.get.mockReturnValue(mainWindow);
ViewManager.getCurrentView.mockReturnValue(view); ViewManager.getCurrentView.mockReturnValue(view);
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
}); });
@@ -208,19 +233,18 @@ describe('main/windows/windowManager', () => {
}, },
}, },
}; };
windowManager.teamDropdown = {
updateWindowBounds: jest.fn(),
};
const mainWindow = { const mainWindow = {
getContentBounds: () => ({width: 1000, height: 900}), getContentBounds: () => ({width: 1000, height: 900}),
getSize: () => [1000, 900], getSize: () => [1000, 900],
}; };
windowManager.teamDropdown = {
updateWindowBounds: jest.fn(),
};
beforeEach(() => { beforeEach(() => {
ViewManager.getCurrentView.mockReturnValue(view);
ViewManager.isLoadingScreenHidden.mockReturnValue(true);
MainWindow.get.mockReturnValue(mainWindow); MainWindow.get.mockReturnValue(mainWindow);
LoadingScreen.isHidden.mockReturnValue(true);
ViewManager.getCurrentView.mockReturnValue(view);
getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height}));
}); });
@@ -331,7 +355,6 @@ describe('main/windows/windowManager', () => {
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
delete windowManager.settingsWindow;
}); });
it('should restore main window if minimized', () => { it('should restore main window if minimized', () => {
@@ -409,8 +432,6 @@ describe('main/windows/windowManager', () => {
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
delete windowManager.mainWindow;
delete windowManager.settingsWindow;
}); });
it('should do nothing when the windows arent set', () => { it('should do nothing when the windows arent set', () => {
@@ -458,68 +479,35 @@ describe('main/windows/windowManager', () => {
describe('switchServer', () => { describe('switchServer', () => {
const windowManager = new WindowManager(); const windowManager = new WindowManager();
const servers = [ const views = new Map([
{ ['tab-1', {id: 'tab-1'}],
name: 'server-1', ['tab-2', {id: 'tab-2'}],
order: 1, ['tab-3', {id: 'tab-3'}],
tabs: [ ]);
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}, {
name: 'server-2',
order: 0,
tabs: [
{
name: 'tab-1',
order: 0,
isOpen: false,
},
{
name: 'tab-2',
order: 2,
isOpen: true,
},
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
lastActiveTab: 2,
},
];
const map = servers.reduce((arr, item) => {
item.tabs.forEach((tab) => {
arr.push([`${item.name}_${tab.name}`, {}]);
});
return arr;
}, []);
const views = new Map(map);
beforeEach(() => { beforeEach(() => {
jest.useFakeTimers(); jest.useFakeTimers();
getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`); const server1 = {
Config.teams = servers.concat(); id: 'server-1',
ViewManager.getView.mockImplementation((name) => views.get(name)); };
const server2 = {
id: 'server-2',
};
ServerManager.getServer.mockImplementation((name) => {
switch (name) {
case 'server-1':
return server1;
case 'server-2':
return server2;
default:
return undefined;
}
});
ViewManager.getView.mockImplementation((viewId) => views.get(viewId));
}); });
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
Config.teams = [];
}); });
afterAll(() => { afterAll(() => {
@@ -531,30 +519,33 @@ describe('main/windows/windowManager', () => {
it('should do nothing if cannot find the server', () => { it('should do nothing if cannot find the server', () => {
windowManager.switchServer('server-3'); windowManager.switchServer('server-3');
expect(getTabViewName).not.toBeCalled(); expect(getTabViewName).not.toBeCalled();
expect(ViewManager.showByName).not.toBeCalled(); expect(ViewManager.showById).not.toBeCalled();
}); });
it('should show first open tab in order when last active not defined', () => { it('should show first open tab in order when last active not defined', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'});
windowManager.switchServer('server-1'); windowManager.switchServer('server-1');
expect(ViewManager.showByName).toHaveBeenCalledWith('server-1_tab-3'); expect(ViewManager.showById).toHaveBeenCalledWith('tab-3');
}); });
it('should show last active tab of chosen server', () => { it('should show last active tab of chosen server', () => {
ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'});
windowManager.switchServer('server-2'); windowManager.switchServer('server-2');
expect(ViewManager.showByName).toHaveBeenCalledWith('server-2_tab-2'); expect(ViewManager.showById).toHaveBeenCalledWith('tab-2');
}); });
it('should wait for view to exist if specified', () => { it('should wait for view to exist if specified', () => {
views.delete('server-1_tab-3'); ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'});
views.delete('tab-3');
windowManager.switchServer('server-1', true); windowManager.switchServer('server-1', true);
expect(ViewManager.showByName).not.toBeCalled(); expect(ViewManager.showById).not.toBeCalled();
jest.advanceTimersByTime(200); jest.advanceTimersByTime(200);
expect(ViewManager.showByName).not.toBeCalled(); expect(ViewManager.showById).not.toBeCalled();
views.set('server-1_tab-3', {}); views.set('tab-3', {});
jest.advanceTimersByTime(200); jest.advanceTimersByTime(200);
expect(ViewManager.showByName).toBeCalledWith('server-1_tab-3'); expect(ViewManager.showById).toBeCalledWith('tab-3');
}); });
}); });
@@ -563,75 +554,69 @@ describe('main/windows/windowManager', () => {
windowManager.switchTab = jest.fn(); windowManager.switchTab = jest.fn();
beforeEach(() => { beforeEach(() => {
Config.teams = [ const tabs = [
{ {
name: 'server-1', id: 'tab-1',
order: 1, type: 'tab-1',
tabs: [ isOpen: false,
{ },
name: 'tab-1', {
order: 0, id: 'tab-2',
isOpen: false, type: 'tab-2',
}, isOpen: true,
{ },
name: 'tab-2', {
order: 2, id: 'tab-3',
isOpen: true, type: 'tab-3',
}, isOpen: true,
{
name: 'tab-3',
order: 1,
isOpen: true,
},
],
}, },
]; ];
ServerManager.getOrderedTabsForServer.mockReturnValue(tabs);
}); });
afterEach(() => { afterEach(() => {
jest.resetAllMocks(); jest.resetAllMocks();
Config.teams = [];
}); });
it('should select next server when open', () => { it('should select next server when open', () => {
ViewManager.getCurrentView.mockReturnValue({ ViewManager.getCurrentView.mockReturnValue({
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
type: 'tab-3', type: 'tab-3',
}, },
}); });
windowManager.selectTab((order) => order + 1); windowManager.selectTab((order) => order + 1);
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-2'); expect(windowManager.switchTab).toBeCalledWith('tab-2');
}); });
it('should select previous server when open', () => { it('should select previous server when open', () => {
ViewManager.getCurrentView.mockReturnValue({ ViewManager.getCurrentView.mockReturnValue({
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
type: 'tab-2', type: 'tab-2',
}, },
}); });
windowManager.selectTab((order, length) => (length + (order - 1))); windowManager.selectTab((order, length) => (length + (order - 1)));
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3'); expect(windowManager.switchTab).toBeCalledWith('tab-3');
}); });
it('should skip over closed tab', () => { it('should skip over closed tab', () => {
ViewManager.getCurrentView.mockReturnValue({ ViewManager.getCurrentView.mockReturnValue({
tab: { tab: {
server: { server: {
name: 'server-1', id: 'server-1',
}, },
type: 'tab-2', type: 'tab-2',
}, },
}); });
windowManager.selectTab((order) => order + 1); windowManager.selectTab((order) => order + 1);
expect(windowManager.switchTab).toBeCalledWith('server-1', 'tab-3'); expect(windowManager.switchTab).toBeCalledWith('tab-3');
}); });
}); });
@@ -639,6 +624,7 @@ describe('main/windows/windowManager', () => {
const windowManager = new WindowManager(); const windowManager = new WindowManager();
it('should return calls widget URL', () => { it('should return calls widget URL', () => {
ViewManager.getView.mockReturnValue({name: 'server-1_tab-messaging'});
CallsWidgetWindow.getURL.mockReturnValue('http://server-1.com'); CallsWidgetWindow.getURL.mockReturnValue('http://server-1.com');
CallsWidgetWindow.isCallsWidget.mockReturnValue(true); CallsWidgetWindow.isCallsWidget.mockReturnValue(true);
expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe('http://server-1.com'); expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe('http://server-1.com');

View File

@@ -2,25 +2,26 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
/* eslint-disable max-lines */ /* eslint-disable max-lines */
import path from 'path';
import {app, BrowserWindow, systemPreferences, ipcMain, IpcMainEvent, IpcMainInvokeEvent} from 'electron'; import {BrowserWindow, systemPreferences, ipcMain, IpcMainEvent} from 'electron';
import { import {
MAXIMIZE_CHANGE, MAXIMIZE_CHANGE,
FOCUS_THREE_DOT_MENU,
GET_DARK_MODE, GET_DARK_MODE,
UPDATE_SHORTCUT_MENU, UPDATE_SHORTCUT_MENU,
GET_VIEW_WEBCONTENTS_ID,
RESIZE_MODAL, RESIZE_MODAL,
VIEW_FINISHED_RESIZING, VIEW_FINISHED_RESIZING,
WINDOW_CLOSE,
WINDOW_MAXIMIZE,
WINDOW_MINIMIZE,
WINDOW_RESTORE,
DOUBLE_CLICK_ON_WINDOW,
} from 'common/communication'; } from 'common/communication';
import {Logger} from 'common/log'; import {Logger} from 'common/log';
import {SECOND} from 'common/utils/constants'; import {SECOND} from 'common/utils/constants';
import Config from 'common/config'; import Config from 'common/config';
import {getTabViewName} from 'common/tabs/TabView';
import {MattermostView} from 'main/views/MattermostView'; import ServerManager from 'common/servers/serverManager';
import { import {
getAdjustedWindowBoundaries, getAdjustedWindowBoundaries,
@@ -29,6 +30,7 @@ import {
import ViewManager from '../views/viewManager'; import ViewManager from '../views/viewManager';
import LoadingScreen from '../views/loadingScreen'; import LoadingScreen from '../views/loadingScreen';
import {MattermostView} from '../views/MattermostView';
import TeamDropdownView from '../views/teamDropdownView'; import TeamDropdownView from '../views/teamDropdownView';
import DownloadsDropdownView from '../views/downloadsDropdownView'; import DownloadsDropdownView from '../views/downloadsDropdownView';
import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView'; import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView';
@@ -42,20 +44,22 @@ import SettingsWindow from './settingsWindow';
const log = new Logger('WindowManager'); const log = new Logger('WindowManager');
export class WindowManager { export class WindowManager {
assetsDir: string; private teamDropdown?: TeamDropdownView;
private downloadsDropdown?: DownloadsDropdownView;
private downloadsDropdownMenu?: DownloadsDropdownMenuView;
teamDropdown?: TeamDropdownView; private isResizing: boolean;
downloadsDropdown?: DownloadsDropdownView;
downloadsDropdownMenu?: DownloadsDropdownMenuView;
currentServerName?: string;
missingScreensharePermissions?: boolean;
constructor() { constructor() {
this.assetsDir = path.resolve(app.getAppPath(), 'assets'); this.isResizing = false;
ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode); ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode);
ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId);
ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing); ipcMain.on(VIEW_FINISHED_RESIZING, this.handleViewFinishedResizing);
ipcMain.on(WINDOW_CLOSE, this.handleClose);
ipcMain.on(WINDOW_MAXIMIZE, this.handleMaximize);
ipcMain.on(WINDOW_MINIMIZE, this.handleMinimize);
ipcMain.on(WINDOW_RESTORE, this.handleRestore);
ipcMain.on(DOUBLE_CLICK_ON_WINDOW, this.handleDoubleClick);
} }
showMainWindow = (deeplinkingURL?: string | URL) => { showMainWindow = (deeplinkingURL?: string | URL) => {
@@ -102,21 +106,153 @@ export class WindowManager {
this.initializeViewManager(); this.initializeViewManager();
} }
handleMaximizeMainWindow = () => { // max retries allows the message to get to the renderer even if it is sent while the app is starting up.
private sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => {
const mainWindow = MainWindow.get();
if (!mainWindow || !MainWindow.isReady) {
if (maxRetries > 0) {
log.debug(`Can't send ${channel}, will retry`);
setTimeout(() => {
this.sendToRendererWithRetry(maxRetries - 1, channel, ...args);
}, SECOND);
} else {
log.error(`Unable to send the message to the main window for message type ${channel}`);
}
return;
}
mainWindow.webContents.send(channel, ...args);
SettingsWindow.get()?.webContents.send(channel, ...args);
}
sendToRenderer = (channel: string, ...args: unknown[]) => {
this.sendToRendererWithRetry(3, channel, ...args);
}
restoreMain = () => {
log.info('restoreMain');
if (!MainWindow.get()) {
this.showMainWindow();
}
const mainWindow = MainWindow.get();
if (!mainWindow) {
throw new Error('Main window does not exist');
}
if (!mainWindow.isVisible() || mainWindow.isMinimized()) {
if (mainWindow.isMinimized()) {
mainWindow.restore();
} else {
mainWindow.show();
}
const settingsWindow = SettingsWindow.get();
if (settingsWindow) {
settingsWindow.focus();
} else {
mainWindow.focus();
}
} else if (SettingsWindow.get()) {
SettingsWindow.get()?.focus();
} else {
mainWindow.focus();
}
}
private initializeViewManager = () => {
ViewManager.init();
}
switchServer = (serverId: string, waitForViewToExist = false) => {
ServerManager.getServerLog(serverId, 'WindowManager').debug('switchServer');
this.showMainWindow();
const server = ServerManager.getServer(serverId);
if (!server) {
ServerManager.getServerLog(serverId, 'WindowManager').error('Cannot find server in config');
return;
}
const nextTab = ServerManager.getLastActiveTabForServer(serverId);
if (waitForViewToExist) {
const timeout = setInterval(() => {
if (ViewManager.getView(nextTab.id)) {
ViewManager.showById(nextTab.id);
clearTimeout(timeout);
}
}, 100);
} else {
ViewManager.showById(nextTab.id);
}
ipcMain.emit(UPDATE_SHORTCUT_MENU);
}
switchTab = (tabId: string) => {
ViewManager.showById(tabId);
}
/**
* ID fetching
*/
getServerURLFromWebContentsId = (id: number) => {
if (CallsWidgetWindow.isCallsWidget(id)) {
return CallsWidgetWindow.getURL();
}
return ViewManager.getViewByWebContentsId(id)?.tab.server.url;
}
/**
* Tab switching
*/
selectNextTab = () => {
this.selectTab((order) => order + 1);
}
selectPreviousTab = () => {
this.selectTab((order, length) => (length + (order - 1)));
}
private 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;
this.switchTab(newTab.id);
}
/*****************
* MAIN WINDOW EVENT HANDLERS
*****************/
private handleMaximizeMainWindow = () => {
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
this.sendToRenderer(MAXIMIZE_CHANGE, true); this.sendToRenderer(MAXIMIZE_CHANGE, true);
} }
handleUnmaximizeMainWindow = () => { private handleUnmaximizeMainWindow = () => {
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
this.sendToRenderer(MAXIMIZE_CHANGE, false); this.sendToRenderer(MAXIMIZE_CHANGE, false);
} }
isResizing = false; private handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => {
handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => {
log.silly('handleWillResizeMainWindow'); log.silly('handleWillResizeMainWindow');
if (!MainWindow.get()) { if (!MainWindow.get()) {
@@ -146,21 +282,15 @@ export class WindowManager {
ipcMain.emit(RESIZE_MODAL, null, newBounds); ipcMain.emit(RESIZE_MODAL, null, newBounds);
} }
handleResizedMainWindow = () => { private handleResizedMainWindow = () => {
log.silly('handleResizedMainWindow'); log.silly('handleResizedMainWindow');
if (MainWindow.get()) { const bounds = this.getBounds();
const bounds = this.getBounds(); this.throttledWillResize(bounds);
this.throttledWillResize(bounds); ipcMain.emit(RESIZE_MODAL, null, bounds);
ipcMain.emit(RESIZE_MODAL, null, bounds); this.teamDropdown?.updateWindowBounds();
this.teamDropdown?.updateWindowBounds(); this.downloadsDropdown?.updateWindowBounds();
this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds();
this.downloadsDropdownMenu?.updateWindowBounds();
}
this.isResizing = false;
}
handleViewFinishedResizing = () => {
this.isResizing = false; this.isResizing = false;
} }
@@ -171,7 +301,7 @@ export class WindowManager {
this.setCurrentViewBounds(newBounds); this.setCurrentViewBounds(newBounds);
} }
handleResizeMainWindow = () => { private handleResizeMainWindow = () => {
log.silly('handleResizeMainWindow'); log.silly('handleResizeMainWindow');
if (!MainWindow.get()) { if (!MainWindow.get()) {
@@ -194,7 +324,7 @@ export class WindowManager {
ipcMain.emit(RESIZE_MODAL, null, bounds); ipcMain.emit(RESIZE_MODAL, null, bounds);
}; };
setCurrentViewBounds = (bounds: {width: number; height: number}) => { private setCurrentViewBounds = (bounds: {width: number; height: number}) => {
log.debug('setCurrentViewBounds', {bounds}); log.debug('setCurrentViewBounds', {bounds});
const currentView = ViewManager.getCurrentView(); const currentView = ViewManager.getCurrentView();
@@ -228,64 +358,46 @@ export class WindowManager {
return bounds as Electron.Rectangle; return bounds as Electron.Rectangle;
} }
// max retries allows the message to get to the renderer even if it is sent while the app is starting up. /*****************
sendToRendererWithRetry = (maxRetries: number, channel: string, ...args: unknown[]) => { * IPC EVENT HANDLERS
const mainWindow = MainWindow.get(); *****************/
if (!mainWindow || !MainWindow.isReady) {
if (maxRetries > 0) { private handleGetDarkMode = () => {
log.info(`Can't send ${channel}, will retry`); return Config.darkMode;
setTimeout(() => { }
this.sendToRendererWithRetry(maxRetries - 1, channel, ...args);
}, SECOND); private handleViewFinishedResizing = () => {
} else { this.isResizing = false;
log.error(`Unable to send the message to the main window for message type ${channel}`); }
}
return; private handleClose = () => {
const focused = BrowserWindow.getFocusedWindow();
focused?.close();
}
private handleMaximize = () => {
const focused = BrowserWindow.getFocusedWindow();
if (focused) {
focused.maximize();
} }
mainWindow.webContents.send(channel, ...args);
SettingsWindow.get()?.webContents.send(channel, ...args);
} }
private handleMinimize = () => {
sendToRenderer = (channel: string, ...args: unknown[]) => { const focused = BrowserWindow.getFocusedWindow();
this.sendToRendererWithRetry(3, channel, ...args); if (focused) {
} focused.minimize();
sendToAll = (channel: string, ...args: unknown[]) => {
this.sendToRenderer(channel, ...args);
SettingsWindow.get()?.webContents.send(channel, ...args);
// TODO: should we include popups?
}
restoreMain = () => {
log.info('restoreMain');
const mainWindow = MainWindow.get(true);
if (!mainWindow) {
throw new Error('Main window does not exist');
} }
}
if (!mainWindow.isVisible() || mainWindow.isMinimized()) { private handleRestore = () => {
if (mainWindow.isMinimized()) { const focused = BrowserWindow.getFocusedWindow();
mainWindow.restore(); if (focused) {
} else { focused.restore();
mainWindow.show(); }
} if (focused?.isFullScreen()) {
const settingsWindow = SettingsWindow.get(); focused.setFullScreen(false);
if (settingsWindow) {
settingsWindow.focus();
} else {
mainWindow.focus();
}
} else if (SettingsWindow.get()) {
SettingsWindow.get()?.focus();
} else {
mainWindow.focus();
} }
} }
handleDoubleClick = (e: IpcMainEvent, windowType?: string) => { handleDoubleClick = (e: IpcMainEvent, windowType?: string) => {
log.debug('WindowManager.handleDoubleClick', windowType); log.debug('handleDoubleClick', windowType);
let action = 'Maximize'; let action = 'Maximize';
if (process.platform === 'darwin') { if (process.platform === 'darwin') {
@@ -313,150 +425,6 @@ export class WindowManager {
break; break;
} }
} }
initializeViewManager = () => {
ViewManager.init();
this.initializeCurrentServerName();
}
initializeCurrentServerName = () => {
if (!this.currentServerName) {
this.currentServerName = (Config.teams.find((team) => team.order === Config.lastActiveTeam) || Config.teams.find((team) => team.order === 0))?.name;
}
}
switchServer = (serverName: string, waitForViewToExist = false) => {
log.debug('switchServer');
this.showMainWindow();
const server = Config.teams.find((team) => team.name === serverName);
if (!server) {
log.error('Cannot find server in config');
return;
}
this.currentServerName = serverName;
let nextTab = server.tabs.find((tab) => tab.isOpen && tab.order === (server.lastActiveTab || 0));
if (!nextTab) {
const openTabs = server.tabs.filter((tab) => tab.isOpen);
nextTab = openTabs.find((e) => e.order === 0) || openTabs.concat().sort((a, b) => a.order - b.order)[0];
}
const tabViewName = getTabViewName(serverName, nextTab.name);
if (waitForViewToExist) {
const timeout = setInterval(() => {
if (ViewManager.getView(tabViewName)) {
ViewManager.showByName(tabViewName);
clearTimeout(timeout);
}
}, 100);
} else {
ViewManager.showByName(tabViewName);
}
ipcMain.emit(UPDATE_SHORTCUT_MENU);
}
switchTab = (serverName: string, tabName: string) => {
log.debug('switchTab');
this.showMainWindow();
const tabViewName = getTabViewName(serverName, tabName);
ViewManager.showByName(tabViewName);
}
focusThreeDotMenu = () => {
MainWindow.get()?.webContents.focus();
MainWindow.get()?.webContents.send(FOCUS_THREE_DOT_MENU);
}
handleLoadingScreenDataRequest = () => {
return {
darkMode: Config.darkMode || false,
};
}
close = () => {
const focused = BrowserWindow.getFocusedWindow();
focused?.close();
}
maximize = () => {
const focused = BrowserWindow.getFocusedWindow();
if (focused) {
focused.maximize();
}
}
minimize = () => {
const focused = BrowserWindow.getFocusedWindow();
if (focused) {
focused.minimize();
}
}
restore = () => {
const focused = BrowserWindow.getFocusedWindow();
if (focused) {
focused.restore();
}
if (focused?.isFullScreen()) {
focused.setFullScreen(false);
}
}
reload = () => {
const currentView = ViewManager.getCurrentView();
if (currentView) {
LoadingScreen.show();
currentView.reload();
}
}
selectNextTab = () => {
this.selectTab((order) => order + 1);
}
selectPreviousTab = () => {
this.selectTab((order, length) => (length + (order - 1)));
}
selectTab = (fn: (order: number, length: number) => number) => {
const currentView = ViewManager.getCurrentView();
if (!currentView) {
return;
}
const currentTeamTabs = Config.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
const filteredTabs = currentTeamTabs?.filter((tab) => tab.isOpen);
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
if (!currentTeamTabs || !currentTab || !filteredTabs) {
return;
}
let currentOrder = currentTab.order;
let nextIndex = -1;
while (nextIndex === -1) {
const nextOrder = (fn(currentOrder, currentTeamTabs.length) % currentTeamTabs.length);
nextIndex = filteredTabs.findIndex((tab) => tab.order === nextOrder);
currentOrder = nextOrder;
}
const newTab = filteredTabs[nextIndex];
this.switchTab(currentView.tab.server.name, newTab.name);
}
handleGetDarkMode = () => {
return Config.darkMode;
}
getCurrentTeamName = () => {
return this.currentServerName;
}
handleGetWebContentsId = (event: IpcMainInvokeEvent) => {
return event.sender.id;
}
getServerURLFromWebContentsId = (id: number) => {
if (CallsWidgetWindow.isCallsWidget(id)) {
return CallsWidgetWindow.getURL();
}
return ViewManager.getViewByWebContentsId(id)?.tab.server.url;
}
} }
const windowManager = new WindowManager(); const windowManager = new WindowManager();

View File

@@ -5,7 +5,7 @@ import React, {useState, useCallback, useEffect} from 'react';
import {useIntl, FormattedMessage} from 'react-intl'; import {useIntl, FormattedMessage} from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import {TeamWithIndex} from 'types/config'; import {MattermostTeam} from 'types/config';
import womanLaptop from 'renderer/assets/svg/womanLaptop.svg'; import womanLaptop from 'renderer/assets/svg/womanLaptop.svg';
@@ -22,8 +22,8 @@ import 'renderer/css/components/ConfigureServer.scss';
import 'renderer/css/components/LoadingScreen.css'; import 'renderer/css/components/LoadingScreen.css';
type ConfigureServerProps = { type ConfigureServerProps = {
currentTeams: TeamWithIndex[]; currentTeams: MattermostTeam[];
team?: TeamWithIndex; team?: MattermostTeam;
mobileView?: boolean; mobileView?: boolean;
darkMode?: boolean; darkMode?: boolean;
messageTitle?: string; messageTitle?: string;
@@ -32,7 +32,7 @@ type ConfigureServerProps = {
alternateLinkMessage?: string; alternateLinkMessage?: string;
alternateLinkText?: string; alternateLinkText?: string;
alternateLinkURL?: string; alternateLinkURL?: string;
onConnect: (data: TeamWithIndex) => void; onConnect: (data: MattermostTeam) => void;
}; };
function ConfigureServer({ function ConfigureServer({
@@ -53,8 +53,7 @@ function ConfigureServer({
const { const {
name: prevName, name: prevName,
url: prevURL, url: prevURL,
order = 0, id,
index = NaN,
} = team || {}; } = team || {};
const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>(); const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>();
@@ -200,8 +199,7 @@ function ConfigureServer({
onConnect({ onConnect({
url: fullURL, url: fullURL,
name, name,
index, id,
order,
}); });
}, MODAL_TRANSITION_TIMEOUT); }, MODAL_TRANSITION_TIMEOUT);
}; };

View File

@@ -10,12 +10,9 @@ import {Container, Row} from 'react-bootstrap';
import {DropResult} from 'react-beautiful-dnd'; import {DropResult} from 'react-beautiful-dnd';
import {injectIntl, IntlShape} from 'react-intl'; import {injectIntl, IntlShape} from 'react-intl';
import {TeamWithTabs} from 'types/config'; import {MattermostTab, MattermostTeam} from 'types/config';
import {DownloadedItems} from 'types/downloads'; import {DownloadedItems} from 'types/downloads';
import {getTabViewName} from 'common/tabs/TabView';
import {escapeRegex} from 'common/utils/util';
import restoreButton from '../../assets/titlebar/chrome-restore.svg'; import restoreButton from '../../assets/titlebar/chrome-restore.svg';
import maximizeButton from '../../assets/titlebar/chrome-maximize.svg'; import maximizeButton from '../../assets/titlebar/chrome-maximize.svg';
import minimizeButton from '../../assets/titlebar/chrome-minimize.svg'; import minimizeButton from '../../assets/titlebar/chrome-minimize.svg';
@@ -40,9 +37,6 @@ enum Status {
} }
type Props = { type Props = {
teams: TeamWithTabs[];
lastActiveTeam?: number;
moveTabs: (teamName: string, originalOrder: number, newOrder: number) => number | undefined;
openMenu: () => void; openMenu: () => void;
darkMode: boolean; darkMode: boolean;
appName: string; appName: string;
@@ -51,8 +45,10 @@ type Props = {
}; };
type State = { type State = {
activeServerName?: string; activeServerId?: string;
activeTabName?: string; activeTabId?: string;
servers: MattermostTeam[];
tabs: Map<string, MattermostTab[]>;
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>; unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
@@ -87,21 +83,14 @@ class MainPage extends React.PureComponent<Props, State> {
this.topBar = React.createRef(); this.topBar = React.createRef();
this.threeDotMenu = React.createRef(); this.threeDotMenu = React.createRef();
const firstServer = this.props.teams.find((team) => team.order === this.props.lastActiveTeam) || this.props.teams.find((team) => team.order === 0);
let firstTab = firstServer?.tabs.find((tab) => tab.order === firstServer.lastActiveTab) || firstServer?.tabs.find((tab) => tab.order === 0);
if (!firstTab?.isOpen) {
const openTabs = firstServer?.tabs.filter((tab) => tab.isOpen) || [];
firstTab = openTabs?.find((e) => e.order === 0) || openTabs[0];
}
this.state = { this.state = {
activeServerName: firstServer?.name, servers: [],
activeTabName: firstTab?.name, tabs: new Map(),
sessionsExpired: {}, sessionsExpired: {},
unreadCounts: {}, unreadCounts: {},
mentionCounts: {}, mentionCounts: {},
maximized: false, maximized: false,
tabViewStatus: new Map(this.props.teams.map((team) => team.tabs.map((tab) => getTabViewName(team.name, tab.name))).flat().map((tabViewName) => [tabViewName, {status: Status.LOADING}])), tabViewStatus: new Map(),
darkMode: this.props.darkMode, darkMode: this.props.darkMode,
isMenuOpen: false, isMenuOpen: false,
isDownloadsDropdownOpen: false, isDownloadsDropdownOpen: false,
@@ -112,10 +101,10 @@ class MainPage extends React.PureComponent<Props, State> {
} }
getTabViewStatus() { getTabViewStatus() {
if (!this.state.activeServerName || !this.state.activeTabName) { if (!this.state.activeTabId) {
return undefined; return undefined;
} }
return this.state.tabViewStatus.get(getTabViewName(this.state.activeServerName, this.state.activeTabName)) ?? {status: Status.NOSERVERS}; return this.state.tabViewStatus.get(this.state.activeTabId) ?? {status: Status.NOSERVERS};
} }
updateTabStatus(tabViewName: string, newStatusValue: TabViewStatus) { updateTabStatus(tabViewName: string, newStatusValue: TabViewStatus) {
@@ -135,13 +124,49 @@ class MainPage extends React.PureComponent<Props, State> {
} }
} }
componentDidMount() { getServersAndTabs = async () => {
const servers = await window.desktop.getOrderedServers();
const tabs = new Map();
const tabViewStatus = new Map(this.state.tabViewStatus);
await Promise.all(
servers.map((srv) => window.desktop.getOrderedTabsForServer(srv.id!).
then((tabs) => ({id: srv.id, tabs}))),
).then((serverTabs) => {
serverTabs.forEach((serverTab) => {
tabs.set(serverTab.id, serverTab.tabs);
serverTab.tabs.forEach((tab) => {
if (!tabViewStatus.has(tab.id!)) {
tabViewStatus.set(tab.id!, {status: Status.LOADING});
}
});
});
});
this.setState({servers, tabs, tabViewStatus});
return Boolean(servers.length);
}
setInitialActiveTab = async () => {
const lastActive = await window.desktop.getLastActive();
this.setActiveView(lastActive.server, lastActive.tab);
}
updateServers = async () => {
const hasServers = await this.getServersAndTabs();
if (hasServers && !(this.state.activeServerId && this.state.activeTabId)) {
await this.setInitialActiveTab();
}
}
async componentDidMount() {
// request downloads // request downloads
this.requestDownloadsLength(); await this.requestDownloadsLength();
await this.updateServers();
window.desktop.onUpdateServers(this.updateServers);
// set page on retry // set page on retry
window.desktop.onLoadRetry((viewName, retry, err, loadUrl) => { window.desktop.onLoadRetry((viewId, retry, err, loadUrl) => {
console.log(`${viewName}: failed to load ${err}, but retrying`); console.log(`${viewId}: failed to load ${err}, but retrying`);
const statusValue = { const statusValue = {
status: Status.RETRY, status: Status.RETRY,
extra: { extra: {
@@ -150,15 +175,15 @@ class MainPage extends React.PureComponent<Props, State> {
url: loadUrl, url: loadUrl,
}, },
}; };
this.updateTabStatus(viewName, statusValue); this.updateTabStatus(viewId, statusValue);
}); });
window.desktop.onLoadSuccess((viewName) => { window.desktop.onLoadSuccess((viewId) => {
this.updateTabStatus(viewName, {status: Status.DONE}); this.updateTabStatus(viewId, {status: Status.DONE});
}); });
window.desktop.onLoadFailed((viewName, err, loadUrl) => { window.desktop.onLoadFailed((viewId, err, loadUrl) => {
console.log(`${viewName}: failed to load ${err}`); console.log(`${viewId}: failed to load ${err}`);
const statusValue = { const statusValue = {
status: Status.FAILED, status: Status.FAILED,
extra: { extra: {
@@ -166,7 +191,7 @@ class MainPage extends React.PureComponent<Props, State> {
url: loadUrl, url: loadUrl,
}, },
}; };
this.updateTabStatus(viewName, statusValue); this.updateTabStatus(viewId, statusValue);
}); });
window.desktop.onDarkModeChange((darkMode) => { window.desktop.onDarkModeChange((darkMode) => {
@@ -174,9 +199,7 @@ class MainPage extends React.PureComponent<Props, State> {
}); });
// can't switch tabs sequentially for some reason... // can't switch tabs sequentially for some reason...
window.desktop.onSetActiveView((serverName, tabName) => { window.desktop.onSetActiveView(this.setActiveView);
this.setState({activeServerName: serverName, activeTabName: tabName});
});
window.desktop.onMaximizeChange(this.handleMaximizeState); window.desktop.onMaximizeChange(this.handleMaximizeState);
@@ -259,6 +282,13 @@ class MainPage extends React.PureComponent<Props, State> {
window.removeEventListener('click', this.handleCloseDropdowns); window.removeEventListener('click', this.handleCloseDropdowns);
} }
setActiveView = (serverId: string, tabId: string) => {
if (serverId === this.state.activeServerId && tabId === this.state.activeTabId) {
return;
}
this.setState({activeServerId: serverId, activeTabId: tabId});
}
handleCloseDropdowns = () => { handleCloseDropdowns = () => {
window.desktop.closeTeamsDropdown(); window.desktop.closeTeamsDropdown();
this.closeDownloadsDropdown(); this.closeDownloadsDropdown();
@@ -272,18 +302,12 @@ class MainPage extends React.PureComponent<Props, State> {
this.setState({fullScreen: isFullScreen}); this.setState({fullScreen: isFullScreen});
} }
handleSelectTab = (name: string) => { handleSelectTab = (tabId: string) => {
if (!this.state.activeServerName) { window.desktop.switchTab(tabId);
return;
}
window.desktop.switchTab(this.state.activeServerName, name);
} }
handleCloseTab = (name: string) => { handleCloseTab = (tabId: string) => {
if (!this.state.activeServerName) { window.desktop.closeTab(tabId);
return;
}
window.desktop.closeTab(this.state.activeServerName, name);
} }
handleDragAndDrop = async (dropResult: DropResult) => { handleDragAndDrop = async (dropResult: DropResult) => {
@@ -292,20 +316,22 @@ class MainPage extends React.PureComponent<Props, State> {
if (addedIndex === undefined || removedIndex === addedIndex) { if (addedIndex === undefined || removedIndex === addedIndex) {
return; return;
} }
if (!this.state.activeServerName) { if (!(this.state.activeServerId && this.state.tabs.has(this.state.activeServerId))) {
return;
}
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs;
if (!currentTabs) {
// TODO: figure out something here // TODO: figure out something here
return; return;
} }
const teamIndex = this.props.moveTabs(this.state.activeServerName, removedIndex, addedIndex < currentTabs.length ? addedIndex : currentTabs.length - 1); const currentTabs = this.state.tabs.get(this.state.activeServerId)!;
if (!teamIndex) { const tabsCopy = currentTabs.concat();
return;
} const tab = tabsCopy.splice(removedIndex, 1);
const name = currentTabs[teamIndex].name; const newOrder = addedIndex < currentTabs.length ? addedIndex : currentTabs.length - 1;
this.handleSelectTab(name); tabsCopy.splice(newOrder, 0, tab[0]);
window.desktop.updateTabOrder(this.state.activeServerId, tabsCopy.map((tab) => tab.id!));
const tabs = new Map(this.state.tabs);
tabs.set(this.state.activeServerId, tabsCopy);
this.setState({tabs});
this.handleSelectTab(tab[0].id!);
} }
handleClose = (e: React.MouseEvent<HTMLDivElement>) => { handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -336,7 +362,7 @@ class MainPage extends React.PureComponent<Props, State> {
} }
focusOnWebView = () => { focusOnWebView = () => {
window.desktop.focusBrowserView(); window.desktop.focusCurrentView();
this.handleCloseDropdowns(); this.handleCloseDropdowns();
} }
@@ -373,7 +399,10 @@ class MainPage extends React.PureComponent<Props, State> {
render() { render() {
const {intl} = this.props; const {intl} = this.props;
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs || []; let currentTabs: MattermostTab[] = [];
if (this.state.activeServerId) {
currentTabs = this.state.tabs.get(this.state.activeServerId) ?? [];
}
const tabsRow = ( const tabsRow = (
<TabBar <TabBar
@@ -383,8 +412,8 @@ class MainPage extends React.PureComponent<Props, State> {
sessionsExpired={this.state.sessionsExpired} sessionsExpired={this.state.sessionsExpired}
unreadCounts={this.state.unreadCounts} unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts} mentionCounts={this.state.mentionCounts}
activeServerName={this.state.activeServerName} activeServerId={this.state.activeServerId}
activeTabName={this.state.activeTabName} activeTabId={this.state.activeTabId}
onSelect={this.handleSelectTab} onSelect={this.handleSelectTab}
onCloseTab={this.handleCloseTab} onCloseTab={this.handleCloseTab}
onDrop={this.handleDragAndDrop} onDrop={this.handleDragAndDrop}
@@ -463,20 +492,22 @@ class MainPage extends React.PureComponent<Props, State> {
); );
} }
const serverMatch = `${escapeRegex(this.state.activeServerName)}___TAB_[A-Z]+`;
const totalMentionCount = Object.keys(this.state.mentionCounts).reduce((sum, key) => { const totalMentionCount = Object.keys(this.state.mentionCounts).reduce((sum, key) => {
// Strip out current server from unread and mention counts // Strip out current server from unread and mention counts
if (this.state.activeServerName && key.match(serverMatch)) { if (this.state.tabs.get(this.state.activeServerId!)?.map((tab) => tab.id).includes(key)) {
return sum; return sum;
} }
return sum + this.state.mentionCounts[key]; return sum + this.state.mentionCounts[key];
}, 0); }, 0);
const hasAnyUnreads = Object.keys(this.state.unreadCounts).reduce((sum, key) => { const hasAnyUnreads = Object.keys(this.state.unreadCounts).reduce((sum, key) => {
if (this.state.activeServerName && key.match(serverMatch)) { if (this.state.tabs.get(this.state.activeServerId!)?.map((tab) => tab.id).includes(key)) {
return sum; return sum;
} }
return sum || this.state.unreadCounts[key]; return sum || this.state.unreadCounts[key];
}, false); }, false);
const activeServer = this.state.servers.find((srv) => srv.id === this.state.activeServerId);
const topRow = ( const topRow = (
<Row <Row
className={topBarClassName} className={topBarClassName}
@@ -486,7 +517,7 @@ class MainPage extends React.PureComponent<Props, State> {
ref={this.topBar} ref={this.topBar}
className={'topBar-bg'} className={'topBar-bg'}
> >
{window.process.platform !== 'linux' && this.props.teams.length === 0 && ( {window.process.platform !== 'linux' && this.state.servers.length === 0 && (
<div className='app-title'> <div className='app-title'>
{intl.formatMessage({id: 'renderer.components.mainPage.titleBar', defaultMessage: 'Mattermost'})} {intl.formatMessage({id: 'renderer.components.mainPage.titleBar', defaultMessage: 'Mattermost'})}
</div> </div>
@@ -506,10 +537,10 @@ class MainPage extends React.PureComponent<Props, State> {
})} })}
/> />
</button> </button>
{this.props.teams.length !== 0 && ( {activeServer && (
<TeamDropdownButton <TeamDropdownButton
isDisabled={this.state.modalOpen} isDisabled={this.state.modalOpen}
activeServerName={this.state.activeServerName} activeServerName={activeServer.name}
totalMentionCount={totalMentionCount} totalMentionCount={totalMentionCount}
hasUnreads={hasAnyUnreads} hasUnreads={hasAnyUnreads}
isMenuOpen={this.state.isMenuOpen} isMenuOpen={this.state.isMenuOpen}
@@ -524,14 +555,14 @@ class MainPage extends React.PureComponent<Props, State> {
); );
const views = () => { const views = () => {
if (!this.props.teams.length) { if (!activeServer) {
return null; return null;
} }
let component; let component;
const tabStatus = this.getTabViewStatus(); const tabStatus = this.getTabViewStatus();
if (!tabStatus) { if (!tabStatus) {
if (this.state.activeTabName || this.state.activeServerName) { if (this.state.activeTabId) {
console.error(`Not tabStatus for ${this.state.activeTabName}`); console.error(`Not tabStatus for ${this.state.activeTabId}`);
} }
return null; return null;
} }
@@ -539,7 +570,7 @@ class MainPage extends React.PureComponent<Props, State> {
case Status.FAILED: case Status.FAILED:
component = ( component = (
<ErrorView <ErrorView
id={this.state.activeTabName + '-fail'} id={activeServer.name + '-fail'}
errorInfo={tabStatus.extra?.error} errorInfo={tabStatus.extra?.error}
url={tabStatus.extra ? tabStatus.extra.url : ''} url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true} active={true}

View File

@@ -6,15 +6,15 @@ import React from 'react';
import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap'; import {Modal, Button, FormGroup, FormControl, FormLabel, FormText} from 'react-bootstrap';
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import {TeamWithIndex} from 'types/config'; import {MattermostTeam} from 'types/config';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
type Props = { type Props = {
onClose?: () => void; onClose?: () => void;
onSave?: (team: TeamWithIndex) => void; onSave?: (team: MattermostTeam) => void;
team?: TeamWithIndex; team?: MattermostTeam;
currentTeams?: TeamWithIndex[]; currentTeams?: MattermostTeam[];
editMode?: boolean; editMode?: boolean;
show?: boolean; show?: boolean;
restoreFocus?: boolean; restoreFocus?: boolean;
@@ -26,7 +26,7 @@ type Props = {
type State = { type State = {
teamName: string; teamName: string;
teamUrl: string; teamUrl: string;
teamIndex?: number; teamId?: string;
teamOrder: number; teamOrder: number;
saveStarted: boolean; saveStarted: boolean;
} }
@@ -55,8 +55,7 @@ class NewTeamModal extends React.PureComponent<Props, State> {
this.setState({ this.setState({
teamName: this.props.team ? this.props.team.name : '', teamName: this.props.team ? this.props.team.name : '',
teamUrl: this.props.team ? this.props.team.url : '', teamUrl: this.props.team ? this.props.team.url : '',
teamIndex: this.props.team?.index, teamId: this.props.team?.id,
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
saveStarted: false, saveStarted: false,
}); });
} }
@@ -67,10 +66,7 @@ class NewTeamModal extends React.PureComponent<Props, State> {
} }
if (this.props.currentTeams) { if (this.props.currentTeams) {
const currentTeams = [...this.props.currentTeams]; const currentTeams = [...this.props.currentTeams];
if (this.props.editMode && this.props.team) { if (currentTeams.find((team) => team.id !== this.state.teamId && team.name === this.state.teamName)) {
currentTeams.splice(this.props.team.index, 1);
}
if (currentTeams.find((team) => team.name === this.state.teamName)) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.serverNameExists' id='renderer.components.newTeamModal.error.serverNameExists'
@@ -103,10 +99,7 @@ class NewTeamModal extends React.PureComponent<Props, State> {
} }
if (this.props.currentTeams) { if (this.props.currentTeams) {
const currentTeams = [...this.props.currentTeams]; const currentTeams = [...this.props.currentTeams];
if (this.props.editMode && this.props.team) { if (currentTeams.find((team) => team.id !== this.state.teamId && team.url === this.state.teamUrl)) {
currentTeams.splice(this.props.team.index, 1);
}
if (currentTeams.find((team) => team.url === this.state.teamUrl)) {
return ( return (
<FormattedMessage <FormattedMessage
id='renderer.components.newTeamModal.error.serverUrlExists' id='renderer.components.newTeamModal.error.serverUrlExists'
@@ -199,8 +192,7 @@ class NewTeamModal extends React.PureComponent<Props, State> {
this.props.onSave?.({ this.props.onSave?.({
url: this.state.teamUrl, url: this.state.teamUrl,
name: this.state.teamName, name: this.state.teamName,
index: this.state.teamIndex!, id: this.state.teamId,
order: this.state.teamOrder,
}); });
} }
}); });

View File

@@ -8,18 +8,18 @@ import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDra
import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; import {FormattedMessage, injectIntl, IntlShape} from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import {ConfigTab} from 'types/config'; import {MattermostTab} from 'types/config';
import {getTabViewName, TabType, canCloseTab, getTabDisplayName} from 'common/tabs/TabView'; import {TabType, canCloseTab, getTabDisplayName} from 'common/tabs/TabView';
type Props = { type Props = {
activeTabName?: string; activeTabId?: string;
activeServerName?: string; activeServerId?: string;
id: string; id: string;
isDarkMode: boolean; isDarkMode: boolean;
onSelect: (name: string, index: number) => void; onSelect: (id: string) => void;
onCloseTab: (name: string) => void; onCloseTab: (id: string) => void;
tabs: ConfigTab[]; tabs: MattermostTab[];
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, boolean>; unreadCounts: Record<string, boolean>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
@@ -41,25 +41,21 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
} }
class TabBar extends React.PureComponent<Props> { class TabBar extends React.PureComponent<Props> {
onCloseTab = (name: string) => { onCloseTab = (id: string) => {
return (event: React.MouseEvent<HTMLButtonElement>) => { return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
this.props.onCloseTab(name); this.props.onCloseTab(id);
}; };
} }
render() { render() {
const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order); const tabs = this.props.tabs.map((tab, index) => {
const tabs = orderedTabs.map((tab, orderedIndex) => { const sessionExpired = this.props.sessionsExpired[tab.id!];
const index = this.props.tabs.indexOf(tab); const hasUnreads = this.props.unreadCounts[tab.id!];
const tabName = getTabViewName(this.props.activeServerName!, tab.name);
const sessionExpired = this.props.sessionsExpired[tabName];
const hasUnreads = this.props.unreadCounts[tabName];
let mentionCount = 0; let mentionCount = 0;
if (this.props.mentionCounts[tabName] > 0) { if (this.props.mentionCounts[tab.id!] > 0) {
mentionCount = this.props.mentionCounts[tabName]; mentionCount = this.props.mentionCounts[tab.id!];
} }
let badgeDiv: React.ReactNode; let badgeDiv: React.ReactNode;
@@ -83,9 +79,9 @@ class TabBar extends React.PureComponent<Props> {
return ( return (
<Draggable <Draggable
key={index} key={tab.id}
draggableId={`teamTabItem${index}`} draggableId={`teamTabItem-${tab.id}`}
index={orderedIndex} index={index}
> >
{(provided, snapshot) => { {(provided, snapshot) => {
if (!tab.isOpen) { if (!tab.isOpen) {
@@ -106,7 +102,7 @@ class TabBar extends React.PureComponent<Props> {
draggable={false} draggable={false}
title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})} title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})}
className={classNames('teamTabItem', { className={classNames('teamTabItem', {
active: this.props.activeTabName === tab.name, active: this.props.activeTabId === tab.id,
dragging: snapshot.isDragging, dragging: snapshot.isDragging,
})} })}
{...provided.draggableProps} {...provided.draggableProps}
@@ -116,10 +112,10 @@ class TabBar extends React.PureComponent<Props> {
<NavLink <NavLink
eventKey={index} eventKey={index}
draggable={false} draggable={false}
active={this.props.activeTabName === tab.name} active={this.props.activeTabId === tab.id}
disabled={this.props.tabsDisabled} disabled={this.props.tabsDisabled}
onSelect={() => { onSelect={() => {
this.props.onSelect(tab.name, index); this.props.onSelect(tab.id!);
}} }}
> >
<div className='TabBar-tabSeperator'> <div className='TabBar-tabSeperator'>
@@ -131,7 +127,7 @@ class TabBar extends React.PureComponent<Props> {
{canCloseTab(tab.name as TabType) && {canCloseTab(tab.name as TabType) &&
<button <button
className='teamTabItem__close' className='teamTabItem__close'
onClick={this.onCloseTab(tab.name)} onClick={this.onCloseTab(tab.id!)}
> >
<i className='icon-close'/> <i className='icon-close'/>
</button> </button>

View File

@@ -7,9 +7,8 @@ import {FormattedMessage} from 'react-intl';
import classNames from 'classnames'; import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd'; import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {FullTeam, TeamWithTabs, TeamWithTabsAndGpo} from 'types/config'; import {MattermostTeam} from 'types/config';
import {getTabViewName} from 'common/tabs/TabView';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH_MAC} from 'common/utils/constants';
import './css/dropdown.scss'; import './css/dropdown.scss';
@@ -17,8 +16,9 @@ import './css/dropdown.scss';
import IntlProvider from './intl_provider'; import IntlProvider from './intl_provider';
type State = { type State = {
teams?: TeamWithTabsAndGpo[]; teams?: MattermostTeam[];
orderedTeams?: TeamWithTabsAndGpo[]; teamOrder?: string[];
orderedTeams?: MattermostTeam[];
activeTeam?: string; activeTeam?: string;
darkMode?: boolean; darkMode?: boolean;
enableServerManagement?: boolean; enableServerManagement?: boolean;
@@ -59,7 +59,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
} }
handleUpdate = ( handleUpdate = (
teams: TeamWithTabsAndGpo[], teams: MattermostTeam[],
darkMode: boolean, darkMode: boolean,
windowBounds: Electron.Rectangle, windowBounds: Electron.Rectangle,
activeTeam?: string, activeTeam?: string,
@@ -71,7 +71,6 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
) => { ) => {
this.setState({ this.setState({
teams, teams,
orderedTeams: teams.concat().sort((a: TeamWithTabs, b: TeamWithTabs) => a.order - b.order),
activeTeam, activeTeam,
darkMode, darkMode,
enableServerManagement, enableServerManagement,
@@ -83,9 +82,12 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
}); });
} }
selectServer = (team: FullTeam) => { selectServer = (team: MattermostTeam) => {
return () => { return () => {
window.desktop.serverDropdown.switchServer(team.name); if (!team.id) {
return;
}
window.desktop.serverDropdown.switchServer(team.id);
this.closeMenu(); this.closeMenu();
}; };
} }
@@ -106,8 +108,8 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
this.closeMenu(); this.closeMenu();
} }
isActiveTeam = (team: FullTeam) => { isActiveTeam = (team: MattermostTeam) => {
return team.name === this.state.activeTeam; return team.id === this.state.activeTeam;
} }
onDragStart = () => { onDragStart = () => {
@@ -124,23 +126,14 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
if (!this.state.teams) { if (!this.state.teams) {
throw new Error('No config'); throw new Error('No config');
} }
const teams = this.state.teams.concat(); const teamsCopy = this.state.teams.concat();
const tabOrder = teams.map((team, index) => {
return {
index,
order: team.order,
};
}).sort((a, b) => (a.order - b.order));
const team = tabOrder.splice(removedIndex, 1); const team = teamsCopy.splice(removedIndex, 1);
const newOrder = addedIndex < this.state.teams.length ? addedIndex : this.state.teams.length - 1; const newOrder = addedIndex < this.state.teams.length ? addedIndex : this.state.teams.length - 1;
tabOrder.splice(newOrder, 0, team[0]); teamsCopy.splice(newOrder, 0, team[0]);
tabOrder.forEach((t, order) => { this.setState({teams: teamsCopy, isAnyDragging: false});
teams[t.index].order = order; window.desktop.updateServerOrder(teamsCopy.map((team) => team.id!));
});
this.setState({teams, orderedTeams: teams.concat().sort((a: FullTeam, b: FullTeam) => a.order - b.order), isAnyDragging: false});
window.desktop.updateTeams(teams);
} }
componentDidMount() { componentDidMount() {
@@ -210,30 +203,30 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
} }
} }
editServer = (teamName: string) => { editServer = (teamId: string) => {
if (this.teamIsGpo(teamName)) { if (this.teamIsPredefined(teamId)) {
return () => {}; return () => {};
} }
return (event: React.MouseEvent<HTMLButtonElement>) => { return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
window.desktop.serverDropdown.showEditServerModal(teamName); window.desktop.serverDropdown.showEditServerModal(teamId);
this.closeMenu(); this.closeMenu();
}; };
} }
removeServer = (teamName: string) => { removeServer = (teamId: string) => {
if (this.teamIsGpo(teamName)) { if (this.teamIsPredefined(teamId)) {
return () => {}; return () => {};
} }
return (event: React.MouseEvent<HTMLButtonElement>) => { return (event: React.MouseEvent<HTMLButtonElement>) => {
event.stopPropagation(); event.stopPropagation();
window.desktop.serverDropdown.showRemoveServerModal(teamName); window.desktop.serverDropdown.showRemoveServerModal(teamId);
this.closeMenu(); this.closeMenu();
}; };
} }
teamIsGpo = (teamName: string) => { teamIsPredefined = (teamId: string) => {
return this.state.orderedTeams?.some((team) => team.name === teamName && team.isGpo); return this.state.teams?.some((team) => team.id === teamId && team.isPredefined);
} }
render() { render() {
@@ -275,15 +268,11 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
ref={provided.innerRef} ref={provided.innerRef}
{...provided.droppableProps} {...provided.droppableProps}
> >
{this.state.orderedTeams?.map((team, orderedIndex) => { {this.state.teams?.map((team, orderedIndex) => {
const index = this.state.teams?.indexOf(team); const index = this.state.teams?.indexOf(team);
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => { const sessionExpired = this.state.expired?.get(team.id!);
const tabName = getTabViewName(team.name, tab.name); const hasUnreads = this.state.unreads?.get(team.id!);
counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired; const mentionCount = this.state.mentions?.get(team.id!);
counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
return counts;
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
let badgeDiv: React.ReactNode; let badgeDiv: React.ReactNode;
if (sessionExpired) { if (sessionExpired) {
@@ -334,16 +323,16 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
{this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>} {this.isActiveTeam(team) ? <i className='icon-check'/> : <i className='icon-server-variant'/>}
<span>{team.name}</span> <span>{team.name}</span>
</div> </div>
{!team.isGpo && <div className='TeamDropdown__indicators'> {!team.isPredefined && <div className='TeamDropdown__indicators'>
<button <button
className='TeamDropdown__button-edit' className='TeamDropdown__button-edit'
onClick={this.editServer(team.name)} onClick={this.editServer(team.id!)}
> >
<i className='icon-pencil-outline'/> <i className='icon-pencil-outline'/>
</button> </button>
<button <button
className='TeamDropdown__button-remove' className='TeamDropdown__button-remove'
onClick={this.removeServer(team.name)} onClick={this.removeServer(team.id!)}
> >
<i className='icon-trash-can-outline'/> <i className='icon-trash-can-outline'/>
</button> </button>
@@ -365,7 +354,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
{this.state.enableServerManagement && {this.state.enableServerManagement &&
<button <button
ref={(ref) => { ref={(ref) => {
this.addButtonRef(this.state.orderedTeams?.length || 0, ref); this.addButtonRef(this.state.teams?.length || 0, ref);
}} }}
className='TeamDropdown__button addServer' className='TeamDropdown__button addServer'
onClick={this.addServer} onClick={this.addServer}

View File

@@ -8,7 +8,7 @@ import 'renderer/css/index.css';
import React from 'react'; import React from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {CombinedConfig, Team} from 'types/config'; import {CombinedConfig} from 'types/config';
import MainPage from './components/MainPage'; import MainPage from './components/MainPage';
import IntlProvider from './intl_provider'; import IntlProvider from './intl_provider';
@@ -45,48 +45,6 @@ class Root extends React.PureComponent<Record<string, never>, State> {
this.setState({config}); this.setState({config});
} }
moveTabs = (teamName: string, originalOrder: number, newOrder: number): number | undefined => {
if (!this.state.config) {
throw new Error('No config');
}
const teams = this.state.config.teams.concat();
const currentTeamIndex = teams.findIndex((team) => team.name === teamName);
const tabs = teams[currentTeamIndex].tabs.concat();
const tabOrder = tabs.map((team, index) => {
return {
index,
order: team.order,
};
}).sort((a, b) => (a.order - b.order));
const team = tabOrder.splice(originalOrder, 1);
tabOrder.splice(newOrder, 0, team[0]);
let teamIndex: number | undefined;
tabOrder.forEach((t, order) => {
if (order === newOrder) {
teamIndex = t.index;
}
tabs[t.index].order = order;
});
teams[currentTeamIndex].tabs = tabs;
this.setState({
config: {
...this.state.config,
teams,
},
});
this.teamConfigChange(teams);
return teamIndex;
};
teamConfigChange = async (updatedTeams: Team[]) => {
window.desktop.updateTeams(updatedTeams).then(() => {
this.reloadConfig();
});
};
reloadConfig = async () => { reloadConfig = async () => {
const config = await this.requestConfig(); const config = await this.requestConfig();
this.setState({config}); this.setState({config});
@@ -120,9 +78,6 @@ class Root extends React.PureComponent<Record<string, never>, State> {
return ( return (
<IntlProvider> <IntlProvider>
<MainPage <MainPage
teams={config.teams}
lastActiveTeam={config.lastActiveTeam}
moveTabs={this.moveTabs}
openMenu={this.openMenu} openMenu={this.openMenu}
darkMode={config.darkMode} darkMode={config.darkMode}
appName={config.appName} appName={config.appName}

View File

@@ -7,7 +7,7 @@ import 'renderer/css/modals.css';
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {TeamWithIndex} from 'types/config'; import {MattermostTeam} from 'types/config';
import IntlProvider from 'renderer/intl_provider'; import IntlProvider from 'renderer/intl_provider';
@@ -18,21 +18,21 @@ import setupDarkMode from '../darkMode';
setupDarkMode(); setupDarkMode();
type ModalInfo = { type ModalInfo = {
team: TeamWithIndex; team: MattermostTeam;
currentTeams: TeamWithIndex[]; currentTeams: MattermostTeam[];
}; };
const onClose = () => { const onClose = () => {
window.desktop.modals.cancelModal(); window.desktop.modals.cancelModal();
}; };
const onSave = (data: TeamWithIndex) => { const onSave = (data: MattermostTeam) => {
window.desktop.modals.finishModal(data); window.desktop.modals.finishModal(data);
}; };
const EditServerModalWrapper: React.FC = () => { const EditServerModalWrapper: React.FC = () => {
const [server, setServer] = useState<TeamWithIndex>(); const [server, setServer] = useState<MattermostTeam>();
const [currentTeams, setCurrentTeams] = useState<TeamWithIndex[]>(); const [currentTeams, setCurrentTeams] = useState<MattermostTeam[]>();
useEffect(() => { useEffect(() => {
window.desktop.modals.getModalInfo<ModalInfo>().then(({team, currentTeams}) => { window.desktop.modals.getModalInfo<ModalInfo>().then(({team, currentTeams}) => {

View File

@@ -7,7 +7,7 @@ import 'renderer/css/modals.css';
import React, {useEffect, useState} from 'react'; import React, {useEffect, useState} from 'react';
import ReactDOM from 'react-dom'; import ReactDOM from 'react-dom';
import {TeamWithIndex} from 'types/config'; import {MattermostTeam} from 'types/config';
import IntlProvider from 'renderer/intl_provider'; import IntlProvider from 'renderer/intl_provider';
@@ -21,19 +21,19 @@ const onClose = () => {
window.desktop.modals.cancelModal(); window.desktop.modals.cancelModal();
}; };
const onSave = (data: TeamWithIndex) => { const onSave = (data: MattermostTeam) => {
window.desktop.modals.finishModal(data); window.desktop.modals.finishModal(data);
}; };
const NewServerModalWrapper: React.FC = () => { const NewServerModalWrapper: React.FC = () => {
const [unremoveable, setUnremovable] = useState<boolean>(); const [unremoveable, setUnremovable] = useState<boolean>();
const [currentTeams, setCurrentTeams] = useState<TeamWithIndex[]>(); const [currentTeams, setCurrentTeams] = useState<MattermostTeam[]>();
useEffect(() => { useEffect(() => {
window.desktop.modals.isModalUncloseable().then((uncloseable) => { window.desktop.modals.isModalUncloseable().then((uncloseable) => {
setUnremovable(uncloseable); setUnremovable(uncloseable);
}); });
window.desktop.modals.getModalInfo<TeamWithIndex[]>().then((teams) => { window.desktop.modals.getModalInfo<MattermostTeam[]>().then((teams) => {
setCurrentTeams(teams); setCurrentTeams(teams);
}); });
}, []); }, []);

View File

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

View File

@@ -11,22 +11,24 @@ export type Team = {
url: string; url: string;
} }
export type FullTeam = Team & {
order: number;
lastActiveTab?: number;
}
export type ConfigTab = Tab & { export type ConfigTab = Tab & {
order: number; order: number;
} }
export type ConfigServer = FullTeam & { export type ConfigServer = Team & {
order: number;
lastActiveTab?: number;
tabs: ConfigTab[]; tabs: ConfigTab[];
} }
export type TeamWithIndex = FullTeam & {index: number}; export type MattermostTeam = Team & {
export type TeamWithTabs = ConfigServer & {tabs: Tab[]}; id?: string;
export type TeamWithTabsAndGpo = TeamWithTabs & {isGpo?: boolean}; isPredefined?: boolean;
}
export type MattermostTab = Tab & {
id?: string;
}
export type Config = ConfigV3; export type Config = ConfigV3;
@@ -111,7 +113,7 @@ export type ConfigV0 = {version: 0; url: string};
export type AnyConfig = ConfigV3 | ConfigV2 | ConfigV1 | ConfigV0; export type AnyConfig = ConfigV3 | ConfigV2 | ConfigV1 | ConfigV0;
export type BuildConfig = { export type BuildConfig = {
defaultTeams?: FullTeam[]; defaultTeams?: Team[];
helpLink: string; helpLink: string;
enableServerManagement: boolean; enableServerManagement: boolean;
enableAutoUpdater: boolean; enableAutoUpdater: boolean;
@@ -125,8 +127,7 @@ export type RegistryConfig = {
enableAutoUpdater: boolean; enableAutoUpdater: boolean;
} }
export type CombinedConfig = ConfigV3 & BuildConfig & { export type CombinedConfig = Omit<ConfigV3, 'teams'> & Omit<BuildConfig, 'defaultTeams'> & {
registryTeams: Team[];
appName: string; appName: string;
useNativeWindow: boolean; useNativeWindow: boolean;
} }

View File

@@ -2,7 +2,6 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
export type RemoteInfo = { export type RemoteInfo = {
name: string;
serverVersion?: string; serverVersion?: string;
siteURL?: string; siteURL?: string;
hasFocalboard?: boolean; hasFocalboard?: boolean;

View File

@@ -1,11 +1,6 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
export type ServerFromURL = {
name: string;
url: string;
}
export type Boundaries = { export type Boundaries = {
maxX: number; maxX: number;
maxY: number; maxY: number;

View File

@@ -5,7 +5,7 @@ import {ipcRenderer, Rectangle} from 'electron/renderer';
import {Language} from '../../i18n/i18n'; import {Language} from '../../i18n/i18n';
import {CombinedConfig, LocalConfiguration, Team, TeamWithTabsAndGpo} from './config'; import {CombinedConfig, LocalConfiguration, MattermostTab, MattermostTeam} from './config';
import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads'; import {DownloadedItem, DownloadedItems, DownloadsMenuOpenEventPayload} from './downloads';
import {SaveQueueItem} from './settings'; import {SaveQueueItem} from './settings';
@@ -34,14 +34,14 @@ declare global {
openAppMenu: () => void; openAppMenu: () => void;
closeTeamsDropdown: () => void; closeTeamsDropdown: () => void;
openTeamsDropdown: () => void; openTeamsDropdown: () => void;
switchTab: (serverName: string, tabName: string) => void; switchTab: (tabId: string) => void;
closeTab: (serverName: string, tabName: string) => void; closeTab: (tabId: string) => void;
closeWindow: () => void; closeWindow: () => void;
minimizeWindow: () => void; minimizeWindow: () => void;
maximizeWindow: () => void; maximizeWindow: () => void;
restoreWindow: () => void; restoreWindow: () => void;
doubleClickOnWindow: (windowName?: string) => void; doubleClickOnWindow: (windowName?: string) => void;
focusBrowserView: () => void; focusCurrentView: () => void;
reloadCurrentView: () => void; reloadCurrentView: () => void;
closeDownloadsDropdown: () => void; closeDownloadsDropdown: () => void;
closeDownloadsDropdownMenu: () => void; closeDownloadsDropdownMenu: () => void;
@@ -50,25 +50,31 @@ declare global {
checkForUpdates: () => void; checkForUpdates: () => void;
updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void; updateConfiguration: (saveQueueItems: SaveQueueItem[]) => void;
updateTeams: (updatedTeams: Team[]) => Promise<void>; updateServerOrder: (serverOrder: string[]) => Promise<void>;
getConfiguration: (option?: keyof CombinedConfig) => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>; updateTabOrder: (serverId: string, tabOrder: string[]) => Promise<void>;
getLastActive: () => Promise<{server: string; tab: string}>;
getOrderedServers: () => Promise<MattermostTeam[]>;
getOrderedTabsForServer: (serverId: string) => Promise<MattermostTab[]>;
onUpdateServers: (listener: () => void) => void;
getConfiguration: () => Promise<CombinedConfig[keyof CombinedConfig] | CombinedConfig>;
getVersion: () => Promise<{name: string; version: string}>; getVersion: () => Promise<{name: string; version: string}>;
getDarkMode: () => Promise<boolean>; getDarkMode: () => Promise<boolean>;
requestHasDownloads: () => Promise<boolean>; requestHasDownloads: () => Promise<boolean>;
getFullScreenStatus: () => Promise<boolean>; getFullScreenStatus: () => Promise<boolean>;
getAvailableSpellCheckerLanguages: () => Promise<string[]>; getAvailableSpellCheckerLanguages: () => Promise<string[]>;
getAvailableLanguages: () => Promise<string[]>; getAvailableLanguages: () => Promise<string[]>;
getLocalConfiguration: (option?: keyof LocalConfiguration) => Promise<LocalConfiguration[keyof LocalConfiguration] | Partial<LocalConfiguration>>; getLocalConfiguration: () => Promise<LocalConfiguration[keyof LocalConfiguration] | Partial<LocalConfiguration>>;
getDownloadLocation: (downloadLocation?: string) => Promise<string>; getDownloadLocation: (downloadLocation?: string) => Promise<string>;
getLanguageInformation: () => Promise<Language>; getLanguageInformation: () => Promise<Language>;
onSynchronizeConfig: (listener: () => void) => void; onSynchronizeConfig: (listener: () => void) => void;
onReloadConfiguration: (listener: () => void) => void; onReloadConfiguration: (listener: () => void) => void;
onDarkModeChange: (listener: (darkMode: boolean) => void) => void; onDarkModeChange: (listener: (darkMode: boolean) => void) => void;
onLoadRetry: (listener: (viewName: string, retry: Date, err: string, loadUrl: string) => void) => void; onLoadRetry: (listener: (viewId: string, retry: Date, err: string, loadUrl: string) => void) => void;
onLoadSuccess: (listener: (viewName: string) => void) => void; onLoadSuccess: (listener: (viewId: string) => void) => void;
onLoadFailed: (listener: (viewName: string, err: string, loadUrl: string) => void) => void; onLoadFailed: (listener: (viewId: string, err: string, loadUrl: string) => void) => void;
onSetActiveView: (listener: (serverName: string, tabName: string) => void) => void; onSetActiveView: (listener: (serverId: string, tabId: string) => void) => void;
onMaximizeChange: (listener: (maximize: boolean) => void) => void; onMaximizeChange: (listener: (maximize: boolean) => void) => void;
onEnterFullScreen: (listener: () => void) => void; onEnterFullScreen: (listener: () => void) => void;
onLeaveFullScreen: (listener: () => void) => void; onLeaveFullScreen: (listener: () => void) => void;
@@ -127,13 +133,13 @@ declare global {
serverDropdown: { serverDropdown: {
requestInfo: () => void; requestInfo: () => void;
sendSize: (width: number, height: number) => void; sendSize: (width: number, height: number) => void;
switchServer: (serverName: string) => void; switchServer: (serverId: string) => void;
showNewServerModal: () => void; showNewServerModal: () => void;
showEditServerModal: (serverName: string) => void; showEditServerModal: (serverId: string) => void;
showRemoveServerModal: (serverName: string) => void; showRemoveServerModal: (serverId: string) => void;
onUpdateServerDropdown: (listener: ( onUpdateServerDropdown: (listener: (
teams: TeamWithTabsAndGpo[], teams: MattermostTeam[],
darkMode: boolean, darkMode: boolean,
windowBounds: Rectangle, windowBounds: Rectangle,
activeTeam?: string, activeTeam?: string,