[MM-36431] Logic to support multiple configurable tabs per server (#1655)

* Updated config, added types and classes for messaging tab

* Working app with tabs and servers

* Remainder of logic

* Make base tab abstract class

* Account for new app case

* Merge'd

* PR feedback
This commit is contained in:
Devin Binnie
2021-07-20 09:05:53 -04:00
committed by GitHub
parent 29a049e8ae
commit d3599fc500
30 changed files with 636 additions and 361 deletions

View File

@@ -2,7 +2,8 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
export const SWITCH_SERVER = 'switch-server'; export const SWITCH_SERVER = 'switch-server';
export const SET_SERVER_KEY = 'set-server-key'; export const SWITCH_TAB = 'switch-tab';
export const SET_ACTIVE_VIEW = 'set-active-view';
export const MARK_READ = 'mark-read'; export const MARK_READ = 'mark-read';
export const FOCUS_BROWSERVIEW = 'focus-browserview'; export const FOCUS_BROWSERVIEW = 'focus-browserview';
export const ZOOM = 'zoom'; export const ZOOM = 'zoom';
@@ -62,7 +63,7 @@ export const SESSION_EXPIRED = 'session_expired';
export const UPDATE_TRAY = 'update_tray'; export const UPDATE_TRAY = 'update_tray';
export const UPDATE_BADGE = 'update_badge'; export const UPDATE_BADGE = 'update_badge';
export const SET_SERVER_NAME = 'set-server-name'; export const SET_VIEW_NAME = 'set-view-name';
export const REACT_APP_INITIALIZED = 'react-app-initialized'; export const REACT_APP_INITIALIZED = 'react-app-initialized';
export const TOGGLE_BACK_BUTTON = 'toggle-back-button'; export const TOGGLE_BACK_BUTTON = 'toggle-back-button';

View File

@@ -10,14 +10,14 @@ import os from 'os';
* @param {number} version - Scheme version. (Not application version) * @param {number} version - Scheme version. (Not application version)
*/ */
import {ConfigV2} from 'types/config'; import {ConfigV3} from 'types/config';
export const getDefaultDownloadLocation = (): string => { export const getDefaultDownloadLocation = (): string => {
return path.join(os.homedir(), 'Downloads'); return path.join(os.homedir(), 'Downloads');
}; };
const defaultPreferences: ConfigV2 = { const defaultPreferences: ConfigV3 = {
version: 2, version: 3,
teams: [], teams: [],
showTrayIcon: true, showTrayIcon: true,
trayIconTheme: 'light', trayIconTheme: 'light',

View File

@@ -16,12 +16,13 @@ import {
Config as ConfigType, Config as ConfigType,
LocalConfiguration, LocalConfiguration,
RegistryConfig as RegistryConfigType, RegistryConfig as RegistryConfigType,
Team, TeamWithTabs,
} from 'types/config'; } from 'types/config';
import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION} from 'common/communication'; import {UPDATE_TEAMS, GET_CONFIGURATION, UPDATE_CONFIGURATION, GET_LOCAL_CONFIGURATION} from 'common/communication';
import * as Validator from '../../main/Validator'; import * as Validator from 'main/Validator';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences'; import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences';
import upgradeConfigData from './upgradePreferences'; import upgradeConfigData from './upgradePreferences';
@@ -263,6 +264,9 @@ export default class Config extends EventEmitter {
// validate based on config file version // validate based on config file version
switch (configData.version) { switch (configData.version) {
case 3:
configData = Validator.validateV3ConfigData(configData)!;
break;
case 2: case 2:
configData = Validator.validateV2ConfigData(configData)!; configData = Validator.validateV2ConfigData(configData)!;
break; break;
@@ -317,16 +321,16 @@ export default class Config extends EventEmitter {
delete this.combinedData!.defaultTeams; delete this.combinedData!.defaultTeams;
// IMPORTANT: properly combine teams from all sources // IMPORTANT: properly combine teams from all sources
let combinedTeams = []; let combinedTeams: TeamWithTabs[] = [];
// - start by adding default teams from buildConfig, if any // - start by adding default teams from buildConfig, if any
if (this.buildConfigData?.defaultTeams?.length) { if (this.buildConfigData?.defaultTeams?.length) {
combinedTeams.push(...this.buildConfigData.defaultTeams); combinedTeams.push(...this.buildConfigData.defaultTeams.map((team) => getDefaultTeamWithTabsFromTeam(team)));
} }
// - add registry defined teams, if any // - add registry defined teams, if any
if (this.registryConfigData?.teams?.length) { if (this.registryConfigData?.teams?.length) {
combinedTeams.push(...this.registryConfigData.teams); combinedTeams.push(...this.registryConfigData.teams.map((team) => getDefaultTeamWithTabsFromTeam(team)));
} }
// - add locally defined teams only if server management is enabled // - add locally defined teams only if server management is enabled
@@ -352,7 +356,7 @@ export default class Config extends EventEmitter {
* *
* @param {array} teams array of teams to check for duplicates * @param {array} teams array of teams to check for duplicates
*/ */
filterOutDuplicateTeams = (teams: Team[]) => { filterOutDuplicateTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams; let newTeams = teams;
const uniqueURLs = new Set(); const uniqueURLs = new Set();
newTeams = newTeams.filter((team) => { newTeams = newTeams.filter((team) => {
@@ -365,7 +369,7 @@ export default class Config extends EventEmitter {
* Returns the provided array fo teams with existing teams filtered out * Returns the provided array fo teams with existing teams filtered out
* @param {array} teams array of teams to check for already defined teams * @param {array} teams array of teams to check for already defined teams
*/ */
filterOutPredefinedTeams = (teams: Team[]) => { filterOutPredefinedTeams = (teams: TeamWithTabs[]) => {
let newTeams = teams; let newTeams = teams;
// filter out predefined teams // filter out predefined teams
@@ -380,7 +384,7 @@ export default class Config extends EventEmitter {
* Apply a default sort order to the team list, if no order is specified. * Apply a default sort order to the team list, if no order is specified.
* @param {array} teams to sort * @param {array} teams to sort
*/ */
sortUnorderedTeams = (teams: Team[]) => { sortUnorderedTeams = (teams: TeamWithTabs[]) => {
// We want to preserve the array order of teams in the config, otherwise a lot of bugs will occur // 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})); const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
@@ -470,7 +474,7 @@ export default class Config extends EventEmitter {
return config; return config;
} }
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: Team[]) => { handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: TeamWithTabs[]) => {
this.set('teams', newTeams); this.set('teams', newTeams);
return this.combinedData!.teams; return this.combinedData!.teams;
} }

View File

@@ -1,9 +1,9 @@
// Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {ConfigV0, ConfigV1} from 'types/config'; import {ConfigV0, ConfigV1, ConfigV2} from 'types/config';
import defaultPreferences from './defaultPreferences'; import defaultPreferences, {getDefaultDownloadLocation} from './defaultPreferences';
const pastDefaultPreferences = { const pastDefaultPreferences = {
0: { 0: {
@@ -26,7 +26,26 @@ const pastDefaultPreferences = {
autostart: true, autostart: true,
spellCheckerLocale: 'en-US', spellCheckerLocale: 'en-US',
} as ConfigV1, } as ConfigV1,
2: defaultPreferences, 2: {
version: 2,
teams: [],
showTrayIcon: true,
trayIconTheme: 'light',
minimizeToTray: true,
notifications: {
flashWindow: 2,
bounceIcon: true,
bounceIconType: 'informational',
},
showUnreadBadge: true,
useSpellChecker: true,
enableHardwareAcceleration: true,
autostart: true,
spellCheckerLocale: 'en-US',
darkMode: false,
downloadLocation: getDefaultDownloadLocation(),
} as ConfigV2,
3: defaultPreferences,
}; };
export default pastDefaultPreferences; export default pastDefaultPreferences;

View File

@@ -1,7 +1,10 @@
// Copyright (c) 2015-2016 Yuya Ochiai // Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {ConfigV3, ConfigV2, ConfigV1, ConfigV0, AnyConfig} from 'types/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import pastDefaultPreferences from './pastDefaultPreferences'; import pastDefaultPreferences from './pastDefaultPreferences';
@@ -30,10 +33,24 @@ function upgradeV1toV2(configV1: ConfigV1) {
return config; return config;
} }
export default function upgradeToLatest(config: AnyConfig): ConfigV2 { function upgradeV2toV3(configV2: ConfigV2) {
const config: ConfigV3 = Object.assign({}, deepCopy<ConfigV3>(pastDefaultPreferences[3]), configV2);
config.version = 3;
config.teams = configV2.teams.map((value) => {
return {
...getDefaultTeamWithTabsFromTeam(value),
lastActiveTab: 0,
};
});
return config;
}
export default function upgradeToLatest(config: AnyConfig): ConfigV3 {
switch (config.version) { switch (config.version) {
case 3:
return config as ConfigV3;
case 2: case 2:
return config as ConfigV2; return upgradeToLatest(upgradeV2toV3(config as ConfigV2));
case 1: case 1:
return upgradeToLatest(upgradeV1toV2(config as ConfigV1)); return upgradeToLatest(upgradeV1toV2(config as ConfigV1));
default: default:

View File

@@ -0,0 +1,23 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {MattermostServer} from 'common/servers/MattermostServer';
import {getTabViewName, TabType, TabView} from './TabView';
export default abstract class BaseTabView implements TabView {
server: MattermostServer;
constructor(server: MattermostServer) {
this.server = server;
}
get name(): string {
return getTabViewName(this.server.name, this.type);
}
get url(): URL {
throw new Error('Not implemented');
}
get type(): TabType {
throw new Error('Not implemented');
}
}

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,61 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Tab, Team} from 'types/config';
import {MattermostServer} from 'common/servers/MattermostServer';
import MessagingTabView from './MessagingTabView';
import FocalboardTabView from './FocalboardTabView';
import PlaybooksTabView from './PlaybooksTabView';
export const TAB_MESSAGING = 'TAB_MESSAGING';
export const TAB_FOCALBOARD = 'TAB_FOCALBOARD';
export const TAB_PLAYBOOKS = 'TAB_PLAYBOOKS';
export type TabType = typeof TAB_MESSAGING | typeof TAB_FOCALBOARD | typeof TAB_PLAYBOOKS;
export interface TabView {
server: MattermostServer;
get name(): string;
get type(): TabType;
get url(): URL;
}
export function getDefaultTeamWithTabsFromTeam(team: Team) {
return {
...team,
tabs: [
{
name: TAB_MESSAGING,
order: 0,
},
{
name: TAB_FOCALBOARD,
order: 1,
},
{
name: TAB_PLAYBOOKS,
order: 2,
},
],
};
}
// TODO: Might need to move this out
export function getServerView(srv: MattermostServer, tab: Tab) {
switch (tab.name) {
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');
}
}
export function getTabViewName(serverName: string, tabType: string) {
return `${serverName}___${tabType}`;
}

View File

@@ -1,11 +1,14 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {Team} from 'types/config';
import {ServerFromURL} from 'types/utils';
import {isHttpsUri, isHttpUri, isUri} from 'valid-url'; import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
import {TeamWithTabs} from 'types/config';
import {ServerFromURL} from 'types/utils';
import buildConfig from '../config/buildConfig'; import buildConfig from '../config/buildConfig';
import {MattermostServer} from '../servers/MattermostServer';
import {getServerView} from '../tabs/TabView';
// supported custom login paths (oath, saml) // supported custom login paths (oath, saml)
const customLoginRegexPaths = [ const customLoginRegexPaths = [
@@ -156,32 +159,35 @@ function isManagedResource(serverUrl: URL | string, inputURL: URL | string) {
managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`)))); managedResources.some((managedResource) => (parsedURL.pathname.toLowerCase().startsWith(`${server.subpath}${managedResource}/`) || parsedURL.pathname.toLowerCase().startsWith(`/${managedResource}/`))));
} }
function getServer(inputURL: URL | string, teams: Team[], ignoreScheme = false): ServerFromURL | undefined { function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = false): ServerFromURL | undefined {
const parsedURL = parseURL(inputURL); const parsedURL = parseURL(inputURL);
if (!parsedURL) { if (!parsedURL) {
return undefined; return undefined;
} }
let parsedServerUrl; let parsedServerUrl;
let firstOption;
let secondOption; let secondOption;
for (let i = 0; i < teams.length; i++) { teams.forEach((team) => {
parsedServerUrl = parseURL(teams[i].url); const srv = new MattermostServer(team.name, team.url);
if (!parsedServerUrl) { team.tabs.forEach((tab) => {
continue; const tabView = getServerView(srv, tab);
} parsedServerUrl = parseURL(tabView.url);
if (parsedServerUrl) {
// check server and subpath matches (without subpath pathname is \ so it always matches) // check server and subpath matches (without subpath pathname is \ so it always matches)
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) { if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
return {name: teams[i].name, url: parsedServerUrl, index: i}; firstOption = {name: tabView.name, url: parsedServerUrl};
} }
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) { if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
// in case the user added something on the path that doesn't really belong to the server // in case the user added something on the path that doesn't really belong to the server
// there might be more than one that matches, but we can't differentiate, so last one // there might be more than one that matches, but we can't differentiate, so last one
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin) // is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
// e.g.: https://community.mattermost.com/core // e.g.: https://community.mattermost.com/core
secondOption = {name: teams[i].name, url: parsedServerUrl, index: i}; secondOption = {name: tabView.name, url: parsedServerUrl};
} }
} }
return secondOption; });
});
return firstOption || secondOption;
} }
// next two functions are defined to clarify intent // next two functions are defined to clarify intent
@@ -199,15 +205,15 @@ function equalUrlsIgnoringSubpath(url1: URL, url2: URL, ignoreScheme?: boolean)
return url1.origin.toLowerCase() === url2.origin.toLowerCase(); return url1.origin.toLowerCase() === url2.origin.toLowerCase();
} }
function isTrustedURL(url: URL | string, teams: Team[]) { function isTrustedURL(url: URL | string, teams: TeamWithTabs[]) {
const parsedURL = parseURL(url); const parsedURL = parseURL(url);
if (!parsedURL) { if (!parsedURL) {
return false; return false;
} }
return getServer(parsedURL, teams) !== null; return getView(parsedURL, teams) !== null;
} }
function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: Team[]): boolean { function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: TeamWithTabs[]): boolean {
const subpath = server ? server.url.pathname : ''; const subpath = server ? server.url.pathname : '';
const parsedURL = parseURL(url); const parsedURL = parseURL(url);
if (!parsedURL) { if (!parsedURL) {
@@ -242,7 +248,7 @@ export default {
isValidURI, isValidURI,
isInternalURL, isInternalURL,
parseURL, parseURL,
getServer, getView,
getServerInfo, getServerInfo,
isAdminUrl, isAdminUrl,
isTeamUrl, isTeamUrl,

View File

@@ -5,7 +5,7 @@ import log from 'electron-log';
import Joi from '@hapi/joi'; import Joi from '@hapi/joi';
import {Args} from 'types/args'; import {Args} from 'types/args';
import {ConfigV0, ConfigV1, ConfigV2} from 'types/config'; import {ConfigV0, ConfigV1, ConfigV2, ConfigV3} from 'types/config';
import {SavedWindowState} from 'types/mainWindow'; import {SavedWindowState} from 'types/mainWindow';
import {AppState} from 'types/appState'; import {AppState} from 'types/appState';
import {ComparableCertificate} from 'types/certificate'; import {ComparableCertificate} from 'types/certificate';
@@ -92,6 +92,35 @@ const configDataSchemaV2 = Joi.object<ConfigV2>({
downloadLocation: Joi.string(), downloadLocation: Joi.string(),
}); });
const configDataSchemaV3 = Joi.object<ConfigV3>({
version: Joi.number().min(2).default(2),
teams: Joi.array().items(Joi.object({
name: Joi.string().required(),
url: Joi.string().required(),
order: Joi.number().integer().min(0),
lastActiveTab: Joi.number().integer().min(0).default(0),
tabs: Joi.array().items(Joi.object({
name: Joi.string().required(),
order: Joi.number().integer().min(0),
})).default([]),
})).default([]),
showTrayIcon: Joi.boolean().default(false),
trayIconTheme: Joi.any().allow('').valid('light', 'dark').default('light'),
minimizeToTray: Joi.boolean().default(false),
notifications: Joi.object({
flashWindow: Joi.any().valid(0, 2).default(0),
bounceIcon: Joi.boolean().default(false),
bounceIconType: Joi.any().allow('').valid('informational', 'critical').default('informational'),
}),
showUnreadBadge: Joi.boolean().default(true),
useSpellChecker: Joi.boolean().default(true),
enableHardwareAcceleration: Joi.boolean().default(true),
autostart: Joi.boolean().default(true),
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
darkMode: Joi.boolean().default(false),
downloadLocation: Joi.string(),
});
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'}; // eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
const certificateStoreSchema = Joi.object().pattern( const certificateStoreSchema = Joi.object().pattern(
Joi.string().uri(), Joi.string().uri(),
@@ -155,15 +184,22 @@ export function validateV1ConfigData(data: ConfigV1) {
return validateAgainstSchema(data, configDataSchemaV1); return validateAgainstSchema(data, configDataSchemaV1);
} }
function cleanURL(url: string): string {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return updatedURL;
}
export function validateV2ConfigData(data: ConfigV2) { export function validateV2ConfigData(data: ConfigV2) {
if (Array.isArray(data.teams) && data.teams.length) { if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes // first replace possible backslashes with forward slashes
let teams = data.teams.map(({name, url, order}) => { let teams = data.teams.map((team) => {
let updatedURL = url; return {
if (updatedURL.includes('\\')) { ...team,
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/'); url: cleanURL(team.url),
} };
return {name, url: updatedURL, order};
}); });
// next filter out urls that are still invalid so all is not lost // next filter out urls that are still invalid so all is not lost
@@ -175,6 +211,25 @@ export function validateV2ConfigData(data: ConfigV2) {
return validateAgainstSchema(data, configDataSchemaV2); return validateAgainstSchema(data, configDataSchemaV2);
} }
export function validateV3ConfigData(data: ConfigV3) {
if (Array.isArray(data.teams) && data.teams.length) {
// first replace possible backslashes with forward slashes
let teams = data.teams.map((team) => {
return {
...team,
url: cleanURL(team.url),
};
});
// next filter out urls that are still invalid so all is not lost
teams = teams.filter(({url}) => urlUtils.isValidURL(url));
// replace original teams
data.teams = teams;
}
return validateAgainstSchema(data, configDataSchemaV3);
}
// validate certificate.json // validate certificate.json
export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) { export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) {
const jsonData = (typeof data === 'object' ? data : JSON.parse(data)); const jsonData = (typeof data === 'object' ? data : JSON.parse(data));

View File

@@ -44,7 +44,7 @@ export class AuthManager {
handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => { handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => {
event.preventDefault(); event.preventDefault();
const parsedURL = new URL(request.url); const parsedURL = new URL(request.url);
const server = urlUtils.getServer(parsedURL, this.config.teams); const server = urlUtils.getView(parsedURL, this.config.teams);
if (!server) { if (!server) {
return; return;
} }

View File

@@ -36,9 +36,10 @@ import {
RELOAD_CONFIGURATION, RELOAD_CONFIGURATION,
USER_ACTIVITY_UPDATE, USER_ACTIVITY_UPDATE,
EMIT_CONFIGURATION, EMIT_CONFIGURATION,
SWITCH_TAB,
} from 'common/communication'; } from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import Utils from 'common/utils/util'; import Utils from 'common/utils/util';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
@@ -234,6 +235,7 @@ function initializeInterCommunicationEventListeners() {
} }
ipcMain.on(SWITCH_SERVER, handleSwitchServer); ipcMain.on(SWITCH_SERVER, handleSwitchServer);
ipcMain.on(SWITCH_TAB, handleSwitchTab);
ipcMain.on(QUIT, handleQuit); ipcMain.on(QUIT, handleQuit);
@@ -468,6 +470,10 @@ function handleSwitchServer(event: IpcMainEvent, serverName: string) {
WindowManager.switchServer(serverName); WindowManager.switchServer(serverName);
} }
function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) {
WindowManager.switchTab(serverName, tabName);
}
function handleNewServerModal() { function handleNewServerModal() {
const html = getLocalURLString('newServer.html'); const html = getLocalURLString('newServer.html');
@@ -482,7 +488,7 @@ function handleNewServerModal() {
modalPromise.then((data) => { modalPromise.then((data) => {
const teams = config.teams; const teams = config.teams;
const order = teams.length; const order = teams.length;
teams.push({...data, order}); teams.push(getDefaultTeamWithTabsFromTeam({...data, order}));
config.set('teams', teams); config.set('teams', teams);
}).catch((e) => { }).catch((e) => {
// e is undefined for user cancellation // e is undefined for user cancellation
@@ -580,7 +586,7 @@ function initializeAfterAppReady() {
item.on('done', (doneEvent, state) => { item.on('done', (doneEvent, state) => {
if (state === 'completed') { if (state === 'completed') {
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams)!); displayDownloadCompleted(filename, item.savePath, urlUtils.getView(webContents.getURL(), config.teams)!);
} }
}); });
}); });

View File

@@ -5,7 +5,7 @@
import {app, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron'; import {app, Menu, MenuItemConstructorOptions, MenuItem, session, shell, WebContents, webContents} from 'electron';
import {ADD_SERVER, SELECT_NEXT_TAB, SELECT_PREVIOUS_TAB} from 'common/communication'; import {ADD_SERVER} from 'common/communication';
import Config from 'common/config'; import Config from 'common/config';
import * as WindowManager from '../windows/windowManager'; import * as WindowManager from '../windows/windowManager';
@@ -217,14 +217,14 @@ function createTemplate(config: Config) {
label: 'Select Next Server', label: 'Select Next Server',
accelerator: 'Ctrl+Tab', accelerator: 'Ctrl+Tab',
click() { click() {
WindowManager.sendToRenderer(SELECT_NEXT_TAB); WindowManager.selectNextTab();
}, },
enabled: (teams.length > 1), enabled: (teams.length > 1),
}, { }, {
label: 'Select Previous Server', label: 'Select Previous Server',
accelerator: 'Ctrl+Shift+Tab', accelerator: 'Ctrl+Shift+Tab',
click() { click() {
WindowManager.sendToRenderer(SELECT_PREVIOUS_TAB); WindowManager.selectPreviousTab();
}, },
enabled: (teams.length > 1), enabled: (teams.length > 1),
}], }],

View File

@@ -44,6 +44,6 @@ window.addEventListener('message', async (event) => {
} }
}); });
ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads) => { ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, expired, mentions, unreads) => {
window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads}}, window.location.href); window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, expired, mentions, unreads}}, window.location.href);
}); });

View File

@@ -9,7 +9,7 @@
import {ipcRenderer, webFrame} from 'electron'; import {ipcRenderer, webFrame} from 'electron';
import log from 'electron-log'; import log from 'electron-log';
import {NOTIFY_MENTION, IS_UNREAD, UNREAD_RESULT, SESSION_EXPIRED, SET_SERVER_NAME, REACT_APP_INITIALIZED, USER_ACTIVITY_UPDATE, CLOSE_TEAMS_DROPDOWN} from 'common/communication'; import {NOTIFY_MENTION, IS_UNREAD, UNREAD_RESULT, SESSION_EXPIRED, SET_VIEW_NAME, REACT_APP_INITIALIZED, USER_ACTIVITY_UPDATE, CLOSE_TEAMS_DROPDOWN} from 'common/communication';
const UNREAD_COUNT_INTERVAL = 1000; const UNREAD_COUNT_INTERVAL = 1000;
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
@@ -19,7 +19,7 @@ Reflect.deleteProperty(global.Buffer); // http://electron.atom.io/docs/tutorial/
let appVersion; let appVersion;
let appName; let appName;
let sessionExpired; let sessionExpired;
let serverName; let viewName;
log.info('Initializing preload'); log.info('Initializing preload');
@@ -58,7 +58,7 @@ window.addEventListener('load', () => {
return; return;
} }
watchReactAppUntilInitialized(() => { watchReactAppUntilInitialized(() => {
ipcRenderer.send(REACT_APP_INITIALIZED, serverName); ipcRenderer.send(REACT_APP_INITIALIZED, viewName);
}); });
}); });
@@ -146,12 +146,12 @@ const findUnread = (favicon) => {
const result = document.getElementsByClassName(classPair); const result = document.getElementsByClassName(classPair);
return result && result.length > 0; return result && result.length > 0;
}); });
ipcRenderer.send(UNREAD_RESULT, favicon, serverName, isUnread); ipcRenderer.send(UNREAD_RESULT, favicon, viewName, isUnread);
}; };
ipcRenderer.on(IS_UNREAD, (event, favicon, server) => { ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
if (typeof serverName === 'undefined') { if (typeof viewName === 'undefined') {
serverName = server; viewName = server;
} }
if (isReactAppInitialized()) { if (isReactAppInitialized()) {
findUnread(favicon); findUnread(favicon);
@@ -162,13 +162,13 @@ ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
} }
}); });
ipcRenderer.on(SET_SERVER_NAME, (_, name) => { ipcRenderer.on(SET_VIEW_NAME, (_, name) => {
serverName = name; viewName = name;
}); });
function getUnreadCount() { function getUnreadCount() {
// LHS not found => Log out => Count should be 0, but session may be expired. // LHS not found => Log out => Count should be 0, but session may be expired.
if (typeof serverName !== 'undefined') { if (typeof viewName !== 'undefined') {
let isExpired; let isExpired;
if (document.getElementById('sidebar-left') === null) { if (document.getElementById('sidebar-left') === null) {
const extraParam = (new URLSearchParams(window.location.search)).get('extra'); const extraParam = (new URLSearchParams(window.location.search)).get('extra');
@@ -178,7 +178,7 @@ function getUnreadCount() {
} }
if (isExpired !== sessionExpired) { if (isExpired !== sessionExpired) {
sessionExpired = isExpired; sessionExpired = isExpired;
ipcRenderer.send(SESSION_EXPIRED, sessionExpired, serverName); ipcRenderer.send(SESSION_EXPIRED, sessionExpired, viewName);
} }
} }
} }

View File

@@ -18,11 +18,11 @@ import {
IS_UNREAD, IS_UNREAD,
UNREAD_RESULT, UNREAD_RESULT,
TOGGLE_BACK_BUTTON, TOGGLE_BACK_BUTTON,
SET_SERVER_NAME, SET_VIEW_NAME,
LOADSCREEN_END, LOADSCREEN_END,
} from 'common/communication'; } from 'common/communication';
import {MattermostServer} from 'main/MattermostServer'; import {TabView} from 'common/tabs/TabView';
import ContextMenu from '../contextMenu'; import ContextMenu from '../contextMenu';
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils'; import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
@@ -42,7 +42,7 @@ const ASTERISK_GROUP = 3;
const MENTIONS_GROUP = 2; const MENTIONS_GROUP = 2;
export class MattermostView extends EventEmitter { export class MattermostView extends EventEmitter {
server: MattermostServer; tab: TabView;
window: BrowserWindow; window: BrowserWindow;
view: BrowserView; view: BrowserView;
isVisible: boolean; isVisible: boolean;
@@ -67,9 +67,9 @@ export class MattermostView extends EventEmitter {
retryLoad?: NodeJS.Timeout; retryLoad?: NodeJS.Timeout;
maxRetries: number; maxRetries: number;
constructor(server: MattermostServer, win: BrowserWindow, options: BrowserViewConstructorOptions) { constructor(tab: TabView, win: BrowserWindow, options: BrowserViewConstructorOptions) {
super(); super();
this.server = server; this.tab = tab;
this.window = win; this.window = win;
const preload = getLocalPreload('preload.js'); const preload = getLocalPreload('preload.js');
@@ -90,7 +90,7 @@ export class MattermostView extends EventEmitter {
this.resetLoadingStatus(); this.resetLoadingStatus();
this.faviconMemoize = new Map(); this.faviconMemoize = new Map();
log.info(`BrowserView created for server ${this.server.name}`); log.info(`BrowserView created for server ${this.tab.name}`);
this.isInitialized = false; this.isInitialized = false;
this.hasBeenShown = false; this.hasBeenShown = false;
@@ -107,7 +107,7 @@ export class MattermostView extends EventEmitter {
// use the same name as the server // use the same name as the server
// TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships) // TODO: we'll need unique identifiers if we have multiple instances of the same server in different tabs (1:N relationships)
get name() { get name() {
return this.server?.name; return this.tab.name;
} }
resetLoadingStatus = () => { resetLoadingStatus = () => {
@@ -119,7 +119,7 @@ export class MattermostView extends EventEmitter {
} }
load = (someURL?: URL | string) => { load = (someURL?: URL | string) => {
if (!this.server) { if (!this.tab) {
return; return;
} }
@@ -130,12 +130,12 @@ export class MattermostView extends EventEmitter {
loadURL = parsedURL.toString(); loadURL = parsedURL.toString();
} else { } else {
log.error('Cannot parse provided url, using current server url', someURL); log.error('Cannot parse provided url, using current server url', someURL);
loadURL = this.server.url.toString(); loadURL = this.tab.url.toString();
} }
} else { } else {
loadURL = this.server.url.toString(); loadURL = this.tab.url.toString();
} }
log.info(`[${Util.shorten(this.server.name)}] Loading ${loadURL}`); log.info(`[${Util.shorten(this.tab.name)}] Loading ${loadURL}`);
const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()}); const loading = this.view.webContents.loadURL(loadURL, {userAgent: composeUserAgent()});
loading.then(this.loadSuccess(loadURL)).catch((err) => { loading.then(this.loadSuccess(loadURL)).catch((err) => {
this.loadRetry(loadURL, err); this.loadRetry(loadURL, err);
@@ -153,9 +153,9 @@ export class MattermostView extends EventEmitter {
if (this.maxRetries-- > 0) { if (this.maxRetries-- > 0) {
this.loadRetry(loadURL, err); this.loadRetry(loadURL, err);
} else { } else {
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString()); this.emit(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.server.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`); log.info(`[${Util.shorten(this.tab.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
this.status = Status.ERROR; this.status = Status.ERROR;
} }
}); });
@@ -164,14 +164,14 @@ export class MattermostView extends EventEmitter {
loadRetry = (loadURL: string, err: any) => { loadRetry = (loadURL: string, err: any) => {
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL); this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
WindowManager.sendToRenderer(LOAD_RETRY, this.server.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString()); WindowManager.sendToRenderer(LOAD_RETRY, this.tab.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
log.info(`[${Util.shorten(this.server.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`); log.info(`[${Util.shorten(this.tab.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
} }
loadSuccess = (loadURL: string) => { loadSuccess = (loadURL: string) => {
return () => { return () => {
log.info(`[${Util.shorten(this.server.name)}] finished loading ${loadURL}`); log.info(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name); WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
this.maxRetries = MAX_SERVER_RETRIES; this.maxRetries = MAX_SERVER_RETRIES;
if (this.status === Status.LOADING) { if (this.status === Status.LOADING) {
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread); ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
@@ -180,9 +180,9 @@ export class MattermostView extends EventEmitter {
} }
this.status = Status.WAITING_MM; this.status = Status.WAITING_MM;
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true); this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
this.emit(LOAD_SUCCESS, this.server.name, loadURL); this.emit(LOAD_SUCCESS, this.tab.name, loadURL);
this.view.webContents.send(SET_SERVER_NAME, this.server.name); this.view.webContents.send(SET_VIEW_NAME, this.tab.name);
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL())))); this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.tab.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.tab.url || '', this.view.webContents.getURL()))));
}; };
} }
@@ -191,7 +191,7 @@ export class MattermostView extends EventEmitter {
const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility; const request = typeof requestedVisibility === 'undefined' ? true : requestedVisibility;
if (request && !this.isVisible) { if (request && !this.isVisible) {
this.window.addBrowserView(this.view); this.window.addBrowserView(this.view);
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.server.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.server.url || '', this.view.webContents.getURL())))); this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.tab.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.tab.url || '', this.view.webContents.getURL()))));
if (this.status === Status.READY) { if (this.status === Status.READY) {
this.focus(); this.focus();
} }
@@ -254,8 +254,8 @@ export class MattermostView extends EventEmitter {
this.status = Status.READY; this.status = Status.READY;
if (timedout) { if (timedout) {
log.info(`${this.server.name} timeout expired will show the browserview`); log.info(`${this.tab.name} timeout expired will show the browserview`);
this.emit(LOADSCREEN_END, this.server.name); this.emit(LOADSCREEN_END, this.tab.name);
} }
clearTimeout(this.removeLoading); clearTimeout(this.removeLoading);
delete this.removeLoading; delete this.removeLoading;
@@ -291,7 +291,7 @@ export class MattermostView extends EventEmitter {
} }
handleDidNavigate = (event: Event, url: string) => { handleDidNavigate = (event: Event, url: string) => {
const isUrlTeamUrl = urlUtils.isTeamUrl(this.server.url || '', url) || urlUtils.isAdminUrl(this.server.url || '', url); const isUrlTeamUrl = urlUtils.isTeamUrl(this.tab.url || '', url) || urlUtils.isAdminUrl(this.tab.url || '', url);
if (isUrlTeamUrl) { if (isUrlTeamUrl) {
this.setBounds(getWindowBoundaries(this.window)); this.setBounds(getWindowBoundaries(this.window));
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false); WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
@@ -304,7 +304,7 @@ export class MattermostView extends EventEmitter {
} }
handleUpdateTarget = (e: Event, url: string) => { handleUpdateTarget = (e: Event, url: string) => {
if (!url || !this.server.sameOrigin(url)) { if (!url || !this.tab.server.sameOrigin(url)) {
this.emit(UPDATE_TARGET_URL, url); this.emit(UPDATE_TARGET_URL, url);
} }
} }
@@ -331,7 +331,7 @@ export class MattermostView extends EventEmitter {
} }
const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0; const mentions = (results && results.value && parseInt(results.value[MENTIONS_GROUP], 10)) || 0;
appState.updateMentions(this.server.name, mentions, unreads); appState.updateMentions(this.tab.name, mentions, unreads);
} }
handleFaviconUpdate = (e: Event, favicons: string[]) => { handleFaviconUpdate = (e: Event, favicons: string[]) => {
@@ -340,7 +340,7 @@ export class MattermostView extends EventEmitter {
// if not, get related info from preload and store it for future changes // if not, get related info from preload and store it for future changes
this.currentFavicon = favicons[0]; this.currentFavicon = favicons[0];
if (this.faviconMemoize.has(favicons[0])) { if (this.faviconMemoize.has(favicons[0])) {
appState.updateUnreads(this.server.name, Boolean(this.faviconMemoize.get(favicons[0]))); appState.updateUnreads(this.tab.name, Boolean(this.faviconMemoize.get(favicons[0])));
} else { } else {
this.findUnreadState(favicons[0]); this.findUnreadState(favicons[0]);
} }
@@ -350,7 +350,7 @@ export class MattermostView extends EventEmitter {
// if favicon is null, it will affect appState, but won't be memoized // if favicon is null, it will affect appState, but won't be memoized
findUnreadState = (favicon: string | null) => { findUnreadState = (favicon: string | null) => {
try { try {
this.view.webContents.send(IS_UNREAD, favicon, this.server.name); this.view.webContents.send(IS_UNREAD, favicon, this.tab.name);
} catch (err) { } catch (err) {
log.error(`There was an error trying to request the unread state: ${err}`); log.error(`There was an error trying to request the unread state: ${err}`);
log.error(err.stack); log.error(err.stack);
@@ -359,13 +359,13 @@ export class MattermostView extends EventEmitter {
// if favicon is null, it means it is the initial load, // if favicon is null, it means it is the initial load,
// so don't memoize as we don't have the favicons and there is no rush to find out. // so don't memoize as we don't have the favicons and there is no rush to find out.
handleFaviconIsUnread = (e: Event, favicon: string, serverName: string, result: boolean) => { handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => {
if (this.server && serverName === this.server.name) { if (this.tab && viewName === this.tab.name) {
if (favicon) { if (favicon) {
this.faviconMemoize.set(favicon, result); this.faviconMemoize.set(favicon, result);
} }
if (!favicon || favicon === this.currentFavicon) { if (!favicon || favicon === this.currentFavicon) {
appState.updateUnreads(serverName, result); appState.updateUnreads(viewName, result);
} }
} }
} }

View File

@@ -2,7 +2,7 @@
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron'; import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
import {CombinedConfig, Team} from 'types/config'; import {CombinedConfig, TeamWithTabs} from 'types/config';
import { import {
CLOSE_TEAMS_DROPDOWN, CLOSE_TEAMS_DROPDOWN,
@@ -12,7 +12,7 @@ import {
UPDATE_DROPDOWN_MENTIONS, UPDATE_DROPDOWN_MENTIONS,
REQUEST_TEAMS_DROPDOWN_INFO, REQUEST_TEAMS_DROPDOWN_INFO,
RECEIVE_DROPDOWN_MENU_SIZE, RECEIVE_DROPDOWN_MENU_SIZE,
SET_SERVER_KEY, SET_ACTIVE_VIEW,
} from 'common/communication'; } from 'common/communication';
import * as AppState from '../appState'; import * as AppState from '../appState';
import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants'; import {TAB_BAR_HEIGHT, THREE_DOT_MENU_WIDTH, THREE_DOT_MENU_WIDTH_MAC, MENU_SHADOW_WIDTH} from 'common/utils/constants';
@@ -22,19 +22,21 @@ import * as WindowManager from '../windows/windowManager';
export default class TeamDropdownView { export default class TeamDropdownView {
view: BrowserView; view: BrowserView;
bounds?: Electron.Rectangle; bounds?: Electron.Rectangle;
teams: Team[]; teams: TeamWithTabs[];
activeTeam?: string; activeTeam?: string;
darkMode: boolean; darkMode: boolean;
enableServerManagement?: boolean;
hasGPOTeams?: boolean; hasGPOTeams?: boolean;
unreads?: Map<string, boolean>; unreads?: Map<string, boolean>;
mentions?: Map<string, number>; mentions?: Map<string, number>;
expired?: Map<string, boolean>; expired?: Map<string, boolean>;
window: BrowserWindow; window: BrowserWindow;
constructor(window: BrowserWindow, teams: Team[], darkMode: boolean) { constructor(window: BrowserWindow, teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
this.teams = teams; this.teams = teams;
this.window = window; this.window = window;
this.darkMode = darkMode; this.darkMode = darkMode;
this.enableServerManagement = enableServerManagement;
const preload = getLocalPreload('dropdown.js'); const preload = getLocalPreload('dropdown.js');
this.view = new BrowserView({webPreferences: { this.view = new BrowserView({webPreferences: {
@@ -51,13 +53,14 @@ export default class TeamDropdownView {
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig); ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown); ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown);
ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize); ipcMain.on(RECEIVE_DROPDOWN_MENU_SIZE, this.handleReceivedMenuSize);
ipcMain.on(SET_SERVER_KEY, this.updateActiveTeam); ipcMain.on(SET_ACTIVE_VIEW, this.updateActiveTeam);
AppState.on(UPDATE_DROPDOWN_MENTIONS, this.updateMentions); AppState.on(UPDATE_DROPDOWN_MENTIONS, this.updateMentions);
} }
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => { updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
this.teams = config.teams; this.teams = config.teams;
this.darkMode = config.darkMode; this.darkMode = config.darkMode;
this.enableServerManagement = config.enableServerManagement;
this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0; this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0;
this.updateDropdown(); this.updateDropdown();
} }
@@ -75,7 +78,17 @@ export default class TeamDropdownView {
} }
updateDropdown = () => { updateDropdown = () => {
this.view.webContents.send(UPDATE_TEAMS_DROPDOWN, this.teams, this.activeTeam, this.darkMode, this.hasGPOTeams, this.expired, this.mentions, this.unreads); this.view.webContents.send(
UPDATE_TEAMS_DROPDOWN,
this.teams,
this.activeTeam,
this.darkMode,
this.enableServerManagement,
this.hasGPOTeams,
this.expired,
this.mentions,
this.unreads,
);
} }
handleOpen = () => { handleOpen = () => {

View File

@@ -4,21 +4,23 @@ import log from 'electron-log';
import {BrowserView, BrowserWindow, dialog, ipcMain} from 'electron'; import {BrowserView, BrowserWindow, dialog, ipcMain} from 'electron';
import {BrowserViewConstructorOptions} from 'electron/main'; import {BrowserViewConstructorOptions} from 'electron/main';
import {CombinedConfig, Team} from 'types/config'; import {CombinedConfig, Tab, TeamWithTabs} from 'types/config';
import {SECOND} from 'common/utils/constants'; import {SECOND} from 'common/utils/constants';
import { import {
UPDATE_TARGET_URL, UPDATE_TARGET_URL,
SET_SERVER_KEY,
LOAD_SUCCESS, LOAD_SUCCESS,
LOAD_FAILED, LOAD_FAILED,
TOGGLE_LOADING_SCREEN_VISIBILITY, TOGGLE_LOADING_SCREEN_VISIBILITY,
GET_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA,
LOADSCREEN_END, LOADSCREEN_END,
SET_ACTIVE_VIEW,
} from 'common/communication'; } from 'common/communication';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import {MattermostServer} from '../MattermostServer'; import {getServerView, getTabViewName} from 'common/tabs/TabView';
import {MattermostServer} from '../../common/servers/MattermostServer';
import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils'; import {getLocalURLString, getLocalPreload, getWindowBoundaries} from '../utils';
import {MattermostView} from './MattermostView'; import {MattermostView} from './MattermostView';
@@ -29,7 +31,7 @@ const URL_VIEW_DURATION = 10 * SECOND;
const URL_VIEW_HEIGHT = 36; const URL_VIEW_HEIGHT = 36;
export class ViewManager { export class ViewManager {
configServers: Team[]; configServers: TeamWithTabs[];
viewOptions: BrowserViewConstructorOptions; viewOptions: BrowserViewConstructorOptions;
views: Map<string, MattermostView>; views: Map<string, MattermostView>;
currentView?: string; currentView?: string;
@@ -53,10 +55,15 @@ export class ViewManager {
return this.configServers; return this.configServers;
} }
loadServer = (server: Team) => { loadServer = (server: TeamWithTabs) => {
const srv = new MattermostServer(server.name, server.url); const srv = new MattermostServer(server.name, server.url);
const view = new MattermostView(srv, this.mainWindow, this.viewOptions); server.tabs.forEach((tab) => this.loadView(srv, tab));
this.views.set(server.name, view); }
loadView = (srv: MattermostServer, tab: Tab) => {
const tabView = getServerView(srv, tab);
const view = new MattermostView(tabView, this.mainWindow, this.viewOptions);
this.views.set(tabView.name, view);
if (!this.loadingScreen) { if (!this.loadingScreen) {
this.createLoadingScreen(); this.createLoadingScreen();
} }
@@ -71,23 +78,27 @@ export class ViewManager {
this.configServers.forEach((server) => this.loadServer(server)); this.configServers.forEach((server) => this.loadServer(server));
} }
reloadConfiguration = (configServers: Team[]) => { reloadConfiguration = (configServers: TeamWithTabs[]) => {
this.configServers = configServers.concat(); this.configServers = configServers.concat();
const oldviews = this.views; const oldviews = this.views;
this.views = new Map(); this.views = new Map();
const sorted = this.configServers.sort((a, b) => a.order - b.order); const sorted = this.configServers.sort((a, b) => a.order - b.order);
let setFocus; let setFocus;
sorted.forEach((server) => { sorted.forEach((server) => {
const recycle = oldviews.get(server.name); const srv = new MattermostServer(server.name, server.url);
if (recycle && recycle.isVisible) { server.tabs.forEach((tab) => {
setFocus = recycle.name; const tabView = getServerView(srv, tab);
} const recycle = oldviews.get(tabView.name);
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) { if (recycle && recycle.isVisible) {
oldviews.delete(recycle.name); setFocus = recycle.name;
this.views.set(recycle.name, recycle); }
} else { if (recycle && recycle.tab.name === tabView.name && recycle.tab.url.toString() === urlUtils.parseURL(tabView.url)!.toString()) {
this.loadServer(server); oldviews.delete(recycle.name);
} this.views.set(recycle.name, recycle);
} else {
this.loadView(srv, tab);
}
});
}); });
oldviews.forEach((unused) => { oldviews.forEach((unused) => {
unused.destroy(); unused.destroy();
@@ -103,7 +114,11 @@ export class ViewManager {
if (this.configServers.length) { if (this.configServers.length) {
const element = this.configServers.find((e) => e.order === 0); const element = this.configServers.find((e) => e.order === 0);
if (element) { if (element) {
this.showByName(element.name); const tab = element.tabs.find((e) => e.order === 0);
if (tab) {
const tabView = getTabViewName(element.name, tab.name);
this.showByName(tabView);
}
} }
} }
} }
@@ -125,13 +140,8 @@ export class ViewManager {
if (newView.needsLoadingScreen()) { if (newView.needsLoadingScreen()) {
this.showLoadingScreen(); this.showLoadingScreen();
} }
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name); newView.window.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type);
if (!serverInfo) { ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type);
log.error(`Couldn't find a server in the config with the name ${newView.server.name}`);
return;
}
newView.window.webContents.send(SET_SERVER_KEY, serverInfo.order);
ipcMain.emit(SET_SERVER_KEY, true, name);
if (newView.isReady()) { if (newView.isReady()) {
// if view is not ready, the renderer will have something to display instead. // if view is not ready, the renderer will have something to display instead.
newView.show(); newView.show();
@@ -326,18 +336,18 @@ export class ViewManager {
} }
} }
deeplinkSuccess = (serverName: string) => { deeplinkSuccess = (viewName: string) => {
const view = this.views.get(serverName); const view = this.views.get(viewName);
if (!view) { if (!view) {
return; return;
} }
this.showByName(serverName); this.showByName(viewName);
view.removeListener(LOAD_FAILED, this.deeplinkFailed); view.removeListener(LOAD_FAILED, this.deeplinkFailed);
}; };
deeplinkFailed = (serverName: string, err: string, url: string) => { deeplinkFailed = (viewName: string, err: string, url: string) => {
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`); log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`);
const view = this.views.get(serverName); const view = this.views.get(viewName);
if (!view) { if (!view) {
return; return;
} }
@@ -345,18 +355,19 @@ export class ViewManager {
} }
handleDeepLink = (url: string | URL) => { handleDeepLink = (url: string | URL) => {
// TODO: fix for new tabs
if (url) { if (url) {
const parsedURL = urlUtils.parseURL(url)!; const parsedURL = urlUtils.parseURL(url)!;
const server = urlUtils.getServer(parsedURL, this.configServers, true); const tabView = urlUtils.getView(parsedURL, this.configServers, true);
if (server) { if (tabView) {
const view = this.views.get(server.name); const view = this.views.get(tabView.name);
if (!view) { if (!view) {
log.error(`Couldn't find a view matching the name ${server.name}`); log.error(`Couldn't find a view matching the name ${tabView.name}`);
return; return;
} }
// attempting to change parsedURL protocol results in it not being modified. // attempting to change parsedURL protocol results in it not being modified.
const urlWithSchema = `${view.server.url.origin}${parsedURL.pathname}${parsedURL.search}`; const urlWithSchema = `${view.tab.url.origin}${parsedURL.pathname}${parsedURL.search}`;
view.resetLoadingStatus(); view.resetLoadingStatus();
view.load(urlWithSchema); view.load(urlWithSchema);
view.once(LOAD_SUCCESS, this.deeplinkSuccess); view.once(LOAD_SUCCESS, this.deeplinkSuccess);

View File

@@ -4,7 +4,7 @@
import {BrowserWindow, shell, WebContents} from 'electron'; import {BrowserWindow, shell, WebContents} from 'electron';
import log from 'electron-log'; import log from 'electron-log';
import {Team} from 'types/config'; import {TeamWithTabs} from 'types/config';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
@@ -37,12 +37,12 @@ function isTrustedPopupWindow(webContents: WebContents) {
const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0]; const scheme = protocols && protocols[0] && protocols[0].schemes && protocols[0].schemes[0];
const generateWillNavigate = (getServersFunction: () => Team[]) => { const generateWillNavigate = (getServersFunction: () => TeamWithTabs[]) => {
return (event: Event & {sender: WebContents}, url: string) => { return (event: Event & {sender: WebContents}, url: string) => {
const contentID = event.sender.id; const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url)!; const parsedURL = urlUtils.parseURL(url)!;
const configServers = getServersFunction(); const configServers = getServersFunction();
const server = urlUtils.getServer(parsedURL, configServers); const server = urlUtils.getView(parsedURL, configServers);
if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || isTrustedPopupWindow(event.sender))) { if (server && (urlUtils.isTeamUrl(server.url, parsedURL) || urlUtils.isAdminUrl(server.url, parsedURL) || isTrustedPopupWindow(event.sender))) {
return; return;
@@ -63,12 +63,12 @@ const generateWillNavigate = (getServersFunction: () => Team[]) => {
}; };
}; };
const generateDidStartNavigation = (getServersFunction: () => Team[]) => { const generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) => {
return (event: Event & {sender: WebContents}, url: string) => { return (event: Event & {sender: WebContents}, url: string) => {
const serverList = getServersFunction(); const serverList = getServersFunction();
const contentID = event.sender.id; const contentID = event.sender.id;
const parsedURL = urlUtils.parseURL(url)!; const parsedURL = urlUtils.parseURL(url)!;
const server = urlUtils.getServer(parsedURL, serverList); const server = urlUtils.getView(parsedURL, serverList);
if (!urlUtils.isTrustedURL(parsedURL, serverList)) { if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
return; return;
@@ -82,7 +82,7 @@ const generateDidStartNavigation = (getServersFunction: () => Team[]) => {
}; };
}; };
const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?: boolean) => { const generateNewWindowListener = (getServersFunction: () => TeamWithTabs[], spellcheck?: boolean) => {
return (event: Event, url: string) => { return (event: Event, url: string) => {
const parsedURL = urlUtils.parseURL(url); const parsedURL = urlUtils.parseURL(url);
if (!parsedURL) { if (!parsedURL) {
@@ -110,7 +110,7 @@ const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?
return; return;
} }
const server = urlUtils.getServer(parsedURL, configServers); const server = urlUtils.getView(parsedURL, configServers);
if (!server) { if (!server) {
shell.openExternal(url); shell.openExternal(url);
@@ -193,7 +193,7 @@ export const removeWebContentsListeners = (id: number) => {
} }
}; };
export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => Team[]) => { export const addWebContentsEventListeners = (mmview: MattermostView, getServersFunction: () => TeamWithTabs[]) => {
const contents = mmview.view.webContents; const contents = mmview.view.webContents;
// initialize custom login tracking // initialize custom login tracking

View File

@@ -10,6 +10,8 @@ import {CombinedConfig} from 'types/config';
import {MAXIMIZE_CHANGE, HISTORY, GET_LOADING_SCREEN_DATA, REACT_APP_INITIALIZED, LOADING_SCREEN_ANIMATION_FINISHED, FOCUS_THREE_DOT_MENU, GET_DARK_MODE} from 'common/communication'; import {MAXIMIZE_CHANGE, HISTORY, GET_LOADING_SCREEN_DATA, REACT_APP_INITIALIZED, LOADING_SCREEN_ANIMATION_FINISHED, FOCUS_THREE_DOT_MENU, GET_DARK_MODE} from 'common/communication';
import urlUtils from 'common/utils/url'; import urlUtils from 'common/utils/url';
import {getTabViewName} from 'common/tabs/TabView';
import {getAdjustedWindowBoundaries} from '../utils'; import {getAdjustedWindowBoundaries} from '../utils';
import {ViewManager} from '../views/viewManager'; import {ViewManager} from '../views/viewManager';
@@ -113,7 +115,7 @@ export function showMainWindow(deeplinkingURL?: string | URL) {
status.viewManager.updateMainWindow(status.mainWindow); status.viewManager.updateMainWindow(status.mainWindow);
} }
status.teamDropdown = new TeamDropdownView(status.mainWindow, status.config.teams, status.config.darkMode); status.teamDropdown = new TeamDropdownView(status.mainWindow, status.config.teams, status.config.darkMode, status.config.enableServerManagement);
} }
initializeViewManager(); initializeViewManager();
@@ -158,7 +160,7 @@ function handleResizeMainWindow() {
const setBoundsFunction = () => { const setBoundsFunction = () => {
if (currentView) { if (currentView) {
currentView.setBounds(getAdjustedWindowBoundaries(bounds.width!, bounds.height!, !urlUtils.isTeamUrl(currentView.server.url, currentView.view.webContents.getURL()))); currentView.setBounds(getAdjustedWindowBoundaries(bounds.width!, bounds.height!, !urlUtils.isTeamUrl(currentView.tab.url, currentView.view.webContents.getURL())));
} }
}; };
@@ -339,7 +341,20 @@ function initializeViewManager() {
export function switchServer(serverName: string) { export function switchServer(serverName: string) {
showMainWindow(); showMainWindow();
status.viewManager?.showByName(serverName); const server = status.config?.teams.find((team) => team.name === serverName);
if (!server) {
log.error('Cannot find server in config');
return;
}
const lastActiveTab = server.tabs[server.lastActiveTab || 0];
const tabViewName = getTabViewName(serverName, lastActiveTab.name);
status.viewManager?.showByName(tabViewName);
}
export function switchTab(serverName: string, tabName: string) {
showMainWindow();
const tabViewName = getTabViewName(serverName, tabName);
status.viewManager?.showByName(tabViewName);
} }
export function focusBrowserView() { export function focusBrowserView() {
@@ -369,9 +384,9 @@ function handleLoadingScreenDataRequest() {
}; };
} }
function handleReactAppInitialized(e: IpcMainEvent, server: string) { function handleReactAppInitialized(e: IpcMainEvent, view: string) {
if (status.viewManager) { if (status.viewManager) {
status.viewManager.setServerInitialized(server); status.viewManager.setServerInitialized(view);
} }
} }
@@ -437,12 +452,52 @@ export function handleHistory(event: IpcMainEvent, offset: number) {
activeView.view.webContents.goToOffset(offset); activeView.view.webContents.goToOffset(offset);
} catch (error) { } catch (error) {
log.error(error); log.error(error);
activeView.load(activeView.server.url); activeView.load(activeView.tab.url);
} }
} }
} }
} }
export function selectNextTab() {
const currentView = status.viewManager?.getCurrentView();
if (!currentView) {
return;
}
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
if (!currentTeamTabs || !currentTab) {
return;
}
const currentOrder = currentTab.order;
const nextOrder = ((currentOrder + 1) % currentTeamTabs.length);
const nextIndex = currentTeamTabs.findIndex((tab) => tab.order === nextOrder);
const newTab = currentTeamTabs[nextIndex];
switchTab(currentView.tab.server.name, newTab.name);
}
export function selectPreviousTab() {
const currentView = status.viewManager?.getCurrentView();
if (!currentView) {
return;
}
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
if (!currentTeamTabs || !currentTab) {
return;
}
const currentOrder = currentTab.order;
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
const nextOrder = ((currentTeamTabs.length + (currentOrder - 1)) % currentTeamTabs.length);
const nextIndex = currentTeamTabs.findIndex((tab) => tab.order === nextOrder);
const newTab = currentTeamTabs[nextIndex];
switchTab(currentView.tab.server.name, newTab.name);
}
function handleGetDarkMode() { function handleGetDarkMode() {
return status.config?.darkMode; return status.config?.darkMode;
} }

View File

@@ -8,7 +8,9 @@ import {DropResult} from 'react-beautiful-dnd';
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon'; import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
import {IpcRendererEvent} from 'electron/renderer'; import {IpcRendererEvent} from 'electron/renderer';
import {Team} from 'types/config'; import {TeamWithTabs} from 'types/config';
import {getTabViewName} from 'common/tabs/TabView';
import { import {
FOCUS_BROWSERVIEW, FOCUS_BROWSERVIEW,
@@ -18,8 +20,6 @@ import {
LOAD_RETRY, LOAD_RETRY,
LOAD_SUCCESS, LOAD_SUCCESS,
LOAD_FAILED, LOAD_FAILED,
SHOW_NEW_SERVER_MODAL,
SWITCH_SERVER,
WINDOW_CLOSE, WINDOW_CLOSE,
WINDOW_MINIMIZE, WINDOW_MINIMIZE,
WINDOW_RESTORE, WINDOW_RESTORE,
@@ -28,16 +28,14 @@ import {
PLAY_SOUND, PLAY_SOUND,
MODAL_OPEN, MODAL_OPEN,
MODAL_CLOSE, MODAL_CLOSE,
SET_SERVER_KEY, SET_ACTIVE_VIEW,
UPDATE_MENTIONS, UPDATE_MENTIONS,
TOGGLE_BACK_BUTTON, TOGGLE_BACK_BUTTON,
SELECT_NEXT_TAB,
SELECT_PREVIOUS_TAB,
ADD_SERVER,
FOCUS_THREE_DOT_MENU, FOCUS_THREE_DOT_MENU,
GET_FULL_SCREEN_STATUS, GET_FULL_SCREEN_STATUS,
CLOSE_TEAMS_DROPDOWN, CLOSE_TEAMS_DROPDOWN,
OPEN_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN,
SWITCH_TAB,
} from 'common/communication'; } from 'common/communication';
import restoreButton from '../../assets/titlebar/chrome-restore.svg'; import restoreButton from '../../assets/titlebar/chrome-restore.svg';
@@ -61,22 +59,21 @@ enum Status {
} }
type Props = { type Props = {
teams: Team[]; teams: TeamWithTabs[];
showAddServerButton: boolean; moveTabs: (teamName: string, originalOrder: number, newOrder: number) => number | undefined;
moveTabs: (originalOrder: number, newOrder: number) => number | undefined;
openMenu: () => void; openMenu: () => void;
darkMode: boolean; darkMode: boolean;
appName: string; appName: string;
}; };
type State = { type State = {
key: number; activeServerName?: string;
activeTabName?: string;
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, number>; unreadCounts: Record<string, number>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
targetURL: string;
maximized: boolean; maximized: boolean;
tabStatus: Map<string, TabStatus>; tabViewStatus: Map<string, TabViewStatus>;
darkMode: boolean; darkMode: boolean;
modalOpen?: boolean; modalOpen?: boolean;
fullScreen?: boolean; fullScreen?: boolean;
@@ -84,7 +81,7 @@ type State = {
isMenuOpen: boolean; isMenuOpen: boolean;
}; };
type TabStatus = { type TabViewStatus = {
status: Status; status: Status;
extra?: { extra?: {
url: string; url: string;
@@ -102,40 +99,39 @@ export default class MainPage extends React.PureComponent<Props, State> {
this.topBar = React.createRef(); this.topBar = React.createRef();
this.threeDotMenu = React.createRef(); this.threeDotMenu = React.createRef();
const firstServer = this.props.teams.find((team) => team.order === 0);
const firstTab = firstServer?.tabs.find((tab) => tab.order === (firstServer.lastActiveTab || 0)) || firstServer?.tabs[0];
this.state = { this.state = {
key: this.props.teams.length ? this.props.teams.findIndex((team) => team.order === 0) : 0, activeServerName: firstServer?.name,
activeTabName: firstTab?.name,
sessionsExpired: {}, sessionsExpired: {},
unreadCounts: {}, unreadCounts: {},
mentionCounts: {}, mentionCounts: {},
targetURL: '',
maximized: false, maximized: false,
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: Status.LOADING}])), tabViewStatus: new Map(this.props.teams.map((team) => team.tabs.map((tab) => getTabViewName(team.name, tab.name))).flat().map((tabViewName) => [tabViewName, {status: Status.LOADING}])),
darkMode: this.props.darkMode, darkMode: this.props.darkMode,
isMenuOpen: false, isMenuOpen: false,
}; };
} }
getTabStatus() { getTabViewStatus() {
if (this.props.teams.length) { if (!this.state.activeServerName || !this.state.activeTabName) {
const tab = this.props.teams[this.state.key]; return undefined;
if (tab) {
const tabname = tab.name;
return this.state.tabStatus.get(tabname);
}
} }
return {status: Status.NOSERVERS}; return this.state.tabViewStatus.get(getTabViewName(this.state.activeServerName, this.state.activeTabName)) ?? {status: Status.NOSERVERS};
} }
updateTabStatus(server: string, newStatusValue: TabStatus) { updateTabStatus(tabViewName: string, newStatusValue: TabViewStatus) {
const status = new Map(this.state.tabStatus); const status = new Map(this.state.tabViewStatus);
status.set(server, newStatusValue); status.set(tabViewName, newStatusValue);
this.setState({tabStatus: status}); this.setState({tabViewStatus: status});
} }
componentDidMount() { componentDidMount() {
// set page on retry // set page on retry
window.ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => { window.ipcRenderer.on(LOAD_RETRY, (_, viewName, retry, err, loadUrl) => {
console.log(`${server}: failed to load ${err}, but retrying`); console.log(`${viewName}: failed to load ${err}, but retrying`);
const statusValue = { const statusValue = {
status: Status.RETRY, status: Status.RETRY,
extra: { extra: {
@@ -144,15 +140,15 @@ export default class MainPage extends React.PureComponent<Props, State> {
url: loadUrl, url: loadUrl,
}, },
}; };
this.updateTabStatus(server, statusValue); this.updateTabStatus(viewName, statusValue);
}); });
window.ipcRenderer.on(LOAD_SUCCESS, (_, server) => { window.ipcRenderer.on(LOAD_SUCCESS, (_, viewName) => {
this.updateTabStatus(server, {status: Status.DONE}); this.updateTabStatus(viewName, {status: Status.DONE});
}); });
window.ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => { window.ipcRenderer.on(LOAD_FAILED, (_, viewName, err, loadUrl) => {
console.log(`${server}: failed to load ${err}`); console.log(`${viewName}: failed to load ${err}`);
const statusValue = { const statusValue = {
status: Status.FAILED, status: Status.FAILED,
extra: { extra: {
@@ -160,7 +156,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
url: loadUrl, url: loadUrl,
}, },
}; };
this.updateTabStatus(server, statusValue); this.updateTabStatus(viewName, statusValue);
}); });
window.ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => { window.ipcRenderer.on(DARK_MODE_CHANGE, (_, darkMode) => {
@@ -168,26 +164,8 @@ export default class MainPage extends React.PureComponent<Props, State> {
}); });
// can't switch tabs sequentially for some reason... // can't switch tabs sequentially for some reason...
window.ipcRenderer.on(SET_SERVER_KEY, (event, key) => { window.ipcRenderer.on(SET_ACTIVE_VIEW, (event, serverName, tabName) => {
const nextIndex = this.props.teams.findIndex((team) => team.order === key); this.setState({activeServerName: serverName, activeTabName: tabName});
this.handleSetServerKey(nextIndex);
});
window.ipcRenderer.on(SELECT_NEXT_TAB, () => {
const currentOrder = this.props.teams[this.state.key].order;
const nextOrder = ((currentOrder + 1) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
const team = this.props.teams[nextIndex];
this.handleSelect(team.name, nextIndex);
});
window.ipcRenderer.on(SELECT_PREVIOUS_TAB, () => {
const currentOrder = this.props.teams[this.state.key].order;
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
const nextOrder = ((this.props.teams.length + (currentOrder - 1)) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
const team = this.props.teams[nextIndex];
this.handleSelect(team.name, nextIndex);
}); });
window.ipcRenderer.on(MAXIMIZE_CHANGE, this.handleMaximizeState); window.ipcRenderer.on(MAXIMIZE_CHANGE, this.handleMaximizeState);
@@ -197,10 +175,6 @@ export default class MainPage extends React.PureComponent<Props, State> {
window.ipcRenderer.invoke(GET_FULL_SCREEN_STATUS).then((fullScreenStatus) => this.handleFullScreenState(fullScreenStatus)); window.ipcRenderer.invoke(GET_FULL_SCREEN_STATUS).then((fullScreenStatus) => this.handleFullScreenState(fullScreenStatus));
window.ipcRenderer.on(ADD_SERVER, () => {
this.addServer();
});
window.ipcRenderer.on(PLAY_SOUND, (_event, soundName) => { window.ipcRenderer.on(PLAY_SOUND, (_event, soundName) => {
playSound(soundName); playSound(soundName);
}); });
@@ -217,18 +191,17 @@ export default class MainPage extends React.PureComponent<Props, State> {
this.setState({showExtraBar}); this.setState({showExtraBar});
}); });
window.ipcRenderer.on(UPDATE_MENTIONS, (_event, team, mentions, unreads, isExpired) => { window.ipcRenderer.on(UPDATE_MENTIONS, (_event, view, mentions, unreads, isExpired) => {
const key = this.props.teams.findIndex((server) => server.name === team);
const {unreadCounts, mentionCounts, sessionsExpired} = this.state; const {unreadCounts, mentionCounts, sessionsExpired} = this.state;
const newMentionCounts = {...mentionCounts}; const newMentionCounts = {...mentionCounts};
newMentionCounts[key] = mentions || 0; newMentionCounts[view] = mentions || 0;
const newUnreads = {...unreadCounts}; const newUnreads = {...unreadCounts};
newUnreads[key] = unreads || false; newUnreads[view] = unreads || false;
const expired = {...sessionsExpired}; const expired = {...sessionsExpired};
expired[key] = isExpired || false; expired[view] = isExpired || false;
this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired}); this.setState({unreadCounts: newUnreads, mentionCounts: newMentionCounts, sessionsExpired: expired});
}); });
@@ -258,14 +231,8 @@ export default class MainPage extends React.PureComponent<Props, State> {
this.setState({fullScreen: isFullScreen}); this.setState({fullScreen: isFullScreen});
} }
handleSetServerKey = (key: number) => { handleSelectTab = (name: string) => {
const newKey = ((this.props.teams.length + key) % this.props.teams.length) || 0; window.ipcRenderer.send(SWITCH_TAB, this.state.activeServerName, name);
this.setState({key: newKey});
}
handleSelect = (name: string, key: number) => {
window.ipcRenderer.send(SWITCH_SERVER, name);
this.handleSetServerKey(key);
} }
handleDragAndDrop = async (dropResult: DropResult) => { handleDragAndDrop = async (dropResult: DropResult) => {
@@ -274,12 +241,20 @@ export default class MainPage extends React.PureComponent<Props, State> {
if (addedIndex === undefined || removedIndex === addedIndex) { if (addedIndex === undefined || removedIndex === addedIndex) {
return; return;
} }
const teamIndex = this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1); if (!this.state.activeServerName) {
return;
}
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs;
if (!currentTabs) {
// TODO: figure out something here
return;
}
const teamIndex = this.props.moveTabs(this.state.activeServerName, removedIndex, addedIndex < currentTabs.length ? addedIndex : currentTabs.length - 1);
if (!teamIndex) { if (!teamIndex) {
return; return;
} }
const name = this.props.teams[teamIndex].name; const name = currentTabs[teamIndex].name;
this.handleSelect(name, teamIndex); this.handleSelectTab(name);
} }
handleClose = (e: React.MouseEvent<HTMLDivElement>) => { handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
@@ -312,17 +287,18 @@ export default class MainPage extends React.PureComponent<Props, State> {
window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW); window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW);
} }
addServer = () => {
window.ipcRenderer.send(SHOW_NEW_SERVER_MODAL);
}
focusOnWebView = () => { focusOnWebView = () => {
window.ipcRenderer.send(FOCUS_BROWSERVIEW); window.ipcRenderer.send(FOCUS_BROWSERVIEW);
window.ipcRenderer.send(CLOSE_TEAMS_DROPDOWN); window.ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
} }
render() { render() {
if (!this.props.teams.length) { if (!this.state.activeServerName || !this.state.activeTabName) {
return null;
}
const currentTabs = this.props.teams.find((team) => team.name === this.state.activeServerName)?.tabs;
if (!currentTabs) {
// TODO: figure out something here
return null; return null;
} }
@@ -330,14 +306,13 @@ export default class MainPage extends React.PureComponent<Props, State> {
<TabBar <TabBar
id='tabBar' id='tabBar'
isDarkMode={this.state.darkMode} isDarkMode={this.state.darkMode}
teams={this.props.teams} tabs={currentTabs}
sessionsExpired={this.state.sessionsExpired} sessionsExpired={this.state.sessionsExpired}
unreadCounts={this.state.unreadCounts} unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts} mentionCounts={this.state.mentionCounts}
activeKey={this.state.key} activeServerName={this.state.activeServerName}
onSelect={this.handleSelect} activeTabName={this.state.activeTabName}
onAddServer={this.addServer} onSelect={this.handleSelectTab}
showAddServerButton={this.props.showAddServerButton}
onDrop={this.handleDragAndDrop} onDrop={this.handleDragAndDrop}
tabsDisabled={this.state.modalOpen} tabsDisabled={this.state.modalOpen}
/> />
@@ -424,7 +399,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
<DotsVerticalIcon/> <DotsVerticalIcon/>
</button> </button>
<TeamDropdownButton <TeamDropdownButton
activeServerName={this.props.teams[this.state.key].name} activeServerName={this.state.activeServerName}
totalMentionCount={totalMentionCount} totalMentionCount={totalMentionCount}
hasUnreads={totalUnreadCount > 0} hasUnreads={totalUnreadCount > 0}
isMenuOpen={this.state.isMenuOpen} isMenuOpen={this.state.isMenuOpen}
@@ -439,11 +414,10 @@ export default class MainPage extends React.PureComponent<Props, State> {
const views = () => { const views = () => {
let component; let component;
const tabStatus = this.getTabStatus(); const tabStatus = this.getTabViewStatus();
if (!tabStatus) { if (!tabStatus) {
const tab = this.props.teams[this.state.key]; if (this.state.activeTabName) {
if (tab) { console.error(`Not tabStatus for ${this.state.activeTabName}`);
console.error(`Not tabStatus for ${this.props.teams[this.state.key].name}`);
} else { } else {
console.error('No tab status, tab doesn\'t exist anymore'); console.error('No tab status, tab doesn\'t exist anymore');
} }
@@ -463,7 +437,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
case Status.FAILED: case Status.FAILED:
component = ( component = (
<ErrorView <ErrorView
id={this.state.key + '-fail'} id={this.state.activeTabName + '-fail'}
errorInfo={tabStatus.extra?.error} errorInfo={tabStatus.extra?.error}
url={tabStatus.extra ? tabStatus.extra.url : ''} url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true} active={true}

View File

@@ -15,6 +15,7 @@ import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
import {DeepPartial} from 'types/utils'; import {DeepPartial} from 'types/utils';
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication'; import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
import TeamList from './TeamList'; import TeamList from './TeamList';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator'; import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
@@ -337,7 +338,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
addServer = (team: Team) => { addServer = (team: Team) => {
const teams = this.state.teams || []; const teams = this.state.teams || [];
teams.push(team); teams.push(getDefaultTeamWithTabsFromTeam(team));
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams}); window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({ this.setState({
teams, teams,

View File

@@ -5,32 +5,26 @@
import React from 'react'; import React from 'react';
import {Nav, NavItem, NavLink} from 'react-bootstrap'; import {Nav, NavItem, NavLink} from 'react-bootstrap';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd'; import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import PlusIcon from 'mdi-react/PlusIcon';
import classNames from 'classnames'; import classNames from 'classnames';
import {Team} from 'types/config'; import {Tab} from 'types/config';
import {GET_CONFIGURATION} from 'common/communication'; import {getTabViewName} from 'common/tabs/TabView';
type Props = { type Props = {
activeKey: number; activeTabName: string;
activeServerName: string;
id: string; id: string;
isDarkMode: boolean; isDarkMode: boolean;
onSelect: (name: string, index: number) => void; onSelect: (name: string, index: number) => void;
teams: Team[]; tabs: Tab[];
sessionsExpired: Record<string, boolean>; sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, number>; unreadCounts: Record<string, number>;
mentionCounts: Record<string, number>; mentionCounts: Record<string, number>;
showAddServerButton: boolean;
onAddServer: () => void;
onDrop: (result: DropResult) => void; onDrop: (result: DropResult) => void;
tabsDisabled?: boolean; tabsDisabled?: boolean;
}; };
type State = {
hasGPOTeams: boolean;
};
function getStyle(style?: DraggingStyle | NotDraggingStyle) { function getStyle(style?: DraggingStyle | NotDraggingStyle) {
if (style?.transform) { if (style?.transform) {
const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`; const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`;
@@ -42,31 +36,19 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
return style; return style;
} }
export default class TabBar extends React.PureComponent<Props, State> { // need "this" export default class TabBar extends React.PureComponent<Props> {
constructor(props: Props) {
super(props);
this.state = {
hasGPOTeams: false,
};
}
componentDidMount() {
window.ipcRenderer.invoke(GET_CONFIGURATION).then((config) => {
this.setState({hasGPOTeams: config.registryTeams && config.registryTeams.length > 0});
});
}
render() { render() {
const orderedTabs = this.props.teams.concat().sort((a, b) => a.order - b.order); const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order);
const tabs = orderedTabs.map((team, orderedIndex) => { const tabs = orderedTabs.map((tab, orderedIndex) => {
const index = this.props.teams.indexOf(team); const index = this.props.tabs.indexOf(tab);
const tabName = getTabViewName(this.props.activeServerName, tab.name);
const sessionExpired = this.props.sessionsExpired[index]; const sessionExpired = this.props.sessionsExpired[tabName];
const hasUnreads = this.props.unreadCounts[index]; const hasUnreads = this.props.unreadCounts[tabName];
let mentionCount = 0; let mentionCount = 0;
if (this.props.mentionCounts[index] > 0) { if (this.props.mentionCounts[tabName] > 0) {
mentionCount = this.props.mentionCounts[index]; mentionCount = this.props.mentionCounts[tabName];
} }
let badgeDiv: React.ReactNode; let badgeDiv: React.ReactNode;
@@ -98,9 +80,9 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
as='li' as='li'
id={`teamTabItem${index}`} id={`teamTabItem${index}`}
draggable={false} draggable={false}
title={team.name} title={tab.name}
className={classNames('teamTabItem', { className={classNames('teamTabItem', {
active: this.props.activeKey === index, active: this.props.activeTabName === tab.name,
dragging: snapshot.isDragging, dragging: snapshot.isDragging,
})} })}
{...provided.draggableProps} {...provided.draggableProps}
@@ -110,15 +92,15 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
<NavLink <NavLink
eventKey={index} eventKey={index}
draggable={false} draggable={false}
active={this.props.activeKey === index} active={this.props.activeTabName === tab.name}
disabled={this.props.tabsDisabled} disabled={this.props.tabsDisabled}
onSelect={() => { onSelect={() => {
this.props.onSelect(team.name, index); this.props.onSelect(tab.name, index);
}} }}
> >
<div className='TabBar-tabSeperator'> <div className='TabBar-tabSeperator'>
<span> <span>
{team.name} {tab.name}
</span> </span>
{ badgeDiv } { badgeDiv }
</div> </div>
@@ -128,49 +110,11 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
</Draggable> </Draggable>
); );
}); });
if (this.props.showAddServerButton === true) {
tabs.push(
<Draggable
draggableId={'TabBar-addServerButton'}
index={this.props.teams.length}
isDragDisabled={true}
>
{(provided) => (
<NavItem
ref={provided.innerRef}
as='li'
className='TabBar-addServerButton'
key='addServerButton'
id='addServerButton'
draggable={false}
title='Add new server'
{...provided.draggableProps}
{...provided.dragHandleProps}
>
<NavLink
eventKey='addServerButton'
draggable={false}
disabled={this.props.tabsDisabled}
onSelect={() => {
this.props.onAddServer();
}}
>
<div className='TabBar-tabSeperator'>
<PlusIcon size={20}/>
</div>
</NavLink>
</NavItem>
)}
</Draggable>,
);
}
// TODO: Replace with products
tabs.length = 0;
return ( return (
<DragDropContext onDragEnd={this.props.onDrop}> <DragDropContext onDragEnd={this.props.onDrop}>
<Droppable <Droppable
isDropDisabled={this.state.hasGPOTeams || this.props.tabsDisabled} isDropDisabled={this.props.tabsDisabled}
droppableId='tabBar' droppableId='tabBar'
direction='horizontal' direction='horizontal'
> >

View File

@@ -6,18 +6,21 @@ import ReactDOM from 'react-dom';
import classNames from 'classnames'; import classNames from 'classnames';
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd'; import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
import {Team} from 'types/config'; import {Team, TeamWithTabs} from 'types/config';
import {CLOSE_TEAMS_DROPDOWN, REQUEST_TEAMS_DROPDOWN_INFO, SEND_DROPDOWN_MENU_SIZE, SHOW_NEW_SERVER_MODAL, SWITCH_SERVER, UPDATE_TEAMS, UPDATE_TEAMS_DROPDOWN} from 'common/communication'; import {CLOSE_TEAMS_DROPDOWN, REQUEST_TEAMS_DROPDOWN_INFO, SEND_DROPDOWN_MENU_SIZE, SHOW_NEW_SERVER_MODAL, SWITCH_SERVER, UPDATE_TEAMS, UPDATE_TEAMS_DROPDOWN} from 'common/communication';
import {getTabViewName} from 'common/tabs/TabView';
import './css/dropdown.scss'; import './css/dropdown.scss';
import './css/compass-icons.css'; import './css/compass-icons.css';
type State = { type State = {
teams?: Team[]; teams?: TeamWithTabs[];
orderedTeams?: Team[]; orderedTeams?: TeamWithTabs[];
activeTeam?: string; activeTeam?: string;
darkMode?: boolean; darkMode?: boolean;
enableServerManagement?: boolean;
unreads?: Map<string, boolean>; unreads?: Map<string, boolean>;
mentions?: Map<string, number>; mentions?: Map<string, number>;
expired?: Map<string, boolean>; expired?: Map<string, boolean>;
@@ -47,12 +50,13 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
handleMessageEvent = (event: MessageEvent) => { handleMessageEvent = (event: MessageEvent) => {
if (event.data.type === UPDATE_TEAMS_DROPDOWN) { if (event.data.type === UPDATE_TEAMS_DROPDOWN) {
const {teams, activeTeam, darkMode, hasGPOTeams, unreads, mentions, expired} = event.data.data; const {teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, unreads, mentions, expired} = event.data.data;
this.setState({ this.setState({
teams, teams,
orderedTeams: teams.concat().sort((a: Team, b: Team) => a.order - b.order), orderedTeams: teams.concat().sort((a: TeamWithTabs, b: TeamWithTabs) => a.order - b.order),
activeTeam, activeTeam,
darkMode, darkMode,
enableServerManagement,
hasGPOTeams, hasGPOTeams,
unreads, unreads,
mentions, mentions,
@@ -168,10 +172,13 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
> >
{this.state.orderedTeams?.map((team, orderedIndex) => { {this.state.orderedTeams?.map((team, orderedIndex) => {
const index = this.state.teams?.indexOf(team); const index = this.state.teams?.indexOf(team);
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => {
const sessionExpired = this.state.expired?.get(team.name); const tabName = getTabViewName(team.name, tab.name);
const hasUnreads = this.state.unreads?.get(team.name); counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired;
const mentionCount = this.state.mentions?.get(team.name); counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
return counts;
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
let badgeDiv: React.ReactNode; let badgeDiv: React.ReactNode;
if (sessionExpired) { if (sessionExpired) {
@@ -250,13 +257,15 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
<hr className='TeamDropdown__divider'/> <hr className='TeamDropdown__divider'/>
<button {this.state.enableServerManagement &&
className='TeamDropdown__button addServer' <button
onClick={this.addServer} className='TeamDropdown__button addServer'
> onClick={this.addServer}
<i className='icon-plus'/> >
<span>{'Add a server'}</span> <i className='icon-plus'/>
</button> <span>{'Add a server'}</span>
</button>
}
</div> </div>
); );
} }

View File

@@ -46,12 +46,15 @@ class Root extends React.PureComponent<Record<string, never>, State> {
this.setState({config}); this.setState({config});
} }
moveTabs = (originalOrder: number, newOrder: number): number | undefined => { moveTabs = (teamName: string, originalOrder: number, newOrder: number): number | undefined => {
if (!this.state.config) { if (!this.state.config) {
throw new Error('No config'); throw new Error('No config');
} }
const teams = this.state.config.teams.concat(); const teams = this.state.config.teams.concat();
const tabOrder = teams.map((team, index) => { const currentTeamIndex = teams.findIndex((team) => team.name === teamName);
const tabs = teams[currentTeamIndex].tabs.concat();
const tabOrder = tabs.map((team, index) => {
return { return {
index, index,
order: team.order, order: team.order,
@@ -66,8 +69,9 @@ class Root extends React.PureComponent<Record<string, never>, State> {
if (order === newOrder) { if (order === newOrder) {
teamIndex = t.index; teamIndex = t.index;
} }
teams[t.index].order = order; tabs[t.index].order = order;
}); });
teams[currentTeamIndex].tabs = tabs;
this.setState({ this.setState({
config: { config: {
...this.state.config, ...this.state.config,
@@ -118,7 +122,6 @@ class Root extends React.PureComponent<Record<string, never>, State> {
return ( return (
<MainPage <MainPage
teams={config.teams} teams={config.teams}
showAddServerButton={config.enableServerManagement}
moveTabs={this.moveTabs} moveTabs={this.moveTabs}
openMenu={this.openMenu} openMenu={this.openMenu}
darkMode={config.darkMode} darkMode={config.darkMode}

View File

@@ -1,19 +1,48 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved. // Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information. // See LICENSE.txt for license information.
export type Team = { export type Tab = {
name: string; name: string;
url: string;
order: number; order: number;
} }
export type TeamWithIndex = Team & {index: number}; export type Team = Tab & {
url: string;
lastActiveTab?: number;
}
export type Config = ConfigV2; export type TeamWithIndex = Team & {index: number};
export type TeamWithTabs = Team & {tabs: Tab[]};
export type Config = ConfigV3;
export type ConfigV3 = {
version: 3;
teams: TeamWithTabs[];
showTrayIcon: boolean;
trayIconTheme: string;
minimizeToTray: boolean;
notifications: {
flashWindow: number;
bounceIcon: boolean;
bounceIconType: 'critical' | 'informational';
};
showUnreadBadge: boolean;
useSpellChecker: boolean;
enableHardwareAcceleration: boolean;
autostart: boolean;
spellCheckerLocale: string;
darkMode: boolean;
downloadLocation: string;
}
export type ConfigV2 = { export type ConfigV2 = {
version: 2; version: 2;
teams: Team[]; teams: Array<{
name: string;
url: string;
order: number;
}>;
showTrayIcon: boolean; showTrayIcon: boolean;
trayIconTheme: string; trayIconTheme: string;
minimizeToTray: boolean; minimizeToTray: boolean;
@@ -54,7 +83,7 @@ export type ConfigV1 = {
export type ConfigV0 = {version: 0; url: string}; export type ConfigV0 = {version: 0; url: string};
export type AnyConfig = ConfigV2 | ConfigV1 | ConfigV0; export type AnyConfig = ConfigV3 | ConfigV2 | ConfigV1 | ConfigV0;
export type BuildConfig = { export type BuildConfig = {
defaultTeams?: Team[]; defaultTeams?: Team[];
@@ -70,7 +99,7 @@ export type RegistryConfig = {
enableAutoUpdater: boolean; enableAutoUpdater: boolean;
} }
export type CombinedConfig = ConfigV2 & BuildConfig & { export type CombinedConfig = ConfigV3 & BuildConfig & {
registryTeams: Team[]; registryTeams: Team[];
appName: string; appName: string;
} }

View File

@@ -4,7 +4,6 @@
export type ServerFromURL = { export type ServerFromURL = {
name: string; name: string;
url: URL; url: URL;
index: number;
} }
export type Boundaries = { export type Boundaries = {