Added title bar and basic message and command input

This commit is contained in:
khlieng 2015-01-21 03:06:34 +01:00
parent 508a04cf4c
commit f42d6011c6
23 changed files with 399 additions and 83 deletions

3
.gitignore vendored
View File

@ -1,3 +1,4 @@
bin/ bin/
client/dist/ client/dist/
client/node_modules/ client/node_modules/
data.db

View File

@ -24,4 +24,4 @@
"react-router": "~0.11.6", "react-router": "~0.11.6",
"react": "~0.12.2" "react": "~0.12.2"
} }
} }

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var sock = require('../socket.js')('/ws'); var sock = require('../socket.js')('/ws');
var channelActions = Reflux.createActions([ var channelActions = Reflux.createActions([
@ -6,7 +7,9 @@ var channelActions = Reflux.createActions([
'joined', 'joined',
'part', 'part',
'parted', 'parted',
'quit',
'setUsers', 'setUsers',
'setTopic',
'load' 'load'
]); ]);
@ -26,10 +29,18 @@ sock.on('part', function(data) {
channelActions.parted(data.user, data.server, data.channels[0]); channelActions.parted(data.user, data.server, data.channels[0]);
}); });
sock.on('quit', function(data) {
channelActions.quit(data.user, data.server);
});
sock.on('users', function(data) { sock.on('users', function(data) {
channelActions.setUsers(data.users, data.server, data.channel); channelActions.setUsers(data.users, data.server, data.channel);
}); });
sock.on('topic', function(data) {
channelActions.setTopic(data.topic, data.server, data.channel);
});
sock.on('channels', function(data) { sock.on('channels', function(data) {
channelActions.load(data); channelActions.load(data);
}); });

View File

