[MM-36432][MM-37072][MM-37073] Logic to support Focalboard and Playbooks tabs (#1680)
* Tab stuff * Inter-tab navigation * Close tab functionality * [MM-36342][MM-37072] Logic to support Focalboard and Playbooks tabs * Update to version 5.0 * Update config.yml * Updated routes * Update names for products * [MM-37073] Close unneeded tabs when not using v6.0 * Merge'd * Update config.yml * Update config.yml * Fix menu names * PR feedback * blank * blank * blank * PR feedback * Update config.yml * PR feedback Co-authored-by: Mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
@@ -554,6 +554,7 @@ workflows:
|
|||||||
branches:
|
branches:
|
||||||
only:
|
only:
|
||||||
- /^release-\d+(\.\d+){1,2}(-rc.*)?/
|
- /^release-\d+(\.\d+){1,2}(-rc.*)?/
|
||||||
|
- pull/1680
|
||||||
|
|
||||||
- store_artifacts:
|
- store_artifacts:
|
||||||
# for master/PR builds
|
# for master/PR builds
|
||||||
|
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "mattermost-desktop",
|
"name": "mattermost-desktop",
|
||||||
"version": "4.8.0-develop",
|
"version": "5.0.0-develop",
|
||||||
"lockfileVersion": 2,
|
"lockfileVersion": 2,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "mattermost-desktop",
|
"name": "mattermost-desktop",
|
||||||
"version": "4.8.0-develop",
|
"version": "5.0.0-develop",
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hapi/joi": "^16.1.8",
|
"@hapi/joi": "^16.1.8",
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "mattermost-desktop",
|
"name": "mattermost-desktop",
|
||||||
"productName": "Mattermost",
|
"productName": "Mattermost",
|
||||||
"version": "4.8.0-develop",
|
"version": "5.0.0-develop",
|
||||||
"description": "Mattermost",
|
"description": "Mattermost",
|
||||||
"main": "dist/index.js",
|
"main": "dist/index.js",
|
||||||
"author": "Mattermost, Inc. <feedback@mattermost.com>",
|
"author": "Mattermost, Inc. <feedback@mattermost.com>",
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
export const SWITCH_SERVER = 'switch-server';
|
export const SWITCH_SERVER = 'switch-server';
|
||||||
export const SWITCH_TAB = 'switch-tab';
|
export const SWITCH_TAB = 'switch-tab';
|
||||||
|
export const CLOSE_TAB = 'close-tab';
|
||||||
|
export const OPEN_TAB = 'open-tab';
|
||||||
export const SET_ACTIVE_VIEW = 'set-active-view';
|
export const SET_ACTIVE_VIEW = 'set-active-view';
|
||||||
export const MARK_READ = 'mark-read';
|
export const MARK_READ = 'mark-read';
|
||||||
export const FOCUS_BROWSERVIEW = 'focus-browserview';
|
export const FOCUS_BROWSERVIEW = 'focus-browserview';
|
||||||
@@ -92,3 +94,5 @@ export const UPDATE_DROPDOWN_MENTIONS = 'update-dropdown-mentions';
|
|||||||
export const REQUEST_TEAMS_DROPDOWN_INFO = 'request-teams-dropdown-info';
|
export const REQUEST_TEAMS_DROPDOWN_INFO = 'request-teams-dropdown-info';
|
||||||
export const RECEIVE_DROPDOWN_MENU_SIZE = 'receive-dropdown-menu-size';
|
export const RECEIVE_DROPDOWN_MENU_SIZE = 'receive-dropdown-menu-size';
|
||||||
export const SEND_DROPDOWN_MENU_SIZE = 'send-dropdown-menu-size';
|
export const SEND_DROPDOWN_MENU_SIZE = 'send-dropdown-menu-size';
|
||||||
|
|
||||||
|
export const BROWSER_HISTORY_PUSH = 'browser-history-push';
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {getFormattedPathName} from 'common/utils/url';
|
||||||
|
|
||||||
import BaseTabView from './BaseTabView';
|
import BaseTabView from './BaseTabView';
|
||||||
import {TabType, TAB_FOCALBOARD} from './TabView';
|
import {TabType, TAB_FOCALBOARD} from './TabView';
|
||||||
|
|
||||||
export default class FocalboardTabView extends BaseTabView {
|
export default class FocalboardTabView extends BaseTabView {
|
||||||
get url(): URL {
|
get url(): URL {
|
||||||
return this.server.url;
|
return new URL(`${this.server.url.origin}${getFormattedPathName(this.server.url.pathname)}boards`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get type(): TabType {
|
get type(): TabType {
|
||||||
|
@@ -1,12 +1,14 @@
|
|||||||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {getFormattedPathName} from 'common/utils/url';
|
||||||
|
|
||||||
import BaseTabView from './BaseTabView';
|
import BaseTabView from './BaseTabView';
|
||||||
import {TabType, TAB_PLAYBOOKS} from './TabView';
|
import {TabType, TAB_PLAYBOOKS} from './TabView';
|
||||||
|
|
||||||
export default class PlaybooksTabView extends BaseTabView {
|
export default class PlaybooksTabView extends BaseTabView {
|
||||||
get url(): URL {
|
get url(): URL {
|
||||||
return this.server.url;
|
return new URL(`${this.server.url.origin}${getFormattedPathName(this.server.url.pathname)}playbooks`);
|
||||||
}
|
}
|
||||||
|
|
||||||
get type(): TabType {
|
get type(): TabType {
|
||||||
|
@@ -29,14 +29,17 @@ export function getDefaultTeamWithTabsFromTeam(team: Team) {
|
|||||||
{
|
{
|
||||||
name: TAB_MESSAGING,
|
name: TAB_MESSAGING,
|
||||||
order: 0,
|
order: 0,
|
||||||
|
isClosed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: TAB_FOCALBOARD,
|
name: TAB_FOCALBOARD,
|
||||||
order: 1,
|
order: 1,
|
||||||
|
isClosed: false,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: TAB_PLAYBOOKS,
|
name: TAB_PLAYBOOKS,
|
||||||
order: 2,
|
order: 2,
|
||||||
|
isClosed: false,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
@@ -59,3 +62,21 @@ export function getServerView(srv: MattermostServer, tab: Tab) {
|
|||||||
export function getTabViewName(serverName: string, tabType: string) {
|
export function getTabViewName(serverName: string, tabType: string) {
|
||||||
return `${serverName}___${tabType}`;
|
return `${serverName}___${tabType}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getTabDisplayName(tabType: TabType) {
|
||||||
|
switch (tabType) {
|
||||||
|
case TAB_MESSAGING:
|
||||||
|
return 'Channels';
|
||||||
|
case TAB_FOCALBOARD:
|
||||||
|
return 'Boards';
|
||||||
|
case TAB_PLAYBOOKS:
|
||||||
|
return 'Playbooks';
|
||||||
|
default:
|
||||||
|
throw new Error('Not implemeneted');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function canCloseTab(tabType: TabType) {
|
||||||
|
// TODO: maybe rework to make the property belong to the class somehow
|
||||||
|
return tabType !== TAB_MESSAGING;
|
||||||
|
}
|
||||||
|
@@ -79,10 +79,14 @@ function getServerInfo(serverUrl: URL | string) {
|
|||||||
|
|
||||||
// does the server have a subpath?
|
// does the server have a subpath?
|
||||||
const pn = parsedServer.pathname.toLowerCase();
|
const pn = parsedServer.pathname.toLowerCase();
|
||||||
const subpath = pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
const subpath = getFormattedPathName(pn);
|
||||||
return {subpath, url: parsedServer};
|
return {subpath, url: parsedServer};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function getFormattedPathName(pn: string) {
|
||||||
|
return pn.endsWith('/') ? pn.toLowerCase() : `${pn}/`;
|
||||||
|
}
|
||||||
|
|
||||||
function getManagedResources() {
|
function getManagedResources() {
|
||||||
if (!buildConfig) {
|
if (!buildConfig) {
|
||||||
return [];
|
return [];
|
||||||
@@ -164,25 +168,24 @@ function getView(inputURL: URL | string, teams: TeamWithTabs[], ignoreScheme = f
|
|||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
let parsedServerUrl;
|
|
||||||
let firstOption;
|
let firstOption;
|
||||||
let secondOption;
|
let secondOption;
|
||||||
teams.forEach((team) => {
|
teams.forEach((team) => {
|
||||||
const srv = new MattermostServer(team.name, team.url);
|
const srv = new MattermostServer(team.name, team.url);
|
||||||
team.tabs.forEach((tab) => {
|
team.tabs.forEach((tab) => {
|
||||||
const tabView = getServerView(srv, tab);
|
const tabView = getServerView(srv, tab);
|
||||||
parsedServerUrl = parseURL(tabView.url);
|
const parsedServerUrl = parseURL(tabView.url);
|
||||||
if (parsedServerUrl) {
|
if (parsedServerUrl) {
|
||||||
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
// check server and subpath matches (without subpath pathname is \ so it always matches)
|
||||||
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
if (equalUrlsWithSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||||
firstOption = {name: tabView.name, url: parsedServerUrl};
|
firstOption = {name: tabView.name, url: parsedServerUrl.toString()};
|
||||||
}
|
}
|
||||||
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
if (equalUrlsIgnoringSubpath(parsedServerUrl, parsedURL, ignoreScheme)) {
|
||||||
// in case the user added something on the path that doesn't really belong to the server
|
// in case the user added something on the path that doesn't really belong to the server
|
||||||
// there might be more than one that matches, but we can't differentiate, so last one
|
// there might be more than one that matches, but we can't differentiate, so last one
|
||||||
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
|
// is as good as any other in case there is no better match (e.g.: two subpath servers with the same origin)
|
||||||
// e.g.: https://community.mattermost.com/core
|
// e.g.: https://community.mattermost.com/core
|
||||||
secondOption = {name: tabView.name, url: parsedServerUrl};
|
secondOption = {name: tabView.name, url: parsedServerUrl.toString()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -214,7 +217,8 @@ function isTrustedURL(url: URL | string, teams: TeamWithTabs[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: TeamWithTabs[]): boolean {
|
function isCustomLoginURL(url: URL | string, server: ServerFromURL, teams: TeamWithTabs[]): boolean {
|
||||||
const subpath = server ? server.url.pathname : '';
|
const serverURL = parseURL(server.url);
|
||||||
|
const subpath = server && serverURL ? serverURL.pathname : '';
|
||||||
const parsedURL = parseURL(url);
|
const parsedURL = parseURL(url);
|
||||||
if (!parsedURL) {
|
if (!parsedURL) {
|
||||||
return false;
|
return false;
|
||||||
|
@@ -37,8 +37,34 @@ function shorten(string: string, max?: number) {
|
|||||||
return string;
|
return string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isServerVersionGreaterThanOrEqualTo(currentVersion: string, compareVersion: string): boolean {
|
||||||
|
if (currentVersion === compareVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We only care about the numbers
|
||||||
|
const currentVersionNumber = (currentVersion || '').split('.').filter((x) => (/^[0-9]+$/).exec(x) !== null);
|
||||||
|
const compareVersionNumber = (compareVersion || '').split('.').filter((x) => (/^[0-9]+$/).exec(x) !== null);
|
||||||
|
|
||||||
|
for (let i = 0; i < Math.max(currentVersionNumber.length, compareVersionNumber.length); i++) {
|
||||||
|
const currentVersion = parseInt(currentVersionNumber[i], 10) || 0;
|
||||||
|
const compareVersion = parseInt(compareVersionNumber[i], 10) || 0;
|
||||||
|
if (currentVersion > compareVersion) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentVersion < compareVersion) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If all components are equal, then return true
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
getDisplayBoundaries,
|
getDisplayBoundaries,
|
||||||
runMode,
|
runMode,
|
||||||
shorten,
|
shorten,
|
||||||
|
isServerVersionGreaterThanOrEqualTo,
|
||||||
};
|
};
|
||||||
|
@@ -103,6 +103,7 @@ const configDataSchemaV3 = Joi.object<ConfigV3>({
|
|||||||
tabs: Joi.array().items(Joi.object({
|
tabs: Joi.array().items(Joi.object({
|
||||||
name: Joi.string().required(),
|
name: Joi.string().required(),
|
||||||
order: Joi.number().integer().min(0),
|
order: Joi.number().integer().min(0),
|
||||||
|
isClosed: Joi.boolean().default(false),
|
||||||
})).default([]),
|
})).default([]),
|
||||||
})).default([]),
|
})).default([]),
|
||||||
showTrayIcon: Joi.boolean().default(false),
|
showTrayIcon: Joi.boolean().default(false),
|
||||||
|
@@ -13,10 +13,9 @@ import installExtension, {REACT_DEVELOPER_TOOLS} from 'electron-devtools-install
|
|||||||
import log from 'electron-log';
|
import log from 'electron-log';
|
||||||
import 'airbnb-js-shims/target/es2015';
|
import 'airbnb-js-shims/target/es2015';
|
||||||
|
|
||||||
import {Team} from 'types/config';
|
import {Team, TeamWithTabs} from 'types/config';
|
||||||
|
|
||||||
import {MentionData} from 'types/notification';
|
import {MentionData} from 'types/notification';
|
||||||
|
import {RemoteInfo} from 'types/server';
|
||||||
import {Boundaries} from 'types/utils';
|
import {Boundaries} from 'types/utils';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -37,14 +36,17 @@ import {
|
|||||||
USER_ACTIVITY_UPDATE,
|
USER_ACTIVITY_UPDATE,
|
||||||
EMIT_CONFIGURATION,
|
EMIT_CONFIGURATION,
|
||||||
SWITCH_TAB,
|
SWITCH_TAB,
|
||||||
|
CLOSE_TAB,
|
||||||
|
OPEN_TAB,
|
||||||
SHOW_EDIT_SERVER_MODAL,
|
SHOW_EDIT_SERVER_MODAL,
|
||||||
SHOW_REMOVE_SERVER_MODAL,
|
SHOW_REMOVE_SERVER_MODAL,
|
||||||
UPDATE_SHORTCUT_MENU,
|
UPDATE_SHORTCUT_MENU,
|
||||||
OPEN_TEAMS_DROPDOWN,
|
OPEN_TEAMS_DROPDOWN,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
import {getDefaultTeamWithTabsFromTeam} from 'common/tabs/TabView';
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
import Utils from 'common/utils/util';
|
import {getDefaultTeamWithTabsFromTeam, TAB_FOCALBOARD, TAB_MESSAGING, TAB_PLAYBOOKS} from 'common/tabs/TabView';
|
||||||
|
import Utils, {isServerVersionGreaterThanOrEqualTo} from 'common/utils/util';
|
||||||
|
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
@@ -71,6 +73,7 @@ import {destroyTray, refreshTrayImages, setTrayMenu, setupTray} from './tray/tra
|
|||||||
import {AuthManager} from './authManager';
|
import {AuthManager} from './authManager';
|
||||||
import {CertificateManager} from './certificateManager';
|
import {CertificateManager} from './certificateManager';
|
||||||
import {setupBadge, setUnreadBadgeSetting} from './badge';
|
import {setupBadge, setUnreadBadgeSetting} from './badge';
|
||||||
|
import {ServerInfo} from './server/serverInfo';
|
||||||
|
|
||||||
if (process.env.NODE_ENV !== 'production' && module.hot) {
|
if (process.env.NODE_ENV !== 'production' && module.hot) {
|
||||||
module.hot.accept();
|
module.hot.accept();
|
||||||
@@ -245,6 +248,8 @@ function initializeInterCommunicationEventListeners() {
|
|||||||
|
|
||||||
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
ipcMain.on(SWITCH_SERVER, handleSwitchServer);
|
||||||
ipcMain.on(SWITCH_TAB, handleSwitchTab);
|
ipcMain.on(SWITCH_TAB, handleSwitchTab);
|
||||||
|
ipcMain.on(CLOSE_TAB, handleCloseTab);
|
||||||
|
ipcMain.on(OPEN_TAB, handleOpenTab);
|
||||||
|
|
||||||
ipcMain.on(QUIT, handleQuit);
|
ipcMain.on(QUIT, handleQuit);
|
||||||
|
|
||||||
@@ -493,6 +498,37 @@ function handleSwitchTab(event: IpcMainEvent, serverName: string, tabName: strin
|
|||||||
WindowManager.switchTab(serverName, tabName);
|
WindowManager.switchTab(serverName, tabName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleCloseTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||||
|
const teams = config.teams;
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (team.name === serverName) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name === tabName) {
|
||||||
|
tab.isClosed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const nextTab = teams.find((team) => team.name === serverName)!.tabs.filter((tab) => !tab.isClosed)[0].name;
|
||||||
|
WindowManager.switchTab(serverName, nextTab);
|
||||||
|
config.set('teams', teams);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleOpenTab(event: IpcMainEvent, serverName: string, tabName: string) {
|
||||||
|
const teams = config.teams;
|
||||||
|
teams.forEach((team) => {
|
||||||
|
if (team.name === serverName) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name === tabName) {
|
||||||
|
tab.isClosed = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
WindowManager.switchTab(serverName, tabName);
|
||||||
|
config.set('teams', teams);
|
||||||
|
}
|
||||||
|
|
||||||
function handleNewServerModal() {
|
function handleNewServerModal() {
|
||||||
const html = getLocalURLString('newServer.html');
|
const html = getLocalURLString('newServer.html');
|
||||||
|
|
||||||
@@ -507,8 +543,10 @@ function handleNewServerModal() {
|
|||||||
modalPromise.then((data) => {
|
modalPromise.then((data) => {
|
||||||
const teams = config.teams;
|
const teams = config.teams;
|
||||||
const order = teams.length;
|
const order = teams.length;
|
||||||
teams.push(getDefaultTeamWithTabsFromTeam({...data, order}));
|
const newTeam = getDefaultTeamWithTabsFromTeam({...data, order});
|
||||||
|
teams.push(newTeam);
|
||||||
config.set('teams', teams);
|
config.set('teams', teams);
|
||||||
|
updateServerInfos([newTeam]);
|
||||||
}).catch((e) => {
|
}).catch((e) => {
|
||||||
// e is undefined for user cancellation
|
// e is undefined for user cancellation
|
||||||
if (e) {
|
if (e) {
|
||||||
@@ -590,6 +628,7 @@ function handleRemoveServerModal(e: IpcMainEvent, name: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initializeAfterAppReady() {
|
function initializeAfterAppReady() {
|
||||||
|
updateServerInfos(config.teams);
|
||||||
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
app.setAppUserModelId('Mattermost.Desktop'); // Use explicit AppUserModelID
|
||||||
const defaultSession = session.defaultSession;
|
const defaultSession = session.defaultSession;
|
||||||
|
|
||||||
@@ -751,6 +790,41 @@ function handleMentionNotification(event: IpcMainEvent, title: string, body: str
|
|||||||
displayMention(title, body, channel, teamId, url, silent, event.sender, data);
|
displayMention(title, body, channel, teamId, url, silent, event.sender, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function updateServerInfos(teams: TeamWithTabs[]) {
|
||||||
|
const serverInfos: Array<Promise<RemoteInfo | string | undefined>> = [];
|
||||||
|
teams.forEach((team) => {
|
||||||
|
const serverInfo = new ServerInfo(new MattermostServer(team.name, team.url));
|
||||||
|
serverInfos.push(serverInfo.promise);
|
||||||
|
});
|
||||||
|
Promise.all(serverInfos).then((data: Array<RemoteInfo | string | undefined>) => {
|
||||||
|
const teams = config.teams;
|
||||||
|
teams.forEach((team) => closeUnneededTabs(data, team));
|
||||||
|
config.set('teams', teams);
|
||||||
|
}).catch((reason: any) => {
|
||||||
|
log.error('Error getting server infos', reason);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeUnneededTabs(data: Array<RemoteInfo | string | undefined>, team: TeamWithTabs) {
|
||||||
|
const remoteInfo = data.find((info) => info && typeof info !== 'string' && info.name === team.name) as RemoteInfo;
|
||||||
|
if (remoteInfo) {
|
||||||
|
team.tabs.forEach((tab) => {
|
||||||
|
if (tab.name === TAB_PLAYBOOKS && !remoteInfo.hasPlaybooks) {
|
||||||
|
log.info(`closing ${team.name}___${tab.name} on !hasPlaybooks`);
|
||||||
|
tab.isClosed = true;
|
||||||
|
}
|
||||||
|
if (tab.name === TAB_FOCALBOARD && !remoteInfo.hasFocalboard) {
|
||||||
|
log.info(`closing ${team.name}___${tab.name} on !hasFocalboard`);
|
||||||
|
tab.isClosed = true;
|
||||||
|
}
|
||||||
|
if (tab.name !== TAB_MESSAGING && remoteInfo.serverVersion && !isServerVersionGreaterThanOrEqualTo(remoteInfo.serverVersion, '6.0.0')) {
|
||||||
|
log.info(`closing ${team.name}___${tab.name} on !serverVersion`);
|
||||||
|
tab.isClosed = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function handleOpenAppMenu() {
|
function handleOpenAppMenu() {
|
||||||
const windowMenu = Menu.getApplicationMenu();
|
const windowMenu = Menu.getApplicationMenu();
|
||||||
if (!windowMenu) {
|
if (!windowMenu) {
|
||||||
|
@@ -7,6 +7,7 @@ import {app, ipcMain, Menu, MenuItemConstructorOptions, MenuItem, session, shell
|
|||||||
|
|
||||||
import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
import {SHOW_NEW_SERVER_MODAL} from 'common/communication';
|
||||||
import Config from 'common/config';
|
import Config from 'common/config';
|
||||||
|
import {TabType, getTabDisplayName} from 'common/tabs/TabView';
|
||||||
|
|
||||||
import * as WindowManager from '../windows/windowManager';
|
import * as WindowManager from '../windows/windowManager';
|
||||||
|
|
||||||
@@ -217,7 +218,7 @@ function createTemplate(config: Config) {
|
|||||||
if (WindowManager.getCurrentTeamName() === team.name) {
|
if (WindowManager.getCurrentTeamName() === team.name) {
|
||||||
team.tabs.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).forEach((tab, i) => {
|
team.tabs.slice(0, 9).sort((teamA, teamB) => teamA.order - teamB.order).forEach((tab, i) => {
|
||||||
items.push({
|
items.push({
|
||||||
label: ` ${tab.name}`, // TODO
|
label: ` ${getTabDisplayName(tab.name as TabType)}`,
|
||||||
accelerator: `CmdOrCtrl+${i + 1}`,
|
accelerator: `CmdOrCtrl+${i + 1}`,
|
||||||
click() {
|
click() {
|
||||||
WindowManager.switchTab(team.name, tab.name);
|
WindowManager.switchTab(team.name, tab.name);
|
||||||
|
@@ -12,7 +12,17 @@ import {ipcRenderer, webFrame} from 'electron';
|
|||||||
// we'll be able to use it again if there is a workaround for the 'os' import
|
// we'll be able to use it again if there is a workaround for the 'os' import
|
||||||
//import log from 'electron-log';
|
//import log from 'electron-log';
|
||||||
|
|
||||||
import {NOTIFY_MENTION, IS_UNREAD, UNREAD_RESULT, SESSION_EXPIRED, SET_VIEW_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,
|
||||||
|
BROWSER_HISTORY_PUSH,
|
||||||
|
} from 'common/communication';
|
||||||
|
|
||||||
const UNREAD_COUNT_INTERVAL = 1000;
|
const UNREAD_COUNT_INTERVAL = 1000;
|
||||||
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
const CLEAR_CACHE_INTERVAL = 6 * 60 * 60 * 1000; // 6 hours
|
||||||
@@ -114,6 +124,11 @@ window.addEventListener('message', ({origin, data = {}} = {}) => {
|
|||||||
ipcRenderer.send(NOTIFY_MENTION, title, body, channel, teamId, url, silent, messageData);
|
ipcRenderer.send(NOTIFY_MENTION, title, body, channel, teamId, url, silent, messageData);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case 'browser-history-push': {
|
||||||
|
const {path} = message;
|
||||||
|
ipcRenderer.send(BROWSER_HISTORY_PUSH, viewName, path);
|
||||||
|
break;
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
if (typeof type === 'undefined') {
|
if (typeof type === 'undefined') {
|
||||||
console.log('ignoring message of undefined type:');
|
console.log('ignoring message of undefined type:');
|
||||||
@@ -211,4 +226,16 @@ window.addEventListener('click', () => {
|
|||||||
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
|
ipcRenderer.send(CLOSE_TEAMS_DROPDOWN);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
ipcRenderer.on(BROWSER_HISTORY_PUSH, (event, pathName) => {
|
||||||
|
window.postMessage(
|
||||||
|
{
|
||||||
|
type: 'browser-history-push-return',
|
||||||
|
message: {
|
||||||
|
pathName,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
/* eslint-enable no-magic-numbers */
|
/* eslint-enable no-magic-numbers */
|
||||||
|
55
src/main/server/serverAPI.ts
Normal file
55
src/main/server/serverAPI.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {net, session} from 'electron';
|
||||||
|
import log from 'electron-log';
|
||||||
|
|
||||||
|
export async function getServerAPI<T>(url: URL, isAuthenticated: boolean, onSuccess?: (data: T) => void, onAbort?: () => void, onError?: (error: Error) => void) {
|
||||||
|
if (isAuthenticated) {
|
||||||
|
const cookies = await session.defaultSession.cookies.get({});
|
||||||
|
if (!cookies) {
|
||||||
|
log.error('Cannot authenticate, no cookies present');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out cookies that aren't part of our domain
|
||||||
|
const filteredCookies = cookies.filter((cookie) => cookie.domain && url.toString().indexOf(cookie.domain) >= 0);
|
||||||
|
|
||||||
|
const userId = filteredCookies.find((cookie) => cookie.name === 'MMUSERID');
|
||||||
|
const csrf = filteredCookies.find((cookie) => cookie.name === 'MMCSRF');
|
||||||
|
const authToken = filteredCookies.find((cookie) => cookie.name === 'MMAUTHTOKEN');
|
||||||
|
|
||||||
|
if (!userId || !csrf || !authToken) {
|
||||||
|
// Missing cookies needed for req
|
||||||
|
log.error(`Cannot authenticate, required cookies for ${url.origin} not found`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const req = net.request({
|
||||||
|
url: url.toString(),
|
||||||
|
session: session.defaultSession,
|
||||||
|
useSessionCookies: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (onSuccess) {
|
||||||
|
req.on('response', (response: Electron.IncomingMessage) => {
|
||||||
|
if (response.statusCode === 200) {
|
||||||
|
response.on('data', (chunk: Buffer) => {
|
||||||
|
const raw = `${chunk}`;
|
||||||
|
const data = JSON.parse(raw) as T;
|
||||||
|
onSuccess(data);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
onError?.(new Error(`Bad status code requesting from ${url.toString()}`));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (onAbort) {
|
||||||
|
req.on('abort', onAbort);
|
||||||
|
}
|
||||||
|
if (onError) {
|
||||||
|
req.on('error', onError);
|
||||||
|
}
|
||||||
|
req.end();
|
||||||
|
}
|
68
src/main/server/serverInfo.ts
Normal file
68
src/main/server/serverInfo.ts
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
import {RemoteInfo} from 'types/server';
|
||||||
|
|
||||||
|
import {MattermostServer} from 'common/servers/MattermostServer';
|
||||||
|
|
||||||
|
import {getServerAPI} from './serverAPI';
|
||||||
|
|
||||||
|
export class ServerInfo {
|
||||||
|
server: MattermostServer;
|
||||||
|
remoteInfo: RemoteInfo;
|
||||||
|
promise: Promise<RemoteInfo | string | undefined>;
|
||||||
|
onRetrievedRemoteInfo?: (result?: RemoteInfo | string) => void;
|
||||||
|
|
||||||
|
constructor(server: MattermostServer) {
|
||||||
|
this.server = server;
|
||||||
|
this.remoteInfo = {name: server.name};
|
||||||
|
|
||||||
|
this.promise = new Promise<RemoteInfo | string | undefined>((resolve) => {
|
||||||
|
this.onRetrievedRemoteInfo = resolve;
|
||||||
|
});
|
||||||
|
this.getRemoteInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
getRemoteInfo = () => {
|
||||||
|
getServerAPI<{Version: string}>(
|
||||||
|
new URL(`${this.server.url.toString()}/api/v4/config/client?format=old`),
|
||||||
|
false,
|
||||||
|
this.onGetConfig,
|
||||||
|
this.onRetrievedRemoteInfo,
|
||||||
|
this.onRetrievedRemoteInfo);
|
||||||
|
|
||||||
|
getServerAPI<Array<{id: string; version: string}>>(
|
||||||
|
new URL(`${this.server.url.toString()}/api/v4/plugins/webapp`),
|
||||||
|
false,
|
||||||
|
this.onGetPlugins,
|
||||||
|
this.onRetrievedRemoteInfo,
|
||||||
|
this.onRetrievedRemoteInfo);
|
||||||
|
}
|
||||||
|
|
||||||
|
onGetConfig = (data: {Version: string}) => {
|
||||||
|
this.remoteInfo.serverVersion = data.Version;
|
||||||
|
|
||||||
|
this.trySendRemoteInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
onGetPlugins = (data: Array<{id: string; version: string}>) => {
|
||||||
|
this.remoteInfo.hasFocalboard = data.some((plugin) => plugin.id === 'focalboard');
|
||||||
|
this.remoteInfo.hasPlaybooks = data.some((plugin) => plugin.id === 'com.mattermost.plugin-incident-management');
|
||||||
|
|
||||||
|
this.trySendRemoteInfo();
|
||||||
|
}
|
||||||
|
|
||||||
|
trySendRemoteInfo = () => {
|
||||||
|
if (this.isRemoteInfoRetrieved()) {
|
||||||
|
this.onRetrievedRemoteInfo?.(this.remoteInfo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
isRemoteInfoRetrieved = () => {
|
||||||
|
return !(
|
||||||
|
typeof this.remoteInfo.serverVersion === 'undefined' ||
|
||||||
|
typeof this.remoteInfo.hasFocalboard === 'undefined' ||
|
||||||
|
typeof this.remoteInfo.hasPlaybooks === 'undefined'
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -57,7 +57,6 @@ export class MattermostView extends EventEmitter {
|
|||||||
usesAsteriskForUnreads?: boolean;
|
usesAsteriskForUnreads?: boolean;
|
||||||
|
|
||||||
currentFavicon?: string;
|
currentFavicon?: string;
|
||||||
isInitialized: boolean;
|
|
||||||
hasBeenShown: boolean;
|
hasBeenShown: boolean;
|
||||||
altLastPressed?: boolean;
|
altLastPressed?: boolean;
|
||||||
contextMenu: ContextMenu;
|
contextMenu: ContextMenu;
|
||||||
@@ -90,7 +89,6 @@ export class MattermostView extends EventEmitter {
|
|||||||
|
|
||||||
log.info(`BrowserView created for server ${this.tab.name}`);
|
log.info(`BrowserView created for server ${this.tab.name}`);
|
||||||
|
|
||||||
this.isInitialized = false;
|
|
||||||
this.hasBeenShown = false;
|
this.hasBeenShown = false;
|
||||||
|
|
||||||
if (process.platform !== 'darwin') {
|
if (process.platform !== 'darwin') {
|
||||||
@@ -98,6 +96,10 @@ export class MattermostView extends EventEmitter {
|
|||||||
this.view.webContents.on('before-input-event', this.handleInputEvents);
|
this.view.webContents.on('before-input-event', this.handleInputEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.view.webContents.on('did-finish-load', () => {
|
||||||
|
this.view.webContents.send(SET_VIEW_NAME, this.tab.name);
|
||||||
|
});
|
||||||
|
|
||||||
this.contextMenu = new ContextMenu({}, this.view);
|
this.contextMenu = new ContextMenu({}, this.view);
|
||||||
this.maxRetries = MAX_SERVER_RETRIES;
|
this.maxRetries = MAX_SERVER_RETRIES;
|
||||||
}
|
}
|
||||||
@@ -179,7 +181,6 @@ export class MattermostView extends EventEmitter {
|
|||||||
this.status = Status.WAITING_MM;
|
this.status = Status.WAITING_MM;
|
||||||
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
this.removeLoading = setTimeout(this.setInitialized, MAX_LOADING_SCREEN_SECONDS, true);
|
||||||
this.emit(LOAD_SUCCESS, this.tab.name, loadURL);
|
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()))));
|
this.setBounds(getWindowBoundaries(this.window, !(urlUtils.isTeamUrl(this.tab.url || '', this.view.webContents.getURL()) || urlUtils.isAdminUrl(this.tab.url || '', this.view.webContents.getURL()))));
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -259,6 +260,10 @@ export class MattermostView extends EventEmitter {
|
|||||||
delete this.removeLoading;
|
delete this.removeLoading;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isInitialized = () => {
|
||||||
|
return this.status === Status.READY;
|
||||||
|
}
|
||||||
|
|
||||||
openDevTools = () => {
|
openDevTools = () => {
|
||||||
this.view.webContents.openDevTools({mode: 'detach'});
|
this.view.webContents.openDevTools({mode: 'detach'});
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
GET_LOADING_SCREEN_DATA,
|
GET_LOADING_SCREEN_DATA,
|
||||||
LOADSCREEN_END,
|
LOADSCREEN_END,
|
||||||
SET_ACTIVE_VIEW,
|
SET_ACTIVE_VIEW,
|
||||||
|
OPEN_TAB,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
@@ -33,6 +34,7 @@ const URL_VIEW_HEIGHT = 36;
|
|||||||
export class ViewManager {
|
export class ViewManager {
|
||||||
configServers: TeamWithTabs[];
|
configServers: TeamWithTabs[];
|
||||||
viewOptions: BrowserViewConstructorOptions;
|
viewOptions: BrowserViewConstructorOptions;
|
||||||
|
closedViews: Map<string, {srv: MattermostServer; tab: Tab}>;
|
||||||
views: Map<string, MattermostView>;
|
views: Map<string, MattermostView>;
|
||||||
currentView?: string;
|
currentView?: string;
|
||||||
urlView?: BrowserView;
|
urlView?: BrowserView;
|
||||||
@@ -45,6 +47,7 @@ export class ViewManager {
|
|||||||
this.viewOptions = {webPreferences: {spellcheck: config.useSpellChecker}};
|
this.viewOptions = {webPreferences: {spellcheck: config.useSpellChecker}};
|
||||||
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
this.views = new Map(); // keep in mind that this doesn't need to hold server order, only tabs on the renderer need that.
|
||||||
this.mainWindow = mainWindow;
|
this.mainWindow = mainWindow;
|
||||||
|
this.closedViews = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
updateMainWindow = (mainWindow: BrowserWindow) => {
|
updateMainWindow = (mainWindow: BrowserWindow) => {
|
||||||
@@ -60,15 +63,19 @@ export class ViewManager {
|
|||||||
server.tabs.forEach((tab) => this.loadView(srv, tab));
|
server.tabs.forEach((tab) => this.loadView(srv, tab));
|
||||||
}
|
}
|
||||||
|
|
||||||
loadView = (srv: MattermostServer, tab: Tab) => {
|
loadView = (srv: MattermostServer, tab: Tab, url?: string) => {
|
||||||
const tabView = getServerView(srv, tab);
|
const tabView = getServerView(srv, tab);
|
||||||
|
if (tab.isClosed) {
|
||||||
|
this.closedViews.set(tabView.name, {srv, tab});
|
||||||
|
return;
|
||||||
|
}
|
||||||
const view = new MattermostView(tabView, this.mainWindow, this.viewOptions);
|
const view = new MattermostView(tabView, this.mainWindow, this.viewOptions);
|
||||||
this.views.set(tabView.name, view);
|
this.views.set(tabView.name, view);
|
||||||
if (!this.loadingScreen) {
|
if (!this.loadingScreen) {
|
||||||
this.createLoadingScreen();
|
this.createLoadingScreen();
|
||||||
}
|
}
|
||||||
view.once(LOAD_SUCCESS, this.activateView);
|
view.once(LOAD_SUCCESS, this.activateView);
|
||||||
view.load();
|
view.load(url);
|
||||||
view.on(UPDATE_TARGET_URL, this.showURLView);
|
view.on(UPDATE_TARGET_URL, this.showURLView);
|
||||||
view.on(LOADSCREEN_END, this.finishLoading);
|
view.on(LOADSCREEN_END, this.finishLoading);
|
||||||
view.once(LOAD_FAILED, this.failLoading);
|
view.once(LOAD_FAILED, this.failLoading);
|
||||||
@@ -92,7 +99,9 @@ export class ViewManager {
|
|||||||
if (recycle && recycle.isVisible) {
|
if (recycle && recycle.isVisible) {
|
||||||
setFocus = recycle.name;
|
setFocus = recycle.name;
|
||||||
}
|
}
|
||||||
if (recycle && recycle.tab.name === tabView.name && recycle.tab.url.toString() === urlUtils.parseURL(tabView.url)!.toString()) {
|
if (tab.isClosed) {
|
||||||
|
this.closedViews.set(tabView.name, {srv, tab});
|
||||||
|
} else if (recycle && recycle.tab.name === tabView.name && recycle.tab.url.toString() === urlUtils.parseURL(tabView.url)!.toString()) {
|
||||||
oldviews.delete(recycle.name);
|
oldviews.delete(recycle.name);
|
||||||
this.views.set(recycle.name, recycle);
|
this.views.set(recycle.name, recycle);
|
||||||
} else {
|
} else {
|
||||||
@@ -114,7 +123,8 @@ export class ViewManager {
|
|||||||
if (this.configServers.length) {
|
if (this.configServers.length) {
|
||||||
const element = this.configServers.find((e) => e.order === 0);
|
const element = this.configServers.find((e) => e.order === 0);
|
||||||
if (element) {
|
if (element) {
|
||||||
const tab = element.tabs.find((e) => e.order === 0);
|
const openTabs = element.tabs.filter((tab) => !tab.isClosed);
|
||||||
|
const tab = openTabs.find((e) => e.order === 0) || openTabs[0];
|
||||||
if (tab) {
|
if (tab) {
|
||||||
const tabView = getTabViewName(element.name, tab.name);
|
const tabView = getTabViewName(element.name, tab.name);
|
||||||
this.showByName(tabView);
|
this.showByName(tabView);
|
||||||
@@ -193,6 +203,24 @@ export class ViewManager {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
openClosedTab = (name: string, url?: string) => {
|
||||||
|
if (!this.closedViews.has(name)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const {srv, tab} = this.closedViews.get(name)!;
|
||||||
|
tab.isClosed = false;
|
||||||
|
this.closedViews.delete(name);
|
||||||
|
this.loadView(srv, tab, url);
|
||||||
|
this.showByName(name);
|
||||||
|
const view = this.views.get(name)!;
|
||||||
|
view.isVisible = true;
|
||||||
|
view.on(LOAD_SUCCESS, () => {
|
||||||
|
view.isVisible = false;
|
||||||
|
this.showByName(name);
|
||||||
|
});
|
||||||
|
ipcMain.emit(OPEN_TAB, null, srv.name, tab.name);
|
||||||
|
}
|
||||||
|
|
||||||
failLoading = () => {
|
failLoading = () => {
|
||||||
this.fadeLoadingScreen();
|
this.fadeLoadingScreen();
|
||||||
}
|
}
|
||||||
@@ -360,18 +388,22 @@ export class ViewManager {
|
|||||||
const parsedURL = urlUtils.parseURL(url)!;
|
const parsedURL = urlUtils.parseURL(url)!;
|
||||||
const tabView = urlUtils.getView(parsedURL, this.configServers, true);
|
const tabView = urlUtils.getView(parsedURL, this.configServers, true);
|
||||||
if (tabView) {
|
if (tabView) {
|
||||||
const view = this.views.get(tabView.name);
|
const urlWithSchema = `${urlUtils.parseURL(tabView.url)?.origin}${parsedURL.pathname}${parsedURL.search}`;
|
||||||
if (!view) {
|
if (this.closedViews.has(tabView.name)) {
|
||||||
log.error(`Couldn't find a view matching the name ${tabView.name}`);
|
this.openClosedTab(tabView.name, urlWithSchema);
|
||||||
return;
|
} else {
|
||||||
}
|
const view = this.views.get(tabView.name);
|
||||||
|
if (!view) {
|
||||||
|
log.error(`Couldn't find a view matching the name ${tabView.name}`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// attempting to change parsedURL protocol results in it not being modified.
|
// attempting to change parsedURL protocol results in it not being modified.
|
||||||
const urlWithSchema = `${view.tab.url.origin}${parsedURL.pathname}${parsedURL.search}`;
|
view.resetLoadingStatus();
|
||||||
view.resetLoadingStatus();
|
view.load(urlWithSchema);
|
||||||
view.load(urlWithSchema);
|
view.once(LOAD_SUCCESS, this.deeplinkSuccess);
|
||||||
view.once(LOAD_SUCCESS, this.deeplinkSuccess);
|
view.once(LOAD_FAILED, this.deeplinkFailed);
|
||||||
view.once(LOAD_FAILED, this.deeplinkFailed);
|
}
|
||||||
} else {
|
} else {
|
||||||
dialog.showErrorBox('No matching server', `there is no configured server in the app that matches the requested url: ${parsedURL.toString()}`);
|
dialog.showErrorBox('No matching server', `there is no configured server in the app that matches the requested url: ${parsedURL.toString()}`);
|
||||||
}
|
}
|
||||||
|
@@ -74,9 +74,11 @@ const generateDidStartNavigation = (getServersFunction: () => TeamWithTabs[]) =>
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const serverURL = urlUtils.parseURL(server?.url || '');
|
||||||
|
|
||||||
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
if (server && urlUtils.isCustomLoginURL(parsedURL, server, serverList)) {
|
||||||
customLogins[contentID].inProgress = true;
|
customLogins[contentID].inProgress = true;
|
||||||
} else if (server && customLogins[contentID].inProgress && urlUtils.isInternalURL(server.url, parsedURL)) {
|
} else if (server && customLogins[contentID].inProgress && urlUtils.isInternalURL(serverURL || new URL(''), parsedURL)) {
|
||||||
customLogins[contentID].inProgress = false;
|
customLogins[contentID].inProgress = false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -16,6 +16,7 @@ import {
|
|||||||
FOCUS_THREE_DOT_MENU,
|
FOCUS_THREE_DOT_MENU,
|
||||||
GET_DARK_MODE,
|
GET_DARK_MODE,
|
||||||
UPDATE_SHORTCUT_MENU,
|
UPDATE_SHORTCUT_MENU,
|
||||||
|
BROWSER_HISTORY_PUSH,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
import urlUtils from 'common/utils/url';
|
import urlUtils from 'common/utils/url';
|
||||||
|
|
||||||
@@ -50,6 +51,7 @@ ipcMain.handle(GET_LOADING_SCREEN_DATA, handleLoadingScreenDataRequest);
|
|||||||
ipcMain.handle(GET_DARK_MODE, handleGetDarkMode);
|
ipcMain.handle(GET_DARK_MODE, handleGetDarkMode);
|
||||||
ipcMain.on(REACT_APP_INITIALIZED, handleReactAppInitialized);
|
ipcMain.on(REACT_APP_INITIALIZED, handleReactAppInitialized);
|
||||||
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, handleLoadingScreenAnimationFinished);
|
ipcMain.on(LOADING_SCREEN_ANIMATION_FINISHED, handleLoadingScreenAnimationFinished);
|
||||||
|
ipcMain.on(BROWSER_HISTORY_PUSH, handleBrowserHistoryPush);
|
||||||
|
|
||||||
export function setConfig(data: CombinedConfig) {
|
export function setConfig(data: CombinedConfig) {
|
||||||
if (data) {
|
if (data) {
|
||||||
@@ -480,15 +482,21 @@ export function selectNextTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||||
|
const filteredTabs = currentTeamTabs?.filter((tab) => !tab.isClosed);
|
||||||
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
||||||
if (!currentTeamTabs || !currentTab) {
|
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentOrder = currentTab.order;
|
let currentOrder = currentTab.order;
|
||||||
const nextOrder = ((currentOrder + 1) % currentTeamTabs.length);
|
let nextIndex = -1;
|
||||||
const nextIndex = currentTeamTabs.findIndex((tab) => tab.order === nextOrder);
|
while (nextIndex === -1) {
|
||||||
const newTab = currentTeamTabs[nextIndex];
|
const nextOrder = ((currentOrder + 1) % currentTeamTabs.length);
|
||||||
|
nextIndex = filteredTabs.findIndex((tab) => tab.order === nextOrder);
|
||||||
|
currentOrder = nextOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = filteredTabs[nextIndex];
|
||||||
switchTab(currentView.tab.server.name, newTab.name);
|
switchTab(currentView.tab.server.name, newTab.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -499,17 +507,22 @@ export function selectPreviousTab() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
const currentTeamTabs = status.config?.teams.find((team) => team.name === currentView.tab.server.name)?.tabs;
|
||||||
|
const filteredTabs = currentTeamTabs?.filter((tab) => !tab.isClosed);
|
||||||
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
const currentTab = currentTeamTabs?.find((tab) => tab.name === currentView.tab.type);
|
||||||
if (!currentTeamTabs || !currentTab) {
|
if (!currentTeamTabs || !currentTab || !filteredTabs) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentOrder = currentTab.order;
|
|
||||||
|
|
||||||
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
|
// 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);
|
let currentOrder = currentTab.order;
|
||||||
const nextIndex = currentTeamTabs.findIndex((tab) => tab.order === nextOrder);
|
let nextIndex = -1;
|
||||||
const newTab = currentTeamTabs[nextIndex];
|
while (nextIndex === -1) {
|
||||||
|
const nextOrder = ((currentTeamTabs.length + (currentOrder - 1)) % currentTeamTabs.length);
|
||||||
|
nextIndex = filteredTabs.findIndex((tab) => tab.order === nextOrder);
|
||||||
|
currentOrder = nextOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newTab = filteredTabs[nextIndex];
|
||||||
switchTab(currentView.tab.server.name, newTab.name);
|
switchTab(currentView.tab.server.name, newTab.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -517,6 +530,20 @@ function handleGetDarkMode() {
|
|||||||
return status.config?.darkMode;
|
return status.config?.darkMode;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleBrowserHistoryPush(e: IpcMainEvent, viewName: string, pathName: string) {
|
||||||
|
const currentView = status.viewManager?.views.get(viewName);
|
||||||
|
const redirectedViewName = urlUtils.getView(`${currentView?.tab.server.url}${pathName}`, status.config!.teams)?.name || viewName;
|
||||||
|
if (status.viewManager?.closedViews.has(redirectedViewName)) {
|
||||||
|
status.viewManager.openClosedTab(redirectedViewName, `${currentView?.tab.server.url}${pathName}`);
|
||||||
|
}
|
||||||
|
const redirectedView = status.viewManager?.views.get(redirectedViewName) || currentView;
|
||||||
|
if (redirectedView !== currentView) {
|
||||||
|
log.info('redirecting to a new view', redirectedView?.name || viewName);
|
||||||
|
status.viewManager?.showByName(redirectedView?.name || viewName);
|
||||||
|
}
|
||||||
|
redirectedView?.view.webContents.send(BROWSER_HISTORY_PUSH, pathName);
|
||||||
|
}
|
||||||
|
|
||||||
export function getCurrentTeamName() {
|
export function getCurrentTeamName() {
|
||||||
return status.currentServerName;
|
return status.currentServerName;
|
||||||
}
|
}
|
||||||
|
@@ -36,6 +36,7 @@ import {
|
|||||||
CLOSE_TEAMS_DROPDOWN,
|
CLOSE_TEAMS_DROPDOWN,
|
||||||
OPEN_TEAMS_DROPDOWN,
|
OPEN_TEAMS_DROPDOWN,
|
||||||
SWITCH_TAB,
|
SWITCH_TAB,
|
||||||
|
CLOSE_TAB,
|
||||||
} from 'common/communication';
|
} from 'common/communication';
|
||||||
|
|
||||||
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
||||||
@@ -236,6 +237,10 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||||||
window.ipcRenderer.send(SWITCH_TAB, this.state.activeServerName, name);
|
window.ipcRenderer.send(SWITCH_TAB, this.state.activeServerName, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
handleCloseTab = (name: string) => {
|
||||||
|
window.ipcRenderer.send(CLOSE_TAB, this.state.activeServerName, name);
|
||||||
|
}
|
||||||
|
|
||||||
handleDragAndDrop = async (dropResult: DropResult) => {
|
handleDragAndDrop = async (dropResult: DropResult) => {
|
||||||
const removedIndex = dropResult.source.index;
|
const removedIndex = dropResult.source.index;
|
||||||
const addedIndex = dropResult.destination?.index;
|
const addedIndex = dropResult.destination?.index;
|
||||||
@@ -314,6 +319,7 @@ export default class MainPage extends React.PureComponent<Props, State> {
|
|||||||
activeServerName={this.state.activeServerName}
|
activeServerName={this.state.activeServerName}
|
||||||
activeTabName={this.state.activeTabName}
|
activeTabName={this.state.activeTabName}
|
||||||
onSelect={this.handleSelectTab}
|
onSelect={this.handleSelectTab}
|
||||||
|
onCloseTab={this.handleCloseTab}
|
||||||
onDrop={this.handleDragAndDrop}
|
onDrop={this.handleDragAndDrop}
|
||||||
tabsDisabled={this.state.modalOpen}
|
tabsDisabled={this.state.modalOpen}
|
||||||
/>
|
/>
|
||||||
|
@@ -9,7 +9,7 @@ import classNames from 'classnames';
|
|||||||
|
|
||||||
import {Tab} from 'types/config';
|
import {Tab} from 'types/config';
|
||||||
|
|
||||||
import {getTabViewName} from 'common/tabs/TabView';
|
import {getTabDisplayName, getTabViewName, TabType, canCloseTab} from 'common/tabs/TabView';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
activeTabName: string;
|
activeTabName: string;
|
||||||
@@ -17,6 +17,7 @@ type Props = {
|
|||||||
id: string;
|
id: string;
|
||||||
isDarkMode: boolean;
|
isDarkMode: boolean;
|
||||||
onSelect: (name: string, index: number) => void;
|
onSelect: (name: string, index: number) => void;
|
||||||
|
onCloseTab: (name: string) => void;
|
||||||
tabs: Tab[];
|
tabs: Tab[];
|
||||||
sessionsExpired: Record<string, boolean>;
|
sessionsExpired: Record<string, boolean>;
|
||||||
unreadCounts: Record<string, number>;
|
unreadCounts: Record<string, number>;
|
||||||
@@ -37,6 +38,13 @@ function getStyle(style?: DraggingStyle | NotDraggingStyle) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default class TabBar extends React.PureComponent<Props> {
|
export default class TabBar extends React.PureComponent<Props> {
|
||||||
|
onCloseTab = (name: string) => {
|
||||||
|
return (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
|
event.stopPropagation();
|
||||||
|
this.props.onCloseTab(name);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order);
|
const orderedTabs = this.props.tabs.concat().sort((a, b) => a.order - b.order);
|
||||||
const tabs = orderedTabs.map((tab, orderedIndex) => {
|
const tabs = orderedTabs.map((tab, orderedIndex) => {
|
||||||
@@ -54,12 +62,14 @@ export default class TabBar extends React.PureComponent<Props> {
|
|||||||
let badgeDiv: React.ReactNode;
|
let badgeDiv: React.ReactNode;
|
||||||
if (sessionExpired) {
|
if (sessionExpired) {
|
||||||
badgeDiv = (
|
badgeDiv = (
|
||||||
<div className='TabBar-expired'/>
|
<div className='TabBar-expired'>
|
||||||
|
<i className='icon-alert-circle-outline'/>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
} else if (mentionCount !== 0) {
|
} else if (mentionCount !== 0) {
|
||||||
badgeDiv = (
|
badgeDiv = (
|
||||||
<div className='TabBar-badge'>
|
<div className='TabBar-badge'>
|
||||||
{mentionCount}
|
<span>{mentionCount}</span>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
} else if (hasUnreads) {
|
} else if (hasUnreads) {
|
||||||
@@ -74,39 +84,59 @@ export default class TabBar extends React.PureComponent<Props> {
|
|||||||
draggableId={`teamTabItem${index}`}
|
draggableId={`teamTabItem${index}`}
|
||||||
index={orderedIndex}
|
index={orderedIndex}
|
||||||
>
|
>
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => {
|
||||||
<NavItem
|
if (tab.isClosed) {
|
||||||
ref={provided.innerRef}
|
return (
|
||||||
as='li'
|
<div
|
||||||
id={`teamTabItem${index}`}
|
ref={provided.innerRef}
|
||||||
draggable={false}
|
{...provided.draggableProps}
|
||||||
title={tab.name}
|
{...provided.dragHandleProps}
|
||||||
className={classNames('teamTabItem', {
|
/>
|
||||||
active: this.props.activeTabName === tab.name,
|
);
|
||||||
dragging: snapshot.isDragging,
|
}
|
||||||
})}
|
|
||||||
{...provided.draggableProps}
|
return (
|
||||||
{...provided.dragHandleProps}
|
<NavItem
|
||||||
style={getStyle(provided.draggableProps.style)}
|
ref={provided.innerRef}
|
||||||
>
|
as='li'
|
||||||
<NavLink
|
id={`teamTabItem${index}`}
|
||||||
eventKey={index}
|
|
||||||
draggable={false}
|
draggable={false}
|
||||||
active={this.props.activeTabName === tab.name}
|
title={tab.name}
|
||||||
disabled={this.props.tabsDisabled}
|
className={classNames('teamTabItem', {
|
||||||
onSelect={() => {
|
active: this.props.activeTabName === tab.name,
|
||||||
this.props.onSelect(tab.name, index);
|
dragging: snapshot.isDragging,
|
||||||
}}
|
})}
|
||||||
|
{...provided.draggableProps}
|
||||||
|
{...provided.dragHandleProps}
|
||||||
|
style={getStyle(provided.draggableProps.style)}
|
||||||
>
|
>
|
||||||
<div className='TabBar-tabSeperator'>
|
<NavLink
|
||||||
<span>
|
eventKey={index}
|
||||||
{tab.name}
|
draggable={false}
|
||||||
</span>
|
active={this.props.activeTabName === tab.name}
|
||||||
{ badgeDiv }
|
disabled={this.props.tabsDisabled}
|
||||||
</div>
|
onSelect={() => {
|
||||||
</NavLink>
|
this.props.onSelect(tab.name, index);
|
||||||
</NavItem>
|
}}
|
||||||
)}
|
>
|
||||||
|
<div className='TabBar-tabSeperator'>
|
||||||
|
<span>
|
||||||
|
{getTabDisplayName(tab.name as TabType)}
|
||||||
|
</span>
|
||||||
|
{ badgeDiv }
|
||||||
|
{canCloseTab(tab.name as TabType) &&
|
||||||
|
<button
|
||||||
|
className='teamTabItem__close'
|
||||||
|
onClick={this.onCloseTab(tab.name)}
|
||||||
|
>
|
||||||
|
<i className='icon-close'/>
|
||||||
|
</button>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
</NavLink>
|
||||||
|
</NavItem>
|
||||||
|
);
|
||||||
|
}}
|
||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
// See LICENSE.txt for license information.
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React, {useEffect} from 'react';
|
||||||
|
|
||||||
import {CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN} from 'common/communication';
|
import {CLOSE_TEAMS_DROPDOWN, OPEN_TEAMS_DROPDOWN} from 'common/communication';
|
||||||
|
|
||||||
@@ -20,6 +20,13 @@ type Props = {
|
|||||||
|
|
||||||
const TeamDropdownButton: React.FC<Props> = (props: Props) => {
|
const TeamDropdownButton: React.FC<Props> = (props: Props) => {
|
||||||
const {isDisabled, activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props;
|
const {isDisabled, activeServerName, totalMentionCount, hasUnreads, isMenuOpen, darkMode} = props;
|
||||||
|
const buttonRef: React.RefObject<HTMLButtonElement> = React.createRef();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!isMenuOpen) {
|
||||||
|
buttonRef.current?.blur();
|
||||||
|
}
|
||||||
|
}, [isMenuOpen]);
|
||||||
|
|
||||||
const handleToggleButton = (event: React.MouseEvent<HTMLButtonElement>) => {
|
const handleToggleButton = (event: React.MouseEvent<HTMLButtonElement>) => {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
@@ -42,6 +49,7 @@ const TeamDropdownButton: React.FC<Props> = (props: Props) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<button
|
<button
|
||||||
|
ref={buttonRef}
|
||||||
disabled={isDisabled}
|
disabled={isDisabled}
|
||||||
className={classNames('TeamDropdownButton', {
|
className={classNames('TeamDropdownButton', {
|
||||||
disabled: isDisabled,
|
disabled: isDisabled,
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
.TabBar {
|
.TabBar {
|
||||||
border: none;
|
border: none;
|
||||||
max-height: 36px;
|
max-height: 40px;
|
||||||
flex: 1 1 auto;
|
flex: 1 1 auto;
|
||||||
display: flex !important;
|
display: flex !important;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
justify-content: flex-start;
|
justify-content: flex-start;
|
||||||
-webkit-app-region: drag;
|
-webkit-app-region: drag;
|
||||||
margin-top: 4px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar .teamTabItem span {
|
.TabBar .teamTabItem span {
|
||||||
@@ -23,145 +23,94 @@
|
|||||||
min-width: 48px;
|
min-width: 48px;
|
||||||
max-width: 224px;
|
max-width: 224px;
|
||||||
position: relative;
|
position: relative;
|
||||||
|
margin: 0 2px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li>a {
|
.TabBar>li>a {
|
||||||
border: none;
|
|
||||||
border-radius: 0;
|
|
||||||
height: 32px;
|
|
||||||
max-height: 32px;
|
|
||||||
line-height: 16px;
|
|
||||||
margin-right: -1px;
|
|
||||||
padding: 6px 0;
|
|
||||||
color: rgba(61,60,64,0.7);
|
color: rgba(61,60,64,0.7);
|
||||||
font-family: Arial;
|
font-family: "Open Sans", sans-serif;
|
||||||
font-size: 14px;
|
font-weight: 600;
|
||||||
|
font-size: 12px;
|
||||||
letter-spacing: -0.2px;
|
letter-spacing: -0.2px;
|
||||||
transition: 0.3s;
|
transition: 0.3s;
|
||||||
|
line-height: 18px;
|
||||||
|
border-radius: 4px;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar.darkMode>li>a {
|
.TabBar.darkMode>li>a {
|
||||||
color: rgba(243,243,243,0.7);
|
color: rgba(221,221,221,0.64);
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li>a:hover {
|
.TabBar>li>a:hover {
|
||||||
background-color: rgba(255,255,255,0.4);
|
background-color: rgba(255,255,255,0.32);
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar.darkMode>li>a:hover {
|
.TabBar.darkMode>li>a:hover {
|
||||||
background-color: rgba(50, 54, 57, 0.4);
|
background-color: rgba(31, 31, 31, 0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li>a:focus {
|
.TabBar>li>a:focus {
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
color: rgba(61,60,64,1);
|
color: #3d3c40;
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
border-radius: 6px 6px 0 0;
|
}
|
||||||
|
|
||||||
|
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
color: rgba(61,60,64,0.32);
|
||||||
|
margin-left: 6px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close:hover {
|
||||||
|
color: #3d3c40;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabBar.darkMode>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close:hover {
|
||||||
|
color: #ddd;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabBar>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close>i::before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.TabBar.darkMode>li.teamTabItem>a>div.TabBar-tabSeperator>.teamTabItem__close {
|
||||||
|
color: rgba(221,221,221,0.32);
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar.darkMode>li>a:focus {
|
.TabBar.darkMode>li>a:focus {
|
||||||
background-color: #323639;
|
background-color: #1f1f1f;
|
||||||
color: rgba(243,243,243,1);
|
color: #ddd;
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li:before, .TabBar>li:after {
|
|
||||||
position: absolute;
|
|
||||||
bottom: -1px;
|
|
||||||
width: 6px;
|
|
||||||
height: 6px;
|
|
||||||
content: "";
|
|
||||||
background-color: inherit;
|
|
||||||
z-index: 9;
|
|
||||||
flex: 0 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.teamTabItem.active:before, .TabBar>li.teamTabItem.dragging:before {
|
|
||||||
left: -4px;
|
|
||||||
border-bottom-right-radius: 6px;
|
|
||||||
border-right: 2px solid #fff;
|
|
||||||
border-bottom: 2px solid #fff;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar.darkMode>li.teamTabItem.active:before, .TabBar.darkMode>li.teamTabItem.dragging:before {
|
|
||||||
border-right: 2px solid #323639;
|
|
||||||
border-bottom: 2px solid #323639;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.teamTabItem.active:after, .TabBar>li.teamTabItem.dragging:after {
|
|
||||||
border-bottom-left-radius: 6px;
|
|
||||||
right: -5px;
|
|
||||||
border-left: 2px solid #fff;
|
|
||||||
border-bottom: 2px solid #fff;
|
|
||||||
z-index: 9;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar.darkMode>li.teamTabItem.active:after, .TabBar.darkMode>li.teamTabItem.dragging:after {
|
|
||||||
border-left: 2px solid #323639;
|
|
||||||
border-bottom: 2px solid #323639;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li>a>div.TabBar-tabSeperator {
|
.TabBar>li>a>div.TabBar-tabSeperator {
|
||||||
padding: 2px 16px;
|
|
||||||
max-height: 20px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
padding: 5px 12px;
|
||||||
|
|
||||||
.TabBar>li.TabBar-addServerButton{
|
|
||||||
flex: 0 0 auto;
|
|
||||||
min-width: 40px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.TabBar-addServerButton>a{
|
|
||||||
color: rgba(61,60,64,0.7);
|
|
||||||
transition: opacity 0.3s ease-in;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.TabBar-addServerButton>a.active{
|
|
||||||
background-color: transparent;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.TabBar-addServerButton svg{
|
|
||||||
margin: -2px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar.darkMode>li.TabBar-addServerButton>a{
|
|
||||||
color: rgba(243,243,243,0.7);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.dragging>a {
|
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.dragging>a {
|
||||||
border: none;
|
border: none;
|
||||||
border-radius: 6px 6px 0 0;
|
|
||||||
color: rgba(61,60,64,1);
|
|
||||||
background-color: #fff;
|
background-color: #fff;
|
||||||
|
color: #3d3c40;
|
||||||
z-index: 9;
|
z-index: 9;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.dragging>a {
|
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.dragging>a {
|
||||||
color: #f3f3f3;
|
background-color: #1f1f1f;
|
||||||
background-color: #323639;
|
color: #ddd;
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
|
||||||
border-left: 1px solid rgba(61,60,64,0.2);
|
|
||||||
margin-left: -1px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||||
border-left: 1px solid rgba(61,60,64,0.2);
|
border-left: 1px solid rgba(61,60,64,0.08);
|
||||||
margin-left: -1px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar.darkMode>li.teamTabItem:not(.active)+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
|
||||||
border-left: 1px solid rgba(243,243,243,0.2);
|
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||||
border-left: 1px solid rgba(243,243,243,0.2);
|
border-left: 1px solid rgba(221,221,221,0.08);
|
||||||
margin-left: -1px;
|
margin-left: -1px;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -175,74 +124,55 @@
|
|||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar>li.teamTabItem:not(.active):not(.disabled)+.TabBar-addServerButton>a:hover>div.TabBar-tabSeperator {
|
|
||||||
border-left: none;
|
|
||||||
margin-left: 0px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar>li.teamTabItem:not(.active):not(.disabled)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
|
.TabBar>li.teamTabItem:not(.active):not(.disabled)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
|
||||||
border-left: none;
|
border-left: none;
|
||||||
margin-left: 0px;
|
margin-left: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar .TabBar-addServerButton>a {
|
|
||||||
border: none;
|
|
||||||
background: transparent;
|
|
||||||
color: #999;
|
|
||||||
font-size: 10px;
|
|
||||||
line-height: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar.darkMode .TabBar-addServerButton>a {
|
|
||||||
color: rgba(243,243,243,0.7);
|
|
||||||
}
|
|
||||||
|
|
||||||
.TabBar .TabBar-dot {
|
.TabBar .TabBar-dot {
|
||||||
background: #579EFF;
|
background: #196CAF;
|
||||||
float: right;
|
float: right;
|
||||||
height: 6px;
|
height: 8px;
|
||||||
width: 6px;
|
width: 8px;
|
||||||
margin-top: 5px;
|
margin-top: 4px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
border-radius: 4px;
|
border-radius: 4px;
|
||||||
flex: 0 0 6px;
|
flex: 0 0 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar .TabBar-expired {
|
.TabBar .TabBar-expired {
|
||||||
float: right;
|
float: right;
|
||||||
height: 16px;
|
font-size: 18px;
|
||||||
width: 16px;
|
line-height: 12px;
|
||||||
margin-left: 8px;
|
margin-left: 8px;
|
||||||
background-image: url(../../../assets/icon-session-expired.svg);
|
color: rgba(61,60,64,0.32);
|
||||||
flex: 0 0 16px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.TabBar .TabBar-expired>i::before {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.TabBar.darkMode .TabBar-expired {
|
.TabBar.darkMode .TabBar-expired {
|
||||||
filter: invert(100%);
|
color: rgba(221,221,221,0.32);
|
||||||
-webkit-filter: invert(100%);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar .TabBar-badge {
|
.TabBar .TabBar-badge {
|
||||||
background: #CB2431;
|
background: #F74343;
|
||||||
float: right;
|
border-radius: 8px;
|
||||||
color: white;
|
display: flex;
|
||||||
font-size: 11px;
|
align-items: center;
|
||||||
text-align: center;
|
justify-content: center;
|
||||||
line-height: 18px;
|
flex: 0 0 auto;
|
||||||
height: 18px;
|
color: white;
|
||||||
margin-left: 8px;
|
font-size: 10px;
|
||||||
border-radius: 100px;
|
padding: 0px 5px;
|
||||||
padding: 0 5px;
|
font-family: "Open Sans", sans-serif;
|
||||||
display: flex;
|
font-weight: bold;
|
||||||
justify-content: center;
|
letter-spacing: normal;
|
||||||
align-items: center;
|
-webkit-font-smoothing: antialiased;
|
||||||
font-family: "Open Sans", sans-serif;
|
margin-left: 6px;
|
||||||
font-weight: bold;
|
margin-top: 2px;
|
||||||
min-width: 18px;
|
height: 12px;
|
||||||
margin-top: -1px;
|
|
||||||
letter-spacing: normal;
|
|
||||||
-webkit-font-smoothing: antialiased;
|
|
||||||
flex: 1 0 18px;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.TabBar .TabBar-badge.TabBar-badge-nomention:after {
|
.TabBar .TabBar-badge.TabBar-badge-nomention:after {
|
||||||
|
@@ -56,12 +56,12 @@
|
|||||||
top: -4px;
|
top: -4px;
|
||||||
right: -2px;
|
right: -2px;
|
||||||
border: 2px solid #efefef;
|
border: 2px solid #efefef;
|
||||||
|
height: 16px;
|
||||||
|
|
||||||
> span {
|
> span {
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 10px;
|
font-size: 10px;
|
||||||
line-height: 11px;
|
padding: 0px 5px;
|
||||||
padding: 1px 5px;
|
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@@ -4,9 +4,12 @@
|
|||||||
export type Tab = {
|
export type Tab = {
|
||||||
name: string;
|
name: string;
|
||||||
order: number;
|
order: number;
|
||||||
|
isClosed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Team = Tab & {
|
export type Team = {
|
||||||
|
name: string;
|
||||||
|
order: number;
|
||||||
url: string;
|
url: string;
|
||||||
lastActiveTab?: number;
|
lastActiveTab?: number;
|
||||||
}
|
}
|
||||||
|
9
src/types/server.ts
Normal file
9
src/types/server.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||||
|
// See LICENSE.txt for license information.
|
||||||
|
|
||||||
|
export type RemoteInfo = {
|
||||||
|
name: string;
|
||||||
|
serverVersion?: string;
|
||||||
|
hasFocalboard?: boolean;
|
||||||
|
hasPlaybooks?: boolean;
|
||||||
|
};
|
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
export type ServerFromURL = {
|
export type ServerFromURL = {
|
||||||
name: string;
|
name: string;
|
||||||
url: URL;
|
url: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export type Boundaries = {
|
export type Boundaries = {
|
||||||
|
Reference in New Issue
Block a user