Init
This commit is contained in:
commit
508a04cf4c
30 changed files with 1545 additions and 0 deletions
78
client/gulpfile.js
Normal file
78
client/gulpfile.js
Normal 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
27
client/package.json
Normal 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
11
client/src/index.html
Normal 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>
|
37
client/src/js/actions/channel.js
Normal file
37
client/src/js/actions/channel.js
Normal 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;
|
13
client/src/js/actions/message.js
Normal file
13
client/src/js/actions/message.js
Normal file
|
@ -0,0 +1,13 @@
|
|||
var Reflux = require('reflux');
|
||||
|
||||
var messageActions = Reflux.createActions([
|
||||
'send',
|
||||
'add',
|
||||
'selectTab'
|
||||
]);
|
||||
|
||||
messageActions.send.preEmit = function() {
|
||||
|
||||
};
|
||||
|
||||
module.exports = messageActions;
|
13
client/src/js/actions/server.js
Normal file
13
client/src/js/actions/server.js
Normal 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;
|
7
client/src/js/actions/tab.js
Normal file
7
client/src/js/actions/tab.js
Normal 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
77
client/src/js/app.js
Normal 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
|
||||
});
|
||||
});
|
||||
});
|
18
client/src/js/components/App.jsx
Normal file
18
client/src/js/components/App.jsx
Normal 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;
|
44
client/src/js/components/MessageBox.jsx
Normal file
44
client/src/js/components/MessageBox.jsx
Normal 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;
|
41
client/src/js/components/TabList.jsx
Normal file
41
client/src/js/components/TabList.jsx
Normal 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;
|
36
client/src/js/components/UserList.jsx
Normal file
36
client/src/js/components/UserList.jsx
Normal 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
40
client/src/js/socket.js
Normal 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;
|
58
client/src/js/stores/channel.js
Normal file
58
client/src/js/stores/channel.js
Normal 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;
|
31
client/src/js/stores/message.js
Normal file
31
client/src/js/stores/message.js
Normal 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;
|
22
client/src/js/stores/selectedTab.js
Normal file
22
client/src/js/stores/selectedTab.js
Normal 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;
|
21
client/src/js/stores/server.js
Normal file
21
client/src/js/stores/server.js
Normal 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
6
client/src/js/util.js
Normal 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
53
client/src/style.css
Normal 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;
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue