[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:
@@ -2,7 +2,8 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
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 FOCUS_BROWSERVIEW = 'focus-browserview';
|
||||
export const ZOOM = 'zoom';
|
||||
@@ -62,7 +63,7 @@ export const SESSION_EXPIRED = 'session_expired';
|
||||
export const UPDATE_TRAY = 'update_tray';
|
||||
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 TOGGLE_BACK_BUTTON = 'toggle-back-button';
|
||||
|
@@ -10,14 +10,14 @@ import os from 'os';
|
||||
* @param {number} version - Scheme version. (Not application version)
|
||||
*/
|
||||
|
||||
import {ConfigV2} from 'types/config';
|
||||
import {ConfigV3} from 'types/config';
|
||||
|
||||
export const getDefaultDownloadLocation = (): string => {
|
||||
return path.join(os.homedir(), 'Downloads');
|
||||
};
|
||||
|
||||
const defaultPreferences: ConfigV2 = {
|
||||
version: 2,
|
||||
const defaultPreferences: ConfigV3 = {
|
||||
version: 3,
|
||||
teams: [],
|
||||
showTrayIcon: true,
|
||||
trayIconTheme: 'light',
|
||||
|
@@ -16,12 +16,13 @@ import {
|
||||
Config as ConfigType,
|
||||
LocalConfiguration,
|
||||
RegistryConfig as RegistryConfigType,
|
||||
Team,
|
||||
TeamWithTabs,
|
||||
} from 'types/config';
|
||||
|
||||
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 upgradeConfigData from './upgradePreferences';
|
||||
@@ -263,6 +264,9 @@ export default class Config extends EventEmitter {
|
||||
|
||||
// validate based on config file version
|
||||
switch (configData.version) {
|
||||
case 3:
|
||||
configData = Validator.validateV3ConfigData(configData)!;
|
||||
break;
|
||||
case 2:
|
||||
configData = Validator.validateV2ConfigData(configData)!;
|
||||
break;
|
||||
@@ -317,16 +321,16 @@ export default class Config extends EventEmitter {
|
||||
delete this.combinedData!.defaultTeams;
|
||||
|
||||
// IMPORTANT: properly combine teams from all sources
|
||||
let combinedTeams = [];
|
||||
let combinedTeams: TeamWithTabs[] = [];
|
||||
|
||||
// - start by adding default teams from buildConfig, if any
|
||||
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
|
||||
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
|
||||
@@ -352,7 +356,7 @@ export default class Config extends EventEmitter {
|
||||
*
|
||||
* @param {array} teams array of teams to check for duplicates
|
||||
*/
|
||||
filterOutDuplicateTeams = (teams: Team[]) => {
|
||||
filterOutDuplicateTeams = (teams: TeamWithTabs[]) => {
|
||||
let newTeams = teams;
|
||||
const uniqueURLs = new Set();
|
||||
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
|
||||
* @param {array} teams array of teams to check for already defined teams
|
||||
*/
|
||||
filterOutPredefinedTeams = (teams: Team[]) => {
|
||||
filterOutPredefinedTeams = (teams: TeamWithTabs[]) => {
|
||||
let newTeams = 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.
|
||||
* @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
|
||||
const mappedTeams = teams.map((team, index) => ({team, originalOrder: index}));
|
||||
|
||||
@@ -470,7 +474,7 @@ export default class Config extends EventEmitter {
|
||||
return config;
|
||||
}
|
||||
|
||||
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: Team[]) => {
|
||||
handleUpdateTeams = (event: Electron.IpcMainInvokeEvent, newTeams: TeamWithTabs[]) => {
|
||||
this.set('teams', newTeams);
|
||||
return this.combinedData!.teams;
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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 = {
|
||||
0: {
|
||||
@@ -26,7 +26,26 @@ const pastDefaultPreferences = {
|
||||
autostart: true,
|
||||
spellCheckerLocale: 'en-US',
|
||||
} 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;
|
||||
|
@@ -1,7 +1,10 @@
|
||||
// Copyright (c) 2015-2016 Yuya Ochiai
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// 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';
|
||||
|
||||
@@ -30,10 +33,24 @@ function upgradeV1toV2(configV1: ConfigV1) {
|
||||
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) {
|
||||
case 3:
|
||||
return config as ConfigV3;
|
||||
case 2:
|
||||
return config as ConfigV2;
|
||||
return upgradeToLatest(upgradeV2toV3(config as ConfigV2));
|
||||
case 1:
|
||||
return upgradeToLatest(upgradeV1toV2(config as ConfigV1));
|
||||
default:
|
||||
|
23
src/common/tabs/BaseTabView.ts
Normal file
23
src/common/tabs/BaseTabView.ts
Normal 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');
|
||||
}
|
||||
}
|
15
src/common/tabs/FocalboardTabView.ts
Normal file
15
src/common/tabs/FocalboardTabView.ts
Normal 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;
|
||||
}
|
||||
}
|
15
src/common/tabs/MessagingTabView.ts
Normal file
15
src/common/tabs/MessagingTabView.ts
Normal 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;
|
||||
}
|
||||
}
|
15
src/common/tabs/PlaybooksTabView.ts
Normal file
15
src/common/tabs/PlaybooksTabView.ts
Normal 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;
|
||||
}
|
||||
}
|
61
src/common/tabs/TabView.ts
Normal file
61
src/common/tabs/TabView.ts
Normal 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}`;
|
||||
}
|
@@ -1,11 +1,14 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {Team} from 'types/config';
|
||||
import {ServerFromURL} from 'types/utils';
|
||||
import {isHttpsUri, isHttpUri, isUri} from 'valid-url';
|
||||
|
||||
import {TeamWithTabs} from 'types/config';
|
||||
import {ServerFromURL} from 'types/utils';
|
||||
|
||||
import buildConfig from '../config/buildConfig';
|
||||
import {MattermostServer} from '../servers/MattermostServer';
|
||||
import {getServerView} from '../tabs/TabView';
|
||||
|
||||
// supported custom login paths (oath, saml)
|
||||
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}/`))));
|
||||
}
|
||||
|
||||
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);
|
||||
if (!parsedURL) {
|
||||
return undefined;
|
||||
}
|
||||
let parsedServerUrl;
|
||||
let firstOption;
|
||||
let secondOption;
|
||||
for (let i = 0; i < teams.length; i++) {
|
||||
parsedServerUrl = parseURL(teams[i].url);
|
||||
if (!parsedServerUrl) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
return {name: teams[i].name, url: parsedServerUrl, index: i};
|
||||
}
|
||||
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
// 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
|
||||
// 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
|
||||
secondOption = {name: teams[i].name, url: parsedServerUrl, index: i};
|
||||
}
|
||||
}
|
||||
return secondOption;
|
||||
teams.forEach((team) => {
|
||||
const srv = new MattermostServer(team.name, team.url);
|
||||
team.tabs.forEach((tab) => {
|
||||
const tabView = getServerView(srv, tab);
|
||||
parsedServerUrl = parseURL(tabView.url);
|
||||
if (parsedServerUrl) {
|
||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
firstOption = {name: tabView.name, url: parsedServerUrl};
|
||||
}
|
||||
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||
// 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
|
||||
// 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
|
||||
secondOption = {name: tabView.name, url: parsedServerUrl};
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
return firstOption || secondOption;
|
||||
}
|
||||
|
||||
// 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();
|
||||
}
|
||||
|
||||
function isTrustedURL(url: URL | string, teams: Team[]) {
|
||||
function isTrustedURL(url: URL | string, teams: TeamWithTabs[]) {
|
||||
const parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
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 parsedURL = parseURL(url);
|
||||
if (!parsedURL) {
|
||||
@@ -242,7 +248,7 @@ export default {
|
||||
isValidURI,
|
||||
isInternalURL,
|
||||
parseURL,
|
||||
getServer,
|
||||
getView,
|
||||
getServerInfo,
|
||||
isAdminUrl,
|
||||
isTeamUrl,
|
||||
|
@@ -5,7 +5,7 @@ import log from 'electron-log';
|
||||
import Joi from '@hapi/joi';
|
||||
|
||||
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 {AppState} from 'types/appState';
|
||||
import {ComparableCertificate} from 'types/certificate';
|
||||
@@ -92,6 +92,35 @@ const configDataSchemaV2 = Joi.object<ConfigV2>({
|
||||
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'};
|
||||
const certificateStoreSchema = Joi.object().pattern(
|
||||
Joi.string().uri(),
|
||||
@@ -155,15 +184,22 @@ export function validateV1ConfigData(data: ConfigV1) {
|
||||
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) {
|
||||
if (Array.isArray(data.teams) && data.teams.length) {
|
||||
// first replace possible backslashes with forward slashes
|
||||
let teams = data.teams.map(({name, url, order}) => {
|
||||
let updatedURL = url;
|
||||
if (updatedURL.includes('\\')) {
|
||||
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
|
||||
}
|
||||
return {name, url: updatedURL, order};
|
||||
// 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
|
||||
@@ -175,6 +211,25 @@ export function validateV2ConfigData(data: ConfigV2) {
|
||||
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
|
||||
export function validateCertificateStore(data: string | Record<string, ComparableCertificate>) {
|
||||
const jsonData = (typeof data === 'object' ? data : JSON.parse(data));
|
||||
|
@@ -44,7 +44,7 @@ export class AuthManager {
|
||||
handleAppLogin = (event: Event, webContents: WebContents, request: AuthenticationResponseDetails, authInfo: AuthInfo, callback?: (username?: string, password?: string) => void) => {
|
||||
event.preventDefault();
|
||||
const parsedURL = new URL(request.url);
|
||||
const server = urlUtils.getServer(parsedURL, this.config.teams);
|
||||
const server = urlUtils.getView(parsedURL, this.config.teams);
|
||||
if (!server) {
|
||||
return;
|
||||
}
|
||||
|
@@ -36,9 +36,10 @@ import {
|
||||
RELOAD_CONFIGURATION,
|
||||
USER_ACTIVITY_UPDATE,
|
||||
EMIT_CONFIGURATION,
|
||||
SWITCH_TAB,
|
||||
} from 'common/communication';
|
||||
import Config from 'common/config';
|
||||
|
||||
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||
import Utils from 'common/utils/util';
|
||||
|
||||
import urlUtils from 'common/utils/url';
|
||||
@@ -234,6 +235,7 @@ function initializeInterCommunicationEventListeners() {
|
||||
}
|
||||
|
||||
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
||||
ipcMain.on(SWITCH_TAB, handleSwitchTab);
|
||||
|
||||
ipcMain.on(QUIT, handleQuit);
|
||||
|
||||
@@ -468,6 +470,10 @@ function handleSwitchServer(event: IpcMainEvent, serverName: string) {
|
||||
WindowManager.switchServer(serverName);
|
||||
}
|
||||
|
||||
function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||
WindowManager.switchTab(serverName, tabName);
|
||||
}
|
||||
|
||||
function handleNewServerModal() {
|
||||
const html = getLocalURLString('newServer.html');
|
||||
|
||||
@@ -482,7 +488,7 @@ function handleNewServerModal() {
|
||||
modalPromise.then((data) => {
|
||||
const teams = config.teams;
|
||||
const order = teams.length;
|
||||
teams.push({...data, order});
|
||||
teams.push(getDefaultTeamWithTabsFromTeam({...data, order}));
|
||||
config.set('teams', teams);
|
||||
}).catch((e) => {
|
||||
// e is undefined for user cancellation
|
||||
@@ -580,7 +586,7 @@ function initializeAfterAppReady() {
|
||||
|
||||
item.on('done', (doneEvent, state) => {
|
||||
if (state === 'completed') {
|
||||
displayDownloadCompleted(filename, item.savePath, urlUtils.getServer(webContents.getURL(), config.teams)!);
|
||||
displayDownloadCompleted(filename, item.savePath, urlUtils.getView(webContents.getURL(), config.teams)!);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
@@ -5,7 +5,7 @@
|
||||
|
||||
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 * as WindowManager from '../windows/windowManager';
|
||||
@@ -217,14 +217,14 @@ function createTemplate(config: Config) {
|
||||
label: 'Select Next Server',
|
||||
accelerator: 'Ctrl+Tab',
|
||||
click() {
|
||||
WindowManager.sendToRenderer(SELECT_NEXT_TAB);
|
||||
WindowManager.selectNextTab();
|
||||
},
|
||||
enabled: (teams.length > 1),
|
||||
}, {
|
||||
label: 'Select Previous Server',
|
||||
accelerator: 'Ctrl+Shift+Tab',
|
||||
click() {
|
||||
WindowManager.sendToRenderer(SELECT_PREVIOUS_TAB);
|
||||
WindowManager.selectPreviousTab();
|
||||
},
|
||||
enabled: (teams.length > 1),
|
||||
}],
|
||||
|
@@ -44,6 +44,6 @@ window.addEventListener('message', async (event) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads) => {
|
||||
window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, hasGPOTeams, expired, mentions, unreads}}, window.location.href);
|
||||
ipcRenderer.on(UPDATE_TEAMS_DROPDOWN, (event, teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, expired, mentions, unreads) => {
|
||||
window.postMessage({type: UPDATE_TEAMS_DROPDOWN, data: {teams, activeTeam, darkMode, enableServerManagement, hasGPOTeams, expired, mentions, unreads}}, window.location.href);
|
||||
});
|
||||
|
@@ -9,7 +9,7 @@
|
||||
import {ipcRenderer, webFrame} from 'electron';
|
||||
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 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 appName;
|
||||
let sessionExpired;
|
||||
let serverName;
|
||||
let viewName;
|
||||
|
||||
log.info('Initializing preload');
|
||||
|
||||
@@ -58,7 +58,7 @@ window.addEventListener('load', () => {
|
||||
return;
|
||||
}
|
||||
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);
|
||||
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) => {
|
||||
if (typeof serverName === 'undefined') {
|
||||
serverName = server;
|
||||
if (typeof viewName === 'undefined') {
|
||||
viewName = server;
|
||||
}
|
||||
if (isReactAppInitialized()) {
|
||||
findUnread(favicon);
|
||||
@@ -162,13 +162,13 @@ ipcRenderer.on(IS_UNREAD, (event, favicon, server) => {
|
||||
}
|
||||
});
|
||||
|
||||
ipcRenderer.on(SET_SERVER_NAME, (_, name) => {
|
||||
serverName = name;
|
||||
ipcRenderer.on(SET_VIEW_NAME, (_, name) => {
|
||||
viewName = name;
|
||||
});
|
||||
|
||||
function getUnreadCount() {
|
||||
// LHS not found => Log out => Count should be 0, but session may be expired.
|
||||
if (typeof serverName !== 'undefined') {
|
||||
if (typeof viewName !== 'undefined') {
|
||||
let isExpired;
|
||||
if (document.getElementById('sidebar-left') === null) {
|
||||
const extraParam = (new URLSearchParams(window.location.search)).get('extra');
|
||||
@@ -178,7 +178,7 @@ function getUnreadCount() {
|
||||
}
|
||||
if (isExpired !== sessionExpired) {
|
||||
sessionExpired = isExpired;
|
||||
ipcRenderer.send(SESSION_EXPIRED, sessionExpired, serverName);
|
||||
ipcRenderer.send(SESSION_EXPIRED, sessionExpired, viewName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -18,11 +18,11 @@ import {
|
||||
IS_UNREAD,
|
||||
UNREAD_RESULT,
|
||||
TOGGLE_BACK_BUTTON,
|
||||
SET_SERVER_NAME,
|
||||
SET_VIEW_NAME,
|
||||
LOADSCREEN_END,
|
||||
} from 'common/communication';
|
||||
|
||||
import {MattermostServer} from 'main/MattermostServer';
|
||||
import {TabView} from 'common/tabs/TabView';
|
||||
|
||||
import ContextMenu from '../contextMenu';
|
||||
import {getWindowBoundaries, getLocalPreload, composeUserAgent} from '../utils';
|
||||
@@ -42,7 +42,7 @@ const ASTERISK_GROUP = 3;
|
||||
const MENTIONS_GROUP = 2;
|
||||
|
||||
export class MattermostView extends EventEmitter {
|
||||
server: MattermostServer;
|
||||
tab: TabView;
|
||||
window: BrowserWindow;
|
||||
view: BrowserView;
|
||||
isVisible: boolean;
|
||||
@@ -67,9 +67,9 @@ export class MattermostView extends EventEmitter {
|
||||
retryLoad?: NodeJS.Timeout;
|
||||
maxRetries: number;
|
||||
|
||||
constructor(server: MattermostServer, win: BrowserWindow, options: BrowserViewConstructorOptions) {
|
||||
constructor(tab: TabView, win: BrowserWindow, options: BrowserViewConstructorOptions) {
|
||||
super();
|
||||
this.server = server;
|
||||
this.tab = tab;
|
||||
this.window = win;
|
||||
|
||||
const preload = getLocalPreload('preload.js');
|
||||
@@ -90,7 +90,7 @@ export class MattermostView extends EventEmitter {
|
||||
this.resetLoadingStatus();
|
||||
|
||||
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.hasBeenShown = false;
|
||||
@@ -107,7 +107,7 @@ export class MattermostView extends EventEmitter {
|
||||
// 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)
|
||||
get name() {
|
||||
return this.server?.name;
|
||||
return this.tab.name;
|
||||
}
|
||||
|
||||
resetLoadingStatus = () => {
|
||||
@@ -119,7 +119,7 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
load = (someURL?: URL | string) => {
|
||||
if (!this.server) {
|
||||
if (!this.tab) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -130,12 +130,12 @@ export class MattermostView extends EventEmitter {
|
||||
loadURL = parsedURL.toString();
|
||||
} else {
|
||||
log.error('Cannot parse provided url, using current server url', someURL);
|
||||
loadURL = this.server.url.toString();
|
||||
loadURL = this.tab.url.toString();
|
||||
}
|
||||
} 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()});
|
||||
loading.then(this.loadSuccess(loadURL)).catch((err) => {
|
||||
this.loadRetry(loadURL, err);
|
||||
@@ -153,9 +153,9 @@ export class MattermostView extends EventEmitter {
|
||||
if (this.maxRetries-- > 0) {
|
||||
this.loadRetry(loadURL, err);
|
||||
} else {
|
||||
WindowManager.sendToRenderer(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
this.emit(LOAD_FAILED, this.server.name, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.server.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
|
||||
WindowManager.sendToRenderer(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
|
||||
this.emit(LOAD_FAILED, this.tab.name, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.tab.name)}] Couldn't stablish a connection with ${loadURL}: ${err}.`);
|
||||
this.status = Status.ERROR;
|
||||
}
|
||||
});
|
||||
@@ -164,14 +164,14 @@ export class MattermostView extends EventEmitter {
|
||||
|
||||
loadRetry = (loadURL: string, err: any) => {
|
||||
this.retryLoad = setTimeout(this.retry(loadURL), RELOAD_INTERVAL);
|
||||
WindowManager.sendToRenderer(LOAD_RETRY, this.server.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`);
|
||||
WindowManager.sendToRenderer(LOAD_RETRY, this.tab.name, Date.now() + RELOAD_INTERVAL, err.toString(), loadURL.toString());
|
||||
log.info(`[${Util.shorten(this.tab.name)}] failed loading ${loadURL}: ${err}, retrying in ${RELOAD_INTERVAL / SECOND} seconds`);
|
||||
}
|
||||
|
||||
loadSuccess = (loadURL: string) => {
|
||||
return () => {
|
||||
log.info(`[${Util.shorten(this.server.name)}] finished loading ${loadURL}`);
|
||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.server.name);
|
||||
log.info(`[${Util.shorten(this.tab.name)}] finished loading ${loadURL}`);
|
||||
WindowManager.sendToRenderer(LOAD_SUCCESS, this.tab.name);
|
||||
this.maxRetries = MAX_SERVER_RETRIES;
|
||||
if (this.status === Status.LOADING) {
|
||||
ipcMain.on(UNREAD_RESULT, this.handleFaviconIsUnread);
|
||||
@@ -180,9 +180,9 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
this.status = Status.WAITING_MM;
|
||||
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
||||
this.emit(LOAD_SUCCESS, this.server.name, loadURL);
|
||||
this.view.webContents.send(SET_SERVER_NAME, this.server.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.emit(LOAD_SUCCESS, this.tab.name, loadURL);
|
||||
this.view.webContents.send(SET_VIEW_NAME, this.tab.name);
|
||||
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;
|
||||
if (request && !this.isVisible) {
|
||||
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) {
|
||||
this.focus();
|
||||
}
|
||||
@@ -254,8 +254,8 @@ export class MattermostView extends EventEmitter {
|
||||
this.status = Status.READY;
|
||||
|
||||
if (timedout) {
|
||||
log.info(`${this.server.name} timeout expired will show the browserview`);
|
||||
this.emit(LOADSCREEN_END, this.server.name);
|
||||
log.info(`${this.tab.name} timeout expired will show the browserview`);
|
||||
this.emit(LOADSCREEN_END, this.tab.name);
|
||||
}
|
||||
clearTimeout(this.removeLoading);
|
||||
delete this.removeLoading;
|
||||
@@ -291,7 +291,7 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
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) {
|
||||
this.setBounds(getWindowBoundaries(this.window));
|
||||
WindowManager.sendToRenderer(TOGGLE_BACK_BUTTON, false);
|
||||
@@ -304,7 +304,7 @@ export class MattermostView extends EventEmitter {
|
||||
}
|
||||
|
||||
handleUpdateTarget = (e: Event, url: string) => {
|
||||
if (!url || !this.server.sameOrigin(url)) {
|
||||
if (!url || !this.tab.server.sameOrigin(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;
|
||||
|
||||
appState.updateMentions(this.server.name, mentions, unreads);
|
||||
appState.updateMentions(this.tab.name, mentions, unreads);
|
||||
}
|
||||
|
||||
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
|
||||
this.currentFavicon = 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 {
|
||||
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
|
||||
findUnreadState = (favicon: string | null) => {
|
||||
try {
|
||||
this.view.webContents.send(IS_UNREAD, favicon, this.server.name);
|
||||
this.view.webContents.send(IS_UNREAD, favicon, this.tab.name);
|
||||
} catch (err) {
|
||||
log.error(`There was an error trying to request the unread state: ${err}`);
|
||||
log.error(err.stack);
|
||||
@@ -359,13 +359,13 @@ export class MattermostView extends EventEmitter {
|
||||
|
||||
// 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.
|
||||
handleFaviconIsUnread = (e: Event, favicon: string, serverName: string, result: boolean) => {
|
||||
if (this.server && serverName === this.server.name) {
|
||||
handleFaviconIsUnread = (e: Event, favicon: string, viewName: string, result: boolean) => {
|
||||
if (this.tab && viewName === this.tab.name) {
|
||||
if (favicon) {
|
||||
this.faviconMemoize.set(favicon, result);
|
||||
}
|
||||
if (!favicon || favicon === this.currentFavicon) {
|
||||
appState.updateUnreads(serverName, result);
|
||||
appState.updateUnreads(viewName, result);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,7 @@
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
import {BrowserView, BrowserWindow, ipcMain, IpcMainEvent} from 'electron';
|
||||
import {CombinedConfig, Team} from 'types/config';
|
||||
import {CombinedConfig, TeamWithTabs} from 'types/config';
|
||||
|
||||
import {
|
||||
CLOSE_TEAMS_DROPDOWN,
|
||||
@@ -12,7 +12,7 @@ import {
|
||||
UPDATE_DROPDOWN_MENTIONS,
|
||||
REQUEST_TEAMS_DROPDOWN_INFO,
|
||||
RECEIVE_DROPDOWN_MENU_SIZE,
|
||||
SET_SERVER_KEY,
|
||||
SET_ACTIVE_VIEW,
|
||||
} from 'common/communication';
|
||||
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';
|
||||
@@ -22,19 +22,21 @@ import * as WindowManager from '../windows/windowManager';
|
||||
export default class TeamDropdownView {
|
||||
view: BrowserView;
|
||||
bounds?: Electron.Rectangle;
|
||||
teams: Team[];
|
||||
teams: TeamWithTabs[];
|
||||
activeTeam?: string;
|
||||
darkMode: boolean;
|
||||
enableServerManagement?: boolean;
|
||||
hasGPOTeams?: boolean;
|
||||
unreads?: Map<string, boolean>;
|
||||
mentions?: Map<string, number>;
|
||||
expired?: Map<string, boolean>;
|
||||
window: BrowserWindow;
|
||||
|
||||
constructor(window: BrowserWindow, teams: Team[], darkMode: boolean) {
|
||||
constructor(window: BrowserWindow, teams: TeamWithTabs[], darkMode: boolean, enableServerManagement: boolean) {
|
||||
this.teams = teams;
|
||||
this.window = window;
|
||||
this.darkMode = darkMode;
|
||||
this.enableServerManagement = enableServerManagement;
|
||||
|
||||
const preload = getLocalPreload('dropdown.js');
|
||||
this.view = new BrowserView({webPreferences: {
|
||||
@@ -51,13 +53,14 @@ export default class TeamDropdownView {
|
||||
ipcMain.on(EMIT_CONFIGURATION, this.updateConfig);
|
||||
ipcMain.on(REQUEST_TEAMS_DROPDOWN_INFO, this.updateDropdown);
|
||||
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);
|
||||
}
|
||||
|
||||
updateConfig = (event: IpcMainEvent, config: CombinedConfig) => {
|
||||
this.teams = config.teams;
|
||||
this.darkMode = config.darkMode;
|
||||
this.enableServerManagement = config.enableServerManagement;
|
||||
this.hasGPOTeams = config.registryTeams && config.registryTeams.length > 0;
|
||||
this.updateDropdown();
|
||||
}
|
||||
@@ -75,7 +78,17 @@ export default class TeamDropdownView {
|
||||
}
|
||||
|
||||
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 = () => {
|
||||
|
@@ -4,21 +4,23 @@ import log from 'electron-log';
|
||||
import {BrowserView, BrowserWindow, dialog, ipcMain} from 'electron';
|
||||
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 {
|
||||
UPDATE_TARGET_URL,
|
||||
SET_SERVER_KEY,
|
||||
LOAD_SUCCESS,
|
||||
LOAD_FAILED,
|
||||
TOGGLE_LOADING_SCREEN_VISIBILITY,
|
||||
GET_LOADING_SCREEN_DATA,
|
||||
LOADSCREEN_END,
|
||||
SET_ACTIVE_VIEW,
|
||||
} from 'common/communication';
|
||||
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 {MattermostView} from './MattermostView';
|
||||
@@ -29,7 +31,7 @@ const URL_VIEW_DURATION = 10 * SECOND;
|
||||
const URL_VIEW_HEIGHT = 36;
|
||||
|
||||
export class ViewManager {
|
||||
configServers: Team[];
|
||||
configServers: TeamWithTabs[];
|
||||
viewOptions: BrowserViewConstructorOptions;
|
||||
views: Map<string, MattermostView>;
|
||||
currentView?: string;
|
||||
@@ -53,10 +55,15 @@ export class ViewManager {
|
||||
return this.configServers;
|
||||
}
|
||||
|
||||
loadServer = (server: Team) => {
|
||||
loadServer = (server: TeamWithTabs) => {
|
||||
const srv = new MattermostServer(server.name, server.url);
|
||||
const view = new MattermostView(srv, this.mainWindow, this.viewOptions);
|
||||
this.views.set(server.name, view);
|
||||
server.tabs.forEach((tab) => this.loadView(srv, tab));
|
||||
}
|
||||
|
||||
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) {
|
||||
this.createLoadingScreen();
|
||||
}
|
||||
@@ -71,23 +78,27 @@ export class ViewManager {
|
||||
this.configServers.forEach((server) => this.loadServer(server));
|
||||
}
|
||||
|
||||
reloadConfiguration = (configServers: Team[]) => {
|
||||
reloadConfiguration = (configServers: TeamWithTabs[]) => {
|
||||
this.configServers = configServers.concat();
|
||||
const oldviews = this.views;
|
||||
this.views = new Map();
|
||||
const sorted = this.configServers.sort((a, b) => a.order - b.order);
|
||||
let setFocus;
|
||||
sorted.forEach((server) => {
|
||||
const recycle = oldviews.get(server.name);
|
||||
if (recycle && recycle.isVisible) {
|
||||
setFocus = recycle.name;
|
||||
}
|
||||
if (recycle && recycle.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) {
|
||||
oldviews.delete(recycle.name);
|
||||
this.views.set(recycle.name, recycle);
|
||||
} else {
|
||||
this.loadServer(server);
|
||||
}
|
||||
const srv = new MattermostServer(server.name, server.url);
|
||||
server.tabs.forEach((tab) => {
|
||||
const tabView = getServerView(srv, tab);
|
||||
const recycle = oldviews.get(tabView.name);
|
||||
if (recycle && recycle.isVisible) {
|
||||
setFocus = recycle.name;
|
||||
}
|
||||
if (recycle && recycle.tab.name === tabView.name && recycle.tab.url.toString() === urlUtils.parseURL(tabView.url)!.toString()) {
|
||||
oldviews.delete(recycle.name);
|
||||
this.views.set(recycle.name, recycle);
|
||||
} else {
|
||||
this.loadView(srv, tab);
|
||||
}
|
||||
});
|
||||
});
|
||||
oldviews.forEach((unused) => {
|
||||
unused.destroy();
|
||||
@@ -103,7 +114,11 @@ export class ViewManager {
|
||||
if (this.configServers.length) {
|
||||
const element = this.configServers.find((e) => e.order === 0);
|
||||
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()) {
|
||||
this.showLoadingScreen();
|
||||
}
|
||||
const serverInfo = this.configServers.find((candidate) => candidate.name === newView.server.name);
|
||||
if (!serverInfo) {
|
||||
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);
|
||||
newView.window.webContents.send(SET_ACTIVE_VIEW, newView.tab.server.name, newView.tab.type);
|
||||
ipcMain.emit(SET_ACTIVE_VIEW, true, newView.tab.server.name, newView.tab.type);
|
||||
if (newView.isReady()) {
|
||||
// if view is not ready, the renderer will have something to display instead.
|
||||
newView.show();
|
||||
@@ -326,18 +336,18 @@ export class ViewManager {
|
||||
}
|
||||
}
|
||||
|
||||
deeplinkSuccess = (serverName: string) => {
|
||||
const view = this.views.get(serverName);
|
||||
deeplinkSuccess = (viewName: string) => {
|
||||
const view = this.views.get(viewName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
this.showByName(serverName);
|
||||
this.showByName(viewName);
|
||||
view.removeListener(LOAD_FAILED, this.deeplinkFailed);
|
||||
};
|
||||
|
||||
deeplinkFailed = (serverName: string, err: string, url: string) => {
|
||||
log.error(`[${serverName}] failed to load deeplink ${url}: ${err}`);
|
||||
const view = this.views.get(serverName);
|
||||
deeplinkFailed = (viewName: string, err: string, url: string) => {
|
||||
log.error(`[${viewName}] failed to load deeplink ${url}: ${err}`);
|
||||
const view = this.views.get(viewName);
|
||||
if (!view) {
|
||||
return;
|
||||
}
|
||||
@@ -345,18 +355,19 @@ export class ViewManager {
|
||||
}
|
||||
|
||||
handleDeepLink = (url: string | URL) => {
|
||||
// TODO: fix for new tabs
|
||||
if (url) {
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, this.configServers, true);
|
||||
if (server) {
|
||||
const view = this.views.get(server.name);
|
||||
const tabView = urlUtils.getView(parsedURL, this.configServers, true);
|
||||
if (tabView) {
|
||||
const view = this.views.get(tabView.name);
|
||||
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;
|
||||
}
|
||||
|
||||
// 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.load(urlWithSchema);
|
||||
view.once(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||
|
@@ -4,7 +4,7 @@
|
||||
import {BrowserWindow, shell, WebContents} from 'electron';
|
||||
import log from 'electron-log';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
import {TeamWithTabs} from 'types/config';
|
||||
|
||||
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 generateWillNavigate = (getServersFunction: () => Team[]) => {
|
||||
const generateWillNavigate = (getServersFunction: () => TeamWithTabs[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
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))) {
|
||||
return;
|
||||
@@ -63,12 +63,12 @@ const generateWillNavigate = (getServersFunction: () => Team[]) => {
|
||||
};
|
||||
};
|
||||
|
||||
const generateDidStartNavigation = (getServersFunction: () => Team[]) => {
|
||||
const generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) => {
|
||||
return (event: Event & {sender: WebContents}, url: string) => {
|
||||
const serverList = getServersFunction();
|
||||
const contentID = event.sender.id;
|
||||
const parsedURL = urlUtils.parseURL(url)!;
|
||||
const server = urlUtils.getServer(parsedURL, serverList);
|
||||
const server = urlUtils.getView(parsedURL, serverList);
|
||||
|
||||
if (!urlUtils.isTrustedURL(parsedURL, serverList)) {
|
||||
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) => {
|
||||
const parsedURL = urlUtils.parseURL(url);
|
||||
if (!parsedURL) {
|
||||
@@ -110,7 +110,7 @@ const generateNewWindowListener = (getServersFunction: () => Team[], spellcheck?
|
||||
return;
|
||||
}
|
||||
|
||||
const server = urlUtils.getServer(parsedURL, configServers);
|
||||
const server = urlUtils.getView(parsedURL, configServers);
|
||||
|
||||
if (!server) {
|
||||
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;
|
||||
|
||||
// initialize custom login tracking
|
||||
|
@@ -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 urlUtils from 'common/utils/url';
|
||||
|
||||
import {getTabViewName} from 'common/tabs/TabView';
|
||||
|
||||
import {getAdjustedWindowBoundaries} from '../utils';
|
||||
|
||||
import {ViewManager} from '../views/viewManager';
|
||||
@@ -113,7 +115,7 @@ export function showMainWindow(deeplinkingURL?: string | URL) {
|
||||
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();
|
||||
|
||||
@@ -158,7 +160,7 @@ function handleResizeMainWindow() {
|
||||
|
||||
const setBoundsFunction = () => {
|
||||
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) {
|
||||
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() {
|
||||
@@ -369,9 +384,9 @@ function handleLoadingScreenDataRequest() {
|
||||
};
|
||||
}
|
||||
|
||||
function handleReactAppInitialized(e: IpcMainEvent, server: string) {
|
||||
function handleReactAppInitialized(e: IpcMainEvent, view: string) {
|
||||
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);
|
||||
} catch (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() {
|
||||
return status.config?.darkMode;
|
||||
}
|
||||
|
||||
|
@@ -8,7 +8,9 @@ import {DropResult} from 'react-beautiful-dnd';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
import {IpcRendererEvent} from 'electron/renderer';
|
||||
|
||||
import {Team} from 'types/config';
|
||||
import {TeamWithTabs} from 'types/config';
|
||||
|
||||
import {getTabViewName} from 'common/tabs/TabView';
|
||||
|
||||
import {
|
||||
FOCUS_BROWSERVIEW,
|
||||
@@ -18,8 +20,6 @@ import {
|
||||
LOAD_RETRY,
|
||||
LOAD_SUCCESS,
|
||||
LOAD_FAILED,
|
||||
SHOW_NEW_SERVER_MODAL,
|
||||
SWITCH_SERVER,
|
||||
WINDOW_CLOSE,
|
||||
WINDOW_MINIMIZE,
|
||||
WINDOW_RESTORE,
|
||||
@@ -28,16 +28,14 @@ import {
|
||||
PLAY_SOUND,
|
||||
MODAL_OPEN,
|
||||
MODAL_CLOSE,
|
||||
SET_SERVER_KEY,
|
||||
SET_ACTIVE_VIEW,
|
||||
UPDATE_MENTIONS,
|
||||
TOGGLE_BACK_BUTTON,
|
||||
SELECT_NEXT_TAB,
|
||||
SELECT_PREVIOUS_TAB,
|
||||
ADD_SERVER,
|
||||
FOCUS_THREE_DOT_MENU,
|
||||
GET_FULL_SCREEN_STATUS,
|
||||
CLOSE_TEAMS_DROPDOWN,
|
||||
OPEN_TEAMS_DROPDOWN,
|
||||
SWITCH_TAB,
|
||||
} from 'common/communication';
|
||||
|
||||
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
||||
@@ -61,22 +59,21 @@ enum Status {
|
||||
}
|
||||
|
||||
type Props = {
|
||||
teams: Team[];
|
||||
showAddServerButton: boolean;
|
||||
moveTabs: (originalOrder: number, newOrder: number) => number | undefined;
|
||||
teams: TeamWithTabs[];
|
||||
moveTabs: (teamName: string, originalOrder: number, newOrder: number) => number | undefined;
|
||||
openMenu: () => void;
|
||||
darkMode: boolean;
|
||||
appName: string;
|
||||
};
|
||||
|
||||
type State = {
|
||||
key: number;
|
||||
activeServerName?: string;
|
||||
activeTabName?: string;
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
targetURL: string;
|
||||
maximized: boolean;
|
||||
tabStatus: Map<string, TabStatus>;
|
||||
tabViewStatus: Map<string, TabViewStatus>;
|
||||
darkMode: boolean;
|
||||
modalOpen?: boolean;
|
||||
fullScreen?: boolean;
|
||||
@@ -84,7 +81,7 @@ type State = {
|
||||
isMenuOpen: boolean;
|
||||
};
|
||||
|
||||
type TabStatus = {
|
||||
type TabViewStatus = {
|
||||
status: Status;
|
||||
extra?: {
|
||||
url: string;
|
||||
@@ -102,40 +99,39 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
this.topBar = 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 = {
|
||||
key: this.props.teams.length ? this.props.teams.findIndex((team) => team.order === 0) : 0,
|
||||
activeServerName: firstServer?.name,
|
||||
activeTabName: firstTab?.name,
|
||||
sessionsExpired: {},
|
||||
unreadCounts: {},
|
||||
mentionCounts: {},
|
||||
targetURL: '',
|
||||
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,
|
||||
isMenuOpen: false,
|
||||
};
|
||||
}
|
||||
|
||||
getTabStatus() {
|
||||
if (this.props.teams.length) {
|
||||
const tab = this.props.teams[this.state.key];
|
||||
if (tab) {
|
||||
const tabname = tab.name;
|
||||
return this.state.tabStatus.get(tabname);
|
||||
}
|
||||
getTabViewStatus() {
|
||||
if (!this.state.activeServerName || !this.state.activeTabName) {
|
||||
return undefined;
|
||||
}
|
||||
return {status: Status.NOSERVERS};
|
||||
return this.state.tabViewStatus.get(getTabViewName(this.state.activeServerName, this.state.activeTabName)) ?? {status: Status.NOSERVERS};
|
||||
}
|
||||
|
||||
updateTabStatus(server: string, newStatusValue: TabStatus) {
|
||||
const status = new Map(this.state.tabStatus);
|
||||
status.set(server, newStatusValue);
|
||||
this.setState({tabStatus: status});
|
||||
updateTabStatus(tabViewName: string, newStatusValue: TabViewStatus) {
|
||||
const status = new Map(this.state.tabViewStatus);
|
||||
status.set(tabViewName, newStatusValue);
|
||||
this.setState({tabViewStatus: status});
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
// set page on retry
|
||||
window.ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}, but retrying`);
|
||||
window.ipcRenderer.on(LOAD_RETRY, (_, viewName, retry, err, loadUrl) => {
|
||||
console.log(`${viewName}: failed to load ${err}, but retrying`);
|
||||
const statusValue = {
|
||||
status: Status.RETRY,
|
||||
extra: {
|
||||
@@ -144,15 +140,15 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
url: loadUrl,
|
||||
},
|
||||
};
|
||||
this.updateTabStatus(server, statusValue);
|
||||
this.updateTabStatus(viewName, statusValue);
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
|
||||
this.updateTabStatus(server, {status: Status.DONE});
|
||||
window.ipcRenderer.on(LOAD_SUCCESS, (_, viewName) => {
|
||||
this.updateTabStatus(viewName, {status: Status.DONE});
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
|
||||
console.log(`${server}: failed to load ${err}`);
|
||||
window.ipcRenderer.on(LOAD_FAILED, (_, viewName, err, loadUrl) => {
|
||||
console.log(`${viewName}: failed to load ${err}`);
|
||||
const statusValue = {
|
||||
status: Status.FAILED,
|
||||
extra: {
|
||||
@@ -160,7 +156,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
url: loadUrl,
|
||||
},
|
||||
};
|
||||
this.updateTabStatus(server, statusValue);
|
||||
this.updateTabStatus(viewName, statusValue);
|
||||
});
|
||||
|
||||
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...
|
||||
window.ipcRenderer.on(SET_SERVER_KEY, (event, key) => {
|
||||
const nextIndex = this.props.teams.findIndex((team) => team.order === key);
|
||||
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(SET_ACTIVE_VIEW, (event, serverName, tabName) => {
|
||||
this.setState({activeServerName: serverName, activeTabName: tabName});
|
||||
});
|
||||
|
||||
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.on(ADD_SERVER, () => {
|
||||
this.addServer();
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(PLAY_SOUND, (_event, soundName) => {
|
||||
playSound(soundName);
|
||||
});
|
||||
@@ -217,18 +191,17 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
this.setState({showExtraBar});
|
||||
});
|
||||
|
||||
window.ipcRenderer.on(UPDATE_MENTIONS, (_event, team, mentions, unreads, isExpired) => {
|
||||
const key = this.props.teams.findIndex((server) => server.name === team);
|
||||
window.ipcRenderer.on(UPDATE_MENTIONS, (_event, view, mentions, unreads, isExpired) => {
|
||||
const {unreadCounts, mentionCounts, sessionsExpired} = this.state;
|
||||
|
||||
const newMentionCounts = {...mentionCounts};
|
||||
newMentionCounts[key] = mentions || 0;
|
||||
newMentionCounts[view] = mentions || 0;
|
||||
|
||||
const newUnreads = {...unreadCounts};
|
||||
newUnreads[key] = unreads || false;
|
||||
newUnreads[view] = unreads || false;
|
||||
|
||||
const expired = {...sessionsExpired};
|
||||
expired[key] = isExpired || false;
|
||||
expired[view] = isExpired || false;
|
||||
|
||||
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});
|
||||
}
|
||||
|
||||
handleSetServerKey = (key: number) => {
|
||||
const newKey = ((this.props.teams.length + key) % this.props.teams.length) || 0;
|
||||
this.setState({key: newKey});
|
||||
}
|
||||
|
||||
handleSelect = (name: string, key: number) => {
|
||||
window.ipcRenderer.send(SWITCH_SERVER, name);
|
||||
this.handleSetServerKey(key);
|
||||
handleSelectTab = (name: string) => {
|
||||
window.ipcRenderer.send(SWITCH_TAB, this.state.activeServerName, name);
|
||||
}
|
||||
|
||||
handleDragAndDrop = async (dropResult: DropResult) => {
|
||||
@@ -274,12 +241,20 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
if (addedIndex === undefined || removedIndex === addedIndex) {
|
||||
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) {
|
||||
return;
|
||||
}
|
||||
const name = this.props.teams[teamIndex].name;
|
||||
this.handleSelect(name, teamIndex);
|
||||
const name = currentTabs[teamIndex].name;
|
||||
this.handleSelectTab(name);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
addServer = () => {
|
||||
window.ipcRenderer.send(SHOW_NEW_SERVER_MODAL);
|
||||
}
|
||||
|
||||
focusOnWebView = () => {
|
||||
window.ipcRenderer.send(FOCUS_BROWSERVIEW);
|
||||
window.ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -330,14 +306,13 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
<TabBar
|
||||
id='tabBar'
|
||||
isDarkMode={this.state.darkMode}
|
||||
teams={this.props.teams}
|
||||
tabs={currentTabs}
|
||||
sessionsExpired={this.state.sessionsExpired}
|
||||
unreadCounts={this.state.unreadCounts}
|
||||
mentionCounts={this.state.mentionCounts}
|
||||
activeKey={this.state.key}
|
||||
onSelect={this.handleSelect}
|
||||
onAddServer={this.addServer}
|
||||
showAddServerButton={this.props.showAddServerButton}
|
||||
activeServerName={this.state.activeServerName}
|
||||
activeTabName={this.state.activeTabName}
|
||||
onSelect={this.handleSelectTab}
|
||||
onDrop={this.handleDragAndDrop}
|
||||
tabsDisabled={this.state.modalOpen}
|
||||
/>
|
||||
@@ -424,7 +399,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
<DotsVerticalIcon/>
|
||||
</button>
|
||||
<TeamDropdownButton
|
||||
activeServerName={this.props.teams[this.state.key].name}
|
||||
activeServerName={this.state.activeServerName}
|
||||
totalMentionCount={totalMentionCount}
|
||||
hasUnreads={totalUnreadCount > 0}
|
||||
isMenuOpen={this.state.isMenuOpen}
|
||||
@@ -439,11 +414,10 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
||||
|
||||
const views = () => {
|
||||
let component;
|
||||
const tabStatus = this.getTabStatus();
|
||||
const tabStatus = this.getTabViewStatus();
|
||||
if (!tabStatus) {
|
||||
const tab = this.props.teams[this.state.key];
|
||||
if (tab) {
|
||||
console.error(`Not tabStatus for ${this.props.teams[this.state.key].name}`);
|
||||
if (this.state.activeTabName) {
|
||||
console.error(`Not tabStatus for ${this.state.activeTabName}`);
|
||||
} else {
|
||||
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:
|
||||
component = (
|
||||
<ErrorView
|
||||
id={this.state.key + '-fail'}
|
||||
id={this.state.activeTabName + '-fail'}
|
||||
errorInfo={tabStatus.extra?.error}
|
||||
url={tabStatus.extra ? tabStatus.extra.url : ''}
|
||||
active={true}
|
||||
|
@@ -15,6 +15,7 @@ import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
|
||||
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 {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
||||
|
||||
import TeamList from './TeamList';
|
||||
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
|
||||
@@ -337,7 +338,7 @@ export default class SettingsPage extends React.PureComponent<Record<string, nev
|
||||
|
||||
addServer = (team: Team) => {
|
||||
const teams = this.state.teams || [];
|
||||
teams.push(team);
|
||||
teams.push(getDefaultTeamWithTabsFromTeam(team));
|
||||
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
|
||||
this.setState({
|
||||
teams,
|
||||
|
@@ -5,32 +5,26 @@
|
||||
import React from 'react';
|
||||
import {Nav, NavItem, NavLink} from 'react-bootstrap';
|
||||
import {DragDropContext, Draggable, DraggingStyle, Droppable, DropResult, NotDraggingStyle} from 'react-beautiful-dnd';
|
||||
import PlusIcon from 'mdi-react/PlusIcon';
|
||||
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 = {
|
||||
activeKey: number;
|
||||
activeTabName: string;
|
||||
activeServerName: string;
|
||||
id: string;
|
||||
isDarkMode: boolean;
|
||||
onSelect: (name: string, index: number) => void;
|
||||
teams: Team[];
|
||||
tabs: Tab[];
|
||||
sessionsExpired: Record<string, boolean>;
|
||||
unreadCounts: Record<string, number>;
|
||||
mentionCounts: Record<string, number>;
|
||||
showAddServerButton: boolean;
|
||||
onAddServer: () => void;
|
||||
onDrop: (result: DropResult) => void;
|
||||
tabsDisabled?: boolean;
|
||||
};
|
||||
|
||||
type State = {
|
||||
hasGPOTeams: boolean;
|
||||
};
|
||||
|
||||
function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
if (style?.transform) {
|
||||
const axisLockX = `${style.transform.slice(0, style.transform.indexOf(','))}, 0px)`;
|
||||
@@ -42,31 +36,19 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
||||
return style;
|
||||
}
|
||||
|
||||
export default class TabBar extends React.PureComponent<Props, State> { // need "this"
|
||||
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});
|
||||
});
|
||||
}
|
||||
|
||||
export default class TabBar extends React.PureComponent<Props> {
|
||||
render() {
|
||||
const orderedTabs = this.props.teams.concat().sort((a, b) => a.order - b.order);
|
||||
const tabs = orderedTabs.map((team, orderedIndex) => {
|
||||
const index = this.props.teams.indexOf(team);
|
||||
const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order);
|
||||
const tabs = orderedTabs.map((tab, orderedIndex) => {
|
||||
const index = this.props.tabs.indexOf(tab);
|
||||
const tabName = getTabViewName(this.props.activeServerName, tab.name);
|
||||
|
||||
const sessionExpired = this.props.sessionsExpired[index];
|
||||
const hasUnreads = this.props.unreadCounts[index];
|
||||
const sessionExpired = this.props.sessionsExpired[tabName];
|
||||
const hasUnreads = this.props.unreadCounts[tabName];
|
||||
|
||||
let mentionCount = 0;
|
||||
if (this.props.mentionCounts[index] > 0) {
|
||||
mentionCount = this.props.mentionCounts[index];
|
||||
if (this.props.mentionCounts[tabName] > 0) {
|
||||
mentionCount = this.props.mentionCounts[tabName];
|
||||
}
|
||||
|
||||
let badgeDiv: React.ReactNode;
|
||||
@@ -98,9 +80,9 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
|
||||
as='li'
|
||||
id={`teamTabItem${index}`}
|
||||
draggable={false}
|
||||
title={team.name}
|
||||
title={tab.name}
|
||||
className={classNames('teamTabItem', {
|
||||
active: this.props.activeKey === index,
|
||||
active: this.props.activeTabName === tab.name,
|
||||
dragging: snapshot.isDragging,
|
||||
})}
|
||||
{...provided.draggableProps}
|
||||
@@ -110,15 +92,15 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
|
||||
<NavLink
|
||||
eventKey={index}
|
||||
draggable={false}
|
||||
active={this.props.activeKey === index}
|
||||
active={this.props.activeTabName === tab.name}
|
||||
disabled={this.props.tabsDisabled}
|
||||
onSelect={() => {
|
||||
this.props.onSelect(team.name, index);
|
||||
this.props.onSelect(tab.name, index);
|
||||
}}
|
||||
>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<span>
|
||||
{team.name}
|
||||
{tab.name}
|
||||
</span>
|
||||
{ badgeDiv }
|
||||
</div>
|
||||
@@ -128,49 +110,11 @@ export default class TabBar extends React.PureComponent<Props, State> { // need
|
||||
</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 (
|
||||
<DragDropContext onDragEnd={this.props.onDrop}>
|
||||
<Droppable
|
||||
isDropDisabled={this.state.hasGPOTeams || this.props.tabsDisabled}
|
||||
isDropDisabled={this.props.tabsDisabled}
|
||||
droppableId='tabBar'
|
||||
direction='horizontal'
|
||||
>
|
||||
|
@@ -6,18 +6,21 @@ import ReactDOM from 'react-dom';
|
||||
import classNames from 'classnames';
|
||||
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 {getTabViewName} from 'common/tabs/TabView';
|
||||
|
||||
import './css/dropdown.scss';
|
||||
import './css/compass-icons.css';
|
||||
|
||||
type State = {
|
||||
teams?: Team[];
|
||||
orderedTeams?: Team[];
|
||||
teams?: TeamWithTabs[];
|
||||
orderedTeams?: TeamWithTabs[];
|
||||
activeTeam?: string;
|
||||
darkMode?: boolean;
|
||||
enableServerManagement?: boolean;
|
||||
unreads?: Map<string, boolean>;
|
||||
mentions?: Map<string, number>;
|
||||
expired?: Map<string, boolean>;
|
||||
@@ -47,12 +50,13 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
|
||||
handleMessageEvent = (event: MessageEvent) => {
|
||||
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({
|
||||
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,
|
||||
darkMode,
|
||||
enableServerManagement,
|
||||
hasGPOTeams,
|
||||
unreads,
|
||||
mentions,
|
||||
@@ -168,10 +172,13 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
>
|
||||
{this.state.orderedTeams?.map((team, orderedIndex) => {
|
||||
const index = this.state.teams?.indexOf(team);
|
||||
|
||||
const sessionExpired = this.state.expired?.get(team.name);
|
||||
const hasUnreads = this.state.unreads?.get(team.name);
|
||||
const mentionCount = this.state.mentions?.get(team.name);
|
||||
const {sessionExpired, hasUnreads, mentionCount} = team.tabs.reduce((counts, tab) => {
|
||||
const tabName = getTabViewName(team.name, tab.name);
|
||||
counts.sessionExpired = this.state.expired?.get(tabName) || counts.sessionExpired;
|
||||
counts.hasUnreads = this.state.unreads?.get(tabName) || counts.hasUnreads;
|
||||
counts.mentionCount += this.state.mentions?.get(tabName) || 0;
|
||||
return counts;
|
||||
}, {sessionExpired: false, hasUnreads: false, mentionCount: 0});
|
||||
|
||||
let badgeDiv: React.ReactNode;
|
||||
if (sessionExpired) {
|
||||
@@ -250,13 +257,15 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
|
||||
</Droppable>
|
||||
</DragDropContext>
|
||||
<hr className='TeamDropdown__divider'/>
|
||||
<button
|
||||
className='TeamDropdown__button addServer'
|
||||
onClick={this.addServer}
|
||||
>
|
||||
<i className='icon-plus'/>
|
||||
<span>{'Add a server'}</span>
|
||||
</button>
|
||||
{this.state.enableServerManagement &&
|
||||
<button
|
||||
className='TeamDropdown__button addServer'
|
||||
onClick={this.addServer}
|
||||
>
|
||||
<i className='icon-plus'/>
|
||||
<span>{'Add a server'}</span>
|
||||
</button>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@@ -46,12 +46,15 @@ class Root extends React.PureComponent<Record<string, never>, State> {
|
||||
this.setState({config});
|
||||
}
|
||||
|
||||
moveTabs = (originalOrder: number, newOrder: number): number | undefined => {
|
||||
moveTabs = (teamName: string, originalOrder: number, newOrder: number): number | undefined => {
|
||||
if (!this.state.config) {
|
||||
throw new Error('No config');
|
||||
}
|
||||
const teams = this.state.config.teams.concat();
|
||||
const 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 {
|
||||
index,
|
||||
order: team.order,
|
||||
@@ -66,8 +69,9 @@ class Root extends React.PureComponent<Record<string, never>, State> {
|
||||
if (order === newOrder) {
|
||||
teamIndex = t.index;
|
||||
}
|
||||
teams[t.index].order = order;
|
||||
tabs[t.index].order = order;
|
||||
});
|
||||
teams[currentTeamIndex].tabs = tabs;
|
||||
this.setState({
|
||||
config: {
|
||||
...this.state.config,
|
||||
@@ -118,7 +122,6 @@ class Root extends React.PureComponent<Record<string, never>, State> {
|
||||
return (
|
||||
<MainPage
|
||||
teams={config.teams}
|
||||
showAddServerButton={config.enableServerManagement}
|
||||
moveTabs={this.moveTabs}
|
||||
openMenu={this.openMenu}
|
||||
darkMode={config.darkMode}
|
||||
|
@@ -1,19 +1,48 @@
|
||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
export type Team = {
|
||||
export type Tab = {
|
||||
name: string;
|
||||
url: string;
|
||||
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 = {
|
||||
version: 2;
|
||||
teams: Team[];
|
||||
teams: Array<{
|
||||
name: string;
|
||||
url: string;
|
||||
order: number;
|
||||
}>;
|
||||
showTrayIcon: boolean;
|
||||
trayIconTheme: string;
|
||||
minimizeToTray: boolean;
|
||||
@@ -54,7 +83,7 @@ export type ConfigV1 = {
|
||||
|
||||
export type ConfigV0 = {version: 0; url: string};
|
||||
|
||||
export type AnyConfig = ConfigV2 | ConfigV1 | ConfigV0;
|
||||
export type AnyConfig = ConfigV3 | ConfigV2 | ConfigV1 | ConfigV0;
|
||||
|
||||
export type BuildConfig = {
|
||||
defaultTeams?: Team[];
|
||||
@@ -70,7 +99,7 @@ export type RegistryConfig = {
|
||||
enableAutoUpdater: boolean;
|
||||
}
|
||||
|
||||
export type CombinedConfig = ConfigV2 & BuildConfig & {
|
||||
export type CombinedConfig = ConfigV3 & BuildConfig & {
|
||||
registryTeams: Team[];
|
||||
appName: string;
|
||||
}
|
||||
|
@@ -4,7 +4,6 @@
|
||||
export type ServerFromURL = {
|
||||
name: string;
|
||||
url: URL;
|
||||
index: number;
|
||||
}
|
||||
|
||||
export type Boundaries = {
|
||||
|
Reference in New Issue
Block a user