This commit is contained in:
khlieng 2015-01-17 02:37:21 +01:00
commit 508a04cf4c
30 changed files with 1545 additions and 0 deletions

11
client/src/index.html Normal file
View file

@ -0,0 +1,11 @@
<!DOCTYPE html>
<html lang="en">
<head>
<title>IRC</title>
<link href='http://fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'>
<link href="style.css" rel="stylesheet">
</head>
<body>
<script src="bundle.js"></script>
</body>
</html>

View file

@ -0,0 +1,37 @@
var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var channelActions = Reflux.createActions([
'join',
'joined',
'part',
'parted',
'setUsers',
'load'
]);
channelActions.join.preEmit = function(data) {
sock.send('join', data);
};
channelActions.part.preEmit = function(data) {
sock.send('part', data);
};
sock.on('join', function(data) {
channelActions.joined(data.user, data.server, data.channels[0]);
});
sock.on('part', function(data) {
channelActions.parted(data.user, data.server, data.channels[0]);
});
sock.on('users', function(data) {
channelActions.setUsers(data.users, data.server, data.channel);
});
sock.on('channels', function(data) {
channelActions.load(data);
});
module.exports = channelActions;

View file

@ -0,0 +1,13 @@
var Reflux = require('reflux');
var messageActions = Reflux.createActions([
'send',
'add',
'selectTab'
]);
messageActions.send.preEmit = function() {
};
module.exports = messageActions;

View file

@ -0,0 +1,13 @@
var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var serverActions = Reflux.createActions([
'connect',
'disconnect'
]);
serverActions.connect.preEmit = function(data) {
sock.send('connect', data);
};
module.exports = serverActions;

View file

@ -0,0 +1,7 @@
var Reflux = require('reflux');
var tabActions = Reflux.createActions([
'select'
]);
module.exports = tabActions;

77
client/src/js/app.js Normal file
View file

@ -0,0 +1,77 @@
var React = require('react');
var _ = require('lodash');
var sock = require('./socket')('/ws');
var util = require('./util');
var App = require('./components/App.jsx');
var messageActions = require('./actions/message.js');
var tabActions = require('./actions/tab.js');
var serverActions = require('./actions/server.js');
var channelActions = require('./actions/channel.js');
React.render(<App />, document.body);
var uuid = localStorage.uuid || (localStorage.uuid = util.UUID());
tabActions.select('irc.freenode.net');
sock.on('connect', function() {
sock.send('uuid', uuid);
serverActions.connect({
server: 'irc.freenode.net',
nick: 'test' + Math.floor(Math.random() * 99999),
username: 'user'
});
channelActions.join({
server: 'irc.freenode.net',
channels: [ '#stuff', '#go-nuts' ]
});
});
channelActions.joined.listen(function(user, server, channel) {
messageActions.add({
server: server,
from: '',
to: channel,
message: user + ' joined the channel'
});
});
channelActions.parted.listen(function(user, server, channel) {
messageActions.add({
server: server,
from: '',
to: channel,
message: user + ' left the channel'
});
});
sock.on('message', function(data) {
messageActions.add(data);
});
sock.on('pm', function(data) {
messageActions.add(data);
});
sock.on('topic', function(data) {
messageActions.add({
server: data.server,
from: '',
to: data.channel,
message: data.topic
});
});
sock.on('motd', function(data) {
_.each(data.content.split('\n'), function(line) {
messageActions.add({
server: data.server,
from: '',
to: data.server,
message: line
});
});
});

View file

@ -0,0 +1,18 @@
var React = require('react');
var TabList = require('./TabList.jsx');
var MessageBox = require('./MessageBox.jsx');
var UserList = require('./UserList.jsx');
var App = React.createClass({
render: function() {
return (
<div>
<TabList />
<MessageBox />
<UserList />
</div>
);
}
});
module.exports = App;

View file

