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

File diff suppressed because one or more lines are too long

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

22
go.mod
View File

@ -10,7 +10,7 @@ require (
github.com/boltdb/bolt v0.0.0-20180302180052-fd01fc79c553
github.com/couchbase/vellum v0.0.0-20180910213445-01d5c56e6095 // indirect
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 // indirect
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 // indirect
github.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7 // indirect
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 // indirect
github.com/dsnet/compress v0.0.0-20171208185109-cc9eb1d7ad76
github.com/edsrzf/mmap-go v0.0.0-20170320065105-0bce6a688712 // indirect
@ -21,7 +21,7 @@ require (
github.com/glycerine/go-unsnap-stream v0.0.0-20180323001048-9f0cb55181dd // indirect
github.com/glycerine/goconvey v0.0.0-20180728074245-46e3a41ad493 // indirect
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db // indirect
github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f // indirect
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e // indirect
github.com/gorilla/websocket v1.4.0
github.com/inconshreveable/mousetrap v1.0.0 // indirect
github.com/jmhodges/levigo v0.0.0-20161115193449-c42d9e0ca023 // indirect
@ -29,30 +29,30 @@ require (
github.com/jtolds/gls v4.2.1+incompatible // indirect
github.com/kjk/betterguid v0.0.0-20170621091430-c442874ba63a
github.com/kr/pretty v0.1.0 // indirect
github.com/kr/pty v1.1.3 // indirect
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329
github.com/miekg/dns v1.0.12 // indirect
github.com/miekg/dns v1.0.15 // indirect
github.com/mitchellh/go-homedir v1.0.0
github.com/mitchellh/mapstructure v1.1.2 // indirect
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae // indirect
github.com/onsi/gomega v1.4.2 // indirect
github.com/philhofer/fwd v1.0.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d // indirect
github.com/smartystreets/goconvey v0.0.0-20180222194500-ef6db91d284a // indirect
github.com/spf13/cast v1.2.0
github.com/spf13/cast v1.3.0
github.com/spf13/cobra v0.0.3
github.com/spf13/pflag v1.0.3 // indirect
github.com/spf13/viper v1.2.1
github.com/steveyen/gtreap v0.0.0-20150807155958-0abe01ef9be2 // indirect
github.com/stretchr/testify v1.2.2
github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d // indirect
github.com/tecbot/gorocksdb v0.0.0-20180907100951-214b6b7bc0f0 // indirect
github.com/syndtr/goleveldb v0.0.0-20181102132633-a4119e27a65d // indirect
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481 // indirect
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0 // indirect
github.com/willf/bitset v1.1.9 // indirect
github.com/xenolf/lego v1.0.1
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 // indirect
golang.org/x/net v0.0.0-20181005035420-146acd28ed58
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e // indirect
github.com/xenolf/lego v1.1.0
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 // indirect
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
gopkg.in/square/go-jose.v2 v2.1.9 // indirect
)

35
go.sum
View File

@ -18,6 +18,7 @@ github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07 h1:UHFGPvSxX4C4YBApSPvmUfL
github.com/cznic/b v0.0.0-20180115125044-35e9bbe41f07/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369 h1:XNT/Zf5l++1Pyg08/HV04ppB0gKxAqtZQBRYiYrUuYk=
github.com/cznic/mathutil v0.0.0-20180504122225-ca4c9f2c1369/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/mathutil v0.0.0-20181021201202-eba54fb065b7/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186 h1:0rkFMAbn5KBKNpJyHQ6Prb95vIKanmAe62KxsrN+sqA=
github.com/cznic/strutil v0.0.0-20171016134553-529a34b1c186/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@ -44,6 +45,8 @@ github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db h1:woRePGFeVFfLKN/pO
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f h1:JJ2EP5vV3LAD2U1CxQtD7PTOO15Y96kXmKDz7TjxGHs=
github.com/gopherjs/gopherjs v0.0.0-20181004151105-1babbf986f6f/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gopherjs/gopherjs v0.0.0-20181103185306-d547d1d9531e/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
github.com/gorilla/websocket v1.4.0 h1:WDFjx/TMzVgy9VdMMQi2K2Emtwi2QcUQsztZ/zLaH/Q=
github.com/gorilla/websocket v1.4.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
@ -63,6 +66,7 @@ github.com/kjk/betterguid v0.0.0-20170621091430-c442874ba63a/go.mod h1:uxRAhHE1n
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/pty v1.1.3/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/magiconair/properties v1.8.0 h1:LLgXmsheXeRoUOBOjtwPQCWIYqM/LU1ayDtDePerRcY=
@ -71,6 +75,10 @@ github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329 h1:2gxZ0XQIU/5z3Z3
github.com/mailru/easyjson v0.0.0-20180823135443-60711f1a8329/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc=
github.com/miekg/dns v1.0.12 h1:814rTNaw7Q7pGncpSEDT06YS8rdGmpUEnKgpQzctJsk=
github.com/miekg/dns v1.0.12/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.0.14 h1:9jZdLNd/P4+SfEJ0TNyxYpsK8N4GtfylBLqtbYN1sbA=
github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/miekg/dns v1.0.15 h1:9+UupePBQCG6zf1q/bGmTO1vumoG13jsrbWOSX1W6Tw=
github.com/miekg/dns v1.0.15/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg=
github.com/mitchellh/go-homedir v1.0.0 h1:vKb8ShqSby24Yrqr/yDYkuFz8d0WUjys40rvnGC8aR0=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.0.0 h1:vVpGvMXJPqSDh2VYHF7gsfQj8Ncx+Xw5Y1KHeTRY+7I=
@ -97,6 +105,8 @@ github.com/spf13/afero v1.1.2 h1:m8/z1t7/fwjysjQRYbP0RD+bUIF/8tJwPdEZsI83ACI=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/cast v1.2.0 h1:HHl1DSRbEQN2i8tJmtS6ViPyHx35+p51amrdsiTCrkg=
github.com/spf13/cast v1.2.0/go.mod h1:r2rcYCSwa1IExKTDiTfzaxqT2FNHs8hODu4LnUfgKEg=
github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.3 h1:ZlrZ4XsMRm04Fr5pSFxBgfND2EBVa1nLpiy1stUsX/8=
github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
github.com/spf13/jwalterweatherman v1.0.0 h1:XHEdyB+EcvlqZamSM4ZOMGlc93t6AcsBEu9Gc1vn7yk=
@ -113,25 +123,50 @@ github.com/stretchr/testify v1.2.2 h1:bSDNvY7ZPG5RlJ8otE/7V6gMiyenm9RtJ7IUVIAoJ1
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d h1:4J9HCZVpvDmj2tiKGSTUnb3Ok/9CEQb9oqu9LHKQQpc=
github.com/syndtr/goleveldb v0.0.0-20180815032940-ae2bd5eed72d/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181012014443-6b91fda63f2e/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/syndtr/goleveldb v0.0.0-20181102132633-a4119e27a65d/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0=
github.com/tecbot/gorocksdb v0.0.0-20180907100951-214b6b7bc0f0 h1:EEAoIgdGCLu3zSryPb/VFHaIGxDlgku3BflSZAtvJD0=
github.com/tecbot/gorocksdb v0.0.0-20180907100951-214b6b7bc0f0/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tecbot/gorocksdb v0.0.0-20181010114359-8752a9433481/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0 h1:uAwzi+JwkDdOtQZVqPYljFvJr7i43ZgUYXKypk9Eibk=
github.com/tinylib/msgp v0.0.0-20180215042507-3b5c87ab5fb0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
github.com/willf/bitset v1.1.9 h1:GBtFynGY9ZWZmEC9sWuu41/7VBXPFCOAbCbqTflOg9c=
github.com/willf/bitset v1.1.9/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
github.com/xenolf/lego v1.0.1 h1:Rr9iqO8MoNxY6OvqdIZTnNZ8bwt0RNz00nGXfoTq4Bc=
github.com/xenolf/lego v1.0.1/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY=
github.com/xenolf/lego v1.1.0 h1:Ias1pE9hO98/fI23RLza0T3461YiM720d96oxTRPyuM=
github.com/xenolf/lego v1.1.0/go.mod h1:fwiGnfsIjG7OHPfOvgK7Y/Qo6+2Ox0iozjNTkZICKbY=
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4 h1:Vk3wNqEZwyGyei9yq5ekj7frek2u7HUfffJ1/opblzc=
golang.org/x/crypto v0.0.0-20181001203147-e3636079e1a4/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774 h1:a4tQYYYuK9QdeO/+kEvNYyuR21S+7ve5EANok6hABhI=
golang.org/x/crypto v0.0.0-20181025213731-e84da0312774/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3 h1:KYQXGkl6vs02hK7pK4eIbw0NpNPedieTSTEiJ//bwGs=
golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16 h1:y6ce7gCWtnH+m3dCjzQ1PCuwl28DDIc3VNnvY29DlIA=
golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58 h1:otZG8yDCO4LVps5+9bxOeNiCvgmOyt96J3roHTYs7oE=
golang.org/x/net v0.0.0-20181005035420-146acd28ed58/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519 h1:x6rhz8Y9CjbgQkccRGmELH6K+LJj7tOoh3XWeC1yaQM=
golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816 h1:mVFkLpejdFLXVUv9E42f3XJVfMdqd0IVLVIVLjZWn5o=
golang.org/x/net v0.0.0-20181029044818-c44066c5c816/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102050134-b7e296877c6e h1:lIf8v8wMiSq+MBwNne+ZEkKrswgZ2NzQ1oeBn8eCA4c=
golang.org/x/net v0.0.0-20181102050134-b7e296877c6e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc h1:ZMCWScCvS2fUVFw8LOpxyUUW5qiviqr4Dg5NdjLeiLU=
golang.org/x/net v0.0.0-20181102091132-c10e9556a7bc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f h1:wMNYb4v58l5UBM7MYRLPG6ZhfOqbKu7X5eyFl8ZhKvA=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180906133057-8cf3aee42992/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e h1:EfdBzeKbFSvOjoIqSZcfS8wp0FBLokGBEs9lz1OtSg0=
golang.org/x/sys v0.0.0-20181005133103-4497e2df6f9e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5 h1:x6r4Jo0KNzOOzYd8lbcRsqjuqEASK6ob3auvWYM4/8U=
golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0 h1:biUuj9O+0+XckRUCDzjoOGm6yFV5c0IHbm1ODP3e4Zw=
golang.org/x/sys v0.0.0-20181030150119-7e31e0c00fa0/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc h1:SdCq5U4J+PpbSDIl9bM0V1e1Ug1jsnBkAFvTs1htn7U=
golang.org/x/sys v0.0.0-20181031143558-9b800f95dbbc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=

View File

@ -1,12 +1,14 @@
<%! data *indexData, cssPath, jsPath string %>
<%! data *indexData, cssPath string, inlineScript string, scripts []string %>
<%% import "github.com/mailru/easyjson" %%>
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#222">
<title>Dispatch</title>
@ -16,12 +18,20 @@
<link rel="preload" href="/font/Montserrat-Bold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<link rel="preload" href="/font/RobotoMono-Bold.woff2" as="font" type="font/woff2" crossorigin="anonymous">
<% if cssPath != "" { %>
<link href="/<%== cssPath %>" rel="stylesheet">
<% } %>
<link rel="icon" href="data:;base64,=">
<script><%== inlineScript %></script>
</head>
<body>
<div id="root"></div>
<script id="env" type="application/json"><% easyjson.MarshalToWriter(data, w) %></script>
<script src="/<%== jsPath %>"></script>
<% for _, script := range scripts { %>
<script src="/<%== script %>"></script>
<% } %>
</body>
</html>

View File

@ -7,13 +7,23 @@ import (
"github.com/mailru/easyjson"
)
func IndexTemplate(w io.Writer, data *indexData, cssPath, jsPath string) error {
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><title>Dispatch</title><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link href=\"/")
func IndexTemplate(w io.Writer, data *indexData, cssPath string, inlineScript string, scripts []string) error {
io.WriteString(w, "<!DOCTYPE html><html lang=\"en\"><head><meta charset=\"UTF-8\"><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"><meta name=\"theme-color\" content=\"#222\"><title>Dispatch</title><link rel=\"preload\" href=\"/font/fontello.woff2?48901973\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Regular.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/Montserrat-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\"><link rel=\"preload\" href=\"/font/RobotoMono-Bold.woff2\" as=\"font\" type=\"font/woff2\" crossorigin=\"anonymous\">")
if cssPath != "" {
io.WriteString(w, "<link href=\"/")
io.WriteString(w, cssPath )
io.WriteString(w, "\" rel=\"stylesheet\"><link rel=\"icon\" href=\"data:;base64,=\"></head><body><div id=\"root\"></div><script id=\"env\" type=\"application/json\">")
io.WriteString(w, "\" rel=\"stylesheet\">")
}
io.WriteString(w, "<link rel=\"icon\" href=\"data:;base64,=\"><script>")
io.WriteString(w, inlineScript )
io.WriteString(w, "</script></head><body><div id=\"root\"></div><script id=\"env\" type=\"application/json\">")
easyjson.MarshalToWriter(data, w)
io.WriteString(w, "</script><script src=\"/")
io.WriteString(w, jsPath )
io.WriteString(w, "\"></script></body></html>")
io.WriteString(w, "</script>")
for _, script := range scripts {
io.WriteString(w, "<script src=\"/")
io.WriteString(w, script )
io.WriteString(w, "\"></script>")
}
io.WriteString(w, "</body></html>")
return nil
}

View File

@ -3,8 +3,9 @@ package server
import (
"bytes"
"compress/gzip"
"crypto/md5"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"io"
"io/ioutil"
"log"
@ -16,9 +17,8 @@ import (
"time"
"github.com/dsnet/compress/brotli"
"github.com/spf13/viper"
"github.com/khlieng/dispatch/assets"
"github.com/spf13/viper"
)
const longCacheControl = "public, max-age=31536000, immutable"
@ -34,25 +34,32 @@ type File struct {
Compressed bool
}
var (
files = []*File{
&File{
Path: "bundle.js",
Asset: "bundle.js.br",
ContentType: "text/javascript",
CacheControl: longCacheControl,
Compressed: true,
},
&File{
Path: "bundle.css",
Asset: "bundle.css.br",
ContentType: "text/css",
CacheControl: longCacheControl,
Compressed: true,
},
type h2PushAsset struct {
path string
hash string
}
func newH2PushAsset(name string) h2PushAsset {
return h2PushAsset{
path: "/" + name,
hash: strings.Split(name, ".")[1],
}
}
var (
files []*File
indexStylesheet string
indexScripts []string
inlineScript string
inlineScriptSha256 string
h2PushAssets []h2PushAsset
h2PushCookieValue string
contentTypes = map[string]string{
".js": "text/javascript",
".css": "text/css",
".woff2": "font/woff2",
".woff": "application/font-woff",
".ttf": "application/x-font-ttf",
@ -63,47 +70,58 @@ var (
)
func (d *Dispatch) initFileServer() {
if !viper.GetBool("dev") {
data, err := assets.Asset(files[0].Asset)
if viper.GetBool("dev") {
indexScripts = []string{"bundle.js"}
} else {
data, err := assets.Asset("asset-manifest.json")
if err != nil {
log.Fatal(err)
}
hash := md5.Sum(data)
files[0].Hash = base64.RawURLEncoding.EncodeToString(hash[:])[:8]
files[0].Path = "bundle." + files[0].Hash + ".js"
br, err := brotli.NewReader(bytes.NewReader(data), nil)
manifest := map[string]string{}
err = json.Unmarshal(data, &manifest)
if err != nil {
log.Fatal(err)
}
buf := &bytes.Buffer{}
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
runtime, err := assets.Asset(manifest["runtime~main.js"] + ".br")
if err != nil {
log.Fatal(err)
}
runtime = decompressAsset(runtime)
inlineScript = string(runtime)
io.Copy(gzw, br)
gzw.Close()
files[0].GzipAsset = buf.Bytes()
hash := sha256.New()
hash.Write(runtime)
inlineScriptSha256 = base64.StdEncoding.EncodeToString(hash.Sum(nil))
data, err = assets.Asset(files[1].Asset)
if err != nil {
log.Fatal(err)
indexStylesheet = manifest["main.css"]
indexScripts = []string{
manifest["vendors~main.js"],
manifest["main.js"],
}
hash = md5.Sum(data)
files[1].Hash = base64.RawURLEncoding.EncodeToString(hash[:])[:8]
files[1].Path = "bundle." + files[1].Hash + ".css"
h2PushAssets = []h2PushAsset{
newH2PushAsset(indexStylesheet),
newH2PushAsset(indexScripts[0]),
newH2PushAsset(indexScripts[1]),
}
br.Reset(bytes.NewReader(data))
buf = &bytes.Buffer{}
gzw.Reset(buf)
for _, asset := range h2PushAssets {
h2PushCookieValue += asset.hash
}
io.Copy(gzw, br)
gzw.Close()
files[1].GzipAsset = buf.Bytes()
for _, assetPath := range manifest {
file := &File{
Path: assetPath,
Asset: assetPath + ".br",
ContentType: contentTypes[filepath.Ext(assetPath)],
CacheControl: longCacheControl,
Compressed: true,
}
files = append(files, file)
}
fonts, err := assets.AssetDir("font")
if err != nil {
@ -121,22 +139,18 @@ func (d *Dispatch) initFileServer() {
Compressed: strings.HasSuffix(font, ".br"),
}
files = append(files, file)
}
for _, file := range files {
if file.Compressed {
data, err = assets.Asset(file.Asset)
data, err := assets.Asset(file.Asset)
if err != nil {
log.Fatal(err)
}
br.Reset(bytes.NewReader(data))
buf = &bytes.Buffer{}
gzw.Reset(buf)
io.Copy(gzw, br)
gzw.Close()
file.GzipAsset = buf.Bytes()
file.GzipAsset = gzipAsset(data)
}
files = append(files, file)
}
if viper.GetBool("https.hsts.enabled") && viper.GetBool("https.enabled") {
@ -154,6 +168,34 @@ func (d *Dispatch) initFileServer() {
}
}
func decompressAsset(data []byte) []byte {
br, err := brotli.NewReader(bytes.NewReader(data), nil)
if err != nil {
log.Fatal(err)
}
buf := &bytes.Buffer{}
io.Copy(buf, br)
return buf.Bytes()
}
func gzipAsset(data []byte) []byte {
br, err := brotli.NewReader(bytes.NewReader(data), nil)
if err != nil {
log.Fatal(err)
}
buf := &bytes.Buffer{}
gzw, err := gzip.NewWriterLevel(buf, gzip.BestCompression)
if err != nil {
log.Fatal(err)
}
io.Copy(gzw, br)
gzw.Close()
return buf.Bytes()
}
func (d *Dispatch) serveFiles(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
d.serveIndex(w, r)
@ -181,7 +223,7 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
connectSrc = "ws://" + r.Host
}
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src data:; connect-src "+connectSrc)
w.Header().Set("Content-Security-Policy", "default-src 'none'; script-src 'self' 'sha256-"+inlineScriptSha256+"'; style-src 'self' 'unsafe-inline'; font-src 'self'; img-src data:; connect-src "+connectSrc)
}
w.Header().Set("Content-Type", "text/html")
@ -200,22 +242,22 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
"Accept-Encoding": r.Header["Accept-Encoding"],
},
}
cookie, err := r.Cookie("push")
if err != nil {
pusher.Push("/"+files[1].Path, options)
pusher.Push("/"+files[0].Path, options)
for _, asset := range h2PushAssets {
pusher.Push(asset.path, options)
}
setPushCookie(w, r)
} else {
pushed := false
if files[1].Hash != cookie.Value[8:] {
pusher.Push("/"+files[1].Path, options)
pushed = true
}
if files[0].Hash != cookie.Value[:8] {
pusher.Push("/"+files[0].Path, options)
pushed = true
for i, asset := range h2PushAssets {
if len(cookie.Value) >= (i+1)*8 &&
asset.hash != cookie.Value[i*8:(i+1)*8] {
pusher.Push(asset.path, options)
pushed = true
}
}
if pushed {
@ -228,17 +270,17 @@ func (d *Dispatch) serveIndex(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Encoding", "gzip")
gzw := gzip.NewWriter(w)
IndexTemplate(gzw, getIndexData(r, state), files[1].Path, files[0].Path)
IndexTemplate(gzw, getIndexData(r, state), indexStylesheet, inlineScript, indexScripts)
gzw.Close()
} else {
IndexTemplate(w, getIndexData(r, state), files[1].Path, files[0].Path)
IndexTemplate(w, getIndexData(r, state), indexStylesheet, inlineScript, indexScripts)
}
}
func setPushCookie(w http.ResponseWriter, r *http.Request) {
http.SetCookie(w, &http.Cookie{
Name: "push",
Value: files[0].Hash + files[1].Hash,
Value: h2PushCookieValue,
Path: "/",
Expires: time.Now().AddDate(1, 0, 0),
HttpOnly: true,