Switch to redux and webpack

This commit is contained in:
Ken-Håvard Lieng 2015-12-29 00:34:32 +01:00
parent b247287075
commit e389454535
97 changed files with 2722 additions and 2656 deletions

View file

@ -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>
);
}
});

View file

@ -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>
);
}
});

View 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>
);
}
}

View file

@ -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;

View file

@ -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;

View 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>
);
}
}

View file

@ -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>
);
}
});

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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);

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>
);
}
}

View file

@ -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;

View 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>;
}
}

View file

@ -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;