@ -0,0 +1,44 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var messageStore = require('../stores/message.js');
var selectedTabStore = require('../stores/selectedTab.js');
var MessageBox = React.createClass({
mixins: [
Reflux.connect(messageStore, 'messages'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
messages: messageStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
componentWillUpdate: function() {
var el = this.getDOMNode();
this.autoScroll = el.scrollTop + el.offsetHeight === el.scrollHeight;
},
componentDidUpdate: function() {
if (this.autoScroll) {
var el = this.getDOMNode();
el.scrollTop = el.scrollHeight;
}
},
render: function() {
var tab = this.state.selectedTab.channel || this.state.selectedTab.server;
var messages = _.map(this.state.messages[tab], function(message) {
return <p>{message.from ? message.from + ': ' : null}{message.message}</p>;
});
return (
<div className="messagebox">{messages}</div>
);
}
});
module.exports = MessageBox;

View file

@ -0,0 +1,41 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var serverStore = require('../stores/server.js');
var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js');
var tabActions = require('../actions/tab.js');
var TabList = React.createClass({
mixins: [
Reflux.connect(serverStore, 'servers'),
Reflux.connect(channelStore, 'channels'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
servers: serverStore.getState(),
channels: channelStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
render: function() {
var self = this;
var tabs = _.map(this.state.channels, function(server, address) {
var channels = _.map(server, function(channel, name) {
return <p onClick={tabActions.select.bind(null, address, name)}>{name}</p>;
});
channels.unshift(<p onClick={tabActions.select.bind(null, address, null)}>{address}</p>);
return channels;
});
return (
<div className="tablist">{tabs}</div>
);
}
});
module.exports = TabList;

View file

@ -0,0 +1,36 @@
var React = require('react');
var Reflux = require('reflux');
var _ = require('lodash');
var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js');
var UserList = React.createClass({
mixins: [
Reflux.connect(channelStore, 'channels'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
channels: channelStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
render: function() {
var users = null;
var tab = this.state.selectedTab;
if (tab.channel) {
users = _.map(this.state.channels[tab.server][tab.channel].users, function(user) {
return <p>{user}</p>;
});
}
return (
<div className="userlist">{users}</div>
);
}
});
module.exports = UserList;

40
client/src/js/socket.js Normal file
View file

@ -0,0 +1,40 @@
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
var sockets = {};
function createSocket(path) {
if (sockets[path]) {
return sockets[path];
} else {
var ws = new WebSocket('ws://' + window.location.host + path);
var sock = {
send: function(type, data) {
ws.send(JSON.stringify({ type: type, request: data }));
}
};
_.extend(sock, EventEmitter.prototype);
sockets[path] = sock;
ws.onopen = function() {
sock.emit('connect');
};
ws.onclose = function() {
sock.emit('disconnect');
};
ws.onmessage = function(e) {
var msg = JSON.parse(e.data);
sock.emit(msg.type, msg.response);
};
return sock;
}
}
module.exports = createSocket;

View file

@ -0,0 +1,58 @@
var Reflux = require('reflux');
var _ = require('lodash');
var actions = require('../actions/channel.js');
var channels = {};
function initChannel(server, channel) {
if (!(server in channels)) {
channels[server] = {};
channels[server][channel] = { users: [] };
} else if (!(channel in channels[server])) {
channels[server][channel] = { users: [] };
}
}
var channelStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
joined: function(user, server, channel) {
initChannel(server, channel);
channels[server][channel].users.push(user);
this.trigger(channels);
},
part: function(data) {
_.each(data.channels, function(channel) {
delete channels[data.server][channel];
});
this.trigger(channels);
},
parted: function(user, server, channel) {
_.pull(channels[server][channel].users, user);
this.trigger(channels);
},
setUsers: function(users, server, channel) {
initChannel(server, channel);
channels[server][channel].users = users;
this.trigger(channels);
},
load: function(storedChannels) {
_.each(storedChannels, function(channel) {
initChannel(channel.server, channel.name);
channels[channel.server][channel.name].users = channel.users;
});
this.trigger(channels);
},
getState: function() {
return channels;
}
});
module.exports = channelStore;

View file

@ -0,0 +1,31 @@
var Reflux = require('reflux');
var actions = require('../actions/message.js');
var messages = {};
var messageStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
add: function(message) {
var dest = message.to || message.from;
if (message.from.indexOf('.') !== -1) {
dest = message.server;
}
if (!(dest in messages)) {
messages[dest] = [message];
} else {
messages[dest].push(message);
}
this.trigger(messages);
},
getState: function() {
return messages;
}
});
module.exports = messageStore;

View file

@ -0,0 +1,22 @@
var Reflux = require('reflux');
var actions = require('../actions/tab.js');
var selectedTab = {};
var selectedTabStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
select: function(server, channel) {
selectedTab.server = server;
selectedTab.channel = channel;
this.trigger(selectedTab);
},
getState: function() {
return selectedTab;
}
});
module.exports = selectedTabStore;

View file

@ -0,0 +1,21 @@
var Reflux = require('reflux');
var actions = require('../actions/server.js');
var servers = {};
var serverStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
},
connect: function(data) {
servers[data.server] = data;
this.trigger(servers);
},
getState: function() {
return servers;
}
});
module.exports = serverStore;

6
client/src/js/util.js Normal file
View file

@ -0,0 +1,6 @@
exports.UUID = function() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
var r = Math.random()*16|0, v = c == 'x' ? r : (r&0x3|0x8);
return v.toString(16);
});
};

53
client/src/style.css Normal file
View file

@ -0,0 +1,53 @@
* {
margin: 0;
padding: 0;
}
body {
font-family: Inconsolata, sans-serif;
background: #111;
color: #FFF;
}
p {
line-height: 1.5;
}
.tablist {
position: fixed;
left: 0;
top: 0;
bottom: 0;
right: 200px;
padding: 20px;
overflow: auto;
}
.tablist p {
cursor: pointer;
}
.tablist p:hover {
color: #AAA;
}
.messagebox {
position: fixed;
left: 200px;
top: 0;
bottom: 0;
right: 200px;
padding: 20px;
overflow: auto;
z-index: 1;
}
.userlist {
position: fixed;
top: 0;
bottom: 0;
right: 0;
width: 200px;
padding: 20px;
overflow: auto;
}