Move wrapWidth handling out of MessageBox, improve scroll position handling, use custom routing, close menu when clicking anywhere

This commit is contained in:
Ken-Håvard Lieng 2017-05-07 22:19:15 +02:00
parent a753efd1dd
commit fec7c93abc
24 changed files with 363 additions and 235 deletions

File diff suppressed because one or more lines are too long

View File

@ -41,19 +41,18 @@
"autolinker": "^1.4.3",
"backo": "^1.1.0",
"base64-arraybuffer": "^0.1.5",
"history": "^4.5.1",
"history": "4.5.0",
"immutable": "^3.8.1",
"lodash": "^4.17.4",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"react-hot-loader": "next",
"react-redux": "^5.0.2",
"react-router": "^3.0.2",
"react-router-redux": "^4.0.8",
"react-virtualized": "^9.3.0",
"redux": "^3.6.0",
"redux-thunk": "^2.2.0",
"reselect": "^3.0.0"
"reselect": "^3.0.0",
"url-pattern": "^1.0.3"
},
"scripts": {
"test": "jest",

View File

@ -415,7 +415,7 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
}
.message {
padding: 3px 15px;
padding: 4px 15px;
}
.message-info {
@ -525,10 +525,20 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
text-overflow: ellipsis;
}
.ReactVirtualized__Grid {
.ReactVirtualized__List {
box-sizing: content-box !important;
outline: none;
}
.rvlist-messages {
padding: 7px 0;
overflow-y: scroll !important;
}
.rvlist-users {
padding: 10px 0;
}
@media (max-width: 600px) {
.tablist {
width: 200px;

View File

@ -1,10 +1,10 @@
import { LOCATION_CHANGE } from 'react-router-redux';
import reducer from '../reducers/tab';
import * as actions from '../actions';
import { setSelectedTab } from '../actions/tab';
import { locationChanged } from '../util/router';
describe('reducers/tab', () => {
it('sets the tab and adds it to history', () => {
it('selects the tab and adds it to history', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
expect(state.toJS()).toEqual({
@ -90,12 +90,7 @@ describe('reducers/tab', () => {
it('clears the tab when navigating to a non-tab page', () => {
let state = reducer(undefined, setSelectedTab('srv', '#chan'));
state = reducer(state, {
type: LOCATION_CHANGE,
payload: {
pathname: '/settings'
}
});
state = reducer(state, locationChanged('settings'));
expect(state.toJS()).toEqual({
selected: { server: null, name: null },
@ -104,4 +99,20 @@ describe('reducers/tab', () => {
]
});
});
it('selects the tab and adds it to history when navigating to a tab', () => {
const state = reducer(undefined,
locationChanged('chat', {
server: 'srv',
name: '#chan'
})
);
expect(state.toJS()).toEqual({
selected: { server: 'srv', name: '#chan' },
history: [
{ server: 'srv', name: '#chan' }
]
});
});
});

View File

@ -121,6 +121,7 @@ export function addMessages(messages, server, to, prepend, next) {
if (next) {
messages[0].id = next;
messages[0].next = true;
}
messages.forEach(message => initMessage(message, server, message.tab || tab, state));

View File

@ -1,14 +1,10 @@
import { push, replace } from 'react-router-redux';
import * as actions from '../actions';
import { push, replace } from '../util/router';
export function select(server, name) {
const pm = name && name.charAt(0) !== '#';
if (pm) {
return push(`/${server}/pm/${name}`);
} else if (name) {
if (name) {
return push(`/${server}/${encodeURIComponent(name)}`);
}
return push(`/${server}`);
}

View File

@ -3,18 +3,19 @@ import { List } from 'react-virtualized/dist/commonjs/List';
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import debounce from 'lodash/debounce';
import Message from './Message';
import { measureScrollBarWidth } from '../util';
import { getScrollPos, saveScrollPos } from '../util/scrollPosition';
const scrollBarWidth = measureScrollBarWidth();
const listStyle = { padding: '7px 0', boxSizing: 'content-box' };
const threshold = 100;
const fetchThreshold = 100;
export default class MessageBox extends PureComponent {
componentDidMount() {
componentWillMount() {
this.loadScrollPos();
}
componentDidMount() {
this.scrollTop = -1;
}
componentWillUpdate(nextProps) {
if (nextProps.messages !== this.props.messages) {
this.list.recomputeRowHeights();
@ -22,6 +23,7 @@ export default class MessageBox extends PureComponent {
if (nextProps.tab !== this.props.tab) {
this.saveScrollPos();
this.bottom = false;
}
if (nextProps.messages.get(0) !== this.props.messages.get(0)) {
@ -41,7 +43,7 @@ export default class MessageBox extends PureComponent {
componentDidUpdate(prevProps) {
if (prevProps.tab !== this.props.tab) {
this.loadScrollPos();
this.loadScrollPos(true);
}
if (this.nextScrollTop > 0) {
@ -50,8 +52,6 @@ export default class MessageBox extends PureComponent {
} else if (this.bottom) {
this.list.scrollToRow(this.props.messages.size);
}
this.updateWidth();
}
componentWillUnmount() {
@ -66,7 +66,7 @@ export default class MessageBox extends PureComponent {
return 0;
}
return this.props.messages.get(index - 1).height;
}
};
listRef = el => {
this.list = el;
@ -80,15 +80,22 @@ export default class MessageBox extends PureComponent {
const { tab } = this.props;
this.scrollKey = `msg:${tab.server}:${tab.name}`;
return this.scrollKey;
}
};
loadScrollPos = () => {
loadScrollPos = scroll => {
const pos = getScrollPos(this.updateScrollKey());
if (pos >= 0) {
this.bottom = false;
this.container.scrollTop = pos;
if (scroll) {
this.list.scrollToPosition(pos);
} else {
this.scrollTop = pos;
}
} else {
this.bottom = true;
if (scroll) {
this.list.scrollToRow(this.props.messages.size);
}
}
};
@ -98,37 +105,6 @@ export default class MessageBox extends PureComponent {
} else {
saveScrollPos(this.scrollKey, this.container.scrollTop);
}
}
scrollDown = () => {
this.container.scrollTop = this.container.scrollHeight - this.container.clientHeight;
};
updateWidth = (width) => {
const { tab, setWrapWidth, updateMessageHeight } = this.props;
let wrapWidth = width || this.width;
if (width) {
if (tab.isChannel() && window.innerWidth > 600) {
wrapWidth += 200;
}
this.width = wrapWidth;
}
if (this.container.scrollHeight > this.container.clientHeight) {
wrapWidth -= scrollBarWidth;
}
if (this.wrapWidth !== wrapWidth) {
this.wrapWidth = wrapWidth;
setWrapWidth(wrapWidth);
updateMessageHeight();
}
};
handleResize = size => {
this.updateWidth(size.width - 30);
};
fetchMore = debounce(() => {
@ -138,7 +114,7 @@ export default class MessageBox extends PureComponent {
handleScroll = ({ scrollTop, clientHeight, scrollHeight }) => {
if (this.props.hasMoreMessages &&
scrollTop <= threshold &&
scrollTop <= fetchThreshold &&
scrollTop < this.prevScrollTop &&
!this.loading) {
if (this.mouseDown) {
@ -196,13 +172,20 @@ export default class MessageBox extends PureComponent {
};
render() {
const props = {};
if (this.bottom) {
props.scrollToIndex = this.props.messages.size;
} else if (this.scrollTop >= 0) {
props.scrollTop = this.scrollTop;
}
return (
<div
className="messagebox"
onMouseDown={this.handleMouseDown}
onMouseUp={this.handleMouseUp}
>
<AutoSizer onResize={this.handleResize}>
<AutoSizer>
{({ width, height }) => (
<List
ref={this.listRef}
@ -212,7 +195,8 @@ export default class MessageBox extends PureComponent {
rowHeight={this.getRowHeight}
rowRenderer={this.renderMessage}
onScroll={this.handleScroll}
style={listStyle}
className="rvlist-messages"
{...props}
/>
)}
</AutoSizer>

View File

@ -2,20 +2,9 @@ import React, { PureComponent } from 'react';
import TabListItem from './TabListItem';
export default class TabList extends PureComponent {
handleTabClick = (server, target) => {
this.props.select(server, target);
this.props.hideMenu();
};
handleConnectClick = () => {
this.props.pushPath('/connect');
this.props.hideMenu();
};
handleSettingsClick = () => {
this.props.pushPath('/settings');
this.props.hideMenu();
};
handleTabClick = (server, target) => this.props.select(server, target);
handleConnectClick = () => this.props.pushPath('/connect');
handleSettingsClick = () => this.props.pushPath('/settings');
render() {
const { tab, channels, servers, privateChats, showTabList } = this.props;

View File

@ -3,8 +3,6 @@ import { List } from 'react-virtualized/dist/commonjs/List';
import { AutoSizer } from 'react-virtualized/dist/commonjs/AutoSizer';
import UserListItem from './UserListItem';
const listStyle = { padding: '10px 0', boxSizing: 'content-box' };
export default class UserList extends PureComponent {
componentWillUpdate(nextProps) {
if (nextProps.users.size === this.props.users.size) {
@ -49,7 +47,7 @@ export default class UserList extends PureComponent {
rowCount={this.props.users.size}
rowHeight={24}
rowRenderer={this.renderUser}
style={listStyle}
className="rvlist-users"
/>
)}
</AutoSizer>

View File

@ -1,19 +1,32 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { push } from 'react-router-redux';
import { push } from '../util/router';
import Route from './Route';
import Chat from './Chat';
import Connect from './Connect';
import Settings from './Settings';
import TabList from '../components/TabList';
import { select } from '../actions/tab';
import { hideMenu } from '../actions/ui';
class App extends PureComponent {
handleClick = () => {
if (this.props.showTabList) {
this.props.hideMenu();
}
};
render() {
const { showTabList, children } = this.props;
const { showTabList } = this.props;
const mainClass = showTabList ? 'main-container off-canvas' : 'main-container';
return (
<div>
<div onClick={this.handleClick}>
<TabList {...this.props} />
<div className={mainClass}>
{children}
<Route name="chat"><Chat /></Route>
<Route name="connect"><Connect /></Route>
<Route name="settings"><Settings /></Route>
</div>
</div>
);

View File

@ -11,7 +11,7 @@ import UserList from '../components/UserList';
import { part } from '../actions/channel';
import { openPrivateChat, closePrivateChat } from '../actions/privateChat';
import { searchMessages, toggleSearch } from '../actions/search';
import { select, setSelectedTab } from '../actions/tab';
import { select } from '../actions/tab';
import { runCommand, sendMessage, updateMessageHeight, fetchMessages } from '../actions/message';
import { disconnect } from '../actions/server';
import { setWrapWidth, setCharWidth } from '../actions/environment';
@ -21,12 +21,6 @@ import * as inputHistoryActions from '../actions/inputHistory';
import { getSelectedTab } from '../reducers/tab';
import { getSelectedMessages } from '../reducers/messages';
function updateSelected({ params, dispatch }) {
if (params.server) {
dispatch(setSelectedTab(params.server, params.channel || params.user));
}
}
function updateCharWidth() {
const charWidth = stringWidth(' ', '16px Roboto Mono, monospace');
window.messageIndent = 6 * charWidth;
@ -36,17 +30,7 @@ function updateCharWidth() {
class Chat extends PureComponent {
componentWillMount() {
const { dispatch } = this.props;
dispatch(updateCharWidth());
setTimeout(() => dispatch(updateCharWidth()), 1000);
updateSelected(this.props);
}
componentWillReceiveProps(nextProps) {
if (nextProps.params.server !== this.props.params.server ||
nextProps.params.channel !== this.props.params.channel ||
nextProps.params.user !== this.props.params.user) {
updateSelected(nextProps);
}
}
handleSearch = phrase => {
@ -154,7 +138,10 @@ const titleSelector = createSelector(
const getHasMoreMessages = createSelector(
getSelectedMessages,
messages => messages.get(0) && typeof messages.get(0).id === 'string'
messages => {
const first = messages.get(0);
return first && first.next;
}
);
const mapStateToProps = createStructuredSelector({

View File

@ -1,13 +1,13 @@
import React, { Component } from 'react';
import { Router } from 'react-router';
import { Provider } from 'react-redux';
import App from './App';
export default class Root extends Component {
render() {
const { store, routes, history } = this.props;
const { store } = this.props;
return (
<Provider store={store}>
<Router routes={routes} history={history} />
<App />
</Provider>
);
}

View File

@ -0,0 +1,20 @@
import { PureComponent } from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
class Route extends PureComponent {
render() {
if (this.props.route === this.props.name) {
return this.props.children;
}
return null;
}
}
const getRoute = state => state.router.route;
const mapStateToProps = createStructuredSelector({
route: getRoute
});
export default connect(mapStateToProps)(Route);

View File

@ -1,21 +1,23 @@
import React from 'react';
import { render } from 'react-dom';
import { browserHistory } from 'react-router';
import { syncHistoryWithStore, replace } from 'react-router-redux';
import { AppContainer } from 'react-hot-loader';
import 'react-virtualized/styles.css';
import configureStore from './store';
import createRoutes from './routes';
import initRouter, { replace } from './util/router';
import routes from './routes';
import Socket from './util/Socket';
import handleSocket from './socket';
import Root from './containers/Root';
import { addMessages } from './actions/message';
import { initWidthUpdates } from './util/messageHeight';
const host = DEV ? `${window.location.hostname}:1337` : window.location.host;
const socket = new Socket(host);
const store = configureStore(socket, browserHistory);
const store = configureStore(socket);
initRouter(routes, store);
handleSocket(socket, store);
const env = JSON.parse(document.getElementById('env').innerHTML);
@ -47,20 +49,17 @@ if (env.users) {
});
}
initWidthUpdates(store);
if (env.messages) {
const { messages, server, to, next } = env.messages;
store.dispatch(addMessages(messages, server, to, false, next));
}
handleSocket(socket, store);
const routes = createRoutes();
const history = syncHistoryWithStore(browserHistory, store);
const renderRoot = () => {
render(
<AppContainer>
<Root store={store} routes={routes} history={history} />
<Root store={store} />
</AppContainer>,
document.getElementById('root')
);
@ -69,5 +68,5 @@ const renderRoot = () => {
renderRoot();
if (module.hot) {
module.hot.accept('./routes', () => renderRoot());
module.hot.accept('./containers/Root', () => renderRoot());
}

View File

@ -1,5 +1,4 @@
import { combineReducers } from 'redux';
import { routerReducer } from 'react-router-redux';
import channels from './channels';
import environment from './environment';
import input from './input';
@ -11,16 +10,18 @@ import settings from './settings';
import tab from './tab';
import ui from './ui';
export default combineReducers({
routing: routerReducer,
channels,
environment,
input,
messages,
privateChats,
search,
servers,
settings,
tab,
ui
});
export default function createReducer(router) {
return combineReducers({
router,
channels,
environment,
input,
messages,
privateChats,
search,
servers,
settings,
tab,
ui
});
}

View File

@ -12,6 +12,7 @@ const Message = Record({
time: null,
type: null,
channel: false,
next: false,
height: 0,
length: 0,
breakpoints: null

View File

@ -1,5 +1,5 @@
import { Record, List } from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
import { LOCATION_CHANGED } from '../util/router';
import createReducer from '../util/createReducer';
import * as actions from '../actions';
@ -19,15 +19,17 @@ const State = Record({
history: List()
});
function selectTab(state, action) {
const tab = new Tab(action);
return state
.set('selected', tab)
.update('history', history => history.push(tab));
}
export const getSelectedTab = state => state.tab.selected;
export default createReducer(new State(), {
[actions.SELECT_TAB](state, action) {
const tab = new Tab(action);
return state
.set('selected', tab)
.update('history', history => history.push(tab));
},
[actions.SELECT_TAB]: selectTab,
[actions.PART](state, action) {
return state.set('history', state.history.filter(tab =>
@ -45,11 +47,12 @@ export default createReducer(new State(), {
return state.set('history', state.history.filter(tab => tab.server !== action.server));
},
[LOCATION_CHANGE](state, action) {
if (action.payload.pathname.indexOf('.') === -1 && state.selected.server) {
return state.set('selected', new Tab());
[LOCATION_CHANGED](state, action) {
const { route, params } = action;
if (route === 'chat') {
return selectTab(state, params);
}
return state;
return state.set('selected', new Tab());
}
});

View File

@ -1,20 +1,24 @@
import { Record } from 'immutable';
import createReducer from '../util/createReducer';
import * as actions from '../actions';
import { LOCATION_CHANGED } from '../util/router';
const State = Record({
showTabList: false,
showUserList: false
});
function hideMenu(state) {
return state.set('showTabList', false);
}
export default createReducer(new State(), {
[actions.TOGGLE_MENU](state) {
return state.update('showTabList', show => !show);
},
[actions.HIDE_MENU](state) {
return state.set('showTabList', false);
},
[actions.HIDE_MENU]: hideMenu,
[LOCATION_CHANGED]: hideMenu,
[actions.TOGGLE_USERLIST](state) {
return state.update('showUserList', show => !show);

View File

@ -1,19 +1,5 @@
import React from 'react';
import { Route, IndexRoute } from 'react-router';
import App from './containers/App';
import Connect from './containers/Connect';
import Chat from './containers/Chat';
import Settings from './containers/Settings';
export default function createRoutes() {
return (
<Route path="/" component={App}>
<Route path="connect" component={Connect} />
<Route path="settings" component={Settings} />
<Route path="/:server" component={Chat} />
<Route path="/:server/:channel" component={Chat} />
<Route path="/:server/pm/:user" component={Chat} />
<IndexRoute component={null} />
</Route>
);
}
export default {
connect: '/connect',
settings: '/settings',
chat: '/:server(/:name)'
};

View File

@ -1,6 +1,6 @@
import { routeActions } from 'react-router-redux';
import { broadcast, inform, addMessage, addMessages } from './actions/message';
import { select } from './actions/tab';
import { replace } from './util/router';
import { normalizeChannel } from './util';
function withReason(message, reason) {
@ -52,7 +52,7 @@ export default function handleSocket(socket, { dispatch, getState }) {
servers(data) {
if (!data) {
dispatch(routeActions.replace('/connect'));
dispatch(replace('/connect'));
}
},

View File

@ -1,20 +1,24 @@
import { createStore, applyMiddleware, compose } from 'redux';
import thunk from 'redux-thunk';
import { routerMiddleware } from 'react-router-redux';
import reducer from '../reducers';
import createReducer from '../reducers';
import { routeReducer, routeMiddleware } from '../util/router';
import createSocketMiddleware from '../middleware/socket';
import commands from '../commands';
export default function configureStore(socket, history) {
export default function configureStore(socket) {
// eslint-disable-next-line no-underscore-dangle
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
return createStore(reducer, composeEnhancers(
const reducer = createReducer(routeReducer);
const store = createStore(reducer, composeEnhancers(
applyMiddleware(
routerMiddleware(history),
routeMiddleware,
thunk,
createSocketMiddleware(socket),
commands
)
));
return store;
}

View File

@ -1,7 +1,53 @@
import { stringWidth, measureScrollBarWidth } from './index';
import { updateMessageHeight } from '../actions/message';
import { setCharWidth, setWrapWidth } from '../actions/environment';
const lineHeight = 24;
let prevWidth;
const menuWidth = 200;
const userListWidth = 200;
const messagePadding = 30;
const smallScreen = 600;
let windowWidth;
export function initWidthUpdates(store) {
const scrollBarWidth = measureScrollBarWidth();
const charWidth = stringWidth(' ', '16px Roboto Mono, monospace');
window.messageIndent = 6 * charWidth;
store.dispatch(setCharWidth(charWidth));
let prevWrapWidth;
function updateWidth(delta, first) {
windowWidth = window.innerWidth;
let wrapWidth = windowWidth - scrollBarWidth - messagePadding;
if (windowWidth > smallScreen) {
wrapWidth -= menuWidth;
}
if (wrapWidth !== prevWrapWidth) {
prevWrapWidth = wrapWidth;
store.dispatch(setWrapWidth(wrapWidth));
if (!first) {
store.dispatch(updateMessageHeight());
}
}
}
let resizeRAF;
function resize() {
if (resizeRAF) {
window.cancelAnimationFrame(resizeRAF);
}
resizeRAF = window.requestAnimationFrame(updateWidth);
}
updateWidth(0, true);
window.addEventListener('resize', resize);
}
export function findBreakpoints(text) {
const breakpoints = [];
@ -20,17 +66,10 @@ export function findBreakpoints(text) {
export function messageHeight(message, width, charWidth, indent = 0) {
let pad = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
let height = lineHeight + 4;
let height = lineHeight + 8;
if (message.channel) {
if (width !== prevWidth) {
prevWidth = width;
windowWidth = window.innerWidth;
}
if (windowWidth > 600) {
width -= 200;
}
if (message.channel && windowWidth > smallScreen) {
width -= userListWidth;
}
if (pad + (message.length * charWidth) < width) {

View File

@ -0,0 +1,104 @@
import createHistory from 'history/createBrowserHistory';
import UrlPattern from 'url-pattern';
const history = createHistory();
export const LOCATION_CHANGED = 'ROUTER_LOCATION_CHANGED';
export const PUSH = 'ROUTER_PUSH';
export const REPLACE = 'ROUTER_REPLACE';
export function locationChanged(route, params, location) {
return {
type: LOCATION_CHANGED,
route,
params,
location
};
}
export function push(path) {
return {
type: PUSH,
path
};
}
export function replace(path) {
return {
type: REPLACE,
path
};
}
export function routeReducer(state = {}, action) {
if (action.type === LOCATION_CHANGED) {
return {
route: action.route,
params: action.params,
location: action.location
};
}
return state;
}
export function routeMiddleware() {
return next => action => {
switch (action.type) {
case PUSH:
history.push(action.path);
break;
case REPLACE:
history.replace(action.path);
break;
default:
return next(action);
}
return undefined;
};
}
function match(routes, location) {
let params;
for (let i = 0; i < routes.length; i++) {
params = routes[i].pattern.match(location.pathname);
if (params !== null) {
return locationChanged(routes[i].name, params, location);
}
}
return null;
}
function decode(location) {
location.pathname = decodeURIComponent(location.pathname);
return location;
}
export default function initRouter(routes, store) {
const patterns = [];
const opts = {
segmentValueCharset: 'a-zA-Z0-9-_.~# %'
};
Object.keys(routes).forEach(name =>
patterns.push({
name,
pattern: new UrlPattern(routes[name], opts)
})
);
let matched = match(patterns, decode(history.location));
if (matched) {
store.dispatch(matched);
} else {
matched = { location: {} };
}
history.listen(location => {
const nextMatch = match(patterns, decode(location));
if (nextMatch && nextMatch.location.pathname !== matched.location.pathname) {
matched = nextMatch;
store.dispatch(matched);
}
});
}

View File

@ -2761,18 +2761,9 @@ hawk@~3.1.3:
hoek "2.x.x"
sntp "1.x.x"
history@^3.0.0:
version "3.3.0"
resolved "https://registry.yarnpkg.com/history/-/history-3.3.0.tgz#fcedcce8f12975371545d735461033579a6dae9c"
dependencies:
invariant "^2.2.1"
loose-envify "^1.2.0"
query-string "^4.2.2"
warning "^3.0.0"
history@^4.5.1:
version "4.6.1"
resolved "https://registry.yarnpkg.com/history/-/history-4.6.1.tgz#911cf8eb65728555a94f2b12780a0c531a14d2fd"
history@4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/history/-/history-4.5.0.tgz#7313388109333bf5796fff7407cee1850e8c5061"
dependencies:
invariant "^2.2.1"
loose-envify "^1.2.0"
@ -2792,7 +2783,7 @@ hoek@2.x.x:
version "2.16.3"
resolved "https://registry.yarnpkg.com/hoek/-/hoek-2.16.3.tgz#20bb7403d3cea398e91dc4710a8ff1b8274a25ed"
hoist-non-react-statics@^1.0.3, hoist-non-react-statics@^1.2.0:
hoist-non-react-statics@^1.0.3:
version "1.2.0"
resolved "https://registry.yarnpkg.com/hoist-non-react-statics/-/hoist-non-react-statics-1.2.0.tgz#aa448cf0986d55cc40773b17174b7dd066cb7cfb"
@ -4593,7 +4584,7 @@ promise@^7.1.1:
dependencies:
asap "~2.0.3"
prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.6, prop-types@^15.5.7, prop-types@~15.5.7:
prop-types@^15.0.0, prop-types@^15.5.4, prop-types@^15.5.7, prop-types@~15.5.7:
version "15.5.8"
resolved "https://registry.yarnpkg.com/prop-types/-/prop-types-15.5.8.tgz#6b7b2e141083be38c8595aa51fc55775c7199394"
dependencies:
@ -4636,7 +4627,7 @@ qs@6.4.0, qs@~6.4.0:
version "6.4.0"
resolved "https://registry.yarnpkg.com/qs/-/qs-6.4.0.tgz#13e26d28ad6b0ffaa91312cd3bf708ed351e7233"
query-string@^4.1.0, query-string@^4.2.2:
query-string@^4.1.0:
version "4.3.4"
resolved "https://registry.yarnpkg.com/query-string/-/query-string-4.3.4.tgz#bbb693b9ca915c232515b228b1a02b609043dbeb"
dependencies:
@ -4725,22 +4716,6 @@ react-redux@^5.0.2:
loose-envify "^1.1.0"
prop-types "^15.0.0"
react-router-redux@^4.0.8:
version "4.0.8"
resolved "https://registry.yarnpkg.com/react-router-redux/-/react-router-redux-4.0.8.tgz#227403596b5151e182377dab835b5d45f0f8054e"
react-router@^3.0.2:
version "3.0.5"
resolved "https://registry.yarnpkg.com/react-router/-/react-router-3.0.5.tgz#c3b7873758045a8bbc9562aef4ff4bc8cce7c136"
dependencies:
create-react-class "^15.5.1"
history "^3.0.0"
hoist-non-react-statics "^1.2.0"
invariant "^2.2.1"
loose-envify "^1.2.0"
prop-types "^15.5.6"
warning "^3.0.0"
react-virtualized@^9.3.0:
version "9.7.4"
resolved "https://registry.yarnpkg.com/react-virtualized/-/react-virtualized-9.7.4.tgz#1b5b25c1397282c6ffc651b416befc69e282df12"
@ -5561,6 +5536,10 @@ unpipe@1.0.0, unpipe@~1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/unpipe/-/unpipe-1.0.0.tgz#b2bf4ee8514aae6165b4817829d21b2ef49904ec"
url-pattern@^1.0.3:
version "1.0.3"
resolved "https://registry.yarnpkg.com/url-pattern/-/url-pattern-1.0.3.tgz#0409292471b24f23c50d65a47931793d2b5acfc1"
url@^0.11.0:
version "0.11.0"
resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"