Clean up container/component relationship

This commit is contained in:
Ken-Håvard Lieng 2017-05-27 07:30:22 +02:00
parent 8b0a53b375
commit 993d29242e
22 changed files with 384 additions and 372 deletions

File diff suppressed because one or more lines are too long

View File

@ -270,6 +270,10 @@ i[class^="icon-"]:before, i[class*=" icon-"]:before {
display: none;
}
.chat-server .userlist, .chat-private .userlist {
display: none;
}
.chat-server .userlist-bar, .chat-private .userlist-bar {
display: none;
}

View File

@ -0,0 +1,24 @@
import React 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';
const App = props => {
const { onClick, ...tabListProps } = props;
const mainClass = props.showTabList ? 'main-container off-canvas' : 'main-container';
return (
<div onClick={onClick}>
<TabList {...tabListProps} />
<div className={mainClass}>
<Route name="chat"><Chat /></Route>
<Route name="connect"><Connect /></Route>
<Route name="settings"><Settings /></Route>
</div>
</div>
);
};
export default App;

View File

@ -4,30 +4,16 @@ import Navicon from '../components/Navicon';
import { linkify } from '../util';
export default class ChatTitle extends PureComponent {
handleLeaveClick = () => {
const { tab, disconnect, part, closePrivateChat } = this.props;
if (tab.isChannel()) {
part([tab.name], tab.server);
} else if (tab.name) {
closePrivateChat(tab.server, tab.name);
} else {
disconnect(tab.server);
}
};
render() {
const { title, tab, channel, toggleSearch, toggleUserList } = this.props;
let topic = channel.get('topic');
topic = topic ? linkify(topic) : null;
const { title, tab, channel, onToggleSearch, onToggleUserList, onCloseClick } = this.props;
let leaveTitle;
let closeTitle;
if (tab.isChannel()) {
leaveTitle = 'Leave';
closeTitle = 'Leave';
} else if (tab.name) {
leaveTitle = 'Close';
closeTitle = 'Close';
} else {
leaveTitle = 'Disconnect';
closeTitle = 'Disconnect';
}
return (
@ -36,19 +22,19 @@ export default class ChatTitle extends PureComponent {
<Navicon />
<span className="chat-title">{title}</span>
<div className="chat-topic-wrap">
<span className="chat-topic">{topic}</span>
<span className="chat-topic">{linkify(channel.get('topic')) || null}</span>
</div>
<i className="icon-search" title="Search" onClick={toggleSearch} />
<i className="icon-search" title="Search" onClick={onToggleSearch} />
<i
className="icon-cancel button-leave"
title={leaveTitle}
onClick={this.handleLeaveClick}
title={closeTitle}
onClick={onCloseClick}
/>
<i className="icon-user button-userlist" onClick={toggleUserList} />
<i className="icon-user button-userlist" onClick={onToggleUserList} />
</div>
<div className="userlist-bar">
<i className="icon-user" />
<span className="chat-usercount">{channel.get('users', List()).size || null}</span>
<span className="chat-usercount">{channel.get('users', List()).size}</span>
</div>
</div>
);

View File

@ -10,7 +10,6 @@ export default class FileInput extends PureComponent {
const reader = new FileReader();
reader.onload = () => {
console.log(reader.result.byteLength);
this.props.onChange(file.name, reader.result);
};

View File

@ -1,7 +1,7 @@
import React, { PureComponent } from 'react';
export default class Message extends PureComponent {
handleNickClick = () => this.props.onNickClick(this.props.message);
handleNickClick = () => this.props.onNickClick(this.props.message.from);
render() {
const { message } = this.props;

View File

@ -6,14 +6,14 @@ export default class MessageInput extends PureComponent {
};
handleKey = e => {
const { tab, runCommand, sendMessage,
const { tab, onCommand, onMessage,
add, reset, increment, decrement, currentHistoryEntry } = this.props;
if (e.key === 'Enter' && e.target.value) {
if (e.target.value[0] === '/') {
runCommand(e.target.value, tab.name, tab.server);
onCommand(e.target.value, tab.name, tab.server);
} else if (tab.name) {
sendMessage(e.target.value, tab.name, tab.server);
onMessage(e.target.value, tab.name, tab.server);
}
add(e.target.value);

View File

@ -1,13 +1,7 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { toggleMenu } from '../state/ui';
import React from 'react';
class Navicon extends PureComponent {
render() {
return (
<i className="icon-menu navicon" onClick={this.props.toggleMenu} />
);
}
}
const Navicon = ({ toggleMenu }) => (
<i className="icon-menu navicon" onClick={toggleMenu} />
);
export default connect(null, { toggleMenu })(Navicon);
export default Navicon;

View File

@ -0,0 +1,11 @@
import React from 'react';
import { Provider } from 'react-redux';
import App from '../containers/App';
const Root = ({ store }) => (
<Provider store={store}>
<App />
</Provider>
);
export default Root;

View File

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

View File

@ -13,38 +13,31 @@ export default class UserList extends PureComponent {
listRef = el => { this.list = el; };
renderUser = ({ index, style, key }) => {
const { users, tab, openPrivateChat, select } = this.props;
const { users, onNickClick } = this.props;
return (
<UserListItem
key={key}
user={users.get(index)}
tab={tab}
openPrivateChat={openPrivateChat}
select={select}
style={style}
onClick={onNickClick}
/>
);
};
render() {
const { tab, showUserList } = this.props;
const { users, showUserList } = this.props;
const className = showUserList ? 'userlist off-canvas' : 'userlist';
const style = {};
if (!tab.isChannel()) {
style.display = 'none';
}
return (
<div className={className} style={style}>
<div className={className}>
<AutoSizer disableWidth>
{({ height }) => (
<List
ref={this.listRef}
width={200}
height={height - 20}
rowCount={this.props.users.size}
rowCount={users.size}
rowHeight={24}
rowRenderer={this.renderUser}
className="rvlist-users"

View File

@ -1,12 +1,7 @@
import React, { PureComponent } from 'react';
export default class UserListItem extends PureComponent {
handleClick = () => {
const { tab, user, openPrivateChat, select } = this.props;
openPrivateChat(tab.server, user.nick);
select(tab.server, user.nick, true);
};
handleClick = () => this.props.onClick(this.props.user.nick);
render() {
return (

View File

@ -0,0 +1,101 @@
import React, { Component } from 'react';
import ChatTitle from '../ChatTitle';
import Search from '../Search';
import MessageBox from '../MessageBox';
import MessageInput from '../MessageInput';
import UserList from '../UserList';
export default class Chat extends Component {
handleCloseClick = () => {
const { tab, part, closePrivateChat, disconnect } = this.props;
if (tab.isChannel()) {
part([tab.name], tab.server);
} else if (tab.name) {
closePrivateChat(tab.server, tab.name);
} else {
disconnect(tab.server);
}
};
handleSearch = phrase => {
const { tab, searchMessages } = this.props;
if (tab.isChannel()) {
searchMessages(tab.server, tab.name, phrase);
}
};
handleNickClick = nick => {
const { tab, openPrivateChat, select } = this.props;
openPrivateChat(tab.server, nick);
select(tab.server, nick);
};
render() {
const {
channel,
currentInputHistoryEntry,
hasMoreMessages,
messages,
nick,
search,
showUserList,
tab,
title,
users,
fetchMessages,
inputActions,
runCommand,
sendMessage,
toggleSearch,
toggleUserList
} = this.props;
let chatClass;
if (tab.isChannel()) {
chatClass = 'chat-channel';
} else if (tab.name) {
chatClass = 'chat-private';
} else {
chatClass = 'chat-server';
}
return (
<div className={chatClass}>
<ChatTitle
channel={channel}
tab={tab}
title={title}
onCloseClick={this.handleCloseClick}
onToggleSearch={toggleSearch}
onToggleUserList={toggleUserList}
/>
<Search
search={search}
onSearch={this.handleSearch}
/>
<MessageBox
hasMoreMessages={hasMoreMessages}
messages={messages}
tab={tab}
onFetchMore={fetchMessages}
onNickClick={this.handleNickClick}
/>
<MessageInput
currentHistoryEntry={currentInputHistoryEntry}
nick={nick}
tab={tab}
onCommand={runCommand}
onMessage={sendMessage}
{...inputActions}
/>
<UserList
showUserList={showUserList}
users={users}
onNickClick={this.handleNickClick}
/>
</div>
);
}
}

View File

@ -0,0 +1,100 @@
import React, { Component } from 'react';
import Navicon from '../../containers/Navicon';
export default class Connect extends Component {
state = {
showOptionals: false,
passwordTouched: false
};
handleSubmit = e => {
const { connect, select, join } = this.props;
e.preventDefault();
let address = e.target.address.value.trim();
const nick = e.target.nick.value.trim();
const channels = e.target.channels.value.split(',').map(s => s.trim()).filter(s => s);
const opts = {
name: e.target.name.value.trim(),
tls: e.target.ssl.checked
};
if (this.state.showOptionals) {
opts.realname = e.target.realname.value.trim();
opts.username = e.target.username.value.trim();
if (this.state.passwordTouched) {
opts.password = e.target.password.value.trim();
}
}
if (address.indexOf('.') > 0 && nick) {
connect(address, nick, opts);
const i = address.indexOf(':');
if (i > 0) {
address = address.slice(0, i);
}
select(address);
if (channels.length > 0) {
join(channels, address);
}
}
};
handleShowClick = () => {
this.setState({ showOptionals: !this.state.showOptionals });
};
handlePasswordChange = () => {
this.setState({ passwordTouched: true });
};
render() {
const { defaults } = this.props;
let optionals = null;
if (this.state.showOptionals) {
optionals = (
<div>
<input name="username" type="text" placeholder="Username" />
<input
name="password"
type="password"
placeholder="Password"
defaultValue={defaults.password ? ' ' : null}
onChange={this.handlePasswordChange}
/>
<input name="realname" type="text" placeholder="Realname" />
</div>
);
}
return (
<div className="connect">
<Navicon />
<form className="connect-form" onSubmit={this.handleSubmit}>
<h1>Connect</h1>
<input name="name" type="text" placeholder="Name" defaultValue={defaults.name} />
<input name="address" type="text" placeholder="Address" defaultValue={defaults.address} />
<input name="nick" type="text" placeholder="Nick" />
<input
name="channels"
type="text"
placeholder="Channels"
defaultValue={defaults.channels ? defaults.channels.join(',') : null}
/>
{optionals}
<p>
<label htmlFor="ssl"><input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL</label>
<i className="icon-ellipsis" onClick={this.handleShowClick} />
</p>
<input type="submit" value="Connect" />
</form>
</div>
);
}
}

View File

@ -0,0 +1,34 @@
import React from 'react';
import Navicon from '../../containers/Navicon';
import FileInput from '../FileInput';
const Settings = ({ settings, onCertChange, onKeyChange, uploadCert }) => {
const status = settings.get('uploadingCert') ? 'Uploading...' : 'Upload';
const error = settings.get('certError');
return (
<div className="settings">
<Navicon />
<h1>Settings</h1>
<h2>Client Certificate</h2>
<div>
<p>Certificate</p>
<FileInput
name={settings.get('certFile') || 'Select Certificate'}
onChange={onCertChange}
/>
</div>
<div>
<p>Private Key</p>
<FileInput
name={settings.get('keyFile') || 'Select Key'}
onChange={onKeyChange}
/>
</div>
<button onClick={uploadCert}>{status}</button>
{ error ? <p className="error">{error}</p> : null }
</div>
);
};
export default Settings;

View File

@ -1,41 +1,13 @@
import React, { PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
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 App from '../components/App';
import { getChannels } from '../state/channels';
import { getPrivateChats } from '../state/privateChats';
import { getServers } from '../state/servers';
import { getSelectedTab, select } from '../state/tab';
import { getShowTabList, hideMenu } from '../state/ui';
class App extends PureComponent {
handleClick = () => {
if (this.props.showTabList) {
this.props.hideMenu();
}
};
render() {
const { showTabList } = this.props;
const mainClass = showTabList ? 'main-container off-canvas' : 'main-container';
return (
<div onClick={this.handleClick}>
<TabList {...this.props} />
<div className={mainClass}>
<Route name="chat"><Chat /></Route>
<Route name="connect"><Connect /></Route>
<Route name="settings"><Settings /></Route>
</div>
</div>
);
}
}
import { push } from '../util/router';
const mapState = createStructuredSelector({
channels: getChannels,
@ -45,4 +17,13 @@ const mapState = createStructuredSelector({
tab: getSelectedTab
});
export default connect(mapState, { pushPath: push, select, hideMenu })(App);
const mapDispatch = (dispatch, props) => ({
onClick: () => {
if (props.showTabList) {
dispatch(hideMenu());
}
},
...bindActionCreators({ push, select }, dispatch)
});
export default connect(mapState, mapDispatch)(App);

View File

@ -1,12 +1,7 @@
import React, { PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import ChatTitle from '../components/ChatTitle';
import Search from '../components/Search';
import MessageBox from '../components/MessageBox';
import MessageInput from '../components/MessageInput';
import UserList from '../components/UserList';
import Chat from '../components/pages/Chat';
import { getSelectedTabTitle } from '../state';
import { getSelectedChannel, getSelectedChannelUsers, part } from '../state/channels';
import { getCurrentInputHistoryEntry, addInputHistory, resetInputHistory,
@ -19,80 +14,6 @@ import { getCurrentNick, disconnect } from '../state/servers';
import { getSelectedTab, select } from '../state/tab';
import { getShowUserList, toggleUserList } from '../state/ui';
class Chat extends PureComponent {
handleSearch = phrase => {
const { dispatch, tab } = this.props;
if (tab.isChannel()) {
dispatch(searchMessages(tab.server, tab.name, phrase));
}
};
handleMessageNickClick = message => {
const { tab } = this.props;
this.props.openPrivateChat(tab.server, message.from);
this.props.select(tab.server, message.from);
};
handleFetchMore = () => this.props.dispatch(fetchMessages());
render() {
const { title, tab, channel, search, currentInputHistoryEntry,
messages, hasMoreMessages, users, showUserList, nick, inputActions } = this.props;
let chatClass;
if (tab.isChannel()) {
chatClass = 'chat-channel';
} else if (tab.name) {
chatClass = 'chat-private';
} else {
chatClass = 'chat-server';
}
return (
<div className={chatClass}>
<ChatTitle
title={title}
tab={tab}
channel={channel}
toggleSearch={this.props.toggleSearch}
toggleUserList={this.props.toggleUserList}
disconnect={this.props.disconnect}
part={this.props.part}
closePrivateChat={this.props.closePrivateChat}
/>
<Search
search={search}
onSearch={this.handleSearch}
/>
<MessageBox
messages={messages}
hasMoreMessages={hasMoreMessages}
tab={tab}
onNickClick={this.handleMessageNickClick}
onFetchMore={this.handleFetchMore}
/>
<MessageInput
tab={tab}
channel={channel}
currentHistoryEntry={currentInputHistoryEntry}
nick={nick}
runCommand={this.props.runCommand}
sendMessage={this.props.sendMessage}
{...inputActions}
/>
<UserList
users={users}
tab={tab}
showUserList={showUserList}
select={this.props.select}
openPrivateChat={this.props.openPrivateChat}
/>
</div>
);
}
}
const mapState = createStructuredSelector({
channel: getSelectedChannel,
currentInputHistoryEntry: getCurrentInputHistoryEntry,
@ -106,28 +27,27 @@ const mapState = createStructuredSelector({
users: getSelectedChannelUsers
});
function mapDispatch(dispatch) {
return {
dispatch,
...bindActionCreators({
closePrivateChat,
disconnect,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
toggleSearch,
toggleUserList
}, dispatch),
inputActions: bindActionCreators({
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
}, dispatch)
};
}
const mapDispatch = dispatch => ({
...bindActionCreators({
closePrivateChat,
disconnect,
fetchMessages,
openPrivateChat,
part,
runCommand,
searchMessages,
select,
sendMessage,
toggleSearch,
toggleUserList
}, dispatch),
inputActions: bindActionCreators({
add: addInputHistory,
reset: resetInputHistory,
increment: incrementInputHistory,
decrement: decrementInputHistory
}, dispatch)
});
export default connect(mapState, mapDispatch)(Chat);

View File

@ -1,111 +1,19 @@
import React, { PureComponent } from 'react';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import Navicon from '../components/Navicon';
import Connect from '../components/pages/Connect';
import { join } from '../state/channels';
import { getConnectDefaults } from '../state/environment';
import { connect as connectServer } from '../state/servers';
import { select } from '../state/tab';
class Connect extends PureComponent {
state = {
showOptionals: false,
passwordTouched: false
};
handleSubmit = e => {
e.preventDefault();
const { dispatch } = this.props;
let address = e.target.address.value.trim();
const nick = e.target.nick.value.trim();
const channels = e.target.channels.value.split(',').map(s => s.trim()).filter(s => s);
const opts = {
name: e.target.name.value.trim(),
tls: e.target.ssl.checked
};
if (this.state.showOptionals) {
opts.realname = e.target.realname.value.trim();
opts.username = e.target.username.value.trim();
if (this.state.passwordTouched) {
opts.password = e.target.password.value.trim();
}
}
if (address.indexOf('.') > 0 && nick) {
dispatch(connectServer(address, nick, opts));
const i = address.indexOf(':');
if (i > 0) {
address = address.slice(0, i);
}
dispatch(select(address));
if (channels.length > 0) {
dispatch(join(channels, address));
}
}
};
handleShowClick = () => {
this.setState({ showOptionals: !this.state.showOptionals });
};
handlePasswordChange = () => {
this.setState({ passwordTouched: true });
};
render() {
const { defaults } = this.props;
let optionals = null;
if (this.state.showOptionals) {
optionals = (
<div>
<input name="username" type="text" placeholder="Username" />
<input
name="password"
type="password"
placeholder="Password"
defaultValue={defaults.password ? ' ' : null}
onChange={this.handlePasswordChange}
/>
<input name="realname" type="text" placeholder="Realname" />
</div>
);
}
return (
<div className="connect">
<Navicon />
<form className="connect-form" onSubmit={this.handleSubmit}>
<h1>Connect</h1>
<input name="name" type="text" placeholder="Name" defaultValue={defaults.name} />
<input name="address" type="text" placeholder="Address" defaultValue={defaults.address} />
<input name="nick" type="text" placeholder="Nick" />
<input
name="channels"
type="text"
placeholder="Channels"
defaultValue={defaults.channels ? defaults.channels.join(',') : null}
/>
{optionals}
<p>
<label htmlFor="ssl"><input name="ssl" type="checkbox" defaultChecked={defaults.ssl} />SSL</label>
<i className="icon-ellipsis" onClick={this.handleShowClick} />
</p>
<input type="submit" value="Connect" />
</form>
</div>
);
}
}
const mapState = createStructuredSelector({
defaults: getConnectDefaults
});
export default connect(mapState)(Connect);
const mapDispatch = {
join,
connect: connectServer,
select
};
export default connect(mapState, mapDispatch)(Connect);

View File

@ -0,0 +1,7 @@
import { connect } from 'react-redux';
import Navicon from '../components/Navicon';
import { toggleMenu } from '../state/ui';
const mapDispatch = { toggleMenu };
export default connect(null, mapDispatch)(Navicon);

View File

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

View File

@ -1,48 +1,17 @@
import React, { PureComponent } from 'react';
import { bindActionCreators } from 'redux';
import { connect } from 'react-redux';
import { createStructuredSelector } from 'reselect';
import Navicon from '../components/Navicon';
import FileInput from '../components/FileInput';
import Settings from '../components/pages/Settings';
import { getSettings, setCert, setKey, uploadCert } from '../state/settings';
class Settings extends PureComponent {
handleCertChange = (name, data) => this.props.dispatch(setCert(name, data));
handleKeyChange = (name, data) => this.props.dispatch(setKey(name, data));
handleCertUpload = () => this.props.dispatch(uploadCert());
render() {
const { settings } = this.props;
const status = settings.get('uploadingCert') ? 'Uploading...' : 'Upload';
const error = settings.get('certError');
return (
<div className="settings">
<Navicon />
<h1>Settings</h1>
<h2>Client Certificate</h2>
<div>
<p>Certificate</p>
<FileInput
name={settings.get('certFile') || 'Select Certificate'}
onChange={this.handleCertChange}
/>
</div>
<div>
<p>Private Key</p>
<FileInput
name={settings.get('keyFile') || 'Select Key'}
onChange={this.handleKeyChange}
/>
</div>
<button onClick={this.handleCertUpload}>{status}</button>
{ error ? <p className="error">{error}</p> : null }
</div>
);
}
}
const mapState = createStructuredSelector({
settings: getSettings
});
export default connect(mapState)(Settings);
const mapDispatch = dispatch => ({
onCertChange(name, data) { dispatch(setCert(name, data)); },
onKeyChange(name, data) { dispatch(setKey(name, data)); },
...bindActionCreators({ uploadCert }, dispatch)
});
export default connect(mapState, mapDispatch)(Settings);

View File

@ -7,7 +7,7 @@ import configureStore from './store';
import initRouter from './util/router';
import routes from './routes';
import Socket from './util/Socket';
import Root from './containers/Root';
import Root from './components/Root';
import runModules from './modules';
const host = DEV ? `${window.location.hostname}:1337` : window.location.host;
@ -27,5 +27,5 @@ const renderRoot = () => render(
renderRoot();
if (module.hot) {
module.hot.accept('./containers/Root', () => renderRoot());
module.hot.accept('./components/Root', () => renderRoot());
}