Code split the client, update dependencies
This commit is contained in:
parent
84c3d5cc88
commit
d930365eeb
File diff suppressed because one or more lines are too long
@ -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: {
|
||||
|
@ -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": {
|
||||
|
@ -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'));
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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;
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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 &&
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
|
@ -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;
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
@ -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>
|
||||
|
9
client/src/js/components/ui/Button.js
Normal file
9
client/src/js/components/ui/Button.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
const Button = ({ children, ...props }) => (
|
||||
<button type="button" {...props}>
|
||||
{children}
|
||||
</button>
|
||||
);
|
||||
|
||||
export default Button;
|
@ -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}
|
||||
|
@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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} />}
|
||||
</>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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';
|
||||
|
||||
|
@ -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?'
|
||||
)
|
||||
) {
|
||||
|
@ -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);
|
||||
|
@ -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++) {
|
||||
|
@ -34,6 +34,7 @@ export default function linkify(text) {
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href={match.getAnchorHref()}
|
||||
key={i}
|
||||
>
|
||||
{match.matchedText}
|
||||
</a>
|
||||
|
@ -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'
|
||||
}
|
||||
})
|
||||
]
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
};
|
||||
|
1737
client/yarn.lock
1737
client/yarn.lock
File diff suppressed because it is too large
Load Diff
22
go.mod
22
go.mod
@ -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
35
go.sum
@ -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=
|
||||
|
@ -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>
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user