[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>
This commit is contained in:
Devin Binnie 2020-01-03 12:00:43 -05:00 committed by GitHub
parent 2a426ebe09
commit 932ddafdb0
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
63 changed files with 2762 additions and 212 deletions

4
.gitignore vendored
View file

@ -14,3 +14,7 @@ test-results.xml
test_config.json
.idea
testUserData
src/browser/*.png
src/browser/*.svg
src/browser/*.woff2

View file

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

File diff suppressed because it is too large Load diff

View file

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

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

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 732 B

BIN
src/assets/linux/dark/MenuIconTemplate.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 649 B

BIN
src/assets/linux/dark/MenuIconUnreadTemplate.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 730 B

BIN
src/assets/linux/light/MenuIconMentionTemplate.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 872 B

After

Width:  |  Height:  |  Size: 768 B

BIN
src/assets/linux/light/MenuIconTemplate.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 544 B

After

Width:  |  Height:  |  Size: 692 B

BIN
src/assets/linux/light/MenuIconUnreadTemplate.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1 KiB

After

Width:  |  Height:  |  Size: 766 B

BIN
src/assets/osx/ClickedMenuIcon.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 332 B

After

Width:  |  Height:  |  Size: 447 B

BIN
src/assets/osx/ClickedMenuIcon@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 598 B

After

Width:  |  Height:  |  Size: 956 B

BIN
src/assets/osx/ClickedMenuIconMention.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 345 B

After

Width:  |  Height:  |  Size: 519 B

BIN
src/assets/osx/ClickedMenuIconMention@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 688 B

After

Width:  |  Height:  |  Size: 1.1 KiB

BIN
src/assets/osx/ClickedMenuIconUnread.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 335 B

After

Width:  |  Height:  |  Size: 513 B

BIN
src/assets/osx/ClickedMenuIconUnread@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 684 B

After

Width:  |  Height:  |  Size: 1 KiB

BIN
src/assets/osx/MenuIcon.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 426 B

BIN
src/assets/osx/MenuIcon@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 719 B

After

Width:  |  Height:  |  Size: 883 B

BIN
src/assets/osx/MenuIconMention.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 342 B

After

Width:  |  Height:  |  Size: 482 B

BIN
src/assets/osx/MenuIconMention@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 766 B

After

Width:  |  Height:  |  Size: 1,012 B

BIN
src/assets/osx/MenuIconUnread.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 344 B

After

Width:  |  Height:  |  Size: 478 B

BIN
src/assets/osx/MenuIconUnread@2x.png Normal file → Executable file

Binary file not shown.

Before

Width:  |  Height:  |  Size: 760 B

After

Width:  |  Height:  |  Size: 1,001 B

View 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

View 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

View 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

View 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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.2 KiB

After

Width:  |  Height:  |  Size: 2.4 KiB

View file

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

View file

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

View file

@ -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,16 +456,52 @@ 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') {
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
}
focusOnWebView = () => {
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
}
activateFinder = () => {
@ -393,29 +523,116 @@ 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>
<TabBar
id='tabBar'
teams={this.props.teams}
sessionsExpired={this.state.sessionsExpired}
unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts}
unreadAtActive={this.state.unreadAtActive}
mentionAtActiveCounts={this.state.mentionAtActiveCounts}
activeKey={this.state.key}
onSelect={this.handleSelect}
onAddServer={this.addServer}
showAddServerButton={this.props.showAddServerButton}
/>
</Row>
const tabsRow = (
<TabBar
id='tabBar'
isDarkMode={this.state.isDarkMode}
teams={this.props.teams}
sessionsExpired={this.state.sessionsExpired}
unreadCounts={this.state.unreadCounts}
mentionCounts={this.state.mentionCounts}
unreadAtActive={this.state.unreadAtActive}
mentionAtActiveCounts={this.state.mentionAtActiveCounts}
activeKey={this.state.key}
onSelect={this.handleSelect}
onAddServer={this.addServer}
showAddServerButton={this.props.showAddServerButton}
onDrop={this.handleDragAndDrop}
/>
);
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 */

View file

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

View file

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

View file

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

View file

@ -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}
ref={id}
active={this.props.activeKey === index}
activeKey={this.props.activeKey}
onMouseDown={() => {
this.props.onSelect(index);
}}
onSelect={() => {
this.props.onSelect(index);
}}
title={team.name}
>
<span
title={team.name}
className={unreadCount === 0 ? '' : 'teamTabItem-unread'}
>
{team.name}
</span>
{ ' ' }
{ badgeDiv }
</NavItem>);
<div className='TabBar-tabSeperator'>
<span>
{team.name}
</span>
{ badgeDiv }
</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,
};

