diff --git a/e2e/modules/environment.js b/e2e/modules/environment.js index 799d0f2a..f31e1d06 100644 --- a/e2e/modules/environment.js +++ b/e2e/modules/environment.js @@ -234,9 +234,8 @@ module.exports = { if (!window.testHelper) { return null; } - const name = await window.testHelper.getViewName(); - const webContentsId = await window.testHelper.getWebContentsId(); - return {viewName: name, webContentsId}; + const info = await window.testHelper.getViewInfoForTest(); + return {viewName: `${info.serverName}___${info.tabType}`, webContentsId: info.webContentsId}; }).then((result) => { if (result) { map[result.viewName] = {win, webContentsId: result.webContentsId}; diff --git a/e2e/specs/server_management/add_server_modal.test.js b/e2e/specs/server_management/add_server_modal.test.js index 9070eaeb..b9a6393a 100644 --- a/e2e/specs/server_management/add_server_modal.test.js +++ b/e2e/specs/server_management/add_server_modal.test.js @@ -137,7 +137,7 @@ describe('Add Server Modal', function desc() { const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); savedConfig.teams.should.deep.contain({ name: 'TestTeam', - url: 'http://example.org', + url: 'http://example.org/', order: 2, tabs: [ { diff --git a/e2e/specs/server_management/configure_server_modal.test.js b/e2e/specs/server_management/configure_server_modal.test.js index 96756209..80fdb584 100644 --- a/e2e/specs/server_management/configure_server_modal.test.js +++ b/e2e/specs/server_management/configure_server_modal.test.js @@ -83,9 +83,8 @@ describe('Configure Server Modal', function desc() { const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8')); savedConfig.teams.should.deep.contain({ - url: 'http://example.org', + url: 'http://example.org/', name: 'TestTeam', - index: null, order: 0, tabs: [ { diff --git a/e2e/specs/server_management/edit_server_modal.test.js b/e2e/specs/server_management/edit_server_modal.test.js index 756674b9..bd162902 100644 --- a/e2e/specs/server_management/edit_server_modal.test.js +++ b/e2e/specs/server_management/edit_server_modal.test.js @@ -201,7 +201,7 @@ describe('EditServerModal', function desc() { }); savedConfig.teams.should.deep.contain({ name: 'example', - url: 'http://google.com', + url: 'http://google.com/', order: 0, tabs: [ { @@ -254,7 +254,7 @@ describe('EditServerModal', function desc() { }); savedConfig.teams.should.deep.contain({ name: 'NewTestTeam', - url: 'http://google.com', + url: 'http://google.com/', order: 0, tabs: [ { diff --git a/src/common/communication.ts b/src/common/communication.ts index 6fe4cde2..29a38e50 100644 --- a/src/common/communication.ts +++ b/src/common/communication.ts @@ -18,7 +18,6 @@ export const GET_LOCAL_CONFIGURATION = 'get-local-configuration'; export const RELOAD_CONFIGURATION = 'reload-config'; export const EMIT_CONFIGURATION = 'emit-configuration'; -export const UPDATE_TEAMS = 'update-teams'; export const DARK_MODE_CHANGE = 'dark_mode_change'; export const GET_DARK_MODE = 'get-dark-mode'; 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_EDIT_SERVER_MODAL = 'show-edit-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 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_VIEW_NAME = 'get-view-name'; -export const GET_VIEW_WEBCONTENTS_ID = 'get-view-webcontents-id'; +export const GET_VIEW_INFO_FOR_TEST = 'get-view-info-for-test'; export const RESIZE_MODAL = 'resize-modal'; 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 PING_DOMAIN = 'ping-domain'; -export const PING_DOMAIN_RESPONSE = 'ping-domain-response'; export const GET_LANGUAGE_INFORMATION = 'get-language-information'; 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_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'; diff --git a/src/common/config/index.test.js b/src/common/config/index.test.js index d2ba76fb..15a583d6 100644 --- a/src/common/config/index.test.js +++ b/src/common/config/index.test.js @@ -314,67 +314,66 @@ describe('common/config', () => { }); }); - // TODO: Re-enable when we migrate to ServerManager fully - // describe('regenerateCombinedConfigData', () => { - // it('should combine config from all sources', () => { - // const config = new Config(); - // config.reload = jest.fn(); - // config.init(configPath, appName, appPath); - // config.useNativeWindow = false; - // config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'}; - // config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'}; - // config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'}; - // config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'}; + describe('regenerateCombinedConfigData', () => { + it('should combine config from all sources', () => { + const config = new Config(); + config.reload = jest.fn(); + config.init(configPath, appName, appPath); + config.useNativeWindow = false; + config.defaultConfigData = {defaultSetting: 'default', otherDefaultSetting: 'default'}; + config.localConfigData = {otherDefaultSetting: 'local', localSetting: 'local', otherLocalSetting: 'local'}; + config.buildConfigData = {otherLocalSetting: 'build', buildSetting: 'build', otherBuildSetting: 'build'}; + config.registryConfigData = {otherBuildSetting: 'registry', registrySetting: 'registry'}; - // config.regenerateCombinedConfigData(); - // config.combinedData.darkMode = false; - // expect(config.combinedData).toStrictEqual({ - // appName: 'app-name', - // useNativeWindow: false, - // darkMode: false, - // otherBuildSetting: 'registry', - // registrySetting: 'registry', - // otherLocalSetting: 'build', - // buildSetting: 'build', - // otherDefaultSetting: 'local', - // localSetting: 'local', - // defaultSetting: 'default', - // }); - // }); + config.regenerateCombinedConfigData(); + config.combinedData.darkMode = false; + expect(config.combinedData).toStrictEqual({ + appName: 'app-name', + useNativeWindow: false, + darkMode: false, + otherBuildSetting: 'registry', + registrySetting: 'registry', + otherLocalSetting: 'build', + buildSetting: 'build', + otherDefaultSetting: 'local', + localSetting: 'local', + defaultSetting: 'default', + }); + }); - // it('should not include any teams in the combined config', () => { - // const config = new Config(); - // config.reload = jest.fn(); - // config.init(configPath, appName, appPath); - // config.defaultConfigData = {}; - // config.localConfigData = {}; - // config.buildConfigData = {enableServerManagement: true}; - // config.registryConfigData = {}; - // config.predefinedTeams.push(team, team); - // config.useNativeWindow = false; - // config.localConfigData = {teams: [ - // team, - // { - // ...team, - // name: 'local-team-2', - // url: 'http://local-team-2.com', - // }, - // { - // ...team, - // name: 'local-team-1', - // order: 1, - // url: 'http://local-team-1.com', - // }, - // ]}; + it('should not include any teams in the combined config', () => { + const config = new Config(); + config.reload = jest.fn(); + config.init(configPath, appName, appPath); + config.defaultConfigData = {}; + config.localConfigData = {}; + config.buildConfigData = {enableServerManagement: true}; + config.registryConfigData = {}; + config.predefinedTeams.push(team, team); + config.useNativeWindow = false; + config.localConfigData = {teams: [ + team, + { + ...team, + name: 'local-team-2', + url: 'http://local-team-2.com', + }, + { + ...team, + name: 'local-team-1', + order: 1, + url: 'http://local-team-1.com', + }, + ]}; - // config.regenerateCombinedConfigData(); - // config.combinedData.darkMode = false; - // expect(config.combinedData).toStrictEqual({ - // appName: 'app-name', - // useNativeWindow: false, - // darkMode: false, - // enableServerManagement: true, - // }); - // }); - // }); + config.regenerateCombinedConfigData(); + config.combinedData.darkMode = false; + expect(config.combinedData).toStrictEqual({ + appName: 'app-name', + useNativeWindow: false, + darkMode: false, + enableServerManagement: true, + }); + }); + }); }); diff --git a/src/common/config/index.ts b/src/common/config/index.ts index 45289a4f..a0ff1f67 100644 --- a/src/common/config/index.ts +++ b/src/common/config/index.ts @@ -169,9 +169,6 @@ export class Config extends EventEmitter { get version() { return this.combinedData?.version ?? defaultPreferences.version; } - get teams() { - return this.combinedData?.teams ?? defaultPreferences.teams; - } get 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 - //delete (this.combinedData as any).teams; + delete (this.combinedData as any).teams; delete (this.combinedData as any).defaultTeams; 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; } } - /** - * 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 private writeFile = (filePath: string, configData: Partial, callback?: fs.NoParamCallback) => { if (!this.defaultConfigData) { diff --git a/src/common/servers/MattermostServer.ts b/src/common/servers/MattermostServer.ts index 6081d94a..061bc70e 100644 --- a/src/common/servers/MattermostServer.ts +++ b/src/common/servers/MattermostServer.ts @@ -3,7 +3,7 @@ import {v4 as uuid} from 'uuid'; -import {Team} from 'types/config'; +import {MattermostTeam, Team} from 'types/config'; import urlUtils from 'common/utils/url'; @@ -13,14 +13,13 @@ export class MattermostServer { url!: URL; isPredefined: boolean; - constructor(server: Team, isPredefined = false) { + constructor(server: Team, isPredefined: boolean) { this.id = uuid(); + this.name = server.name; this.updateURL(server.url); + this.isPredefined = isPredefined; - if (!this.url) { - throw new Error('Invalid url for creating a server'); - } } updateURL = (url: string) => { @@ -29,4 +28,13 @@ export class MattermostServer { 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, + }; + } } diff --git a/src/common/servers/serverManager.test.js b/src/common/servers/serverManager.test.js index d6218333..aa3bc882 100644 --- a/src/common/servers/serverManager.test.js +++ b/src/common/servers/serverManager.test.js @@ -32,9 +32,9 @@ describe('common/servers/serverManager', () => { }; serverManager.servers = new Map([['server-1', server]]); serverManager.tabs = new Map([ - ['tab-1', {id: 'tab-1', name: TAB_MESSAGING, isOpen: true, server}], - ['tab-2', {id: 'tab-2', name: TAB_PLAYBOOKS, server}], - ['tab-3', {id: 'tab-3', name: TAB_FOCALBOARD, server}], + ['tab-1', {id: 'tab-1', type: TAB_MESSAGING, isOpen: true, server}], + ['tab-2', {id: 'tab-2', type: TAB_PLAYBOOKS, server}], + ['tab-3', {id: 'tab-3', type: TAB_FOCALBOARD, server}], ]); serverManager.tabOrder = new Map([['server-1', ['tab-1', 'tab-2', 'tab-3']]]); serverManager.persistServers = jest.fn(); diff --git a/src/common/servers/serverManager.ts b/src/common/servers/serverManager.ts index 04ac4fe1..4abc7215 100644 --- a/src/common/servers/serverManager.ts +++ b/src/common/servers/serverManager.ts @@ -145,9 +145,9 @@ export class ServerManager extends EventEmitter { } 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. - filter((tab) => tab && tab.name !== TAB_MESSAGING). + filter((tab) => tab && tab.type !== TAB_MESSAGING). forEach((tab) => { if (parsedURL.pathname.match(new RegExp(`^${tab.url.pathname}(/(.+))?`))) { selectedTab = tab; @@ -187,6 +187,10 @@ export class ServerManager extends EventEmitter { }); 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 this.emit(SERVERS_URL_MODIFIED, [newServer.id]); this.persistServers(); @@ -230,6 +234,10 @@ export class ServerManager extends EventEmitter { this.remoteInfo.delete(serverId); this.servers.delete(serverId); + if (this.currentServerId === serverId && this.hasServers()) { + this.currentServerId = this.serverOrder[0]; + } + this.persistServers(); } @@ -274,7 +282,7 @@ export class ServerManager extends EventEmitter { } this.filterOutDuplicateTeams(); this.serverOrder = serverOrder; - if (Config.lastActiveTeam) { + if (Config.lastActiveTeam && this.serverOrder[Config.lastActiveTeam]) { this.currentServerId = this.serverOrder[Config.lastActiveTeam]; } else { this.currentServerId = this.serverOrder[0]; @@ -417,13 +425,13 @@ export class ServerManager extends EventEmitter { tabOrder.forEach((tabId) => { const tab = this.tabs.get(tabId); 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'); tab.isOpen = true; this.tabs.set(tabId, tab); 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'); tab.isOpen = true; this.tabs.set(tabId, tab); @@ -453,7 +461,7 @@ export class ServerManager extends EventEmitter { if (!view) { 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)); }; } diff --git a/src/common/tabs/BaseTabView.ts b/src/common/tabs/BaseTabView.ts index d5745340..f05737d2 100644 --- a/src/common/tabs/BaseTabView.ts +++ b/src/common/tabs/BaseTabView.ts @@ -3,9 +3,11 @@ import {v4 as uuid} from 'uuid'; +import {MattermostTab} from 'types/config'; + import {MattermostServer} from 'common/servers/MattermostServer'; -import {getTabViewName, TabType, TabView} from './TabView'; +import {TabType, TabView} from './TabView'; export default abstract class BaseTabView implements TabView { id: string; @@ -17,9 +19,6 @@ export default abstract class BaseTabView implements TabView { this.server = server; this.isOpen = isOpen; } - get name(): string { - return getTabViewName(this.server.name, this.type); - } get url(): URL { throw new Error('Not implemented'); } @@ -29,4 +28,12 @@ export default abstract class BaseTabView implements TabView { get shouldNotify(): boolean { return false; } + + toMattermostTab = (): MattermostTab => { + return { + id: this.id, + name: this.type, + isOpen: this.isOpen, + }; + } } diff --git a/src/common/tabs/TabView.ts b/src/common/tabs/TabView.ts index 060fc118..a4c8f57a 100644 --- a/src/common/tabs/TabView.ts +++ b/src/common/tabs/TabView.ts @@ -1,7 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import {Team} from 'types/config'; +import {MattermostTab, Team} from 'types/config'; import {MattermostServer} from 'common/servers/MattermostServer'; @@ -15,10 +15,11 @@ export interface TabView { server: MattermostServer; isOpen?: boolean; - get name(): string; get type(): TabType; get url(): URL; get shouldNotify(): boolean; + + toMattermostTab(): MattermostTab; } 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) { return tabType !== TAB_MESSAGING; } diff --git a/src/main/app/app.ts b/src/main/app/app.ts index 784612f8..5b0e36a5 100644 --- a/src/main/app/app.ts +++ b/src/main/app/app.ts @@ -116,7 +116,6 @@ export async function handleAppCertificateError(event: Event, webContents: WebCo certificateErrorCallbacks.set(errorID, callback); - // TODO: should we move this to window manager or provide a handler for dialogs? const mainWindow = MainWindow.get(); if (!mainWindow) { return; diff --git a/src/main/app/config.test.js b/src/main/app/config.test.js index beb9d851..557430a5 100644 --- a/src/main/app/config.test.js +++ b/src/main/app/config.test.js @@ -28,7 +28,6 @@ jest.mock('electron', () => ({ jest.mock('main/app/utils', () => ({ handleUpdateMenuEvent: jest.fn(), updateSpellCheckerLocales: jest.fn(), - updateServerInfos: jest.fn(), setLoggingLevel: jest.fn(), })); jest.mock('main/app/intercom', () => ({ @@ -51,7 +50,6 @@ jest.mock('main/views/loadingScreen', () => ({})); jest.mock('main/windows/windowManager', () => ({ handleUpdateConfig: jest.fn(), sendToRenderer: jest.fn(), - initializeCurrentServerName: jest.fn(), })); describe('main/app/config', () => { diff --git a/src/main/app/config.ts b/src/main/app/config.ts index f8a33929..887d737a 100644 --- a/src/main/app/config.ts +++ b/src/main/app/config.ts @@ -3,7 +3,7 @@ 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 Config from 'common/config'; @@ -12,12 +12,11 @@ import {Logger, setLoggingLevel} from 'common/log'; import AutoLauncher from 'main/AutoLauncher'; import {setUnreadBadgeSetting} from 'main/badge'; import {refreshTrayImages} from 'main/tray/tray'; -import ViewManager from 'main/views/viewManager'; import LoadingScreen from 'main/views/loadingScreen'; import WindowManager from 'main/windows/windowManager'; import {handleMainWindowIsShown} from './intercom'; -import {handleUpdateMenuEvent, updateServerInfos, updateSpellCheckerLocales} from './utils'; +import {handleUpdateMenuEvent, updateSpellCheckerLocales} from './utils'; const log = new Logger('App.Config'); @@ -60,14 +59,6 @@ export function handleUpdateTheme() { 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) { if (newConfig.logLevel) { setLoggingLevel(newConfig.logLevel); @@ -81,7 +72,6 @@ export function handleConfigUpdate(newConfig: CombinedConfig) { } if (app.isReady()) { - ViewManager.reloadConfiguration(); WindowManager.sendToRenderer(RELOAD_CONFIGURATION); } @@ -106,8 +96,6 @@ export function handleConfigUpdate(newConfig: CombinedConfig) { } if (app.isReady()) { - updateServerInfos(newConfig.teams); - WindowManager.initializeCurrentServerName(); handleMainWindowIsShown(); } diff --git a/src/main/app/initialize.test.js b/src/main/app/initialize.test.js index 2e2010bd..27e9dc0f 100644 --- a/src/main/app/initialize.test.js +++ b/src/main/app/initialize.test.js @@ -124,10 +124,10 @@ jest.mock('main/app/utils', () => ({ getDeeplinkingURL: jest.fn(), handleUpdateMenuEvent: jest.fn(), shouldShowTrayIcon: jest.fn(), - updateServerInfos: jest.fn(), updateSpellCheckerLocales: jest.fn(), wasUpdated: jest.fn(), initCookieManager: jest.fn(), + updateServerInfos: jest.fn(), })); jest.mock('main/appState', () => ({ on: jest.fn(), @@ -149,6 +149,11 @@ jest.mock('main/notifications', () => ({ displayDownloadCompleted: 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', () => ({ refreshTrayImages: jest.fn(), setupTray: jest.fn(), @@ -194,7 +199,6 @@ describe('main/app/initialize', () => { } }); Config.data = {}; - Config.teams = []; app.whenReady.mockResolvedValue(); app.requestSingleInstanceLock.mockReturnValue(true); app.getPath.mockImplementation((p) => `/basedir/${p}`); diff --git a/src/main/app/initialize.ts b/src/main/app/initialize.ts index 82b95d9b..edd61a8b 100644 --- a/src/main/app/initialize.ts +++ b/src/main/app/initialize.ts @@ -11,16 +11,9 @@ import { SWITCH_SERVER, FOCUS_BROWSERVIEW, QUIT, - DOUBLE_CLICK_ON_WINDOW, SHOW_NEW_SERVER_MODAL, - WINDOW_CLOSE, - WINDOW_MAXIMIZE, - WINDOW_MINIMIZE, - WINDOW_RESTORE, NOTIFY_MENTION, GET_DOWNLOAD_LOCATION, - SHOW_SETTINGS_WINDOW, - RELOAD_CONFIGURATION, SWITCH_TAB, CLOSE_TAB, OPEN_TAB, @@ -33,13 +26,17 @@ import { START_UPGRADE, START_UPDATE_DOWNLOAD, PING_DOMAIN, - MAIN_WINDOW_SHOWN, OPEN_APP_MENU, GET_CONFIGURATION, GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, 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'; import Config from 'common/config'; import {Logger} from 'common/log'; @@ -57,7 +54,7 @@ import CriticalErrorHandler from 'main/CriticalErrorHandler'; import downloadsManager from 'main/downloadsManager'; import i18nManager from 'main/i18nManager'; import parseArgs from 'main/ParseArgs'; -import SettingsWindow from 'main/windows/settingsWindow'; +import ServerManager from 'common/servers/serverManager'; import TrustedOriginsStore from 'main/trustedOrigins'; import {refreshTrayImages, setupTray} from 'main/tray/tray'; import UserActivityMonitor from 'main/UserActivityMonitor'; @@ -84,7 +81,6 @@ import { handleGetLocalConfiguration, handleUpdateTheme, updateConfiguration, - handleUpdateTeams, } from './config'; import { handleMainWindowIsShown, @@ -96,24 +92,26 @@ import { handleOpenAppMenu, handleOpenTab, handleQuit, - handleReloadConfig, handleRemoveServerModal, handleSelectDownload, handleSwitchServer, handleSwitchTab, handleUpdateLastActive, handlePingDomain, + handleGetOrderedServers, + handleGetOrderedTabsForServer, + handleGetLastActive, } from './intercom'; import { clearAppCache, getDeeplinkingURL, handleUpdateMenuEvent, shouldShowTrayIcon, - updateServerInfos, updateSpellCheckerLocales, wasUpdated, initCookieManager, migrateMacAppStore, + updateServerInfos, } from './utils'; export const mainProtocol = protocols?.[0]?.schemes?.[0]; @@ -153,7 +151,7 @@ export async function initialize() { // initialization that should run once the app is ready initializeInterCommunicationEventListeners(); - initializeAfterAppReady(); + await initializeAfterAppReady(); } // @@ -262,7 +260,6 @@ function initializeBeforeAppReady() { } function initializeInterCommunicationEventListeners() { - ipcMain.on(RELOAD_CONFIGURATION, handleReloadConfig); ipcMain.on(NOTIFY_MENTION, handleMentionNotification); ipcMain.handle('get-app-version', handleAppVersion); ipcMain.on(UPDATE_SHORTCUT_MENU, handleUpdateMenuEvent); @@ -280,17 +277,9 @@ function initializeInterCommunicationEventListeners() { ipcMain.on(QUIT, handleQuit); - ipcMain.on(DOUBLE_CLICK_ON_WINDOW, WindowManager.handleDoubleClick); - ipcMain.on(SHOW_NEW_SERVER_MODAL, handleNewServerModal); ipcMain.on(SHOW_EDIT_SERVER_MODAL, handleEditServerModal); 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_DOWNLOAD_LOCATION, handleSelectDownload); ipcMain.on(START_UPDATE_DOWNLOAD, handleStartDownload); @@ -298,12 +287,24 @@ function initializeInterCommunicationEventListeners() { ipcMain.handle(PING_DOMAIN, handlePingDomain); ipcMain.handle(GET_CONFIGURATION, handleGetConfiguration); ipcMain.handle(GET_LOCAL_CONFIGURATION, handleGetLocalConfiguration); - ipcMain.handle(UPDATE_TEAMS, handleUpdateTeams); 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() { - updateServerInfos(Config.teams); +async function initializeAfterAppReady() { + 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 const defaultSession = session.defaultSession; diff --git a/src/main/app/intercom.test.js b/src/main/app/intercom.test.js index 31f9e9a2..7f14528a 100644 --- a/src/main/app/intercom.test.js +++ b/src/main/app/intercom.test.js @@ -1,10 +1,10 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // See LICENSE.txt for license information. -import Config from 'common/config'; import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; import {getLocalURLString, getLocalPreload} from 'main/utils'; +import ServerManager from 'common/servers/serverManager'; import MainWindow from 'main/windows/mainWindow'; import ModalManager from 'main/views/modalManager'; import WindowManager from 'main/windows/windowManager'; @@ -26,6 +26,17 @@ jest.mock('common/tabs/TabView', () => ({ getDefaultConfigTeamFromTeam: jest.fn(), })); 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', () => ({ getLocalPreload: jest.fn(), getLocalURLString: jest.fn(), @@ -43,9 +54,6 @@ jest.mock('main/windows/mainWindow', () => ({ })); jest.mock('./app', () => ({})); -jest.mock('./utils', () => ({ - updateServerInfos: jest.fn(), -})); const tabs = [ { @@ -66,6 +74,7 @@ const tabs = [ ]; const teams = [ { + id: 'server-1', name: 'server-1', url: 'http://server-1.com', tabs, @@ -74,53 +83,46 @@ const teams = [ describe('main/app/intercom', () => { 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', () => { - handleCloseTab(null, 'server-1', 'tab-3'); - expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-2'); - expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-3').isOpen).toBe(false); + ServerManager.getTab.mockReturnValue({server: {id: 'server-1'}}); + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-2'}); + handleCloseTab(null, 'tab-3'); + expect(ServerManager.setTabIsOpen).toBeCalledWith('tab-3', false); + expect(WindowManager.switchTab).toBeCalledWith('tab-2'); }); }); 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', () => { - handleOpenTab(null, 'server-1', 'tab-1'); - expect(WindowManager.switchTab).toBeCalledWith('server-1', 'tab-1'); - expect(Config.teams.find((team) => team.name === 'server-1').tabs.find((tab) => tab.name === 'tab-1').isOpen).toBe(true); + handleOpenTab(null, 'tab-1'); + expect(WindowManager.switchTab).toBeCalledWith('tab-1'); }); }); describe('handleNewServerModal', () => { + let teamsCopy; + beforeEach(() => { getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); - Config.setServers.mockImplementation((value) => { - Config.teams = value; + teamsCopy = JSON.parse(JSON.stringify(teams)); + 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) => ({ ...team, @@ -128,10 +130,6 @@ describe('main/app/intercom', () => { })); }); - afterEach(() => { - delete Config.teams; - }); - it('should add new team to the config', async () => { const promise = Promise.resolve({ name: 'new-team', @@ -141,29 +139,42 @@ describe('main/app/intercom', () => { handleNewServerModal(); await promise; - expect(Config.teams).toContainEqual(expect.objectContaining({ + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'new-team', url: 'http://new-team.com', tabs, })); - expect(WindowManager.switchServer).toBeCalledWith('new-team', true); + expect(WindowManager.switchServer).toBeCalledWith('server-1', true); }); }); describe('handleEditServerModal', () => { + let teamsCopy; + beforeEach(() => { getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); - Config.setServers.mockImplementation((value) => { - Config.teams = value; + teamsCopy = JSON.parse(JSON.stringify(teams)); + ServerManager.getServer.mockImplementation((id) => { + if (id !== teamsCopy[0].id) { + return undefined; + } + return {...teamsCopy[0], toMattermostTeam: jest.fn()}; }); - Config.teams = JSON.parse(JSON.stringify(teams)); - }); - - afterEach(() => { - delete Config.teams; + ServerManager.editServer.mockImplementation((id, team) => { + if (id !== teamsCopy[0].id) { + return; + } + 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', () => { @@ -180,12 +191,14 @@ describe('main/app/intercom', () => { handleEditServerModal(null, 'server-1'); await promise; - expect(Config.teams).not.toContainEqual(expect.objectContaining({ + expect(teamsCopy).not.toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'server-1', url: 'http://server-1.com', tabs, })); - expect(Config.teams).toContainEqual(expect.objectContaining({ + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'new-team', url: 'http://new-team.com', tabs, @@ -194,19 +207,24 @@ describe('main/app/intercom', () => { }); describe('handleRemoveServerModal', () => { + let teamsCopy; + beforeEach(() => { getLocalURLString.mockReturnValue('/some/index.html'); getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); - Config.setServers.mockImplementation((value) => { - Config.teams = value; + teamsCopy = JSON.parse(JSON.stringify(teams)); + ServerManager.getServer.mockImplementation((id) => { + if (id !== teamsCopy[0].id) { + return undefined; + } + return teamsCopy[0]; }); - Config.teams = JSON.parse(JSON.stringify(teams)); - }); - - afterEach(() => { - delete Config.teams; + ServerManager.removeServer.mockImplementation(() => { + teamsCopy = []; + }); + ServerManager.getAllServers.mockReturnValue(teamsCopy); }); it('should remove the existing team', async () => { @@ -215,7 +233,8 @@ describe('main/app/intercom', () => { handleRemoveServerModal(null, 'server-1'); await promise; - expect(Config.teams).not.toContainEqual(expect.objectContaining({ + expect(teamsCopy).not.toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'server-1', url: 'http://server-1.com', tabs, @@ -226,7 +245,8 @@ describe('main/app/intercom', () => { const promise = Promise.resolve(false); ModalManager.addModal.mockReturnValue(promise); - expect(Config.teams).toContainEqual(expect.objectContaining({ + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'server-1', url: 'http://server-1.com', tabs, @@ -234,7 +254,8 @@ describe('main/app/intercom', () => { handleRemoveServerModal(null, 'server-1'); await promise; - expect(Config.teams).toContainEqual(expect.objectContaining({ + expect(teamsCopy).toContainEqual(expect.objectContaining({ + id: 'server-1', name: 'server-1', url: 'http://server-1.com', tabs, @@ -248,10 +269,8 @@ describe('main/app/intercom', () => { getLocalPreload.mockReturnValue('/some/preload.js'); MainWindow.get.mockReturnValue({}); - Config.setServers.mockImplementation((value) => { - Config.teams = value; - }); - Config.teams = JSON.parse(JSON.stringify([])); + ServerManager.getAllServers.mockReturnValue([]); + ServerManager.hasServers.mockReturnValue(false); }); it('should show welcomeScreen modal', async () => { @@ -270,18 +289,7 @@ describe('main/app/intercom', () => { MainWindow.get.mockReturnValue({ isVisible: () => 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)); + ServerManager.hasServers.mockReturnValue(true); handleMainWindowIsShown(); expect(ModalManager.addModal).not.toHaveBeenCalled(); diff --git a/src/main/app/intercom.ts b/src/main/app/intercom.ts index cca8f9bf..becad495 100644 --- a/src/main/app/intercom.ts +++ b/src/main/app/intercom.ts @@ -3,33 +3,24 @@ 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 Config from 'common/config'; import {Logger} from 'common/log'; -import {getDefaultConfigTeamFromTeam} from 'common/tabs/TabView'; import {ping} from 'common/utils/requests'; import {displayMention} from 'main/notifications'; import {getLocalPreload, getLocalURLString} from 'main/utils'; +import ServerManager from 'common/servers/serverManager'; import ModalManager from 'main/views/modalManager'; -import ViewManager from 'main/views/viewManager'; import WindowManager from 'main/windows/windowManager'; import MainWindow from 'main/windows/mainWindow'; import {handleAppBeforeQuit} from './app'; -import {updateServerInfos} from './utils'; const log = new Logger('App.Intercom'); -export function handleReloadConfig() { - log.debug('handleReloadConfig'); - - Config.reload(); - ViewManager.reloadConfiguration(); -} - export function handleAppVersion() { return { name: app.getName(), @@ -44,52 +35,50 @@ export function handleQuit(e: IpcMainEvent, reason: string, stack: string) { app.quit(); } -export function handleSwitchServer(event: IpcMainEvent, serverName: string) { - log.silly('handleSwitchServer', serverName); - WindowManager.switchServer(serverName); +export function handleSwitchServer(event: IpcMainEvent, serverId: string) { + log.silly('handleSwitchServer', serverId); + WindowManager.switchServer(serverId); } -export function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) { - log.silly('handleSwitchTab', {serverName, tabName}); - WindowManager.switchTab(serverName, tabName); +export function handleSwitchTab(event: IpcMainEvent, tabId: string) { + log.silly('handleSwitchTab', {tabId}); + WindowManager.switchTab(tabId); } -export function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) { - log.debug('handleCloseTab', {serverName, tabName}); +export function handleCloseTab(event: IpcMainEvent, tabId: string) { + log.debug('handleCloseTab', {tabId}); - const teams = Config.teams; - teams.forEach((team) => { - if (team.name === serverName) { - team.tabs.forEach((tab) => { - if (tab.name === tabName) { - tab.isOpen = false; - } - }); - } - }); - const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => tab.isOpen)[0].name; - WindowManager.switchTab(serverName, nextTab); - Config.setServers(teams); + const tab = ServerManager.getTab(tabId); + if (!tab) { + return; + } + ServerManager.setTabIsOpen(tabId, false); + const nextTab = ServerManager.getLastActiveTabForServer(tab.server.id); + WindowManager.switchTab(nextTab.id); } -export function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) { - log.debug('handleOpenTab', {serverName, tabName}); +export function handleOpenTab(event: IpcMainEvent, tabId: string) { + log.debug('handleOpenTab', {tabId}); - const teams = Config.teams; - teams.forEach((team) => { - if (team.name === serverName) { - team.tabs.forEach((tab) => { - if (tab.name === tabName) { - tab.isOpen = true; - } - }); - } - }); - WindowManager.switchTab(serverName, tabName); - Config.setServers(teams); + ServerManager.setTabIsOpen(tabId, true); + WindowManager.switchTab(tabId); } -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}); if (showWelcomeScreen) { @@ -117,8 +106,8 @@ export function handleMainWindowIsShown() { // eslint-disable-next-line no-undef // eslint-disable-next-line @typescript-eslint/ban-ts-comment // @ts-ignore - const showWelcomeScreen = () => !(Boolean(__SKIP_ONBOARDING_SCREENS__) || Config.teams.length); - const showNewServerModal = () => Config.teams.length === 0; + const showWelcomeScreen = () => !(Boolean(__SKIP_ONBOARDING_SCREENS__) || ServerManager.hasServers()); + const showNewServerModal = () => !ServerManager.hasServers(); /** * 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(); - log.debug('handleMainWindowIsShown', {configTeams: Config.teams, showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)}); + log.debug('handleMainWindowIsShown', {showWelcomeScreen, showNewServerModal, mainWindow: Boolean(mainWindow)}); if (mainWindow?.isVisible()) { handleShowOnboardingScreens(showWelcomeScreen(), showNewServerModal(), true); } else { @@ -148,16 +137,11 @@ export function handleNewServerModal() { if (!mainWindow) { return; } - const modalPromise = ModalManager.addModal('newServer', html, preload, Config.teams.map((team, index) => ({...team, index})), mainWindow, Config.teams.length === 0); + const modalPromise = ModalManager.addModal('newServer', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers()); if (modalPromise) { modalPromise.then((data) => { - const teams = Config.teams; - const order = teams.length; - const newTeam = getDefaultConfigTeamFromTeam({...data, order}); - teams.push(newTeam); - Config.setServers(teams); - updateServerInfos([newTeam]); - WindowManager.switchServer(newTeam.name, true); + const newTeam = ServerManager.addServer(data); + WindowManager.switchServer(newTeam.id, true); }).catch((e) => { // e is undefined for user cancellation if (e) { @@ -169,8 +153,8 @@ export function handleNewServerModal() { } } -export function handleEditServerModal(e: IpcMainEvent, name: string) { - log.debug('handleEditServerModal', name); +export function handleEditServerModal(e: IpcMainEvent, id: string) { + log.debug('handleEditServerModal', id); const html = getLocalURLString('editServer.html'); @@ -180,27 +164,21 @@ export function handleEditServerModal(e: IpcMainEvent, name: string) { if (!mainWindow) { return; } - const serverIndex = Config.teams.findIndex((team) => team.name === name); - if (serverIndex < 0) { + const server = ServerManager.getServer(id); + if (!server) { return; } - const modalPromise = ModalManager.addModal<{currentTeams: TeamWithIndex[]; team: TeamWithIndex}, Team>( + const modalPromise = ModalManager.addModal<{currentTeams: MattermostTeam[]; team: MattermostTeam}, Team>( 'editServer', html, preload, { - currentTeams: Config.teams.map((team, index) => ({...team, index})), - team: {...Config.teams[serverIndex], index: serverIndex}, + currentTeams: ServerManager.getAllServers().map((team) => team.toMattermostTeam()), + team: server.toMattermostTeam(), }, mainWindow); if (modalPromise) { - modalPromise.then((data) => { - const teams = Config.teams; - teams[serverIndex].name = data.name; - teams[serverIndex].url = data.url; - Config.setServers(teams); - updateServerInfos([teams[serverIndex]]); - }).catch((e) => { + modalPromise.then((data) => ServerManager.editServer(id, data)).catch((e) => { // e is undefined for user cancellation if (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) { - log.debug('handleRemoveServerModal', name); +export function handleRemoveServerModal(e: IpcMainEvent, id: string) { + log.debug('handleRemoveServerModal', id); const html = getLocalURLString('removeServer.html'); const preload = getLocalPreload('desktopAPI.js'); + const server = ServerManager.getServer(id); + if (!server) { + return; + } const mainWindow = MainWindow.get(); if (!mainWindow) { return; } - const modalPromise = ModalManager.addModal('removeServer', html, preload, name, mainWindow); + const modalPromise = ModalManager.addModal('removeServer', html, preload, server.name, mainWindow); if (modalPromise) { modalPromise.then((remove) => { if (remove) { - const teams = Config.teams; - 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); + ServerManager.removeServer(server.id); } }).catch((e) => { // e is undefined for user cancellation @@ -262,16 +232,11 @@ export function handleWelcomeScreenModal() { if (!mainWindow) { return; } - const modalPromise = ModalManager.addModal('welcomeScreen', html, preload, Config.teams.map((team, index) => ({...team, index})), mainWindow, Config.teams.length === 0); + const modalPromise = ModalManager.addModal('welcomeScreen', html, preload, ServerManager.getAllServers().map((team) => team.toMattermostTeam()), mainWindow, !ServerManager.hasServers()); if (modalPromise) { modalPromise.then((data) => { - const teams = Config.teams; - const order = teams.length; - const newTeam = getDefaultConfigTeamFromTeam({...data, order}); - teams.push(newTeam); - Config.setServers(teams); - updateServerInfos([newTeam]); - WindowManager.switchServer(newTeam.name, true); + const newTeam = ServerManager.addServer(data); + WindowManager.switchServer(newTeam.id, true); }).catch((e) => { // e is undefined for user cancellation if (e) { @@ -314,17 +279,10 @@ export async function handleSelectDownload(event: IpcMainInvokeEvent, startFrom: return result.filePaths[0]; } -export function handleUpdateLastActive(event: IpcMainEvent, serverName: string, viewName: string) { - log.debug('handleUpdateLastActive', {serverName, viewName}); +export function handleUpdateLastActive(event: IpcMainEvent, tabId: string) { + log.debug('handleUpdateLastActive', {tabId}); - const teams = Config.teams; - 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); + ServerManager.updateLastActive(tabId); } export function handlePingDomain(event: IpcMainInvokeEvent, url: string): Promise { diff --git a/src/main/app/utils.test.js b/src/main/app/utils.test.js index c3f50fc1..f432d4f2 100644 --- a/src/main/app/utils.test.js +++ b/src/main/app/utils.test.js @@ -5,15 +5,11 @@ import fs from 'fs-extra'; import {dialog, screen} from 'electron'; -import Config from 'common/config'; 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 {ServerInfo} from 'main/server/serverInfo'; -import {getDeeplinkingURL, updateServerInfos, resizeScreen, migrateMacAppStore} from './utils'; +import {getDeeplinkingURL, resizeScreen, migrateMacAppStore} from './utils'; jest.mock('fs-extra', () => ({ readFileSync: jest.fn(), @@ -43,9 +39,6 @@ jest.mock('common/config', () => ({ setServers: jest.fn(), })); jest.mock('common/JsonFileManager'); -jest.mock('common/utils/util', () => ({ - isVersionGreaterThanOrEqualTo: jest.fn(), -})); jest.mock('main/autoUpdater', () => ({})); jest.mock('main/constants', () => ({ @@ -56,9 +49,6 @@ jest.mock('main/i18nManager', () => ({ })); jest.mock('main/menus/app', () => ({})); jest.mock('main/menus/tray', () => ({})); -jest.mock('main/server/serverInfo', () => ({ - ServerInfo: jest.fn(), -})); jest.mock('main/tray/tray', () => ({})); jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/mainWindow', () => ({})); @@ -69,107 +59,6 @@ jest.mock('./initialize', () => ({ })); 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', () => { it('should return undefined if deeplinking URL is not last argument', () => { expect(getDeeplinkingURL(['mattermost', 'mattermost://server-1.com', '--oops'])).toBeUndefined(); diff --git a/src/main/app/utils.ts b/src/main/app/utils.ts index cee3dc1d..04f0bce0 100644 --- a/src/main/app/utils.ts +++ b/src/main/app/utils.ts @@ -7,17 +7,16 @@ import fs from 'fs-extra'; 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 {Boundaries} from 'types/utils'; import Config from 'common/config'; import {Logger} from 'common/log'; import JsonFileManager from 'common/JsonFileManager'; +import ServerManager from 'common/servers/serverManager'; import {MattermostServer} from 'common/servers/MattermostServer'; -import {TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView'; import urlUtils from 'common/utils/url'; -import Utils from 'common/utils/util'; import {APP_MENU_WILL_CLOSE} from 'common/communication'; 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> = []; - teams.forEach((team) => { - const serverInfo = new ServerInfo(new MattermostServer(team)); - serverInfos.push(serverInfo.promise); - }); - Promise.all(serverInfos).then((data: Array) => { - 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, 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, 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() { log.debug('handleUpdateMenuEvent'); @@ -117,7 +63,7 @@ export function handleUpdateMenuEvent() { // set up context menu for tray icon if (shouldShowTrayIcon()) { - const tMenu = createTrayMenu(Config.data!); + const tMenu = createTrayMenu(); setTrayMenu(tMenu); } } @@ -291,3 +237,18 @@ export function migrateMacAppStore() { log.error('MAS: An error occurred importing the existing configuration', e); } } + +export async function updateServerInfos(servers: MattermostServer[]) { + const map: Map = 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); +} diff --git a/src/main/appState.ts b/src/main/appState.ts index dcb2a5aa..89109a6f 100644 --- a/src/main/appState.ts +++ b/src/main/appState.ts @@ -4,14 +4,11 @@ import events from 'events'; 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 ServerManager from 'common/servers/serverManager'; import WindowManager from './windows/windowManager'; -const log = new Logger('AppState'); - const status = { unreads: new Map(), mentions: new Map(), @@ -19,13 +16,13 @@ const status = { emitter: new events.EventEmitter(), }; -const emitMentions = (serverName: string) => { - const newMentions = getMentions(serverName); - const newUnreads = getUnreads(serverName); - const isExpired = getIsExpired(serverName); +const emitMentions = (viewId: string) => { + const newMentions = getMentions(viewId); + const newUnreads = getUnreads(viewId); + const isExpired = getIsExpired(viewId); - WindowManager.sendToRenderer(UPDATE_MENTIONS, serverName, newMentions, newUnreads, isExpired); - log.silly('emitMentions', {serverName, isExpired, newMentions, newUnreads}); + WindowManager.sendToRenderer(UPDATE_MENTIONS, viewId, newMentions, newUnreads, isExpired); + ServerManager.getViewLog(viewId, 'AppState').silly('emitMentions', {isExpired, newMentions, newUnreads}); emitStatus(); }; @@ -50,17 +47,17 @@ const emitStatus = () => { 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') { - status.unreads.set(serverName, Boolean(unreads)); + status.unreads.set(viewId, Boolean(unreads)); } - status.mentions.set(serverName, mentions || 0); - emitMentions(serverName); + status.mentions.set(viewId, mentions || 0); + emitMentions(viewId); }; -export const updateUnreads = (serverName: string, unreads: boolean) => { - status.unreads.set(serverName, Boolean(unreads)); - emitMentions(serverName); +export const updateUnreads = (viewId: string, unreads: boolean) => { + status.unreads.set(viewId, Boolean(unreads)); + emitMentions(viewId); }; export const updateBadge = () => { @@ -70,16 +67,16 @@ export const updateBadge = () => { emitBadge(expired, mentions, unreads); }; -const getUnreads = (serverName: string) => { - return status.unreads.get(serverName) || false; +const getUnreads = (viewId: string) => { + return status.unreads.get(viewId) || false; }; -const getMentions = (serverName: 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. +const getMentions = (viewId: string) => { + 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) => { - return status.expired.get(serverName) || false; +const getIsExpired = (viewId: string) => { + return status.expired.get(viewId) || false; }; const totalMentions = () => { @@ -111,19 +108,19 @@ const anyExpired = () => { // add any other event emitter methods if needed 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 old = status.expired.get(serverName); - status.expired.set(serverName, isExpired); + const old = status.expired.get(viewId); + status.expired.set(viewId, isExpired); if (typeof old !== 'undefined' && old !== isExpired) { emitTray(); } - emitMentions(serverName); + emitMentions(viewId); }; -ipcMain.on(SESSION_EXPIRED, (event, isExpired, serverName) => { +ipcMain.on(SESSION_EXPIRED, (event, isExpired, viewId) => { if (isExpired) { - log.debug('SESSION_EXPIRED', {isExpired, serverName}); + ServerManager.getViewLog(viewId, 'AppState').debug('SESSION_EXPIRED', isExpired); } - setSessionExpired(serverName, isExpired); + setSessionExpired(viewId, isExpired); }); diff --git a/src/main/diagnostics/steps/step3.serverConnectivity.ts b/src/main/diagnostics/steps/step3.serverConnectivity.ts index f4a17387..006e46d8 100644 --- a/src/main/diagnostics/steps/step3.serverConnectivity.ts +++ b/src/main/diagnostics/steps/step3.serverConnectivity.ts @@ -4,7 +4,7 @@ import {ElectronLog} from 'electron-log'; import {DiagnosticStepResponse} from 'types/diagnostics'; -import Config from 'common/config'; +import ServerManager from 'common/servers/serverManager'; import DiagnosticsStep from '../DiagnosticStep'; @@ -15,7 +15,7 @@ const stepDescriptiveName = 'serverConnectivity'; const run = async (logger: ElectronLog): Promise => { try { - const teams = Config.teams || []; + const teams = ServerManager.getAllServers(); await Promise.all(teams.map(async (team) => { logger.debug('Pinging server: ', team.url); diff --git a/src/main/menus/app.test.js b/src/main/menus/app.test.js index 1b988db3..25edc7cb 100644 --- a/src/main/menus/app.test.js +++ b/src/main/menus/app.test.js @@ -6,7 +6,7 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state'; import {localizeMessage} from 'main/i18nManager'; -import WindowManager from 'main/windows/windowManager'; +import ServerManager from 'common/servers/serverManager'; import {createTemplate} from './app'; @@ -49,13 +49,18 @@ jest.mock('macos-notification-state', () => ({ jest.mock('main/i18nManager', () => ({ 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/downloadsManager', () => ({ hasDownloads: jest.fn(), })); jest.mock('main/views/viewManager', () => ({})); jest.mock('main/windows/windowManager', () => ({ - getCurrentTeamName: jest.fn(), sendToRenderer: jest.fn(), })); jest.mock('main/windows/settingsWindow', () => ({})); @@ -66,54 +71,42 @@ jest.mock('common/tabs/TabView', () => ({ describe('main/menus/app', () => { const config = { 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', }; + 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(() => { + ServerManager.getCurrentServer.mockReturnValue(servers[0]); + ServerManager.getOrderedServers.mockReturnValue(servers); + ServerManager.getOrderedTabsForServer.mockReturnValue(tabs); getDarwinDoNotDisturb.mockReturnValue(false); }); @@ -192,6 +185,7 @@ describe('main/menus/app', () => { return id; } }); + ServerManager.hasServers.mockReturnValue(true); const menu = createTemplate(config); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); @@ -209,6 +203,7 @@ describe('main/menus/app', () => { return ''; } }); + ServerManager.hasServers.mockReturnValue(true); const modifiedConfig = { ...config, enableServerManagement: false, @@ -230,11 +225,8 @@ describe('main/menus/app', () => { return ''; } }); - const modifiedConfig = { - ...config, - teams: [], - }; - const menu = createTemplate(modifiedConfig); + ServerManager.hasServers.mockReturnValue(false); + const menu = createTemplate(config); const fileMenu = menu.find((item) => item.label === '&AppName' || item.label === '&File'); const signInOption = fileMenu.submenu.find((item) => item.label === 'Sign in to Another Server'); expect(signInOption).toBe(undefined); @@ -247,31 +239,27 @@ describe('main/menus/app', () => { } return id; }); - const modifiedConfig = { - teams: [...Array(15).keys()].map((key) => ({ - name: `server-${key}`, - url: `http://server-${key}.com`, - order: (key + 5) % 15, - lastActiveTab: 0, - tab: [ - { - name: 'TAB_MESSAGING', - isOpen: true, - }, - ], - })), - }; - const menu = createTemplate(modifiedConfig); + const modifiedServers = [...Array(15).keys()].map((key) => ({ + id: `server-${key}`, + name: `server-${key}`, + url: `http://server-${key}.com`, + })); + const modifiedTabs = [ + { + id: 'tab-1', + type: 'TAB_MESSAGING', + isOpen: true, + }, + ]; + ServerManager.getOrderedServers.mockReturnValue(modifiedServers); + ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs); + const menu = createTemplate(config); 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}`); expect(menuItem).not.toBe(undefined); } - for (let i = 0; i < 4; i++) { - const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`); - expect(menuItem).not.toBe(undefined); - } - for (let i = 4; i < 10; i++) { + for (let i = 9; i < 15; i++) { const menuItem = windowMenu.submenu.find((item) => item.label === `server-${i}`); expect(menuItem).toBe(undefined); } @@ -287,31 +275,21 @@ describe('main/menus/app', () => { } return id; }); - WindowManager.getCurrentTeamName.mockImplementation(() => config.teams[0].name); + ServerManager.getCurrentServer.mockImplementation(() => ({id: servers[0].id})); - const modifiedConfig = { - teams: [ - { - ...config.teams[0], - tabs: [...Array(15).keys()].map((key) => ({ - name: `tab-${key}`, - isOpen: true, - order: (key + 5) % 15, - })), - }, - ], - }; - const menu = createTemplate(modifiedConfig); + const modifiedTabs = [...Array(15).keys()].map((key) => ({ + id: `tab-${key}`, + type: `tab-${key}`, + isOpen: true, + })); + ServerManager.getOrderedTabsForServer.mockReturnValue(modifiedTabs); + const menu = createTemplate(config); 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}`); expect(menuItem).not.toBe(undefined); } - for (let i = 0; i < 4; i++) { - const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`); - expect(menuItem).not.toBe(undefined); - } - for (let i = 4; i < 10; i++) { + for (let i = 9; i < 15; i++) { const menuItem = windowMenu.submenu.find((item) => item.label === ` tab-${i}`); expect(menuItem).toBe(undefined); } diff --git a/src/main/menus/app.ts b/src/main/menus/app.ts index 8e3b192d..b52f2448 100644 --- a/src/main/menus/app.ts +++ b/src/main/menus/app.ts @@ -12,6 +12,7 @@ import {getTabDisplayName, TabType} from 'common/tabs/TabView'; import {Config} from 'common/config'; import {localizeMessage} from 'main/i18nManager'; +import ServerManager from 'common/servers/serverManager'; import WindowManager from 'main/windows/windowManager'; import {UpdateManager} from 'main/autoUpdater'; 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({ label: localizeMessage('main.menus.app.file.signInToAnotherServer', 'Sign in to Another Server'), click() { @@ -231,7 +232,7 @@ export function createTemplate(config: Config, updateManager: UpdateManager) { }], }); - const teams = config.teams || []; + const teams = ServerManager.getOrderedServers(); const windowMenu = { id: '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'), accelerator: 'CmdOrCtrl+W', }, separatorItem, - ...(config.teams.length ? [{ + ...(ServerManager.hasServers() ? [{ label: localizeMessage('main.menus.app.window.showServers', 'Show Servers'), accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+S`, click() { 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 = []; items.push({ label: team.name, accelerator: `${process.platform === 'darwin' ? 'Cmd+Ctrl' : 'Ctrl+Shift'}+${i + 1}`, click() { - WindowManager.switchServer(team.name); + WindowManager.switchServer(team.id); }, }); - if (WindowManager.getCurrentTeamName() === team.name) { - team.tabs.filter((tab) => tab.isOpen).sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).forEach((tab, i) => { + if (ServerManager.getCurrentServer().id === team.id) { + ServerManager.getOrderedTabsForServer(team.id).slice(0, 9).forEach((tab, i) => { 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}`, click() { - WindowManager.switchTab(team.name, tab.name); + WindowManager.switchTab(tab.id); }, }); }); diff --git a/src/main/menus/tray.test.js b/src/main/menus/tray.test.js index 563f0f8d..2d2ae6c7 100644 --- a/src/main/menus/tray.test.js +++ b/src/main/menus/tray.test.js @@ -3,41 +3,35 @@ 'use strict'; +import ServerManager from 'common/servers/serverManager'; + import {createTemplate} from './tray'; jest.mock('main/i18nManager', () => ({ localizeMessage: jest.fn(), })); +jest.mock('common/servers/serverManager', () => ({ + getOrderedServers: jest.fn(), +})); + jest.mock('main/windows/settingsWindow', () => ({})); jest.mock('main/windows/windowManager', () => ({})); describe('main/menus/tray', () => { it('should show the first 9 servers (using order)', () => { - const config = { - teams: [...Array(15).keys()].map((key) => ({ - name: `server-${key}`, - url: `http://server-${key}.com`, - order: (key + 5) % 15, - lastActiveTab: 0, - tab: [ - { - name: 'TAB_MESSAGING', - isOpen: true, - }, - ], - })), - }; - const menu = createTemplate(config); - for (let i = 10; i < 15; i++) { + const servers = [...Array(15).keys()].map((key) => ({ + id: `server-${key}`, + name: `server-${key}`, + url: `http://server-${key}.com`, + })); + ServerManager.getOrderedServers.mockReturnValue(servers); + const menu = createTemplate(); + for (let i = 0; i < 9; i++) { const menuItem = menu.find((item) => item.label === `server-${i}`); expect(menuItem).not.toBe(undefined); } - for (let i = 0; i < 4; i++) { - const menuItem = menu.find((item) => item.label === `server-${i}`); - expect(menuItem).not.toBe(undefined); - } - for (let i = 4; i < 10; i++) { + for (let i = 9; i < 15; i++) { const menuItem = menu.find((item) => item.label === `server-${i}`); expect(menuItem).toBe(undefined); } diff --git a/src/main/menus/tray.ts b/src/main/menus/tray.ts index aab87d8d..79bb2591 100644 --- a/src/main/menus/tray.ts +++ b/src/main/menus/tray.ts @@ -4,20 +4,20 @@ 'use strict'; import {Menu, MenuItem, MenuItemConstructorOptions} from 'electron'; -import {CombinedConfig} from 'types/config'; import WindowManager from 'main/windows/windowManager'; import {localizeMessage} from 'main/i18nManager'; +import ServerManager from 'common/servers/serverManager'; import SettingsWindow from 'main/windows/settingsWindow'; -export function createTemplate(config: CombinedConfig) { - const teams = config.teams; +export function createTemplate() { + const teams = ServerManager.getOrderedServers(); const template = [ - ...teams.sort((teamA, teamB) => teamA.order - teamB.order).slice(0, 9).map((team) => { + ...teams.slice(0, 9).map((team) => { return { label: team.name.length > 50 ? `${team.name.slice(0, 50)}...` : team.name, click: () => { - WindowManager.switchServer(team.name); + WindowManager.switchServer(team.id); }, }; }), { @@ -36,7 +36,7 @@ export function createTemplate(config: CombinedConfig) { return template; } -export function createMenu(config: CombinedConfig) { +export function createMenu() { // Electron is enforcing certain variables that it doesn't need - return Menu.buildFromTemplate(createTemplate(config) as Array); + return Menu.buildFromTemplate(createTemplate() as Array); } diff --git a/src/main/notifications/index.test.js b/src/main/notifications/index.test.js index 16b412da..4a99d567 100644 --- a/src/main/notifications/index.test.js +++ b/src/main/notifications/index.test.js @@ -11,7 +11,6 @@ import {getDoNotDisturb as getDarwinDoNotDisturb} from 'macos-notification-state import {PLAY_SOUND} from 'common/communication'; import Config from 'common/config'; -import {TAB_MESSAGING} from 'common/tabs/TabView'; import {localizeMessage} from 'main/i18nManager'; @@ -249,7 +248,7 @@ describe('main/notifications', () => { ); const mention = mentions.find((m) => m.body === 'mention_click_body'); 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', () => { diff --git a/src/main/notifications/index.ts b/src/main/notifications/index.ts index 4d0dd32c..0a4e2001 100644 --- a/src/main/notifications/index.ts +++ b/src/main/notifications/index.ts @@ -10,7 +10,6 @@ import {MentionData} from 'types/notification'; import Config from 'common/config'; import {PLAY_SOUND} from 'common/communication'; import {Logger} from 'common/log'; -import {TAB_MESSAGING} from 'common/tabs/TabView'; import ViewManager from '../views/viewManager'; import MainWindow from '../windows/mainWindow'; @@ -76,7 +75,7 @@ export function displayMention(title: string, body: string, channel: {id: string mention.on('click', () => { log.debug('notification click', serverName, mention); if (serverName) { - WindowManager.switchTab(serverName, TAB_MESSAGING); + WindowManager.switchTab(view.id); webcontents.send('notification-clicked', {channel, teamId, url}); } }); diff --git a/src/main/preload/desktopAPI.js b/src/main/preload/desktopAPI.js index 85f74b51..a789e3ac 100644 --- a/src/main/preload/desktopAPI.js +++ b/src/main/preload/desktopAPI.js @@ -9,8 +9,6 @@ import {ipcRenderer, contextBridge} from 'electron'; import { GET_LANGUAGE_INFORMATION, QUIT, - GET_VIEW_NAME, - GET_VIEW_WEBCONTENTS_ID, OPEN_APP_MENU, CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN, @@ -29,7 +27,6 @@ import { HISTORY, CHECK_FOR_UPDATES, UPDATE_CONFIGURATION, - UPDATE_TEAMS, GET_CONFIGURATION, GET_DARK_MODE, REQUEST_HAS_DOWNLOADS, @@ -85,17 +82,16 @@ import { LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY, 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'; 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', { platform: process.platform, env: { @@ -117,14 +113,14 @@ contextBridge.exposeInMainWorld('desktop', { openAppMenu: () => ipcRenderer.send(OPEN_APP_MENU), closeTeamsDropdown: () => ipcRenderer.send(CLOSE_TEAMS_DROPDOWN), openTeamsDropdown: () => ipcRenderer.send(OPEN_TEAMS_DROPDOWN), - switchTab: (serverName, tabName) => ipcRenderer.send(SWITCH_TAB, serverName, tabName), - closeTab: (serverName, tabName) => ipcRenderer.send(CLOSE_TAB, serverName, tabName), + switchTab: (tabId) => ipcRenderer.send(SWITCH_TAB, tabId), + closeTab: (tabId) => ipcRenderer.send(CLOSE_TAB, tabId), closeWindow: () => ipcRenderer.send(WINDOW_CLOSE), minimizeWindow: () => ipcRenderer.send(WINDOW_MINIMIZE), maximizeWindow: () => ipcRenderer.send(WINDOW_MAXIMIZE), restoreWindow: () => ipcRenderer.send(WINDOW_RESTORE), 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), closeDownloadsDropdown: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN), closeDownloadsDropdownMenu: () => ipcRenderer.send(CLOSE_DOWNLOADS_DROPDOWN_MENU), @@ -133,25 +129,31 @@ contextBridge.exposeInMainWorld('desktop', { checkForUpdates: () => ipcRenderer.send(CHECK_FOR_UPDATES), updateConfiguration: (saveQueueItems) => ipcRenderer.send(UPDATE_CONFIGURATION, saveQueueItems), - updateTeams: (updatedTeams) => ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams), - getConfiguration: (option) => ipcRenderer.invoke(GET_CONFIGURATION, option), + updateServerOrder: (serverOrder) => ipcRenderer.send(UPDATE_SERVER_ORDER, serverOrder), + 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'), getDarkMode: () => ipcRenderer.invoke(GET_DARK_MODE), requestHasDownloads: () => ipcRenderer.invoke(REQUEST_HAS_DOWNLOADS), getFullScreenStatus: () => ipcRenderer.invoke(GET_FULL_SCREEN_STATUS), getAvailableSpellCheckerLanguages: () => ipcRenderer.invoke(GET_AVAILABLE_SPELL_CHECKER_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), getLanguageInformation: () => ipcRenderer.invoke(GET_LANGUAGE_INFORMATION), onSynchronizeConfig: (listener) => ipcRenderer.on('synchronize-config', () => listener()), onReloadConfiguration: (listener) => ipcRenderer.on(RELOAD_CONFIGURATION, () => listener()), 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)), - onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewName) => listener(viewName)), - onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewName, err, loadUrl) => listener(viewName, err, loadUrl)), - onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverName, tabName) => listener(serverName, tabName)), + onLoadRetry: (listener) => ipcRenderer.on(LOAD_RETRY, (_, viewId, retry, err, loadUrl) => listener(viewId, retry, err, loadUrl)), + onLoadSuccess: (listener) => ipcRenderer.on(LOAD_SUCCESS, (_, viewId) => listener(viewId)), + onLoadFailed: (listener) => ipcRenderer.on(LOAD_FAILED, (_, viewId, err, loadUrl) => listener(viewId, err, loadUrl)), + onSetActiveView: (listener) => ipcRenderer.on(SET_ACTIVE_VIEW, (_, serverId, tabId) => listener(serverId, tabId)), onMaximizeChange: (listener) => ipcRenderer.on(MAXIMIZE_CHANGE, (_, maximize) => listener(maximize)), onEnterFullScreen: (listener) => ipcRenderer.on('enter-full-screen', () => listener()), onLeaveFullScreen: (listener) => ipcRenderer.on('leave-full-screen', () => listener()), @@ -195,10 +197,10 @@ contextBridge.exposeInMainWorld('desktop', { serverDropdown: { requestInfo: () => ipcRenderer.send(REQUEST_TEAMS_DROPDOWN_INFO), 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), - showEditServerModal: (serverName) => ipcRenderer.send(SHOW_EDIT_SERVER_MODAL, serverName), - showRemoveServerModal: (serverName) => ipcRenderer.send(SHOW_REMOVE_SERVER_MODAL, serverName), + showEditServerModal: (serverId) => ipcRenderer.send(SHOW_EDIT_SERVER_MODAL, serverId), + showRemoveServerModal: (serverId) => ipcRenderer.send(SHOW_REMOVE_SERVER_MODAL, serverId), onUpdateServerDropdown: (listener) => ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (_, teams, diff --git a/src/main/preload/mattermost.js b/src/main/preload/mattermost.js index caafec15..7f65b38e 100644 --- a/src/main/preload/mattermost.js +++ b/src/main/preload/mattermost.js @@ -25,8 +25,7 @@ import { BROWSER_HISTORY_PUSH, APP_LOGGED_IN, APP_LOGGED_OUT, - GET_VIEW_NAME, - GET_VIEW_WEBCONTENTS_ID, + GET_VIEW_INFO_FOR_TEST, DISPATCH_GET_DESKTOP_SOURCES, DESKTOP_SOURCES_RESULT, VIEW_FINISHED_RESIZING, @@ -45,15 +44,14 @@ const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours let appVersion; let appName; let sessionExpired; -let viewName; +let viewId; let shouldSendNotifications; 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), + getViewInfoForTest: () => ipcRenderer.invoke(GET_VIEW_INFO_FOR_TEST), }); } @@ -92,8 +90,8 @@ window.addEventListener('load', () => { return; } watchReactAppUntilInitialized(() => { - ipcRenderer.send(REACT_APP_INITIALIZED, viewName); - ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewName); + ipcRenderer.send(REACT_APP_INITIALIZED, viewId); + ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId); }); }); @@ -152,27 +150,27 @@ window.addEventListener('message', ({origin, data = {}} = {}) => { } case 'browser-history-push': { const {path} = message; - ipcRenderer.send(BROWSER_HISTORY_PUSH, viewName, path); + ipcRenderer.send(BROWSER_HISTORY_PUSH, viewId, path); break; } case 'history-button': { - ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewName); + ipcRenderer.send(BROWSER_HISTORY_BUTTON, viewId); break; } case 'get-desktop-sources': { - ipcRenderer.send(DISPATCH_GET_DESKTOP_SOURCES, viewName, message); + ipcRenderer.send(DISPATCH_GET_DESKTOP_SOURCES, viewId, message); break; } case CALLS_JOIN_CALL: { - ipcRenderer.send(CALLS_JOIN_CALL, viewName, message); + ipcRenderer.send(CALLS_JOIN_CALL, viewId, message); break; } case CALLS_WIDGET_SHARE_SCREEN: { - ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, viewName, message); + ipcRenderer.send(CALLS_WIDGET_SHARE_SCREEN, viewId, message); break; } case CALLS_LEAVE_CALL: { - ipcRenderer.send(CALLS_LEAVE_CALL, viewName, message); + ipcRenderer.send(CALLS_LEAVE_CALL, viewId, message); break; } } @@ -202,12 +200,12 @@ const findUnread = (favicon) => { const result = document.getElementsByClassName(classPair); 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) => { - if (typeof viewName === 'undefined') { - viewName = server; + if (typeof viewId === 'undefined') { + viewId = server; } if (isReactAppInitialized()) { findUnread(favicon); @@ -219,13 +217,13 @@ ipcRenderer.on(IS_UNREAD, (event, favicon, server) => { }); ipcRenderer.on(SET_VIEW_OPTIONS, (_, name, shouldNotify) => { - viewName = name; + viewId = name; shouldSendNotifications = shouldNotify; }); function getUnreadCount() { // LHS not found => Log out => Count should be 0, but session may be expired. - if (typeof viewName !== 'undefined') { + if (typeof viewId !== 'undefined') { let isExpired; if (document.getElementById('sidebar-left') === null) { const extraParam = (new URLSearchParams(window.location.search)).get('extra'); @@ -235,7 +233,7 @@ function getUnreadCount() { } if (isExpired !== sessionExpired) { 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) => { 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) { - ipcRenderer.send(APP_LOGGED_OUT, viewName); + ipcRenderer.send(APP_LOGGED_OUT, viewId); } }); diff --git a/src/main/server/serverInfo.ts b/src/main/server/serverInfo.ts index 38f07f30..133d7bb2 100644 --- a/src/main/server/serverInfo.ts +++ b/src/main/server/serverInfo.ts @@ -4,72 +4,56 @@ import {ClientConfig, RemoteInfo} from 'types/server'; import {MattermostServer} from 'common/servers/MattermostServer'; -import {Logger} from 'common/log'; import {getServerAPI} from './serverAPI'; -const log = new Logger('ServerInfo'); - export class ServerInfo { - server: MattermostServer; - remoteInfo: RemoteInfo; - promise: Promise; - onRetrievedRemoteInfo?: (result?: RemoteInfo | string) => void; + private server: MattermostServer; + private remoteInfo: RemoteInfo; constructor(server: MattermostServer) { this.server = server; - this.remoteInfo = {name: server.name}; - - this.promise = new Promise((resolve) => { - this.onRetrievedRemoteInfo = resolve; - }); - this.getRemoteInfo(); + this.remoteInfo = {}; } - getRemoteInfo = () => { - getServerAPI( + fetchRemoteInfo = async () => { + await this.getRemoteInfo( new URL(`${this.server.url.toString()}/api/v4/config/client?format=old`), - false, this.onGetConfig, - this.onRetrievedRemoteInfo, - this.onRetrievedRemoteInfo); - - getServerAPI>( + ); + await this.getRemoteInfo>( new URL(`${this.server.url.toString()}/api/v4/plugins/webapp`), - false, this.onGetPlugins, - this.onRetrievedRemoteInfo, - this.onRetrievedRemoteInfo); + ); + + return this.remoteInfo; } - onGetConfig = (data: ClientConfig) => { + private getRemoteInfo = ( + url: URL, + callback: (data: T) => void, + ) => { + return new Promise((resolve, reject) => { + getServerAPI( + 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.siteURL = data.SiteURL; 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.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' - ); } } diff --git a/src/main/views/MattermostView.test.js b/src/main/views/MattermostView.test.js index 474cff99..0e7aa2f1 100644 --- a/src/main/views/MattermostView.test.js +++ b/src/main/views/MattermostView.test.js @@ -180,7 +180,7 @@ describe('main/views/MattermostView', () => { await expect(promise).rejects.toThrow(error); expect(mattermostView.view.webContents.loadURL).toBeCalledWith('http://server-1.com', expect.any(Object)); 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); jest.runAllTimers(); expect(retryInBackgroundFn).toBeCalled(); @@ -374,14 +374,7 @@ describe('main/views/MattermostView', () => { const mattermostView = new MattermostView(tabView, {}, {}); mattermostView.view.webContents.destroy = jest.fn(); mattermostView.destroy(); - expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.name, 0, false); - }); - - it('should destroy context menu', () => { - const mattermostView = new MattermostView(tabView, {}, {}); - mattermostView.view.webContents.destroy = jest.fn(); - mattermostView.destroy(); - expect(contextMenu.dispose).toBeCalled(); + expect(appState.updateMentions).toBeCalledWith(mattermostView.tab.id, 0, false); }); it('should clear outstanding timeouts', () => { @@ -479,12 +472,12 @@ describe('main/views/MattermostView', () => { it('should parse mentions from title', () => { 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', () => { mattermostView.updateMentionsFromTitle('* Mattermost'); - expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.name, 0); + expect(appState.updateMentions).toHaveBeenCalledWith(mattermostView.tab.id, 0); }); }); }); diff --git a/src/main/views/MattermostView.ts b/src/main/views/MattermostView.ts index 45cad123..b2957ea1 100644 --- a/src/main/views/MattermostView.ts +++ b/src/main/views/MattermostView.ts @@ -18,12 +18,12 @@ import { SET_VIEW_OPTIONS, LOADSCREEN_END, BROWSER_HISTORY_BUTTON, + SERVERS_URL_MODIFIED, } from 'common/communication'; +import ServerManager from 'common/servers/serverManager'; import {Logger} from 'common/log'; 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 WindowManager from 'main/windows/windowManager'; @@ -45,7 +45,6 @@ const titleParser = /(\((\d+)\) )?(\* )?/g; export class MattermostView extends EventEmitter { tab: TabView; - serverInfo: ServerInfo; isVisible: boolean; private log: Logger; @@ -60,10 +59,9 @@ export class MattermostView extends EventEmitter { private maxRetries: number; private altPressStatus: boolean; - constructor(tab: TabView, serverInfo: ServerInfo, options: BrowserViewConstructorOptions) { + constructor(tab: TabView, options: BrowserViewConstructorOptions) { super(); this.tab = tab; - this.serverInfo = serverInfo; const preload = getLocalPreload('preload.js'); this.options = Object.assign({}, options); @@ -81,7 +79,7 @@ export class MattermostView extends EventEmitter { this.view = new BrowserView(this.options); this.resetLoadingStatus(); - this.log = new Logger(this.name, 'MattermostView'); + this.log = ServerManager.getViewLog(this.id, 'MattermostView'); this.log.verbose('View created'); this.view.webContents.on('did-finish-load', this.handleDidFinishLoad); @@ -103,10 +101,12 @@ export class MattermostView extends EventEmitter { MainWindow.get()?.on('blur', () => { this.altPressStatus = false; }); + + ServerManager.on(SERVERS_URL_MODIFIED, this.handleServerWasModified); } - get name() { - return this.tab.name; + get id() { + return this.tab.id; } get isAtRoot() { return this.atRoot; @@ -121,17 +121,6 @@ export class MattermostView extends EventEmitter { 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) => { if (this.isLoggedIn === loggedIn) { 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()); } - 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) => { if (!this.tab) { return; @@ -201,9 +180,9 @@ export class MattermostView extends EventEmitter { const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); loading.then(this.loadSuccess(loadURL)).catch((err) => { if (err.code && err.code.startsWith('ERR_CERT')) { - WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); - this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); - this.log.info('Invalid certificate, stop retrying until the user decides what to do.', err); + WindowManager.sendToRenderer(LOAD_FAILED, this.id, 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.status = Status.ERROR; return; } @@ -258,7 +237,7 @@ export class MattermostView extends EventEmitter { destroy = () => { WebContentsEventManager.removeWebContentsListeners(this.webContentsId); - appState.updateMentions(this.name, 0, false); + appState.updateMentions(this.id, 0, false); MainWindow.get()?.removeBrowserView(this.view); // workaround to eliminate zombie processes @@ -274,8 +253,6 @@ export class MattermostView extends EventEmitter { if (this.removeLoading) { clearTimeout(this.removeLoading); } - - this.contextMenu.dispose(); } /** @@ -307,7 +284,7 @@ export class MattermostView extends EventEmitter { if (timedout) { this.log.verbose('timeout expired will show the browserview'); - this.emit(LOADSCREEN_END, this.name); + this.emit(LOADSCREEN_END, this.id); } clearTimeout(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 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 private findUnreadState = (favicon: string | null) => { try { - this.view.webContents.send(IS_UNREAD, favicon, this.name); + this.view.webContents.send(IS_UNREAD, favicon, this.id); } catch (err: any) { 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) { this.loadRetry(loadURL, err); } else { - WindowManager.sendToRenderer(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); - this.emit(LOAD_FAILED, this.name, err.toString(), loadURL.toString()); + WindowManager.sendToRenderer(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); + this.emit(LOAD_FAILED, this.id, err.toString(), loadURL.toString()); this.log.info(`Couldn't establish a connection with ${loadURL}, will continue to retry in the background`, err); this.status = Status.ERROR; this.retryLoad = setTimeout(this.retryInBackground(loadURL), RELOAD_INTERVAL); @@ -442,14 +419,14 @@ export class MattermostView extends EventEmitter { private loadRetry = (loadURL: string, err: Error) => { 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`); } private loadSuccess = (loadURL: string) => { return () => { this.log.verbose(`finished loading ${loadURL}`); - WindowManager.sendToRenderer(LOAD_SUCCESS, this.name); + WindowManager.sendToRenderer(LOAD_SUCCESS, this.id); this.maxRetries = MAX_SERVER_RETRIES; if (this.status === Status.LOADING) { this.updateMentionsFromTitle(this.view.webContents.getTitle()); @@ -457,7 +434,7 @@ export class MattermostView extends EventEmitter { } this.status = Status.WAITING_MM; 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(); if (mainWindow) { this.setBounds(getWindowBoundaries(mainWindow, shouldHaveBackBar(this.tab.url || '', this.currentURL))); @@ -470,7 +447,7 @@ export class MattermostView extends EventEmitter { */ 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 const timeout = setInterval(() => { @@ -480,7 +457,7 @@ export class MattermostView extends EventEmitter { if (!this.view.webContents.isLoading()) { 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); } catch (e) { 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) => { this.log.debug('handleDidNavigate', url); + const mainWindow = MainWindow.get(); + if (!mainWindow) { + return; + } + if (shouldHaveBackBar(this.tab.url || '', url)) { - this.setBounds(getWindowBoundaries(MainWindow.get()!, true)); + this.setBounds(getWindowBoundaries(mainWindow, true)); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, true); this.log.debug('show back button'); } else { - this.setBounds(getWindowBoundaries(MainWindow.get()!)); + this.setBounds(getWindowBoundaries(mainWindow)); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); this.log.debug('hide back button'); } @@ -511,4 +493,10 @@ export class MattermostView extends EventEmitter { this.emit(UPDATE_TARGET_URL); } } + + private handleServerWasModified = (serverIds: string) => { + if (serverIds.includes(this.tab.server.id)) { + this.reload(); + } + } } diff --git a/src/main/views/teamDropdownView.test.js b/src/main/views/teamDropdownView.test.js index a81bd581..64308d07 100644 --- a/src/main/views/teamDropdownView.test.js +++ b/src/main/views/teamDropdownView.test.js @@ -36,6 +36,11 @@ jest.mock('../windows/windowManager', () => ({ sendToRenderer: jest.fn(), })); +jest.mock('common/servers/serverManager', () => ({ + on: jest.fn(), + getOrderedServers: jest.fn().mockReturnValue([]), +})); + describe('main/views/teamDropdownView', () => { describe('getBounds', () => { beforeEach(() => { @@ -62,52 +67,4 @@ describe('main/views/teamDropdownView', () => { teamDropdownView.handleClose(); 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, - }]); - }); - }); }); diff --git a/src/main/views/teamDropdownView.ts b/src/main/views/teamDropdownView.ts index 0668f49d..79a60a99 100644 --- a/src/main/views/teamDropdownView.ts +++ b/src/main/views/teamDropdownView.ts @@ -3,7 +3,7 @@ import {BrowserView, ipcMain, IpcMainEvent} from 'electron'; -import {CombinedConfig, Team, TeamWithTabs, TeamWithTabsAndGpo} from 'types/config'; +import {CombinedConfig, MattermostTeam} from 'types/config'; import { CLOSE_TEAMS_DROPDOWN, @@ -14,10 +14,12 @@ import { REQUEST_TEAMS_DROPDOWN_INFO, RECEIVE_DROPDOWN_MENU_SIZE, SET_ACTIVE_VIEW, + SERVERS_UPDATE, } from 'common/communication'; import Config from 'common/config'; 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 ServerManager from 'common/servers/serverManager'; import {getLocalPreload, getLocalURLString} from 'main/utils'; @@ -30,7 +32,7 @@ const log = new Logger('TeamDropdownView'); export default class TeamDropdownView { view: BrowserView; bounds?: Electron.Rectangle; - teams: TeamWithTabsAndGpo[]; + teams: MattermostTeam[]; activeTeam?: string; darkMode: boolean; enableServerManagement?: boolean; @@ -42,7 +44,8 @@ export default class TeamDropdownView { isOpen: boolean; constructor() { - this.teams = this.addGpoToTeams(Config.teams, []); + this.teams = this.getOrderedTeams(); + this.hasGPOTeams = this.teams.some((srv) => srv.isPredefined); this.darkMode = Config.darkMode; this.enableServerManagement = Config.enableServerManagement; this.isOpen = false; @@ -69,6 +72,17 @@ export default class TeamDropdownView { ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); ipcMain.on(SET_ACTIVE_VIEW, this.updateActiveTeam); 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) => { @@ -76,23 +90,33 @@ export default class TeamDropdownView { this.darkMode = config.darkMode; this.enableServerManagement = config.enableServerManagement; - this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0; this.updateDropdown(); } - updateActiveTeam = (event: IpcMainEvent, name: string) => { - log.silly('updateActiveTeam', {name}); + updateActiveTeam = (event: IpcMainEvent, serverId: string) => { + log.silly('updateActiveTeam', {serverId}); - this.activeTeam = name; + this.activeTeam = serverId; this.updateDropdown(); } + private reduceNotifications = (items: Map, 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, mentions: Map, unreads: Map) => { log.silly('updateMentions', {expired, mentions, unreads}); - this.unreads = unreads; - this.mentions = mentions; - this.expired = expired; + this.unreads = this.reduceNotifications(unreads, (base, value) => base || value || false); + this.mentions = this.reduceNotifications(mentions, (base, value) => (base ?? 0) + (value ?? 0)); + this.expired = this.reduceNotifications(expired, (base, value) => base || value || false); this.updateDropdown(); } @@ -164,16 +188,4 @@ export default class TeamDropdownView { // @ts-ignore 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), - }; - }); - } } diff --git a/src/main/views/viewManager.test.js b/src/main/views/viewManager.test.js index a94aad7d..ed67ca1b 100644 --- a/src/main/views/viewManager.test.js +++ b/src/main/views/viewManager.test.js @@ -4,13 +4,12 @@ /* eslint-disable max-lines */ 'use strict'; -import {dialog, ipcMain} from 'electron'; +import {dialog} from 'electron'; -import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, MAIN_WINDOW_SHOWN} from 'common/communication'; -import Config from 'common/config'; -import {MattermostServer} from 'common/servers/MattermostServer'; -import {getTabViewName} from 'common/tabs/TabView'; -import {equalUrlsIgnoringSubpath} from 'common/utils/url'; +import {BROWSER_HISTORY_PUSH, LOAD_SUCCESS, SET_ACTIVE_VIEW} from 'common/communication'; +import {TAB_MESSAGING} from 'common/tabs/TabView'; +import ServerManager from 'common/servers/serverManager'; +import urlUtils from 'common/utils/url'; import MainWindow from 'main/windows/mainWindow'; @@ -71,32 +70,61 @@ jest.mock('main/views/loadingScreen', () => ({ jest.mock('main/windows/mainWindow', () => ({ 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', () => ({ MattermostView: jest.fn(), })); jest.mock('./modalManager', () => ({ showModal: jest.fn(), + isModalDisplayed: jest.fn(), })); jest.mock('./webContentEvents', () => ({})); jest.mock('../appState', () => ({})); describe('main/views/viewManager', () => { describe('loadView', () => { - const viewManager = new ViewManager({}); + const viewManager = new ViewManager(); const onceFn = jest.fn(); const loadFn = jest.fn(); const destroyFn = jest.fn(); beforeEach(() => { - viewManager.showByName = jest.fn(); - viewManager.getServerView = jest.fn().mockImplementation((srv, tabName) => ({name: `${srv.name}-${tabName}`})); + viewManager.showById = jest.fn(); + MainWindow.get.mockReturnValue({}); MattermostView.mockImplementation((tab) => ({ on: jest.fn(), load: loadFn, once: onceFn, destroy: destroyFn, - name: tab.name, + id: tab.id, })); }); @@ -107,48 +135,39 @@ describe('main/views/viewManager', () => { }); it('should add closed tabs to closedViews', () => { - viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: false}); - expect(viewManager.closedViews.has('server1-tab1')).toBe(true); + viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: false}); + expect(viewManager.closedViews.has('tab1')).toBe(true); }); it('should remove from remove from closedViews when the tab is open', () => { - viewManager.closedViews.set('server1-tab1', {}); - expect(viewManager.closedViews.has('server1-tab1')).toBe(true); - viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}); - expect(viewManager.closedViews.has('server1-tab1')).toBe(false); + viewManager.closedViews.set('tab1', {}); + expect(viewManager.closedViews.has('tab1')).toBe(true); + viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}); + expect(viewManager.closedViews.has('tab1')).toBe(false); }); it('should add view to views map and add listeners', () => { - viewManager.loadView({name: 'server1'}, {}, {name: 'tab1', isOpen: true}, 'http://server-1.com/subpath'); - expect(viewManager.views.has('server1-tab1')).toBe(true); + viewManager.loadView({id: 'server1'}, {id: 'tab1', isOpen: true}, 'http://server-1.com/subpath'); + expect(viewManager.views.has('tab1')).toBe(true); expect(onceFn).toHaveBeenCalledWith(LOAD_SUCCESS, viewManager.activateView); expect(loadFn).toHaveBeenCalledWith('http://server-1.com/subpath'); }); }); - describe('reloadConfiguration', () => { + describe('handleReloadConfiguration', () => { const viewManager = new ViewManager(); beforeEach(() => { viewManager.loadView = jest.fn(); - viewManager.showByName = jest.fn(); + viewManager.showById = jest.fn(); viewManager.showInitial = jest.fn(); - - const mainWindow = { + viewManager.focus = jest.fn(); + MainWindow.get.mockReturnValue({ webContents: { 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 loadFn = jest.fn(); const destroyFn = jest.fn(); @@ -157,11 +176,10 @@ describe('main/views/viewManager', () => { load: loadFn, once: onceFn, destroy: destroyFn, - name: tab.name, + id: tab.id, updateServerInfo: jest.fn(), tab, })); - getTabViewName.mockImplementation((a, b) => `${a}-${b}`); }); afterEach(() => { @@ -172,342 +190,289 @@ describe('main/views/viewManager', () => { }); 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 view = new MattermostView({ - name: 'server1-tab1', - server: 'server1', + id: 'tab1', + server: { + id: 'server1', + }, }); - viewManager.views.set('server1-tab1', view); - viewManager.reloadConfiguration(); - expect(viewManager.views.get('server1-tab1')).toBe(view); + viewManager.views.set('tab1', view); + ServerManager.getAllServers.mockReturnValue([{ + 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(); makeSpy.mockRestore(); }); 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', - url: 'http://server1.com', - order: 1, - tabs: [ - { - name: 'tab1', - isOpen: false, - }, - ], + id: 'tab1', + isOpen: false, }, - ]; - viewManager.reloadConfiguration(); - expect(viewManager.closedViews.has('server1-tab1')).toBe(true); + ]); + viewManager.handleReloadConfiguration(); + expect(viewManager.closedViews.has('tab1')).toBe(true); }); it('should create new views for new tabs', () => { 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', - url: 'http://server1.com', - order: 1, - tabs: [ - { - name: 'tab1', - isOpen: true, - }, - ], + id: 'tab1', + name: 'tab1', + isOpen: true, + url: new URL('http://server1.com/tab'), }, - ]; - viewManager.reloadConfiguration(); + ]); + viewManager.handleReloadConfiguration(); expect(makeSpy).toHaveBeenCalledWith( { + id: 'server1', name: 'server1', url: new URL('http://server1.com'), }, - expect.any(Object), { + id: 'tab1', name: 'tab1', isOpen: true, + url: new URL('http://server1.com/tab'), }, - 'http://server1.com', ); makeSpy.mockRestore(); }); it('should set focus to current view on reload', () => { const view = { - name: 'server1-tab1', + id: 'tab1', tab: { server: { - name: 'server-1', + id: 'server-1', }, - name: 'server1-tab1', + id: 'tab1', url: new URL('http://server1.com'), }, destroy: jest.fn(), updateServerInfo: jest.fn(), + focus: jest.fn(), }; - viewManager.currentView = 'server1-tab1'; - viewManager.views.set('server1-tab1', view); - Config.teams = [ + viewManager.currentView = 'tab1'; + viewManager.views.set('tab1', view); + ServerManager.getAllServers.mockReturnValue([{ + id: 'server1', + url: new URL('http://server1.com'), + }]); + ServerManager.getOrderedTabsForServer.mockReturnValue([ { - name: 'server1', - url: 'http://server1.com', - order: 1, - tabs: [ - { - name: 'tab1', - isOpen: true, - }, - ], + id: 'tab1', + isOpen: true, }, - ]; - viewManager.reloadConfiguration(); - expect(viewManager.showByName).toHaveBeenCalledWith('server1-tab1'); + ]); + viewManager.handleReloadConfiguration(); + expect(view.focus).toHaveBeenCalled(); }); it('should show initial if currentView has been removed', () => { const view = { - name: 'server1-tab1', + id: 'tab1', tab: { - name: 'server1-tab1', + id: 'tab1', url: new URL('http://server1.com'), }, destroy: jest.fn(), updateServerInfo: jest.fn(), }; - viewManager.currentView = 'server1-tab1'; - viewManager.views.set('server1-tab1', view); - Config.teams = [ + viewManager.currentView = 'tab1'; + viewManager.views.set('tab1', view); + ServerManager.getAllServers.mockReturnValue([{ + id: 'server2', + url: new URL('http://server2.com'), + }]); + ServerManager.getOrderedTabsForServer.mockReturnValue([ { - name: 'server2', - url: 'http://server2.com', - order: 1, - tabs: [ - { - name: 'tab1', - isOpen: true, - }, - ], + id: 'tab1', + isOpen: false, }, - ]; - viewManager.reloadConfiguration(); + ]); + viewManager.handleReloadConfiguration(); expect(viewManager.showInitial).toBeCalled(); }); it('should remove unused views', () => { const view = { - name: 'server1-tab1', + name: 'tab1', tab: { - name: 'server1-tab1', + name: 'tab1', url: new URL('http://server1.com'), }, destroy: jest.fn(), }; - viewManager.views.set('server1-tab1', view); - Config.teams = [ + viewManager.views.set('tab1', view); + ServerManager.getAllServers.mockReturnValue([{ + id: 'server2', + url: new URL('http://server2.com'), + }]); + ServerManager.getOrderedTabsForServer.mockReturnValue([ { - name: 'server2', - url: 'http://server2.com', - order: 1, - tabs: [ - { - name: 'tab1', - isOpen: true, - }, - ], + id: 'tab1', + isOpen: false, }, - ]; - viewManager.reloadConfiguration(); + ]); + viewManager.handleReloadConfiguration(); expect(view.destroy).toBeCalled(); expect(viewManager.showInitial).toBeCalled(); }); }); describe('showInitial', () => { - const viewManager = new ViewManager({}); + const viewManager = new ViewManager(); + const window = {webContents: {send: jest.fn()}}; beforeEach(() => { - 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, - }, - ], - }]; - viewManager.showByName = jest.fn(); - getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`); + viewManager.showById = jest.fn(); + MainWindow.get.mockReturnValue(window); + ServerManager.hasServers.mockReturnValue(true); + ServerManager.getCurrentServer.mockReturnValue({id: 'server-0'}); }); afterEach(() => { 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(); - expect(viewManager.showByName).toHaveBeenCalledWith('server-2_tab-3'); - }); - - 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'); + expect(viewManager.showById).toHaveBeenCalledWith('tab-1'); }); it('should open new server modal when no servers exist', () => { - viewManager.mainWindow = { - webContents: { - send: jest.fn(), - }, - }; - Config.teams = []; + ServerManager.hasServers.mockReturnValue(false); 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 baseView = { isReady: jest.fn(), @@ -545,12 +510,12 @@ describe('main/views/viewManager', () => { }; viewManager.views.set('server1-tab1', view); - viewManager.showByName('server1-tab1'); + viewManager.showById('server1-tab1'); expect(viewManager.currentView).toBeUndefined(); expect(view.isReady).not.toBeCalled(); expect(view.show).not.toBeCalled(); - viewManager.showByName('some-view-name'); + viewManager.showById('some-view-name'); expect(viewManager.currentView).toBeUndefined(); expect(view.isReady).not.toBeCalled(); expect(view.show).not.toBeCalled(); @@ -569,7 +534,7 @@ describe('main/views/viewManager', () => { viewManager.views.set('oldView', oldView); viewManager.views.set('newView', newView); viewManager.currentView = 'oldView'; - viewManager.showByName('newView'); + viewManager.showById('newView'); expect(oldView.hide).toHaveBeenCalled(); }); @@ -577,7 +542,7 @@ describe('main/views/viewManager', () => { const view = {...baseView}; view.isErrored.mockReturnValue(true); viewManager.views.set('view1', view); - viewManager.showByName('view1'); + viewManager.showById('view1'); expect(view.show).not.toHaveBeenCalled(); }); @@ -586,7 +551,7 @@ describe('main/views/viewManager', () => { view.isErrored.mockReturnValue(false); view.needsLoadingScreen.mockImplementation(() => true); viewManager.views.set('view1', view); - viewManager.showByName('view1'); + viewManager.showById('view1'); expect(LoadingScreen.show).toHaveBeenCalled(); }); @@ -595,113 +560,12 @@ describe('main/views/viewManager', () => { view.needsLoadingScreen.mockImplementation(() => false); view.isErrored.mockReturnValue(false); viewManager.views.set('view1', view); - viewManager.showByName('view1'); + viewManager.showById('view1'); expect(viewManager.currentView).toBe('view1'); 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', () => { const viewManager = new ViewManager({}); const baseView = { @@ -719,7 +583,6 @@ describe('main/views/viewManager', () => { beforeEach(() => { viewManager.openClosedTab = jest.fn(); - viewManager.getViewByURL = jest.fn(); }); afterEach(() => { @@ -729,7 +592,7 @@ describe('main/views/viewManager', () => { }); 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}; viewManager.views.set('view1', view); 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', () => { - 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 = { ...baseView, - serverInfo: { - remoteInfo: { - serverVersion: '6.0.0', - }, - }, tab: { server: { url: new URL('http://server-1.com'), @@ -758,7 +617,7 @@ describe('main/views/viewManager', () => { }); 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}; viewManager.handleDeepLink('mattermost://server-1.com/deep/link?thing=yes'); expect(view.load).not.toHaveBeenCalled(); @@ -772,10 +631,10 @@ describe('main/views/viewManager', () => { }); 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.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'); }); }); }); diff --git a/src/main/views/viewManager.ts b/src/main/views/viewManager.ts index 47815026..92e9d8de 100644 --- a/src/main/views/viewManager.ts +++ b/src/main/views/viewManager.ts @@ -1,9 +1,7 @@ // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // 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 { @@ -16,28 +14,25 @@ import { BROWSER_HISTORY_PUSH, UPDATE_LAST_ACTIVE, UPDATE_URL_VIEW_WIDTH, - MAIN_WINDOW_SHOWN, - RELOAD_CURRENT_VIEW, + SERVERS_UPDATE, REACT_APP_INITIALIZED, - APP_LOGGED_IN, BROWSER_HISTORY_BUTTON, APP_LOGGED_OUT, + APP_LOGGED_IN, + RELOAD_CURRENT_VIEW, UNREAD_RESULT, - GET_VIEW_NAME, HISTORY, + GET_VIEW_INFO_FOR_TEST, } from 'common/communication'; import Config from 'common/config'; 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 {MattermostServer} from 'common/servers/MattermostServer'; -import {getTabViewName, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView'; -import MessagingTabView from 'common/tabs/MessagingTabView'; -import FocalboardTabView from 'common/tabs/FocalboardTabView'; -import PlaybooksTabView from 'common/tabs/PlaybooksTabView'; +import ServerManager from 'common/servers/serverManager'; +import {TabView, TAB_MESSAGING} from 'common/tabs/TabView'; import {localizeMessage} from 'main/i18nManager'; -import {ServerInfo} from 'main/server/serverInfo'; import MainWindow from 'main/windows/mainWindow'; import * as appState from '../appState'; @@ -52,21 +47,17 @@ const URL_VIEW_DURATION = 10 * SECOND; const URL_VIEW_HEIGHT = 20; export class ViewManager { - private closedViews: Map; + private closedViews: Map; private views: Map; private currentView?: string; private urlViewCancel?: () => void; - private lastActiveServer?: number; - private viewOptions: BrowserViewConstructorOptions; - 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.closedViews = new Map(); + ipcMain.handle(GET_VIEW_INFO_FOR_TEST, this.handleGetViewInfoForTest); ipcMain.on(HISTORY, this.handleHistory); ipcMain.on(REACT_APP_INITIALIZED, this.handleReactAppInitialized); ipcMain.on(BROWSER_HISTORY_PUSH, this.handleBrowserHistoryPush); @@ -75,23 +66,24 @@ export class ViewManager { ipcMain.on(APP_LOGGED_OUT, this.handleAppLoggedOut); ipcMain.on(RELOAD_CURRENT_VIEW, this.handleReloadCurrentView); ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); - ipcMain.handle(GET_VIEW_NAME, this.handleGetViewName); + + ServerManager.on(SERVERS_UPDATE, this.handleReloadConfiguration); } init = () => { - this.getServers().forEach((server) => this.loadServer(server)); + LoadingScreen.show(); + ServerManager.getAllServers().forEach((server) => this.loadServer(server)); this.showInitial(); } - getView = (viewName: string) => { - return this.views.get(viewName); + getView = (viewId: string) => { + return this.views.get(viewId); } getCurrentView = () => { if (this.currentView) { return this.views.get(this.currentView); } - return undefined; } @@ -99,42 +91,50 @@ export class ViewManager { return [...this.views.values()].find((view) => view.webContentsId === webContentsId); } - showByName = (name: string) => { - log.debug('viewManager.showByName', name); + isViewClosed = (viewId: string) => { + 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.isVisible) { return; } - if (this.currentView && this.currentView !== name) { + let hidePrevious; + if (this.currentView && this.currentView !== tabId) { const previous = this.getCurrentView(); if (previous) { - previous.hide(); + hidePrevious = () => previous.hide(); } } - this.currentView = name; + this.currentView = tabId; if (!newView.isErrored()) { newView.show(); if (newView.needsLoadingScreen()) { LoadingScreen.show(); } } - MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type); - ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type); + hidePrevious?.(); + 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()) { - ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.server.name, newView.tab.type); + ipcMain.emit(UPDATE_LAST_ACTIVE, true, newView.tab.id); } else { - log.warn(`couldn't show ${name}, not ready`); + this.getViewLogger(tabId).warn(`couldn't show ${tabId}, not ready`); } } 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(); } focusCurrentView = () => { + log.debug('focusCurrentView'); + if (modalManager.isModalDisplayed()) { modalManager.focusCurrentModal(); return; @@ -171,25 +171,24 @@ export class ViewManager { */ handleDeepLink = (url: string | URL) => { - // TODO: fix for new tabs if (url) { const parsedURL = urlUtils.parseURL(url)!; - const tabView = this.getViewByURL(parsedURL, true); + const tabView = ServerManager.lookupTabByURL(parsedURL, true); if (tabView) { - const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`; - if (this.closedViews.has(tabView.name)) { - this.openClosedTab(tabView.name, urlWithSchema); + const urlWithSchema = `${tabView.url.origin}${parsedURL.pathname}${parsedURL.search}`; + if (this.closedViews.has(tabView.id)) { + this.openClosedTab(tabView.id, urlWithSchema); } else { - const view = this.views.get(tabView.name); + const view = this.views.get(tabView.id); 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; } - 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(), '')}`; view.sendToRenderer(BROWSER_HISTORY_PUSH, pathName); - this.deeplinkSuccess(view.name); + this.deeplinkSuccess(view.id); } else { // attempting to change parsedURL protocol results in it not being modified. view.resetLoadingStatus(); @@ -207,83 +206,67 @@ export class ViewManager { } }; - private deeplinkSuccess = (viewName: string) => { - log.debug('deeplinkSuccess', viewName); + private deeplinkSuccess = (viewId: string) => { + this.getViewLogger(viewId).debug('deeplinkSuccess'); - const view = this.views.get(viewName); - if (!view) { - return; - } - this.showByName(viewName); - view.removeListener(LOAD_FAILED, this.deeplinkFailed); + this.showById(viewId); + this.views.get(viewId)?.removeListener(LOAD_FAILED, this.deeplinkFailed); }; - private deeplinkFailed = (viewName: string, err: string, url: string) => { - log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`); - const view = this.views.get(viewName); - if (!view) { - return; - } - view.removeListener(LOAD_SUCCESS, this.deeplinkSuccess); + private deeplinkFailed = (viewId: string, err: string, url: string) => { + this.getViewLogger(viewId).error(`failed to load deeplink ${url}`, err); + this.views.get(viewId)?.removeListener(LOAD_SUCCESS, this.deeplinkSuccess); } /** * View loading helpers */ - private loadServer = (server: TeamWithTabs) => { - const srv = new MattermostServer(server); - const serverInfo = new ServerInfo(srv); - server.tabs.forEach((tab) => this.loadView(srv, serverInfo, tab)); + private loadServer = (server: MattermostServer) => { + const tabs = ServerManager.getOrderedTabsForServer(server.id); + tabs.forEach((tab) => this.loadView(server, tab)); } - private loadView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string) => { + private loadView = (srv: MattermostServer, tab: TabView, url?: string) => { if (!tab.isOpen) { - this.closedViews.set(getTabViewName(srv.name, tab.name), {srv, tab}); + this.closedViews.set(tab.id, {srv, tab}); return; } - const view = this.makeView(srv, serverInfo, tab, url); + const view = this.makeView(srv, tab, url); this.addView(view); } - private makeView = (srv: MattermostServer, serverInfo: ServerInfo, tab: Tab, url?: string): MattermostView => { - const tabView = this.getServerView(srv, tab.name); - const view = new MattermostView(tabView, serverInfo, this.viewOptions); + private makeView = (srv: MattermostServer, tab: TabView, url?: string): MattermostView => { + const mainWindow = MainWindow.get(); + if (!mainWindow) { + throw new Error('Cannot create view, no main window present'); + } + + const view = new MattermostView(tab, {webPreferences: {spellcheck: Config.useSpellChecker}}); view.once(LOAD_SUCCESS, this.activateView); - view.load(url); - view.on(UPDATE_TARGET_URL, this.showURLView); view.on(LOADSCREEN_END, this.finishLoading); view.on(LOAD_FAILED, this.failLoading); + view.on(UPDATE_TARGET_URL, this.showURLView); + view.load(url); return view; } private addView = (view: MattermostView): void => { - this.views.set(view.name, view); - if (this.closedViews.has(view.name)) { - this.closedViews.delete(view.name); + this.views.set(view.id, view); + if (this.closedViews.has(view.id)) { + this.closedViews.delete(view.id); } } private showInitial = () => { log.verbose('showInitial'); - const servers = this.getServers(); - if (servers.length) { - const element = servers.find((e) => e.order === this.lastActiveServer) || servers.find((e) => e.order === 0); - if (element && element.tabs.length) { - 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); - } - } + if (ServerManager.hasServers()) { + const lastActiveServer = ServerManager.getCurrentServer(); + const lastActiveTab = ServerManager.getLastActiveTabForServer(lastActiveServer.id); + this.showById(lastActiveTab.id); } else { - MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, null, null); - ipcMain.emit(MAIN_WINDOW_SHOWN); + MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW); } } @@ -291,29 +274,28 @@ export class ViewManager { * Mattermost view event handlers */ - private activateView = (viewName: string) => { - log.debug('activateView', viewName); + private activateView = (viewId: string) => { + this.getViewLogger(viewId).debug('activateView'); - if (this.currentView === viewName) { - this.showByName(this.currentView); + if (this.currentView === viewId) { + this.showById(this.currentView); } } - private finishLoading = (server: string) => { - log.debug('finishLoading', server); + private finishLoading = (viewId: string) => { + this.getViewLogger(viewId).debug('finishLoading'); - const view = this.views.get(server); - if (view && this.getCurrentView() === view) { - this.showByName(this.currentView!); + if (this.currentView === viewId) { + this.showById(this.currentView); LoadingScreen.fade(); } } - private failLoading = (tabName: string) => { - log.debug('failLoading', tabName); + private failLoading = (viewId: string) => { + this.getViewLogger(viewId).debug('failLoading'); LoadingScreen.fade(); - if (this.currentView === tabName) { + if (this.currentView === viewId) { this.getCurrentView()?.hide(); } } @@ -344,7 +326,7 @@ export class ViewManager { const query = new Map([['url', urlString]]); const localURL = getLocalURLString('urlView.html', query); urlView.webContents.loadURL(localURL); - mainWindow.addBrowserView(urlView); + MainWindow.get()?.addBrowserView(urlView); const boundaries = this.views.get(this.currentView || '')?.getBounds() ?? mainWindow.getBounds(); const hideView = () => { @@ -372,7 +354,7 @@ export class ViewManager { height: URL_VIEW_HEIGHT, }; - log.silly('showURLView setBounds', boundaries, bounds); + log.silly('showURLView.setBounds', boundaries, bounds); urlView.setBounds(bounds); }; @@ -397,34 +379,30 @@ export class ViewManager { * Servers or tabs have been added or edited. We need to * close, open, or reload tabs, taking care to reuse tabs and * preserve focus on the currently selected tab. */ - reloadConfiguration = () => { - log.debug('reloadConfiguration'); + private handleReloadConfiguration = () => { + log.debug('handleReloadConfiguration'); + + const currentTabId: string | undefined = this.views.get(this.currentView as string)?.tab.id; const current: Map = new Map(); for (const view of this.views.values()) { - current.set(view.name, view); + current.set(view.tab.id, view); } const views: Map = new Map(); - const closed: Map = new Map(); + const closed: Map = new Map(); - const sortedTabs = this.getServers().flatMap((x) => [...x.tabs]. - sort((a, b) => a.order - b.order). - map((t): [TeamWithTabs, Tab] => [x, t])); + const sortedTabs = ServerManager.getAllServers().flatMap((x) => ServerManager.getOrderedTabsForServer(x.id). + map((t): [MattermostServer, TabView] => [x, t])); - for (const [team, tab] of sortedTabs) { - const srv = new MattermostServer(team); - const info = new ServerInfo(srv); - const tabName = getTabViewName(team.name, tab.name); - const recycle = current.get(tabName); + for (const [srv, tab] of sortedTabs) { + const recycle = current.get(tab.id); if (!tab.isOpen) { - const view = this.getServerView(srv, tab.name); - closed.set(tabName, {srv, tab, name: view.name}); + closed.set(tab.id, {srv, tab}); } else if (recycle) { - recycle.updateServerInfo(srv); - views.set(tabName, recycle); + views.set(tab.id, recycle); } 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 this.views = new Map(); for (const x of views.values()) { - this.views.set(x.name, x); + this.views.set(x.id, x); } // commit closed 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 (this.getServers().length) { + if ((currentTabId && closed.has(currentTabId)) || (this.currentView && this.closedViews.has(this.currentView))) { + if (ServerManager.hasServers()) { this.currentView = undefined; this.showInitial(); } else { @@ -457,12 +435,14 @@ export class ViewManager { } // show the focused tab (or initial) - if (this.currentView && views.has(this.currentView)) { - const view = views.get(this.currentView); - if (view) { - this.currentView = view.name; - this.showByName(view.name); - MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.name, view.tab.type); + if (currentTabId && views.has(currentTabId)) { + const view = views.get(currentTabId); + if (view && view.id !== this.currentView) { + this.currentView = view.id; + this.showById(view.id); + MainWindow.get()?.webContents.send(SET_ACTIVE_VIEW, view.tab.server.id, view.tab.id); + } else { + this.focusCurrentView(); } } else { this.showInitial(); @@ -481,21 +461,21 @@ export class ViewManager { this.getView(viewId)?.onLogin(false); } - private handleBrowserHistoryPush = (e: IpcMainEvent, viewName: string, pathName: string) => { - log.debug('handleBrowserHistoryPush', {viewName, pathName}); + private handleBrowserHistoryPush = (e: IpcMainEvent, viewId: string, pathName: string) => { + 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 redirectedViewName = this.getViewByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.name || viewName; - if (this.closedViews.has(redirectedViewName)) { + const redirectedviewId = ServerManager.lookupTabByURL(`${currentView?.tab.server.url.toString().replace(/\/$/, '')}${cleanedPathName}`)?.id || viewId; + if (this.isViewClosed(redirectedviewId)) { // 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; } - let redirectedView = this.views.get(redirectedViewName) || currentView; - if (redirectedView !== currentView && redirectedView?.tab.name === this.currentView && redirectedView?.isLoggedIn) { - log.info('redirecting to a new view', redirectedView?.name || viewName); - this.showByName(redirectedView?.name || viewName); + let redirectedView = this.getView(redirectedviewId) || currentView; + if (redirectedView !== currentView && redirectedView?.tab.server.id === ServerManager.getCurrentServer().id && redirectedView?.isLoggedIn) { + log.info('redirecting to a new view', redirectedView?.id || viewId); + this.showById(redirectedView?.id || viewId); } else { redirectedView = currentView; } @@ -504,21 +484,19 @@ export class ViewManager { if (!(redirectedView !== currentView && redirectedView?.tab.type === TAB_MESSAGING && cleanedPathName === '/')) { redirectedView?.sendToRenderer(BROWSER_HISTORY_PUSH, cleanedPathName); if (redirectedView) { - this.handleBrowserHistoryButton(e, redirectedView.name); + this.handleBrowserHistoryButton(e, redirectedView.id); } } } - private handleBrowserHistoryButton = (e: IpcMainEvent, viewName: string) => { - log.debug('handleBrowserHistoryButton', viewName); - - this.getView(viewName)?.updateHistoryButton(); + private handleBrowserHistoryButton = (e: IpcMainEvent, viewId: string) => { + this.getView(viewId)?.updateHistoryButton(); } - private handleReactAppInitialized = (e: IpcMainEvent, viewName: string) => { - log.debug('handleReactAppInitialized', viewName); + private handleReactAppInitialized = (e: IpcMainEvent, viewId: string) => { + log.debug('handleReactAppInitialized', viewId); - const view = this.views.get(viewName); + const view = this.views.get(viewId); if (view) { view.setInitialized(); if (this.getCurrentView() === view) { @@ -535,94 +513,53 @@ export class ViewManager { return; } view?.reload(); - this.showByName(view?.name); + this.showById(view?.id); } // 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. - private handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => { - log.silly('handleFaviconIsUnread', {favicon, viewName, result}); + private handleFaviconIsUnread = (e: Event, favicon: string, viewId: string, result: boolean) => { + log.silly('handleFaviconIsUnread', {favicon, viewId, result}); - appState.updateUnreads(viewName, result); + appState.updateUnreads(viewId, result); } /** * Helper functions */ - private openClosedTab = (name: string, url?: string) => { - if (!this.closedViews.has(name)) { + private openClosedTab = (id: string, url?: string) => { + if (!this.closedViews.has(id)) { return; } - const {srv, tab} = this.closedViews.get(name)!; + const {srv, tab} = this.closedViews.get(id)!; tab.isOpen = true; - this.loadView(srv, new ServerInfo(srv), tab, url); - this.showByName(name); - const view = this.views.get(name)!; + this.loadView(srv, tab, url); + this.showById(id); + const view = this.views.get(id)!; view.isVisible = true; view.on(LOAD_SUCCESS, () => { 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) => { - log.silly('getViewByURL', `${inputURL}`, ignoreScheme); + private getViewLogger = (viewId: string) => { + return ServerManager.getViewLog(viewId, 'ViewManager'); + } - const parsedURL = urlUtils.parseURL(inputURL); - if (!parsedURL) { - return undefined; - } - 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(); - } + private handleGetViewInfoForTest = (event: IpcMainInvokeEvent) => { + const view = this.getViewByWebContentsId(event.sender.id); + if (!view) { + return null; } + return { + id: view.id, + webContentsId: view.webContentsId, + serverName: view.tab.server.name, + tabType: view.tab.type, + }; } } diff --git a/src/main/views/webContentEvents.test.js b/src/main/views/webContentEvents.test.js index 454c5100..3ca9a515 100644 --- a/src/main/views/webContentEvents.test.js +++ b/src/main/views/webContentEvents.test.js @@ -23,12 +23,13 @@ jest.mock('electron', () => ({ session: {}, })); jest.mock('main/contextMenu', () => jest.fn()); - +jest.mock('main/windows/mainWindow', () => ({ + get: jest.fn(), +})); jest.mock('../allowProtocolDialog', () => ({})); jest.mock('main/windows/callsWidgetWindow', () => ({})); jest.mock('main/views/viewManager', () => ({ getViewByWebContentsId: jest.fn(), - getViewByURL: jest.fn(), })); jest.mock('../windows/windowManager', () => ({ getServerURLFromWebContentsId: jest.fn(), diff --git a/src/main/views/webContentEvents.ts b/src/main/views/webContentEvents.ts index b8b6fd2a..aee0e531 100644 --- a/src/main/views/webContentEvents.ts +++ b/src/main/views/webContentEvents.ts @@ -9,17 +9,18 @@ import urlUtils from 'common/utils/url'; import {flushCookiesStore} from 'main/app/utils'; 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 ViewManager from 'main/views/viewManager'; +import CallsWidgetWindow from 'main/windows/callsWidgetWindow'; import {protocols} from '../../../electron-builder.json'; import allowProtocolDialog from '../allowProtocolDialog'; import {composeUserAgent} from '../utils'; -import ViewManager from './viewManager'; - type CustomLogin = { inProgress: boolean; } @@ -38,10 +39,16 @@ export class WebContentsEventManager { } private log = (webContentsId?: number) => { - if (webContentsId) { - return log.withPrefix(String(webContentsId)); + if (!webContentsId) { + return log; } - return log; + + const view = ViewManager.getViewByWebContentsId(webContentsId); + if (!view) { + return log; + } + + return ServerManager.getViewLog(view.id, 'WebContentsEventManager'); } private isTrustedPopupWindow = (webContentsId: number) => { @@ -59,7 +66,7 @@ export class WebContentsEventManager { return WindowManager.getServerURLFromWebContentsId(webContentsId); } - generateWillNavigate = (webContentsId: number) => { + private generateWillNavigate = (webContentsId: number) => { return (event: Event, url: string) => { 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) => { - this.log(webContentsId).debug('did-start-navigation', {webContentsId, url}); + this.log(webContentsId).debug('did-start-navigation', url); const parsedURL = urlUtils.parseURL(url)!; 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}.`); return {action: 'deny'}; }; - generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => { + private generateNewWindowListener = (webContentsId: number, spellcheck?: boolean) => { return (details: Electron.HandlerDetails): {action: 'deny' | 'allow'} => { this.log(webContentsId).debug('new-window', details.url); @@ -199,7 +206,7 @@ export class WebContentsEventManager { this.popupWindow = { 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 - //parent: WindowManager.getMainWindow(), + parent: MainWindow.get(), show: false, center: true, webPreferences: { @@ -250,7 +257,7 @@ export class WebContentsEventManager { return {action: 'deny'}; } - const otherServerURL = ViewManager.getViewByURL(parsedURL); + const otherServerURL = ServerManager.lookupTabByURL(parsedURL); if (otherServerURL && urlUtils.isTeamUrl(otherServerURL.server.url, parsedURL, true)) { WindowManager.showMainWindow(parsedURL); return {action: 'deny'}; diff --git a/src/main/windows/callsWidgetWindow.test.js b/src/main/windows/callsWidgetWindow.test.js index b9475246..ab8bf1f0 100644 --- a/src/main/windows/callsWidgetWindow.test.js +++ b/src/main/windows/callsWidgetWindow.test.js @@ -645,7 +645,6 @@ describe('main/windows/callsWidgetWindow', () => { thumbnail: { toDataURL: jest.fn(), }, - }, ]); @@ -741,7 +740,7 @@ describe('main/windows/callsWidgetWindow', () => { callsWidgetWindow.mainView = { tab: { server: { - name: 'server-1', + id: 'server-1', }, }, sendToRenderer: jest.fn(), @@ -807,7 +806,7 @@ describe('main/windows/callsWidgetWindow', () => { callsWidgetWindow.mainView = { tab: { server: { - name: 'server-2', + id: 'server-2', }, }, sendToRenderer: jest.fn(), @@ -874,7 +873,7 @@ describe('main/windows/callsWidgetWindow', () => { callsWidgetWindow.mainView = { tab: { server: { - name: 'server-2', + id: 'server-2', }, }, sendToRenderer: jest.fn(), @@ -901,7 +900,7 @@ describe('main/windows/callsWidgetWindow', () => { const view = { tab: { server: { - name: 'server-1', + id: 'server-1', }, }, sendToRenderer: jest.fn(), diff --git a/src/main/windows/callsWidgetWindow.ts b/src/main/windows/callsWidgetWindow.ts index f388eded..6019b419 100644 --- a/src/main/windows/callsWidgetWindow.ts +++ b/src/main/windows/callsWidgetWindow.ts @@ -80,8 +80,8 @@ export class CallsWidgetWindow { return this.options?.callID; } - private get serverName() { - return this.mainView?.tab.server.name; + private get serverID() { + return this.mainView?.tab.server.id; } /** @@ -450,11 +450,11 @@ export class CallsWidgetWindow { private handleDesktopSourcesModalRequest = () => { log.debug('handleDesktopSourcesModalRequest'); - if (!this.serverName) { + if (!this.serverID) { return; } - WindowManager.switchServer(this.serverName); + WindowManager.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(DESKTOP_SOURCES_MODAL_REQUEST); } @@ -468,11 +468,11 @@ export class CallsWidgetWindow { private handleCallsWidgetChannelLinkClick = () => { log.debug('handleCallsWidgetChannelLinkClick'); - if (!this.serverName) { + if (!this.serverID) { return; } - WindowManager.switchServer(this.serverName); + WindowManager.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, this.options?.channelURL); } @@ -480,11 +480,11 @@ export class CallsWidgetWindow { private handleCallsError = (_: string, msg: CallsErrorMessage) => { log.debug('handleCallsError', msg); - if (!this.serverName) { + if (!this.serverID) { return; } - WindowManager.switchServer(this.serverName); + WindowManager.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(CALLS_ERROR, msg); } @@ -492,11 +492,11 @@ export class CallsWidgetWindow { private handleCallsLinkClick = (_: string, msg: CallsLinkClickMessage) => { log.debug('handleCallsLinkClick with linkURL', msg.link); - if (!this.serverName) { + if (!this.serverID) { return; } - WindowManager.switchServer(this.serverName); + WindowManager.switchServer(this.serverID); MainWindow.get()?.focus(); this.mainView?.sendToRenderer(BROWSER_HISTORY_PUSH, msg.link); } diff --git a/src/main/windows/mainWindow.ts b/src/main/windows/mainWindow.ts index 7f0d0a96..e550f6b7 100644 --- a/src/main/windows/mainWindow.ts +++ b/src/main/windows/mainWindow.ts @@ -11,9 +11,10 @@ import {app, BrowserWindow, BrowserWindowConstructorOptions, dialog, Event, glob 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 {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 Utils from 'common/utils/util'; import * as Validator from 'common/Validator'; @@ -39,6 +40,8 @@ export class MainWindow { this.savedWindowState = this.getSavedWindowState(); ipcMain.handle(GET_FULL_SCREEN_STATUS, () => this.win?.isFullScreen()); + + ServerManager.on(SERVERS_UPDATE, this.handleUpdateConfig); } init = () => { @@ -321,6 +324,13 @@ export class MainWindow { } }); } + + /** + * Server Manager update handler + */ + private handleUpdateConfig = () => { + this.win?.webContents.send(SERVERS_UPDATE); + } } const mainWindow = new MainWindow(); diff --git a/src/main/windows/windowManager.test.js b/src/main/windows/windowManager.test.js index f7fc13d6..7a53cf59 100644 --- a/src/main/windows/windowManager.test.js +++ b/src/main/windows/windowManager.test.js @@ -6,13 +6,13 @@ import {systemPreferences} from 'electron'; -import Config from 'common/config'; import {getTabViewName} from 'common/tabs/TabView'; +import ServerManager from 'common/servers/serverManager'; 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 MainWindow from './mainWindow'; @@ -56,10 +56,10 @@ jest.mock('../utils', () => ({ resetScreensharePermissionsMacOS: jest.fn(), })); jest.mock('../views/viewManager', () => ({ - isLoadingScreenHidden: jest.fn(), - getView: jest.fn(), - getViewByWebContentsId: jest.fn(), + reloadConfiguration: jest.fn(), + showById: jest.fn(), getCurrentView: jest.fn(), + getView: jest.fn(), isViewClosed: jest.fn(), openClosedTab: jest.fn(), handleDeepLink: jest.fn(), @@ -81,12 +81,36 @@ jest.mock('./settingsWindow', () => ({ })); jest.mock('./mainWindow', () => ({ get: jest.fn(), - focus: jest.fn(), })); jest.mock('../downloadsManager', () => ({ 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', () => ({ isCallsWidget: jest.fn(), getURL: jest.fn(), @@ -159,6 +183,7 @@ describe('main/windows/windowManager', () => { beforeEach(() => { MainWindow.get.mockReturnValue(mainWindow); jest.useFakeTimers(); + MainWindow.get.mockReturnValue(mainWindow); ViewManager.getCurrentView.mockReturnValue(view); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); }); @@ -208,19 +233,18 @@ describe('main/windows/windowManager', () => { }, }, }; - - windowManager.teamDropdown = { - updateWindowBounds: jest.fn(), - }; const mainWindow = { getContentBounds: () => ({width: 1000, height: 900}), getSize: () => [1000, 900], }; + windowManager.teamDropdown = { + updateWindowBounds: jest.fn(), + }; beforeEach(() => { - ViewManager.getCurrentView.mockReturnValue(view); - ViewManager.isLoadingScreenHidden.mockReturnValue(true); MainWindow.get.mockReturnValue(mainWindow); + LoadingScreen.isHidden.mockReturnValue(true); + ViewManager.getCurrentView.mockReturnValue(view); getAdjustedWindowBoundaries.mockImplementation((width, height) => ({width, height})); }); @@ -331,7 +355,6 @@ describe('main/windows/windowManager', () => { afterEach(() => { jest.resetAllMocks(); - delete windowManager.settingsWindow; }); it('should restore main window if minimized', () => { @@ -409,8 +432,6 @@ describe('main/windows/windowManager', () => { afterEach(() => { jest.resetAllMocks(); - delete windowManager.mainWindow; - delete windowManager.settingsWindow; }); it('should do nothing when the windows arent set', () => { @@ -458,68 +479,35 @@ describe('main/windows/windowManager', () => { describe('switchServer', () => { const windowManager = new WindowManager(); - const servers = [ - { - 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, - }, - ]; - const map = servers.reduce((arr, item) => { - item.tabs.forEach((tab) => { - arr.push([`${item.name}_${tab.name}`, {}]); - }); - return arr; - }, []); - const views = new Map(map); + const views = new Map([ + ['tab-1', {id: 'tab-1'}], + ['tab-2', {id: 'tab-2'}], + ['tab-3', {id: 'tab-3'}], + ]); beforeEach(() => { jest.useFakeTimers(); - getTabViewName.mockImplementation((server, tab) => `${server}_${tab}`); - Config.teams = servers.concat(); - ViewManager.getView.mockImplementation((name) => views.get(name)); + const server1 = { + id: 'server-1', + }; + 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(() => { jest.resetAllMocks(); - Config.teams = []; }); afterAll(() => { @@ -531,30 +519,33 @@ describe('main/windows/windowManager', () => { it('should do nothing if cannot find the server', () => { windowManager.switchServer('server-3'); 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', () => { + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); 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', () => { + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-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', () => { - views.delete('server-1_tab-3'); + ServerManager.getLastActiveTabForServer.mockReturnValue({id: 'tab-3'}); + views.delete('tab-3'); windowManager.switchServer('server-1', true); - expect(ViewManager.showByName).not.toBeCalled(); + expect(ViewManager.showById).not.toBeCalled(); 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); - 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(); beforeEach(() => { - Config.teams = [ + const tabs = [ { - 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, - }, - ], + id: 'tab-1', + type: 'tab-1', + isOpen: false, + }, + { + id: 'tab-2', + type: 'tab-2', + isOpen: true, + }, + { + id: 'tab-3', + type: 'tab-3', + isOpen: true, }, ]; + ServerManager.getOrderedTabsForServer.mockReturnValue(tabs); }); afterEach(() => { jest.resetAllMocks(); - Config.teams = []; }); it('should select next server when open', () => { ViewManager.getCurrentView.mockReturnValue({ tab: { server: { - name: 'server-1', + id: 'server-1', }, type: 'tab-3', }, }); 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', () => { ViewManager.getCurrentView.mockReturnValue({ tab: { server: { - name: 'server-1', + id: 'server-1', }, type: 'tab-2', }, }); 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', () => { ViewManager.getCurrentView.mockReturnValue({ tab: { server: { - name: 'server-1', + id: 'server-1', }, type: 'tab-2', }, }); 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(); it('should return calls widget URL', () => { + ViewManager.getView.mockReturnValue({name: 'server-1_tab-messaging'}); CallsWidgetWindow.getURL.mockReturnValue('http://server-1.com'); CallsWidgetWindow.isCallsWidget.mockReturnValue(true); expect(windowManager.getServerURLFromWebContentsId('callsID')).toBe('http://server-1.com'); diff --git a/src/main/windows/windowManager.ts b/src/main/windows/windowManager.ts index e96a2d1f..4e203c9c 100644 --- a/src/main/windows/windowManager.ts +++ b/src/main/windows/windowManager.ts @@ -2,25 +2,26 @@ // See LICENSE.txt for license information. /* 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 { MAXIMIZE_CHANGE, - FOCUS_THREE_DOT_MENU, GET_DARK_MODE, UPDATE_SHORTCUT_MENU, - GET_VIEW_WEBCONTENTS_ID, RESIZE_MODAL, VIEW_FINISHED_RESIZING, + WINDOW_CLOSE, + WINDOW_MAXIMIZE, + WINDOW_MINIMIZE, + WINDOW_RESTORE, + DOUBLE_CLICK_ON_WINDOW, } from 'common/communication'; import {Logger} from 'common/log'; import {SECOND} from 'common/utils/constants'; import Config from 'common/config'; -import {getTabViewName} from 'common/tabs/TabView'; -import {MattermostView} from 'main/views/MattermostView'; +import ServerManager from 'common/servers/serverManager'; import { getAdjustedWindowBoundaries, @@ -29,6 +30,7 @@ import { import ViewManager from '../views/viewManager'; import LoadingScreen from '../views/loadingScreen'; +import {MattermostView} from '../views/MattermostView'; import TeamDropdownView from '../views/teamDropdownView'; import DownloadsDropdownView from '../views/downloadsDropdownView'; import DownloadsDropdownMenuView from '../views/downloadsDropdownMenuView'; @@ -42,20 +44,22 @@ import SettingsWindow from './settingsWindow'; const log = new Logger('WindowManager'); export class WindowManager { - assetsDir: string; + private teamDropdown?: TeamDropdownView; + private downloadsDropdown?: DownloadsDropdownView; + private downloadsDropdownMenu?: DownloadsDropdownMenuView; - teamDropdown?: TeamDropdownView; - downloadsDropdown?: DownloadsDropdownView; - downloadsDropdownMenu?: DownloadsDropdownMenuView; - currentServerName?: string; - missingScreensharePermissions?: boolean; + private isResizing: boolean; constructor() { - this.assetsDir = path.resolve(app.getAppPath(), 'assets'); + this.isResizing = false; ipcMain.handle(GET_DARK_MODE, this.handleGetDarkMode); - ipcMain.handle(GET_VIEW_WEBCONTENTS_ID, this.handleGetWebContentsId); 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) => { @@ -102,21 +106,153 @@ export class WindowManager { 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.downloadsDropdownMenu?.updateWindowBounds(); this.sendToRenderer(MAXIMIZE_CHANGE, true); } - handleUnmaximizeMainWindow = () => { + private handleUnmaximizeMainWindow = () => { this.downloadsDropdown?.updateWindowBounds(); this.downloadsDropdownMenu?.updateWindowBounds(); this.sendToRenderer(MAXIMIZE_CHANGE, false); } - isResizing = false; - - handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => { + private handleWillResizeMainWindow = (event: Event, newBounds: Electron.Rectangle) => { log.silly('handleWillResizeMainWindow'); if (!MainWindow.get()) { @@ -146,21 +282,15 @@ export class WindowManager { ipcMain.emit(RESIZE_MODAL, null, newBounds); } - handleResizedMainWindow = () => { + private handleResizedMainWindow = () => { log.silly('handleResizedMainWindow'); - if (MainWindow.get()) { - const bounds = this.getBounds(); - this.throttledWillResize(bounds); - ipcMain.emit(RESIZE_MODAL, null, bounds); - this.teamDropdown?.updateWindowBounds(); - this.downloadsDropdown?.updateWindowBounds(); - this.downloadsDropdownMenu?.updateWindowBounds(); - } - this.isResizing = false; - } - - handleViewFinishedResizing = () => { + const bounds = this.getBounds(); + this.throttledWillResize(bounds); + ipcMain.emit(RESIZE_MODAL, null, bounds); + this.teamDropdown?.updateWindowBounds(); + this.downloadsDropdown?.updateWindowBounds(); + this.downloadsDropdownMenu?.updateWindowBounds(); this.isResizing = false; } @@ -171,7 +301,7 @@ export class WindowManager { this.setCurrentViewBounds(newBounds); } - handleResizeMainWindow = () => { + private handleResizeMainWindow = () => { log.silly('handleResizeMainWindow'); if (!MainWindow.get()) { @@ -194,7 +324,7 @@ export class WindowManager { ipcMain.emit(RESIZE_MODAL, null, bounds); }; - setCurrentViewBounds = (bounds: {width: number; height: number}) => { + private setCurrentViewBounds = (bounds: {width: number; height: number}) => { log.debug('setCurrentViewBounds', {bounds}); const currentView = ViewManager.getCurrentView(); @@ -228,64 +358,46 @@ export class WindowManager { 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[]) => { - const mainWindow = MainWindow.get(); - if (!mainWindow || !MainWindow.isReady) { - if (maxRetries > 0) { - log.info(`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; + /***************** + * IPC EVENT HANDLERS + *****************/ + + private handleGetDarkMode = () => { + return Config.darkMode; + } + + private handleViewFinishedResizing = () => { + this.isResizing = false; + } + + 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); } - - sendToRenderer = (channel: string, ...args: unknown[]) => { - this.sendToRendererWithRetry(3, channel, ...args); - } - - 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'); + private handleMinimize = () => { + const focused = BrowserWindow.getFocusedWindow(); + if (focused) { + focused.minimize(); } - - 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 handleRestore = () => { + const focused = BrowserWindow.getFocusedWindow(); + if (focused) { + focused.restore(); + } + if (focused?.isFullScreen()) { + focused.setFullScreen(false); } } handleDoubleClick = (e: IpcMainEvent, windowType?: string) => { - log.debug('WindowManager.handleDoubleClick', windowType); + log.debug('handleDoubleClick', windowType); let action = 'Maximize'; if (process.platform === 'darwin') { @@ -313,150 +425,6 @@ export class WindowManager { 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(); diff --git a/src/renderer/components/ConfigureServer.tsx b/src/renderer/components/ConfigureServer.tsx index 363a282f..8270e4ba 100644 --- a/src/renderer/components/ConfigureServer.tsx +++ b/src/renderer/components/ConfigureServer.tsx @@ -5,7 +5,7 @@ import React, {useState, useCallback, useEffect} from 'react'; import {useIntl, FormattedMessage} from 'react-intl'; import classNames from 'classnames'; -import {TeamWithIndex} from 'types/config'; +import {MattermostTeam} from 'types/config'; import womanLaptop from 'renderer/assets/svg/womanLaptop.svg'; @@ -22,8 +22,8 @@ import 'renderer/css/components/ConfigureServer.scss'; import 'renderer/css/components/LoadingScreen.css'; type ConfigureServerProps = { - currentTeams: TeamWithIndex[]; - team?: TeamWithIndex; + currentTeams: MattermostTeam[]; + team?: MattermostTeam; mobileView?: boolean; darkMode?: boolean; messageTitle?: string; @@ -32,7 +32,7 @@ type ConfigureServerProps = { alternateLinkMessage?: string; alternateLinkText?: string; alternateLinkURL?: string; - onConnect: (data: TeamWithIndex) => void; + onConnect: (data: MattermostTeam) => void; }; function ConfigureServer({ @@ -53,8 +53,7 @@ function ConfigureServer({ const { name: prevName, url: prevURL, - order = 0, - index = NaN, + id, } = team || {}; const [transition, setTransition] = useState<'inFromRight' | 'outToLeft'>(); @@ -200,8 +199,7 @@ function ConfigureServer({ onConnect({ url: fullURL, name, - index, - order, + id, }); }, MODAL_TRANSITION_TIMEOUT); }; diff --git a/src/renderer/components/MainPage.tsx b/src/renderer/components/MainPage.tsx index b7ba76e4..92b670b4 100644 --- a/src/renderer/components/MainPage.tsx +++ b/src/renderer/components/MainPage.tsx @@ -10,12 +10,9 @@ import {Container, Row} from 'react-bootstrap'; import {DropResult} from 'react-beautiful-dnd'; import {injectIntl, IntlShape} from 'react-intl'; -import {TeamWithTabs} from 'types/config'; +import {MattermostTab, MattermostTeam} from 'types/config'; 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 maximizeButton from '../../assets/titlebar/chrome-maximize.svg'; import minimizeButton from '../../assets/titlebar/chrome-minimize.svg'; @@ -40,9 +37,6 @@ enum Status { } type Props = { - teams: TeamWithTabs[]; - lastActiveTeam?: number; - moveTabs: (teamName: string, originalOrder: number, newOrder: number) => number | undefined; openMenu: () => void; darkMode: boolean; appName: string; @@ -51,8 +45,10 @@ type Props = { }; type State = { - activeServerName?: string; - activeTabName?: string; + activeServerId?: string; + activeTabId?: string; + servers: MattermostTeam[]; + tabs: Map; sessionsExpired: Record; unreadCounts: Record; mentionCounts: Record; @@ -87,21 +83,14 @@ class MainPage extends React.PureComponent { this.topBar = 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 = { - activeServerName: firstServer?.name, - activeTabName: firstTab?.name, + servers: [], + tabs: new Map(), sessionsExpired: {}, unreadCounts: {}, mentionCounts: {}, 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, isMenuOpen: false, isDownloadsDropdownOpen: false, @@ -112,10 +101,10 @@ class MainPage extends React.PureComponent { } getTabViewStatus() { - if (!this.state.activeServerName || !this.state.activeTabName) { + if (!this.state.activeTabId) { 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) { @@ -135,13 +124,49 @@ class MainPage extends React.PureComponent { } } - 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 - this.requestDownloadsLength(); + await this.requestDownloadsLength(); + await this.updateServers(); + + window.desktop.onUpdateServers(this.updateServers); // set page on retry - window.desktop.onLoadRetry((viewName, retry, err, loadUrl) => { - console.log(`${viewName}: failed to load ${err}, but retrying`); + window.desktop.onLoadRetry((viewId, retry, err, loadUrl) => { + console.log(`${viewId}: failed to load ${err}, but retrying`); const statusValue = { status: Status.RETRY, extra: { @@ -150,15 +175,15 @@ class MainPage extends React.PureComponent { url: loadUrl, }, }; - this.updateTabStatus(viewName, statusValue); + this.updateTabStatus(viewId, statusValue); }); - window.desktop.onLoadSuccess((viewName) => { - this.updateTabStatus(viewName, {status: Status.DONE}); + window.desktop.onLoadSuccess((viewId) => { + this.updateTabStatus(viewId, {status: Status.DONE}); }); - window.desktop.onLoadFailed((viewName, err, loadUrl) => { - console.log(`${viewName}: failed to load ${err}`); + window.desktop.onLoadFailed((viewId, err, loadUrl) => { + console.log(`${viewId}: failed to load ${err}`); const statusValue = { status: Status.FAILED, extra: { @@ -166,7 +191,7 @@ class MainPage extends React.PureComponent { url: loadUrl, }, }; - this.updateTabStatus(viewName, statusValue); + this.updateTabStatus(viewId, statusValue); }); window.desktop.onDarkModeChange((darkMode) => { @@ -174,9 +199,7 @@ class MainPage extends React.PureComponent { }); // can't switch tabs sequentially for some reason... - window.desktop.onSetActiveView((serverName, tabName) => { - this.setState({activeServerName: serverName, activeTabName: tabName}); - }); + window.desktop.onSetActiveView(this.setActiveView); window.desktop.onMaximizeChange(this.handleMaximizeState); @@ -259,6 +282,13 @@ class MainPage extends React.PureComponent { 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 = () => { window.desktop.closeTeamsDropdown(); this.closeDownloadsDropdown(); @@ -272,18 +302,12 @@ class MainPage extends React.PureComponent { this.setState({fullScreen: isFullScreen}); } - handleSelectTab = (name: string) => { - if (!this.state.activeServerName) { - return; - } - window.desktop.switchTab(this.state.activeServerName, name); + handleSelectTab = (tabId: string) => { + window.desktop.switchTab(tabId); } - handleCloseTab = (name: string) => { - if (!this.state.activeServerName) { - return; - } - window.desktop.closeTab(this.state.activeServerName, name); + handleCloseTab = (tabId: string) => { + window.desktop.closeTab(tabId); } handleDragAndDrop = async (dropResult: DropResult) => { @@ -292,20 +316,22 @@ class MainPage extends React.PureComponent { if (addedIndex === undefined || removedIndex === addedIndex) { return; } - if (!this.state.activeServerName) { - return; - } - const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs; - if (!currentTabs) { + if (!(this.state.activeServerId && this.state.tabs.has(this.state.activeServerId))) { // TODO: figure out something here return; } - const teamIndex = this.props.moveTabs(this.state.activeServerName, removedIndex, addedIndex < currentTabs.length ? addedIndex : currentTabs.length - 1); - if (!teamIndex) { - return; - } - const name = currentTabs[teamIndex].name; - this.handleSelectTab(name); + const currentTabs = this.state.tabs.get(this.state.activeServerId)!; + const tabsCopy = currentTabs.concat(); + + const tab = tabsCopy.splice(removedIndex, 1); + const newOrder = addedIndex < currentTabs.length ? addedIndex : currentTabs.length - 1; + 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) => { @@ -336,7 +362,7 @@ class MainPage extends React.PureComponent { } focusOnWebView = () => { - window.desktop.focusBrowserView(); + window.desktop.focusCurrentView(); this.handleCloseDropdowns(); } @@ -373,7 +399,10 @@ class MainPage extends React.PureComponent { render() { 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 = ( { sessionsExpired={this.state.sessionsExpired} unreadCounts={this.state.unreadCounts} mentionCounts={this.state.mentionCounts} - activeServerName={this.state.activeServerName} - activeTabName={this.state.activeTabName} + activeServerId={this.state.activeServerId} + activeTabId={this.state.activeTabId} onSelect={this.handleSelectTab} onCloseTab={this.handleCloseTab} onDrop={this.handleDragAndDrop} @@ -463,20 +492,22 @@ class MainPage extends React.PureComponent { ); } - const serverMatch = `${escapeRegex(this.state.activeServerName)}___TAB_[A-Z]+`; const totalMentionCount = Object.keys(this.state.mentionCounts).reduce((sum, key) => { // 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 + this.state.mentionCounts[key]; }, 0); 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 || this.state.unreadCounts[key]; }, false); + + const activeServer = this.state.servers.find((srv) => srv.id === this.state.activeServerId); + const topRow = ( { ref={this.topBar} className={'topBar-bg'} > - {window.process.platform !== 'linux' && this.props.teams.length === 0 && ( + {window.process.platform !== 'linux' && this.state.servers.length === 0 && (
{intl.formatMessage({id: 'renderer.components.mainPage.titleBar', defaultMessage: 'Mattermost'})}
@@ -506,10 +537,10 @@ class MainPage extends React.PureComponent { })} /> - {this.props.teams.length !== 0 && ( + {activeServer && ( { ); const views = () => { - if (!this.props.teams.length) { + if (!activeServer) { return null; } let component; const tabStatus = this.getTabViewStatus(); if (!tabStatus) { - if (this.state.activeTabName || this.state.activeServerName) { - console.error(`Not tabStatus for ${this.state.activeTabName}`); + if (this.state.activeTabId) { + console.error(`Not tabStatus for ${this.state.activeTabId}`); } return null; } @@ -539,7 +570,7 @@ class MainPage extends React.PureComponent { case Status.FAILED: component = ( void; - onSave?: (team: TeamWithIndex) => void; - team?: TeamWithIndex; - currentTeams?: TeamWithIndex[]; + onSave?: (team: MattermostTeam) => void; + team?: MattermostTeam; + currentTeams?: MattermostTeam[]; editMode?: boolean; show?: boolean; restoreFocus?: boolean; @@ -26,7 +26,7 @@ type Props = { type State = { teamName: string; teamUrl: string; - teamIndex?: number; + teamId?: string; teamOrder: number; saveStarted: boolean; } @@ -55,8 +55,7 @@ class NewTeamModal extends React.PureComponent { this.setState({ teamName: this.props.team ? this.props.team.name : '', teamUrl: this.props.team ? this.props.team.url : '', - teamIndex: this.props.team?.index, - teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0), + teamId: this.props.team?.id, saveStarted: false, }); } @@ -67,10 +66,7 @@ class NewTeamModal extends React.PureComponent { } if (this.props.currentTeams) { const currentTeams = [...this.props.currentTeams]; - if (this.props.editMode && this.props.team) { - currentTeams.splice(this.props.team.index, 1); - } - if (currentTeams.find((team) => team.name === this.state.teamName)) { + if (currentTeams.find((team) => team.id !== this.state.teamId && team.name === this.state.teamName)) { return ( { } if (this.props.currentTeams) { const currentTeams = [...this.props.currentTeams]; - if (this.props.editMode && this.props.team) { - currentTeams.splice(this.props.team.index, 1); - } - if (currentTeams.find((team) => team.url === this.state.teamUrl)) { + if (currentTeams.find((team) => team.id !== this.state.teamId && team.url === this.state.teamUrl)) { return ( { this.props.onSave?.({ url: this.state.teamUrl, name: this.state.teamName, - index: this.state.teamIndex!, - order: this.state.teamOrder, + id: this.state.teamId, }); } }); diff --git a/src/renderer/components/TabBar.tsx b/src/renderer/components/TabBar.tsx index fcb6ecdb..ce178e2b 100644 --- a/src/renderer/components/TabBar.tsx +++ b/src/renderer/components/TabBar.tsx @@ -8,18 +8,18 @@ import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDra import {FormattedMessage, injectIntl, IntlShape} from 'react-intl'; 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 = { - activeTabName?: string; - activeServerName?: string; + activeTabId?: string; + activeServerId?: string; id: string; isDarkMode: boolean; - onSelect: (name: string, index: number) => void; - onCloseTab: (name: string) => void; - tabs: ConfigTab[]; + onSelect: (id: string) => void; + onCloseTab: (id: string) => void; + tabs: MattermostTab[]; sessionsExpired: Record; unreadCounts: Record; mentionCounts: Record; @@ -41,25 +41,21 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) { } class TabBar extends React.PureComponent { - onCloseTab = (name: string) => { + onCloseTab = (id: string) => { return (event: React.MouseEvent) => { event.stopPropagation(); - this.props.onCloseTab(name); + this.props.onCloseTab(id); }; } render() { - const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order); - const tabs = orderedTabs.map((tab, orderedIndex) => { - const index = this.props.tabs.indexOf(tab); - const tabName = getTabViewName(this.props.activeServerName!, tab.name); - - const sessionExpired = this.props.sessionsExpired[tabName]; - const hasUnreads = this.props.unreadCounts[tabName]; + const tabs = this.props.tabs.map((tab, index) => { + const sessionExpired = this.props.sessionsExpired[tab.id!]; + const hasUnreads = this.props.unreadCounts[tab.id!]; let mentionCount = 0; - if (this.props.mentionCounts[tabName] > 0) { - mentionCount = this.props.mentionCounts[tabName]; + if (this.props.mentionCounts[tab.id!] > 0) { + mentionCount = this.props.mentionCounts[tab.id!]; } let badgeDiv: React.ReactNode; @@ -83,9 +79,9 @@ class TabBar extends React.PureComponent { return ( {(provided, snapshot) => { if (!tab.isOpen) { @@ -106,7 +102,7 @@ class TabBar extends React.PureComponent { draggable={false} title={this.props.intl.formatMessage({id: `common.tabs.${tab.name}`, defaultMessage: getTabDisplayName(tab.name as TabType)})} className={classNames('teamTabItem', { - active: this.props.activeTabName === tab.name, + active: this.props.activeTabId === tab.id, dragging: snapshot.isDragging, })} {...provided.draggableProps} @@ -116,10 +112,10 @@ class TabBar extends React.PureComponent { { - this.props.onSelect(tab.name, index); + this.props.onSelect(tab.id!); }} >
@@ -131,7 +127,7 @@ class TabBar extends React.PureComponent { {canCloseTab(tab.name as TabType) && diff --git a/src/renderer/dropdown.tsx b/src/renderer/dropdown.tsx index 13a7a4b5..4ef2a053 100644 --- a/src/renderer/dropdown.tsx +++ b/src/renderer/dropdown.tsx @@ -7,9 +7,8 @@ import {FormattedMessage} from 'react-intl'; import classNames from 'classnames'; 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 './css/dropdown.scss'; @@ -17,8 +16,9 @@ import './css/dropdown.scss'; import IntlProvider from './intl_provider'; type State = { - teams?: TeamWithTabsAndGpo[]; - orderedTeams?: TeamWithTabsAndGpo[]; + teams?: MattermostTeam[]; + teamOrder?: string[]; + orderedTeams?: MattermostTeam[]; activeTeam?: string; darkMode?: boolean; enableServerManagement?: boolean; @@ -59,7 +59,7 @@ class TeamDropdown extends React.PureComponent, State> { } handleUpdate = ( - teams: TeamWithTabsAndGpo[], + teams: MattermostTeam[], darkMode: boolean, windowBounds: Electron.Rectangle, activeTeam?: string, @@ -71,7 +71,6 @@ class TeamDropdown extends React.PureComponent, State> { ) => { this.setState({ teams, - orderedTeams: teams.concat().sort((a: TeamWithTabs, b: TeamWithTabs) => a.order - b.order), activeTeam, darkMode, enableServerManagement, @@ -83,9 +82,12 @@ class TeamDropdown extends React.PureComponent, State> { }); } - selectServer = (team: FullTeam) => { + selectServer = (team: MattermostTeam) => { return () => { - window.desktop.serverDropdown.switchServer(team.name); + if (!team.id) { + return; + } + window.desktop.serverDropdown.switchServer(team.id); this.closeMenu(); }; } @@ -106,8 +108,8 @@ class TeamDropdown extends React.PureComponent, State> { this.closeMenu(); } - isActiveTeam = (team: FullTeam) => { - return team.name === this.state.activeTeam; + isActiveTeam = (team: MattermostTeam) => { + return team.id === this.state.activeTeam; } onDragStart = () => { @@ -124,23 +126,14 @@ class TeamDropdown extends React.PureComponent, State> { if (!this.state.teams) { throw new Error('No config'); } - const teams = this.state.teams.concat(); - const tabOrder = teams.map((team, index) => { - return { - index, - order: team.order, - }; - }).sort((a, b) => (a.order - b.order)); + const teamsCopy = this.state.teams.concat(); - 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; - tabOrder.splice(newOrder, 0, team[0]); + teamsCopy.splice(newOrder, 0, team[0]); - tabOrder.forEach((t, order) => { - teams[t.index].order = order; - }); - this.setState({teams, orderedTeams: teams.concat().sort((a: FullTeam, b: FullTeam) => a.order - b.order), isAnyDragging: false}); - window.desktop.updateTeams(teams); + this.setState({teams: teamsCopy, isAnyDragging: false}); + window.desktop.updateServerOrder(teamsCopy.map((team) => team.id!)); } componentDidMount() { @@ -210,30 +203,30 @@ class TeamDropdown extends React.PureComponent, State> { } } - editServer = (teamName: string) => { - if (this.teamIsGpo(teamName)) { + editServer = (teamId: string) => { + if (this.teamIsPredefined(teamId)) { return () => {}; } return (event: React.MouseEvent) => { event.stopPropagation(); - window.desktop.serverDropdown.showEditServerModal(teamName); + window.desktop.serverDropdown.showEditServerModal(teamId); this.closeMenu(); }; } - removeServer = (teamName: string) => { - if (this.teamIsGpo(teamName)) { + removeServer = (teamId: string) => { + if (this.teamIsPredefined(teamId)) { return () => {}; } return (event: React.MouseEvent) => { event.stopPropagation(); - window.desktop.serverDropdown.showRemoveServerModal(teamName); + window.desktop.serverDropdown.showRemoveServerModal(teamId); this.closeMenu(); }; } - teamIsGpo = (teamName: string) => { - return this.state.orderedTeams?.some((team) => team.name === teamName && team.isGpo); + teamIsPredefined = (teamId: string) => { + return this.state.teams?.some((team) => team.id === teamId && team.isPredefined); } render() { @@ -275,15 +268,11 @@ class TeamDropdown extends React.PureComponent, State> { ref={provided.innerRef} {...provided.droppableProps} > - {this.state.orderedTeams?.map((team, orderedIndex) => { + {this.state.teams?.map((team, orderedIndex) => { const index = this.state.teams?.indexOf(team); - const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => { - const tabName = getTabViewName(team.name, tab.name); - counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired; - 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}); + const sessionExpired = this.state.expired?.get(team.id!); + const hasUnreads = this.state.unreads?.get(team.id!); + const mentionCount = this.state.mentions?.get(team.id!); let badgeDiv: React.ReactNode; if (sessionExpired) { @@ -334,16 +323,16 @@ class TeamDropdown extends React.PureComponent, State> { {this.isActiveTeam(team) ? : } {team.name}
- {!team.isGpo &&
+ {!team.isPredefined &&
@@ -365,7 +354,7 @@ class TeamDropdown extends React.PureComponent, State> { {this.state.enableServerManagement &&