Files
mattermostest/src/browser/components/MainPage.jsx
FalseHonesty 5d0a937bb9 [MM-21835] Use URL instead of the url library (#1384)
Additionally, migrate all of the URL related helper functions
from `src/utils/utils.js` to the new `src/utils/url.js` file
and migrate tests.

Issue MM-21835
Fixes #1206
2020-11-04 20:59:07 +01:00

912 lines
28 KiB
JavaScript

// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// This files uses setState().
/* eslint-disable react/no-set-state */
import os from 'os';
import React, {Fragment} 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, shell} from 'electron';
import Utils from '../../utils/util';
import urlUtils from '../../utils/url';
import contextmenu from '../js/contextMenu';
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';
import HoveringURL from './HoveringURL.jsx';
import Finder from './Finder.jsx';
import NewTeamModal from './NewTeamModal.jsx';
import SelectCertificateModal from './SelectCertificateModal.jsx';
import PermissionModal from './PermissionModal.jsx';
import ExtraBar from './ExtraBar.jsx';
export default class MainPage extends React.Component {
constructor(props) {
super(props);
let key = this.props.teams.findIndex((team) => team.order === this.props.initialIndex);
if (this.props.deeplinkingUrl !== null) {
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl);
if (parsedDeeplink) {
key = parsedDeeplink.teamIndex;
}
}
this.topBar = React.createRef();
this.state = {
key,
sessionsExpired: new Array(this.props.teams.length),
unreadCounts: new Array(this.props.teams.length),
mentionCounts: new Array(this.props.teams.length),
unreadAtActive: new Array(this.props.teams.length),
mentionAtActiveCounts: new Array(this.props.teams.length),
loginQueue: [],
targetURL: '',
certificateRequests: [],
maximized: false,
showNewTeamModal: false,
focusFinder: false,
finderVisible: false,
};
contextmenu.setup({
useSpellChecker: this.props.useSpellChecker,
onSelectSpellCheckerLocale: (locale) => {
if (this.props.onSelectSpellCheckerLocale) {
this.props.onSelectSpellCheckerLocale(locale);
}
},
});
}
parseDeeplinkURL(deeplink, teams = this.props.teams) {
if (deeplink && Array.isArray(teams) && teams.length) {
const deeplinkURL = urlUtils.parseURL(deeplink);
let parsedDeeplink = null;
teams.forEach((team, index) => {
const teamURL = urlUtils.parseURL(team.url);
if (deeplinkURL.host === teamURL.host) {
parsedDeeplink = {
teamURL,
teamIndex: index,
originalURL: deeplinkURL,
url: `${teamURL.origin}${deeplinkURL.pathname || '/'}`,
path: deeplinkURL.pathname || '/',
};
}
});
return parsedDeeplink;
}
return null;
}
getTabWebContents(index = this.state.key || 0, teams = this.props.teams) {
const allWebContents = remote.webContents.getAllWebContents();
if (this.state.showNewTeamModal) {
const indexURL = '/browser/index.html';
return allWebContents.find((webContents) => webContents.getURL().includes(indexURL));
}
if (!teams || !teams.length || index > teams.length) {
return null;
}
const tabURL = teams[index].url;
if (!tabURL) {
return null;
}
const tab = allWebContents.find((webContents) => webContents.isFocused() && webContents.getURL().includes(this.refs[`mattermostView${index}`].getSrc()));
return tab || remote.webContents.getFocusedWebContents();
}
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) => {
this.loginRequest(event, request, authInfo);
});
ipcRenderer.on('select-user-certificate', (_, origin, certificateList) => {
const certificateRequests = self.state.certificateRequests;
certificateRequests.push({
server: origin,
certificateList,
});
self.setState({
certificateRequests,
});
if (certificateRequests.length === 1) {
self.switchToTabForCertificateRequest(origin);
}
});
// can't switch tabs sequentially for some reason...
ipcRenderer.on('switch-tab', (event, key) => {
const nextIndex = this.props.teams.findIndex((team) => team.order === key);
this.handleSelect(nextIndex);
});
ipcRenderer.on('select-next-tab', () => {
const currentOrder = this.props.teams[this.state.key].order;
const nextOrder = ((currentOrder + 1) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
this.handleSelect(nextIndex);
});
ipcRenderer.on('select-previous-tab', () => {
const currentOrder = this.props.teams[this.state.key].order;
// js modulo operator returns a negative number if result is negative, so we have to ensure it's positive
const nextOrder = ((this.props.teams.length + (currentOrder - 1)) % this.props.teams.length);
const nextIndex = this.props.teams.findIndex((team) => team.order === nextOrder);
this.handleSelect(nextIndex);
});
// reload the activated tab
ipcRenderer.on('reload-tab', () => {
this.refs[`mattermostView${this.state.key}`].reload();
});
ipcRenderer.on('clear-cache-and-reload-tab', () => {
this.refs[`mattermostView${this.state.key}`].clearCacheAndReload();
});
ipcRenderer.on('download-complete', this.showDownloadCompleteNotification);
const currentWindow = remote.getCurrentWindow();
currentWindow.on('focus', self.focusListener);
currentWindow.on('blur', self.blurListener);
window.addEventListener('beforeunload', () => {
currentWindow.removeListener('focus', self.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', () => {
self.focusListener();
});
ipcRenderer.on('open-devtool', () => {
document.getElementById(`mattermostView${self.state.key}`).openDevTools();
});
ipcRenderer.on('zoom-in', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
if (activeTabWebContents.zoomLevel >= 9) {
return;
}
activeTabWebContents.zoomLevel += 1;
});
ipcRenderer.on('zoom-out', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
if (activeTabWebContents.zoomLevel <= -8) {
return;
}
activeTabWebContents.zoomLevel -= 1;
});
ipcRenderer.on('zoom-reset', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.zoomLevel = 0;
});
ipcRenderer.on('undo', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.undo();
});
ipcRenderer.on('redo', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.redo();
});
ipcRenderer.on('cut', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.cut();
});
ipcRenderer.on('copy', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.copy();
});
ipcRenderer.on('paste', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.paste();
});
ipcRenderer.on('paste-and-match', () => {
const activeTabWebContents = this.getTabWebContents(this.state.key);
if (!activeTabWebContents) {
return;
}
activeTabWebContents.pasteAndMatchStyle();
});
//goBack and goForward
ipcRenderer.on('go-back', () => {
const mattermost = self.refs[`mattermostView${self.state.key}`];
if (mattermost.canGoBack()) {
mattermost.goBack();
}
});
ipcRenderer.on('go-forward', () => {
const mattermost = self.refs[`mattermostView${self.state.key}`];
if (mattermost.canGoForward()) {
mattermost.goForward();
}
});
ipcRenderer.on('add-server', () => {
this.addServer();
});
ipcRenderer.on('focus-on-webview', () => {
this.focusOnWebView();
});
ipcRenderer.on('protocol-deeplink', (event, deepLinkUrl) => {
const parsedDeeplink = this.parseDeeplinkURL(deepLinkUrl);
if (parsedDeeplink) {
if (this.state.key !== parsedDeeplink.teamIndex) {
this.handleSelect(parsedDeeplink.teamIndex);
}
self.refs[`mattermostView${parsedDeeplink.teamIndex}`].handleDeepLink(parsedDeeplink.path);
}
});
ipcRenderer.on('toggle-find', () => {
this.activateFinder(true);
});
if (process.platform === 'darwin') {
self.setState({
isDarkMode: remote.nativeTheme.shouldUseDarkColors,
});
remote.systemPreferences.subscribeNotification('AppleInterfaceThemeChangedNotification', () => {
self.setState({
isDarkMode: remote.nativeTheme.shouldUseDarkColors,
});
});
} 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();
}
});
}
}
focusListener = () => {
if (this.state.showNewTeamModal && this.inputRef && this.inputRef.current) {
this.inputRef.current.focus();
} else if (!(this.state.finderVisible && this.state.focusFinder)) {
this.handleOnTeamFocused(this.state.key);
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
}
this.setState({unfocused: false});
}
blurListener = () => {
this.setState({unfocused: true});
}
loginRequest = (event, request, authInfo) => {
const loginQueue = this.state.loginQueue;
loginQueue.push({
request,
authInfo,
});
this.setState({
loginRequired: true,
loginQueue,
});
};
componentDidUpdate(prevProps, prevState) {
if (prevState.key !== this.state.key) { // i.e. When tab has been changed
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
}
}
switchToTabForCertificateRequest = (origin) => {
// origin is server name + port, if the port doesn't match the protocol, it is kept by URL
const originURL = urlUtils.parseURL(`http://${origin.split(':')[0]}`);
const secureOriginURL = urlUtils.parseURL(`https://${origin.split(':')[0]}`);
const key = this.props.teams.findIndex((team) => {
const parsedURL = urlUtils.parseURL(team.url);
return (parsedURL.origin === originURL.origin) || (parsedURL.origin === secureOriginURL.origin);
});
this.handleSelect(key);
};
handleInterTeamLink = (linkUrl) => {
const selectedTeam = urlUtils.getServer(linkUrl, this.props.teams);
if (!selectedTeam) {
return;
}
this.refs[`mattermostView${selectedTeam.index}`].handleDeepLink(linkUrl.href);
this.setState({key: selectedTeam.index});
}
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({
key: newKey,
finderVisible: false,
});
const webview = document.getElementById('mattermostView' + newKey);
ipcRenderer.send('update-title', {
title: webview.getTitle(),
});
window.focus();
webview.focus();
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;
const mentionCounts = this.state.mentionCounts;
const unreadAtActive = this.state.unreadAtActive;
const mentionAtActiveCounts = this.state.mentionAtActiveCounts;
sessionsExpired[index] = sessionExpired;
unreadCounts[index] = unreadCount;
mentionCounts[index] = mentionCount;
// Never turn on the unreadAtActive flag at current focused tab.
if (this.state.key !== index || !remote.getCurrentWindow().isFocused()) {
unreadAtActive[index] = unreadAtActive[index] || isUnread;
if (isMentioned) {
mentionAtActiveCounts[index]++;
}
}
this.setState({
sessionsExpired,
unreadCounts,
mentionCounts,
unreadAtActive,
mentionAtActiveCounts,
});
this.handleBadgesChange();
}
markReadAtActive = (index) => {
const unreadAtActive = this.state.unreadAtActive;
const mentionAtActiveCounts = this.state.mentionAtActiveCounts;
unreadAtActive[index] = false;
mentionAtActiveCounts[index] = 0;
this.setState({
unreadAtActive,
mentionAtActiveCounts,
});
this.handleBadgesChange();
}
handleBadgesChange = () => {
if (this.props.onBadgeChange) {
const someSessionsExpired = this.state.sessionsExpired.some((sessionExpired) => sessionExpired);
let allUnreadCount = this.state.unreadCounts.reduce((prev, curr) => {
return prev + curr;
}, 0);
this.state.unreadAtActive.forEach((state) => {
if (state) {
allUnreadCount += 1;
}
});
let allMentionCount = this.state.mentionCounts.reduce((prev, curr) => {
return prev + curr;
}, 0);
this.state.mentionAtActiveCounts.forEach((count) => {
allMentionCount += count;
});
this.props.onBadgeChange(someSessionsExpired, allUnreadCount, allMentionCount);
}
}
handleOnTeamFocused = (index) => {
// Turn off the flag to indicate whether unread message of active channel contains at current tab.
this.markReadAtActive(index);
}
handleLogin = (request, username, password) => {
ipcRenderer.send('login-credentials', request, username, password);
const loginQueue = this.state.loginQueue;
loginQueue.shift();
this.setState({loginQueue});
}
handleLoginCancel = (request) => {
ipcRenderer.send('login-cancel', request);
const loginQueue = this.state.loginQueue;
loginQueue.shift();
this.setState({loginQueue});
}
handleTargetURLChange = (targetURL) => {
clearTimeout(this.targetURLDisappearTimeout);
if (targetURL === '' || this.parseDeeplinkURL(targetURL, [this.props.teams[this.state.key]])) { // Do not show URL for internal links on current team
// set delay to avoid momentary disappearance when hovering over multiple links
this.targetURLDisappearTimeout = setTimeout(() => {
this.setState({targetURL: ''});
}, 500);
} else {
this.setState({targetURL});
}
}
handleClose = (e) => {
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
const win = remote.getCurrentWindow();
win.close();
}
handleMinimize = (e) => {
e.stopPropagation();
const win = remote.getCurrentWindow();
win.minimize();
}
handleMaximize = (e) => {
e.stopPropagation();
const win = remote.getCurrentWindow();
win.maximize();
}
handleRestore = () => {
const win = remote.getCurrentWindow();
win.restore();
}
openMenu = () => {
// @eslint-ignore
this.threeDotMenu.current.blur();
this.props.openMenu();
}
handleDoubleClick = () => {
if (process.platform === 'darwin') {
const doubleClickAction = remote.systemPreferences.getUserDefault('AppleActionOnDoubleClick', 'string');
const win = remote.getCurrentWindow();
if (doubleClickAction === 'Minimize') {
win.minimize();
} else if (!win.isMaximized()) {
win.maximize();
} else if (win.isMaximized()) {
win.unmaximize();
}
}
}
addServer = () => {
this.setState({
showNewTeamModal: true,
});
}
focusOnWebView = () => {
this.refs[`mattermostView${this.state.key}`].focusOnWebView();
}
activateFinder = () => {
this.setState({
finderVisible: true,
focusFinder: true,
});
}
closeFinder = () => {
this.setState({
finderVisible: false,
focusFinder: false,
});
}
inputFocus = (e, focus) => {
this.setState({
focusFinder: focus,
});
}
handleSelectCertificate = (certificate) => {
const certificateRequests = this.state.certificateRequests;
const current = certificateRequests.shift();
this.setState({certificateRequests});
ipcRenderer.send('selected-client-certificate', current.server, certificate);
if (certificateRequests.length > 0) {
this.switchToTabForCertificateRequest(certificateRequests[0].server);
}
}
handleCancelCertificate = () => {
const certificateRequests = this.state.certificateRequests;
const current = certificateRequests.shift();
this.setState({certificateRequests});
ipcRenderer.send('selected-client-certificate', current.server);
if (certificateRequests.length > 0) {
this.switchToTabForCertificateRequest(certificateRequests[0].server);
}
};
showDownloadCompleteNotification = async (event, item) => {
const title = process.platform === 'win32' ? item.serverInfo.name : 'Download Complete';
const notificationBody = process.platform === 'win32' ? `Download Complete \n ${item.fileName}` : item.fileName;
await Utils.dispatchNotification(title, notificationBody, false, {}, () => {
shell.showItemInFolder(item.path.normalize());
});
}
setDarkMode() {
this.setState({
isDarkMode: this.props.setDarkMode(),
});
}
setInputRef = (ref) => {
this.inputRef = ref;
}
showExtraBar = () => {
const ref = this.refs[`mattermostView${this.state.key}`];
if (typeof ref !== 'undefined') {
return !urlUtils.isTeamUrl(this.props.teams[this.state.key].url, ref.getSrc());
}
return false;
}
render() {
const self = this;
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 (os.platform() === 'win32' && os.release().startsWith('10')) {
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}
aria-label='Context menu'
>
<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);
}
function handleNotificationClick() {
self.handleSelect(index);
}
const id = 'mattermostView' + index;
const isActive = self.state.key === index;
let teamUrl = team.url;
if (this.props.deeplinkingUrl) {
const parsedDeeplink = this.parseDeeplinkURL(this.props.deeplinkingUrl, [team]);
if (parsedDeeplink) {
teamUrl = parsedDeeplink.url;
}
}
return (
<MattermostView
key={id}
id={id}
teams={this.props.teams}
useSpellChecker={this.props.useSpellChecker}
onSelectSpellCheckerLocale={this.props.onSelectSpellCheckerLocale}
src={teamUrl}
name={team.name}
onTargetURLChange={self.handleTargetURLChange}
onBadgeChange={handleBadgeChange}
onNotificationClick={handleNotificationClick}
handleInterTeamLink={self.handleInterTeamLink}
ref={id}
active={isActive}
allowExtraBar={this.showExtraBar()}
/>);
});
const viewsRow = (
<Fragment>
<ExtraBar
darkMode={this.state.isDarkMode}
show={this.showExtraBar()}
mattermostView={this.refs[`mattermostView${this.state.key}`]}
/>
<Row>
{views}
</Row>
</Fragment>);
let request = null;
let authServerURL = null;
let authInfo = null;
if (this.state.loginQueue.length !== 0) {
request = this.state.loginQueue[0].request;
const tmpURL = urlUtils.parseURL(this.state.loginQueue[0].request.url);
authServerURL = tmpURL.origin;
authInfo = this.state.loginQueue[0].authInfo;
}
const modal = (
<NewTeamModal
currentOrder={this.props.teams.length}
show={this.state.showNewTeamModal}
setInputRef={this.setInputRef}
onClose={() => {
this.setState({
showNewTeamModal: false,
});
}}
onSave={(newTeam) => {
this.props.localTeams.push(newTeam);
this.props.onTeamConfigChange(this.props.localTeams, () => {
self.setState({
showNewTeamModal: false,
key: this.props.teams.length - 1,
});
});
}}
/>
);
return (
<div
className='MainPage'
onClick={this.focusOnWebView}
>
<LoginModal
show={this.state.loginQueue.length !== 0}
request={request}
authInfo={authInfo}
authServerURL={authServerURL}
onLogin={this.handleLogin}
onCancel={this.handleLoginCancel}
/>
<PermissionModal/>
<SelectCertificateModal
certificateRequests={this.state.certificateRequests}
onSelect={this.handleSelectCertificate}
onCancel={this.handleCancelCertificate}
/>
<Grid fluid={true}>
{ topRow }
{ viewsRow }
{ this.state.finderVisible ? (
<Finder
webviewKey={this.state.key}
close={this.closeFinder}
focusState={this.state.focusFinder}
inputFocus={this.inputFocus}
/>
) : null}
</Grid>
<TransitionGroup>
{ (this.state.targetURL === '') ?
null :
<CSSTransition
classNames='hovering'
timeout={{enter: 300, exit: 500}}
>
<HoveringURL
key='hoveringURL'
targetURL={this.state.targetURL}
/>
</CSSTransition>
}
</TransitionGroup>
<div>
{ modal }
</div>
</div>
);
}
}
MainPage.propTypes = {
onBadgeChange: PropTypes.func.isRequired,
teams: PropTypes.array.isRequired,
localTeams: PropTypes.array.isRequired,
onTeamConfigChange: PropTypes.func.isRequired,
initialIndex: PropTypes.number.isRequired,
useSpellChecker: PropTypes.bool.isRequired,
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 */