Immutable messages, search and selectedTab

This commit is contained in:
khlieng 2015-05-19 01:17:23 +02:00
parent 77c723344c
commit 11ea241b60
26 changed files with 332 additions and 260 deletions

View File

@ -27,6 +27,8 @@
"react-router": "0.13.3", "react-router": "0.13.3",
"react": "0.13.3", "react": "0.13.3",
"react-infinite": "0.3.4", "react-infinite": "0.3.4",
"autolinker": "khlieng/Autolinker.js" "autolinker": "khlieng/Autolinker.js",
"immutable": "~3.7.2",
"react-pure-render": "~1.0.1"
} }
} }

View File

@ -13,7 +13,7 @@ var App = React.createClass({
Reflux.listenTo(routeActions.navigate, 'navigate') Reflux.listenTo(routeActions.navigate, 'navigate')
], ],
navigate: function(path, replace) { navigate(path, replace) {
if (!replace) { if (!replace) {
this.transitionTo(path); this.transitionTo(path);
} else { } else {
@ -21,7 +21,7 @@ var App = React.createClass({
} }
}, },
render: function() { render() {
return ( return (
<div> <div>
<TabList /> <TabList />

View File

@ -9,20 +9,22 @@ var MessageInput = require('./MessageInput.jsx');
var UserList = require('./UserList.jsx'); var UserList = require('./UserList.jsx');
var selectedTabStore = require('../stores/selectedTab'); var selectedTabStore = require('../stores/selectedTab');
var tabActions = require('../actions/tab'); var tabActions = require('../actions/tab');
var PureMixin = require('../mixins/pure');
var Chat = React.createClass({ var Chat = React.createClass({
mixins: [ mixins: [
PureMixin,
Router.State, Router.State,
Reflux.connect(selectedTabStore, 'selectedTab') Reflux.connect(selectedTabStore, 'selectedTab')
], ],
getInitialState: function() { getInitialState() {
return { return {
selectedTab: selectedTabStore.getState() selectedTab: selectedTabStore.getState()
}; };
}, },
componentWillMount: function() { componentWillMount() {
if (!window.loaded) { if (!window.loaded) {
var p = this.getParams(); var p = this.getParams();
@ -34,7 +36,7 @@ var Chat = React.createClass({
} }
}, },
render: function() { render() {
var chatClass; var chatClass;
var tab = this.state.selectedTab; var tab = this.state.selectedTab;

View File

@ -14,7 +14,7 @@ var ChatTitle = React.createClass({
Reflux.listenTo(selectedTabStore, 'selectedTabChanged') Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
], ],
getInitialState: function() { getInitialState() {
var tab = selectedTabStore.getState(); var tab = selectedTabStore.getState();
return { return {
@ -23,20 +23,20 @@ var ChatTitle = React.createClass({
}; };
}, },
channelsChanged: function() { channelsChanged() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
this.setState({ usercount: channelStore.getUsers(tab.server, tab.channel).length }); this.setState({ usercount: channelStore.getUsers(tab.server, tab.channel).length });
}, },
selectedTabChanged: function(tab) { selectedTabChanged(tab) {
this.setState({ this.setState({
selectedTab: tab, selectedTab: tab,
usercount: channelStore.getUsers(tab.server, tab.channel).length usercount: channelStore.getUsers(tab.server, tab.channel).length
}); });
}, },
handleLeaveClick: function() { handleLeaveClick() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
if (!tab.channel) { if (!tab.channel) {
@ -48,7 +48,7 @@ var ChatTitle = React.createClass({
} }
}, },
render: function() { render() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
var leaveTitle; var leaveTitle;

View File

@ -5,13 +5,13 @@ var serverActions = require('../actions/server');
var channelActions = require('../actions/channel'); var channelActions = require('../actions/channel');
var Connect = React.createClass({ var Connect = React.createClass({
getInitialState: function() { getInitialState() {
return { return {
showOptionals: false showOptionals: false
}; };
}, },
handleSubmit: function(e) { handleSubmit(e) {
e.preventDefault(); e.preventDefault();
var address = e.target.address.value.trim(); var address = e.target.address.value.trim();

View File

@ -5,17 +5,20 @@ var Infinite = require('react-infinite');
var Autolinker = require('autolinker'); var Autolinker = require('autolinker');
var MessageHeader = require('./MessageHeader.jsx'); var MessageHeader = require('./MessageHeader.jsx');
var MessageLine = require('./MessageLine.jsx');
var messageLineStore = require('../stores/messageLine'); var messageLineStore = require('../stores/messageLine');
var selectedTabStore = require('../stores/selectedTab'); var selectedTabStore = require('../stores/selectedTab');
var messageActions = require('../actions/message'); var messageActions = require('../actions/message');
var PureMixin = require('../mixins/pure');
var MessageBox = React.createClass({ var MessageBox = React.createClass({
mixins: [ mixins: [
PureMixin,
Reflux.connect(messageLineStore, 'messages'), Reflux.connect(messageLineStore, 'messages'),
Reflux.connect(selectedTabStore, 'selectedTab') Reflux.connect(selectedTabStore, 'selectedTab')
], ],
getInitialState: function() { getInitialState() {
return { return {
messages: messageLineStore.getState(), messages: messageLineStore.getState(),
selectedTab: selectedTabStore.getState(), selectedTab: selectedTabStore.getState(),
@ -23,20 +26,20 @@ var MessageBox = React.createClass({
}; };
}, },
componentDidMount: function() { componentDidMount() {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
}, },
componentWillUnmount: function() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
componentWillUpdate: function() { componentWillUpdate() {
var el = this.refs.list.getDOMNode(); var el = this.refs.list.getDOMNode();
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight; this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
}, },
componentDidUpdate: function() { componentDidUpdate() {
this.updateWidth(); this.updateWidth();
if (this.autoScroll) { if (this.autoScroll) {
@ -45,12 +48,12 @@ var MessageBox = React.createClass({
} }
}, },
handleResize: function() { handleResize() {
this.updateWidth(); this.updateWidth();
this.setState({ height: window.innerHeight - 100 }); this.setState({ height: window.innerHeight - 100 });
}, },
updateWidth: function() { updateWidth() {
var width = this.refs.list.getDOMNode().firstChild.offsetWidth; var width = this.refs.list.getDOMNode().firstChild.offsetWidth;
if (this.width !== width) { if (this.width !== width) {
@ -59,7 +62,7 @@ var MessageBox = React.createClass({
} }
}, },
render: function() { render() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
var dest = tab.channel || tab.server; var dest = tab.channel || tab.server;
var lines = []; var lines = [];
@ -67,27 +70,17 @@ var MessageBox = React.createClass({
paddingLeft: this.props.indent + 'px' paddingLeft: this.props.indent + 'px'
}; };
for (var j = 0; j < this.state.messages.length; j++) { this.state.messages.forEach((message, j) => {
var message = this.state.messages[j]; var key = message.server + dest + j;
var messageClass = 'message';
var key = message.server + dest + j;
if (message.type) {
messageClass += ' message-' + message.type;
}
lines.push(<MessageHeader key={key} message={message} />); lines.push(<MessageHeader key={key} message={message} />);
for (var i = 1; i < message.lines.length; i++) { for (var i = 1; i < message.lines.length; i++) {
var line = Autolinker.link(message.lines[i], { keepOriginalText: true });
lines.push( lines.push(
<p key={key + '-' + i} className={messageClass} style={innerStyle}> <MessageLine key={key + '-' + i} type={message.type} line={message.lines[i]} />
<span dangerouslySetInnerHTML={{ __html: line }}></span>
</p>
); );
} }
} });
if (lines.length !== 1) { if (lines.length !== 1) {
return ( return (

View File

@ -7,14 +7,18 @@ var privateChatActions = require('../actions/privateChat');
var tabActions = require('../actions/tab'); var tabActions = require('../actions/tab');
var MessageHeader = React.createClass({ var MessageHeader = React.createClass({
handleSenderClick: function() { shouldComponentUpdate(nextProps) {
return nextProps.message.lines[0] !== this.props.message.lines[0];
},
handleSenderClick() {
var message = this.props.message; var message = this.props.message;
privateChatActions.open(message.server, message.from); privateChatActions.open(message.server, message.from);
tabActions.select(message.server, message.from); tabActions.select(message.server, message.from);
}, },
render: function() { render() {
var message = this.props.message; var message = this.props.message;
var sender = null; var sender = null;
var messageClass = 'message'; var messageClass = 'message';

View File

@ -6,33 +6,33 @@ var selectedTabStore = require('../stores/selectedTab');
var messageActions = require('../actions/message'); var messageActions = require('../actions/message');
var inputHistoryActions = require('../actions/inputHistory'); var inputHistoryActions = require('../actions/inputHistory');
var tabActions = require('../actions/tab'); var tabActions = require('../actions/tab');
var PureMixin = require('../mixins/pure');
var MessageInput = React.createClass({ var MessageInput = React.createClass({
mixins: [ mixins: [
Reflux.connect(selectedTabStore, 'selectedTab'), PureMixin,
Reflux.connect(inputHistoryStore, 'history'), Reflux.connect(inputHistoryStore, 'history'),
Reflux.listenTo(tabActions.select, 'tabSelected') Reflux.listenTo(tabActions.select, 'tabSelected')
], ],
getInitialState: function() { getInitialState() {
return { return {
selectedTab: selectedTabStore.getState(),
history: inputHistoryStore.getState(), history: inputHistoryStore.getState(),
value: '' value: ''
}; };
}, },
componentDidMount: function() { componentDidMount() {
this.refs.input.getDOMNode().focus(); this.refs.input.getDOMNode().focus();
}, },
tabSelected: function() { tabSelected() {
this.refs.input.getDOMNode().focus(); this.refs.input.getDOMNode().focus();
}, },
handleKey: function(e) { handleKey(e) {
if (e.which === 13 && e.target.value) { if (e.which === 13 && e.target.value) {
var tab = this.state.selectedTab; var tab = selectedTabStore.getState();
if (e.target.value[0] === '/') { if (e.target.value[0] === '/') {
messageActions.command(e.target.value, tab.channel, tab.server); messageActions.command(e.target.value, tab.channel, tab.server);
@ -55,11 +55,11 @@ var MessageInput = React.createClass({
} }
}, },
handleChange: function(e) { handleChange(e) {
this.setState({ value: e.target.value }); this.setState({ value: e.target.value });
}, },
render: function() { render() {
return ( return (
<div className="message-input-wrap"> <div className="message-input-wrap">
<input <input

View File

@ -0,0 +1,28 @@
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

@ -6,27 +6,29 @@ var util = require('../util');
var searchStore = require('../stores/search'); var searchStore = require('../stores/search');
var selectedTabStore = require('../stores/selectedTab'); var selectedTabStore = require('../stores/selectedTab');
var searchActions = require('../actions/search'); var searchActions = require('../actions/search');
var PureMixin = require('../mixins/pure');
var Search = React.createClass({ var Search = React.createClass({
mixins: [ mixins: [
Reflux.connect(searchStore), PureMixin,
Reflux.connect(searchStore, 'search'),
Reflux.connect(selectedTabStore, 'selectedTab') Reflux.connect(selectedTabStore, 'selectedTab')
], ],
getInitialState: function() { getInitialState() {
var state = _.extend({}, searchStore.getState()); return {
state.selectedTab = selectedTabStore.getState(); search: searchStore.getState(),
selectedTab: selectedTabStore.getState()
return state; };
}, },
componentDidUpdate: function(prevProps, prevState) { componentDidUpdate(prevProps, prevState) {
if (!prevState.show && this.state.show) { if (!prevState.search.get('show') && this.state.search.get('show')) {
this.refs.input.getDOMNode().focus(); this.refs.input.getDOMNode().focus();
} }
}, },
handleChange: function(e) { handleChange(e) {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
if (tab.channel) { if (tab.channel) {
@ -34,12 +36,12 @@ var Search = React.createClass({
} }
}, },
render: function() { render() {
var style = { var style = {
display: this.state.show ? 'block' : 'none' display: this.state.search.get('show') ? 'block' : 'none'
}; };
var results = _.map(this.state.results, (result) => { var results = this.state.search.get('results').map(result => {
return ( return (
<p key={result.id}>{util.timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p> <p key={result.id}>{util.timestamp(new Date(result.time * 1000))} {result.from} {result.content}</p>
); );

View File

@ -1,7 +1,7 @@
var React = require('react'); var React = require('react');
var Settings = React.createClass({ var Settings = React.createClass({
render: function() { render() {
return ( return (
<div> <div>
<h1>Settings</h1> <h1>Settings</h1>

View File

@ -15,7 +15,7 @@ var TabList = React.createClass({
Reflux.connect(privateChatStore, 'privateChats') Reflux.connect(privateChatStore, 'privateChats')
], ],
getInitialState: function() { getInitialState() {
return { return {
servers: serverStore.getState(), servers: serverStore.getState(),
channels: channelStore.getState(), channels: channelStore.getState(),
@ -23,15 +23,15 @@ var TabList = React.createClass({
}; };
}, },
handleConnectClick: function() { handleConnectClick() {
routeActions.navigate('connect'); routeActions.navigate('connect');
}, },
handleSettingsClick: function() { handleSettingsClick() {
routeActions.navigate('settings'); routeActions.navigate('settings');
}, },
render: function() { render() {
var tabs = _.map(this.state.channels, (server, address) => { var tabs = _.map(this.state.channels, (server, address) => {
var serverTabs = _.map(server, (channel, name) => { var serverTabs = _.map(server, (channel, name) => {
return ( return (

View File

@ -3,27 +3,33 @@ var Reflux = require('reflux');
var selectedTabStore = require('../stores/selectedTab'); var selectedTabStore = require('../stores/selectedTab');
var tabActions = require('../actions/tab'); var tabActions = require('../actions/tab');
var PureMixin = require('../mixins/pure');
var TabListItem = React.createClass({ var TabListItem = React.createClass({
mixins: [Reflux.connect(selectedTabStore)], mixins: [
PureMixin,
Reflux.connect(selectedTabStore, 'tab')
],
getInitialState: function() { getInitialState() {
return selectedTabStore.getState(); return {
tab: selectedTabStore.getState()
};
}, },
handleClick: function() { handleClick() {
tabActions.select(this.props.server, this.props.channel); tabActions.select(this.props.server, this.props.channel);
}, },
render: function() { render() {
var classes = []; var classes = [];
if (!this.props.channel) { if (!this.props.channel) {
classes.push('tab-server'); classes.push('tab-server');
} }
if (this.props.server === this.state.server && if (this.props.server === this.state.tab.server &&
this.props.channel === this.state.channel) { this.props.channel === this.state.tab.channel) {
classes.push('selected'); classes.push('selected');
} }

View File

@ -13,7 +13,7 @@ var UserList = React.createClass({
Reflux.listenTo(selectedTabStore, 'selectedTabChanged') Reflux.listenTo(selectedTabStore, 'selectedTabChanged')
], ],
getInitialState: function() { getInitialState() {
var tab = selectedTabStore.getState(); var tab = selectedTabStore.getState();
return { return {
@ -23,32 +23,32 @@ var UserList = React.createClass({
}; };
}, },
componentDidMount: function() { componentDidMount() {
window.addEventListener('resize', this.handleResize); window.addEventListener('resize', this.handleResize);
}, },
componentWillUnmount: function() { componentWillUnmount() {
window.removeEventListener('resize', this.handleResize); window.removeEventListener('resize', this.handleResize);
}, },
channelsChanged: function() { channelsChanged() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
this.setState({ users: channelStore.getUsers(tab.server, tab.channel) }); this.setState({ users: channelStore.getUsers(tab.server, tab.channel) });
}, },
selectedTabChanged: function(tab) { selectedTabChanged(tab) {
this.setState({ this.setState({
selectedTab: tab, selectedTab: tab,
users: channelStore.getUsers(tab.server, tab.channel) users: channelStore.getUsers(tab.server, tab.channel)
}); });
}, },
handleResize: function() { handleResize() {
this.setState({ height: window.innerHeight - 100 }); this.setState({ height: window.innerHeight - 100 });
}, },
render: function() { render() {
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
var users = []; var users = [];
var style = {}; var style = {};

View File

@ -5,14 +5,14 @@ var privateChatActions = require('../actions/privateChat');
var tabActions = require('../actions/tab'); var tabActions = require('../actions/tab');
var UserListItem = React.createClass({ var UserListItem = React.createClass({
handleClick: function() { handleClick() {
var server = selectedTabStore.getServer(); var server = selectedTabStore.getServer();
privateChatActions.open(server, this.props.user.nick); privateChatActions.open(server, this.props.user.nick);
tabActions.select(server, this.props.user.nick); tabActions.select(server, this.props.user.nick);
}, },
render: function() { render() {
return <p onClick={this.handleClick}>{this.props.user.renderName}</p>; return <p onClick={this.handleClick}>{this.props.user.renderName}</p>;
} }
}); });

View File

@ -0,0 +1,17 @@
var shallowEqual = require('react-pure-render/shallowEqual');
module.exports = {
shouldComponentUpdate(nextProps, nextState) {
if (this.context.router) {
var changed = this.pureComponentLastPath !== this.context.router.getCurrentPath();
this.pureComponentLastPath = this.context.router.getCurrentPath();
if (changed) {
return true;
}
}
return !shallowEqual(this.props, nextProps) ||
!shallowEqual(this.state, nextState);
}
};

View File

@ -79,42 +79,42 @@ function sortUsers(server, channel) {
} }
var channelStore = Reflux.createStore({ var channelStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
this.listenTo(serverActions.connect, 'addServer'); this.listenTo(serverActions.connect, 'addServer');
this.listenTo(serverActions.disconnect, 'removeServer'); this.listenTo(serverActions.disconnect, 'removeServer');
this.listenTo(serverActions.load, 'loadServers'); this.listenTo(serverActions.load, 'loadServers');
}, },
part: function(partChannels, server) { part(partChannels, server) {
_.each(partChannels, function(channel) { _.each(partChannels, function(channel) {
delete channels[server][channel]; delete channels[server][channel];
}); });
this.trigger(channels); this.trigger(channels);
}, },
addUser: function(user, server, channel) { addUser(user, server, channel) {
initChannel(server, channel); initChannel(server, channel);
channels[server][channel].users.push(createUser(user)); channels[server][channel].users.push(createUser(user));
sortUsers(server, channel); sortUsers(server, channel);
this.trigger(channels); this.trigger(channels);
}, },
removeUser: function(user, server, channel) { removeUser(user, server, channel) {
if (channels[server][channel]) { if (channels[server][channel]) {
_.remove(channels[server][channel].users, { nick: user }); _.remove(channels[server][channel].users, { nick: user });
this.trigger(channels); this.trigger(channels);
} }
}, },
removeUserAll: function(user, server) { removeUserAll(user, server) {
_.each(channels[server], function(channel) { _.each(channels[server], function(channel) {
_.remove(channel.users, { nick: user }); _.remove(channel.users, { nick: user });
}); });
this.trigger(channels); this.trigger(channels);
}, },
renameUser: function(oldNick, newNick, server) { renameUser(oldNick, newNick, server) {
_.each(channels[server], function(channel, channelName) { _.each(channels[server], function(channel, channelName) {
var user = _.find(channel.users, { nick: oldNick }); var user = _.find(channel.users, { nick: oldNick });
if (user) { if (user) {
@ -126,7 +126,7 @@ var channelStore = Reflux.createStore({
this.trigger(channels); this.trigger(channels);
}, },
setUsers: function(users, server, channel) { setUsers(users, server, channel) {
initChannel(server, channel); initChannel(server, channel);
var chan = channels[server][channel]; var chan = channels[server][channel];
@ -140,12 +140,12 @@ var channelStore = Reflux.createStore({
this.trigger(channels); this.trigger(channels);
}, },
setTopic: function(topic, server, channel) { setTopic(topic, server, channel) {
channels[server][channel].topic = topic; channels[server][channel].topic = topic;
this.trigger(channels); this.trigger(channels);
}, },
setMode: function(mode) { setMode(mode) {
var user = _.find(channels[mode.server][mode.channel].users, { nick: mode.user }); var user = _.find(channels[mode.server][mode.channel].users, { nick: mode.user });
if (user) { if (user) {
_.each(mode.remove, function(mode) { _.each(mode.remove, function(mode) {
@ -161,7 +161,7 @@ var channelStore = Reflux.createStore({
} }
}, },
load: function(storedChannels) { load(storedChannels) {
_.each(storedChannels, function(channel) { _.each(storedChannels, function(channel) {
initChannel(channel.server, channel.name); initChannel(channel.server, channel.name);
var chan = channels[channel.server][channel.name]; var chan = channels[channel.server][channel.name];
@ -179,19 +179,19 @@ var channelStore = Reflux.createStore({
this.trigger(channels); this.trigger(channels);
}, },
addServer: function(server) { addServer(server) {
if (!(server in channels)) { if (!(server in channels)) {
channels[server] = {}; channels[server] = {};
this.trigger(channels); this.trigger(channels);
} }
}, },
removeServer: function(server) { removeServer(server) {
delete channels[server]; delete channels[server];
this.trigger(channels); this.trigger(channels);
}, },
loadServers: function(storedServers) { loadServers(storedServers) {
_.each(storedServers, function(server) { _.each(storedServers, function(server) {
if (!(server.address in channels)) { if (!(server.address in channels)) {
channels[server.address] = {}; channels[server.address] = {};
@ -200,25 +200,25 @@ var channelStore = Reflux.createStore({
this.trigger(channels); this.trigger(channels);
}, },
getChannels: function(server) { getChannels(server) {
return channels[server]; return channels[server];
}, },
getUsers: function(server, channel) { getUsers(server, channel) {
if (channels[server] && channels[server][channel]) { if (channels[server] && channels[server][channel]) {
return channels[server][channel].users; return channels[server][channel].users;
} }
return []; return [];
}, },
getTopic: function(server, channel) { getTopic(server, channel) {
if (channels[server] && channels[server][channel]) { if (channels[server] && channels[server][channel]) {
return channels[server][channel].topic || null; return channels[server][channel].topic || null;
} }
return null; return null;
}, },
getState: function() { getState() {
return channels; return channels;
} }
}); });

View File

@ -14,11 +14,11 @@ if (stored) {
} }
var inputHistoryStore = Reflux.createStore({ var inputHistoryStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
}, },
add: function(line) { add(line) {
if (line.trim() && line !== history[0]) { if (line.trim() && line !== history[0]) {
history.unshift(line); history.unshift(line);
@ -30,28 +30,28 @@ var inputHistoryStore = Reflux.createStore({
} }
}, },
reset: function() { reset() {
if (index !== -1) { if (index !== -1) {
index = -1; index = -1;
this.trigger(history[index]); this.trigger(history[index]);
} }
}, },
increment: function() { increment() {
if (index !== history.length - 1) { if (index !== history.length - 1) {
index++; index++;
this.trigger(history[index]); this.trigger(history[index]);
} }
}, },
decrement: function() { decrement() {
if (index !== -1) { if (index !== -1) {
index--; index--;
this.trigger(history[index]); this.trigger(history[index]);
} }
}, },
getState: function() { getState() {
if (index !== -1) { if (index !== -1) {
return history[index]; return history[index];
} }

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var Immutable = require('immutable');
var _ = require('lodash'); var _ = require('lodash');
var serverStore = require('./server'); var serverStore = require('./server');
@ -7,7 +8,18 @@ var actions = require('../actions/message');
var serverActions = require('../actions/server'); var serverActions = require('../actions/server');
var channelActions = require('../actions/channel'); var channelActions = require('../actions/channel');
var messages = {}; var messages = Immutable.Map();
var empty = Immutable.List();
var Message = Immutable.Record({
server: null,
from: null,
to: null,
message: '',
time: null,
type: null,
lines: []
});
function addMessage(message, dest) { function addMessage(message, dest) {
message.time = new Date(); message.time = new Date();
@ -19,24 +31,17 @@ function addMessage(message, dest) {
message.message = from + message.message.slice(7); message.message = from + message.message.slice(7);
} }
if (!(message.server in messages)) { messages = messages.updateIn([message.server, dest], empty, list => list.push(new Message(message)));
messages[message.server] = {};
messages[message.server][dest] = [message];
} else if (!(dest in messages[message.server])) {
messages[message.server][dest] = [message];
} else {
messages[message.server][dest].push(message);
}
} }
var messageStore = Reflux.createStore({ var messageStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
this.listenTo(serverActions.disconnect, 'disconnect'); this.listenTo(serverActions.disconnect, 'disconnect');
this.listenTo(channelActions.part, 'part'); this.listenTo(channelActions.part, 'part');
}, },
send: function(message, to, server) { send(message, to, server) {
addMessage({ addMessage({
server: server, server: server,
from: serverStore.getNick(server), from: serverStore.getNick(server),
@ -47,7 +52,7 @@ var messageStore = Reflux.createStore({
this.trigger(messages); this.trigger(messages);
}, },
add: function(message) { add(message) {
var dest = message.to || message.from; var dest = message.to || message.from;
if (message.from && message.from.indexOf('.') !== -1) { if (message.from && message.from.indexOf('.') !== -1) {
dest = message.server; dest = message.server;
@ -57,7 +62,7 @@ var messageStore = Reflux.createStore({
this.trigger(messages); this.trigger(messages);
}, },
broadcast: function(message, server, user) { broadcast(message, server, user) {
_.each(channelStore.getChannels(server), function(channel, channelName) { _.each(channelStore.getChannels(server), function(channel, channelName) {
if (!user || (user && _.find(channel.users, { nick: user }))) { if (!user || (user && _.find(channel.users, { nick: user }))) {
addMessage({ addMessage({
@ -71,7 +76,7 @@ var messageStore = Reflux.createStore({
this.trigger(messages); this.trigger(messages);
}, },
inform: function(message, server, channel) { inform(message, server, channel) {
if (_.isArray(message)) { if (_.isArray(message)) {
_.each(message, (msg) => { _.each(message, (msg) => {
addMessage({ addMessage({
@ -93,26 +98,23 @@ var messageStore = Reflux.createStore({
this.trigger(messages); this.trigger(messages);
}, },
disconnect: function(server) { disconnect(server) {
delete messages[server]; messages = messages.delete(server);
this.trigger(messages); this.trigger(messages);
}, },
part: function(channels, server) { part(channels, server) {
_.each(channels, function(channel) { _.each(channels, function(channel) {
delete messages[server][channel]; messages = messages.deleteIn([server, channel]);
}); });
this.trigger(messages); this.trigger(messages);
}, },
getMessages: function(server, dest) { getMessages(server, dest) {
if (messages[server] && messages[server][dest]) { return messages.getIn([server, dest]) || empty;
return messages[server][dest];
}
return [];
}, },
getState: function() { getState() {
return messages; return messages;
} }
}); });

View File

@ -10,43 +10,53 @@ var width = window.innerWidth;
window.charWidth = util.stringWidth(' ', '16px Droid Sans Mono'); window.charWidth = util.stringWidth(' ', '16px Droid Sans Mono');
window.messageIndent = 6 * charWidth; window.messageIndent = 6 * charWidth;
// Temporary hack incase this runs before the font has loaded
setTimeout(() => window.charWidth = util.stringWidth(' ', '16px Droid Sans Mono'), 1000);
var tab = selectedTabStore.getState(); var tab = selectedTabStore.getState();
var messages; var messages;
var prev;
function wrap() { function wrap() {
messages = messageStore.getMessages(tab.server, tab.channel || tab.server); var next = messageStore.getMessages(tab.server, tab.channel || tab.server);
util.wrapMessages(messages, width, charWidth, messageIndent); if (next !== prev) {
prev = next;
messages = util.wrapMessages(next, width, charWidth, messageIndent);
return true;
}
return false;
} }
wrap(); wrap();
var messageLineStore = Reflux.createStore({ var messageLineStore = Reflux.createStore({
init: function() { init() {
this.listenTo(messageActions.setWrapWidth, 'setWrapWidth'); this.listenTo(messageActions.setWrapWidth, 'setWrapWidth');
this.listenTo(messageStore, 'messagesChanged'); this.listenTo(messageStore, 'messagesChanged');
this.listenTo(selectedTabStore, 'selectedTabChanged'); this.listenTo(selectedTabStore, 'selectedTabChanged');
}, },
setWrapWidth: function(w) { setWrapWidth(w) {
width = w; width = w;
messages = util.wrapMessages(messages, width, charWidth, messageIndent);
util.wrapMessages(messages, width, charWidth, messageIndent);
this.trigger(messages); this.trigger(messages);
}, },
messagesChanged: function() { messagesChanged() {
wrap(); if (wrap()) {
this.trigger(messages); this.trigger(messages);
}
}, },
selectedTabChanged: function(selectedTab) { selectedTabChanged(selectedTab) {
tab = selectedTab; tab = selectedTab;
wrap(); if (wrap()) {
this.trigger(messages); this.trigger(messages);
}
}, },
getState: function() { getState() {
return messages; return messages;
} }
}); });

View File

@ -21,35 +21,35 @@ function initChat(server, nick) {
} }
var privateChatStore = Reflux.createStore({ var privateChatStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
this.listenTo(messageActions.add, 'messageAdded'); this.listenTo(messageActions.add, 'messageAdded');
this.listenTo(serverActions.disconnect, 'disconnect'); this.listenTo(serverActions.disconnect, 'disconnect');
}, },
open: function(server, nick) { open(server, nick) {
if (initChat(server, nick)) { if (initChat(server, nick)) {
this.trigger(privateChats); this.trigger(privateChats);
} }
}, },
close: function(server, nick) { close(server, nick) {
delete privateChats[server][nick]; delete privateChats[server][nick];
this.trigger(privateChats); this.trigger(privateChats);
}, },
messageAdded: function(message) { messageAdded(message) {
if (!message.to && message.from.indexOf('.') === -1) { if (!message.to && message.from.indexOf('.') === -1) {
this.open(message.server, message.from); this.open(message.server, message.from);
} }
}, },
disconnect: function(server) { disconnect(server) {
delete privateChats[server]; delete privateChats[server];
this.trigger(privateChats); this.trigger(privateChats);
}, },
getState: function() { getState() {
return privateChats; return privateChats;
} }
}); });

View File

@ -1,28 +1,29 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var Immutable = require('immutable');
var actions = require('../actions/search'); var actions = require('../actions/search');
var state = { var state = Immutable.Map({
show: false, show: false,
results: [] results: Immutable.List()
}; });
var searchStore = Reflux.createStore({ var searchStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
}, },
searchDone: function(results) { searchDone(results) {
state.results = results; state = state.set('results', Immutable.List(results));
this.trigger(state); this.trigger(state);
}, },
toggle: function() { toggle() {
state.show = !state.show; state = state.update('show', show => !show);
this.trigger(state); this.trigger(state);
}, },
getState: function() { getState() {
return state; return state;
} }
}); });

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var Immutable = require('immutable');
var _ = require('lodash'); var _ = require('lodash');
var serverStore = require('./server'); var serverStore = require('./server');
@ -8,14 +9,20 @@ var serverActions = require('../actions/server');
var routeActions = require('../actions/route'); var routeActions = require('../actions/route');
var privateChatActions = require('../actions/privateChat'); var privateChatActions = require('../actions/privateChat');
var selectedTab = {}; var Tab = Immutable.Record({
server: null,
channel: null,
name: null
});
var selectedTab = new Tab();
var history = []; var history = [];
function selectPrevTab() { function selectPrevTab() {
history.pop(); history.pop();
if (history.length > 0) { if (history.length > 0) {
selectedTab = _.extend({}, history[history.length - 1]); selectedTab = history[history.length - 1];
return true; return true;
} }
@ -23,14 +30,12 @@ function selectPrevTab() {
} }
function updateChannelName(name) { function updateChannelName(name) {
selectedTab.channel = name; selectedTab = selectedTab.set('channel', name).set('name', name);
selectedTab.name = name; history[history.length - 1] = selectedTab;
history[history.length - 1].channel = name;
history[history.length - 1].name = name;
} }
var selectedTabStore = Reflux.createStore({ var selectedTabStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
this.listenTo(channelActions.part, 'part'); this.listenTo(channelActions.part, 'part');
this.listenTo(privateChatActions.close, 'close'); this.listenTo(privateChatActions.close, 'close');
@ -41,58 +46,57 @@ var selectedTabStore = Reflux.createStore({
this.listenTo(routeActions.navigate, 'navigate'); this.listenTo(routeActions.navigate, 'navigate');
}, },
select: function(server, channel = null) { select(server, channel = null) {
selectedTab.server = server; selectedTab = new Tab({
selectedTab.channel = channel; server,
channel,
name: channel || serverStore.getName(server)
});
if (channel) { history.push(selectedTab);
selectedTab.name = channel;
} else {
selectedTab.name = serverStore.getName(server);
}
history.push(_.extend({}, selectedTab));
this.trigger(selectedTab); this.trigger(selectedTab);
}, },
part: function(channels, server) { part(channels, server) {
if (server === selectedTab.server && if (server === selectedTab.server &&
channels.indexOf(selectedTab.channel) !== -1) { channels.indexOf(selectedTab.channel) !== -1) {
if (!selectPrevTab()) { if (!selectPrevTab()) {
selectedTab.channel = null; selectedTab = selectedTab
selectedTab.name = serverStore.getName(server); .set('channel', null)
.set('name', serverStore.getName(server));
} }
this.trigger(selectedTab); this.trigger(selectedTab);
} }
}, },
close: function(server, nick) { close(server, nick) {
if (server === selectedTab.server && if (server === selectedTab.server &&
nick === selectedTab.channel) { nick === selectedTab.channel) {
if (!selectPrevTab()) { if (!selectPrevTab()) {
selectedTab.channel = null; selectedTab = selectedTab
selectedTab.name = serverStore.getName(server); .set('channel', null)
.set('name', serverStore.getName(server));
} }
this.trigger(selectedTab); this.trigger(selectedTab);
} }
}, },
disconnect: function(server) { disconnect(server) {
if (server === selectedTab.server) { if (server === selectedTab.server) {
_.remove(history, { server: server }); _.remove(history, { server: server });
if (!selectPrevTab()) { if (!selectPrevTab()) {
selectedTab = {}; selectedTab = new Tab();
} }
this.trigger(selectedTab); this.trigger(selectedTab);
} }
}, },
userAdded: function(user, server, channel) { userAdded(user, server, channel) {
if (selectedTab.channel && if (selectedTab.channel &&
server === selectedTab.server && server === selectedTab.server &&
user === serverStore.getNick(server) && user === serverStore.getNick(server) &&
@ -103,7 +107,7 @@ var selectedTabStore = Reflux.createStore({
} }
}, },
loadChannels: function(channels) { loadChannels(channels) {
_.each(channels, (channel) => { _.each(channels, (channel) => {
if (channel.server === selectedTab.server && if (channel.server === selectedTab.server &&
channel.name !== selectedTab.channel && channel.name !== selectedTab.channel &&
@ -118,39 +122,38 @@ var selectedTabStore = Reflux.createStore({
}); });
}, },
loadServers: function(servers) { loadServers(servers) {
var server = _.find(servers, { address: selectedTab.server }); var server = _.find(servers, { address: selectedTab.server });
if (server && !selectedTab.channel) { if (server && !selectedTab.channel) {
selectedTab.name = server.name; selectedTab = selectedTab.set('name', server.name);
history[history.length - 1].name = server.name; history[history.length - 1] = selectedTab;
this.trigger(selectedTab); this.trigger(selectedTab);
} }
}, },
navigate: function(route) { navigate(route) {
if (route.indexOf('.') === -1 && selectedTab.server) { if (route.indexOf('.') === -1 && selectedTab.server) {
selectedTab.server = null; selectedTab = new Tab();
selectedTab.channel = null;
this.trigger(selectedTab); this.trigger(selectedTab);
} }
}, },
getServer: function() { getServer() {
return selectedTab.server; return selectedTab.server;
}, },
getChannel: function() { getChannel() {
return selectedTab.channel; return selectedTab.channel;
}, },
getState: function() { getState() {
return selectedTab; return selectedTab;
} }
}); });
selectedTabStore.listen(function(selectedTab) { selectedTabStore.listen(selectedTab => {
var channel = selectedTab.channel; var channel = selectedTab.channel;
if (selectedTab.server) { if (selectedTab.server) {

View File

@ -7,11 +7,11 @@ var tabActions = require('../actions/tab');
var servers = {}; var servers = {};
var serverStore = Reflux.createStore({ var serverStore = Reflux.createStore({
init: function() { init() {
this.listenToMany(actions); this.listenToMany(actions);
}, },
connect: function(server, nick, opts) { connect(server, nick, opts) {
var i = server.indexOf(':'); var i = server.indexOf(':');
if (i > 0) { if (i > 0) {
server = server.slice(0, i); server = server.slice(0, i);
@ -27,38 +27,38 @@ var serverStore = Reflux.createStore({
tabActions.select(server); tabActions.select(server);
}, },
disconnect: function(server) { disconnect(server) {
delete servers[server]; delete servers[server];
this.trigger(servers); this.trigger(servers);
}, },
setNick: function(nick, server) { setNick(nick, server) {
servers[server].nick = nick; servers[server].nick = nick;
this.trigger(servers); this.trigger(servers);
}, },
load: function(storedServers) { load(storedServers) {
_.each(storedServers, function(server) { _.each(storedServers, function(server) {
servers[server.address] = server; servers[server.address] = server;
}); });
this.trigger(servers); this.trigger(servers);
}, },
getNick: function(server) { getNick(server) {
if (servers[server]) { if (servers[server]) {
return servers[server].nick; return servers[server].nick;
} }
return null; return null;
}, },
getName: function(server) { getName(server) {
if (servers[server]) { if (servers[server]) {
return servers[server].name; return servers[server].name;
} }
return null; return null;
}, },
getState: function() { getState() {
return servers; return servers;
} }
}); });

View File

@ -17,67 +17,69 @@ exports.timestamp = function(date) {
}; };
exports.wrapMessages = function(messages, width, charWidth, indent = 0) { exports.wrapMessages = function(messages, width, charWidth, indent = 0) {
for (var j = 0, llen = messages.length; j < llen; j++) { return messages.withMutations(m => {
var message = messages[j]; for (var j = 0, llen = messages.size; j < llen; j++) {
var lineWidth = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth; var message = messages.get(j);
var lineWidth = (6 + (message.from ? message.from.length + 1 : 0)) * charWidth;
if (lineWidth + message.message.length * charWidth < width) { if (lineWidth + message.message.length * charWidth < width) {
message.lines = [message.message]; m.setIn([j, 'lines'], [message.message]);
continue; continue;
}
var words = message.message.split(' ');
var line = '';
var wrapped = [];
var wordCount = 0;
var hasWrapped = false;
// Add empty line if first word after timestamp + sender wraps
if (words.length > 0 && message.from && lineWidth + words[0].length * charWidth >= width) {
wrapped.push(line);
lineWidth = 0;
}
for (var i = 0, wlen = words.length; i < wlen; i++) {
var word = words[i];
if (hasWrapped) {
hasWrapped = false;
lineWidth += indent;
} }
lineWidth += word.length * charWidth; var words = message.message.split(' ');
wordCount++; var line = '';
var wrapped = [];
var wordCount = 0;
var hasWrapped = false;
if (lineWidth >= width) { // Add empty line if first word after timestamp + sender wraps
if (wordCount !== 1) { if (words.length > 0 && message.from && lineWidth + words[0].length * charWidth >= width) {
wrapped.push(line); wrapped.push(line);
lineWidth = 0;
}
if (i !== wlen - 1) { for (var i = 0, wlen = words.length; i < wlen; i++) {
line = word + ' '; var word = words[i];
lineWidth = (word.length + 1) * charWidth;
wordCount = 1; if (hasWrapped) {
} else { hasWrapped = false;
wrapped.push(word); lineWidth += indent;
}
} else {
wrapped.push(word);
lineWidth = 0;
wordCount = 0;
} }
hasWrapped = true; lineWidth += word.length * charWidth;
} else if (i !== wlen - 1) { wordCount++;
line += word + ' ';
lineWidth += charWidth;
} else {
line += word;
wrapped.push(line);
}
}
message.lines = wrapped; if (lineWidth >= width) {
} if (wordCount !== 1) {
wrapped.push(line);
if (i !== wlen - 1) {
line = word + ' ';
lineWidth = (word.length + 1) * charWidth;
wordCount = 1;
} else {
wrapped.push(word);
}
} else {
wrapped.push(word);
lineWidth = 0;
wordCount = 0;
}
hasWrapped = true;
} else if (i !== wlen - 1) {
line += word + ' ';
lineWidth += charWidth;
} else {
line += word;
wrapped.push(line);
}
}
m.setIn([j, 'lines'], wrapped);
}
});
}; };
var canvas = document.createElement('canvas'); var canvas = document.createElement('canvas');

File diff suppressed because one or more lines are too long