diff options
Diffstat (limited to 'app/components')
| -rw-r--r-- | app/components/Account.js | 47 | ||||
| -rw-r--r-- | app/components/AccountInput.js | 187 | ||||
| -rw-r--r-- | app/components/Connect.js | 52 | ||||
| -rw-r--r-- | app/components/CustomScrollbars.js | 23 | ||||
| -rw-r--r-- | app/components/HeaderBar.js | 21 | ||||
| -rw-r--r-- | app/components/Layout.js | 69 | ||||
| -rw-r--r-- | app/components/Login.js | 140 | ||||
| -rw-r--r-- | app/components/SelectLocation.js | 39 | ||||
| -rw-r--r-- | app/components/Settings.js | 45 | ||||
| -rw-r--r-- | app/components/Switch.js | 89 | ||||
| -rw-r--r-- | app/components/WindowChrome.js | 18 |
11 files changed, 321 insertions, 409 deletions
diff --git a/app/components/Account.js b/app/components/Account.js index 7aaec4f2e6..e817baa280 100644 --- a/app/components/Account.js +++ b/app/components/Account.js @@ -1,43 +1,40 @@ +// @flow import moment from 'moment'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { If, Then, Else } from 'react-if'; import { Layout, Container, Header } from './Layout'; import { formatAccount } from '../lib/formatters'; import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; -export default class Account extends Component { - - static propTypes = { - onLogout: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - onExternalLink: PropTypes.func.isRequired - } +import type { UserReduxState } from '../reducers/user'; - onClose() { - this.props.onClose(); - } +export type AccountProps = { + user: UserReduxState; + onLogout: () => void; + onClose: () => void; + onExternalLink: (type: string) => void; +}; - onExternalLink(type) { - this.props.onExternalLink(type); - } +export default class Account extends Component { + props: AccountProps; - onLogout() { - this.props.onLogout(); - } + onBuyMore = () => this.props.onExternalLink('purchase'); + onClose = () => this.props.onClose(); + onLogout = () => this.props.onLogout(); - render() { - let paidUntil = moment(this.props.user.paidUntil); - let formattedAccountId = formatAccount(this.props.user.account); - let formattedPaidUntil = paidUntil.format('hA, D MMMM YYYY').toUpperCase(); - let isOutOfTime = paidUntil.isSameOrBefore(moment()); + render(): React.Element<*> { + const user = this.props.user; + const paidUntil = moment(user.paidUntil); + const formattedAccountId = formatAccount(user.account || ''); + const formattedPaidUntil = paidUntil.format('hA, D MMMM YYYY').toUpperCase(); + const isOutOfTime = paidUntil.isSameOrBefore(moment()); return ( <Layout> <Header hidden={ true } style={ 'defaultDark' } /> <Container> <div className="account"> - <div className="account__close" onClick={ ::this.onClose }> + <div className="account__close" onClick={ this.onClose }> <img className="account__close-icon" src="./assets/images/icon-back.svg" /> <span className="account__close-title">Settings</span> </div> @@ -68,11 +65,11 @@ export default class Account extends Component { </div> <div className="account__footer"> - <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }> + <button className="button button--positive" onClick={ this.onBuyMore }> <span className="button-label">Buy more time</span> <ExternalLinkSVG className="button-icon button-icon--16" /> </button> - <button className="button button--negative" onClick={ ::this.onLogout }>Logout</button> + <button className="button button--negative" onClick={ this.onLogout }>Logout</button> </div> </div> diff --git a/app/components/AccountInput.js b/app/components/AccountInput.js index 3e6993a9bb..0aeed76551 100644 --- a/app/components/AccountInput.js +++ b/app/components/AccountInput.js @@ -1,56 +1,57 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { formatAccount } from '../lib/formatters'; -/** - * Account input field with automatic formatting - * - * @export - * @class AccountInput - * @extends {React.Component} - */ +// @TODO: move it into types.js + +// ESLint issue: https://github.com/babel/babel-eslint/issues/445 +/* eslint-disable no-unused-vars */ +declare class ClipboardData { + setData(type: string, data: string): void; + getData(type: string): string; +} + +declare class ClipboardEvent extends Event { + clipboardData: ClipboardData; +} + +type AccountInputProps = { + value: string; + onEnter: () => void; + onChange: (newValue: string) => void; +}; + +type AccountInputState = { + value: string; + selectionRange: SelectionRange; +}; + +type SelectionRange = [number, number]; + export default class AccountInput extends Component { + props: AccountInputProps; + state: AccountInputState = { + value: '', + selectionRange: [0, 0] + }; - /** - * Prop types - * @static - * - * @memberOf AccountInput - */ - static propTypes = { - value: PropTypes.string, - onEnter: PropTypes.func, - onChange: PropTypes.func - } + _ref: ?HTMLInputElement; + _ignoreSelect = false; - /** - * Creates an instance of AccountInput. - * @param {object} props - * - * @memberOf AccountInput - */ - constructor(props) { + constructor(props: AccountInputProps) { super(props); // selection range holds selection converted from DOM selection range to // internal unformatted representation of account number const val = this.sanitize(props.value); - /** - * @type {object} - * @property {string} value - raw text value - * @property {number[]} selectionRange - raw text mapped selection range [start, end] - */ this.state = { value: val, selectionRange: [val.length, val.length] }; } - /** - * @override - */ - componentWillReceiveProps(nextProps) { + componentWillReceiveProps(nextProps: AccountInputProps) { const nextVal = this.sanitize(nextProps.value); if(nextVal !== this.state.value) { const len = nextVal.length; @@ -58,10 +59,7 @@ export default class AccountInput extends Component { } } - /** - * @override - */ - shouldComponentUpdate(nextProps, nextState) { + shouldComponentUpdate(nextProps: AccountInputProps, nextState: AccountInputState) { return (this.props.value !== nextProps.value || this.props.onEnter !== nextProps.onEnter || this.props.onChange !== nextProps.onChange || @@ -70,31 +68,20 @@ export default class AccountInput extends Component { this.state.selectionRange[1] !== nextState.selectionRange[1]); } - /** - * @override - */ - render() { + render(): React.Element<*> { const displayString = formatAccount(this.state.value || ''); - const props = Object.assign({}, this.props); - - // exclude built-in props - for(let key of Object.keys(AccountInput.propTypes)) { - if(props.hasOwnProperty(key)) { - delete props[key]; - } - } - + const { value, onChange, onEnter, ...otherProps } = this.props; return ( - <input type="text" + <input { ...otherProps } + type="text" value={ displayString } onChange={ () => {} } - onSelect={ ::this.onSelect } - onKeyUp={ ::this.onKeyUp } - onKeyDown={ ::this.onKeyDown } - onPaste={ ::this.onPaste } - onCut={ ::this.onCut } - ref={ ::this.onRef } - { ...props } /> + onSelect={ this.onSelect } + onKeyUp={ this.onKeyUp } + onKeyDown={ this.onKeyDown } + onPaste={ this.onPaste } + onCut={ this.onCut } + ref={ this.onRef } /> ); } @@ -102,14 +89,8 @@ export default class AccountInput extends Component { /** * Modify original string inserting substring using selection range - * - * @private - * @param {String?} val string - * @returns {String} - * - * @memberOf AccountInput */ - sanitize(val) { + sanitize(val: ?string): string { return (val || '').replace(/[^0-9]/g, ''); } @@ -121,10 +102,8 @@ export default class AccountInput extends Component { * @param {String} insert insertion string * @param {Array} selRange selection range ([x,y]) * @returns {Object} - * - * @memberOf AccountInput */ - insert(val, insert, selRange) { + insert(val: string, insert: string, selRange: SelectionRange): AccountInputState { const head = val.slice(0, selRange[0]); const tail = val.slice(selRange[1], val.length); const newVal = head + insert + tail; @@ -144,7 +123,7 @@ export default class AccountInput extends Component { * * @memberOf AccountInput */ - remove(val, selRange) { + remove(val: string, selRange: SelectionRange): AccountInputState { let newVal, selectionOffset; if(selRange[0] === selRange[1]) { @@ -174,7 +153,7 @@ export default class AccountInput extends Component { * * @memberOf AccountInput */ - toInternalSelectionRange(val, domRange) { + toInternalSelectionRange(val: string, domRange: SelectionRange): SelectionRange { const countSpaces = (val) => { return (val.match(/\s/g) || []).length; }; @@ -202,7 +181,7 @@ export default class AccountInput extends Component { * * @memberOf AccountInput */ - toDomSelection(val, selRange) { + toDomSelection(val: string, selRange: SelectionRange): SelectionRange { const countSpaces = (val, untilIndex) => { if(val.length > 12) { return 0; } return Math.floor(untilIndex / 4); // groups of 4 digits @@ -221,13 +200,7 @@ export default class AccountInput extends Component { // Events - /** - * Key down handler - * @private - * @param {event} e - * @memberOf AccountInput - */ - onKeyDown(e) { + onKeyDown = (e: KeyboardEvent) => { const { value, selectionRange } = this.state; if(e.which === 8) { // backspace @@ -255,13 +228,7 @@ export default class AccountInput extends Component { } } - /** - * Key up handler - * @private - * @param {event} e - * @memberOf AccountInput - */ - onKeyUp(e) { + onKeyUp = (e: KeyboardEvent) => { this._ignoreSelect = false; if(e.which === 13 && this.props.onEnter) { @@ -269,14 +236,11 @@ export default class AccountInput extends Component { } } - /** - * Select handler - * @private - * @param {event} e - * @memberOf AccountInput - */ - onSelect(e) { + onSelect = (e: Event) => { const ref = e.target; + if(!(ref instanceof HTMLInputElement)) { + throw new Error('ref must be an instance of HTMLInputElement'); + } if(this._ignoreSelect) { return; @@ -288,13 +252,7 @@ export default class AccountInput extends Component { this.setState({ selectionRange: selRange }); } - /** - * Paste handler - * @private - * @param {event} e - * @memberOf AccountInput - */ - onPaste(e) { + onPaste = (e: ClipboardEvent) => { const { value, selectionRange } = this.state; const pastedData = e.clipboardData.getData('text'); const filteredData = this.sanitize(pastedData); @@ -307,13 +265,12 @@ export default class AccountInput extends Component { }); } - /** - * Cut handler - * @private - * @param {event} e - * @memberOf AccountInput - */ - onCut(e) { + onCut = (e: ClipboardEvent) => { + const target = e.target; + if(!(target instanceof HTMLInputElement)) { + throw new Error('ref must be an instance of HTMLInputElement'); + } + const { value, selectionRange } = this.state; e.preventDefault(); @@ -322,7 +279,7 @@ export default class AccountInput extends Component { if(selectionRange[0] !== selectionRange[1]) { const result = this.remove(value, selectionRange); const domSelectionRange = this.toDomSelection(value, selectionRange); - const slice = e.target.value.slice(domSelectionRange[0], domSelectionRange[1]); + const slice = target.value.slice(domSelectionRange[0], domSelectionRange[1]); e.clipboardData.setData('text', slice); @@ -334,13 +291,7 @@ export default class AccountInput extends Component { } } - /** - * Reference handler - * @private - * @param {DOMElement} ref - * @memberOf AccountInput - */ - onRef(ref) { + onRef = (ref: HTMLInputElement) => { this._ref = ref; if(!ref) { return; } @@ -351,10 +302,6 @@ export default class AccountInput extends Component { ref.selectionEnd = domRange[1]; } - /** - * Focus on text field - * @memberOf AccountInput - */ focus() { if(this._ref) { this._ref.focus(); diff --git a/app/components/Connect.js b/app/components/Connect.js index 6ffb516ec6..7ad0d4cc8b 100644 --- a/app/components/Connect.js +++ b/app/components/Connect.js @@ -22,21 +22,21 @@ type DisplayLocation = { city: ?string; }; -export default class Connect extends Component { - - props: { - user: UserReduxState, - connect: ConnectReduxState, - settings: SettingsReduxState, - onSettings: () => void, - onSelectLocation: () => void, - onConnect: (address: string) => void, - onCopyIP: () => void, - onDisconnect: () => void, - onExternalLink: (type: string) => void, - getServerInfo: (identifier: string) => ?ServerInfo - }; +export type ConnectProps = { + user: UserReduxState, + connect: ConnectReduxState, + settings: SettingsReduxState, + onSettings: () => void, + onSelectLocation: () => void, + onConnect: (address: string) => void, + onCopyIP: () => void, + onDisconnect: () => void, + onExternalLink: (type: string) => void, + getServerInfo: (identifier: string) => ?ServerInfo +}; +export default class Connect extends Component { + props: ConnectProps; state = { isFirstPass: true, showCopyIPMessage: false @@ -89,12 +89,12 @@ export default class Connect extends Component { </div> <If condition={ error.type === 'NO_CREDIT' }> <Then> - <div> - <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }> - <span className="button-label">Buy more time</span> - <ExternalLinkSVG className="button-icon button-icon--16" /> - </button> - </div> + <div> + <button className="button button--positive" onClick={ this.onExternalLink.bind(this, 'purchase') }> + <span className="button-label">Buy more time</span> + <ExternalLinkSVG className="button-icon button-icon--16" /> + </button> + </div> </Then> </If> </div> @@ -129,12 +129,12 @@ export default class Connect extends Component { <div className="connect"> <div className="connect__map"> <ReactMapboxGl - style={ mapboxConfig.styleURL } - accessToken={ mapboxConfig.accessToken } - containerStyle={{ height: '100%' }} - interactive={ false } - fitBounds={ mapBounds } - fitBoundsOptions={ mapBoundsOptions }> + style={ mapboxConfig.styleURL } + accessToken={ mapboxConfig.accessToken } + containerStyle={{ height: '100%' }} + interactive={ false } + fitBounds={ mapBounds } + fitBoundsOptions={ mapBoundsOptions }> <If condition={ isConnected }> <Then> <Marker coordinates={ serverLocation } offset={ [0, -10] }> diff --git a/app/components/CustomScrollbars.js b/app/components/CustomScrollbars.js index e74cbd7c65..4a0899c1be 100644 --- a/app/components/CustomScrollbars.js +++ b/app/components/CustomScrollbars.js @@ -1,28 +1,13 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { Scrollbars } from 'react-custom-scrollbars'; -/** - * Custom scrollbars component - * - * @export - * @class CustomScrollbars - * @extends {React.Component} - */ export default class CustomScrollbars extends Component { - /** - * PropTypes - * @static - * @memberOf CustomScrollbars - */ - static propTypes = { - children: PropTypes.element + props: { + children: ?React.Element<*> } - /** - * @override - */ - render() { + render(): React.Element<*> { return ( <Scrollbars { ...this.props } diff --git a/app/components/HeaderBar.js b/app/components/HeaderBar.js index 5574951a06..bae3c4fddf 100644 --- a/app/components/HeaderBar.js +++ b/app/components/HeaderBar.js @@ -3,20 +3,21 @@ import React, { Component } from 'react'; import { If, Then } from 'react-if'; export type HeaderBarStyle = 'default' | 'defaultDark' | 'error' | 'success'; +export type HeaderBarProps = { + style: HeaderBarStyle; + hidden: boolean; + showSettings: boolean; + onSettings: ?(() => void); +}; -/** - * Header bar component - */ export default class HeaderBar extends Component { - - props: { - style: HeaderBarStyle, - hidden: boolean, - showSettings: boolean, - onSettings: () => void + props: HeaderBarProps; + static defaultProps: $Shape<HeaderBarProps> = { + hidden: false, + showSettings: false }; - render() { + render(): React.Element<*> { let containerClass = [ 'headerbar', 'headerbar--' + process.platform, diff --git a/app/components/Layout.js b/app/components/Layout.js index 58af3d0813..5c0e1f5bcb 100644 --- a/app/components/Layout.js +++ b/app/components/Layout.js @@ -1,20 +1,14 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import HeaderBar from './HeaderBar'; -/** - * Layout header - * - * @export - * @class Header - * @extends {React.Component} - */ +import type { HeaderBarProps } from './HeaderBar'; + export class Header extends Component { + props: HeaderBarProps; + static defaultProps = HeaderBar.defaultProps; - /** - * @override - */ - render() { + render(): React.Element<*> { return ( <div className="layout__header"> <HeaderBar { ...this.props } /> @@ -23,28 +17,12 @@ export class Header extends Component { } } -/** - * Content container - * - * @export - * @class Container - * @extends {React.Component} - */ export class Container extends Component { + props: { + children: React.Element<*> + } - /** - * PropTypes - * @static - * @memberOf Container - */ - static propTypes = { - children: PropTypes.element.isRequired - }; - - /** - * @override - */ - render() { + render(): React.Element<*> { return ( <div className="layout__container"> { this.props.children } @@ -53,31 +31,12 @@ export class Container extends Component { } } -/** - * Layout container - * - * @export - * @class Layout - * @extends {React.Component} - */ export class Layout extends Component { + props: { + children: Array<React.Element<*>> | React.Element<*> + } - /** - * PropTypes - * @static - * @memberOf Container - */ - static propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.element), - PropTypes.element, - ]) - }; - - /** - * @override - */ - render() { + render(): React.Element<*> { return ( <div className="layout"> { this.props.children } diff --git a/app/components/Login.js b/app/components/Login.js index 72ef933082..9daa3f7e78 100644 --- a/app/components/Login.js +++ b/app/components/Login.js @@ -1,58 +1,41 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { If, Then } from 'react-if'; import { Layout, Container, Header } from './Layout'; import AccountInput from './AccountInput'; import ExternalLinkSVG from '../assets/images/icon-extLink.svg'; import LoginArrowSVG from '../assets/images/icon-arrow.svg'; -export default class Login extends Component { - static propTypes = { - user: PropTypes.object.isRequired, - onLogin: PropTypes.func.isRequired, - onSettings: PropTypes.func.isRequired, - onChange: PropTypes.func.isRequired, - onFirstChangeAfterFailure: PropTypes.func.isRequired, - onExternalLink: PropTypes.func.isRequired, - }; +import type { LoginState } from '../enums'; +import type { UserReduxState } from '../reducers/user'; - constructor(props) { - super(props); - this.state = { - notifyOnFirstChangeAfterFailure: false, - isActive: false - }; - } +export type LoginPropTypes = { + user: UserReduxState, + onLogin: (accountNumber: string) => void, + onSettings: ?(() => void), + onChange: (input: string) => void, + onFirstChangeAfterFailure: () => void, + onExternalLink: (type: string) => void, +}; - componentWillReceiveProps(nextProps) { - const prev = this.props.user || {}; - const next = nextProps.user || {}; - - if(prev.status !== next.status && next.status === 'failed') { - this.setState({ notifyOnFirstChangeAfterFailure: true }); - } +export default class Login extends Component { + props: LoginPropTypes; + state = { + notifyOnFirstChangeAfterFailure: false, + isActive: false } - onLogin() { + onCreateAccount = () => this.props.onExternalLink('createAccount'); + onFocus = () => this.setState({ isActive: true }); + onBlur = () => this.setState({ isActive: false }); + onLogin = () => { const { account } = this.props.user; - if(account.length > 0) { + if(account && account.length > 0) { this.props.onLogin(account); } } - onCreateAccount() { - this.props.onExternalLink('createAccount'); - } - - onFocus() { - this.setState({ isActive: true }); - } - - onBlur() { - this.setState({ isActive: false }); - } - - onInputChange(val) { + onInputChange = (val: string) => { // notify delegate on first change after login failure if(this.state.notifyOnFirstChangeAfterFailure) { this.setState({ notifyOnFirstChangeAfterFailure: false }); @@ -61,7 +44,7 @@ export default class Login extends Component { this.props.onChange(val); } - formTitle(s) { + formTitle(s: LoginState): string { switch(s) { case 'connecting': return 'Logging in...'; case 'failed': return 'Login failed'; @@ -70,22 +53,22 @@ export default class Login extends Component { } } - formSubtitle(s, e) { + formSubtitle(s: LoginState, e: ?Error): string { switch(s) { - case 'failed': return e.message; + case 'failed': return (e && e.message) || 'Unknown error'; case 'connecting': return 'Checking account number'; default: return 'Enter your account number'; } } - inputWrapClass(user) { + inputWrapClass(s: LoginState): string { const classes = ['login-form__input-wrap']; if(this.state.isActive) { classes.push('login-form__input-wrap--active'); } - switch(user.status) { + switch(s) { case 'connecting': classes.push('login-form__input-wrap--inactive'); break; @@ -97,9 +80,9 @@ export default class Login extends Component { return classes.join(' '); } - footerClass(user) { + footerClass(s: LoginState): string { const classes = ['login-footer']; - switch(user.status) { + switch(s) { case 'ok': case 'connecting': classes.push('login-footer--invisible'); @@ -108,31 +91,46 @@ export default class Login extends Component { return classes.join(' '); } - submitClass(user) { + submitClass(s: LoginState, account: ?string): string { const classes = ['login-form__submit']; - if(typeof(user.account) === 'string' && user.account.length > 0) { + if(account && account.length > 0) { classes.push('login-form__submit--active'); } - if(user.status === 'connecting') { + if(s === 'connecting') { classes.push('login-form__submit--invisible'); } return classes.join(' '); } - render() { + componentWillReceiveProps(nextProps: LoginPropTypes) { + const prev = this.props.user || {}; + const next = nextProps.user || {}; + + if(prev.status !== next.status && next.status === 'failed') { + this.setState({ notifyOnFirstChangeAfterFailure: true }); + } + } + + render(): React.Element<*> { const { account, status, error } = this.props.user; const title = this.formTitle(status); const subtitle = this.formSubtitle(status, error); - const isConnecting = status === 'connecting'; - const isFailed = status === 'failed'; - const isLoggedIn = status === 'ok'; - const inputWrapClass = this.inputWrapClass(this.props.user); - const footerClass = this.footerClass(this.props.user); - const submitClass = this.submitClass(this.props.user); + let isConnecting = false; + let isFailed = false; + let isLoggedIn = false; + switch(status) { + case 'connecting': isConnecting = true; break; + case 'failed': isFailed = true; break; + case 'ok': isLoggedIn = true; break; + } + + const inputWrapClass = this.inputWrapClass(status); + const footerClass = this.footerClass(status); + const submitClass = this.submitClass(status, account); const autoFocusRef = input => { if(isFailed && input) { @@ -178,25 +176,25 @@ export default class Login extends Component { <div className="login-form__subtitle">{ subtitle }</div> <div className={ inputWrapClass }> <AccountInput className="login-form__input-field" - type="text" - placeholder="e.g 0000 0000 0000" - onFocus={ ::this.onFocus } - onBlur={ ::this.onBlur } - onChange={ ::this.onInputChange } - onEnter={ ::this.onLogin } - value={ account } - disabled={ isConnecting } - autoFocus={ true } - ref={ autoFocusRef } /> - <button className={ submitClass } onClick={ ::this.onLogin }> - <LoginArrowSVG className="login-form__submit-icon" /> - </button> + type="text" + placeholder="e.g 0000 0000 0000" + onFocus={ this.onFocus } + onBlur={ this.onBlur } + onChange={ this.onInputChange } + onEnter={ this.onLogin } + value={ account || '' } + disabled={ isConnecting } + autoFocus={ true } + ref={ autoFocusRef } /> + <button className={ submitClass } onClick={ this.onLogin }> + <LoginArrowSVG className="login-form__submit-icon" /> + </button> </div> </div> </div> <div className={ footerClass }> - <div className="login-footer__prompt">Don't have an account number?</div> - <button className="button button--primary" onClick={ ::this.onCreateAccount }> + <div className="login-footer__prompt">{ 'Don\'t have an account number?' }</div> + <button className="button button--primary" onClick={ this.onCreateAccount }> <span className="button-label">Create account</span> <ExternalLinkSVG className="button-icon button-icon--16" /> </button> diff --git a/app/components/SelectLocation.js b/app/components/SelectLocation.js index 0dd52ab2bb..c94df2a11d 100644 --- a/app/components/SelectLocation.js +++ b/app/components/SelectLocation.js @@ -1,26 +1,31 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { If, Then } from 'react-if'; import { Layout, Container, Header } from './Layout'; import { servers } from '../config'; import CustomScrollbars from './CustomScrollbars'; -export default class SelectLocation extends Component { +import type { SettingsReduxState } from '../reducers/settings'; - static propTypes = { - onClose: PropTypes.func.isRequired, - onSelect: PropTypes.func.isRequired - } +export type SelectLocationProps = { + settings: SettingsReduxState, + onClose: () => void; + onSelect: (server: string) => void; +}; + +export default class SelectLocation extends Component { + props: SelectLocationProps; + _selectedCell: ?HTMLElement; - onSelect(name) { + onSelect(name: string) { this.props.onSelect(name); } - isSelected(key) { - return key === this.props.settings.preferredServer; + isSelected(server: string) { + return server === this.props.settings.preferredServer; } - drawCell(key, name, icon, onClick) { + drawCell(key: string, name: string, icon: ?string, onClick: (e: Event) => void): React.Element<*> { const classes = ['select-location__cell']; const selected = this.isSelected(key); @@ -51,7 +56,7 @@ export default class SelectLocation extends Component { ); } - onCellRef(key, element) { + onCellRef(key: string, element: HTMLElement) { // save reference to selected cell if(this.isSelected(key)) { this._selectedCell = element; @@ -60,12 +65,18 @@ export default class SelectLocation extends Component { componentDidMount() { // restore scroll to selected cell - if(this._selectedCell) { - this._selectedCell.scrollIntoViewIfNeeded(true); + const cell = this._selectedCell; + if(cell) { + // this is non-standard webkit method but it works great! + if(typeof(cell.scrollIntoViewIfNeeded) !== 'function') { + console.warn('HTMLElement.scrollIntoViewIfNeeded() is not available anymore! Please replace it with viable alternative.'); + return; + } + cell.scrollIntoViewIfNeeded(true); } } - render() { + render(): React.Element<*> { return ( <Layout> <Header hidden={ true } style={ 'defaultDark' } /> diff --git a/app/components/Settings.js b/app/components/Settings.js index eaec0355f2..9b8eff5321 100644 --- a/app/components/Settings.js +++ b/app/components/Settings.js @@ -1,39 +1,36 @@ +// @flow import moment from 'moment'; import React, { Component } from 'react'; -import PropTypes from 'prop-types'; import { If, Then, Else } from 'react-if'; import { Layout, Container, Header } from './Layout'; import Switch from './Switch'; import CustomScrollbars from './CustomScrollbars'; -export default class Settings extends Component { +import type { UserReduxState } from '../reducers/user'; +import type { SettingsReduxState } from '../reducers/settings'; - static propTypes = { - onQuit: PropTypes.func.isRequired, - onLogout: PropTypes.func.isRequired, - onClose: PropTypes.func.isRequired, - onViewAccount: PropTypes.func.isRequired, - onExternalLink: PropTypes.func.isRequired, - onUpdateSettings: PropTypes.func.isRequired - } +export type SettingsProps = { + user: UserReduxState, + settings: SettingsReduxState, + onQuit: () => void, + onClose: () => void, + onViewAccount: () => void, + onExternalLink: (type: string) => void, + onUpdateSettings: (update: $Shape<SettingsReduxState>) => void +}; - onClose() { - this.props.onClose(); - } +export default class Settings extends Component { - onAutoSecure(isOn) { - this.props.onUpdateSettings({ autoSecure: isOn }); - } + props: SettingsProps; - onExternalLink(type) { - this.props.onExternalLink(type); - } + onClose = () => this.props.onClose(); + onAutoSecure = (autoSecure: boolean) => this.props.onUpdateSettings({ autoSecure }); - onLogout() { - this.props.onLogout(); + onExternalLink(type: string) { + this.props.onExternalLink(type); } - render() { + render(): React.Element<*> { const isLoggedIn = this.props.user.status === 'ok'; let isOutOfTime = false, formattedPaidUntil = ''; let paidUntilIso = this.props.user.paidUntil; @@ -49,7 +46,7 @@ export default class Settings extends Component { <Header hidden={ true } style={ 'defaultDark' } /> <Container> <div className="settings"> - <button className="settings__close" onClick={ ::this.onClose } /> + <button className="settings__close" onClick={ this.onClose } /> <div className="settings__container"> <div className="settings__header"> <h2 className="settings__title">Settings</h2> @@ -82,7 +79,7 @@ export default class Settings extends Component { <div className="settings__cell"> <div className="settings__cell-label">Auto-connect</div> <div className="settings__cell-value"> - <Switch onChange={ ::this.onAutoSecure } isOn={ this.props.settings.autoSecure } /> + <Switch onChange={ this.onAutoSecure } isOn={ this.props.settings.autoSecure } /> </div> </div> <div className="settings__cell-footer"> diff --git a/app/components/Switch.js b/app/components/Switch.js index 4cb0ea34c3..46baeb8b22 100644 --- a/app/components/Switch.js +++ b/app/components/Switch.js @@ -1,27 +1,32 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; + +import type { Point2d } from '../types'; const CLICK_TIMEOUT = 1000; const MOVE_THRESHOLD = 10; export default class Switch extends Component { + props: { + isOn: boolean; + onChange: ?((isOn: boolean) => void); + } - static propTypes = { - isOn: PropTypes.bool, - onChange: PropTypes.func + defaultProps = { + isOn: false } - constructor(props) { - super(props); - this.state = { - isTracking: false, - ignoreChange: false, - initialPos: null, - startTime: null - }; + ref: ?HTMLInputElement; + onRef = (e: HTMLInputElement) => this.ref = e; + + state = { + isTracking: false, + ignoreChange: false, + initialPos: (null: ?Point2d), + startTime: (null: ?number) } - handleMouseDown(e) { + handleMouseDown = (e: MouseEvent) => { const { pageX: x, pageY: y } = e; this.setState({ isTracking: true, @@ -30,14 +35,19 @@ export default class Switch extends Component { }); } - handleMouseMove(e) { - if(!this.state.isTracking) { return; } + handleMouseMove = (e: MouseEvent) => { + if(!this.state.isTracking) { + return; + } + const inputElement = this.ref; const { x: x0 } = this.state.initialPos; const { pageX: x, pageY: y } = e; const dx = Math.abs(x0 - x); - if(dx < MOVE_THRESHOLD) { return; } + if(dx < MOVE_THRESHOLD) { + return; + } const isOn = !!this.props.isOn; let nextOn = isOn; @@ -53,12 +63,16 @@ export default class Switch extends Component { initialPos: { x, y }, ignoreChange: true }); - this.refs.input.checked = nextOn; + + if(inputElement) { + inputElement.checked = nextOn; + } + this.notify(nextOn); } } - handleMouseUp() { + handleMouseUp = () => { if(this.state.isTracking) { this.setState({ isTracking: false, @@ -67,8 +81,19 @@ export default class Switch extends Component { } } - handleChange(e) { - const dt = e.timeStamp - this.state.startTime; + handleChange = (e: Event) => { + const startTime = this.state.startTime; + const eventTarget = e.target; + + if(typeof(startTime) !== 'number') { + throw new Error('startTime must be a number.'); + } + + if(!(eventTarget instanceof HTMLInputElement)) { + throw new Error('e.target must be an instance of HTMLInputElement.'); + } + + const dt = e.timeStamp - startTime; if(this.state.ignoreChange) { this.setState({ ignoreChange: false }); @@ -76,30 +101,32 @@ export default class Switch extends Component { } else if(dt > CLICK_TIMEOUT) { e.preventDefault(); } else { - this.notify(e.target.checked); + this.notify(eventTarget.checked); } } - notify(isOn) { - if(this.props.onChange) { - this.props.onChange(isOn); + notify(isOn: boolean) { + const onChange = this.props.onChange; + if(onChange) { + onChange(isOn); } } componentDidMount() { - document.addEventListener('mousemove', ::this.handleMouseMove); - document.addEventListener('mouseup', ::this.handleMouseUp); + document.addEventListener('mousemove', this.handleMouseMove); + document.addEventListener('mouseup', this.handleMouseUp); } componentWillUnmount() { - document.removeEventListener('mousemove', ::this.handleMouseMove); - document.removeEventListener('mouseup', ::this.handleMouseUp); + document.removeEventListener('mousemove', this.handleMouseMove); + document.removeEventListener('mouseup', this.handleMouseUp); } - render() { + render(): React.Element<*> { return ( - <input type="checkbox" ref="input" className="switch" checked={ this.props.isOn } - onMouseDown={ ::this.handleMouseDown } onChange={ ::this.handleChange } /> + <input type="checkbox" ref={ this.onRef } className="switch" checked={ this.props.isOn } + onMouseDown={ this.handleMouseDown } + onChange={ this.handleChange } /> ); } } diff --git a/app/components/WindowChrome.js b/app/components/WindowChrome.js index 16bdb70e28..141c99fdb4 100644 --- a/app/components/WindowChrome.js +++ b/app/components/WindowChrome.js @@ -1,20 +1,10 @@ +// @flow import React, { Component } from 'react'; -import PropTypes from 'prop-types'; -/** - * A component used to chip out arrow in the app header using CSS mask - * - * @export - * @class WindowChrome - * @extends {Component} - */ export default class WindowChrome extends Component { - static propTypes = { - children: PropTypes.oneOfType([ - PropTypes.arrayOf(PropTypes.element), - PropTypes.element, - ]) - }; + props: { + children: Array<React.Element<*>> | React.Element<*> + } render() { const chromeClass = ['window-chrome', 'window-chrome--' + process.platform]; |
