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

78
client/gulpfile.js Normal file
View file

@ -0,0 +1,78 @@
var gulp = require('gulp');
var gulpif = require('gulp-if');
var minifyHTML = require('gulp-minify-html');
var minifyCSS = require('gulp-minify-css');
var autoprefixer = require('gulp-autoprefixer');
var uglify = require('gulp-uglify');
var browserify = require('browserify');
var source = require('vinyl-source-stream');
var streamify = require('gulp-streamify');
var reactify = require('reactify');
var strictify = require('strictify');
var watchify = require('watchify');
var argv = require('yargs')
.alias('p', 'production')
.argv;
if (argv.production) {
process.env['NODE_ENV'] = 'production';
}
gulp.task('html', function() {
gulp.src('./src/*.html')
.pipe(minifyHTML())
.pipe(gulp.dest('./dist'));
});
gulp.task('css', function() {
gulp.src('./src/*.css')
.pipe(autoprefixer())
.pipe(minifyCSS())
.pipe(gulp.dest('./dist'));
});
gulp.task('js', function() {
return js(false);
});
function js(watch) {
var bundler, rebundle;
bundler = browserify('./src/js/app.js', {
debug: !argv.production,
cache: {},
packageCache: {},
fullPaths: watch
});
if (watch) {
bundler = watchify(bundler);
}
bundler
.transform(reactify)
.transform(strictify);
rebundle = function() {
var stream = bundler.bundle();
stream.on('error', console.log);
return stream
.pipe(source('bundle.js'))
.pipe(gulpif(argv.production, streamify(uglify())))
.pipe(gulp.dest('./dist'));
};
bundler.on('time', function(time) {
console.log('JS bundle: ' + time + ' ms');
});
bundler.on('update', rebundle);
return rebundle();
}
gulp.task('watch', ['default'], function() {
gulp.watch('./src/*.html', ['html']);
gulp.watch('./src/*.css', ['css']);
return js(true);
});
gulp.task('default', ['html', 'css', 'js']);

27
client/package.json Normal file
View file

@ -0,0 +1,27 @@
{
"name": "irc",
"version": "0.0.0",
"description": "",
"main": "index.js",
"devDependencies": {
"yargs": "~1.3.3",
"strictify": "~0.2.0",
"vinyl-source-stream": "~1.0.0",
"gulp-if": "~1.2.5",
"gulp": "~3.8.10",
"gulp-uglify": "~1.0.2",
"gulp-minify-css": "~0.3.11",
"gulp-streamify": "0.0.5",
"gulp-minify-html": "~0.1.8",
"watchify": "~2.2.1",
"browserify": "~8.0.3",
"gulp-autoprefixer": "~2.0.0",
"reactify": "~0.17.1"
},
"dependencies": {
"lodash": "~2.4.1",
"reflux": "~0.2.2",
"react-router": "~0.11.6",
"react": "~0.12.2"
}
}

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