[MM-10586] Desktop App Window/Tabs Update (#1056)
* [MM-19054] Added new server tab look and feel, still missing proper hover states and session expired icon * [MM-19055] Added window controls and removed border for macOS * [MM-19055] Add dark mode for macOS * [MM-19054] Added session expired icon * Test windows titlebar * Fixed the menu issue and added non-macOS dark mode * Blank commit * Fixed a lint issue * Fixed more lint issues * Fixed more issues * New tray icons * [MM-19603] Drag and drop tabs * Fixed some assets and fixed build output to include missing assets * Fixed a couple small issues * Only show tabs for only 1 server on Mac * Fixed some more tests * Fixed another test * Revert "Fixed another test" This reverts commit 36040294a71a68663d06996d71eecc5ed23d7014. * Fixed another test * Trial and error! * A bunch of additional fixes * Fixed a lint issue * Fixed restore focus on add server tab causing bad UX * Trial and error on flaky test again * Fixed some bugs based on PR feedback. * blank commit to push tests * Revert "Test windows titlebar" This reverts commit 9cd46b71b1427b75942434ac49185870d2437b85. * Remove the rest of the old new titlebar and fixes * Added three-dot link * New menu * Rest of new windows menu and other fixes * Fixed lint errors * Added windows 10 style title bar buttons for non mac OS * Lint fixes and enabled the tab bar regardless of number of servers * Missed one * Fixed unicode characters * Commenting out test that should no longer be applicable * Removed Windows 10 style titlebar icons and used material design instead * Fixed a lint issue * Some small UX fixes * blank commit * Fixed an issue where dropping the first tab moves it too far over before snapping into place * Additional style fixes * Another small issue fix * Back to Windows 10 style * Lint fixes * Accessible three dot menu * Lint fixes * Shrinking tabs when window is too small * Gradient between tabs and title bar buttons when window is too small * Add drag to gradient * Replaced icons, drag and drop cursor sticking fix, slight tab change * Lint and some mac fixes * Light theme fix to three dot menu * Hack for tab sticking to cursor on macOS * Fixes for the find utility * Fix for Catalina dark mode * Revert "Fix for Catalina dark mode" This reverts commit 45da05dd0f17f46efd1c53fafb92e9c1fd9dd8d9. * Fixed a couple issues Dean found * More fixes * Three dot hover effect to circle * PR feedback * Test fixes * Test and config fixes * Disable dragging when there are GPO servers * [MM-20757] Fixed dark mode on debug when running macOS Catalina * Allow future config versions to use v2 config if launching this version of the app * Oops * New titlebar icons, blur for titlebar on inactive * Lint fix * Set unfocused opacity to 0.4 * Final FINAL icons * Fixed closing menu not returning focus to the app * Lint fix * Update src/browser/components/TabBar.jsx Co-Authored-By: Guillermo Vayá <guivaya@gmail.com> * Update src/main/Validator.js Co-Authored-By: Guillermo Vayá <guivaya@gmail.com> * Lint fixes * Moved react-smooth-dnd fork to MM org and fixed another merge issue Co-authored-by: mattermod <mattermod@users.noreply.github.com> Co-authored-by: Guillermo Vayá <guivaya@gmail.com>
4
.gitignore
vendored
|
@ -14,3 +14,7 @@ test-results.xml
|
|||
test_config.json
|
||||
.idea
|
||||
testUserData
|
||||
|
||||
src/browser/*.png
|
||||
src/browser/*.svg
|
||||
src/browser/*.woff2
|
|
@ -12,7 +12,7 @@
|
|||
},
|
||||
"files": [
|
||||
"main_bundle.js",
|
||||
"browser/**/*{.html,.css,_bundle.js}",
|
||||
"browser/**/*{.html,.css,_bundle.js,.svg,.png}",
|
||||
"assets/**/*",
|
||||
"node_modules/bootstrap/dist/**",
|
||||
"node_modules/simple-spellchecker/dict/*.dic"
|
||||
|
|
1624
package-lock.json
generated
|
@ -14,7 +14,8 @@
|
|||
"url": "git://github.com/mattermost/desktop.git"
|
||||
},
|
||||
"scripts": {
|
||||
"postinstall": "electron-builder install-app-deps && npm run extract-dict",
|
||||
"postinstall": "electron-builder install-app-deps && npm run extract-dict && npm run fix-catalina-dark-mode",
|
||||
"fix-catalina-dark-mode": "node scripts/fix_catalina_dark_mode_debug.js",
|
||||
"extract-dict": "node scripts/extract_dict.js src/node_modules/simple-spellchecker/dict",
|
||||
"build": "npm-run-all build:*",
|
||||
"build:main": "webpack-cli --bail --config webpack.config.main.js",
|
||||
|
@ -64,11 +65,14 @@
|
|||
"eslint-plugin-import": "^2.14.0",
|
||||
"eslint-plugin-react": "^7.11.1",
|
||||
"file-loader": "^2.0.0",
|
||||
"image-webpack-loader": "5.0.0",
|
||||
"mdi-react": "^6.2.0",
|
||||
"mocha": "^5.2.0",
|
||||
"mocha-circleci-reporter": "0.0.3",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"react": "^16.6.3",
|
||||
"react-dom": "^16.6.3",
|
||||
"react-smooth-dnd": "github:mattermost/react-smooth-dnd#af6b471295007274560a375799622c1cd52d678a",
|
||||
"spectron": "^6.0.0",
|
||||
"style-loader": "^0.23.1",
|
||||
"url-loader": "^1.1.2",
|
||||
|
|
13
scripts/fix_catalina_dark_mode_debug.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
|
||||
const {exec} = require('child_process');
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
exec('plutil -insert NSRequiresAquaSystemAppearance -bool NO ./node_modules/electron/dist/Electron.app/Contents/Info.plist', (err) => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
3
src/assets/icon-session-expired.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
|
||||
<path fill="#3D3C40" fill-opacity=".7" fill-rule="evenodd" d="M12,4 C16.4152,4 20,7.5848 20,12 C20,16.4152 16.4152,20 12,20 C7.5848,20 4,16.4152 4,12 C4,7.5848 7.5848,4 12,4 Z M12,5 C8.1367,5 5,8.1367 5,12 C5,15.8633 8.1367,19 12,19 C15.8633,19 19,15.8633 19,12 C19,8.1367 15.8633,5 12,5 Z M13,14 L13,16 L11,16 L11,14 L13,14 Z M13,8 L13,13 L11,13 L11,8 L13,8 Z" transform="translate(-4 -4)"/>
|
||||
</svg>
|
After Width: | Height: | Size: 486 B |
BIN
src/assets/linux/dark/MenuIconMentionTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 732 B |
BIN
src/assets/linux/dark/MenuIconTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 649 B |
BIN
src/assets/linux/dark/MenuIconUnreadTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 1.1 KiB After Width: | Height: | Size: 730 B |
BIN
src/assets/linux/light/MenuIconMentionTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 872 B After Width: | Height: | Size: 768 B |
BIN
src/assets/linux/light/MenuIconTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 544 B After Width: | Height: | Size: 692 B |
BIN
src/assets/linux/light/MenuIconUnreadTemplate.png
Normal file → Executable file
Before Width: | Height: | Size: 1 KiB After Width: | Height: | Size: 766 B |
BIN
src/assets/osx/ClickedMenuIcon.png
Normal file → Executable file
Before Width: | Height: | Size: 332 B After Width: | Height: | Size: 447 B |
BIN
src/assets/osx/ClickedMenuIcon@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 598 B After Width: | Height: | Size: 956 B |
BIN
src/assets/osx/ClickedMenuIconMention.png
Normal file → Executable file
Before Width: | Height: | Size: 345 B After Width: | Height: | Size: 519 B |
BIN
src/assets/osx/ClickedMenuIconMention@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 688 B After Width: | Height: | Size: 1.1 KiB |
BIN
src/assets/osx/ClickedMenuIconUnread.png
Normal file → Executable file
Before Width: | Height: | Size: 335 B After Width: | Height: | Size: 513 B |
BIN
src/assets/osx/ClickedMenuIconUnread@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 684 B After Width: | Height: | Size: 1 KiB |
BIN
src/assets/osx/MenuIcon.png
Normal file → Executable file
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 426 B |
BIN
src/assets/osx/MenuIcon@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 719 B After Width: | Height: | Size: 883 B |
BIN
src/assets/osx/MenuIconMention.png
Normal file → Executable file
Before Width: | Height: | Size: 342 B After Width: | Height: | Size: 482 B |
BIN
src/assets/osx/MenuIconMention@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 766 B After Width: | Height: | Size: 1,012 B |
BIN
src/assets/osx/MenuIconUnread.png
Normal file → Executable file
Before Width: | Height: | Size: 344 B After Width: | Height: | Size: 478 B |
BIN
src/assets/osx/MenuIconUnread@2x.png
Normal file → Executable file
Before Width: | Height: | Size: 760 B After Width: | Height: | Size: 1,001 B |
3
src/assets/titlebar/chrome-close.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M9.16763 0.00233459L9.98984 0.825763L5.8259 4.99587L9.99767 9.16147L9.17424 9.98368L5.0037 5.8193L0.831506 9.99768L0.0092966 9.17425L4.18027 4.99709L0.00232887 0.82533L0.825757 0.00311899L5.00248 4.17366L9.16763 0.00233459Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 392 B |
3
src/assets/titlebar/chrome-maximize.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect x="0.5" y="0.5" width="9" height="9" stroke="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 163 B |
3
src/assets/titlebar/chrome-minimize.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<rect y="4" width="10" height="1" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 152 B |
3
src/assets/titlebar/chrome-restore.svg
Normal file
|
@ -0,0 +1,3 @@
|
|||
<svg width="10" height="10" viewBox="0 0 10 10" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M8 8V10H0V2H2V0H10V8H8ZM7 3H1V9H7V3ZM3 2H8V7H9V1H3V2Z" fill="black"/>
|
||||
</svg>
|
After Width: | Height: | Size: 222 B |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.4 KiB |
Before Width: | Height: | Size: 5.2 KiB After Width: | Height: | Size: 2.4 KiB |
|
@ -14,9 +14,6 @@ export default function ErrorView(props) {
|
|||
if (!props.active) {
|
||||
classNames.push('ErrorView-hidden');
|
||||
}
|
||||
if (props.withTab) {
|
||||
classNames.push('ErrorView-with-tab');
|
||||
}
|
||||
function handleClick(event) {
|
||||
event.preventDefault();
|
||||
shell.openExternal(props.errorInfo.validatedURL);
|
||||
|
@ -84,5 +81,4 @@ ErrorView.propTypes = {
|
|||
errorInfo: PropTypes.object,
|
||||
id: PropTypes.number,
|
||||
active: PropTypes.bool,
|
||||
withTab: PropTypes.bool,
|
||||
};
|
||||
|
|
|
@ -77,7 +77,7 @@ export default class Finder extends React.Component {
|
|||
render() {
|
||||
return (
|
||||
<div id='finder'>
|
||||
<div className='finder'>
|
||||
<div className={`finder${process.platform === 'darwin' ? ' macOS' : ''}`}>
|
||||
<div className='finder-input-wrapper'>
|
||||
<input
|
||||
className='finder-input'
|
||||
|
|
|
@ -11,9 +11,15 @@ import React from 'react';
|
|||
import PropTypes from 'prop-types';
|
||||
import {CSSTransition, TransitionGroup} from 'react-transition-group';
|
||||
import {Grid, Row} from 'react-bootstrap';
|
||||
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
|
||||
|
||||
import {ipcRenderer, remote} from 'electron';
|
||||
|
||||
import restoreButton from '../../assets/titlebar/chrome-restore.svg';
|
||||
import maximizeButton from '../../assets/titlebar/chrome-maximize.svg';
|
||||
import minimizeButton from '../../assets/titlebar/chrome-minimize.svg';
|
||||
import closeButton from '../../assets/titlebar/chrome-close.svg';
|
||||
|
||||
import LoginModal from './LoginModal.jsx';
|
||||
import MattermostView from './MattermostView.jsx';
|
||||
import TabBar from './TabBar.jsx';
|
||||
|
@ -33,6 +39,8 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
this.topBar = React.createRef();
|
||||
|
||||
this.state = {
|
||||
key,
|
||||
sessionsExpired: new Array(this.props.teams.length),
|
||||
|
@ -42,6 +50,7 @@ export default class MainPage extends React.Component {
|
|||
mentionAtActiveCounts: new Array(this.props.teams.length),
|
||||
loginQueue: [],
|
||||
targetURL: '',
|
||||
maximized: false,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -79,6 +88,29 @@ export default class MainPage extends React.Component {
|
|||
|
||||
componentDidMount() {
|
||||
const self = this;
|
||||
|
||||
// Due to a bug in Chrome on macOS, mousemove events from the webview won't register when the webview isn't in focus,
|
||||
// thus you can't drag tabs unless you're right on the container.
|
||||
// this makes it so your tab won't get stuck to your cursor no matter where you mouse up
|
||||
if (process.platform === 'darwin') {
|
||||
self.topBar.current.addEventListener('mouseleave', () => {
|
||||
if (event.target === self.topBar.current) {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
});
|
||||
|
||||
// Hack for when it leaves the electron window because apparently mouseleave isn't good enough there...
|
||||
self.topBar.current.addEventListener('mousemove', () => {
|
||||
if (event.clientY === 0 || event.clientX === 0 || event.clientX >= window.innerWidth) {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
ipcRenderer.on('login-request', (event, request, authInfo) => {
|
||||
self.setState({
|
||||
loginRequired: true,
|
||||
|
@ -115,14 +147,32 @@ export default class MainPage extends React.Component {
|
|||
function focusListener() {
|
||||
self.handleOnTeamFocused(self.state.key);
|
||||
self.refs[`mattermostView${self.state.key}`].focusOnWebView();
|
||||
self.setState({unfocused: false});
|
||||
}
|
||||
|
||||
function blurListener() {
|
||||
self.setState({unfocused: true});
|
||||
}
|
||||
|
||||
const currentWindow = remote.getCurrentWindow();
|
||||
currentWindow.on('focus', focusListener);
|
||||
currentWindow.on('blur', blurListener);
|
||||
window.addEventListener('beforeunload', () => {
|
||||
currentWindow.removeListener('focus', focusListener);
|
||||
});
|
||||
|
||||
if (currentWindow.isMaximized()) {
|
||||
self.setState({maximized: true});
|
||||
}
|
||||
currentWindow.on('maximize', this.handleMaximizeState);
|
||||
currentWindow.on('unmaximize', this.handleMaximizeState);
|
||||
|
||||
if (currentWindow.isFullScreen()) {
|
||||
self.setState({fullScreen: true});
|
||||
}
|
||||
currentWindow.on('enter-full-screen', this.handleFullScreenState);
|
||||
currentWindow.on('leave-full-screen', this.handleFullScreenState);
|
||||
|
||||
// https://github.com/mattermost/desktop/pull/371#issuecomment-263072803
|
||||
currentWindow.webContents.on('devtools-closed', () => {
|
||||
focusListener();
|
||||
|
@ -246,6 +296,32 @@ export default class MainPage extends React.Component {
|
|||
ipcRenderer.on('toggle-find', () => {
|
||||
this.activateFinder(true);
|
||||
});
|
||||
|
||||
if (process.platform === 'darwin') {
|
||||
self.setState({
|
||||
isDarkMode: remote.systemPreferences.isDarkMode(),
|
||||
});
|
||||
remote.systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
|
||||
self.setState({
|
||||
isDarkMode: remote.systemPreferences.isDarkMode(),
|
||||
});
|
||||
});
|
||||
} else {
|
||||
self.setState({
|
||||
isDarkMode: this.props.getDarkMode(),
|
||||
});
|
||||
|
||||
ipcRenderer.on('set-dark-mode', () => {
|
||||
this.setDarkMode();
|
||||
});
|
||||
|
||||
this.threeDotMenu = React.createRef();
|
||||
ipcRenderer.on('focus-three-dot-menu', () => {
|
||||
if (this.threeDotMenu.current) {
|
||||
this.threeDotMenu.current.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
|
@ -254,6 +330,16 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleMaximizeState = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
this.setState({maximized: win.isMaximized()});
|
||||
}
|
||||
|
||||
handleFullScreenState = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
this.setState({fullScreen: win.isFullScreen()});
|
||||
}
|
||||
|
||||
handleSelect = (key) => {
|
||||
const newKey = (this.props.teams.length + key) % this.props.teams.length;
|
||||
this.setState({
|
||||
|
@ -269,6 +355,14 @@ export default class MainPage extends React.Component {
|
|||
this.handleOnTeamFocused(newKey);
|
||||
}
|
||||
|
||||
handleDragAndDrop = (dropResult) => {
|
||||
const {removedIndex, addedIndex} = dropResult;
|
||||
if (removedIndex !== addedIndex) {
|
||||
const teamIndex = this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
|
||||
this.handleSelect(teamIndex);
|
||||
}
|
||||
}
|
||||
|
||||
handleBadgeChange = (index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) => {
|
||||
const sessionsExpired = this.state.sessionsExpired;
|
||||
const unreadCounts = this.state.unreadCounts;
|
||||
|
@ -362,17 +456,53 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
}
|
||||
|
||||
handleClose = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
win.close();
|
||||
}
|
||||
|
||||
handleMinimize = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
win.minimize();
|
||||
}
|
||||
|
||||
handleMaximize = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
win.maximize();
|
||||
}
|
||||
|
||||
handleRestore = () => {
|
||||
const win = remote.getCurrentWindow();
|
||||
win.restore();
|
||||
}
|
||||
|
||||
openMenu = () => {
|
||||
// @eslint-ignore
|
||||
this.threeDotMenu.current.blur();
|
||||
this.props.openMenu();
|
||||
}
|
||||
|
||||
handleDoubleClick = () => {
|
||||
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
|
||||
const win = remote.getCurrentWindow();
|
||||
if (doubleClickAction === 'Minimize') {
|
||||
win.minimize();
|
||||
} else if (doubleClickAction === 'Maximize' && !win.isMaximized()) {
|
||||
win.maximize();
|
||||
} else if (doubleClickAction === 'Maximize' && win.isMaximized()) {
|
||||
win.unmaximize();
|
||||
}
|
||||
}
|
||||
|
||||
addServer = () => {
|
||||
this.setState({
|
||||
showNewTeamModal: true,
|
||||
});
|
||||
}
|
||||
|
||||
focusOnWebView = (e) => {
|
||||
if (e.target.className !== 'finder-input') {
|
||||
focusOnWebView = () => {
|
||||
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
|
||||
}
|
||||
}
|
||||
|
||||
activateFinder = () => {
|
||||
this.setState({
|
||||
|
@ -393,14 +523,18 @@ export default class MainPage extends React.Component {
|
|||
});
|
||||
}
|
||||
|
||||
setDarkMode() {
|
||||
this.setState({
|
||||
isDarkMode: this.props.setDarkMode(),
|
||||
});
|
||||
}
|
||||
|
||||
render() {
|
||||
const self = this;
|
||||
let tabsRow;
|
||||
if (this.props.teams.length > 1) {
|
||||
tabsRow = (
|
||||
<Row>
|
||||
const tabsRow = (
|
||||
<TabBar
|
||||
id='tabBar'
|
||||
isDarkMode={this.state.isDarkMode}
|
||||
teams={this.props.teams}
|
||||
sessionsExpired={this.state.sessionsExpired}
|
||||
unreadCounts={this.state.unreadCounts}
|
||||
|
@ -411,11 +545,94 @@ export default class MainPage extends React.Component {
|
|||
onSelect={this.handleSelect}
|
||||
onAddServer={this.addServer}
|
||||
showAddServerButton={this.props.showAddServerButton}
|
||||
onDrop={this.handleDragAndDrop}
|
||||
/>
|
||||
</Row>
|
||||
);
|
||||
|
||||
let topBarClassName = 'topBar';
|
||||
if (process.platform === 'darwin') {
|
||||
topBarClassName += ' macOS';
|
||||
}
|
||||
if (this.state.isDarkMode) {
|
||||
topBarClassName += ' darkMode';
|
||||
}
|
||||
if (this.state.fullScreen) {
|
||||
topBarClassName += ' fullScreen';
|
||||
}
|
||||
|
||||
let maxButton;
|
||||
if (this.state.maximized) {
|
||||
maxButton = (
|
||||
<div
|
||||
className='button restore-button'
|
||||
onClick={this.handleRestore}
|
||||
>
|
||||
<img src={restoreButton}/>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
maxButton = (
|
||||
<div
|
||||
className='button max-button'
|
||||
onClick={this.handleMaximize}
|
||||
>
|
||||
<img src={maximizeButton}/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
let overlayGradient;
|
||||
if (process.platform !== 'darwin') {
|
||||
overlayGradient = (
|
||||
<span className='overlay-gradient'/>
|
||||
);
|
||||
}
|
||||
|
||||
let titleBarButtons;
|
||||
if (process.platform !== 'darwin') {
|
||||
titleBarButtons = (
|
||||
<span className='title-bar-btns'>
|
||||
<div
|
||||
className='button min-button'
|
||||
onClick={this.handleMinimize}
|
||||
>
|
||||
<img src={minimizeButton}/>
|
||||
</div>
|
||||
{maxButton}
|
||||
<div
|
||||
className='button close-button'
|
||||
onClick={this.handleClose}
|
||||
>
|
||||
<img src={closeButton}/>
|
||||
</div>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
const topRow = (
|
||||
<Row
|
||||
className={topBarClassName}
|
||||
onDoubleClick={this.handleDoubleClick}
|
||||
>
|
||||
<div
|
||||
ref={this.topBar}
|
||||
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
|
||||
>
|
||||
<button
|
||||
className='three-dot-menu'
|
||||
onClick={this.openMenu}
|
||||
tabIndex={0}
|
||||
ref={this.threeDotMenu}
|
||||
>
|
||||
<DotsVerticalIcon/>
|
||||
</button>
|
||||
{tabsRow}
|
||||
{overlayGradient}
|
||||
{titleBarButtons}
|
||||
</div>
|
||||
</Row>
|
||||
);
|
||||
|
||||
const views = this.props.teams.map((team, index) => {
|
||||
function handleBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) {
|
||||
self.handleBadgeChange(index, sessionExpired, unreadCount, mentionCount, isUnread, isMentioned);
|
||||
|
@ -439,7 +656,7 @@ export default class MainPage extends React.Component {
|
|||
<MattermostView
|
||||
key={id}
|
||||
id={id}
|
||||
withTab={this.props.teams.length > 1}
|
||||
|
||||
useSpellChecker={this.props.useSpellChecker}
|
||||
onSelectSpellCheckerLocale={this.props.onSelectSpellCheckerLocale}
|
||||
src={teamUrl}
|
||||
|
@ -467,7 +684,9 @@ export default class MainPage extends React.Component {
|
|||
}
|
||||
const modal = (
|
||||
<NewTeamModal
|
||||
currentOrder={this.props.teams.length}
|
||||
show={this.state.showNewTeamModal}
|
||||
restoreFocus={false}
|
||||
onClose={() => {
|
||||
this.setState({
|
||||
showNewTeamModal: false,
|
||||
|
@ -498,7 +717,7 @@ export default class MainPage extends React.Component {
|
|||
onCancel={this.handleLoginCancel}
|
||||
/>
|
||||
<Grid fluid={true}>
|
||||
{ tabsRow }
|
||||
{ topRow }
|
||||
{ viewsRow }
|
||||
{ this.state.finderVisible ? (
|
||||
<Finder
|
||||
|
@ -540,6 +759,10 @@ MainPage.propTypes = {
|
|||
onSelectSpellCheckerLocale: PropTypes.func.isRequired,
|
||||
deeplinkingUrl: PropTypes.string,
|
||||
showAddServerButton: PropTypes.bool.isRequired,
|
||||
getDarkMode: PropTypes.func.isRequired,
|
||||
setDarkMode: PropTypes.func.isRequired,
|
||||
moveTabs: PropTypes.func.isRequired,
|
||||
openMenu: PropTypes.func.isRequired,
|
||||
};
|
||||
|
||||
/* eslint-enable react/no-set-state */
|
||||
|
|
|
@ -179,6 +179,12 @@ export default class MattermostView extends React.Component {
|
|||
case 'onNotificationClick':
|
||||
self.props.onNotificationClick();
|
||||
break;
|
||||
case 'mouse-move':
|
||||
this.handleMouseMove(event.args[0]);
|
||||
break;
|
||||
case 'mouse-up':
|
||||
this.handleMouseUp();
|
||||
break;
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -243,12 +249,24 @@ export default class MattermostView extends React.Component {
|
|||
focusOnWebView = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
const webContents = webview.getWebContents(); // webContents might not be created yet.
|
||||
if (webContents && !webContents.isFocused()) {
|
||||
if (webContents) {
|
||||
webview.focus();
|
||||
webContents.focus();
|
||||
}
|
||||
}
|
||||
|
||||
handleMouseMove = (event) => {
|
||||
const moveEvent = document.createEvent('MouseEvents');
|
||||
moveEvent.initMouseEvent('mousemove', null, null, null, null, null, null, event.clientX, event.clientY);
|
||||
document.dispatchEvent(moveEvent);
|
||||
}
|
||||
|
||||
handleMouseUp = () => {
|
||||
const upEvent = document.createEvent('MouseEvents');
|
||||
upEvent.initMouseEvent('mouseup');
|
||||
document.dispatchEvent(upEvent);
|
||||
}
|
||||
|
||||
canGoBack = () => {
|
||||
const webview = this.webviewRef.current;
|
||||
return webview.getWebContents().canGoBack();
|
||||
|
@ -301,7 +319,6 @@ export default class MattermostView extends React.Component {
|
|||
className='errorView'
|
||||
errorInfo={this.state.errorInfo}
|
||||
active={this.props.active}
|
||||
withTab={this.props.withTab}
|
||||
/>) : null;
|
||||
|
||||
// Need to keep webview mounted when failed to load.
|
||||
|
@ -309,7 +326,7 @@ export default class MattermostView extends React.Component {
|
|||
if (this.props.withTab) {
|
||||
classNames.push('mattermostView-with-tab');
|
||||
}
|
||||
if (!this.props.active) {
|
||||
if (!this.props.active || this.state.errorInfo) {
|
||||
classNames.push('mattermostView-hidden');
|
||||
}
|
||||
|
||||
|
@ -342,11 +359,11 @@ export default class MattermostView extends React.Component {
|
|||
MattermostView.propTypes = {
|
||||
name: PropTypes.string,
|
||||
id: PropTypes.string,
|
||||
withTab: PropTypes.bool,
|
||||
onTargetURLChange: PropTypes.func,
|
||||
onBadgeChange: PropTypes.func,
|
||||
src: PropTypes.string,
|
||||
active: PropTypes.bool,
|
||||
withTab: PropTypes.bool,
|
||||
useSpellChecker: PropTypes.bool,
|
||||
onSelectSpellCheckerLocale: PropTypes.func,
|
||||
};
|
||||
|
|
|
@ -9,13 +9,18 @@ import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 're
|
|||
import Utils from '../../utils/util';
|
||||
|
||||
export default class NewTeamModal extends React.Component {
|
||||
constructor() {
|
||||
super();
|
||||
static defaultProps = {
|
||||
restoreFocus: true,
|
||||
};
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
|
||||
this.wasShown = false;
|
||||
this.state = {
|
||||
teamName: '',
|
||||
teamUrl: '',
|
||||
teamOrder: props.currentOrder || 0,
|
||||
saveStarted: false,
|
||||
};
|
||||
}
|
||||
|
@ -25,6 +30,7 @@ export default class NewTeamModal extends React.Component {
|
|||
teamName: this.props.team ? this.props.team.name : '',
|
||||
teamUrl: this.props.team ? this.props.team.url : '',
|
||||
teamIndex: this.props.team ? this.props.team.index : false,
|
||||
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
|
||||
saveStarted: false,
|
||||
});
|
||||
}
|
||||
|
@ -100,6 +106,7 @@ export default class NewTeamModal extends React.Component {
|
|||
url: this.state.teamUrl,
|
||||
name: this.state.teamName,
|
||||
index: this.state.teamIndex,
|
||||
order: this.state.teamOrder,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -132,6 +139,8 @@ export default class NewTeamModal extends React.Component {
|
|||
show={this.props.show}
|
||||
id='newServerModal'
|
||||
onHide={this.props.onClose}
|
||||
container={this.props.modalContainer}
|
||||
restoreFocus={this.props.restoreFocus}
|
||||
onKeyDown={(e) => {
|
||||
switch (e.key) {
|
||||
case 'Enter':
|
||||
|
@ -221,4 +230,7 @@ NewTeamModal.propTypes = {
|
|||
team: PropTypes.object,
|
||||
editMode: PropTypes.bool,
|
||||
show: PropTypes.bool,
|
||||
modalContainer: PropTypes.object,
|
||||
restoreFocus: PropTypes.bool,
|
||||
currentOrder: PropTypes.number,
|
||||
};
|
||||
|
|
|
@ -342,6 +342,7 @@ export default class SettingsPage extends React.Component {
|
|||
onTeamClick={(index) => {
|
||||
backToIndex(index + this.state.buildTeams.length + this.state.registryTeams.length);
|
||||
}}
|
||||
modalContainer={this}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
|
@ -605,7 +606,7 @@ export default class SettingsPage extends React.Component {
|
|||
) : null;
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className='modal-container'>
|
||||
<Navbar
|
||||
className='navbar-fixed-top'
|
||||
style={settingsPage.navbar}
|
||||
|
|
|
@ -2,12 +2,17 @@
|
|||
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
|
||||
// See LICENSE.txt for license information.
|
||||
import React from 'react';
|
||||
import {remote} from 'electron';
|
||||
import PropTypes from 'prop-types';
|
||||
import {Glyphicon, Nav, NavItem} from 'react-bootstrap';
|
||||
import {Nav, NavItem} from 'react-bootstrap';
|
||||
import {Container, Draggable} from 'react-smooth-dnd';
|
||||
import PlusIcon from 'mdi-react/PlusIcon';
|
||||
|
||||
export default class TabBar extends React.Component { // need "this"
|
||||
render() {
|
||||
const tabs = this.props.teams.map((team, index) => {
|
||||
const orderedTabs = this.props.teams.concat().sort((a, b) => a.order - b.order);
|
||||
const tabs = orderedTabs.map((team) => {
|
||||
const index = this.props.teams.indexOf(team);
|
||||
const sessionExpired = this.props.sessionsExpired[index];
|
||||
|
||||
let unreadCount = 0;
|
||||
|
@ -29,7 +34,7 @@ export default class TabBar extends React.Component { // need "this"
|
|||
let badgeDiv;
|
||||
if (sessionExpired) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-badge TabBar-badge-nomention'/>
|
||||
<div className='TabBar-expired'/>
|
||||
);
|
||||
} else if (mentionCount !== 0) {
|
||||
badgeDiv = (
|
||||
|
@ -37,29 +42,45 @@ export default class TabBar extends React.Component { // need "this"
|
|||
{mentionCount}
|
||||
</div>
|
||||
);
|
||||
} else if (unreadCount !== 0) {
|
||||
badgeDiv = (
|
||||
<div className='TabBar-dot'/>
|
||||
);
|
||||
}
|
||||
const id = 'teamTabItem' + index;
|
||||
|
||||
// draggable=false is a workaround for https://github.com/mattermost/desktop/issues/667
|
||||
// It would obstruct https://github.com/mattermost/desktop/issues/478
|
||||
return (
|
||||
const id = `teamTabItem${index}`;
|
||||
const navItem = () => (
|
||||
<NavItem
|
||||
className='teamTabItem'
|
||||
key={id}
|
||||
id={id}
|
||||
eventKey={index}
|
||||
ref={id}
|
||||
draggable={false}
|
||||
>
|
||||
<span
|
||||
ref={id}
|
||||
active={this.props.activeKey === index}
|
||||
activeKey={this.props.activeKey}
|
||||
onMouseDown={() => {
|
||||
this.props.onSelect(index);
|
||||
}}
|
||||
onSelect={() => {
|
||||
this.props.onSelect(index);
|
||||
}}
|
||||
title={team.name}
|
||||
className={unreadCount === 0 ? '' : 'teamTabItem-unread'}
|
||||
>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<span>
|
||||
{team.name}
|
||||
</span>
|
||||
{ ' ' }
|
||||
{ badgeDiv }
|
||||
</NavItem>);
|
||||
</div>
|
||||
</NavItem>
|
||||
);
|
||||
|
||||
return (
|
||||
<Draggable
|
||||
key={id}
|
||||
render={navItem}
|
||||
className='teamTabItem'
|
||||
/>);
|
||||
});
|
||||
if (this.props.showAddServerButton === true) {
|
||||
tabs.push(
|
||||
|
@ -68,36 +89,50 @@ export default class TabBar extends React.Component { // need "this"
|
|||
key='addServerButton'
|
||||
id='addServerButton'
|
||||
eventKey='addServerButton'
|
||||
title='Add new server'
|
||||
draggable={false}
|
||||
title='Add new server'
|
||||
activeKey={this.props.activeKey}
|
||||
onSelect={() => {
|
||||
this.props.onAddServer();
|
||||
}}
|
||||
>
|
||||
<Glyphicon glyph='plus'/>
|
||||
<div className='TabBar-tabSeperator'>
|
||||
<PlusIcon size={20}/>
|
||||
</div>
|
||||
</NavItem>
|
||||
);
|
||||
}
|
||||
return (
|
||||
|
||||
const navContainer = (ref) => (
|
||||
<Nav
|
||||
className='TabBar'
|
||||
ref={ref}
|
||||
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
|
||||
id={this.props.id}
|
||||
bsStyle='tabs'
|
||||
activeKey={this.props.activeKey}
|
||||
onSelect={(eventKey) => {
|
||||
if (eventKey === 'addServerButton') {
|
||||
this.props.onAddServer();
|
||||
} else {
|
||||
this.props.onSelect(eventKey);
|
||||
}
|
||||
}}
|
||||
>
|
||||
{ tabs }
|
||||
</Nav>
|
||||
);
|
||||
return (
|
||||
<Container
|
||||
ref={this.container}
|
||||
render={navContainer}
|
||||
orientation='horizontal'
|
||||
lockAxis={'x'}
|
||||
onDrop={this.props.onDrop}
|
||||
animationDuration={300}
|
||||
shouldAcceptDrop={() => {
|
||||
return !(remote.getCurrentWindow().registryConfigData.teams && remote.getCurrentWindow().registryConfigData.teams.length > 0);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
TabBar.propTypes = {
|
||||
activeKey: PropTypes.number,
|
||||
id: PropTypes.string,
|
||||
isDarkMode: PropTypes.bool,
|
||||
onSelect: PropTypes.func,
|
||||
teams: PropTypes.array,
|
||||
sessionsExpired: PropTypes.array,
|
||||
|
@ -107,4 +142,5 @@ TabBar.propTypes = {
|
|||
mentionAtActiveCounts: PropTypes.array,
|
||||
showAddServerButton: PropTypes.bool,
|
||||
onAddServer: PropTypes.func,
|
||||
onDrop: PropTypes.func,
|
||||
};
|
||||
|
|
|
@ -20,6 +20,7 @@ export default class TeamList extends React.Component {
|
|||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
order: props.teams.length,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -27,7 +28,13 @@ export default class TeamList extends React.Component {
|
|||
handleTeamRemove = (index) => {
|
||||
console.log(index);
|
||||
const teams = this.props.teams;
|
||||
const removedOrder = this.props.teams[index].order;
|
||||
teams.splice(index, 1);
|
||||
teams.forEach((value) => {
|
||||
if (value.order > removedOrder) {
|
||||
value.order--;
|
||||
}
|
||||
});
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
|
@ -38,6 +45,7 @@ export default class TeamList extends React.Component {
|
|||
if ((typeof team.index !== 'undefined') && teams[team.index]) {
|
||||
teams[team.index].name = team.name;
|
||||
teams[team.index].url = team.url;
|
||||
teams[team.index].order = team.order;
|
||||
} else {
|
||||
teams.push(team);
|
||||
}
|
||||
|
@ -48,19 +56,21 @@ export default class TeamList extends React.Component {
|
|||
url: '',
|
||||
name: '',
|
||||
index: false,
|
||||
order: teams.length,
|
||||
},
|
||||
});
|
||||
|
||||
this.props.onTeamsChange(teams);
|
||||
}
|
||||
|
||||
handleTeamEditing = (teamName, teamUrl, teamIndex) => {
|
||||
handleTeamEditing = (teamName, teamUrl, teamIndex, teamOrder) => {
|
||||
this.setState({
|
||||
showEditTeamForm: true,
|
||||
team: {
|
||||
url: teamUrl,
|
||||
name: teamName,
|
||||
index: teamIndex,
|
||||
order: teamOrder,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -83,7 +93,7 @@ export default class TeamList extends React.Component {
|
|||
|
||||
function handleTeamEditing() {
|
||||
document.activeElement.blur();
|
||||
self.handleTeamEditing(team.name, team.url, i);
|
||||
self.handleTeamEditing(team.name, team.url, i, team.order);
|
||||
}
|
||||
|
||||
function handleTeamClick() {
|
||||
|
@ -105,6 +115,7 @@ export default class TeamList extends React.Component {
|
|||
|
||||
const addServerForm = (
|
||||
<NewTeamModal
|
||||
currentOrder={this.props.teams.length}
|
||||
show={this.props.showAddTeamForm || this.state.showEditTeamForm}
|
||||
editMode={this.state.showEditTeamForm}
|
||||
onClose={() => {
|
||||
|
@ -114,6 +125,7 @@ export default class TeamList extends React.Component {
|
|||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
order: this.props.teams.length,
|
||||
},
|
||||
});
|
||||
this.props.setAddTeamFormVisibility(false);
|
||||
|
@ -122,6 +134,7 @@ export default class TeamList extends React.Component {
|
|||
const teamData = {
|
||||
name: newTeam.name,
|
||||
url: newTeam.url,
|
||||
order: newTeam.order,
|
||||
};
|
||||
if (this.props.showAddTeamForm) {
|
||||
this.props.addServer(teamData);
|
||||
|
@ -135,12 +148,14 @@ export default class TeamList extends React.Component {
|
|||
name: '',
|
||||
url: '',
|
||||
index: false,
|
||||
order: newTeam.order + 1,
|
||||
},
|
||||
});
|
||||
this.render();
|
||||
this.props.setAddTeamFormVisibility(false);
|
||||
}}
|
||||
team={this.state.team}
|
||||
modalContainer={this.props.modalContainer}
|
||||
/>);
|
||||
|
||||
const removeServer = this.props.teams[this.state.indexToRemoveServer];
|
||||
|
@ -154,6 +169,7 @@ export default class TeamList extends React.Component {
|
|||
this.handleTeamRemove(this.state.indexToRemoveServer);
|
||||
this.closeServerRemoveModal();
|
||||
}}
|
||||
modalContainer={this.props.modalContainer}
|
||||
/>
|
||||
);
|
||||
|
||||
|
@ -176,4 +192,5 @@ TeamList.propTypes = {
|
|||
toggleAddTeamForm: PropTypes.func,
|
||||
setAddTeamFormVisibility: PropTypes.func,
|
||||
onTeamClick: PropTypes.func,
|
||||
modalContainer: PropTypes.object,
|
||||
};
|
||||
|
|
|
@ -1,15 +1,11 @@
|
|||
.ErrorView {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
top: 32px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.ErrorView-with-tab {
|
||||
top: 32px;
|
||||
}
|
||||
|
||||
.ErrorView-hidden {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
border: none;
|
||||
background: #d2d2d2;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
font-size: 18px;
|
||||
height: 26px;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ div[id*="-permissionDialog"] {
|
|||
.finder {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
right: 20px;
|
||||
right: 200px;
|
||||
padding: 4px;
|
||||
background: #eee;
|
||||
border: 1px solid #d7d7d7;
|
||||
|
@ -24,4 +24,9 @@ div[id*="-permissionDialog"] {
|
|||
border-bottom-left-radius: 5px;
|
||||
border-bottom-right-radius: 5px;
|
||||
font-size: 0px;
|
||||
-webkit-app-region: no-drag;
|
||||
}
|
||||
|
||||
.finder.macOS {
|
||||
right: 20px;
|
||||
}
|
||||
|
|
|
@ -8,16 +8,12 @@
|
|||
|
||||
.mattermostView webview, .mattermostView .mattermostView-loadingScreen {
|
||||
position: absolute;
|
||||
top: 0px;
|
||||
top: 40px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
left: 0px;
|
||||
}
|
||||
|
||||
.mattermostView-with-tab webview, .mattermostView-with-tab .mattermostView-loadingScreen {
|
||||
top: 31px;
|
||||
}
|
||||
|
||||
.mattermostView-hidden webview {
|
||||
visibility: hidden;
|
||||
z-index: -1;
|
||||
|
|
|
@ -1,27 +1,189 @@
|
|||
.TabBar {
|
||||
border: none;
|
||||
max-height: 36px;
|
||||
flex: 1 1 auto;
|
||||
display: flex !important;
|
||||
flex-wrap: nowrap;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.TabBar.darkMode {
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.TabBar .teamTabItem span {
|
||||
display: inline-block;
|
||||
flex: 0 1 auto;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
max-width: 170px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.TabBar>li {
|
||||
-webkit-app-region: no-drag;
|
||||
-webkit-user-select: none;
|
||||
min-width: 48px;
|
||||
}
|
||||
|
||||
.TabBar>li>a {
|
||||
background: rgba(0, 0, 0, 0.05);
|
||||
border-radius: 2px 2px 0 0;
|
||||
border: 1px solid #ddd;
|
||||
color: #888;
|
||||
height: 31px;
|
||||
line-height: 29px;
|
||||
border: none;
|
||||
border-radius: 0;
|
||||
height: 32px;
|
||||
max-height: 32px;
|
||||
line-height: 16px;
|
||||
margin-right: -1px;
|
||||
margin-top: 0px;
|
||||
padding: 0 15px;
|
||||
margin-top: 4px;
|
||||
padding: 6px 0;
|
||||
color: rgba(61,60,64,0.7);
|
||||
font-family: Arial;
|
||||
font-size: 14px;
|
||||
letter-spacing: -0.2px;
|
||||
transition: 0.3s;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active)>a:hover {
|
||||
background-color: #e6e6e6;
|
||||
border: 1px solid #ddd;
|
||||
transition: background-color 0.2s ease;
|
||||
.TabBar.darkMode>li>a {
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar>li>a:hover {
|
||||
background-color: rgba(255,255,255,0.4);
|
||||
text-decoration: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li>a:hover {
|
||||
background-color: rgba(50, 54, 57, 0.4);
|
||||
}
|
||||
|
||||
.TabBar>li>a:focus {
|
||||
background-color: #fff;
|
||||
color: rgba(61,60,64,1);
|
||||
text-decoration: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li>a:focus {
|
||||
background-color: #323639;
|
||||
color: rgba(243,243,243,1);
|
||||
}
|
||||
|
||||
.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.smooth-dnd-ghost: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.smooth-dnd-ghost:before {
|
||||
border-right: 2px solid #323639;
|
||||
border-bottom: 2px solid #323639;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem.active:after, .TabBar>li.teamTabItem.smooth-dnd-ghost: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.smooth-dnd-ghost:after {
|
||||
border-left: 2px solid #323639;
|
||||
border-bottom: 2px solid #323639;
|
||||
}
|
||||
|
||||
.TabBar>li>a>div.TabBar-tabSeperator {
|
||||
padding: 2px 16px;
|
||||
max-height: 20px;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.TabBar>li.TabBar-addServerButton{
|
||||
transition: none !important;
|
||||
transform: none !important;
|
||||
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 svg{
|
||||
margin: -2px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.TabBar-addServerButton>a{
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem.active>a, .TabBar>li.teamTabItem.smooth-dnd-ghost>a {
|
||||
border: none;
|
||||
border-radius: 6px 6px 0 0;
|
||||
color: rgba(61,60,64,1);
|
||||
background-color: #fff;
|
||||
z-index: 9;
|
||||
}
|
||||
|
||||
.smooth-dnd-no-user-select li.TabBar-addServerButton>a {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem.active>a, .TabBar.darkMode>li.teamTabItem.smooth-dnd-ghost>a {
|
||||
color: #f3f3f3;
|
||||
background-color: #323639;
|
||||
}
|
||||
|
||||
.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 {
|
||||
border-left: 1px solid rgba(61,60,64,0.2);
|
||||
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;
|
||||
}
|
||||
|
||||
.TabBar.darkMode>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||
border-left: 1px solid rgba(243,243,243,0.2);
|
||||
margin-left: -1px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):hover+.TabBar-addServerButton>a>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active):hover+li.teamTabItem:not(.active)>a>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active)+.TabBar-addServerButton>a:hover>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar>li.teamTabItem:not(.active)+li.teamTabItem:not(.active)>a:hover>div.TabBar-tabSeperator {
|
||||
border-left: none;
|
||||
margin-left: 0px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-addServerButton>a {
|
||||
|
@ -29,34 +191,59 @@
|
|||
background: transparent;
|
||||
color: #999;
|
||||
font-size: 10px;
|
||||
line-height: 31px;
|
||||
line-height: 16px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-addServerButton>a:hover {
|
||||
color: #333;
|
||||
background-color: #e6e6e6;
|
||||
border-color: #adadad;
|
||||
transition: background-color 0.2s ease;
|
||||
.TabBar.darkMode .TabBar-addServerButton>a {
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.TabBar .TabBar-dot {
|
||||
background: #579EFF;
|
||||
float: right;
|
||||
height: 6px;
|
||||
width: 6px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
border-radius: 4px;
|
||||
flex: 0 0 6px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-expired {
|
||||
float: right;
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
margin-left: 8px;
|
||||
background-image: url(../../../assets/icon-session-expired.svg);
|
||||
flex: 0 0 16px;
|
||||
}
|
||||
|
||||
.TabBar.darkMode .TabBar-expired {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.TabBar .TabBar-badge {
|
||||
background: #FF1744;
|
||||
background: #CB2431;
|
||||
float: right;
|
||||
color: white;
|
||||
min-width: 18px;
|
||||
font-size: 12px;
|
||||
font-size: 11px;
|
||||
text-align: center;
|
||||
line-height: 20px;
|
||||
line-height: 18px;
|
||||
height: 18px;
|
||||
margin-left: 5px;
|
||||
margin-top: 5px;
|
||||
margin-left: 8px;
|
||||
border-radius: 100px;
|
||||
padding: 0 5px;
|
||||
border-radius: 9px;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
font-family: "HelveticaNeue-Light", "Helvetica Neue Light", "Helvetica Neue", Helvetica, Arial, "Lucida Grande", sans-serif;
|
||||
font-weight: 600;
|
||||
font-family: "Open Sans", sans-serif;
|
||||
font-weight: bold;
|
||||
min-width: 18px;
|
||||
margin-top: -1px;
|
||||
letter-spacing: normal;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
flex: 1 0 18px;
|
||||
}
|
||||
|
||||
.TabBar .TabBar-badge.TabBar-badge-nomention:after {
|
||||
|
|
|
@ -5,5 +5,4 @@
|
|||
.TeamListItem-left {
|
||||
display: inline-block;
|
||||
width: calc(100% - 100px);
|
||||
cursor: pointer;
|
||||
}
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
@import url("components/index.css");
|
||||
|
||||
@font-face {
|
||||
font-family: 'Open Sans';
|
||||
font-style: normal;
|
||||
font-weight: 600;
|
||||
src: url('../../assets/fonts/open-sans-v13-latin-ext_latin_cyrillic-ext_greek-ext_greek_cyrillic_vietnamese-600.woff2') format('woff2');
|
||||
}
|
||||
|
||||
html {
|
||||
height: 100%;
|
||||
}
|
||||
body {
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
.hovering-enter {
|
||||
opacity: 0.01;
|
||||
}
|
||||
|
@ -26,3 +40,177 @@
|
|||
.modal-error {
|
||||
color: #a94442;
|
||||
}
|
||||
|
||||
.topBar {
|
||||
height: 40px;
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.topBar>.topBar-bg {
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
-webkit-app-region: drag;
|
||||
height: 36px;
|
||||
background-color: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.topBar>.topBar-bg.unfocused {
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.topBar.darkMode {
|
||||
background-color: #323639;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.topBar.darkMode>.topBar-bg {
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu {
|
||||
font-size: 20px;
|
||||
line-height: 20px;
|
||||
height: 36px;
|
||||
float: left;
|
||||
padding-top: 5px;
|
||||
border: none;
|
||||
flex: 0 0 40px;
|
||||
z-index: 9;
|
||||
color: rgba(61,60,64,0.7);
|
||||
-webkit-app-region: no-drag;
|
||||
background-color: rgba(229, 229, 229, 1);
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu svg {
|
||||
border-radius: 100px;
|
||||
padding: 4px;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.topBar .three-dot-menu:hover svg, .topBar .three-dot-menu:focus svg, .topBar .three-dot-menu:active svg {
|
||||
outline: none;
|
||||
background-color: #c8c8c8;
|
||||
}
|
||||
|
||||
.topBar.darkMode .three-dot-menu:hover svg, .topBar.darkMode .three-dot-menu:focus svg, .topBar.darkMode .three-dot-menu:active svg {
|
||||
background-color: #383A3F;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu:hover svg, .topBar.macOS .three-dot-menu:focus svg, .topBar.macOS .three-dot-menu:active svg {
|
||||
background: none !important;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu {
|
||||
flex-basis: 80px;
|
||||
}
|
||||
|
||||
.topBar.macOS.fullScreen .three-dot-menu {
|
||||
flex-basis: 0px;
|
||||
}
|
||||
|
||||
.topBar.macOS .three-dot-menu>svg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.topBar.darkMode .three-dot-menu {
|
||||
background-color: #202124;
|
||||
color: rgba(243,243,243,0.7);
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns {
|
||||
color: rgba(243,243,243,0.7);
|
||||
background-color: #202124;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns {
|
||||
position: relative;
|
||||
line-height: 36px;
|
||||
height: 36px;
|
||||
z-index: 9;
|
||||
color: rgba(61,60,64,0.7);
|
||||
-webkit-app-region: no-drag;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 46px);
|
||||
background-color: #e5e5e5;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button {
|
||||
grid-row: 1 / span 1;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns>.button:hover {
|
||||
background: rgba(255,255,255,0.1);
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns>.button:active {
|
||||
background: rgba(255,255,255,0.2);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button:hover {
|
||||
background: rgba(0,0,0,0.1);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.button:active {
|
||||
background: rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:hover {
|
||||
background: #E81123 !important;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:hover>img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:active {
|
||||
background: #f1707a !important;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button:active>img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns img {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.topBar.darkMode .title-bar-btns img {
|
||||
filter: invert(100%);
|
||||
-webkit-filter: invert(100%);
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.min-button {
|
||||
grid-column: 1;
|
||||
}
|
||||
.topBar .title-bar-btns>.max-button, .topBar .title-bar-btns>.restore-button {
|
||||
grid-column: 2;
|
||||
}
|
||||
|
||||
.topBar .title-bar-btns>.close-button {
|
||||
grid-column: 3;
|
||||
}
|
||||
|
||||
.topBar .overlay-gradient {
|
||||
flex: 0 0 40px;
|
||||
z-index: 9;
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #e5e5e5 100%);
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
.topBar.darkMode .overlay-gradient {
|
||||
background: linear-gradient(90deg, rgba(0, 0, 0, 0) 0%, #202124 100%);
|
||||
}
|
|
@ -26,6 +26,14 @@ const config = new Config(remote.app.getPath('userData') + '/config.json', remot
|
|||
|
||||
const teams = config.teams;
|
||||
|
||||
// Make sure teams have an order
|
||||
if (teams.every((team) => !team.order)) {
|
||||
teams.forEach((team, index) => {
|
||||
team.order = index;
|
||||
});
|
||||
teamConfigChange(teams);
|
||||
}
|
||||
|
||||
remote.getCurrentWindow().removeAllListeners('focus');
|
||||
|
||||
if (teams.length === 0) {
|
||||
|
@ -33,7 +41,7 @@ if (teams.length === 0) {
|
|||
}
|
||||
|
||||
const parsedURL = url.parse(window.location.href, true);
|
||||
const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : 0;
|
||||
const initialIndex = parsedURL.query.index ? parseInt(parsedURL.query.index, 10) : getInitialIndex();
|
||||
|
||||
let deeplinkingUrl = null;
|
||||
if (!parsedURL.query.index || parsedURL.query.index === null) {
|
||||
|
@ -52,6 +60,11 @@ ipcRenderer.on('reload-config', () => {
|
|||
config.reload();
|
||||
});
|
||||
|
||||
function getInitialIndex() {
|
||||
const element = teams.find((e) => e.order === 0);
|
||||
return element ? teams.indexOf(element) : 0;
|
||||
}
|
||||
|
||||
function showBadgeWindows(sessionExpired, unreadCount, mentionCount) {
|
||||
function sendBadge(dataURL, description) {
|
||||
// window.setOverlayIcon() does't work with NativeImage across remote boundaries.
|
||||
|
@ -136,6 +149,50 @@ function handleSelectSpellCheckerLocale(locale) {
|
|||
ipcRenderer.send('update-dict', locale);
|
||||
}
|
||||
|
||||
function moveTabs(originalOrder, newOrder) {
|
||||
const tabOrder = teams.concat().map((team, index) => {
|
||||
return {
|
||||
index,
|
||||
order: team.order,
|
||||
};
|
||||
}).sort((a, b) => (a.order - b.order));
|
||||
|
||||
const team = tabOrder.splice(originalOrder, 1);
|
||||
tabOrder.splice(newOrder, 0, team[0]);
|
||||
|
||||
let teamIndex;
|
||||
tabOrder.forEach((t, order) => {
|
||||
if (order === newOrder) {
|
||||
teamIndex = t.index;
|
||||
}
|
||||
teams[t.index].order = order;
|
||||
});
|
||||
teamConfigChange(teams);
|
||||
return teamIndex;
|
||||
}
|
||||
|
||||
function getDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
return config.darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function setDarkMode() {
|
||||
if (process.platform !== 'darwin') {
|
||||
const darkMode = Boolean(config.darkMode);
|
||||
config.set('darkMode', !darkMode);
|
||||
return !darkMode;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function openMenu() {
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcRenderer.send('open-app-menu');
|
||||
}
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<MainPage
|
||||
teams={teams}
|
||||
|
@ -146,6 +203,10 @@ ReactDOM.render(
|
|||
onSelectSpellCheckerLocale={handleSelectSpellCheckerLocale}
|
||||
deeplinkingUrl={deeplinkingUrl}
|
||||
showAddServerButton={config.enableServerManagement}
|
||||
getDarkMode={getDarkMode}
|
||||
setDarkMode={setDarkMode}
|
||||
moveTabs={moveTabs}
|
||||
openMenu={openMenu}
|
||||
/>,
|
||||
document.getElementById('content')
|
||||
);
|
||||
|
|
|
@ -47,6 +47,15 @@ window.addEventListener('load', () => {
|
|||
});
|
||||
});
|
||||
|
||||
// Sent for drag and drop tabs to work properly
|
||||
document.addEventListener('mousemove', (event) => {
|
||||
ipcRenderer.sendToHost('mouse-move', {clientX: event.clientX, clientY: event.clientY});
|
||||
});
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
ipcRenderer.sendToHost('mouse-up');
|
||||
});
|
||||
|
||||
// listen for messages from the webapp
|
||||
window.addEventListener('message', ({origin, data: {type, message = {}} = {}} = {}) => {
|
||||
if (origin !== window.location.origin) {
|
||||
|
|
|
@ -72,6 +72,7 @@ export default class RegistryConfig extends EventEmitter {
|
|||
teams.push({
|
||||
name: team.name,
|
||||
url: team.value,
|
||||
order: team.order,
|
||||
});
|
||||
}
|
||||
return teams;
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
* @param {number} version - Scheme version. (Not application version)
|
||||
*/
|
||||
const defaultPreferences = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [],
|
||||
showTrayIcon: false,
|
||||
trayIconTheme: 'light',
|
||||
|
@ -22,6 +22,7 @@ const defaultPreferences = {
|
|||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
spellCheckerLocale: 'en-US',
|
||||
darkMode: false,
|
||||
};
|
||||
|
||||
export default defaultPreferences;
|
||||
|
|
|
@ -144,6 +144,9 @@ export default class Config extends EventEmitter {
|
|||
get teams() {
|
||||
return this.combinedData.teams;
|
||||
}
|
||||
get darkMode() {
|
||||
return this.combinedData.darkMode;
|
||||
}
|
||||
get localTeams() {
|
||||
return this.localConfigData.teams;
|
||||
}
|
||||
|
@ -209,11 +212,17 @@ export default class Config extends EventEmitter {
|
|||
configData = this.readFileSync(this.configFilePath);
|
||||
|
||||
// validate based on config file version
|
||||
if (configData.version > 0) {
|
||||
configData = Validator.validateV1ConfigData(configData);
|
||||
if (configData.version > 1) {
|
||||
configData = Validator.validateV2ConfigData(configData);
|
||||
} else {
|
||||
switch (configData.version) {
|
||||
case 1:
|
||||
configData = Validator.validateV1ConfigData(configData);
|
||||
break;
|
||||
default:
|
||||
configData = Validator.validateV0ConfigData(configData);
|
||||
}
|
||||
}
|
||||
if (!configData) {
|
||||
throw new Error('Provided configuration file does not validate, using defaults instead.');
|
||||
}
|
||||
|
|
|
@ -7,6 +7,23 @@ const pastDefaultPreferences = {
|
|||
0: {
|
||||
url: '',
|
||||
},
|
||||
1: {
|
||||
version: 1,
|
||||
teams: [],
|
||||
showTrayIcon: false,
|
||||
trayIconTheme: 'light',
|
||||
minimizeToTray: false,
|
||||
notifications: {
|
||||
flashWindow: 0,
|
||||
bounceIcon: false,
|
||||
bounceIconType: 'informational',
|
||||
},
|
||||
showUnreadBadge: true,
|
||||
useSpellChecker: true,
|
||||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
spellCheckerLocale: 'en-US',
|
||||
},
|
||||
};
|
||||
|
||||
pastDefaultPreferences[`${defaultPreferences.version}`] = defaultPreferences;
|
||||
|
|
|
@ -19,9 +19,21 @@ function upgradeV0toV1(configV0) {
|
|||
return config;
|
||||
}
|
||||
|
||||
function upgradeV1toV2(configV1) {
|
||||
const config = deepCopy(configV1);
|
||||
config.version = 2;
|
||||
config.teams.forEach((value, index) => {
|
||||
value.order = index;
|
||||
});
|
||||
config.darkMode = false;
|
||||
return config;
|
||||
}
|
||||
|
||||
export default function upgradeToLatest(config) {
|
||||
const configVersion = config.version ? config.version : 0;
|
||||
switch (configVersion) {
|
||||
case 1:
|
||||
return upgradeToLatest(upgradeV1toV2(config));
|
||||
case 0:
|
||||
return upgradeToLatest(upgradeV0toV1(config));
|
||||
default:
|
||||
|
|
29
src/main.js
|
@ -66,6 +66,7 @@ let registryConfig = null;
|
|||
let config = null;
|
||||
let trayIcon = null;
|
||||
let trayImages = null;
|
||||
let altLastPressed = false;
|
||||
|
||||
// supported custom login paths (oath, saml)
|
||||
const customLoginRegexPaths = [
|
||||
|
@ -216,6 +217,9 @@ function initializeInterCommunicationEventListeners() {
|
|||
if (shouldShowTrayIcon()) {
|
||||
ipcMain.on('update-unread', handleUpdateUnreadEvent);
|
||||
}
|
||||
if (process.platform !== 'darwin') {
|
||||
ipcMain.on('open-app-menu', handleOpenAppMenu);
|
||||
}
|
||||
}
|
||||
|
||||
function initializeMainWindowListeners() {
|
||||
|
@ -462,6 +466,19 @@ function handleAppWebContentsCreated(dc, contents) {
|
|||
|
||||
// implemented to temporarily help solve for https://community-daily.mattermost.com/core/pl/b95bi44r4bbnueqzjjxsi46qiw
|
||||
contents.on('before-input-event', (event, input) => {
|
||||
if (input.key === 'Alt' && input.type === 'keyUp' && altLastPressed) {
|
||||
altLastPressed = false;
|
||||
mainWindow.webContents.send('focus-three-dot-menu');
|
||||
return;
|
||||
}
|
||||
|
||||
// Hack to detect keyPress so that alt+<key> combinations don't default back to the 3-dot menu
|
||||
if (input.key === 'Alt' && input.type === 'keyDown') {
|
||||
altLastPressed = true;
|
||||
} else {
|
||||
altLastPressed = false;
|
||||
}
|
||||
|
||||
if (!input.shift && !input.control && !input.alt && !input.meta) {
|
||||
// hacky fix for https://mattermost.atlassian.net/browse/MM-19226
|
||||
if ((input.key === 'Escape' || input.key === 'f') && input.type === 'keyDown') {
|
||||
|
@ -755,9 +772,21 @@ function handleUpdateUnreadEvent(event, arg) {
|
|||
}
|
||||
}
|
||||
|
||||
function handleOpenAppMenu() {
|
||||
Menu.getApplicationMenu().popup({
|
||||
x: 18,
|
||||
y: 18,
|
||||
});
|
||||
}
|
||||
|
||||
function handleCloseAppMenu(event) {
|
||||
mainWindow.webContents.send('focus-on-webview', event);
|
||||
}
|
||||
|
||||
function handleUpdateMenuEvent(event, configData) {
|
||||
const aMenu = appMenu.createMenu(mainWindow, configData, global.isDev);
|
||||
Menu.setApplicationMenu(aMenu);
|
||||
aMenu.addListener('menu-will-close', handleCloseAppMenu);
|
||||
|
||||
// set up context menu for tray icon
|
||||
if (shouldShowTrayIcon()) {
|
||||
|
|
|
@ -61,6 +61,29 @@ const configDataSchemaV1 = Joi.object({
|
|||
spellCheckerLocale: Joi.string().regex(/^[a-z]{2}-[A-Z]{2}$/).default('en-US'),
|
||||
});
|
||||
|
||||
const configDataSchemaV2 = Joi.object({
|
||||
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),
|
||||
})).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),
|
||||
});
|
||||
|
||||
// eg. data['community.mattermost.com'] = { data: 'certificate data', issuerName: 'COMODO RSA Domain Validation Secure Server CA'};
|
||||
const certificateStoreSchema = Joi.object().pattern(
|
||||
Joi.string().uri(),
|
||||
|
@ -113,6 +136,26 @@ export function validateV1ConfigData(data) {
|
|||
return validateAgainstSchema(data, configDataSchemaV1);
|
||||
}
|
||||
|
||||
export function validateV2ConfigData(data) {
|
||||
if (Array.isArray(data.teams) && data.teams.length) {
|
||||
// first replace possible backslashes with forward slashes
|
||||
let teams = data.teams.map(({name, url, order}) => {
|
||||
let updatedURL = url;
|
||||
if (updatedURL.includes('\\')) {
|
||||
updatedURL = updatedURL.toLowerCase().replace(/\\/gi, '/');
|
||||
}
|
||||
return {name, url: updatedURL, order};
|
||||
});
|
||||
|
||||
// next filter out urls that are still invalid so all is not lost
|
||||
teams = teams.filter(({url}) => Utils.isValidURL(url));
|
||||
|
||||
// replace original teams
|
||||
data.teams = teams;
|
||||
}
|
||||
return validateAgainstSchema(data, configDataSchemaV2);
|
||||
}
|
||||
|
||||
// validate certificate.json
|
||||
export function validateCertificateStore(data) {
|
||||
return validateAgainstSchema(data, certificateStoreSchema);
|
||||
|
|
|
@ -51,7 +51,9 @@ function createMainWindow(config, options) {
|
|||
show: hideOnStartup || false,
|
||||
minWidth: minimumWindowWidth,
|
||||
minHeight: minimumWindowHeight,
|
||||
frame: false,
|
||||
fullscreen: false,
|
||||
titleBarStyle: 'hiddenInset',
|
||||
backgroundColor: '#fff', // prevents blurry text: https://electronjs.org/docs/faq#the-font-looks-blurry-what-is-this-and-what-can-i-do
|
||||
webPreferences: {
|
||||
nodeIntegration: true,
|
||||
|
@ -156,10 +158,6 @@ function createMainWindow(config, options) {
|
|||
}
|
||||
});
|
||||
|
||||
mainWindow.on('sheet-end', () => {
|
||||
mainWindow.webContents.send('focus-on-webview');
|
||||
});
|
||||
|
||||
// Register keyboard shortcuts
|
||||
mainWindow.webContents.on('before-input-event', (event, input) => {
|
||||
// Add Alt+Cmd+(Right|Left) as alternative to switch between servers
|
||||
|
|
|
@ -116,9 +116,8 @@ function createTemplate(mainWindow, config, isDev) {
|
|||
role: 'selectall',
|
||||
}],
|
||||
});
|
||||
template.push({
|
||||
label: '&View',
|
||||
submenu: [{
|
||||
|
||||
const viewSubMenu = [{
|
||||
label: 'Find..',
|
||||
accelerator: 'CmdOrCtrl+F',
|
||||
click(item, focusedWindow) {
|
||||
|
@ -188,7 +187,21 @@ function createTemplate(mainWindow, config, isDev) {
|
|||
click() {
|
||||
mainWindow.webContents.send('open-devtool');
|
||||
},
|
||||
}],
|
||||
}];
|
||||
|
||||
if (process.platform !== 'darwin') {
|
||||
viewSubMenu.push(separatorItem);
|
||||
viewSubMenu.push({
|
||||
label: 'Toggle Dark Mode',
|
||||
click() {
|
||||
mainWindow.webContents.send('set-dark-mode');
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
template.push({
|
||||
label: '&View',
|
||||
submenu: viewSubMenu,
|
||||
});
|
||||
template.push({
|
||||
label: '&History',
|
||||
|
|
|
@ -72,13 +72,15 @@ describe('application', function desc() {
|
|||
|
||||
it('should show index.html when there is config file', async () => {
|
||||
const config = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'example',
|
||||
url: env.mattermostURL,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'github',
|
||||
url: 'https://github.com/',
|
||||
order: 1,
|
||||
}],
|
||||
showTrayIcon: false,
|
||||
trayIconTheme: 'light',
|
||||
|
@ -92,6 +94,7 @@ describe('application', function desc() {
|
|||
useSpellChecker: true,
|
||||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
darkMode: false,
|
||||
};
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify(config));
|
||||
await this.app.restart();
|
||||
|
|
|
@ -14,13 +14,15 @@ describe('browser/index.html', function desc() {
|
|||
this.timeout(30000);
|
||||
|
||||
const config = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'example',
|
||||
url: env.mattermostURL,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'github',
|
||||
url: 'https://github.com/',
|
||||
order: 1,
|
||||
}],
|
||||
showTrayIcon: false,
|
||||
trayIconTheme: 'light',
|
||||
|
@ -34,6 +36,7 @@ describe('browser/index.html', function desc() {
|
|||
useSpellChecker: true,
|
||||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
darkMode: false,
|
||||
};
|
||||
|
||||
const serverPort = 8181;
|
||||
|
@ -65,16 +68,6 @@ describe('browser/index.html', function desc() {
|
|||
this.server.close(done);
|
||||
});
|
||||
|
||||
it('should NOT show tabs when there is one team', async () => {
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||
url: env.mattermostURL,
|
||||
}));
|
||||
await this.app.restart();
|
||||
|
||||
const existing = await this.app.client.isExisting('#tabBar');
|
||||
existing.should.be.false;
|
||||
});
|
||||
|
||||
it('should set src of webview from config file', async () => {
|
||||
const src0 = await this.app.client.getAttribute('#mattermostView0', 'src');
|
||||
src0.should.equal(config.teams[0].url);
|
||||
|
@ -107,10 +100,11 @@ describe('browser/index.html', function desc() {
|
|||
it.skip('should show error when using incorrect URL', async () => {
|
||||
this.timeout(30000);
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'error_1',
|
||||
url: 'http://false',
|
||||
order: 0,
|
||||
}],
|
||||
}));
|
||||
await this.app.restart();
|
||||
|
@ -120,10 +114,11 @@ describe('browser/index.html', function desc() {
|
|||
|
||||
it('should set window title by using webview\'s one', async () => {
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'title_test',
|
||||
url: `http://localhost:${serverPort}`,
|
||||
order: 0,
|
||||
}],
|
||||
}));
|
||||
await this.app.restart();
|
||||
|
@ -135,13 +130,15 @@ describe('browser/index.html', function desc() {
|
|||
// Skip because it's very unstable in CI
|
||||
it.skip('should update window title when the activated tab\'s title is updated', async () => {
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'title_test_0',
|
||||
url: `http://localhost:${serverPort}`,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'title_test_1',
|
||||
url: `http://localhost:${serverPort}`,
|
||||
order: 1,
|
||||
}],
|
||||
}));
|
||||
await this.app.restart();
|
||||
|
@ -171,13 +168,15 @@ describe('browser/index.html', function desc() {
|
|||
// Skip because it's very unstable in CI
|
||||
it.skip('should update window title when a tab is selected', async () => {
|
||||
fs.writeFileSync(env.configFilePath, JSON.stringify({
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'title_test_0',
|
||||
url: `http://localhost:${serverPort}`,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'title_test_1',
|
||||
url: `http://localhost:${serverPort}`,
|
||||
order: 1,
|
||||
}],
|
||||
}));
|
||||
await this.app.restart();
|
||||
|
|
|
@ -12,13 +12,15 @@ describe('browser/settings.html', function desc() {
|
|||
this.timeout(30000);
|
||||
|
||||
const config = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'example',
|
||||
url: env.mattermostURL,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'github',
|
||||
url: 'https://github.com/',
|
||||
order: 1,
|
||||
}],
|
||||
showTrayIcon: false,
|
||||
trayIconTheme: 'light',
|
||||
|
@ -32,6 +34,7 @@ describe('browser/settings.html', function desc() {
|
|||
useSpellChecker: true,
|
||||
enableHardwareAcceleration: true,
|
||||
autostart: true,
|
||||
darkMode: false,
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -357,8 +360,13 @@ describe('browser/settings.html', function desc() {
|
|||
|
||||
await this.app.client.waitForVisible('#serversSaveIndicator', 10000, true);
|
||||
|
||||
const expectedConfig = JSON.parse(JSON.stringify(config.teams.slice(1)));
|
||||
expectedConfig.forEach((value) => {
|
||||
value.order--;
|
||||
});
|
||||
|
||||
const savedConfig = JSON.parse(fs.readFileSync(env.configFilePath, 'utf8'));
|
||||
savedConfig.teams.should.deep.equal(config.teams.slice(1));
|
||||
savedConfig.teams.should.deep.equal(expectedConfig);
|
||||
});
|
||||
|
||||
it('should NOT remove existing team on click Cancel', async () => {
|
||||
|
@ -512,6 +520,7 @@ describe('browser/settings.html', function desc() {
|
|||
savedConfig.teams.should.deep.contain({
|
||||
name: 'TestTeam',
|
||||
url: 'http://example.org',
|
||||
order: 2,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -16,13 +16,15 @@ describe.skip('security', function desc() {
|
|||
const testURL = `http://localhost:${serverPort}`;
|
||||
|
||||
const config = {
|
||||
version: 1,
|
||||
version: 2,
|
||||
teams: [{
|
||||
name: 'example_1',
|
||||
url: testURL,
|
||||
order: 0,
|
||||
}, {
|
||||
name: 'example_2',
|
||||
url: testURL,
|
||||
order: 1,
|
||||
}],
|
||||
};
|
||||
|
||||
|
|
|
@ -41,6 +41,18 @@ module.exports = merge(base, {
|
|||
use: {
|
||||
loader: 'url-loader',
|
||||
},
|
||||
}, {
|
||||
test: /\.(svg|woff2)$/,
|
||||
use: [
|
||||
{
|
||||
loader: 'file-loader',
|
||||
options: {
|
||||
name: '[hash].[ext]',
|
||||
publicPath: './',
|
||||
},
|
||||
},
|
||||
{loader: 'image-webpack-loader'},
|
||||
],
|
||||
}],
|
||||
},
|
||||
node: {
|
||||
|
|