Code split the client, update dependencies
This commit is contained in:
parent
84c3d5cc88
commit
d930365eeb
37 changed files with 2036 additions and 1181 deletions
|
@ -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
Loading…
Add table
Add a link
Reference in a new issue