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/
client/dist/
client/node_modules/
client/node_modules/
data.db

View File

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

View File

@ -1,4 +1,5 @@
var Reflux = require('reflux');
var sock = require('../socket.js')('/ws');
var channelActions = Reflux.createActions([
@ -6,7 +7,9 @@ var channelActions = Reflux.createActions([
'joined',
'part',
'parted',
'quit',
'setUsers',
'setTopic',
'load'
]);
@ -26,10 +29,18 @@ sock.on('part', function(data) {
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) {
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) {
channelActions.load(data);
});

View File

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

View File

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

View File

@ -26,7 +26,7 @@ sock.on('connect', function() {
channelActions.join({
server: 'irc.freenode.net',
channels: [ '#stuff', '#go-nuts' ]
channels: [ '#stuff' ]
});
});
@ -35,7 +35,8 @@ channelActions.joined.listen(function(user, server, channel) {
server: server,
from: '',
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,
from: '',
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);
});
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({

View File

@ -1,15 +1,14 @@
var React = require('react');
var TabList = require('./TabList.jsx');
var MessageBox = require('./MessageBox.jsx');
var UserList = require('./UserList.jsx');
var Chat = require('./Chat.jsx');
var App = React.createClass({
render: function() {
return (
<div>
<TabList />
<MessageBox />
<UserList />
<Chat />
</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 Reflux = require('reflux');
var _ = require('lodash');
var messageStore = require('../stores/message.js');
var selectedTabStore = require('../stores/selectedTab.js');
@ -32,7 +33,13 @@ var MessageBox = React.createClass({
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>;
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 (

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

View File

@ -1,4 +1,5 @@
var EventEmitter = require('events').EventEmitter;
var _ = require('lodash');
var sockets = {};
@ -8,7 +9,7 @@ function createSocket(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 }));

View File

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

View File

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

View File

@ -1,11 +1,15 @@
var Reflux = require('reflux');
var _ = require('lodash');
var actions = require('../actions/tab.js');
var channelActions = require('../actions/channel.js');
var selectedTab = {};
var selectedTabStore = Reflux.createStore({
init: function() {
this.listenToMany(actions);
this.listenTo(channelActions.part, 'part');
},
select: function(server, channel) {
@ -14,6 +18,19 @@ var selectedTabStore = Reflux.createStore({
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() {
return selectedTab;
}

View File

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

View File

@ -1,6 +1,7 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
@ -9,6 +10,14 @@ body {
color: #FFF;
}
input {
font: 16px Inconsolata, sans-serif;
background: rgba(0,0,0,0.25);
color: #FFF;
outline: none;
border: none;
}
p {
line-height: 1.5;
}
@ -19,7 +28,7 @@ p {
top: 0;
bottom: 0;
right: 200px;
padding: 20px;
padding: 15px;
overflow: auto;
}
@ -31,23 +40,64 @@ p {
color: #AAA;
}
.messagebox {
.chat-title-bar {
position: fixed;
left: 200px;
top: 0;
bottom: 0;
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;
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 {
position: fixed;
top: 0;
bottom: 0;
bottom: 50px;
right: 0;
width: 200px;
padding: 20px;
padding: 15px;
overflow: auto;
}

View File

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

View File

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

View File

@ -5,63 +5,98 @@ import (
)
type ChannelStore struct {
data map[string]map[string][]string
lock sync.Mutex
users map[string]map[string][]string
userLock sync.Mutex
topic map[string]map[string]string
topicLock sync.Mutex
}
func NewChannelStore() *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 {
c.lock.Lock()
c.userLock.Lock()
users := make([]string, len(c.data[server][channel]))
copy(users, c.data[server][channel])
users := make([]string, len(c.users[server][channel]))
copy(users, c.users[server][channel])
c.lock.Unlock()
c.userLock.Unlock()
return users
}
func (c *ChannelStore) SetUsers(users []string, server, channel string) {
c.lock.Lock()
c.userLock.Lock()
if _, ok := c.data[server]; !ok {
c.data[server] = make(map[string][]string)
if _, ok := c.users[server]; !ok {
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) {
c.lock.Lock()
c.userLock.Lock()
if _, ok := c.data[server]; !ok {
c.data[server] = make(map[string][]string)
if _, ok := c.users[server]; !ok {
c.users[server] = make(map[string][]string)
}
if users, ok := c.data[server][channel]; ok {
c.data[server][channel] = append(users, user)
if users, ok := c.users[server][channel]; ok {
c.users[server][channel] = append(users, user)
} 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) {
c.lock.Lock()
defer c.lock.Unlock()
c.userLock.Lock()
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 {
users := c.data[server][channel]
c.data[server][channel] = append(users[:i], users[i+1:]...)
users := c.users[server][channel]
c.users[server][channel] = append(users[:i], users[i+1:]...)
return
}
}

View File

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

View File

@ -48,6 +48,7 @@ func handleWS(ws *websocket.Conn) {
channels := session.user.GetChannels()
for i, channel := range channels {
channels[i].Users = channelStore.GetUsers(channel.Server, channel.Name)
channels[i].Topic = channelStore.GetTopic(channel.Server, channel.Name)
}
session.sendJSON("channels", channels)
@ -106,6 +107,17 @@ func handleWS(ws *websocket.Conn) {
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":
var data Chat