Code split the client, update dependencies

This commit is contained in:
Ken-Håvard Lieng 2018-11-04 07:22:46 +01:00
parent 84c3d5cc88
commit d930365eeb
37 changed files with 2036 additions and 1181 deletions

View file

@ -10,9 +10,10 @@ module.exports = {
'@babel/preset-react'
],
plugins: [
'@babel/plugin-proposal-class-properties',
['@babel/plugin-proposal-class-properties', { loose: true }],
'@babel/plugin-proposal-export-default-from',
'@babel/plugin-proposal-export-namespace-from'
'@babel/plugin-proposal-export-namespace-from',
'@babel/plugin-syntax-dynamic-import'
],
env: {
development: {

View file

@ -1,5 +1,5 @@
{
"extends": ["airbnb", "plugin:prettier/recommended"],
"extends": ["airbnb", "prettier", "prettier/react"],
"parser": "babel-eslint",
"env": {
"browser": true
@ -7,16 +7,14 @@
"rules": {
"consistent-return": 0,
"jsx-a11y/click-events-have-key-events": 0,
"jsx-a11y/no-autofocus": 0,
"jsx-a11y/no-noninteractive-element-interactions": 0,
"jsx-a11y/no-static-element-interactions": 0,
"no-console": 1,
"no-param-reassign": 0,
"no-plusplus": 0,
"no-restricted-globals": 1,
"react/destructuring-assignment": 0,
"react/jsx-filename-extension": 0,
"react/no-array-index-key": 0,
"react/prefer-stateless-function": 0,
"react/prop-types": 0
},
"settings": {

View file

@ -66,7 +66,7 @@ function fonts() {
function compress() {
return gulp
.src(['dist/!(*.toml)'])
.src(['dist/!(*.toml|*.json)'])
.pipe(brotli({ quality: 11 }))
.pipe(gulp.dest('dist'));
}

View file

@ -5,71 +5,75 @@
"license": "MIT",
"main": "index.js",
"browserslist": [
">0.4%",
"not op_mini all"
"Edge >= 16",
"Firefox >= 60",
"Chrome >= 61",
"Safari >= 10.1",
"iOS >= 10.3"
],
"devDependencies": {
"@babel/core": "^7.1.2",
"@babel/plugin-proposal-class-properties": "^7.1.0",
"@babel/plugin-proposal-export-default-from": "^7.0.0",
"@babel/plugin-proposal-export-namespace-from": "^7.0.0",
"@babel/plugin-syntax-dynamic-import": "^7.0.0",
"@babel/plugin-transform-react-constant-elements": "^7.0.0",
"@babel/plugin-transform-react-inline-elements": "^7.0.0",
"@babel/preset-env": "^7.1.0",
"@babel/preset-react": "^7.0.0",
"autoprefixer": "^9.1.5",
"babel-core": "^7.0.0-0",
"babel-eslint": "^10.0.1",
"babel-jest": "^23.6.0",
"babel-loader": "^8.0.4",
"brotli": "^1.3.1",
"css-loader": "^1.0.0",
"cssnano": "^4.1.4",
"css-loader": "^1.0.1",
"cssnano": "^4.1.7",
"del": "^3.0.0",
"eslint": "^5.6.1",
"eslint-config-airbnb": "^16.1.0",
"eslint": "^5.8.0",
"eslint-config-airbnb": "^17.1.0",
"eslint-config-prettier": "^3.1.0",
"eslint-import-resolver-webpack": "^0.10.1",
"eslint-loader": "^2.1.1",
"eslint-plugin-import": "^2.14.0",
"eslint-plugin-jsx-a11y": "^6.1.2",
"eslint-plugin-prettier": "^3.0.0",
"eslint-plugin-react": "^7.11.1",
"express": "^4.14.1",
"express": "^4.16.4",
"express-http-proxy": "^1.4.0",
"gulp": "4.0.0",
"gulp-util": "^3.0.8",
"jest": "^23.6.0",
"mini-css-extract-plugin": "^0.4.3",
"mini-css-extract-plugin": "^0.4.4",
"postcss-flexbugs-fixes": "^4.1.0",
"postcss-loader": "^3.0.0",
"postcss-preset-env": "^6.3.0",
"prettier": "1.14.3",
"style-loader": "^0.23.0",
"style-loader": "^0.23.1",
"terser-webpack-plugin": "^1.1.0",
"through2": "^2.0.3",
"webpack": "^4.20.2",
"webpack": "^4.23.1",
"webpack-dev-middleware": "^3.4.0",
"webpack-hot-middleware": "^2.24.2"
"webpack-hot-middleware": "^2.24.3",
"webpack-manifest-plugin": "^2.0.4"
},
"dependencies": {
"@sindresorhus/fnv1a": "^1.0.0",
"autolinker": "^1.7.1",
"backo": "^1.1.0",
"classnames": "^2.2.6",
"es6-promise": "^4.2.5",
"fontfaceobserver": "^2.0.9",
"formik": "1.3.1",
"formik": "^1.3.1",
"history": "4.5.1",
"hsluv": "^0.0.3",
"immer": "^1.7.2",
"immer": "^1.7.3",
"js-cookie": "^2.1.4",
"lodash": "^4.17.11",
"react": "^16.5.2",
"react-dom": "^16.5.2",
"react": "^16.7.0-alpha.0",
"react-dom": "^16.7.0-alpha.0",
"react-hot-loader": "^4.3.11",
"react-redux": "^5.0.2",
"react-redux": "^5.1.0",
"react-virtualized-auto-sizer": "^1.0.2",
"react-window": "^1.2.1",
"redux": "^4.0.0",
"react-window": "^1.2.2",
"redux": "^4.0.1",
"redux-thunk": "^2.3.0",
"reselect": "^4.0.0",
"url-pattern": "^1.0.3"

View file

@ -790,6 +790,15 @@ input.message-input-nick.invalid {
text-align: center;
}
.suspense-fallback {
display: flex;
align-items: center;
justify-content: center;
font: 700 64px 'Montserrat', sans-serif;
height: 100%;
color: #ddd;
}
@media (max-width: 600px) {
.app-info {
font-size: 12px;

View file

@ -1,53 +1,55 @@
import React, { Component } from 'react';
import React, { Suspense, lazy } from 'react';
import Route from 'containers/Route';
import Chat from 'containers/Chat';
import Connect from 'containers/Connect';
import Settings from 'containers/Settings';
import TabList from 'components/TabList';
import classnames from 'classnames';
export default class App extends Component {
handleClick = () => {
const { showTabList, hideMenu } = this.props;
const Chat = lazy(() => import('containers/Chat'));
const Connect = lazy(() => import('containers/Connect'));
const Settings = lazy(() => import('containers/Settings'));
const App = ({
connected,
tab,
channels,
servers,
privateChats,
showTabList,
select,
push,
hideMenu
}) => {
const mainClass = classnames('main-container', {
'off-canvas': showTabList
});
const handleClick = () => {
if (showTabList) {
hideMenu();
}
};
render() {
const {
connected,
tab,
channels,
servers,
privateChats,
showTabList,
select,
push
} = this.props;
const mainClass = classnames('main-container', {
'off-canvas': showTabList
});
return (
<div className="wrap">
{!connected && (
<div className="app-info">
Connection lost, attempting to reconnect...
</div>
)}
<div className="app-container" onClick={this.handleClick}>
<TabList
tab={tab}
channels={channels}
servers={servers}
privateChats={privateChats}
showTabList={showTabList}
select={select}
push={push}
/>
<div className={mainClass}>
return (
<div className="wrap" onClick={handleClick}>
{!connected && (
<div className="app-info">
Connection lost, attempting to reconnect...
</div>
)}
<div className="app-container">
<TabList
tab={tab}
channels={channels}
servers={servers}
privateChats={privateChats}
showTabList={showTabList}
select={select}
push={push}
/>
<div className={mainClass}>
<Suspense
maxDuration={1000}
fallback={<div className="suspense-fallback">...</div>}
>
<Route name="chat">
<Chat />
</Route>
@ -57,9 +59,11 @@ export default class App extends Component {
<Route name="settings">
<Settings />
</Route>
</div>
</Suspense>
</div>
</div>
);
}
}
</div>
);
};
export default App;

View file

@ -1,5 +1,6 @@
import React, { PureComponent } from 'react';
import classnames from 'classnames';
import Button from 'components/ui/Button';
import TabListItem from './TabListItem';
export default class TabList extends PureComponent {
@ -70,7 +71,7 @@ export default class TabList extends PureComponent {
<div className={className}>
<div className="tab-container">{tabs}</div>
<div className="side-buttons">
<button onClick={this.handleConnectClick}>+</button>
<Button onClick={this.handleConnectClick}>+</Button>
<i className="icon-user" />
<i className="icon-cog" onClick={this.handleSettingsClick} />
</div>

View file

@ -1,26 +1,26 @@
import React, { PureComponent } from 'react';
import React, { memo } from 'react';
import classnames from 'classnames';
export default class TabListItem extends PureComponent {
handleClick = () => {
const { server, target, onClick } = this.props;
onClick(server, target);
};
const TabListItem = ({
target,
content,
server,
selected,
connected,
onClick
}) => {
const className = classnames({
'tab-server': !target,
success: !target && connected,
error: !target && !connected,
selected
});
render() {
const { target, content, selected, connected } = this.props;
return (
<p className={className} onClick={() => onClick(server, target)}>
<span className="tab-content">{content}</span>
</p>
);
};
const className = classnames({
'tab-server': !target,
success: !target && connected,
error: !target && !connected,
selected
});
return (
<p className={className} onClick={this.handleClick}>
<span className="tab-content">{content}</span>
</p>
);
}
}
export default memo(TabListItem);

View file

@ -1,75 +1,73 @@
import React, { PureComponent } from 'react';
import React, { memo } from 'react';
import Navicon from 'containers/Navicon';
import Editable from 'components/ui/Editable';
import { isValidServerName } from 'state/servers';
import { isChannel, linkify } from 'utils';
export default class ChatTitle extends PureComponent {
render() {
const {
status,
title,
tab,
channel,
onTitleChange,
onToggleSearch,
onToggleUserList,
onCloseClick
} = this.props;
const ChatTitle = ({
status,
title,
tab,
channel,
onTitleChange,
onToggleSearch,
onToggleUserList,
onCloseClick
}) => {
let closeTitle;
if (isChannel(tab)) {
closeTitle = 'Leave';
} else if (tab.name) {
closeTitle = 'Close';
} else {
closeTitle = 'Disconnect';
}
let closeTitle;
if (isChannel(tab)) {
closeTitle = 'Leave';
} else if (tab.name) {
closeTitle = 'Close';
} else {
closeTitle = 'Disconnect';
}
let serverError = null;
if (!tab.name && status.error) {
serverError = (
<span className="chat-topic error">
Error:
{status.error}
</span>
);
}
return (
<div>
<div className="chat-title-bar">
<Navicon />
<Editable
className="chat-title"
editable={!tab.name}
value={title}
validate={isValidServerName}
onChange={onTitleChange}
>
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">
{channel && linkify(channel.topic)}
</span>
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
<i
className="icon-cancel button-leave"
title={closeTitle}
onClick={onCloseClick}
/>
<i className="icon-user button-userlist" onClick={onToggleUserList} />
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">
{channel && channel.users.length}
</span>
</div>
</div>
let serverError = null;
if (!tab.name && status.error) {
serverError = (
<span className="chat-topic error">
Error:
{status.error}
</span>
);
}
}
return (
<div>
<div className="chat-title-bar">
<Navicon />
<Editable
className="chat-title"
editable={!tab.name}
value={title}
validate={isValidServerName}
onChange={onTitleChange}
>
<span className="chat-title">{title}</span>
</Editable>
<div className="chat-topic-wrap">
<span className="chat-topic">
{channel && linkify(channel.topic)}
</span>
{serverError}
</div>
<i className="icon-search" title="Search" onClick={onToggleSearch} />
<i
className="icon-cancel button-leave"
title={closeTitle}
onClick={onCloseClick}
/>
<i className="icon-user button-userlist" onClick={onToggleUserList} />
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">
{channel && channel.users.length}
</span>
</div>
</div>
);
};
export default memo(ChatTitle);

View file

@ -1,43 +1,38 @@
import React, { PureComponent } from 'react';
import React, { memo } from 'react';
import classnames from 'classnames';
import stringToRGB from 'utils/color';
export default class Message extends PureComponent {
handleNickClick = () => this.props.onNickClick(this.props.message.from);
const Message = ({ message, coloredNick, style, onNickClick }) => {
const className = classnames('message', {
[`message-${message.type}`]: message.type
});
render() {
const { message, coloredNick } = this.props;
style = {
...style,
paddingLeft: `${window.messageIndent + 15}px`,
textIndent: `-${window.messageIndent}px`
};
const className = classnames('message', {
[`message-${message.type}`]: message.type
});
const style = {
paddingLeft: `${window.messageIndent + 15}px`,
textIndent: `-${window.messageIndent}px`,
...this.props.style
};
const senderStyle = {};
if (message.from && coloredNick) {
senderStyle.color = stringToRGB(message.from);
}
return (
<p className={className} style={style}>
<span className="message-time">{message.time}</span>
{message.from && (
<span
className="message-sender"
style={senderStyle}
onClick={this.handleNickClick}
>
{' '}
{message.from}
</span>
)}{' '}
{message.content}
</p>
);
const senderStyle = {};
if (message.from && coloredNick) {
senderStyle.color = stringToRGB(message.from);
}
}
return (
<p className={className} style={style}>
<span className="message-time">{message.time}</span>{' '}
{message.from && (
<span
className="message-sender"
style={senderStyle}
onClick={() => onNickClick(message.from)}
>
{message.from}
</span>
)}
{` ${message.content}`}
</p>
);
};
export default memo(Message);

View file

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { PureComponent, createRef } from 'react';
import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import debounce from 'lodash/debounce';
@ -12,6 +12,15 @@ const fetchThreshold = 600;
const scrollbackDebounce = 100;
export default class MessageBox extends PureComponent {
list = createRef();
outer = createRef();
addMore = debounce(() => {
const { tab, onAddMore } = this.props;
this.ready = true;
onAddMore(tab.server, tab.name);
}, scrollbackDebounce);
constructor(props) {
super(props);
@ -30,6 +39,23 @@ export default class MessageBox extends PureComponent {
});
}
componentDidUpdate(prevProps) {
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos(true);
}
if (this.nextScrollTop > 0) {
this.list.current.scrollTo(this.nextScrollTop);
this.nextScrollTop = 0;
} else if (this.bottom) {
this.list.current.scrollToItem(this.props.messages.length + 1);
}
}
componentWillUnmount() {
this.saveScrollPos();
}
getSnapshotBeforeUpdate(prevProps) {
if (prevProps.messages !== this.props.messages) {
this.list.current.resetAfterIndex(0);
@ -64,23 +90,6 @@ export default class MessageBox extends PureComponent {
return null;
}
componentDidUpdate(prevProps) {
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos(true);
}
if (this.nextScrollTop > 0) {
this.list.current.scrollTo(this.nextScrollTop);
this.nextScrollTop = 0;
} else if (this.bottom) {
this.list.current.scrollToItem(this.props.messages.length + 1);
}
}
componentWillUnmount() {
this.saveScrollPos();
}
getRowHeight = index => {
const { messages, hasMoreMessages } = this.props;
@ -106,9 +115,6 @@ export default class MessageBox extends PureComponent {
return messages[index - 1].id;
};
list = React.createRef();
outer = React.createRef();
updateScrollKey = () => {
const { tab } = this.props;
this.scrollKey = `msg:${tab.server}:${tab.name}`;
@ -145,12 +151,6 @@ export default class MessageBox extends PureComponent {
this.props.onFetchMore();
};
addMore = debounce(() => {
const { tab, onAddMore } = this.props;
this.ready = true;
onAddMore(tab.server, tab.name);
}, scrollbackDebounce);
handleScroll = ({ scrollOffset, scrollDirection }) => {
if (
!this.loading &&

View file

@ -1,25 +1,24 @@
import React, { PureComponent } from 'react';
import React, { memo, useState } from 'react';
import classnames from 'classnames';
import Editable from 'components/ui/Editable';
import { isValidNick } from 'utils';
export default class MessageInput extends PureComponent {
state = {
value: ''
};
handleKey = e => {
const {
tab,
onCommand,
onMessage,
add,
reset,
increment,
decrement,
currentHistoryEntry
} = this.props;
const MessageInput = ({
nick,
currentHistoryEntry,
onNickChange,
onNickEditDone,
tab,
onCommand,
onMessage,
add,
reset,
increment,
decrement
}) => {
const [value, setValue] = useState('');
const handleKey = e => {
if (e.key === 'Enter' && e.target.value) {
if (e.target.value[0] === '/') {
onCommand(e.target.value, tab.name, tab.server);
@ -29,50 +28,41 @@ export default class MessageInput extends PureComponent {
add(e.target.value);
reset();
this.setState({ value: '' });
setValue('');
} else if (e.key === 'ArrowUp') {
e.preventDefault();
increment();
} else if (e.key === 'ArrowDown') {
decrement();
} else if (currentHistoryEntry) {
this.setState({ value: e.target.value });
setValue(e.target.value);
reset();
}
};
handleChange = e => {
this.setState({ value: e.target.value });
};
const handleChange = e => setValue(e.target.value);
render() {
const {
nick,
currentHistoryEntry,
onNickChange,
onNickEditDone
} = this.props;
return (
<div className="message-input-wrap">
<Editable
className={classnames('message-input-nick', {
invalid: !isValidNick(nick)
})}
value={nick}
onBlur={onNickEditDone}
onChange={onNickChange}
>
<span className="message-input-nick">{nick}</span>
</Editable>
<input
className="message-input"
type="text"
value={currentHistoryEntry || value}
onKeyDown={handleKey}
onChange={handleChange}
/>
</div>
);
};
return (
<div className="message-input-wrap">
<Editable
className={classnames('message-input-nick', {
invalid: !isValidNick(nick)
})}
value={nick}
onBlur={onNickEditDone}
onChange={onNickChange}
>
<span className="message-input-nick">{nick}</span>
</Editable>
<input
className="message-input"
type="text"
value={currentHistoryEntry || this.state.value}
onKeyDown={this.handleKey}
onChange={this.handleChange}
/>
</div>
);
}
}
export default memo(MessageInput);

View file

@ -1,42 +1,41 @@
import React, { PureComponent } from 'react';
import React, { memo, useRef, useEffect } from 'react';
import SearchResult from './SearchResult';
export default class Search extends PureComponent {
componentDidUpdate(prevProps) {
if (!prevProps.search.show && this.props.search.show) {
this.input.focus();
}
}
const Search = ({ search, onSearch }) => {
const inputEl = useRef();
inputRef = el => {
this.input = el;
useEffect(
() => {
if (search.show) {
inputEl.current.focus();
}
},
[search.show]
);
const style = {
display: search.show ? 'block' : 'none'
};
handleSearch = e => this.props.onSearch(e.target.value);
let i = 0;
const results = search.results.map(result => (
<SearchResult key={i++} result={result} />
));
render() {
const { search } = this.props;
const style = {
display: search.show ? 'block' : 'none'
};
const results = search.results.map(result => (
<SearchResult key={result.id} result={result} />
));
return (
<div className="search" style={style}>
<div className="search-input-wrap">
<i className="icon-search" />
<input
ref={this.inputRef}
className="search-input"
type="text"
onChange={this.handleSearch}
/>
</div>
<div className="search-results">{results}</div>
return (
<div className="search" style={style}>
<div className="search-input-wrap">
<i className="icon-search" />
<input
ref={inputEl}
className="search-input"
type="text"
onChange={e => onSearch(e.target.value)}
/>
</div>
);
}
}
<div className="search-results">{results}</div>
</div>
);
};
export default memo(Search);

View file

@ -1,25 +1,24 @@
import React, { PureComponent } from 'react';
import React, { memo } from 'react';
import { timestamp, linkify } from 'utils';
export default class SearchResult extends PureComponent {
render() {
const { result } = this.props;
const style = {
paddingLeft: `${window.messageIndent}px`,
textIndent: `-${window.messageIndent}px`
};
const SearchResult = ({ result }) => {
const style = {
paddingLeft: `${window.messageIndent}px`,
textIndent: `-${window.messageIndent}px`
};
return (
<p className="search-result" style={style}>
<span className="message-time">
{timestamp(new Date(result.time * 1000))}
</span>
<span>
{' '}
<span className="message-sender">{result.from}</span>
</span>
<span> {linkify(result.content)}</span>
</p>
);
}
}
return (
<p className="search-result" style={style}>
<span className="message-time">
{timestamp(new Date(result.time * 1000))}
</span>
<span>
{' '}
<span className="message-sender">{result.from}</span>
</span>
<span> {linkify(result.content)}</span>
</p>
);
};
export default memo(SearchResult);

View file

@ -1,10 +1,12 @@
import React, { PureComponent } from 'react';
import React, { PureComponent, createRef } from 'react';
import { VariableSizeList as List } from 'react-window';
import AutoSizer from 'react-virtualized-auto-sizer';
import classnames from 'classnames';
import UserListItem from './UserListItem';
export default class UserList extends PureComponent {
list = createRef();
getSnapshotBeforeUpdate(prevProps) {
if (this.list.current) {
const { users } = this.props;
@ -43,8 +45,6 @@ export default class UserList extends PureComponent {
return index;
};
list = React.createRef();
renderUser = ({ index, style }) => {
const { users, coloredNicks, onNickClick } = this.props;

View file

@ -1,24 +1,19 @@
import React, { PureComponent } from 'react';
import React, { memo } from 'react';
import stringToRGB from 'utils/color';
export default class UserListItem extends PureComponent {
handleClick = () => this.props.onClick(this.props.user.nick);
render() {
const { user, coloredNick } = this.props;
let { style } = this.props;
if (coloredNick) {
style = {
color: stringToRGB(user.nick),
...style
};
}
return (
<p style={style} onClick={this.handleClick}>
{user.renderName}
</p>
);
const UserListItem = ({ user, coloredNick, style, onClick }) => {
if (coloredNick) {
style = {
...style,
color: stringToRGB(user.nick)
};
}
}
return (
<p style={style} onClick={() => onClick(user.nick)}>
{user.renderName}
</p>
);
};
export default memo(UserListItem);

View file

@ -2,6 +2,7 @@ import React, { Component } from 'react';
import { createSelector } from 'reselect';
import { Form, withFormik } from 'formik';
import Navicon from 'containers/Navicon';
import Button from 'components/ui/Button';
import Checkbox from 'components/ui/formik/Checkbox';
import TextInput from 'components/ui/TextInput';
import Error from 'components/ui/formik/Error';
@ -27,7 +28,7 @@ class Connect extends Component {
};
handleShowClick = () => {
this.setState({ showOptionals: !this.state.showOptionals });
this.setState(prevState => ({ showOptionals: !prevState.showOptionals }));
};
renderOptionals = () => {
@ -35,12 +36,9 @@ class Connect extends Component {
return (
<div>
{!hexIP && [
<TextInput name="username" placeholder="Username" />,
<Error name="username" />
]}
<TextInput type="password" name="password" placeholder="Password" />
<TextInput name="realname" placeholder="Realname" />
{!hexIP && <TextInput name="username" />}
<TextInput name="password" type="password" />
<TextInput name="realname" />
</div>
);
};
@ -64,19 +62,18 @@ class Connect extends Component {
))}
</div>
)}
<TextInput name="nick" placeholder="Nick" />
<Error name="nick" />
<button>Connect</button>
<TextInput name="nick" />
<Button type="submit">Connect</Button>
</Form>
);
} else {
form = (
<Form className="connect-form">
<h1>Connect</h1>
<TextInput name="name" placeholder="Name" autoCapitalize="words" />
<TextInput name="name" autoCapitalize="words" />
<div className="connect-form-address">
<TextInput name="host" placeholder="Host" />
<TextInput name="port" type="number" placeholder="Port" />
<TextInput name="host" noError />
<TextInput name="port" type="number" noError />
<Checkbox
name="tls"
label="SSL"
@ -86,13 +83,11 @@ class Connect extends Component {
</div>
<Error name="host" />
<Error name="port" />
<TextInput name="nick" placeholder="Nick" />
<Error name="nick" />
<TextInput name="channels" placeholder="Channels" />
<Error name="channels" />
<TextInput name="nick" />
<TextInput name="channels" />
{this.state.showOptionals && this.renderOptionals()}
<i className="icon-ellipsis" onClick={this.handleShowClick} />
<button>Connect</button>
<Button type="submit">Connect</Button>
</Form>
);
}

View file

@ -1,5 +1,6 @@
import React from 'react';
import Navicon from 'containers/Navicon';
import Button from 'components/ui/Button';
import Checkbox from 'components/ui/Checkbox';
import FileInput from 'components/ui/FileInput';
@ -44,9 +45,13 @@ const Settings = ({
onChange={onKeyChange}
/>
</div>
<button className="settings-button" onClick={uploadCert}>
<Button
type="submit"
className="settings-button"
onClick={uploadCert}
>
{status}
</button>
</Button>
{error ? <p className="error">{error}</p> : null}
</div>
</div>

View file

@ -0,0 +1,9 @@
import React from 'react';
const Button = ({ children, ...props }) => (
<button type="button" {...props}>
{children}
</button>
);
export default Button;

View file

@ -1,4 +1,4 @@
import React, { PureComponent } from 'react';
import React, { PureComponent, createRef } from 'react';
import { stringWidth } from 'utils';
export default class Editable extends PureComponent {
@ -6,26 +6,29 @@ export default class Editable extends PureComponent {
editable: true
};
inputEl = createRef();
state = {
editing: false
};
componentWillReceiveProps(nextProps) {
if (this.state.editing && nextProps.value !== this.props.value) {
this.updateInputWidth(nextProps.value);
}
}
componentDidUpdate(prevProps, prevState) {
if (!prevState.editing && this.state.editing) {
// eslint-disable-next-line react/no-did-update-set-state
this.updateInputWidth(this.props.value);
this.inputEl.current.focus();
}
}
getSnapshotBeforeUpdate(prevProps) {
if (this.state.editing && prevProps.value !== this.props.value) {
this.updateInputWidth(this.props.value);
}
}
updateInputWidth = value => {
if (this.input) {
const style = window.getComputedStyle(this.input);
if (this.inputEl.current) {
const style = window.getComputedStyle(this.inputEl.current);
const padding = parseInt(style.paddingRight, 10);
// Make sure the width is at least 1px so the caret always shows
const width =
@ -75,10 +78,6 @@ export default class Editable extends PureComponent {
e.target.value = val;
};
inputRef = el => {
this.input = el;
};
render() {
const { children, className, value } = this.props;
@ -90,8 +89,7 @@ export default class Editable extends PureComponent {
return this.state.editing ? (
<input
autoFocus
ref={this.inputRef}
ref={this.inputEl}
className={className}
type="text"
value={value}

View file

@ -1,11 +1,14 @@
import React, { PureComponent } from 'react';
import Button from 'components/ui/Button';
export default class FileInput extends PureComponent {
static defaultProps = {
type: 'text'
};
componentWillMount() {
constructor(props) {
super(props);
this.input = window.document.createElement('input');
this.input.setAttribute('type', 'file');
@ -37,9 +40,9 @@ export default class FileInput extends PureComponent {
render() {
return (
<button className="input-file" onClick={this.handleClick}>
<Button className="input-file" onClick={this.handleClick}>
{this.props.name}
</button>
</Button>
);
}
}

View file

@ -1,6 +1,8 @@
import React, { PureComponent } from 'react';
import { Field } from 'formik';
import { FastField } from 'formik';
import classnames from 'classnames';
import capitalize from 'lodash/capitalize';
import Error from 'components/ui/formik/Error';
export default class TextInput extends PureComponent {
constructor(props) {
@ -36,44 +38,49 @@ export default class TextInput extends PureComponent {
};
render() {
const { name, placeholder, ...props } = this.props;
const { name, label = capitalize(name), noError, ...props } = this.props;
return (
<Field
<FastField
name={name}
render={({ field, form }) => (
<div className="textinput">
<input
className={field.value && 'value'}
type="text"
name={name}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
ref={this.input}
onFocus={this.handleFocus}
{...field}
{...props}
/>
<span
className={classnames('textinput-1', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{placeholder}
</span>
<span
className={classnames('textinput-2', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{placeholder}
</span>
</div>
)}
render={({ field, form }) => {
return (
<>
<div className="textinput">
<input
className={field.value && 'value'}
type="text"
name={name}
autoCapitalize="off"
autoCorrect="off"
autoComplete="off"
spellCheck="false"
ref={this.input}
onFocus={this.handleFocus}
{...field}
{...props}
/>
<span
className={classnames('textinput-1', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
<span
className={classnames('textinput-2', {
value: field.value,
error: form.touched[name] && form.errors[name]
})}
>
{label}
</span>
</div>
{!noError && <Error name={name} />}
</>
);
}}
/>
);
}

View file

@ -1,25 +1,27 @@
import React from 'react';
import { Field } from 'formik';
import React, { memo } from 'react';
import { FastField } from 'formik';
import Checkbox from 'components/ui/Checkbox';
const FormikCheckbox = ({ name, onChange, ...props }) => (
<Field
<FastField
name={name}
render={({ field, form }) => (
<Checkbox
name={name}
checked={field.value}
onChange={e => {
form.setFieldTouched(name, true);
field.onChange(e);
if (onChange) {
onChange(e);
}
}}
{...props}
/>
)}
render={({ field, form }) => {
return (
<Checkbox
name={name}
checked={field.value}
onChange={e => {
form.setFieldTouched(name, true);
field.onChange(e);
if (onChange) {
onChange(e);
}
}}
{...props}
/>
);
}}
/>
);
export default FormikCheckbox;
export default memo(FormikCheckbox);

View file

@ -1,5 +1,5 @@
import 'es6-promise/auto';
import 'utils/ie11';
//import 'es6-promise/auto';
//import 'utils/ie11';
import React from 'react';
import { render } from 'react-dom';

View file

@ -123,7 +123,7 @@ export default function handleSocket({
connection_update({ server, errorType }) {
if (
errorType === 'verify' &&
confirm(
window.confirm(
'The server is using a self-signed certificate, continue anyway?'
)
) {

View file

@ -19,7 +19,7 @@ export const getCurrentInputHistoryEntry = state => {
export default createReducer(initialState, {
[actions.INPUT_HISTORY_ADD](state, { line }) {
if (line.trim() && line !== state.history[0]) {
if (history.length === HISTORY_MAX_LENGTH) {
if (state.history.length === HISTORY_MAX_LENGTH) {
state.history.pop();
}
state.history.unshift(line);

View file

@ -1,6 +1,26 @@
import fnv1a from '@sindresorhus/fnv1a';
/* eslint-disable no-bitwise */
import { hsluvToHex } from 'hsluv';
//
// github.com/sindresorhus/fnv1a
//
const OFFSET_BASIS_32 = 2166136261;
const fnv1a = string => {
let hash = OFFSET_BASIS_32;
for (let i = 0; i < string.length; i++) {
hash ^= string.charCodeAt(i);
// 32-bit FNV prime: 2**24 + 2**8 + 0x93 = 16777619
// Using bitshift for accuracy and performance. Numbers in JS suck.
hash +=
(hash << 1) + (hash << 4) + (hash << 7) + (hash << 8) + (hash << 24);
}
return hash >>> 0;
};
const colors = [];
for (let i = 0; i < 72; i++) {

View file

@ -34,6 +34,7 @@ export default function linkify(text) {
target="_blank"
rel="noopener noreferrer"
href={match.getAnchorHref()}
key={i}
>
{match.matchedText}
</a>

View file

@ -1,6 +1,6 @@
var path = require('path');
var webpack = require('webpack');
var autoprefixer = require('autoprefixer');
var postcssPresetEnv = require('postcss-preset-env');
module.exports = {
mode: 'development',
@ -44,8 +44,10 @@ module.exports = {
options: {
plugins: [
require('postcss-flexbugs-fixes'),
autoprefixer({
flexbox: 'no-2009'
postcssPresetEnv({
autoprefixer: {
flexbox: 'no-2009'
}
})
]
}

View file

@ -1,14 +1,16 @@
var path = require('path');
var webpack = require('webpack');
var MiniCssExtractPlugin = require('mini-css-extract-plugin');
var autoprefixer = require('autoprefixer');
var postcssPresetEnv = require('postcss-preset-env');
var cssnano = require('cssnano');
var TerserPlugin = require('terser-webpack-plugin');
var ManifestPlugin = require('webpack-manifest-plugin');
module.exports = {
mode: 'production',
entry: ['./src/js/index'],
output: {
filename: 'bundle.js'
filename: '[name].[chunkhash:8].js',
chunkFilename: '[name].[chunkhash:8].js'
},
resolve: {
alias: {
@ -29,7 +31,11 @@ module.exports = {
fix: true
}
},
{ test: /\.js$/, loader: 'babel-loader', exclude: /node_modules/ },
{
test: /\.js$/,
loader: 'babel-loader',
exclude: /node_modules/
},
{
test: /\.css$/,
use: [
@ -43,10 +49,13 @@ module.exports = {
{
loader: 'postcss-loader',
options: {
ident: 'postcss',
plugins: [
require('postcss-flexbugs-fixes'),
autoprefixer({
flexbox: 'no-2009'
postcssPresetEnv({
autoprefixer: {
flexbox: 'no-2009'
}
}),
cssnano({
discardUnused: {
@ -62,17 +71,24 @@ module.exports = {
},
plugins: [
new MiniCssExtractPlugin({
filename: 'bundle.css'
filename: '[name].[contenthash:8].css',
chunkFilename: '[name].[contenthash:8].css'
}),
new ManifestPlugin({
fileName: 'asset-manifest.json'
})
],
optimization: {
minimizer: [new TerserPlugin()],
splitChunks: {
chunks: 'all',
cacheGroups: {
styles: {
test: /\.css$/,
chunks: 'all'
}
}
}
},
runtimeChunk: true
}
};

File diff suppressed because it is too large Load diff