Migrate app to TypeScript (#1637)

* Initial setup and migrated src/common

* WIP

* WIP

* WIP

* Main module basically finished

* Renderer process migrated

* Added CI step and some fixes

* Fixed remainder of issues and added proper ESLint config

* Fixed a couple issues

* Progress!

* Some more fixes

* Fixed a test

* Fix build step

* PR feedback
This commit is contained in:
Devin Binnie
2021-06-28 09:51:23 -04:00
committed by GitHub
parent 422673a740
commit 1b3d0eac8f
115 changed files with 16246 additions and 9921 deletions

View File

@@ -1,35 +1,42 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Alert} from 'react-bootstrap';
const baseClassName = 'AutoSaveIndicator';
const leaveClassName = `${baseClassName}-Leave`;
const SAVING_STATE_SAVING = 'saving';
const SAVING_STATE_SAVED = 'saved';
const SAVING_STATE_ERROR = 'error';
const SAVING_STATE_DONE = 'done';
export enum SavingState {
SAVING_STATE_SAVING = 'saving',
SAVING_STATE_SAVED = 'saved',
SAVING_STATE_ERROR = 'error',
SAVING_STATE_DONE = 'done',
}
function getClassNameAndMessage(savingState, errorMessage) {
function getClassNameAndMessage(savingState: SavingState, errorMessage?: string) {
switch (savingState) {
case SAVING_STATE_SAVING:
case SavingState.SAVING_STATE_SAVING:
return {className: baseClassName, message: 'Saving...'};
case SAVING_STATE_SAVED:
case SavingState.SAVING_STATE_SAVED:
return {className: baseClassName, message: 'Saved'};
case SAVING_STATE_ERROR:
case SavingState.SAVING_STATE_ERROR:
return {className: `${baseClassName}`, message: errorMessage};
case SAVING_STATE_DONE:
case SavingState.SAVING_STATE_DONE:
return {className: `${baseClassName} ${leaveClassName}`, message: 'Saved'};
default:
return {className: `${baseClassName} ${leaveClassName}`, message: ''};
}
}
export default function AutoSaveIndicator(props) {
type Props = {
id?: string;
savingState: SavingState;
errorMessage?: string;
};
export default function AutoSaveIndicator(props: Props) {
const {savingState, errorMessage, ...rest} = props;
const {className, message} = getClassNameAndMessage(savingState, errorMessage);
return (
@@ -42,15 +49,3 @@ export default function AutoSaveIndicator(props) {
</Alert>
);
}
AutoSaveIndicator.propTypes = {
savingState: PropTypes.string.isRequired,
errorMessage: PropTypes.string,
};
Object.assign(AutoSaveIndicator, {
SAVING_STATE_SAVING,
SAVING_STATE_SAVED,
SAVING_STATE_ERROR,
SAVING_STATE_DONE,
});

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import {storiesOf} from '@storybook/react';

View File

@@ -1,12 +1,21 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Modal} from 'react-bootstrap';
export default function DestructiveConfirmationModal(props) {
type Props = {
title: string;
body: React.ReactNode;
acceptLabel: string;
cancelLabel: string;
onHide: () => void;
onAccept: React.MouseEventHandler<Button>;
onCancel: React.MouseEventHandler<Button>;
};
export default function DestructiveConfirmationModal(props: Props) {
const {
title,
body,
@@ -14,9 +23,13 @@ export default function DestructiveConfirmationModal(props) {
cancelLabel,
onAccept,
onCancel,
onHide,
...rest} = props;
return (
<Modal {...rest}>
<Modal
onHide={onHide}
{...rest}
>
<Modal.Header closeButton={true}>
<Modal.Title>{title}</Modal.Title>
</Modal.Header>
@@ -34,12 +47,3 @@ export default function DestructiveConfirmationModal(props) {
</Modal>
);
}
DestructiveConfirmationModal.propTypes = {
title: PropTypes.string.isRequired,
body: PropTypes.node.isRequired,
acceptLabel: PropTypes.string.isRequired,
cancelLabel: PropTypes.string.isRequired,
onAccept: PropTypes.func.isRequired,
onCancel: PropTypes.func.isRequired,
};

View File

@@ -1,14 +1,21 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
// ErrorCode: https://code.google.com/p/chromium/codesearch#chromium/src/net/base/net_error_list.h
import React from 'react';
import PropTypes from 'prop-types';
import {Grid, Row, Col} from 'react-bootstrap';
export default function ErrorView(props) {
type Props = {
errorInfo?: string;
url?: string;
id?: string;
active?: boolean;
appName?: string;
};
export default function ErrorView(props: Props) {
const classNames = ['container', 'ErrorView'];
if (!props.active) {
classNames.push('ErrorView-hidden');
@@ -58,7 +65,7 @@ export default function ErrorView(props) {
target='_blank'
rel='noreferrer'
>
{props.errorInfo.validatedURL}
{props.url}
</a>{' from a browser window.'}</li>
</ul>
<br/>
@@ -78,11 +85,3 @@ export default function ErrorView(props) {
</Grid>
);
}
ErrorView.propTypes = {
errorInfo: PropTypes.string,
url: PropTypes.string,
id: PropTypes.string,
active: PropTypes.bool,
appName: PropTypes.string,
};

View File

@@ -2,10 +2,15 @@
// See LICENSE.txt for license information.
import React from 'react';
import PropTypes from 'prop-types';
import {Row, Button} from 'react-bootstrap';
export default class ExtraBar extends React.PureComponent {
type Props = {
darkMode?: boolean;
goBack?: () => void;
show?: boolean;
};
export default class ExtraBar extends React.PureComponent<Props> {
handleBack = () => {
if (this.props.goBack) {
this.props.goBack();
@@ -42,9 +47,3 @@ export default class ExtraBar extends React.PureComponent {
);
}
}
ExtraBar.propTypes = {
darkMode: PropTypes.bool,
goBack: PropTypes.func,
show: PropTypes.bool,
};

View File

@@ -3,11 +3,10 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import useAnimationEnd from '../../hooks/useAnimationEnd.js';
import useAnimationEnd from '../../hooks/useAnimationEnd';
import LoadingIcon from './LoadingIcon.jsx';
import LoadingIcon from './LoadingIcon';
const LOADING_STATE = {
INITIALIZING: 'initializing', // animation graphics are hidden
@@ -18,6 +17,12 @@ const LOADING_STATE = {
const ANIMATION_COMPLETION_DELAY = 500;
type Props = {
loading: boolean;
darkMode: boolean;
onLoadAnimationComplete?: () => void;
}
/**
* A function component for rendering the animated MM logo loading sequence
* @param {boolean} loading - Prop that indicates whether currently loading or not
@@ -27,7 +32,7 @@ const ANIMATION_COMPLETION_DELAY = 500;
function LoadingAnimation({
loading = false,
darkMode = false,
onLoadAnimationComplete = null},
onLoadAnimationComplete = undefined}: Props,
) {
const loadingIconContainerRef = React.useRef(null);
const [animationState, setAnimationState] = React.useState(LOADING_STATE.INITIALIZING);
@@ -55,14 +60,14 @@ function LoadingAnimation({
}, [loadingAnimationComplete]);
// listen for end of the css logo animation sequence
useAnimationEnd(loadingIconContainerRef, () => {
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
setTimeout(() => {
setLoadingAnimationComplete(true);
}, ANIMATION_COMPLETION_DELAY);
}, 'LoadingAnimation__compass-shrink');
// listen for end of final css logo fade/shrink animation sequence
useAnimationEnd(loadingIconContainerRef, () => {
useAnimationEnd<HTMLDivElement>(loadingIconContainerRef, () => {
if (onLoadAnimationComplete) {
onLoadAnimationComplete();
}
@@ -84,10 +89,4 @@ function LoadingAnimation({
);
}
LoadingAnimation.propTypes = {
loading: PropTypes.bool,
darkMode: PropTypes.bool,
onLoadAnimationComplete: PropTypes.func,
};
export default LoadingAnimation;

View File

@@ -1,4 +1,4 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
export {default} from './LoadingAnimation.jsx';
export {default} from './LoadingAnimation';

View File

@@ -3,19 +3,24 @@
import React from 'react';
import classNames from 'classnames';
import PropTypes from 'prop-types';
import useTransitionEnd from '../hooks/useTransitionEnd.js';
import useTransitionEnd from '../hooks/useTransitionEnd';
import LoadingAnimation from './LoadingAnimation';
type Props = {
loading?: boolean;
darkMode?: boolean;
onFadeOutComplete?: () => void;
};
/**
* A function component for rendering the desktop app loading screen
* @param {boolean} loading - Prop that indicates whether currently loading or not
* @param {boolean} darkMode - Prop that indicates if dark mode is enabled
* @param {() => void} onFadeOutComplete - Function to call when the loading animation is completely finished
*/
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}) {
function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = () => null}: Props) {
const loadingScreenRef = React.useRef(null);
const [loadingIsComplete, setLoadingIsComplete] = React.useState(true);
@@ -35,10 +40,10 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
setLoadAnimationIsComplete(true);
}
useTransitionEnd(loadingScreenRef, React.useCallback(() => {
useTransitionEnd<HTMLDivElement>(loadingScreenRef, React.useCallback(() => {
setFadeOutIsComplete(true);
onFadeOutComplete();
}), ['opacity']);
}, []), ['opacity']);
function loadingInProgress() {
return !(loadingIsComplete && loadAnimationIsComplete && fadeOutIsComplete);
@@ -69,10 +74,4 @@ function LoadingScreen({loading = false, darkMode = false, onFadeOutComplete = (
return loadingInProgress() ? loadingScreen : null;
}
LoadingScreen.propTypes = {
loading: PropTypes.bool,
darkMode: PropTypes.bool,
onFadeOutComplete: PropTypes.func,
};
export default LoadingScreen;

View File

@@ -1,11 +1,14 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Grid, Row} from 'react-bootstrap';
import DotsVerticalIcon from 'mdi-react/DotsVerticalIcon';
import {IpcRendererEvent} from 'electron/renderer';
import {DropResult} from 'react-smooth-dnd';
import {Team} from 'types/config';
import {
FOCUS_BROWSERVIEW,
@@ -42,18 +45,54 @@ import closeButton from '../../assets/titlebar/chrome-close.svg';
import {playSound} from '../notificationSounds';
import TabBar from './TabBar.jsx';
import ExtraBar from './ExtraBar.jsx';
import ErrorView from './ErrorView.jsx';
import TabBar from './TabBar';
import ExtraBar from './ExtraBar';
import ErrorView from './ErrorView';
const LOADING = 1;
const DONE = 2;
const RETRY = -1;
const FAILED = 0;
const NOSERVERS = -2;
enum Status {
LOADING = 1,
DONE = 2,
RETRY = -1,
FAILED = 0,
NOSERVERS = -2,
}
export default class MainPage extends React.PureComponent {
constructor(props) {
type Props = {
teams: Team[];
showAddServerButton: boolean;
moveTabs: (originalOrder: number, newOrder: number) => Promise<number | undefined>;
openMenu: () => void;
darkMode: boolean;
appName: string;
};
type State = {
key: number;
sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, number>;
mentionCounts: Record<string, number>;
targetURL: string;
maximized: boolean;
tabStatus: Map<string, TabStatus>;
darkMode: boolean;
modalOpen?: boolean;
fullScreen?: boolean;
showExtraBar?: boolean;
};
type TabStatus = {
status: Status;
extra?: {
url: string;
error: string;
};
}
export default class MainPage extends React.PureComponent<Props, State> {
topBar: React.RefObject<HTMLDivElement>;
threeDotMenu: React.RefObject<HTMLButtonElement>;
constructor(props: Props) {
super(props);
this.topBar = React.createRef();
@@ -66,7 +105,7 @@ export default class MainPage extends React.PureComponent {
mentionCounts: {},
targetURL: '',
maximized: false,
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: LOADING, extra: null}])),
tabStatus: new Map(this.props.teams.map((server) => [server.name, {status: Status.LOADING}])),
darkMode: this.props.darkMode,
};
}
@@ -79,10 +118,10 @@ export default class MainPage extends React.PureComponent {
return this.state.tabStatus.get(tabname);
}
}
return {status: NOSERVERS};
return {status: Status.NOSERVERS};
}
updateTabStatus(server, newStatusValue) {
updateTabStatus(server: string, newStatusValue: TabStatus) {
const status = new Map(this.state.tabStatus);
status.set(server, newStatusValue);
this.setState({tabStatus: status});
@@ -93,7 +132,7 @@ export default class MainPage extends React.PureComponent {
window.ipcRenderer.on(LOAD_RETRY, (_, server, retry, err, loadUrl) => {
console.log(`${server}: failed to load ${err}, but retrying`);
const statusValue = {
status: RETRY,
status: Status.RETRY,
extra: {
retry,
error: err,
@@ -104,13 +143,13 @@ export default class MainPage extends React.PureComponent {
});
window.ipcRenderer.on(LOAD_SUCCESS, (_, server) => {
this.updateTabStatus(server, {status: DONE});
this.updateTabStatus(server, {status: Status.DONE});
});
window.ipcRenderer.on(LOAD_FAILED, (_, server, err, loadUrl) => {
console.log(`${server}: failed to load ${err}`);
const statusValue = {
status: FAILED,
status: Status.FAILED,
extra: {
error: err,
url: loadUrl,
@@ -198,44 +237,50 @@ export default class MainPage extends React.PureComponent {
}
}
handleMaximizeState = (_, maximized) => {
handleMaximizeState = (_: IpcRendererEvent, maximized: boolean) => {
this.setState({maximized});
}
handleFullScreenState = (isFullScreen) => {
handleFullScreenState = (isFullScreen: boolean) => {
this.setState({fullScreen: isFullScreen});
}
handleSetServerKey = (key) => {
handleSetServerKey = (key: number) => {
const newKey = (this.props.teams.length + key) % this.props.teams.length;
this.setState({key: newKey});
}
handleSelect = (name, key) => {
handleSelect = (name: string, key: number) => {
window.ipcRenderer.send(SWITCH_SERVER, name);
this.handleSetServerKey(key);
}
handleDragAndDrop = async (dropResult) => {
handleDragAndDrop = async (dropResult: DropResult) => {
const {removedIndex, addedIndex} = dropResult;
if (removedIndex === null || addedIndex === null) {
return;
}
if (removedIndex !== addedIndex) {
const teamIndex = await this.props.moveTabs(removedIndex, addedIndex < this.props.teams.length ? addedIndex : this.props.teams.length - 1);
if (!teamIndex) {
return;
}
const name = this.props.teams[teamIndex].name;
this.handleSelect(name, teamIndex);
}
}
handleClose = (e) => {
handleClose = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation(); // since it is our button, the event goes into MainPage's onclick event, getting focus back.
window.ipcRenderer.send(WINDOW_CLOSE);
}
handleMinimize = (e) => {
handleMinimize = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
window.ipcRenderer.send(WINDOW_MINIMIZE);
}
handleMaximize = (e) => {
handleMaximize = (e: React.MouseEvent<HTMLDivElement>) => {
e.stopPropagation();
window.ipcRenderer.send(WINDOW_MAXIMIZE);
}
@@ -246,7 +291,7 @@ export default class MainPage extends React.PureComponent {
openMenu = () => {
if (window.process.platform !== 'darwin') {
this.threeDotMenu.current.blur();
this.threeDotMenu.current?.blur();
}
this.props.openMenu();
}
@@ -263,10 +308,6 @@ export default class MainPage extends React.PureComponent {
window.ipcRenderer.send(FOCUS_BROWSERVIEW);
}
setInputRef = (ref) => {
this.inputRef = ref;
}
render() {
const tabsRow = (
<TabBar
@@ -352,7 +393,7 @@ export default class MainPage extends React.PureComponent {
>
<div
ref={this.topBar}
className={`topBar-bg${this.state.unfocused ? ' unfocused' : ''}`}
className={'topBar-bg'}
>
<button
className='three-dot-menu'
@@ -383,32 +424,29 @@ export default class MainPage extends React.PureComponent {
return null;
}
switch (tabStatus.status) {
case NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
case Status.NOSERVERS: // TODO: substitute with https://mattermost.atlassian.net/browse/MM-25003
component = (
<ErrorView
id={'NoServers'}
className='errorView'
errorInfo={'No Servers configured'}
url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true}
retry={null}
appName={this.props.appName}
/>);
break;
case FAILED:
case Status.FAILED:
component = (
<ErrorView
id={this.state.key + '-fail'}
className='errorView'
errorInfo={tabStatus.extra ? tabStatus.extra.error : null}
errorInfo={tabStatus.extra?.error}
url={tabStatus.extra ? tabStatus.extra.url : ''}
active={true}
appName={this.props.appName}
/>);
break;
case LOADING:
case RETRY:
case DONE:
case Status.LOADING:
case Status.RETRY:
case Status.DONE:
component = null;
}
return component;
@@ -441,12 +479,3 @@ export default class MainPage extends React.PureComponent {
);
}
}
MainPage.propTypes = {
teams: PropTypes.array.isRequired,
showAddServerButton: PropTypes.bool.isRequired,
moveTabs: PropTypes.func.isRequired,
openMenu: PropTypes.func.isRequired,
darkMode: PropTypes.bool.isRequired,
appName: PropTypes.string.isRequired,
};

View File

@@ -1,19 +1,42 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, FormGroup, FormControl, ControlLabel, HelpBlock} from 'react-bootstrap';
import {TeamWithIndex} from 'types/config';
import urlUtils from 'common/utils/url';
export default class NewTeamModal extends React.PureComponent {
type Props = {
onClose: () => void;
onSave?: (team: TeamWithIndex) => void;
team?: TeamWithIndex;
editMode?: boolean;
show?: boolean;
restoreFocus?: boolean;
currentOrder?: number;
setInputRef?: (inputRef: HTMLInputElement) => void;
};
type State = {
teamName: string;
teamUrl: string;
teamIndex?: number;
teamOrder: number;
saveStarted: boolean;
}
export default class NewTeamModal extends React.PureComponent<Props, State> {
wasShown?: boolean;
teamNameInputRef?: HTMLInputElement;
static defaultProps = {
restoreFocus: true,
};
constructor(props) {
constructor(props: Props) {
super(props);
this.wasShown = false;
@@ -29,7 +52,7 @@ export default class NewTeamModal extends React.PureComponent {
this.setState({
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,
teamIndex: this.props.team?.index,
teamOrder: this.props.team ? this.props.team.order : (this.props.currentOrder || 0),
saveStarted: false,
});
@@ -46,7 +69,7 @@ export default class NewTeamModal extends React.PureComponent {
return this.getTeamNameValidationError() === null ? null : 'error';
}
handleTeamNameChange = (e) => {
handleTeamNameChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
this.setState({
teamName: e.target.value,
});
@@ -72,7 +95,7 @@ export default class NewTeamModal extends React.PureComponent {
return this.getTeamUrlValidationError() === null ? null : 'error';
}
handleTeamUrlChange = (e) => {
handleTeamUrlChange = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
this.setState({
teamUrl: e.target.value,
});
@@ -102,10 +125,10 @@ export default class NewTeamModal extends React.PureComponent {
saveStarted: true,
}, () => {
if (this.validateForm()) {
this.props.onSave({
this.props.onSave?.({
url: this.state.teamUrl,
name: this.state.teamName,
index: this.state.teamIndex,
index: this.state.teamIndex!,
order: this.state.teamOrder,
});
}
@@ -139,7 +162,7 @@ export default class NewTeamModal extends React.PureComponent {
show={this.props.show}
id='newServerModal'
enforceFocus={true}
onEntered={() => this.teamNameInputRef.focus()}
onEntered={() => this.teamNameInputRef?.focus()}
onHide={this.props.onClose}
restoreFocus={this.props.restoreFocus}
onKeyDown={(e) => {
@@ -231,14 +254,3 @@ export default class NewTeamModal extends React.PureComponent {
);
}
}
NewTeamModal.propTypes = {
onClose: PropTypes.func,
onSave: PropTypes.func,
team: PropTypes.object,
editMode: PropTypes.bool,
show: PropTypes.bool,
restoreFocus: PropTypes.bool,
currentOrder: PropTypes.number,
setInputRef: PropTypes.func,
};

View File

@@ -1,14 +1,21 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Modal} from 'react-bootstrap';
import {Button, Modal} from 'react-bootstrap';
import DestructiveConfirmationModal from './DestructiveConfirmModal.jsx';
import DestructiveConfirmationModal from './DestructiveConfirmModal';
export default function RemoveServerModal(props) {
type Props = {
show: boolean;
serverName: string;
onHide: () => void;
onAccept: React.MouseEventHandler<Button>;
onCancel: React.MouseEventHandler<Button>;
}
export default function RemoveServerModal(props: Props) {
const {serverName, ...rest} = props;
return (
<DestructiveConfirmationModal
@@ -30,7 +37,3 @@ export default function RemoveServerModal(props) {
/>
);
}
RemoveServerModal.propTypes = {
serverName: PropTypes.string.isRequired,
};

View File

@@ -1,38 +1,79 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
/* eslint-disable max-lines */
import 'renderer/css/settings.css';
import React from 'react';
import PropTypes from 'prop-types';
import {Checkbox, Col, FormGroup, Grid, HelpBlock, Navbar, Radio, Row, Button} from 'react-bootstrap';
import {debounce} from 'underscore';
import {CombinedConfig, LocalConfiguration, Team} from 'types/config';
import {DeepPartial} from 'types/utils';
import {GET_LOCAL_CONFIGURATION, UPDATE_CONFIGURATION, DOUBLE_CLICK_ON_WINDOW, GET_DOWNLOAD_LOCATION, SWITCH_SERVER, ADD_SERVER, RELOAD_CONFIGURATION} from 'common/communication';
import TeamList from './TeamList.jsx';
import AutoSaveIndicator from './AutoSaveIndicator.jsx';
import TeamList from './TeamList';
import AutoSaveIndicator, {SavingState} from './AutoSaveIndicator';
const CONFIG_TYPE_SERVERS = 'servers';
const CONFIG_TYPE_APP_OPTIONS = 'appOptions';
function backToIndex(serverName) {
type ConfigType = typeof CONFIG_TYPE_SERVERS | typeof CONFIG_TYPE_APP_OPTIONS;
type State = DeepPartial<CombinedConfig> & {
ready: boolean;
maximized?: boolean;
teams?: Team[];
showAddTeamForm: boolean;
trayWasVisible?: boolean;
firstRun?: boolean;
savingState: SavingStateItems;
userOpenedDownloadDialog: boolean;
}
type SavingStateItems = {
appOptions: SavingState;
servers: SavingState;
};
type SaveQueueItem = {
configType: ConfigType;
key: keyof CombinedConfig;
data: CombinedConfig[keyof CombinedConfig];
}
function backToIndex(serverName: string) {
window.ipcRenderer.send(SWITCH_SERVER, serverName);
window.close();
}
export default class SettingsPage extends React.PureComponent {
constructor(props) {
export default class SettingsPage extends React.PureComponent<Record<string, never>, State> {
trayIconThemeRef: React.RefObject<FormGroup>;
downloadLocationRef: React.RefObject<HTMLInputElement>;
showTrayIconRef: React.RefObject<Checkbox>;
autostartRef: React.RefObject<Checkbox>;
minimizeToTrayRef: React.RefObject<Checkbox>;
flashWindowRef: React.RefObject<Checkbox>;
bounceIconRef: React.RefObject<Checkbox>;
showUnreadBadgeRef: React.RefObject<Checkbox>;
useSpellCheckerRef: React.RefObject<Checkbox>;
enableHardwareAccelerationRef: React.RefObject<Checkbox>;
saveQueue: SaveQueueItem[];
constructor(props: Record<string, never>) {
super(props);
this.state = {
ready: false,
teams: [],
showAddTeamForm: false,
savingState: {
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
servers: AutoSaveIndicator.SAVING_STATE_DONE,
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
},
userOpenedDownloadDialog: false,
};
@@ -67,26 +108,26 @@ export default class SettingsPage extends React.PureComponent {
getConfig = () => {
window.ipcRenderer.invoke(GET_LOCAL_CONFIGURATION).then((config) => {
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config)});
this.setState({ready: true, maximized: false, ...this.convertConfigDataToState(config) as Omit<State, 'ready'>});
});
}
convertConfigDataToState = (configData, currentState = {}) => {
const newState = Object.assign({}, configData);
convertConfigDataToState = (configData: Partial<LocalConfiguration>, currentState: Partial<State> = {}) => {
const newState = Object.assign({} as State, configData);
newState.showAddTeamForm = currentState.showAddTeamForm || false;
newState.trayWasVisible = currentState.trayWasVisible || false;
if (newState.teams.length === 0 && currentState.firstRun !== false) {
if (newState.teams?.length === 0 && currentState.firstRun !== false) {
newState.firstRun = false;
newState.showAddTeamForm = true;
}
newState.savingState = currentState.savingState || {
appOptions: AutoSaveIndicator.SAVING_STATE_DONE,
servers: AutoSaveIndicator.SAVING_STATE_DONE,
appOptions: SavingState.SAVING_STATE_DONE,
servers: SavingState.SAVING_STATE_DONE,
};
return newState;
}
saveSetting = (configType, {key, data}) => {
saveSetting = (configType: ConfigType, {key, data}: {key: keyof CombinedConfig; data: CombinedConfig[keyof CombinedConfig]}) => {
this.saveQueue.push({
configType,
key,
@@ -115,25 +156,25 @@ export default class SettingsPage extends React.PureComponent {
Object.entries(queuedUpdateCounts).forEach(([configType, count]) => {
if (count > 0) {
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVING;
} else if (count === 0 && savingState[configType] === AutoSaveIndicator.SAVING_STATE_SAVING) {
savingState[configType] = AutoSaveIndicator.SAVING_STATE_SAVED;
this.resetSaveState(configType);
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVING;
} else if (count === 0 && savingState[configType as keyof SavingStateItems] === SavingState.SAVING_STATE_SAVING) {
savingState[configType as keyof SavingStateItems] = SavingState.SAVING_STATE_SAVED;
this.resetSaveState(configType as keyof SavingStateItems);
}
});
this.setState({savingState});
}
resetSaveState = debounce((configType) => {
if (this.state.savingState[configType] !== AutoSaveIndicator.SAVING_STATE_SAVING) {
resetSaveState = debounce((configType: keyof SavingStateItems) => {
if (this.state.savingState[configType] !== SavingState.SAVING_STATE_SAVING) {
const savingState = Object.assign({}, this.state.savingState);
savingState[configType] = AutoSaveIndicator.SAVING_STATE_DONE;
savingState[configType] = SavingState.SAVING_STATE_DONE;
this.setState({savingState});
}
}, 2000);
handleTeamsChange = (teams) => {
handleTeamsChange = (teams: Team[]) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
showAddTeamForm: false,
@@ -145,7 +186,7 @@ export default class SettingsPage extends React.PureComponent {
}
handleChangeShowTrayIcon = () => {
const shouldShowTrayIcon = !this.showTrayIconRef.current.props.checked;
const shouldShowTrayIcon = !this.showTrayIconRef.current?.props.checked;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showTrayIcon', data: shouldShowTrayIcon});
this.setState({
showTrayIcon: shouldShowTrayIcon,
@@ -158,7 +199,7 @@ export default class SettingsPage extends React.PureComponent {
}
}
handleChangeTrayIconTheme = (theme) => {
handleChangeTrayIconTheme = (theme: string) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'trayIconTheme', data: theme});
this.setState({
trayIconTheme: theme,
@@ -166,14 +207,14 @@ export default class SettingsPage extends React.PureComponent {
}
handleChangeAutoStart = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current.props.checked});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'autostart', data: !this.autostartRef.current?.props.checked});
this.setState({
autostart: !this.autostartRef.current.props.checked,
autostart: !this.autostartRef.current?.props.checked,
});
}
handleChangeMinimizeToTray = () => {
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current.props.checked;
const shouldMinimizeToTray = this.state.showTrayIcon && !this.minimizeToTrayRef.current?.props.checked;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'minimizeToTray', data: shouldMinimizeToTray});
this.setState({
@@ -185,10 +226,10 @@ export default class SettingsPage extends React.PureComponent {
this.setState({
showAddTeamForm: !this.state.showAddTeamForm,
});
document.activeElement.blur();
(document.activeElement as HTMLElement).blur();
}
setShowTeamFormVisibility = (val) => {
setShowTeamFormVisibility = (val: boolean) => {
this.setState({
showAddTeamForm: val,
});
@@ -199,13 +240,13 @@ export default class SettingsPage extends React.PureComponent {
key: 'notifications',
data: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
},
});
this.setState({
notifications: {
...this.state.notifications,
flashWindow: this.flashWindowRef.current.props.checked ? 0 : 2,
flashWindow: this.flashWindowRef.current?.props.checked ? 0 : 2,
},
});
}
@@ -215,18 +256,18 @@ export default class SettingsPage extends React.PureComponent {
key: 'notifications',
data: {
...this.state.notifications,
bounceIcon: !this.bounceIconRef.current.props.checked,
bounceIcon: !this.bounceIconRef.current?.props.checked,
},
});
this.setState({
notifications: {
...this.state.notifications,
bounceIcon: !this.bounceIconRef.current.props.checked,
bounceIcon: !this.bounceIconRef.current?.props.checked,
},
});
}
handleBounceIconType = (event) => {
handleBounceIconType = (event: React.ChangeEvent<Radio & HTMLInputElement>) => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {
key: 'notifications',
data: {
@@ -237,33 +278,33 @@ export default class SettingsPage extends React.PureComponent {
this.setState({
notifications: {
...this.state.notifications,
bounceIconType: event.target.value,
bounceIconType: event.target.value as 'critical' | 'informational',
},
});
}
handleShowUnreadBadge = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current.props.checked});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'showUnreadBadge', data: !this.showUnreadBadgeRef.current?.props.checked});
this.setState({
showUnreadBadge: !this.showUnreadBadgeRef.current.props.checked,
showUnreadBadge: !this.showUnreadBadgeRef.current?.props.checked,
});
}
handleChangeUseSpellChecker = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current.props.checked});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'useSpellChecker', data: !this.useSpellCheckerRef.current?.props.checked});
this.setState({
useSpellChecker: !this.useSpellCheckerRef.current.props.checked,
useSpellChecker: !this.useSpellCheckerRef.current?.props.checked,
});
}
handleChangeEnableHardwareAcceleration = () => {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current.props.checked});
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'enableHardwareAcceleration', data: !this.enableHardwareAccelerationRef.current?.props.checked});
this.setState({
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current.props.checked,
enableHardwareAcceleration: !this.enableHardwareAccelerationRef.current?.props.checked,
});
}
saveDownloadLocation = (location) => {
saveDownloadLocation = (location: string) => {
if (!location) {
return;
}
@@ -273,7 +314,7 @@ export default class SettingsPage extends React.PureComponent {
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_APP_OPTIONS, {key: 'downloadLocation', data: location});
}
handleChangeDownloadLocation = (e) => {
handleChangeDownloadLocation = (e: React.ChangeEvent<HTMLInputElement>) => {
this.saveDownloadLocation(e.target.value);
}
@@ -285,8 +326,8 @@ export default class SettingsPage extends React.PureComponent {
this.setState({userOpenedDownloadDialog: false});
}
updateTeam = (index, newData) => {
const teams = this.state.teams;
updateTeam = (index: number, newData: Team) => {
const teams = this.state.teams || [];
teams[index] = newData;
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
@@ -294,8 +335,8 @@ export default class SettingsPage extends React.PureComponent {
});
}
addServer = (team) => {
const teams = this.state.teams;
addServer = (team: Team) => {
const teams = this.state.teams || [];
teams.push(team);
window.timers.setImmediate(this.saveSetting, CONFIG_TYPE_SERVERS, {key: 'teams', data: teams});
this.setState({
@@ -303,12 +344,6 @@ export default class SettingsPage extends React.PureComponent {
});
}
openMenu = () => {
// @eslint-ignore
this.threeDotMenu.current.blur();
this.props.openMenu();
}
handleDoubleClick = () => {
window.ipcRenderer.send(DOUBLE_CLICK_ON_WINDOW, 'settings');
}
@@ -317,7 +352,7 @@ export default class SettingsPage extends React.PureComponent {
const settingsPage = {
navbar: {
backgroundColor: '#fff',
position: 'relative',
position: 'relative' as const,
},
close: {
textDecoration: 'none',
@@ -329,7 +364,7 @@ export default class SettingsPage extends React.PureComponent {
color: '#bbb',
},
heading: {
textAlign: 'center',
textAlign: 'center' as const,
fontSize: '24px',
margin: '0',
padding: '1em 0',
@@ -356,7 +391,7 @@ export default class SettingsPage extends React.PureComponent {
padding: '0 12px',
borderRadius: '4px',
border: '1px solid #ccc',
fontWeight: '500',
fontWeight: 500,
},
downloadLocationButton: {
@@ -372,14 +407,12 @@ export default class SettingsPage extends React.PureComponent {
<Row>
<Col md={12}>
<TeamList
teams={this.state.teams}
teams={this.state.teams!}
showAddTeamForm={this.state.showAddTeamForm}
toggleAddTeamForm={this.toggleShowTeamForm}
setAddTeamFormVisibility={this.setShowTeamFormVisibility}
onTeamsChange={this.handleTeamsChange}
updateTeam={this.updateTeam}
addServer={this.addServer}
allowTeamEdit={this.state.enableServerManagement}
onTeamClick={(name) => {
backToIndex(name);
}}
@@ -576,7 +609,7 @@ export default class SettingsPage extends React.PureComponent {
name='trayIconTheme'
value='light'
defaultChecked={this.state.trayIconTheme === 'light' || !this.state.trayIconTheme}
onChange={(event) => this.handleChangeTrayIconTheme('light', event)}
onChange={() => this.handleChangeTrayIconTheme('light')}
>
{'Light'}
</Radio>
@@ -586,7 +619,7 @@ export default class SettingsPage extends React.PureComponent {
name='trayIconTheme'
value='dark'
defaultChecked={this.state.trayIconTheme === 'dark'}
onChange={(event) => this.handleChangeTrayIconTheme('dark', event)}
onChange={() => this.handleChangeTrayIconTheme('dark')}
>{'Dark'}</Radio>
</FormGroup>,
);
@@ -719,7 +752,3 @@ export default class SettingsPage extends React.PureComponent {
);
}
}
SettingsPage.propTypes = {
openMenu: PropTypes.func.isRequired,
};

View File

@@ -1,17 +1,39 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Nav, NavItem} from 'react-bootstrap';
import {Container, Draggable} from 'react-smooth-dnd';
import {Container, Draggable, OnDropCallback} from 'react-smooth-dnd';
import PlusIcon from 'mdi-react/PlusIcon';
import {Team} from 'types/config';
import {GET_CONFIGURATION} from 'common/communication';
export default class TabBar extends React.PureComponent { // need "this"
constructor(props) {
type Props = {
activeKey: number;
id: string;
isDarkMode: boolean;
onSelect: (name: string, index: number) => void;
teams: Team[];
sessionsExpired: Record<string, boolean>;
unreadCounts: Record<string, number>;
mentionCounts: Record<string, number>;
showAddServerButton: boolean;
onAddServer: () => void;
onDrop: OnDropCallback;
tabsDisabled?: boolean;
};
type State = {
hasGPOTeams: boolean;
};
export default class TabBar extends React.PureComponent<Props, State> { // need "this"
container?: React.RefObject<Container>;
constructor(props: Props) {
super(props);
this.state = {
hasGPOTeams: false,
@@ -37,7 +59,7 @@ export default class TabBar extends React.PureComponent { // need "this"
mentionCount = this.props.mentionCounts[index];
}
let badgeDiv;
let badgeDiv: React.ReactNode;
if (sessionExpired) {
badgeDiv = (
<div className='TabBar-expired'/>
@@ -63,7 +85,6 @@ export default class TabBar extends React.PureComponent { // need "this"
draggable={false}
ref={id}
active={this.props.activeKey === index}
activeKey={this.props.activeKey}
onMouseDown={() => {
this.props.onSelect(team.name, index);
}}
@@ -98,7 +119,6 @@ export default class TabBar extends React.PureComponent { // need "this"
eventKey='addServerButton'
draggable={false}
title='Add new server'
activeKey={this.props.activeKey}
onSelect={() => {
this.props.onAddServer();
}}
@@ -111,7 +131,7 @@ export default class TabBar extends React.PureComponent { // need "this"
);
}
const navContainer = (ref) => (
const navContainer = (ref: React.RefObject<Nav>) => (
<Nav
ref={ref}
className={`smooth-dnd-container TabBar${this.props.isDarkMode ? ' darkMode' : ''}`}
@@ -136,18 +156,3 @@ export default class TabBar extends React.PureComponent { // need "this"
);
}
}
TabBar.propTypes = {
activeKey: PropTypes.number,
id: PropTypes.string,
isDarkMode: PropTypes.bool,
onSelect: PropTypes.func,
teams: PropTypes.array,
sessionsExpired: PropTypes.object,
unreadCounts: PropTypes.object,
mentionCounts: PropTypes.object,
showAddServerButton: PropTypes.bool,
onAddServer: PropTypes.func,
onDrop: PropTypes.func,
tabsDisabled: PropTypes.bool,
};

View File

@@ -1,17 +1,34 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {ListGroup} from 'react-bootstrap';
import TeamListItem from './TeamListItem.jsx';
import NewTeamModal from './NewTeamModal.jsx';
import RemoveServerModal from './RemoveServerModal.jsx';
import {Team, TeamWithIndex} from 'types/config';
export default class TeamList extends React.PureComponent {
constructor(props) {
import TeamListItem from './TeamListItem';
import NewTeamModal from './NewTeamModal';
import RemoveServerModal from './RemoveServerModal';
type Props = {
onTeamClick: (teamName: string) => void;
onTeamsChange: (teams: Team[]) => void;
showAddTeamForm?: boolean;
teams: Team[];
addServer: (team: Team) => void;
updateTeam: (index: number, team: Team) => void;
setAddTeamFormVisibility: (visible: boolean) => void;
};
type State = {
team: TeamWithIndex;
showEditTeamForm: boolean;
indexToRemoveServer: number;
}
export default class TeamList extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
@@ -20,13 +37,13 @@ export default class TeamList extends React.PureComponent {
team: {
url: '',
name: '',
index: false,
index: 0,
order: props.teams.length,
},
};
}
handleTeamRemove = (index) => {
handleTeamRemove = (index: number) => {
console.log(index);
const teams = this.props.teams;
const removedOrder = this.props.teams[index].order;
@@ -39,7 +56,7 @@ export default class TeamList extends React.PureComponent {
this.props.onTeamsChange(teams);
}
handleTeamAdd = (team) => {
handleTeamAdd = (team: TeamWithIndex) => {
const teams = this.props.teams;
// check if team already exists and then change existing team or add new one
@@ -56,7 +73,7 @@ export default class TeamList extends React.PureComponent {
team: {
url: '',
name: '',
index: false,
index: 0,
order: teams.length,
},
});
@@ -64,7 +81,7 @@ export default class TeamList extends React.PureComponent {
this.props.onTeamsChange(teams);
}
openServerRemoveModal = (indexForServer) => {
openServerRemoveModal = (indexForServer: number) => {
this.setState({indexToRemoveServer: indexForServer});
}
@@ -72,16 +89,16 @@ export default class TeamList extends React.PureComponent {
this.setState({indexToRemoveServer: -1});
}
handleTeamRemovePrompt = (index) => {
handleTeamRemovePrompt = (index: number) => {
return () => {
document.activeElement.blur();
(document.activeElement as HTMLElement).blur();
this.openServerRemoveModal(index);
};
}
handleTeamEditing = (team, index) => {
handleTeamEditing = (team: Team, index: number) => {
return () => {
document.activeElement.blur();
(document.activeElement as HTMLElement).blur();
this.setState({
showEditTeamForm: true,
team: {
@@ -119,7 +136,7 @@ export default class TeamList extends React.PureComponent {
team: {
name: '',
url: '',
index: false,
index: 0,
order: this.props.teams.length,
},
});
@@ -137,12 +154,11 @@ export default class TeamList extends React.PureComponent {
this.props.updateTeam(newTeam.index, teamData);
}
this.setState({
showNewTeamModal: false,
showEditTeamForm: false,
team: {
name: '',
url: '',
index: false,
index: 0,
order: newTeam.order + 1,
},
});
@@ -175,13 +191,3 @@ export default class TeamList extends React.PureComponent {
);
}
}
TeamList.propTypes = {
onTeamClick: PropTypes.func,
onTeamsChange: PropTypes.func,
showAddTeamForm: PropTypes.bool,
teams: PropTypes.array,
addServer: PropTypes.func,
updateTeam: PropTypes.func,
setAddTeamFormVisibility: PropTypes.func,
};

View File

@@ -1,11 +1,18 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
export default class TeamListItem extends React.PureComponent {
type Props = {
name: string;
onTeamEditing: () => void;
onTeamRemove: () => void;
onTeamClick: React.MouseEventHandler<HTMLDivElement>;
url: string;
};
export default class TeamListItem extends React.PureComponent<Props> {
handleTeamRemove = () => {
this.props.onTeamRemove();
}
@@ -39,11 +46,3 @@ export default class TeamListItem extends React.PureComponent {
);
}
}
TeamListItem.propTypes = {
name: PropTypes.string,
onTeamEditing: PropTypes.func,
onTeamRemove: PropTypes.func,
onTeamClick: PropTypes.func,
url: PropTypes.string,
};

View File

@@ -1,12 +1,17 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import propTypes from 'prop-types';
import {Button, Navbar, ProgressBar} from 'react-bootstrap';
function InstallButton(props) {
type InstallButtonProps = {
notifyOnly?: boolean;
onClickInstall?: React.MouseEventHandler<Button>;
onClickDownload?: React.MouseEventHandler<Button>;
};
function InstallButton(props: InstallButtonProps) {
if (props.notifyOnly) {
return (
<Button
@@ -23,13 +28,20 @@ function InstallButton(props) {
);
}
InstallButton.propTypes = {
notifyOnly: propTypes.bool.isRequired,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
type UpdaterPageProps = {
appName: string;
notifyOnly?: boolean;
isDownloading?: boolean;
progress?: number;
onClickInstall?: React.MouseEventHandler<Button>;
onClickDownload?: React.MouseEventHandler<Button>;
onClickReleaseNotes?: React.MouseEventHandler<HTMLAnchorElement>;
onClickRemind?: React.MouseEventHandler<Button>;
onClickSkip?: React.MouseEventHandler<Button>;
onClickCancel?: React.MouseEventHandler<Button>;
};
function UpdaterPage(props) {
function UpdaterPage(props: UpdaterPageProps) {
let footer;
if (props.isDownloading) {
footer = (
@@ -97,17 +109,4 @@ function UpdaterPage(props) {
);
}
UpdaterPage.propTypes = {
appName: propTypes.string.isRequired,
notifyOnly: propTypes.bool.isRequired,
isDownloading: propTypes.bool.isRequired,
progress: propTypes.number,
onClickInstall: propTypes.func.isRequired,
onClickDownload: propTypes.func.isRequired,
onClickReleaseNotes: propTypes.func.isRequired,
onClickRemind: propTypes.func.isRequired,
onClickSkip: propTypes.func.isRequired,
onClickCancel: propTypes.func.isRequired,
};
export default UpdaterPage;

View File

@@ -1,13 +1,13 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import {storiesOf} from '@storybook/react';
import {action} from '@storybook/addon-actions';
import UpdaterPage from '../UpdaterPage.jsx';
import UpdaterPage from '../UpdaterPage';
import '../../css/components/UpdaterPage.css';
/*

View File

@@ -2,16 +2,20 @@
// See LICENSE.txt for license information.
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, Row, Col} from 'react-bootstrap';
import {Certificate} from 'electron/renderer';
export default class ShowCertificateModal extends React.PureComponent {
static propTypes = {
certificate: PropTypes.object,
onOk: PropTypes.func.isRequired,
};
type Props = {
certificate: Certificate;
onOk: () => void;
};
constructor(props) {
type State = {
certificate?: Certificate;
}
export default class ShowCertificateModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
certificate: props.certificate,
@@ -19,12 +23,12 @@ export default class ShowCertificateModal extends React.PureComponent {
}
handleOk = () => {
this.setState({certificate: null});
this.setState({certificate: undefined});
this.props.onOk();
}
render() {
const certificateSection = (descriptor) => {
const certificateSection = (descriptor: React.ReactNode) => {
return (
<Fragment>
<dt className={'certificate-key'}>{descriptor}</dt>
@@ -32,7 +36,7 @@ export default class ShowCertificateModal extends React.PureComponent {
</Fragment>
);
};
const certificateItem = (descriptor, value) => {
const certificateItem = (descriptor: React.ReactNode, value: React.ReactNode) => {
const val = value ? `${value}` : <span/>;
return (
<Fragment>
@@ -47,6 +51,7 @@ export default class ShowCertificateModal extends React.PureComponent {
<Modal
bsClass='modal'
className='show-certificate'
onHide={() => {}}
>
<Modal.Body>
{'No certificate Selected'}
@@ -55,22 +60,22 @@ export default class ShowCertificateModal extends React.PureComponent {
);
}
const utcSeconds = (date) => {
const utcSeconds = (date: number) => {
const d = new Date(0);
d.setUTCSeconds(date);
return d;
};
const expiration = utcSeconds(this.state.certificate.validExpiry);
const creation = utcSeconds(this.state.certificate.validStart);
const dateDisplayOptions = {dateStyle: 'full', timeStyle: 'full'};
const expiration = utcSeconds(this.state.certificate?.validExpiry || 0);
const creation = utcSeconds(this.state.certificate?.validStart || 0);
const dateDisplayOptions = {dateStyle: 'full' as const, timeStyle: 'full' as const};
const dateLocale = 'en-US';
return (
<Modal
bsClass='modal'
className='show-certificate'
show={this.state.certificate !== null}
scrollable={'true'}
onHide={() => {}}
>
<Modal.Header className={'no-border'}>
<Modal.Title>{'Certificate information'}</Modal.Title>
@@ -79,20 +84,20 @@ export default class ShowCertificateModal extends React.PureComponent {
<p className='details'>{'Details'}</p>
<dl>
{certificateSection('Subject Name')}
{certificateItem('Common Name', this.state.certificate.subject.commonName)}
{certificateItem('Common Name', this.state.certificate?.subject.commonName)}
</dl>
<dl>
{certificateSection('Issuer Name')}
{certificateItem('Common Name', this.state.certificate.issuer.commonName)}
{certificateItem('Common Name', this.state.certificate?.issuer.commonName)}
</dl>
<dl>
{certificateItem('Serial Number', this.state.certificate.serialNumber)}
{certificateItem('Serial Number', this.state.certificate?.serialNumber)}
{certificateItem('Not Valid Before', creation.toLocaleString(dateLocale, dateDisplayOptions))}
{certificateItem('Not Valid After', expiration.toLocaleString(dateLocale, dateDisplayOptions))}
</dl>
<dl>
{certificateSection('Public Key Info')}
{certificateItem('Algorithm', this.state.certificate.fingerprint.split('/')[0])}
{certificateItem('Algorithm', this.state.certificate?.fingerprint.split('/')[0])}
</dl>
</Modal.Body>
<Modal.Footer className={'no-border'}>
@@ -100,7 +105,6 @@ export default class ShowCertificateModal extends React.PureComponent {
<Row>
<Col>
<Button
variant={'primary'}
onClick={this.handleOk}
className={'primary'}
>{'Close'}</Button>

View File

@@ -2,9 +2,8 @@
// See LICENSE.txt for license information.
import React from 'react';
import propTypes from 'prop-types';
export default function UrlDescription(props) {
export default function UrlDescription(props: {url: string}) {
if (props.url) {
return (
<div className='HoveringURL HoveringURL-left'>
@@ -12,8 +11,6 @@ export default function UrlDescription(props) {
</div>
);
}
}
UrlDescription.propTypes = {
url: propTypes.string,
};
return null;
}

View File

@@ -12,18 +12,18 @@ import React from 'react';
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
* ignores events bubbling up from descendent elements
*/
function useAnimationEnd(
ref,
callback,
animationName,
function useAnimationEnd<T extends Element>(
ref: React.RefObject<T>,
callback: (event: Event) => void,
animationName: string,
listenForEventBubbling = true,
) {
): void {
React.useEffect(() => {
if (!ref.current) {
return undefined;
}
function handleAnimationend(event) {
function handleAnimationend(event: Event & {animationName?: string}) {
if (!listenForEventBubbling && event.target !== ref.current) {
return;
}
@@ -36,7 +36,7 @@ function useAnimationEnd(
ref.current.addEventListener('animationend', handleAnimationend);
return () => {
ref.current.removeEventListener('animationend', handleAnimationend);
ref.current?.removeEventListener('animationend', handleAnimationend);
};
}, [ref, callback, animationName, listenForEventBubbling]);
}

View File

@@ -12,10 +12,10 @@ import React from 'react';
* bubbled from all descendent elements but when false, only listens for events coming from the target element and
* ignores events bubbling up from descendent elements
*/
function useTransitionend(
ref,
callback,
properties,
function useTransitionend<T extends Element>(
ref: React.RefObject<T>,
callback: (event: Event) => void,
properties: string[],
listenForEventBubbling = true,
) {
React.useEffect(() => {
@@ -23,7 +23,7 @@ function useTransitionend(
return undefined;
}
function handleTransitionEnd(event) {
function handleTransitionEnd(event: Event & {propertyName?: string}) {
if (!listenForEventBubbling && event.target !== ref.current) {
return;
}

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/index.css';
@@ -8,11 +8,18 @@ import 'renderer/css/index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import {CombinedConfig, Team} from 'types/config';
import {GET_CONFIGURATION, UPDATE_TEAMS, QUIT, RELOAD_CONFIGURATION} from 'common/communication';
import MainPage from './components/MainPage.jsx';
class Root extends React.PureComponent {
constructor(props) {
import MainPage from './components/MainPage';
type State = {
config?: CombinedConfig;
}
class Root extends React.PureComponent<Record<string, never>, State> {
constructor(props: Record<string, never>) {
super(props);
this.state = {};
}
@@ -39,7 +46,10 @@ class Root extends React.PureComponent {
this.setState({config});
}
moveTabs = async (originalOrder, newOrder) => {
moveTabs = async (originalOrder: number, newOrder: number): Promise<number | undefined> => {
if (!this.state.config) {
throw new Error('No config');
}
const teams = this.state.config.teams.concat();
const tabOrder = teams.map((team, index) => {
return {
@@ -62,12 +72,9 @@ class Root extends React.PureComponent {
return teamIndex;
};
teamConfigChange = async (updatedTeams, callback) => {
const updatedConfig = await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
teamConfigChange = async (updatedTeams: Team[]) => {
await window.ipcRenderer.invoke(UPDATE_TEAMS, updatedTeams);
await this.reloadConfig();
if (callback) {
callback(updatedConfig);
}
};
reloadConfig = async () => {
@@ -75,7 +82,7 @@ class Root extends React.PureComponent {
this.setState({config});
};
requestConfig = async (exitOnError) => {
requestConfig = async (exitOnError?: boolean) => {
// todo: should we block?
try {
const configRequest = await window.ipcRenderer.invoke(GET_CONFIGURATION);
@@ -115,6 +122,8 @@ class Root extends React.PureComponent {
}
window.ipcRenderer.invoke('get-app-version').then(({name, version}) => {
// eslint-disable-next-line no-undef
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
console.log(`Starting ${name} v${version} commit: ${__HASH_VERSION__}`);
});

View File

@@ -3,10 +3,11 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {Certificate} from 'electron/renderer';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
import SelectCertificateModal from './certificateModal.jsx';
import SelectCertificateModal from './certificateModal';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
@@ -16,7 +17,7 @@ const handleCancel = () => {
window.postMessage({type: MODAL_CANCEL}, window.location.href);
};
const handleSelect = (cert) => {
const handleSelect = (cert: Certificate) => {
window.postMessage({type: MODAL_RESULT, data: {cert}}, window.location.href);
};

View File

@@ -1,27 +1,34 @@
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
import {Certificate} from 'electron/renderer';
import React, {Fragment} from 'react';
import PropTypes from 'prop-types';
import {Modal, Button, Table, Row, Col} from 'react-bootstrap';
import {CertificateModalData} from 'types/certificate';
import {ModalMessage} from 'types/modals';
import {MODAL_INFO} from 'common/communication';
import ShowCertificateModal from '../../components/showCertificateModal.jsx';
import ShowCertificateModal from '../../components/showCertificateModal';
export default class SelectCertificateModal extends React.PureComponent {
static propTypes = {
onSelect: PropTypes.func.isRequired,
onCancel: PropTypes.func,
getCertInfo: PropTypes.func,
}
type Props = {
onSelect: (cert: Certificate) => void;
onCancel?: () => void;
getCertInfo: () => void;
}
constructor(props) {
type State = {
selectedIndex?: number;
showCertificate?: Certificate;
url?: string;
list?: Certificate[];
}
export default class SelectCertificateModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
selectedIndex: null,
showCertificate: null,
};
this.state = {};
}
componentDidMount() {
@@ -34,7 +41,7 @@ export default class SelectCertificateModal extends React.PureComponent {
window.removeEventListener('message', this.handleCertInfoMessage);
}
handleCertInfoMessage = (event) => {
handleCertInfoMessage = (event: {data: ModalMessage<CertificateModalData>}) => {
switch (event.data.type) {
case MODAL_INFO: {
const {url, list} = event.data.data;
@@ -46,13 +53,13 @@ export default class SelectCertificateModal extends React.PureComponent {
}
}
selectfn = (index) => {
selectfn = (index: number) => {
return (() => {
this.setState({selectedIndex: index});
});
};
renderCert = (cert, index) => {
renderCert = (cert: Certificate, index: number) => {
const issuer = (cert.issuerName || (cert.issuer && cert.issuer.commonName) || '');
const subject = (cert.subjectName || (cert.subject && cert.subject.commonName) || '');
const serial = cert.serialNumber || '';
@@ -75,7 +82,7 @@ export default class SelectCertificateModal extends React.PureComponent {
</tr>);
};
renderCerts = (certificateList) => {
renderCerts = (certificateList: Certificate[]) => {
if (certificateList) {
const certs = certificateList.map(this.renderCert);
return (
@@ -88,12 +95,15 @@ export default class SelectCertificateModal extends React.PureComponent {
}
getSelectedCert = () => {
return this.state.selectedIndex === null ? null : this.state.list[this.state.selectedIndex];
if (this.state.list && this.state.selectedIndex) {
return this.state.list[this.state.selectedIndex];
}
return undefined;
};
handleOk = () => {
const cert = this.getSelectedCert();
if (cert !== null) {
if (cert) {
this.props.onSelect(cert);
}
}
@@ -104,7 +114,7 @@ export default class SelectCertificateModal extends React.PureComponent {
}
certificateInfoClose = () => {
this.setState({showCertificate: null});
this.setState({showCertificate: undefined});
}
render() {
@@ -122,6 +132,7 @@ export default class SelectCertificateModal extends React.PureComponent {
bsClass='modal'
className='certificate-modal'
show={Boolean(this.state.list && this.state.url)}
onHide={() => {}}
>
<Modal.Header>
<Modal.Title >{'Select a certificate'}</Modal.Title>
@@ -131,7 +142,6 @@ export default class SelectCertificateModal extends React.PureComponent {
<Table
striped={true}
hover={true}
size={'sm'}
responsive={true}
className='certificate-list'
tabIndex={1}
@@ -144,7 +154,7 @@ export default class SelectCertificateModal extends React.PureComponent {
</tr>
</thead>
<tbody>
{this.renderCerts(this.state.list)}
{this.renderCerts(this.state.list!)}
<tr/* this is to correct table height without affecting real rows *//>
</tbody>
</Table>
@@ -154,7 +164,6 @@ export default class SelectCertificateModal extends React.PureComponent {
<Row>
<Col sm={4}>
<Button
variant={'info'}
disabled={this.state.selectedIndex === null}
onClick={this.handleCertificateInfo}
className={'info'}
@@ -163,11 +172,9 @@ export default class SelectCertificateModal extends React.PureComponent {
<Col sm={8}>
<Button
onClick={this.props.onCancel}
variant={'secondary'}
className={'secondary'}
>{'Cancel'}</Button>
<Button
variant={'primary'}
onClick={this.handleOk}
disabled={this.state.selectedIndex === null}
className={'primary'}

View File

@@ -4,18 +4,27 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication.js';
import {ModalMessage} from 'types/modals';
import LoadingScreen from '../../components/LoadingScreen.jsx';
import {RECEIVED_LOADING_SCREEN_DATA, GET_LOADING_SCREEN_DATA, LOADING_SCREEN_ANIMATION_FINISHED, TOGGLE_LOADING_SCREEN_VISIBILITY} from 'common/communication';
import LoadingScreen from '../../components/LoadingScreen';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
import 'renderer/css/components/LoadingAnimation.css';
import 'renderer/css/components/LoadingScreen.css';
class LoadingScreenRoot extends React.PureComponent {
constructor() {
super();
type Props = Record<string, never>;
type State = {
showLoadingScreen: boolean;
darkMode: boolean;
}
class LoadingScreenRoot extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
showLoadingScreen: true,
darkMode: false,
@@ -32,7 +41,7 @@ class LoadingScreenRoot extends React.PureComponent {
window.removeEventListener('message', this.handleMessageEvent);
}
handleMessageEvent = (event) => {
handleMessageEvent = (event: {data: ModalMessage<any>}) => {
if (event.data.type === RECEIVED_LOADING_SCREEN_DATA) {
this.setState({
darkMode: event.data.data.darkMode,

View File

@@ -3,19 +3,20 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {AuthenticationResponseDetails} from 'electron/renderer';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication.js';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO} from 'common/communication';
import LoginModal from './loginModal.jsx';
import LoginModal from './loginModal';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
const handleLoginCancel = (request) => {
const handleLoginCancel = (request: AuthenticationResponseDetails) => {
window.postMessage({type: MODAL_CANCEL, data: {request}}, window.location.href);
};
const handleLogin = (request, username, password) => {
const handleLogin = (request: AuthenticationResponseDetails, username: string, password: string) => {
window.postMessage({type: MODAL_RESULT, data: {request, username, password}}, window.location.href);
};

View File

@@ -3,20 +3,34 @@
// Copyright (c) 2015-2016 Yuya Ochiai
import React from 'react';
import PropTypes from 'prop-types';
import {Button, Col, ControlLabel, Form, FormGroup, FormControl, Modal} from 'react-bootstrap';
import {MODAL_INFO} from 'common/communication';
import urlUtils from 'common/utils/url';
import {LoginModalData} from 'types/auth';
import {ModalMessage} from 'types/modals';
import {AuthenticationResponseDetails, AuthInfo} from 'electron/renderer';
export default class LoginModal extends React.PureComponent {
constructor(props) {
import urlUtils from 'common/utils/url';
import {MODAL_INFO} from 'common/communication';
type Props = {
onCancel: (request: AuthenticationResponseDetails) => void;
onLogin: (request: AuthenticationResponseDetails, username: string, password: string) => void;
getAuthInfo: () => void;
};
type State = {
username: string;
password: string;
request?: AuthenticationResponseDetails;
authInfo?: AuthInfo;
};
export default class LoginModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
username: '',
password: '',
request: null,
authInfo: null,
};
}
@@ -30,7 +44,7 @@ export default class LoginModal extends React.PureComponent {
window.removeEventListener('message', this.handleAuthInfoMessage);
}
handleAuthInfoMessage = (event) => {
handleAuthInfoMessage = (event: {data: ModalMessage<LoginModalData>}) => {
switch (event.data.type) {
case MODAL_INFO: {
const {request, authInfo} = event.data.data;
@@ -42,33 +56,33 @@ export default class LoginModal extends React.PureComponent {
}
}
handleSubmit = (event) => {
handleSubmit = (event: React.MouseEvent<Button>) => {
event.preventDefault();
this.props.onLogin(this.state.request, this.state.username, this.state.password);
this.props.onLogin(this.state.request!, this.state.username, this.state.password);
this.setState({
username: '',
password: '',
request: null,
authInfo: null,
request: undefined,
authInfo: undefined,
});
}
handleCancel = (event) => {
handleCancel = (event: React.MouseEvent<Button>) => {
event.preventDefault();
this.props.onCancel(this.state.request);
this.props.onCancel(this.state.request!);
this.setState({
username: '',
password: '',
request: null,
authInfo: null,
request: undefined,
authInfo: undefined,
});
}
setUsername = (e) => {
setUsername = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
this.setState({username: e.target.value});
}
setPassword = (e) => {
setPassword = (e: React.ChangeEvent<FormControl & HTMLInputElement>) => {
this.setState({password: e.target.value});
}
@@ -80,11 +94,14 @@ export default class LoginModal extends React.PureComponent {
theServer = `The proxy ${this.state.authInfo.host}:${this.state.authInfo.port}`;
} else {
const tmpURL = urlUtils.parseURL(this.state.request.url);
theServer = `The server ${tmpURL.protocol}//${tmpURL.host}`;
theServer = `The server ${tmpURL?.protocol}//${tmpURL?.host}`;
}
const message = `${theServer} requires a username and password.`;
return (
<Modal show={Boolean(this.state.request && this.state.authInfo)}>
<Modal
show={Boolean(this.state.request && this.state.authInfo)}
onHide={() => {}}
>
<Modal.Header>
<Modal.Title>{'Authentication Required'}</Modal.Title>
</Modal.Header>
@@ -148,9 +165,3 @@ export default class LoginModal extends React.PureComponent {
);
}
}
LoginModal.propTypes = {
onCancel: PropTypes.func,
onLogin: PropTypes.func,
getAuthInfo: PropTypes.func,
};

View File

@@ -4,21 +4,20 @@
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
const queryString = window.location.search;
const urlParams = new URLSearchParams(queryString);
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication.js';
import {TeamWithIndex} from 'types/config';
import NewTeamModal from '../../components/NewTeamModal.jsx'; //'./addServer.jsx';
import {MODAL_CANCEL, MODAL_RESULT} from 'common/communication';
import NewTeamModal from '../../components/NewTeamModal'; //'./addServer.jsx';
const onClose = () => {
window.postMessage({type: MODAL_CANCEL}, window.location.href);
};
const onSave = (data) => {
const onSave = (data: TeamWithIndex) => {
window.postMessage({type: MODAL_RESULT, data}, window.location.href);
};
@@ -29,7 +28,6 @@ const start = async () => {
onSave={onSave}
editMode={false}
show={true}
url={decodeURIComponent(urlParams.get('url'))}
/>,
document.getElementById('app'),
);

View File

@@ -4,9 +4,9 @@
import React from 'react';
import ReactDOM from 'react-dom';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication.js';
import {MODAL_CANCEL, MODAL_RESULT, RETRIEVE_MODAL_INFO, MODAL_SEND_IPC_MESSAGE} from 'common/communication';
import PermissionModal from './permissionModal.jsx';
import PermissionModal from './permissionModal';
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/modals.css';
@@ -23,7 +23,7 @@ const getPermissionInfo = () => {
window.postMessage({type: RETRIEVE_MODAL_INFO}, window.location.href);
};
const openExternalLink = (protocol, url) => {
const openExternalLink = (protocol: string, url: string) => {
window.postMessage({type: MODAL_SEND_IPC_MESSAGE, data: {type: 'confirm-protocol', args: [protocol, url]}}, window.location.href);
};

View File

@@ -3,14 +3,29 @@
import React from 'react';
import {Modal, Button} from 'react-bootstrap';
import PropTypes from 'prop-types';
import {PermissionType} from 'types/trustedOrigin';
import {ModalMessage} from 'types/modals';
import urlUtil from 'common/utils/url';
import {MODAL_INFO} from 'common/communication';
import {PERMISSION_DESCRIPTION} from 'common/permissions';
export default class PermissionModal extends React.PureComponent {
constructor(props) {
type Props = {
handleDeny: React.MouseEventHandler<Button>;
handleGrant: React.MouseEventHandler<Button>;
getPermissionInfo: () => void;
openExternalLink: (protocol: string, url: string) => void;
};
type State = {
url?: string;
permission?: PermissionType;
}
export default class PermissionModal extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = {};
}
@@ -25,7 +40,7 @@ export default class PermissionModal extends React.PureComponent {
window.removeEventListener('message', this.handlePermissionInfoMessage);
}
handlePermissionInfoMessage = (event) => {
handlePermissionInfoMessage = (event: {data: ModalMessage<{url: string; permission: PermissionType}>}) => {
switch (event.data.type) {
case MODAL_INFO: {
const {url, permission} = event.data.data;
@@ -38,7 +53,7 @@ export default class PermissionModal extends React.PureComponent {
}
getModalTitle() {
return `${PERMISSION_DESCRIPTION[this.state.permission]} Required`;
return `${PERMISSION_DESCRIPTION[this.state.permission!]} Required`;
}
getModalBody() {
@@ -46,12 +61,12 @@ export default class PermissionModal extends React.PureComponent {
const originDisplay = url ? urlUtil.getHost(url) : 'unknown origin';
const originLink = url ? originDisplay : '';
const click = (e) => {
const click = (e: React.MouseEvent<HTMLAnchorElement>) => {
e.preventDefault();
let parseUrl;
try {
parseUrl = urlUtil.parseURL(originLink);
this.props.openExternalLink(parseUrl.protocol, originLink);
this.props.openExternalLink(parseUrl!.protocol, originLink);
} catch (err) {
console.error(`invalid url ${originLink} supplied to externallink: ${err}`);
}
@@ -60,7 +75,7 @@ export default class PermissionModal extends React.PureComponent {
return (
<div>
<p>
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission]}.`}
{`A site that's not included in your Mattermost server configuration requires access for ${PERMISSION_DESCRIPTION[permission!]}.`}
</p>
<p>
<span>{'This request originated from '}</span>
@@ -78,6 +93,7 @@ export default class PermissionModal extends React.PureComponent {
show={Boolean(this.state.url && this.state.permission)}
id='requestPermissionModal'
enforceFocus={true}
onHide={() => {}}
>
<Modal.Header>
<Modal.Title>{this.getModalTitle()}</Modal.Title>
@@ -100,10 +116,3 @@ export default class PermissionModal extends React.PureComponent {
);
}
}
PermissionModal.propTypes = {
handleDeny: PropTypes.func,
handleGrant: PropTypes.func,
getPermissionInfo: PropTypes.func,
openExternalLink: PropTypes.func,
};

View File

@@ -9,12 +9,12 @@ const urlParams = new URLSearchParams(queryString);
import React from 'react';
import ReactDOM from 'react-dom';
import UrlDescription from '../../components/urlDescription.jsx';
import UrlDescription from '../../components/urlDescription';
const start = async () => {
ReactDOM.render(
<UrlDescription
url={decodeURIComponent(urlParams.get('url'))}
url={decodeURIComponent(urlParams.get('url')!)}
/>,
document.getElementById('app'),
);

View File

@@ -22,7 +22,7 @@ const notificationSounds = new Map([
['Upstairs', upstairs],
]);
export const playSound = throttle((soundName) => {
export const playSound = throttle((soundName: string) => {
if (soundName) {
const audio = new Audio(notificationSounds.get(soundName));
audio.play();

View File

@@ -1,6 +1,6 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import 'bootstrap/dist/css/bootstrap.min.css';
import 'renderer/css/index.css';
@@ -9,19 +9,11 @@ import 'renderer/css/settings.css';
import React from 'react';
import ReactDOM from 'react-dom';
import SettingsPage from './components/SettingsPage.jsx';
function openMenu() {
if (window.process.platform !== 'darwin') {
window.ipcRenderer.send('open-app-menu');
}
}
import SettingsPage from './components/SettingsPage';
const start = async () => {
ReactDOM.render(
<SettingsPage
openMenu={openMenu}
/>,
<SettingsPage/>,
document.getElementById('app'),
);
};

View File

@@ -1,21 +1,29 @@
// Copyright (c) 2015-2016 Yuya Ochiai
// Copyright (c) 2016-present Mattermost, Inc. All Rights Reserved.
// See LICENSE.txt for license information.
// Copyright (c) 2015-2016 Yuya Ochiai
import url from 'url';
import React from 'react';
import ReactDOM from 'react-dom';
import propTypes from 'prop-types';
import {remote} from 'electron';
import UpdaterPage from './components/UpdaterPage.jsx';
import UpdaterPage from './components/UpdaterPage';
const thisURL = url.parse(location.href, true);
const notifyOnly = thisURL.query.notifyOnly === 'true';
class UpdaterPageContainer extends React.PureComponent {
constructor(props) {
type Props = {
notifyOnly: boolean;
initialState: State;
};
type State = {
}
class UpdaterPageContainer extends React.PureComponent<Props, State> {
constructor(props: Props) {
super(props);
this.state = props.initialState;
}
@@ -143,11 +151,6 @@ class UpdaterPageContainer extends React.PureComponent {
}
}
UpdaterPageContainer.propTypes = {
notifyOnly: propTypes.bool,
initialState: propTypes.object,
};
ReactDOM.render(
<UpdaterPageContainer
notifyOnly={notifyOnly}