IRC output gets queued until RPL_WELCOME, added tcp timeouts and error handling, store selected tab in localStorage, more design work, upgraded to lodash 3.0.0
This commit is contained in:
parent
5c6c43e017
commit
3c02b00303
|
@ -19,9 +19,9 @@
|
|||
"reactify": "~0.17.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"lodash": "~2.4.1",
|
||||
"lodash": "3.0.0",
|
||||
"reflux": "~0.2.2",
|
||||
"react-router": "~0.11.6",
|
||||
"react": "~0.12.2"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<title>IRC</title>
|
||||
<link href='http://fonts.googleapis.com/css?family=Inconsolata' rel='stylesheet' type='text/css'>
|
||||
<link href="//fonts.googleapis.com/css?family=Montserrat|Roboto" rel="stylesheet">
|
||||
<link href="style.css" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
|
|
|
@ -8,11 +8,12 @@ var serverActions = Reflux.createActions([
|
|||
'load'
|
||||
]);
|
||||
|
||||
serverActions.connect.preEmit = function(server, nick, username) {
|
||||
serverActions.connect.preEmit = function(server, nick, username, tls) {
|
||||
socket.send('connect', {
|
||||
server: server,
|
||||
nick: nick,
|
||||
username: username
|
||||
username: username,
|
||||
tls: tls || false
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -14,8 +14,14 @@ socket.on('connect', function() {
|
|||
socket.send('uuid', uuid);
|
||||
|
||||
serverActions.connect('irc.freenode.net', nick, 'username');
|
||||
serverActions.connect('irc.quakenet.org', nick, 'username');
|
||||
|
||||
channelActions.join(['#stuff'], 'irc.freenode.net');
|
||||
tabActions.select('irc.freenode.net');
|
||||
channelActions.join(['#herp'], 'irc.quakenet.org');
|
||||
});
|
||||
|
||||
socket.on('error', function(error) {
|
||||
console.log(error.server + ': ' + error.message);
|
||||
});
|
||||
|
||||
React.render(<App />, document.body);
|
|
@ -20,18 +20,15 @@ var ChatTitle = React.createClass({
|
|||
render: function() {
|
||||
var tab = this.state.selectedTab;
|
||||
var title;
|
||||
var topic;
|
||||
var usercount;
|
||||
|
||||
if (tab.channel) {
|
||||
if (tab.channel && this.state.channels[tab.server]) {
|
||||
var channel = this.state.channels[tab.server][tab.channel];
|
||||
if (channel) {
|
||||
title = tab.channel
|
||||
title += ' [';
|
||||
title += channel.users.length;
|
||||
title += ']';
|
||||
|
||||
if (channel.topic) {
|
||||
title += ': ' + channel.topic;
|
||||
}
|
||||
usercount = channel.users.length;
|
||||
topic = channel.topic || '';
|
||||
}
|
||||
} else {
|
||||
title = tab.server;
|
||||
|
@ -39,7 +36,11 @@ var ChatTitle = React.createClass({
|
|||
|
||||
return (
|
||||
<div className="chat-title-bar">
|
||||
<span className="chat-title" title={title}>{title}</span>
|
||||
<div>
|
||||
<span className="chat-title">{title}</span>
|
||||
<span className="chat-topic" title={topic}>{topic}</span>
|
||||
</div>
|
||||
<span className="chat-usercount">{usercount}</span>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ var React = require('react');
|
|||
var Reflux = require('reflux');
|
||||
var _ = require('lodash');
|
||||
|
||||
var util = require('../util');
|
||||
var messageStore = require('../stores/message');
|
||||
var selectedTabStore = require('../stores/selectedTab');
|
||||
|
||||
|
@ -38,12 +39,20 @@ var MessageBox = React.createClass({
|
|||
if (this.state.messages[tab.server] && dest) {
|
||||
messages = _.map(this.state.messages[tab.server][dest], function(message) {
|
||||
var messageClass = 'message';
|
||||
|
||||
switch (message.type) {
|
||||
case 'info':
|
||||
messageClass += ' message-info';
|
||||
break;
|
||||
}
|
||||
return <p className={messageClass}>{message.from ? message.from + ': ' : null}{message.message}</p>;
|
||||
|
||||
return (
|
||||
<p className={messageClass}>
|
||||
<span className="message-time">{util.timestamp(message.time)}</span>
|
||||
{ message.from ? <span className="message-sender">{message.from}</span> : null }
|
||||
{message.message}
|
||||
</p>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -21,16 +21,41 @@ var TabList = React.createClass({
|
|||
|
||||
render: function() {
|
||||
var self = this;
|
||||
var tabClass;
|
||||
var selected = this.state.selectedTab;
|
||||
|
||||
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>;
|
||||
var channels = _.map(server, function(channel, name) {
|
||||
if (address === selected.server &&
|
||||
name === selected.channel) {
|
||||
tabClass = 'selected';
|
||||
} else {
|
||||
tabClass = '';
|
||||
}
|
||||
|
||||
return <p className={tabClass} onClick={tabActions.select.bind(null, address, name)}>{name}</p>;
|
||||
});
|
||||
channels.unshift(<p onClick={tabActions.select.bind(null, address, null)}>{address}</p>);
|
||||
|
||||
if (address === selected.server &&
|
||||
selected.channel === null) {
|
||||
tabClass = 'tab-server selected';
|
||||
} else {
|
||||
tabClass = 'tab-server';
|
||||
}
|
||||
|
||||
channels.unshift(<p className={tabClass} onClick={tabActions.select.bind(null, address, null)}>{address}</p>);
|
||||
|
||||
return channels;
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="tablist">{tabs}</div>
|
||||
<div className="tablist">
|
||||
<button className="button-connect">Add Network</button>
|
||||
{tabs}
|
||||
<div className="side-buttons">
|
||||
<button>Settings</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -22,7 +22,7 @@ var UserList = React.createClass({
|
|||
var users = null;
|
||||
var tab = this.state.selectedTab;
|
||||
|
||||
if (tab.channel) {
|
||||
if (tab.channel && this.state.channels[tab.server]) {
|
||||
var channel = this.state.channels[tab.server][tab.channel];
|
||||
if (channel) {
|
||||
users = _.map(channel.users, function(user) {
|
||||
|
|
|
@ -2,6 +2,7 @@ var Reflux = require('reflux');
|
|||
var _ = require('lodash');
|
||||
|
||||
var actions = require('../actions/channel');
|
||||
var serverActions = require('../actions/server');
|
||||
|
||||
var channels = {};
|
||||
|
||||
|
@ -80,6 +81,7 @@ function sortUsers(server, channel) {
|
|||
var channelStore = Reflux.createStore({
|
||||
init: function() {
|
||||
this.listenToMany(actions);
|
||||
this.listenTo(serverActions.connect, 'addServer');
|
||||
},
|
||||
|
||||
part: function(partChannels, server) {
|
||||
|
@ -161,6 +163,13 @@ var channelStore = Reflux.createStore({
|
|||
this.trigger(channels);
|
||||
},
|
||||
|
||||
addServer: function(server) {
|
||||
if (!(server in channels)) {
|
||||
channels[server] = {};
|
||||
this.trigger(channels);
|
||||
}
|
||||
},
|
||||
|
||||
getState: function() {
|
||||
return channels;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
var Reflux = require('reflux');
|
||||
|
||||
var serverStore = require('../stores/server');
|
||||
var serverStore = require('./server');
|
||||
var actions = require('../actions/message');
|
||||
|
||||
var messages = {};
|
||||
|
@ -26,7 +26,8 @@ var messageStore = Reflux.createStore({
|
|||
server: server,
|
||||
from: serverStore.getNick(server),
|
||||
to: to,
|
||||
message: message
|
||||
message: message,
|
||||
time: new Date()
|
||||
}, to);
|
||||
|
||||
this.trigger(messages);
|
||||
|
@ -38,6 +39,8 @@ var messageStore = Reflux.createStore({
|
|||
dest = message.server;
|
||||
}
|
||||
|
||||
message.time = new Date();
|
||||
|
||||
addMessage(message, dest);
|
||||
this.trigger(messages);
|
||||
},
|
||||
|
|
|
@ -5,6 +5,11 @@ var actions = require('../actions/tab');
|
|||
var channelActions = require('../actions/channel');
|
||||
|
||||
var selectedTab = {};
|
||||
var stored = localStorage.selectedTab;
|
||||
|
||||
if (stored) {
|
||||
selectedTab = JSON.parse(stored);
|
||||
}
|
||||
|
||||
var selectedTabStore = Reflux.createStore({
|
||||
init: function() {
|
||||
|
@ -36,4 +41,8 @@ var selectedTabStore = Reflux.createStore({
|
|||
}
|
||||
});
|
||||
|
||||
selectedTabStore.listen(function(selected) {
|
||||
localStorage.selectedTab = JSON.stringify(selected);
|
||||
});
|
||||
|
||||
module.exports = selectedTabStore;
|
|
@ -1,6 +1,17 @@
|
|||
var _ = require('lodash');
|
||||
|
||||
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);
|
||||
});
|
||||
};
|
||||
|
||||
exports.timestamp = function(date) {
|
||||
date = date || new Date();
|
||||
|
||||
var h = _.padLeft(date.getHours(), 2, '0')
|
||||
var m = _.padLeft(date.getMinutes(), 2, '0');
|
||||
|
||||
return h + ':' + m;
|
||||
};
|
|
@ -5,17 +5,24 @@
|
|||
}
|
||||
|
||||
body {
|
||||
font-family: Inconsolata, sans-serif;
|
||||
font-family: Roboto, sans-serif;
|
||||
background: #f0f0f0;
|
||||
}
|
||||
|
||||
input {
|
||||
font: 16px Inconsolata, sans-serif;
|
||||
font: 16px Roboto, sans-serif;
|
||||
border: none;
|
||||
border-top: 1px solid #DDD;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
button {
|
||||
font: 16px Montserrat, sans-serif;
|
||||
border: none;
|
||||
outline: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
p {
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
@ -26,20 +33,64 @@ p {
|
|||
top: 0;
|
||||
bottom: 0;
|
||||
width: 200px;
|
||||
padding: 15px;
|
||||
overflow: auto;
|
||||
background: #272626;
|
||||
background: #222;
|
||||
color: #FFF;
|
||||
font-family: Montserrat, sans-serif;
|
||||
}
|
||||
|
||||
.tablist p {
|
||||
padding: 3px 15px;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.tablist p:hover {
|
||||
background: #111;
|
||||
}
|
||||
|
||||
.tablist p.selected {
|
||||
padding-left: 10px;
|
||||
border-left: 5px solid #6BB758;
|
||||
}
|
||||
|
||||
.tab-server {
|
||||
color: #AAA;
|
||||
margin-top: 15px !important;
|
||||
}
|
||||
|
||||
.button-connect {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
background: #6BB758;
|
||||
color: #FFF;
|
||||
}
|
||||
|
||||
.button-connect:hover {
|
||||
background: #7BBF6A;
|
||||
}
|
||||
|
||||
.button-connect:active {
|
||||
background: #6BB758;
|
||||
}
|
||||
|
||||
.side-buttons {
|
||||
position: fixed;
|
||||
bottom: 0;
|
||||
height: 50px;
|
||||
width: 200px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.side-buttons button {
|
||||
background: #333;
|
||||
color: #FFF;
|
||||
margin: 5px;
|
||||
height: 40px;
|
||||
width: 100px;
|
||||
}
|
||||
|
||||
.chat-title-bar {
|
||||
font-family: Montserrat, sans-serif;
|
||||
position: fixed;
|
||||
left: 200px;
|
||||
top: 0;
|
||||
|
@ -47,14 +98,34 @@ p {
|
|||
height: 50px;
|
||||
padding: 0 15px;
|
||||
line-height: 50px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
border-bottom: 1px solid #DDD;
|
||||
}
|
||||
|
||||
.chat-title-bar div {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 100px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.chat-title {
|
||||
font-size: 24px;
|
||||
margin-left: 15px;
|
||||
font-size: 28px;
|
||||
}
|
||||
|
||||
.chat-topic {
|
||||
display: none;
|
||||
font: 16px Roboto, sans-serif;
|
||||
line-height: 50px;
|
||||
vertical-align: top;
|
||||
color: #222;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
||||
.chat-usercount {
|
||||
font-size: 20px;
|
||||
float: right;
|
||||
}
|
||||
|
||||
.messagebox {
|
||||
|
@ -69,11 +140,25 @@ p {
|
|||
}
|
||||
|
||||
.message {
|
||||
padding-left: 50px;
|
||||
text-indent: -50px;
|
||||
}
|
||||
|
||||
.message span {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.message-info {
|
||||
color: #666;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
.message-sender {
|
||||
color: #6BB758;
|
||||
font: 16px Montserrat, sans-serif;
|
||||
}
|
||||
|
||||
.message-input-wrap {
|
||||
|
@ -100,4 +185,5 @@ p {
|
|||
padding: 15px;
|
||||
overflow: auto;
|
||||
border-left: 1px solid #DDD;
|
||||
overflow-x: hidden;
|
||||
}
|
54
irc.go
54
irc.go
|
@ -6,6 +6,8 @@ import (
|
|||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
|
@ -54,6 +56,8 @@ type Message struct {
|
|||
type IRC struct {
|
||||
conn net.Conn
|
||||
reader *bufio.Reader
|
||||
out chan string
|
||||
ready sync.WaitGroup
|
||||
|
||||
Messages chan *Message
|
||||
Server string
|
||||
|
@ -71,10 +75,11 @@ func NewIRC(nick, username string) *IRC {
|
|||
Username: username,
|
||||
Realname: nick,
|
||||
Messages: make(chan *Message, 32),
|
||||
out: make(chan string, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) Connect(address string) {
|
||||
func (i *IRC) Connect(address string) error {
|
||||
if idx := strings.Index(address, ":"); idx < 0 {
|
||||
i.Host = address
|
||||
|
||||
|
@ -88,13 +93,24 @@ func (i *IRC) Connect(address string) {
|
|||
}
|
||||
i.Server = address
|
||||
|
||||
dialer := &net.Dialer{Timeout: 5 * time.Second}
|
||||
|
||||
if i.TLS {
|
||||
if i.TLSConfig == nil {
|
||||
i.TLSConfig = &tls.Config{InsecureSkipVerify: true}
|
||||
}
|
||||
i.conn, _ = tls.Dial("tcp", address, i.TLSConfig)
|
||||
|
||||
if conn, err := tls.DialWithDialer(dialer, "tcp", address, i.TLSConfig); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i.conn = conn
|
||||
}
|
||||
} else {
|
||||
i.conn, _ = net.Dial("tcp", address)
|
||||
if conn, err := dialer.Dial("tcp", address); err != nil {
|
||||
return err
|
||||
} else {
|
||||
i.conn = conn
|
||||
}
|
||||
}
|
||||
|
||||
i.reader = bufio.NewReader(i.conn)
|
||||
|
@ -102,19 +118,23 @@ func (i *IRC) Connect(address string) {
|
|||
i.Nick(i.nick)
|
||||
i.User(i.Username, i.Realname)
|
||||
|
||||
i.ready.Add(1)
|
||||
go i.send()
|
||||
go i.recv()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (i *IRC) Pass(password string) {
|
||||
i.Write("PASS " + password)
|
||||
i.write("PASS " + password)
|
||||
}
|
||||
|
||||
func (i *IRC) Nick(nick string) {
|
||||
i.Write("NICK " + nick)
|
||||
i.write("NICK " + nick)
|
||||
}
|
||||
|
||||
func (i *IRC) User(username, realname string) {
|
||||
i.Writef("USER %s 0 * :%s", username, realname)
|
||||
i.writef("USER %s 0 * :%s", username, realname)
|
||||
}
|
||||
|
||||
func (i *IRC) Join(channels ...string) {
|
||||
|
@ -147,13 +167,28 @@ func (i *IRC) Quit() {
|
|||
}
|
||||
|
||||
func (i *IRC) Write(data string) {
|
||||
fmt.Fprint(i.conn, data+"\r\n")
|
||||
i.out <- data + "\r\n"
|
||||
}
|
||||
|
||||
func (i *IRC) Writef(format string, a ...interface{}) {
|
||||
i.out <- fmt.Sprintf(format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (i *IRC) write(data string) {
|
||||
fmt.Fprint(i.conn, data+"\r\n")
|
||||
}
|
||||
|
||||
func (i *IRC) writef(format string, a ...interface{}) {
|
||||
fmt.Fprintf(i.conn, format+"\r\n", a...)
|
||||
}
|
||||
|
||||
func (i *IRC) send() {
|
||||
i.ready.Wait()
|
||||
for message := range i.out {
|
||||
fmt.Fprint(i.conn, message)
|
||||
}
|
||||
}
|
||||
|
||||
func (i *IRC) recv() {
|
||||
defer i.conn.Close()
|
||||
for {
|
||||
|
@ -167,7 +202,10 @@ func (i *IRC) recv() {
|
|||
|
||||
switch msg.Command {
|
||||
case PING:
|
||||
i.Write("PONG :" + msg.Trailing)
|
||||
i.write("PONG :" + msg.Trailing)
|
||||
|
||||
case RPL_WELCOME:
|
||||
i.ready.Done()
|
||||
}
|
||||
|
||||
i.Messages <- msg
|
||||
|
|
|
@ -16,6 +16,7 @@ type WSResponse struct {
|
|||
|
||||
type Connect struct {
|
||||
Server string `json:"server"`
|
||||
TLS bool `json:"tls"`
|
||||
Nick string `json:"nick"`
|
||||
Username string `json:"username"`
|
||||
}
|
||||
|
@ -63,3 +64,8 @@ type MOTD struct {
|
|||
Title string `json:"title"`
|
||||
Content string `json:"content"`
|
||||
}
|
||||
|
||||
type Error struct {
|
||||
Server string `json:"server"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
|
|
@ -65,11 +65,18 @@ func (s *Session) sendJSON(t string, v interface{}) {
|
|||
s.out <- res
|
||||
}
|
||||
|
||||
func (s *Session) sendError(err error, server string) {
|
||||
s.sendJSON("error", Error{
|
||||
Server: server,
|
||||
Message: err.Error(),
|
||||
})
|
||||
}
|
||||
|
||||
func (s *Session) write() {
|
||||
for res := range s.out {
|
||||
s.wsLock.Lock()
|
||||
for _, ws := range s.ws {
|
||||
ws.In <- res
|
||||
ws.Out <- res
|
||||
}
|
||||
s.wsLock.Unlock()
|
||||
}
|
||||
|
|
|
@ -7,18 +7,18 @@ import (
|
|||
type WebSocket struct {
|
||||
conn *websocket.Conn
|
||||
|
||||
In chan []byte
|
||||
Out chan []byte
|
||||
}
|
||||
|
||||
func NewWebSocket(ws *websocket.Conn) *WebSocket {
|
||||
return &WebSocket{
|
||||
conn: ws,
|
||||
In: make(chan []byte, 32),
|
||||
Out: make(chan []byte, 32),
|
||||
}
|
||||
}
|
||||
|
||||
func (w *WebSocket) write() {
|
||||
for data := range w.In {
|
||||
for data := range w.Out {
|
||||
w.conn.Write(data)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -74,18 +74,22 @@ func handleWS(ws *websocket.Conn) {
|
|||
log.Println(addr, "connecting to", data.Server)
|
||||
|
||||
irc := NewIRC(data.Nick, data.Username)
|
||||
irc.TLS = true
|
||||
irc.Connect(data.Server)
|
||||
irc.TLS = data.TLS
|
||||
|
||||
session.setIRC(irc.Host, irc)
|
||||
if err := irc.Connect(data.Server); err != nil {
|
||||
session.sendError(err, irc.Host)
|
||||
log.Println(err)
|
||||
} else {
|
||||
session.setIRC(irc.Host, irc)
|
||||
|
||||
go handleMessages(irc, session)
|
||||
go handleMessages(irc, session)
|
||||
|
||||
session.user.AddServer(storage.Server{
|
||||
Address: irc.Host,
|
||||
Nick: data.Nick,
|
||||
Username: data.Username,
|
||||
})
|
||||
session.user.AddServer(storage.Server{
|
||||
Address: irc.Host,
|
||||
Nick: data.Nick,
|
||||
Username: data.Username,
|
||||
})
|
||||
}
|
||||
} else {
|
||||
log.Println(addr, "already connected to", data.Server)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue