Move wrapWidth handling out of MessageBox, improve scroll position handling, use custom routing, close menu when clicking anywhere
This commit is contained in:
parent
a753efd1dd
commit
fec7c93abc
File diff suppressed because one or more lines are too long
|
@ -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",
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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' }
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -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}`);
|
||||
}
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -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);
|
|
@ -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());
|
||||
}
|
||||
|
|
|
@ -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
|
||||
});
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@ const Message = Record({
|
|||
time: null,
|
||||
type: null,
|
||||
channel: false,
|
||||
next: false,
|
||||
height: 0,
|
||||
length: 0,
|
||||
breakpoints: null
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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)'
|
||||
};
|
||||
|
|
|
@ -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'));
|
||||
}
|
||||
},
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue