[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.
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';

View File

@@ -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',

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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:

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.
// 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;
}
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)) {
return {name: teams[i].name, url: parsedServerUrl, index: i};
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: 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
@@ -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,

View File

@@ -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);
}
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}) => {
function cleanURL(url: string): string {
let updatedURL = url;
if (updatedURL.includes('\\')) {
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
}
return {name, url: updatedURL, order};
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((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));

View File

@@ -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;
}

View File

@@ -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)!);
}
});
});

View File

@@ -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),
}],

View File

@@ -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);
});

View File

@@ -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);
}
}
}

View File

@@ -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);
}
}
}

View File

@@ -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 = () => {

View File

@@ -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,24 +78,28 @@ 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);
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.server.name === server.name && recycle.server.url.toString() === urlUtils.parseURL(server.url)!.toString()) {
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.loadServer(server);
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);

View File

@@ -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

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 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;
}

View File

@@ -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}

View File

@@ -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,

View File

@@ -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'
>

View File

@@ -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,6 +257,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
</Droppable>
</DragDropContext>
<hr className='TeamDropdown__divider'/>
{this.state.enableServerManagement &&
<button
className='TeamDropdown__button addServer'
onClick={this.addServer}
@@ -257,6 +265,7 @@ class TeamDropdown extends React.PureComponent<Record<string, never>, State> {
<i className='icon-plus'/>
<span>{'Add a server'}</span>
</button>
}
</div>
);
}

View File

@@ -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}

View File

@@ -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;
}

View File

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