Switch to redux and webpack
This commit is contained in:
parent
b247287075
commit
e389454535
97 changed files with 2722 additions and 2656 deletions
|
@ -1,52 +0,0 @@
|
|||
import React from 'react';
|
||||
import Reflux from 'reflux';
|
||||
import { Router } from 'react-router';
|
||||
import TabList from './TabList.jsx';
|
||||
import routeActions from '../actions/route';
|
||||
import tabActions from '../actions/tab';
|
||||
import PureMixin from '../mixins/pure';
|
||||
|
||||
export default React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.listenTo(routeActions.navigate, 'navigate'),
|
||||
Reflux.listenTo(tabActions.hideMenu, 'hideMenu'),
|
||||
Reflux.listenTo(tabActions.toggleMenu, 'toggleMenu')
|
||||
],
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
menuToggled: false
|
||||
};
|
||||
},
|
||||
|
||||
navigate(path, replace) {
|
||||
const { history } = this.props;
|
||||
if (!replace) {
|
||||
history.pushState(null, path);
|
||||
} else {
|
||||
history.replaceState(null, path);
|
||||
}
|
||||
},
|
||||
|
||||
hideMenu() {
|
||||
this.setState({ menuToggled: false });
|
||||
},
|
||||
|
||||
toggleMenu() {
|
||||
this.setState({ menuToggled: !this.state.menuToggled });
|
||||
},
|
||||
|
||||
render() {
|
||||
const mainClass = this.state.menuToggled ? 'main-container off-canvas' : 'main-container';
|
||||
|
||||
return (
|
||||
<div>
|
||||
<TabList menuToggled={this.state.menuToggled} />
|
||||
<div className={mainClass}>
|
||||
{this.props.children}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
|
@ -1,53 +0,0 @@
|
|||
import React from 'react';
|
||||
import Reflux from 'reflux';
|
||||
import Router from 'react-router';
|
||||
import ChatTitle from './ChatTitle.jsx';
|
||||
import Search from './Search.jsx';
|
||||
import MessageBox from './MessageBox.jsx';
|
||||
import MessageInput from './MessageInput.jsx';
|
||||
import UserList from './UserList.jsx';
|
||||
import selectedTabStore from '../stores/selectedTab';
|
||||
import tabActions from '../actions/tab';
|
||||
import PureMixin from '../mixins/pure';
|
||||
|
||||
export default React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Router.State,
|
||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
||||
],
|
||||
|
||||
componentWillMount() {
|
||||
if (!window.loaded) {
|
||||
const { params } = this.props;
|
||||
if (params.channel) {
|
||||
tabActions.select(params.server, '#' + params.channel);
|
||||
} else if (params.server) {
|
||||
tabActions.select(params.server);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
let chatClass;
|
||||
const tab = this.state.selectedTab;
|
||||
|
||||
if (!tab.channel) {
|
||||
chatClass = 'chat-server';
|
||||
} else if (tab.channel[0] !== '#') {
|
||||
chatClass = 'chat-private';
|
||||
} else {
|
||||
chatClass = 'chat-channel';
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={chatClass}>
|
||||
<ChatTitle />
|
||||
<Search />
|
||||
<MessageBox />
|
||||
<MessageInput />
|
||||
<UserList />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
56
client/src/js/components/ChatTitle.js
Normal file
56
client/src/js/components/ChatTitle.js
Normal file
|
@ -0,0 +1,56 @@
|
|||
import React, { Component } from 'react';
|
||||
import { List } from 'immutable';
|
||||
import Autolinker from 'autolinker';
|
||||
import pure from 'pure-render-decorator';
|
||||
import Navicon from '../components/Navicon';
|
||||
|
||||
@pure
|
||||
export default class ChatTitle extends Component {
|
||||
handleLeaveClick = () => {
|
||||
const { tab, channel, disconnect, part, closePrivateChat } = this.props;
|
||||
|
||||
if (tab.channel) {
|
||||
part([channel.get('name')], tab.server);
|
||||
} else if (tab.user) {
|
||||
closePrivateChat(tab.server, tab.user);
|
||||
} else {
|
||||
disconnect(tab.server);
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { title, tab, channel, toggleSearch } = this.props;
|
||||
const topic = Autolinker.link(channel.get('topic') || '', { keepOriginalText: true });
|
||||
|
||||
let leaveTitle;
|
||||
if (tab.channel) {
|
||||
leaveTitle = 'Leave';
|
||||
} else if (tab.user) {
|
||||
leaveTitle = 'Close';
|
||||
} else {
|
||||
leaveTitle = 'Disconnect';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-title-bar">
|
||||
<Navicon />
|
||||
<span className="chat-title">{title}</span>
|
||||
<div className="chat-topic-wrap">
|
||||
<span className="chat-topic" dangerouslySetInnerHTML={{ __html: topic }}></span>
|
||||
</div>
|
||||
<i className="icon-search" title="Search" onClick={toggleSearch} />
|
||||
<i
|
||||
className="icon-logout button-leave"
|
||||
title={leaveTitle}
|
||||
onClick={this.handleLeaveClick}
|
||||
/>
|
||||
</div>
|
||||
<div className="userlist-bar">
|
||||
<i className="icon-user" />
|
||||
<span className="chat-usercount">{channel.get('users', List()).size || null}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,89 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
var Autolinker = require('autolinker');
|
||||
|
||||
var Navicon = require('./Navicon.jsx');
|
||||
var channelStore = require('../stores/channel');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var serverActions = require('../actions/server');
|
||||
var channelActions = require('../actions/channel');
|
||||
var searchActions = require('../actions/search');
|
||||
var privateChatActions = require('../actions/privateChat');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
function buildState(tab) {
|
||||
return {
|
||||
selectedTab: tab,
|
||||
usercount: channelStore.getUsers(tab.server, tab.channel).size,
|
||||
topic: channelStore.getTopic(tab.server, tab.channel)
|
||||
};
|
||||
}
|
||||
|
||||
var ChatTitle = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.listenTo(channelStore, 'channelsChanged'),
|
||||
Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
|
||||
],
|
||||
|
||||
getInitialState() {
|
||||
return buildState(selectedTabStore.getState());
|
||||
},
|
||||
|
||||
channelsChanged() {
|
||||
this.setState(buildState(this.state.selectedTab));
|
||||
},
|
||||
|
||||
selectedTabChanged(tab) {
|
||||
this.setState(buildState(tab));
|
||||
},
|
||||
|
||||
handleLeaveClick() {
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
if (!tab.channel) {
|
||||
serverActions.disconnect(tab.server);
|
||||
} else if (tab.channel[0] === '#') {
|
||||
channelActions.part([tab.channel], tab.server);
|
||||
} else {
|
||||
privateChatActions.close(tab.server, tab.channel);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
var tab = this.state.selectedTab;
|
||||
var topic = Autolinker.link(this.state.topic || '', { keepOriginalText: true });
|
||||
var leaveTitle;
|
||||
|
||||
if (!tab.channel) {
|
||||
leaveTitle = 'Disconnect';
|
||||
} else if (tab.channel[0] !== '#') {
|
||||
leaveTitle = 'Close';
|
||||
} else {
|
||||
leaveTitle = 'Leave';
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="chat-title-bar">
|
||||
<Navicon />
|
||||
<span className="chat-title">{tab.name}</span>
|
||||
<div className="chat-topic-wrap">
|
||||
<span className="chat-topic" dangerouslySetInnerHTML={{ __html: topic }}></span>
|
||||
</div>
|
||||
<i className="icon-search" title="Search" onClick={searchActions.toggle}></i>
|
||||
<i
|
||||
className="icon-logout button-leave"
|
||||
title={leaveTitle}
|
||||
onClick={this.handleLeaveClick}></i>
|
||||
</div>
|
||||
<div className="userlist-bar">
|
||||
<i className="icon-user"></i>
|
||||
<span className="chat-usercount">{this.state.usercount || null}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = ChatTitle;
|
|
@ -1,82 +0,0 @@
|
|||
var React = require('react');
|
||||
var _ = require('lodash');
|
||||
|
||||
var Navicon = require('./Navicon.jsx');
|
||||
var serverActions = require('../actions/server');
|
||||
var channelActions = require('../actions/channel');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var Connect = React.createClass({
|
||||
mixins: [PureMixin],
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
showOptionals: false
|
||||
};
|
||||
},
|
||||
|
||||
handleSubmit(e) {
|
||||
e.preventDefault();
|
||||
|
||||
var address = e.target.address.value.trim();
|
||||
var nick = e.target.nick.value.trim();
|
||||
var channels = _.filter(_.map(e.target.channels.value.split(','), _.trim));
|
||||
var 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();
|
||||
opts.password = e.target.password.value.trim();
|
||||
}
|
||||
|
||||
if (address.indexOf('.') > 0 && nick) {
|
||||
serverActions.connect(address, nick, opts);
|
||||
|
||||
if (channels.length > 0) {
|
||||
channelActions.join(channels, address);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
handleShowClick: function() {
|
||||
this.setState({ showOptionals: !this.state.showOptionals});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var optionals = null;
|
||||
|
||||
if (this.state.showOptionals) {
|
||||
optionals = (
|
||||
<div>
|
||||
<input name="username" type="text" placeholder="Username" />
|
||||
<input name="password" type="text" placeholder="Password" />
|
||||
<input name="realname" type="text" placeholder="Realname" />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="connect">
|
||||
<Navicon />
|
||||
<form ref="form" className="connect-form" onSubmit={this.handleSubmit}>
|
||||
<h1>Connect</h1>
|
||||
<input name="name" type="text" placeholder="Name" defaultValue="Freenode" />
|
||||
<input name="address" type="text" placeholder="Address" defaultValue="irc.freenode.net" />
|
||||
<input name="nick" type="text" placeholder="Nick" />
|
||||
<input name="channels" type="text" placeholder="Channels" />
|
||||
{optionals}
|
||||
<p>
|
||||
<label><input name="ssl" type="checkbox" />SSL</label>
|
||||
<i className="icon-ellipsis" onClick={this.handleShowClick}></i>
|
||||
</p>
|
||||
<input type="submit" value="Connect" />
|
||||
</form>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Connect;
|
90
client/src/js/components/MessageBox.js
Normal file
90
client/src/js/components/MessageBox.js
Normal file
|
@ -0,0 +1,90 @@
|
|||
import React, { Component } from 'react';
|
||||
import Infinite from 'react-infinite';
|
||||
import pure from 'pure-render-decorator';
|
||||
import MessageHeader from './MessageHeader';
|
||||
import MessageLine from './MessageLine';
|
||||
|
||||
@pure
|
||||
export default class MessageBox extends Component {
|
||||
state = {
|
||||
height: window.innerHeight - 100
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
this.updateWidth();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUpdate() {
|
||||
const el = this.refs.list.refs.scrollable;
|
||||
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
|
||||
}
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(this.updateWidth, 0);
|
||||
|
||||
if (this.autoScroll) {
|
||||
const el = this.refs.list.refs.scrollable;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
updateWidth = () => {
|
||||
const { setWrapWidth } = this.props;
|
||||
const { list } = this.refs;
|
||||
if (list) {
|
||||
const width = list.refs.scrollable.offsetWidth - 30;
|
||||
if (this.width !== width) {
|
||||
this.width = width;
|
||||
setWrapWidth(width);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
this.updateWidth();
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tab, messages, select, openPrivateChat } = this.props;
|
||||
const dest = tab.channel || tab.user || tab.server;
|
||||
const lines = [];
|
||||
|
||||
messages.forEach((message, j) => {
|
||||
const key = message.server + dest + j;
|
||||
lines.push(
|
||||
<MessageHeader
|
||||
key={key}
|
||||
message={message}
|
||||
select={select}
|
||||
openPrivateChat={openPrivateChat}
|
||||
/>
|
||||
);
|
||||
|
||||
for (let i = 1; i < message.lines.length; i++) {
|
||||
lines.push(
|
||||
<MessageLine key={key + '-' + i} type={message.type} line={message.lines[i]} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="messagebox">
|
||||
<Infinite
|
||||
ref="list"
|
||||
className="messagebox-scrollable"
|
||||
containerHeight={this.state.height}
|
||||
elementHeight={24}
|
||||
displayBottomUpwards={false}
|
||||
>
|
||||
{lines}
|
||||
</Infinite>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,93 +0,0 @@
|
|||
import React from 'react';
|
||||
import Reflux from 'reflux';
|
||||
import Infinite from 'react-infinite';
|
||||
import MessageHeader from './MessageHeader.jsx';
|
||||
import MessageLine from './MessageLine.jsx';
|
||||
import messageLineStore from '../stores/messageLine';
|
||||
import selectedTabStore from '../stores/selectedTab';
|
||||
import messageActions from '../actions/message';
|
||||
import PureMixin from '../mixins/pure';
|
||||
|
||||
export default React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.connect(messageLineStore, 'messages'),
|
||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
||||
],
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
height: window.innerHeight - 100
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
this.updateWidth();
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentWillUpdate() {
|
||||
var el = this.refs.list.refs.scrollable;
|
||||
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
|
||||
},
|
||||
|
||||
componentDidUpdate() {
|
||||
setTimeout(this.updateWidth, 0);
|
||||
|
||||
if (this.autoScroll) {
|
||||
var el = this.refs.list.refs.scrollable;
|
||||
el.scrollTop = el.scrollHeight;
|
||||
}
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
this.updateWidth();
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
},
|
||||
|
||||
updateWidth() {
|
||||
const { list } = this.refs;
|
||||
if (list) {
|
||||
const width = list.refs.scrollable.offsetWidth - 30;
|
||||
if (this.width !== width) {
|
||||
this.width = width;
|
||||
messageActions.setWrapWidth(width);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
const tab = this.state.selectedTab;
|
||||
const dest = tab.channel || tab.server;
|
||||
const lines = [];
|
||||
|
||||
this.state.messages.forEach((message, j) => {
|
||||
const key = message.server + dest + j;
|
||||
|
||||
lines.push(<MessageHeader key={key} message={message} />);
|
||||
|
||||
for (let i = 1; i < message.lines.length; i++) {
|
||||
lines.push(
|
||||
<MessageLine key={key + '-' + i} type={message.type} line={message.lines[i]} />
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="messagebox">
|
||||
<Infinite
|
||||
ref="list"
|
||||
className="messagebox-scrollable"
|
||||
containerHeight={this.state.height}
|
||||
elementHeight={24}
|
||||
displayBottomUpwards={false}>
|
||||
{lines}
|
||||
</Infinite>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
46
client/src/js/components/MessageHeader.js
Normal file
46
client/src/js/components/MessageHeader.js
Normal file
|
@ -0,0 +1,46 @@
|
|||
import React, { Component } from 'react';
|
||||
import Autolinker from 'autolinker';
|
||||
import { timestamp } from '../util';
|
||||
|
||||
export default class MessageHeader extends Component {
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.message.lines[0] !== this.props.message.lines[0];
|
||||
}
|
||||
|
||||
handleSenderClick = () => {
|
||||
const { message, openPrivateChat, select } = this.props;
|
||||
|
||||
openPrivateChat(message.server, message.from);
|
||||
select(message.server, message.from, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
const { message } = this.props;
|
||||
const line = Autolinker.link(message.lines[0], { stripPrefix: false });
|
||||
let sender = null;
|
||||
let messageClass = 'message';
|
||||
|
||||
if (message.from) {
|
||||
sender = (
|
||||
<span>
|
||||
{' '}
|
||||
<span className="message-sender" onClick={this.handleSenderClick}>
|
||||
{message.from}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type) {
|
||||
messageClass += ' message-' + message.type;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={messageClass}>
|
||||
<span className="message-time">{timestamp(message.time)}</span>
|
||||
{sender}
|
||||
<span dangerouslySetInnerHTML={{ __html: ' ' + line }}></span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,51 +0,0 @@
|
|||
var React = require('react');
|
||||
var Autolinker = require('autolinker');
|
||||
|
||||
var util = require('../util');
|
||||
var privateChatActions = require('../actions/privateChat');
|
||||
var tabActions = require('../actions/tab');
|
||||
|
||||
var MessageHeader = React.createClass({
|
||||
shouldComponentUpdate(nextProps) {
|
||||
return nextProps.message.lines[0] !== this.props.message.lines[0];
|
||||
},
|
||||
|
||||
handleSenderClick() {
|
||||
var message = this.props.message;
|
||||
|
||||
privateChatActions.open(message.server, message.from);
|
||||
tabActions.select(message.server, message.from);
|
||||
},
|
||||
|
||||
render() {
|
||||
var message = this.props.message;
|
||||
var sender = null;
|
||||
var messageClass = 'message';
|
||||
var line = Autolinker.link(message.lines[0], { keepOriginalText: true });
|
||||
|
||||
if (message.from) {
|
||||
sender = (
|
||||
<span>
|
||||
{' '}
|
||||
<span className="message-sender" onClick={this.handleSenderClick}>
|
||||
{message.from}
|
||||
</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
if (message.type) {
|
||||
messageClass += ' message-' + message.type;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={messageClass}>
|
||||
<span className="message-time">{util.timestamp(message.time)}</span>
|
||||
{sender}
|
||||
<span dangerouslySetInnerHTML={{ __html: ' ' + line }}></span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MessageHeader;
|
57
client/src/js/components/MessageInput.js
Normal file
57
client/src/js/components/MessageInput.js
Normal file
|
@ -0,0 +1,57 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
|
||||
@pure
|
||||
export default class MessageInput extends Component {
|
||||
state = {
|
||||
value: ''
|
||||
}
|
||||
|
||||
handleKey = e => {
|
||||
const { tab, runCommand, sendMessage, addInputHistory, incrementInputHistory,
|
||||
decrementInputHistory, resetInputHistory } = this.props;
|
||||
|
||||
if (e.which === 13 && e.target.value) {
|
||||
if (e.target.value[0] === '/') {
|
||||
runCommand(e.target.value, tab.channel || tab.user, tab.server);
|
||||
} else if (tab.channel) {
|
||||
sendMessage(e.target.value, tab.channel, tab.server);
|
||||
} else if (tab.user) {
|
||||
sendMessage(e.target.value, tab.user, tab.server);
|
||||
}
|
||||
|
||||
addInputHistory(e.target.value);
|
||||
resetInputHistory();
|
||||
this.setState({ value: '' });
|
||||
} else if (e.which === 38) {
|
||||
e.preventDefault();
|
||||
incrementInputHistory();
|
||||
} else if (e.which === 40) {
|
||||
decrementInputHistory();
|
||||
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
resetInputHistory();
|
||||
} else if (e.key === 'Unidentified') {
|
||||
this.setState({ value: e.target.value });
|
||||
resetInputHistory();
|
||||
}
|
||||
}
|
||||
|
||||
handleChange = e => {
|
||||
this.setState({ value: e.target.value });
|
||||
}
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="message-input-wrap">
|
||||
<input
|
||||
ref="input"
|
||||
className="message-input"
|
||||
type="text"
|
||||
value={this.props.history || this.state.value}
|
||||
onKeyDown={this.handleKey}
|
||||
onChange={this.handleChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,66 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var inputHistoryStore = require('../stores/inputHistory');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var messageActions = require('../actions/message');
|
||||
var inputHistoryActions = require('../actions/inputHistory');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var MessageInput = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.connect(inputHistoryStore, 'history')
|
||||
],
|
||||
|
||||
getInitialState() {
|
||||
return {
|
||||
value: ''
|
||||
};
|
||||
},
|
||||
|
||||
handleKey(e) {
|
||||
if (e.which === 13 && e.target.value) {
|
||||
var tab = selectedTabStore.getState();
|
||||
|
||||
if (e.target.value[0] === '/') {
|
||||
messageActions.command(e.target.value, tab.channel, tab.server);
|
||||
} else {
|
||||
messageActions.send(e.target.value, tab.channel, tab.server);
|
||||
}
|
||||
|
||||
inputHistoryActions.add(e.target.value);
|
||||
inputHistoryActions.reset();
|
||||
this.setState({ value: '' });
|
||||
} else if (e.which === 38) {
|
||||
e.preventDefault();
|
||||
inputHistoryActions.increment();
|
||||
} else if (e.which === 40) {
|
||||
inputHistoryActions.decrement();
|
||||
} else if (e.key === 'Backspace' || e.key === 'Delete') {
|
||||
inputHistoryActions.reset();
|
||||
} else if (e.key === 'Unidentified') {
|
||||
inputHistoryActions.reset();
|
||||
}
|
||||
},
|
||||
|
||||
handleChange(e) {
|
||||
this.setState({ value: e.target.value });
|
||||
},
|
||||
|
||||
render() {
|
||||
return (
|
||||
<div className="message-input-wrap">
|
||||
<input
|
||||
ref="input"
|
||||
className="message-input"
|
||||
type="text"
|
||||
value={this.state.history || this.state.value}
|
||||
onKeyDown={this.handleKey}
|
||||
onChange={this.handleChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MessageInput;
|
25
client/src/js/components/MessageLine.js
Normal file
25
client/src/js/components/MessageLine.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React, { Component } from 'react';
|
||||
import Autolinker from 'autolinker';
|
||||
import pure from 'pure-render-decorator';
|
||||
|
||||
@pure
|
||||
export default class MessageLine extends Component {
|
||||
render() {
|
||||
const line = Autolinker.link(this.props.line, { stripPrefix: false });
|
||||
|
||||
let messageClass = 'message';
|
||||
if (this.props.type) {
|
||||
messageClass += ' message-' + this.props.type;
|
||||
}
|
||||
|
||||
const style = {
|
||||
paddingLeft: window.messageIndent + 'px'
|
||||
};
|
||||
|
||||
return (
|
||||
<p className={messageClass} style={style}>
|
||||
<span dangerouslySetInnerHTML={{ __html: line }}></span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,28 +0,0 @@
|
|||
var React = require('react');
|
||||
var Autolinker = require('autolinker');
|
||||
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var MessageLine = React.createClass({
|
||||
mixins: [PureMixin],
|
||||
|
||||
render() {
|
||||
var line = Autolinker.link(this.props.line, { keepOriginalText: true });
|
||||
var messageClass = 'message';
|
||||
var style = {
|
||||
paddingLeft: window.messageIndent + 'px'
|
||||
};
|
||||
|
||||
if (this.props.type) {
|
||||
messageClass += ' message-' + this.props.type;
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={messageClass} style={style}>
|
||||
<span dangerouslySetInnerHTML={{ __html: line }}></span>
|
||||
</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = MessageLine;
|
16
client/src/js/components/Navicon.js
Normal file
16
client/src/js/components/Navicon.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import pure from 'pure-render-decorator';
|
||||
import { toggleMenu } from '../actions/tab';
|
||||
|
||||
@pure
|
||||
class Navicon extends Component {
|
||||
render() {
|
||||
const { dispatch } = this.props;
|
||||
return (
|
||||
<i className="icon-menu navicon" onClick={() => dispatch(toggleMenu())}></i>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default connect()(Navicon);
|
|
@ -1,13 +0,0 @@
|
|||
var React = require('react');
|
||||
|
||||
var tabActions = require('../actions/tab');
|
||||
|
||||
var Navicon = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<i className="icon-menu navicon" onClick={tabActions.toggleMenu}></i>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Navicon;
|
37
client/src/js/components/Search.js
Normal file
37
client/src/js/components/Search.js
Normal file
|
@ -0,0 +1,37 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
import { timestamp } from '../util';
|
||||
|
||||
@pure
|
||||
export default class Search extends Component {
|
||||
componentDidUpdate(prevProps) {
|
||||
if (!prevProps.search.show && this.props.search.show) {
|
||||
this.refs.input.focus();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { search, onSearch } = this.props;
|
||||
const results = search.results.map(result => {
|
||||
return (
|
||||
<p key={result.id}>{timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
|
||||
);
|
||||
});
|
||||
|
||||
const style = {
|
||||
display: search.show ? 'block' : 'none'
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="search" style={style}>
|
||||
<input
|
||||
ref="input"
|
||||
className="search-input"
|
||||
type="text"
|
||||
onChange={e => onSearch(e.target.value)}
|
||||
/>
|
||||
<div className="search-results">{results}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,55 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var util = require('../util');
|
||||
var searchStore = require('../stores/search');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var searchActions = require('../actions/search');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var Search = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.connect(searchStore, 'search'),
|
||||
Reflux.connect(selectedTabStore, 'selectedTab')
|
||||
],
|
||||
|
||||
componentDidUpdate(prevProps, prevState) {
|
||||
if (!prevState.search.show && this.state.search.show) {
|
||||
this.refs.input.getDOMNode().focus();
|
||||
}
|
||||
},
|
||||
|
||||
handleChange(e) {
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
if (tab.channel) {
|
||||
searchActions.search(tab.server, tab.channel, e.target.value);
|
||||
}
|
||||
},
|
||||
|
||||
render() {
|
||||
var style = {
|
||||
display: this.state.search.show ? 'block' : 'none'
|
||||
};
|
||||
|
||||
var results = this.state.search.results.map(result => {
|
||||
return (
|
||||
<p key={result.id}>{util.timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
|
||||
);
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="search" style={style}>
|
||||
<input
|
||||
ref="input"
|
||||
className="search-input"
|
||||
type="text"
|
||||
onChange={this.handleChange} />
|
||||
<div className="search-results">{results}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Search;
|
14
client/src/js/components/Settings.js
Normal file
14
client/src/js/components/Settings.js
Normal file
|
@ -0,0 +1,14 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
import Navicon from './Navicon';
|
||||
|
||||
@pure
|
||||
export default class Settings extends Component {
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Navicon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,15 +0,0 @@
|
|||
var React = require('react');
|
||||
|
||||
var Navicon = require('./Navicon.jsx');
|
||||
|
||||
var Settings = React.createClass({
|
||||
render() {
|
||||
return (
|
||||
<div>
|
||||
<Navicon />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = Settings;
|
69
client/src/js/components/TabList.js
Normal file
69
client/src/js/components/TabList.js
Normal file
|
@ -0,0 +1,69 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
import TabListItem from './TabListItem';
|
||||
|
||||
@pure
|
||||
export default class TabList extends Component {
|
||||
handleConnectClick = () => {
|
||||
this.props.pushPath('/connect');
|
||||
this.props.hideMenu();
|
||||
}
|
||||
|
||||
handleSettingsClick = () => {
|
||||
this.props.pushPath('/settings');
|
||||
this.props.hideMenu();
|
||||
}
|
||||
|
||||
render() {
|
||||
const { channels, servers, privateChats, showMenu, select, selected } = this.props;
|
||||
const className = showMenu ? 'tablist off-canvas' : 'tablist';
|
||||
const tabs = [];
|
||||
|
||||
channels.forEach((server, address) => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address}
|
||||
server
|
||||
content={servers.getIn([address, 'name'])}
|
||||
selected={selected.server === address && selected.channel === null && selected.user === null}
|
||||
onClick={() => select(address)}
|
||||
/>
|
||||
);
|
||||
|
||||
server.forEach((channel, name) => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + channel.get('name')}
|
||||
content={channel.get('name')}
|
||||
selected={selected.server === address && selected.channel === name}
|
||||
onClick={() => select(address, channel.get('name'))}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
if (privateChats.has(address)) {
|
||||
privateChats.get(address).forEach(nick => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + nick}
|
||||
content={nick}
|
||||
selected={selected.server === address && selected.user === nick}
|
||||
onClick={() => select(address, nick, true)}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
|
||||
<div className="tab-container">{tabs}</div>
|
||||
<div className="side-buttons">
|
||||
<i className="icon-user"></i>
|
||||
<i className="icon-cog" onClick={this.handleSettingsClick}></i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var TabListItem = require('./TabListItem.jsx');
|
||||
var channelStore = require('../stores/channel');
|
||||
var privateChatStore = require('../stores/privateChat');
|
||||
var serverStore = require('../stores/server');
|
||||
var routeActions = require('../actions/route');
|
||||
var tabActions = require('../actions/tab');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var TabList = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.connect(serverStore, 'servers'),
|
||||
Reflux.connect(channelStore, 'channels'),
|
||||
Reflux.connect(privateChatStore, 'privateChats')
|
||||
],
|
||||
|
||||
handleConnectClick() {
|
||||
routeActions.navigate('connect');
|
||||
tabActions.hideMenu();
|
||||
},
|
||||
|
||||
handleSettingsClick() {
|
||||
routeActions.navigate('settings');
|
||||
tabActions.hideMenu();
|
||||
},
|
||||
|
||||
render() {
|
||||
var className = this.props.menuToggled ? 'tablist off-canvas' : 'tablist';
|
||||
var tabs = [];
|
||||
|
||||
this.state.channels.forEach((server, address) => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address}
|
||||
server={address}
|
||||
channel={null}
|
||||
name={this.state.servers.getIn([address, 'name'])}>
|
||||
</TabListItem>
|
||||
);
|
||||
|
||||
server.forEach((channel, name) => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + name}
|
||||
server={address}
|
||||
channel={name}
|
||||
name={name}>
|
||||
</TabListItem>
|
||||
);
|
||||
});
|
||||
|
||||
if (this.state.privateChats.has(address)) {
|
||||
this.state.privateChats.get(address).forEach(nick => {
|
||||
tabs.push(
|
||||
<TabListItem
|
||||
key={address + nick}
|
||||
server={address}
|
||||
channel={nick}
|
||||
name={nick}>
|
||||
</TabListItem>
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
<button className="button-connect" onClick={this.handleConnectClick}>Connect</button>
|
||||
<div className="tab-container">{tabs}</div>
|
||||
<div className="side-buttons">
|
||||
<i className="icon-user"></i>
|
||||
<i className="icon-cog" onClick={this.handleSettingsClick}></i>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TabList;
|
21
client/src/js/components/TabListItem.js
Normal file
21
client/src/js/components/TabListItem.js
Normal file
|
@ -0,0 +1,21 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
|
||||
@pure
|
||||
export default class TabListItem extends Component {
|
||||
render() {
|
||||
const classes = [];
|
||||
|
||||
if (this.props.server) {
|
||||
classes.push('tab-server');
|
||||
}
|
||||
|
||||
if (this.props.selected) {
|
||||
classes.push('selected');
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={classes.join(' ')} onClick={this.props.onClick}>{this.props.content}</p>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,36 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var tabActions = require('../actions/tab');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var TabListItem = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.connect(selectedTabStore, 'tab')
|
||||
],
|
||||
|
||||
handleClick() {
|
||||
tabActions.select(this.props.server, this.props.channel);
|
||||
},
|
||||
|
||||
render() {
|
||||
var classes = [];
|
||||
|
||||
if (!this.props.channel) {
|
||||
classes.push('tab-server');
|
||||
}
|
||||
|
||||
if (this.props.server === this.state.tab.server &&
|
||||
this.props.channel === this.state.tab.channel) {
|
||||
classes.push('selected');
|
||||
}
|
||||
|
||||
return (
|
||||
<p className={classes.join(' ')} onClick={this.handleClick}>{this.props.name}</p>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = TabListItem;
|
53
client/src/js/components/UserList.js
Normal file
53
client/src/js/components/UserList.js
Normal file
|
@ -0,0 +1,53 @@
|
|||
import React, { Component } from 'react';
|
||||
import Infinite from 'react-infinite';
|
||||
import pure from 'pure-render-decorator';
|
||||
import UserListItem from './UserListItem';
|
||||
|
||||
@pure
|
||||
export default class UserList extends Component {
|
||||
state = {
|
||||
height: window.innerHeight - 100
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
}
|
||||
|
||||
handleResize = () => {
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
}
|
||||
|
||||
render() {
|
||||
const { tab, openPrivateChat, select } = this.props;
|
||||
const users = [];
|
||||
const style = {};
|
||||
|
||||
if (!tab.channel) {
|
||||
style.display = 'none';
|
||||
} else {
|
||||
this.props.users.forEach(user => {
|
||||
users.push(
|
||||
<UserListItem
|
||||
key={user.nick}
|
||||
user={user}
|
||||
tab={tab}
|
||||
openPrivateChat={openPrivateChat}
|
||||
select={select}
|
||||
/>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="userlist" style={style}>
|
||||
<Infinite containerHeight={this.state.height} elementHeight={24}>
|
||||
{users}
|
||||
</Infinite>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -1,83 +0,0 @@
|
|||
var React = require('react');
|
||||
var Reflux = require('reflux');
|
||||
var Infinite = require('react-infinite');
|
||||
|
||||
var UserListItem = require('./UserListItem.jsx');
|
||||
var channelStore = require('../stores/channel');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var UserList = React.createClass({
|
||||
mixins: [
|
||||
PureMixin,
|
||||
Reflux.listenTo(channelStore, 'channelsChanged'),
|
||||
Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
|
||||
],
|
||||
|
||||
getInitialState() {
|
||||
var tab = selectedTabStore.getState();
|
||||
|
||||
return {
|
||||
users: channelStore.getUsers(tab.server, tab.channel),
|
||||
selectedTab: tab,
|
||||
height: window.innerHeight - 100
|
||||
};
|
||||
},
|
||||
|
||||
componentDidMount() {
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
componentWillUnmount() {
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
|
||||
channelsChanged() {
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
this.setState({ users: channelStore.getUsers(tab.server, tab.channel) });
|
||||
},
|
||||
|
||||
selectedTabChanged(tab) {
|
||||
this.setState({
|
||||
selectedTab: tab,
|
||||
users: channelStore.getUsers(tab.server, tab.channel)
|
||||
});
|
||||
},
|
||||
|
||||
handleResize() {
|
||||
this.setState({ height: window.innerHeight - 100 });
|
||||
},
|
||||
|
||||
render() {
|
||||
var tab = this.state.selectedTab;
|
||||
var users = [];
|
||||
var style = {};
|
||||
|
||||
if (!tab.channel || tab.channel[0] !== '#') {
|
||||
style.display = 'none';
|
||||
} else {
|
||||
this.state.users.forEach(user => {
|
||||
users.push(<UserListItem key={user.nick} user={user} />);
|
||||
});
|
||||
}
|
||||
|
||||
if (users.length > 1) {
|
||||
return (
|
||||
<div className="userlist" style={style}>
|
||||
<Infinite containerHeight={this.state.height} elementHeight={24}>
|
||||
{users}
|
||||
</Infinite>
|
||||
</div>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="userlist" style={style}>
|
||||
<div>{users}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserList;
|
16
client/src/js/components/UserListItem.js
Normal file
16
client/src/js/components/UserListItem.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import React, { Component } from 'react';
|
||||
import pure from 'pure-render-decorator';
|
||||
|
||||
@pure
|
||||
export default class UserListItem extends Component {
|
||||
handleClick = () => {
|
||||
const { tab, user, openPrivateChat, select } = this.props;
|
||||
|
||||
openPrivateChat(tab.server, user.nick);
|
||||
select(tab.server, user.nick, true);
|
||||
}
|
||||
|
||||
render() {
|
||||
return <p onClick={this.handleClick}>{this.props.user.renderName}</p>;
|
||||
}
|
||||
}
|
|
@ -1,23 +0,0 @@
|
|||
var React = require('react');
|
||||
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
var privateChatActions = require('../actions/privateChat');
|
||||
var tabActions = require('../actions/tab');
|
||||
var PureMixin = require('../mixins/pure');
|
||||
|
||||
var UserListItem = React.createClass({
|
||||
mixins: [PureMixin],
|
||||
|
||||
handleClick() {
|
||||
var server = selectedTabStore.getServer();
|
||||
|
||||
privateChatActions.open(server, this.props.user.nick);
|
||||
tabActions.select(server, this.props.user.nick);
|
||||
},
|
||||
|
||||
render() {
|
||||
return <p onClick={this.handleClick}>{this.props.user.renderName}</p>;
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = UserListItem;
|
Loading…
Add table
Add a link
Reference in a new issue