@ -1,13 +1,19 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var messageActions = Reflux.createActions([ var messageActions = Reflux.createActions([
'send', 'send',
'add', 'add',
'selectTab' 'selectTab'
]); ]);
messageActions.send.preEmit = function() { messageActions.send.preEmit = function(message, to, server) {
sock.send('chat', {
server: server,
to: to,
message: message
});
}; };
module.exports = messageActions; module.exports = messageActions;

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var sock = require('../socket.js')('/ws'); var sock = require('../socket.js')('/ws');
var serverActions = Reflux.createActions([ var serverActions = Reflux.createActions([

View File

@ -26,7 +26,7 @@ sock.on('connect', function() {
channelActions.join({ channelActions.join({
server: 'irc.freenode.net', server: 'irc.freenode.net',
channels: [ '#stuff', '#go-nuts' ] channels: [ '#stuff' ]
}); });
}); });
@ -35,7 +35,8 @@ channelActions.joined.listen(function(user, server, channel) {
server: server, server: server,
from: '', from: '',
to: channel, to: channel,
message: user + ' joined the channel' message: user + ' joined the channel',
type: 'info'
}); });
}); });
@ -44,7 +45,8 @@ channelActions.parted.listen(function(user, server, channel) {
server: server, server: server,
from: '', from: '',
to: channel, to: channel,
message: user + ' left the channel' message: user + ' left the channel',
type: 'info'
}); });
}); });
@ -56,15 +58,6 @@ sock.on('pm', function(data) {
messageActions.add(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) { sock.on('motd', function(data) {
_.each(data.content.split('\n'), function(line) { _.each(data.content.split('\n'), function(line) {
messageActions.add({ messageActions.add({

View File

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

View File

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

View File

@ -0,0 +1,48 @@
var React = require('react');
var Reflux = require('reflux');
var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js');
var ChatTitle = React.createClass({
mixins: [
Reflux.connect(channelStore, 'channels'),
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
channels: channelStore.getState(),
selectedTab: selectedTabStore.getState()
};
},
render: function() {
var tab = this.state.selectedTab;
var title;
if (tab.channel) {
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;
}
}
} else {
title = tab.server;
}
return (
<div className="chat-title-bar">
<span className="chat-title" title={title}>{title}</span>
</div>
);
}
});
module.exports = ChatTitle;

View File

@ -1,6 +1,7 @@
var React = require('react'); var React = require('react');
var Reflux = require('reflux'); var Reflux = require('reflux');
var _ = require('lodash'); var _ = require('lodash');
var messageStore = require('../stores/message.js'); var messageStore = require('../stores/message.js');
var selectedTabStore = require('../stores/selectedTab.js'); var selectedTabStore = require('../stores/selectedTab.js');
@ -32,7 +33,13 @@ var MessageBox = React.createClass({
render: function() { render: function() {
var tab = this.state.selectedTab.channel || this.state.selectedTab.server; var tab = this.state.selectedTab.channel || this.state.selectedTab.server;
var messages = _.map(this.state.messages[tab], function(message) { var messages = _.map(this.state.messages[tab], function(message) {
return <p>{message.from ? message.from + ': ' : null}{message.message}</p>; 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 ( return (

View File

@ -0,0 +1,65 @@
var React = require('react');
var Reflux = require('reflux');
var selectedTabStore = require('../stores/selectedTab.js');
var messageActions = require('../actions/message.js');
var channelActions = require('../actions/channel.js');
function dispatchCommand(cmd, channel, server) {
var params = cmd.slice(1).split(' ');
switch (params[0].toLowerCase()) {
case 'join':
if (params[1]) {
channelActions.join({
server: server,
channels: [params[1]]
});
}
break;
case 'part':
if (channel) {
channelActions.part({
server: server,
channels: [channel]
});
}
break;
}
}
var MessageInput = React.createClass({
mixins: [
Reflux.connect(selectedTabStore, 'selectedTab')
],
getInitialState: function() {
return {
selectedTab: selectedTabStore.getState()
};
},
handleKey: function(e) {
if (e.which === 13 && e.target.value) {
var tab = this.state.selectedTab;
if (e.target.value.charAt(0) === '/') {
dispatchCommand(e.target.value, tab.channel, tab.server);
} else {
messageActions.send(e.target.value, tab.channel, tab.server);
}
e.target.value = '';
}
},
render: function() {
return (
<div className="message-input-wrap">
<input className="message-input" type="text" onKeyDown={this.handleKey} />
</div>
);
}
});
module.exports = MessageInput;

View File

@ -1,6 +1,7 @@
var React = require('react'); var React = require('react');
var Reflux = require('reflux'); var Reflux = require('reflux');
var _ = require('lodash'); var _ = require('lodash');
var channelStore = require('../stores/channel.js'); var channelStore = require('../stores/channel.js');
var selectedTabStore = require('../stores/selectedTab.js'); var selectedTabStore = require('../stores/selectedTab.js');
@ -20,11 +21,14 @@ var UserList = React.createClass({
render: function() { render: function() {
var users = null; var users = null;
var tab = this.state.selectedTab; var tab = this.state.selectedTab;
if (tab.channel) { if (tab.channel) {
users = _.map(this.state.channels[tab.server][tab.channel].users, function(user) { var channel = this.state.channels[tab.server][tab.channel];
return <p>{user}</p>; if (channel) {
}); users = _.map(channel.users, function(user) {
return <p>{user}</p>;
});
}
} }
return ( return (

View File

@ -1,4 +1,5 @@
var EventEmitter = require('events').EventEmitter; var EventEmitter = require('events').EventEmitter;
var _ = require('lodash'); var _ = require('lodash');
var sockets = {}; var sockets = {};
@ -8,7 +9,7 @@ function createSocket(path) {
return sockets[path]; return sockets[path];
} else { } else {
var ws = new WebSocket('ws://' + window.location.host + path); var ws = new WebSocket('ws://' + window.location.host + path);
var sock = { var sock = {
send: function(type, data) { send: function(type, data) {
ws.send(JSON.stringify({ type: type, request: data })); ws.send(JSON.stringify({ type: type, request: data }));

View File

@ -1,5 +1,6 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var _ = require('lodash'); var _ = require('lodash');
var actions = require('../actions/channel.js'); var actions = require('../actions/channel.js');
var channels = {}; var channels = {};
@ -36,16 +37,29 @@ var channelStore = Reflux.createStore({
this.trigger(channels); this.trigger(channels);
}, },
quit: function(user, server) {
_.each(channels[server], function(channel) {
_.pull(channel.users, user);
});
this.trigger(channels);
},
setUsers: function(users, server, channel) { setUsers: function(users, server, channel) {
initChannel(server, channel); initChannel(server, channel);
channels[server][channel].users = users; channels[server][channel].users = users;
this.trigger(channels); this.trigger(channels);
}, },
setTopic: function(topic, server, channel) {
channels[server][channel].topic = topic;
this.trigger(channels);
},
load: function(storedChannels) { load: function(storedChannels) {
_.each(storedChannels, function(channel) { _.each(storedChannels, function(channel) {
initChannel(channel.server, channel.name); initChannel(channel.server, channel.name);
channels[channel.server][channel.name].users = channel.users; channels[channel.server][channel.name].users = channel.users;
channels[channel.server][channel.name].topic = channel.topic;
}); });
this.trigger(channels); this.trigger(channels);
}, },

View File

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

View File

@ -1,11 +1,15 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var _ = require('lodash');
var actions = require('../actions/tab.js'); var actions = require('../actions/tab.js');
var channelActions = require('../actions/channel.js');
var selectedTab = {}; var selectedTab = {};
var selectedTabStore = Reflux.createStore({ var selectedTabStore = Reflux.createStore({
init: function() { init: function() {
this.listenToMany(actions); this.listenToMany(actions);
this.listenTo(channelActions.part, 'part');
}, },
select: function(server, channel) { select: function(server, channel) {
@ -14,6 +18,19 @@ var selectedTabStore = Reflux.createStore({
this.trigger(selectedTab); this.trigger(selectedTab);
}, },
part: function(data) {
var self = this;
if (data.server === selectedTab.server) {
_.each(data.channels, function(channel) {
if (channel === selectedTab.channel) {
delete selectedTab.channel;
self.trigger(selectedTab);
return;
}
});
}
},
getState: function() { getState: function() {
return selectedTab; return selectedTab;
} }

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux'); var Reflux = require('reflux');
var actions = require('../actions/server.js'); var actions = require('../actions/server.js');
var servers = {}; var servers = {};

View File

@ -1,6 +1,7 @@
* { * {
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box;
} }
body { body {
@ -9,6 +10,14 @@ body {
color: #FFF; color: #FFF;
} }
input {
font: 16px Inconsolata, sans-serif;
background: rgba(0,0,0,0.25);
color: #FFF;
outline: none;
border: none;
}
p { p {
line-height: 1.5; line-height: 1.5;
} }
@ -19,7 +28,7 @@ p {
top: 0; top: 0;
bottom: 0; bottom: 0;
right: 200px; right: 200px;
padding: 20px; padding: 15px;
overflow: auto; overflow: auto;
} }
@ -31,23 +40,64 @@ p {
color: #AAA; color: #AAA;
} }
.messagebox { .chat-title-bar {
position: fixed; position: fixed;
left: 200px; left: 200px;
top: 0; top: 0;
bottom: 0;
right: 200px; right: 200px;
padding: 20px; height: 50px;
padding: 0 15px;
line-height: 50px;
background: rgba(0,0,0,0.25);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.chat-title {
}
.messagebox {
position: fixed;
left: 200px;
top: 50px;
bottom: 50px;
right: 200px;
padding: 15px;
overflow: auto; overflow: auto;
z-index: 1; z-index: 1;
} }
.message {
}
.message-info {
color: #666;
}
.message-input-wrap {
position: fixed;
left: 200px;
bottom: 0px;
right: 0;
height: 50px;
z-index: 1;
}
.message-input {
width: 100%;
height: 100%;
padding: 15px;
}
.userlist { .userlist {
position: fixed; position: fixed;
top: 0; top: 0;
bottom: 0; bottom: 50px;
right: 0; right: 0;
width: 200px; width: 200px;
padding: 20px; padding: 15px;
overflow: auto; overflow: auto;
} }

View File

@ -26,6 +26,11 @@ type Join struct {
Channels []string `json:"channels"` Channels []string `json:"channels"`
} }
type Quit struct {
Server string `json:"server"`
User string `json:"user"`
}
type Chat struct { type Chat struct {
Server string `json:"server"` Server string `json:"server"`
From string `json:"from"` From string `json:"from"`

View File

@ -2,7 +2,7 @@ package main
import ( import (
"bytes" "bytes"
"fmt" "log"
"strings" "strings"
"github.com/khlieng/irc/storage" "github.com/khlieng/irc/storage"
@ -16,7 +16,7 @@ func handleMessages(irc *IRC, session *Session) {
for msg := range irc.Messages { for msg := range irc.Messages {
switch msg.Command { switch msg.Command {
case JOIN: case JOIN:
user := parseUser(msg.Prefix) user := msg.Prefix
session.sendJSON("join", Join{ session.sendJSON("join", Join{
Server: irc.Host, Server: irc.Host,
@ -34,7 +34,7 @@ func handleMessages(irc *IRC, session *Session) {
} }
case PART: case PART:
user := parseUser(msg.Prefix) user := msg.Prefix
session.sendJSON("part", Join{ session.sendJSON("part", Join{
Server: irc.Host, Server: irc.Host,
@ -65,15 +65,18 @@ func handleMessages(irc *IRC, session *Session) {
} }
case QUIT: case QUIT:
/* user := msg.Prefix
session.sendJSON("quit", Quit{
Server: irc.Host, session.sendJSON("quit", Quit{
User: user, Server: irc.Host,
}) User: user,
*/ })
channelStore.RemoveUserAll(user, irc.Host)
case RPL_WELCOME, case RPL_WELCOME,
RPL_YOURHOST, RPL_YOURHOST,
RPL_CREATED,
RPL_LUSERCLIENT, RPL_LUSERCLIENT,
RPL_LUSEROP, RPL_LUSEROP,
RPL_LUSERUNKNOWN, RPL_LUSERUNKNOWN,
@ -92,6 +95,8 @@ func handleMessages(irc *IRC, session *Session) {
Topic: msg.Trailing, Topic: msg.Trailing,
}) })
channelStore.SetTopic(msg.Trailing, irc.Host, msg.Params[1])
case RPL_NAMREPLY: case RPL_NAMREPLY:
users := strings.Split(msg.Trailing, " ") users := strings.Split(msg.Trailing, " ")
@ -138,10 +143,5 @@ func handleMessages(irc *IRC, session *Session) {
} }
func printMessage(msg *Message, irc *IRC) { func printMessage(msg *Message, irc *IRC) {
fmt.Printf("%s: %s %s %s\n", irc.nick, msg.Prefix, msg.Command, msg.Params) log.Println(irc.nick+":", msg.Prefix, msg.Command, msg.Params, msg.Trailing)
if msg.Trailing != "" {
fmt.Println(msg.Trailing)
}
fmt.Println()
} }

View File

@ -5,63 +5,98 @@ import (
) )
type ChannelStore struct { type ChannelStore struct {
data map[string]map[string][]string users map[string]map[string][]string
lock sync.Mutex userLock sync.Mutex
topic map[string]map[string]string
topicLock sync.Mutex
} }
func NewChannelStore() *ChannelStore { func NewChannelStore() *ChannelStore {
return &ChannelStore{ return &ChannelStore{
data: make(map[string]map[string][]string), users: make(map[string]map[string][]string),
topic: make(map[string]map[string]string),
} }
} }
func (c *ChannelStore) GetUsers(server, channel string) []string { func (c *ChannelStore) GetUsers(server, channel string) []string {
c.lock.Lock() c.userLock.Lock()
users := make([]string, len(c.data[server][channel])) users := make([]string, len(c.users[server][channel]))
copy(users, c.data[server][channel]) copy(users, c.users[server][channel])
c.lock.Unlock() c.userLock.Unlock()
return users return users
} }
func (c *ChannelStore) SetUsers(users []string, server, channel string) { func (c *ChannelStore) SetUsers(users []string, server, channel string) {
c.lock.Lock() c.userLock.Lock()
if _, ok := c.data[server]; !ok { if _, ok := c.users[server]; !ok {
c.data[server] = make(map[string][]string) c.users[server] = make(map[string][]string)
} }
c.data[server][channel] = users c.users[server][channel] = users
c.lock.Unlock() c.userLock.Unlock()
} }
func (c *ChannelStore) AddUser(user, server, channel string) { func (c *ChannelStore) AddUser(user, server, channel string) {
c.lock.Lock() c.userLock.Lock()
if _, ok := c.data[server]; !ok { if _, ok := c.users[server]; !ok {
c.data[server] = make(map[string][]string) c.users[server] = make(map[string][]string)
} }
if users, ok := c.data[server][channel]; ok { if users, ok := c.users[server][channel]; ok {
c.data[server][channel] = append(users, user) c.users[server][channel] = append(users, user)
} else { } else {
c.data[server][channel] = []string{user} c.users[server][channel] = []string{user}
} }
c.lock.Unlock() c.userLock.Unlock()
} }
func (c *ChannelStore) RemoveUser(user, server, channel string) { func (c *ChannelStore) RemoveUser(user, server, channel string) {
c.lock.Lock() c.userLock.Lock()
defer c.lock.Unlock() c.removeUser(user, server, channel)
c.userLock.Unlock()
}
for i, u := range c.data[server][channel] { func (c *ChannelStore) RemoveUserAll(user, server string) {
c.userLock.Lock()
for channel, _ := range c.users[server] {
c.removeUser(user, server, channel)
}
c.userLock.Unlock()
}
func (c *ChannelStore) GetTopic(server, channel string) string {
c.topicLock.Lock()
defer c.topicLock.Unlock()
return c.topic[server][channel]
}
func (c *ChannelStore) SetTopic(topic, server, channel string) {
c.topicLock.Lock()
if _, ok := c.topic[server]; !ok {
c.topic[server] = make(map[string]string)
}
c.topic[server][channel] = topic
c.topicLock.Unlock()
}
func (c *ChannelStore) removeUser(user, server, channel string) {
for i, u := range c.users[server][channel] {
if u == user { if u == user {
users := c.data[server][channel] users := c.users[server][channel]
c.data[server][channel] = append(users[:i], users[i+1:]...) c.users[server][channel] = append(users[:i], users[i+1:]...)
return return
} }
} }

View File

@ -18,6 +18,7 @@ type Channel struct {
Server string `json:"server"` Server string `json:"server"`
Name string `json:"name"` Name string `json:"name"`
Users []string `json:"users"` Users []string `json:"users"`
Topic string `json:"topic,omitempty"`
} }
type User struct { type User struct {
@ -99,7 +100,7 @@ func (u User) GetChannels() []Channel {
} }
func (u User) AddServer(server Server) { func (u User) AddServer(server Server) {
db.Update(func(tx *bolt.Tx) error { go db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Servers")) b := tx.Bucket([]byte("Servers"))
data, _ := json.Marshal(server) data, _ := json.Marshal(server)
@ -110,7 +111,7 @@ func (u User) AddServer(server Server) {
} }
func (u User) AddChannel(channel Channel) { func (u User) AddChannel(channel Channel) {
db.Update(func(tx *bolt.Tx) error { go db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte("Channels")) b := tx.Bucket([]byte("Channels"))
data, _ := json.Marshal(channel) data, _ := json.Marshal(channel)
@ -121,16 +122,25 @@ func (u User) AddChannel(channel Channel) {
} }
func (u User) RemoveServer(address string) { func (u User) RemoveServer(address string) {
db.Update(func(tx *bolt.Tx) error { go db.Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte("Channels")).Delete([]byte(u.UUID + ":" + address)) serverID := []byte(u.UUID + ":" + address)
tx.Bucket([]byte("Servers")).Delete(serverID)
b := tx.Bucket([]byte("Channels"))
c := b.Cursor()
for k, _ := c.Seek(serverID); bytes.HasPrefix(k, serverID); k, _ = c.Next() {
b.Delete(k)
}
return nil return nil
}) })
} }
func (u User) RemoveChannel(server, channel string) { func (u User) RemoveChannel(server, channel string) {
db.Update(func(tx *bolt.Tx) error { go db.Update(func(tx *bolt.Tx) error {
tx.Bucket([]byte("Servers")).Delete([]byte(u.UUID + ":" + server + ":" + channel)) tx.Bucket([]byte("Channels")).Delete([]byte(u.UUID + ":" + server + ":" + channel))
return nil return nil
}) })

View File

@ -48,6 +48,7 @@ func handleWS(ws *websocket.Conn) {
channels := session.user.GetChannels() channels := session.user.GetChannels()
for i, channel := range channels { for i, channel := range channels {
channels[i].Users = channelStore.GetUsers(channel.Server, channel.Name) channels[i].Users = channelStore.GetUsers(channel.Server, channel.Name)
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
} }
session.sendJSON("channels", channels) session.sendJSON("channels", channels)
@ -106,6 +107,17 @@ func handleWS(ws *websocket.Conn) {
irc.Part(data.Channels...) irc.Part(data.Channels...)
} }
case "quit":
var data Quit
json.Unmarshal(req.Request, &data)
if irc, ok := session.getIRC(data.Server); ok {
channelStore.RemoveUserAll(irc.nick, data.Server)
session.user.RemoveServer(data.Server)
irc.Quit()
}
case "chat": case "chat":
var data Chat var data Chat