Files
mattermostest/src/browser/components/MattermostView.jsx
Devin Binnie 932ddafdb0 [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>
2020-01-03 12:00:43 -05:00

372 lines
11 KiB
JavaScript

// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This file uses setState().
/* eslint-disable react/no-set-state */
import url from 'url';
import React from 'react';
import PropTypes from 'prop-types';
import {ipcRenderer, remote, shell} from 'electron';
import log from 'electron-log';
import contextMenu from '../js/contextMenu';
import Utils from '../../utils/util';
import {protocols} from '../../../electron-builder.json';
const scheme = protocols[0].schemes[0];
import ErrorView from './ErrorView.jsx';
const preloadJS = `file://${remote.app.getAppPath()}/browser/webview/mattermost_bundle.js`;
const ERR_NOT_IMPLEMENTED = -11;
const U2F_EXTENSION_URL = 'chrome-extension://kmendfapggjehodndflmmgagdbamhnfd/u2f-comms.html';
const appIconURL = `file:///${remote.app.getAppPath()}/assets/appicon_48.png`;
export default class MattermostView extends React.Component {
constructor(props) {
super(props);
this.state = {
errorInfo: null,
isContextMenuAdded: false,
reloadTimeoutID: null,
isLoaded: false,
basename: '/',
};
this.webviewRef = React.createRef();
}
handleUnreadCountChange = (sessionExpired, unreadCount, mentionCount, isUnread, isMentioned) => {
if (this.props.onBadgeChange) {
this.props.onBadgeChange(sessionExpired, unreadCount, mentionCount, isUnread, isMentioned);
}
}
dispatchNotification = async (title, body, channel, teamId, silent) => {
const permission = await Notification.requestPermission();
if (permission !== 'granted') {
log.error('Notifications not granted');
return;
}
const notification = new Notification(title, {
body,
tag: body,
icon: appIconURL,
requireInteraction: false,
silent,
});
notification.onclick = () => {
this.webviewRef.current.send('notification-clicked', {channel, teamId});
};
notification.onerror = () => {
log.error('Notification failed to show');
};
}
componentDidMount() {
const self = this;
const webview = this.webviewRef.current;
webview.addEventListener('did-fail-load', (e) => {
console.log(self.props.name, 'webview did-fail-load', e);
if (e.errorCode === -3) { // An operation was aborted (due to user action).
return;
}
if (e.errorCode === ERR_NOT_IMPLEMENTED && e.validatedURL === U2F_EXTENSION_URL) {
// U2F device is not supported, but the guest page should fall back to PIN code in 2FA.
// https://github.com/mattermost/desktop/issues/708
return;
}
self.setState({
errorInfo: e,
isLoaded: true,
});
function reload() {
window.removeEventListener('online', reload);
self.reload();
}
if (navigator.onLine) {
self.setState({
reloadTimeoutID: setTimeout(reload, 30000),
});
} else {
window.addEventListener('online', reload);
}
});
// Open link in browserWindow. for example, attached files.
webview.addEventListener('new-window', (e) => {
if (!Utils.isValidURI(e.url)) {
return;
}
const currentURL = url.parse(webview.getURL());
const destURL = url.parse(e.url);
if (destURL.protocol !== 'http:' && destURL.protocol !== 'https:' && destURL.protocol !== `${scheme}:`) {
ipcRenderer.send('confirm-protocol', destURL.protocol, e.url);
return;
}
if (Utils.isInternalURL(destURL, currentURL, this.state.basename)) {
if (destURL.path.match(/^\/api\/v[3-4]\/public\/files\//)) {
ipcRenderer.send('download-url', e.url);
} else if (destURL.path.match(/^\/help\//)) {
// continue to open special case internal urls in default browser
shell.openExternal(e.url);
} else {
// New window should disable nodeIntegration.
window.open(e.url, remote.app.getName(), 'nodeIntegration=no, contextIsolation=yes, show=yes');
}
} else {
// if the link is external, use default browser.
shell.openExternal(e.url);
}
});
// 'dom-ready' means "content has been loaded"
// So this would be emitted again when reloading a webview
webview.addEventListener('dom-ready', () => {
// webview.openDevTools();
// Remove this once https://github.com/electron/electron/issues/14474 is fixed
// - fixes missing cursor bug in electron
// - only apply this focus fix if the current view is active
if (this.props.active) {
webview.blur();
webview.focus();
}
if (!this.state.isContextMenuAdded) {
contextMenu.setup(webview, {
useSpellChecker: this.props.useSpellChecker,
onSelectSpellCheckerLocale: (locale) => {
if (this.props.onSelectSpellCheckerLocale) {
this.props.onSelectSpellCheckerLocale(locale);
}
webview.send('set-spellchecker');
},
});
this.setState({isContextMenuAdded: true});
}
});
webview.addEventListener('update-target-url', (event) => {
if (self.props.onTargetURLChange) {
self.props.onTargetURLChange(event.url);
}
});
webview.addEventListener('ipc-message', (event) => {
switch (event.channel) {
case 'onGuestInitialized':
self.setState({
isLoaded: true,
basename: event.args[0] || '/',
});
break;
case 'onBadgeChange': {
self.handleUnreadCountChange(...event.args);
break;
}
case 'dispatchNotification': {
self.dispatchNotification(...event.args);
break;
}
case 'onNotificationClick':
self.props.onNotificationClick();
break;
case 'mouse-move':
this.handleMouseMove(event.args[0]);
break;
case 'mouse-up':
this.handleMouseUp();
break;
}
});
webview.addEventListener('page-title-updated', (event) => {
if (self.props.active) {
ipcRenderer.send('update-title', {
title: event.title,
});
}
});
webview.addEventListener('console-message', (e) => {
const message = `[${this.props.name}] ${e.message}`;
switch (e.level) {
case 0:
console.log(message);
break;
case 1:
console.warn(message);
break;
case 2:
console.error(message);
break;
default:
console.log(message);
break;
}
});
// start listening for user status updates from main
ipcRenderer.on('user-activity-update', this.handleUserActivityUpdate);
ipcRenderer.on('exit-fullscreen', this.handleExitFullscreen);
}
componentWillUnmount() {
// stop listening for user status updates from main
ipcRenderer.removeListener('user-activity-update', this.handleUserActivityUpdate);
ipcRenderer.removeListener('exit-fullscreen', this.handleExitFullscreen);
}
reload = () => {
clearTimeout(this.state.reloadTimeoutID);
this.setState({
errorInfo: null,
reloadTimeoutID: null,
isLoaded: false,
});
const webview = this.webviewRef.current;
webview.reload();
}
clearCacheAndReload = () => {
this.setState({
errorInfo: null,
});
const webContents = this.webviewRef.current.getWebContents();
webContents.session.clearCache(() => {
webContents.reload();
});
}
focusOnWebView = () => {
const webview = this.webviewRef.current;
const webContents = webview.getWebContents(); // webContents might not be created yet.
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();
}
canGoForward = () => {
const webview = this.webviewRef.current;
return webview.getWebContents().canGoForward();
}
goBack = () => {
const webview = this.webviewRef.current;
webview.getWebContents().goBack();
}
goForward = () => {
const webview = this.webviewRef.current;
webview.getWebContents().goForward();
}
getSrc = () => {
const webview = this.webviewRef.current;
return webview.src;
}
handleDeepLink = (relativeUrl) => {
const webview = this.webviewRef.current;
webview.executeJavaScript(
'history.pushState(null, null, "' + relativeUrl + '");'
);
webview.executeJavaScript(
'dispatchEvent(new PopStateEvent("popstate", null));'
);
}
handleUserActivityUpdate = (event, status) => {
// pass user activity update to the webview
this.webviewRef.current.send('user-activity-update', status);
}
handleExitFullscreen = () => {
// pass exit fullscreen request to the webview
this.webviewRef.current.send('exit-fullscreen');
}
render() {
const errorView = this.state.errorInfo ? (
<ErrorView
id={this.props.id + '-fail'}
className='errorView'
errorInfo={this.state.errorInfo}
active={this.props.active}
/>) : null;
// Need to keep webview mounted when failed to load.
const classNames = ['mattermostView'];
if (this.props.withTab) {
classNames.push('mattermostView-with-tab');
}
if (!this.props.active || this.state.errorInfo) {
classNames.push('mattermostView-hidden');
}
const loadingImage = !this.state.errorInfo && this.props.active && !this.state.isLoaded ? (
<div className='mattermostView-loadingScreen'>
<img
className='mattermostView-loadingImage'
src='../assets/loading.gif'
srcSet='../assets/loading.gif 1x, ../assets/loading@2x.gif 2x'
/>
</div>
) : null;
return (
<div
className={classNames.join(' ')}
>
{ errorView }
<webview
id={this.props.id}
preload={preloadJS}
src={this.props.src}
ref={this.webviewRef}
/>
{ loadingImage }
</div>);
}
}
MattermostView.propTypes = {
name: PropTypes.string,
id: PropTypes.string,
withTab: PropTypes.bool,
onTargetURLChange: PropTypes.func,
onBadgeChange: PropTypes.func,
src: PropTypes.string,
active: PropTypes.bool,
useSpellChecker: PropTypes.bool,
onSelectSpellCheckerLocale: PropTypes.func,
};
/* eslint-enable react/no-set-state */