View file

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

View file

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

View file

@ -8,7 +8,6 @@
border: none;
background: #d2d2d2;
outline: none;
cursor: pointer;
font-size: 18px;
height: 26px;
}

View file

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

View file

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

View file

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

View file

@ -5,5 +5,4 @@
.TeamListItem-left {
display: inline-block;
width: calc(100% - 100px);
cursor: pointer;
}

View file

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

View file

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

View file

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

View file

@ -72,6 +72,7 @@ export default class RegistryConfig extends EventEmitter {
teams.push({
name: team.name,
url: team.value,
order: team.order,
});
}
return teams;

View file

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

View file

@ -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,10 +212,16 @@ 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 {
configData = Validator.validateV0ConfigData(configData);
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.');

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -116,79 +116,92 @@ function createTemplate(mainWindow, config, isDev) {
role: 'selectall',
}],
});
const viewSubMenu = [{
label: 'Find..',
accelerator: 'CmdOrCtrl+F',
click(item, focusedWindow) {
focusedWindow.webContents.send('toggle-find');
},
}, {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow === mainWindow) {
mainWindow.webContents.send('reload-tab');
} else {
focusedWindow.reload();
}
}
},
}, {
label: 'Clear Cache and Reload',
accelerator: 'Shift+CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow === mainWindow) {
mainWindow.webContents.send('clear-cache-and-reload-tab');
} else {
focusedWindow.webContents.session.clearCache(() => {
focusedWindow.reload();
});
}
}
},
}, {
role: 'togglefullscreen',
}, separatorItem, {
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click() {
mainWindow.webContents.send('zoom-reset');
},
}, {
label: 'Zoom In',
accelerator: 'CmdOrCtrl+SHIFT+=',
click() {
mainWindow.webContents.send('zoom-in');
},
}, {
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click() {
mainWindow.webContents.send('zoom-out');
},
}, separatorItem, {
label: 'Developer Tools for Application Wrapper',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
}
return 'Ctrl+Shift+I';
})(),
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools();
}
},
}, {
label: 'Developer Tools for Current Server',
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: [{
label: 'Find..',
accelerator: 'CmdOrCtrl+F',
click(item, focusedWindow) {
focusedWindow.webContents.send('toggle-find');
},
}, {
label: 'Reload',
accelerator: 'CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow === mainWindow) {
mainWindow.webContents.send('reload-tab');
} else {
focusedWindow.reload();
}
}
},
}, {
label: 'Clear Cache and Reload',
accelerator: 'Shift+CmdOrCtrl+R',
click(item, focusedWindow) {
if (focusedWindow) {
if (focusedWindow === mainWindow) {
mainWindow.webContents.send('clear-cache-and-reload-tab');
} else {
focusedWindow.webContents.session.clearCache(() => {
focusedWindow.reload();
});
}
}
},
}, {
role: 'togglefullscreen',
}, separatorItem, {
label: 'Actual Size',
accelerator: 'CmdOrCtrl+0',
click() {
mainWindow.webContents.send('zoom-reset');
},
}, {
label: 'Zoom In',
accelerator: 'CmdOrCtrl+SHIFT+=',
click() {
mainWindow.webContents.send('zoom-in');
},
}, {
label: 'Zoom Out',
accelerator: 'CmdOrCtrl+-',
click() {
mainWindow.webContents.send('zoom-out');
},
}, separatorItem, {
label: 'Developer Tools for Application Wrapper',
accelerator: (() => {
if (process.platform === 'darwin') {
return 'Alt+Command+I';
}
return 'Ctrl+Shift+I';
})(),
click(item, focusedWindow) {
if (focusedWindow) {
focusedWindow.toggleDevTools();
}
},
}, {
label: 'Developer Tools for Current Server',
click() {
mainWindow.webContents.send('open-devtool');
},
}],
submenu: viewSubMenu,
});
template.push({
label: '&History',

View file

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

View file

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

View file

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

View file

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

View file

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