Render chat

This commit is contained in:
Mattias Erming 2014-06-30 03:20:54 +02:00
parent 4ef13d6a18
commit f3f3858663
17 changed files with 579 additions and 219 deletions

View File

@ -1,4 +1,5 @@
module.exports = function(grunt) { module.exports = function(grunt) {
var components = "";
var files = [ var files = [
"./lib/**/*.js", "./lib/**/*.js",
"./client/js/shout.js" "./client/js/shout.js"
@ -14,7 +15,10 @@ module.exports = function(grunt) {
uglify: { uglify: {
js: { js: {
files: { files: {
"client/js/components.min.js": ["client/components/*.js"] "client/js/components.min.js": [
"client/components/*.js",
"client/components/jquery/*.js"
]
} }
} }
} }

256
client/components/jquery/tabcomplete.js vendored Normal file
View File

@ -0,0 +1,256 @@
/*!
* tabcomplete
* http://github.com/erming/tabcomplete
* v1.3.1
*/
(function($) {
var keys = {
backspace: 8,
tab: 9,
up: 38,
down: 40
};
$.tabcomplete = {};
$.tabcomplete.defaultOptions = {
after: "",
arrowKeys: false,
caseSensitive: false,
hint: "placeholder",
minLength: 1
};
$.fn.tab = // Alias
$.fn.tabcomplete = function(args, options) {
if (this.length > 1) {
return this.each(function() {
$(this).tabcomplete(args, options);
});
}
// Only enable the plugin on <input> and <textarea> elements.
var tag = this.prop("tagName");
if (tag != "INPUT" && tag != "TEXTAREA") {
return;
}
// Set default options.
options = $.extend(
$.tabcomplete.defaultOptions,
options
);
// Remove any leftovers.
// This allows us to override the plugin if necessary.
this.unbind(".tabcomplete");
this.prev(".hint").remove();
var self = this;
var backspace = false;
var i = -1;
var words = [];
var last = "";
var hint = $.noop;
// Determine what type of hinting to use.
switch (options.hint) {
case "placeholder":
hint = placeholder;
break;
case "select":
hint = select;
break;
}
this.on("input.tabcomplete", function() {
var input = self.val();
var word = input.split(/ |\n/).pop();
// Reset iteration.
i = -1;
last = "";
words = [];
// Check for matches if the current word is the last word.
if (self[0].selectionStart == input.length
&& word.length) {
if (typeof args === "function") {
// If the user supplies a function, invoke it
// and keep the result.
words = args(word);
} else {
// Otherwise, call the .match() function.
words = match(word, args, options.caseSensitive);
}
// Append 'after' to each word.
if (options.after) {
words = $.map(words, function(w) { return w + options.after; });
}
}
// Emit the number of matching words with the 'match' event.
self.trigger("match", words.length);
if (options.hint) {
if (!(options.hint == "select" && backspace) && word.length >= options.minLength) {
// Show hint.
hint.call(self, words[0]);
} else {
// Clear hinting.
// This call is needed when using backspace.
hint.call(self, "");
}
}
if (backspace) {
backspace = false;
}
});
this.on("keydown.tabcomplete", function(e) {
var key = e.which;
if (key == keys.tab
|| (options.arrowKeys && (key == keys.up || key == keys.down))) {
// Don't lose focus on tab click.
e.preventDefault();
// Iterate the matches with tab and the up and down keys by incrementing
// or decrementing the 'i' variable.
if (key != keys.up) {
i++;
} else {
if (i == -1) return;
if (i == 0) {
// Jump to the last word.
i = words.length - 1;
} else {
i--;
}
}
// Get next match.
var word = words[i % words.length];
if (!word) {
return;
}
var value = self.val();
last = last || value.split(/ |\n/).pop();
// Return if the 'minLength' requirement isn't met.
if (last.length < options.minLength) {
return;
}
// Update element with the completed text.
var text = value.substr(0, self[0].selectionStart - last.length) + word;
self.val(text);
// Put the cursor at the end after completion.
// This isn't strictly necessary, but solves an issue with
// Internet Explorer.
if (options.hint == "select") {
self[0].selectionStart = text.length;
}
// Remember the word until next time.
last = word;
// Emit event.
self.trigger("tabcomplete", last);
if (options.hint) {
// Turn off any additional hinting.
hint.call(self, "");
}
} else if (e.which == keys.backspace) {
// Remember that backspace was pressed. This is used
// by the 'input' event.
backspace = true;
// Reset iteration.
i = -1;
last = "";
}
});
if (options.hint) {
// If enabled, turn on hinting.
hint.call(this, "");
}
return this;
}
// Simple matching.
// Filter the array and return the items that begins with 'word'.
function match(word, array, caseSensitive) {
return $.grep(
array,
function(w) {
if (caseSensitive) {
return !w.indexOf(word);
} else {
return !w.toLowerCase().indexOf(word.toLowerCase());
}
}
);
}
// Show placeholder text.
// This works by creating a copy of the input and placing it behind
// the real input.
function placeholder(word) {
var input = this;
var clone = input.prev(".hint");
input.css({
backgroundColor: "transparent",
position: "relative",
});
// Lets create a clone of the input if it does
// not already exist.
if (!clone.length) {
input.wrap(
$("<div>").css({position: "relative", height: input.css("height")})
);
clone = input
.clone()
.attr("tabindex", -1)
.removeAttr("id name placeholder")
.addClass("hint")
.insertBefore(input);
clone.css({
position: "absolute",
});
}
var hint = "";
if (typeof word !== "undefined") {
var value = input.val();
hint = value + word.substr(value.split(/ |\n/).pop().length);
}
clone.val(hint);
}
// Hint by selecting part of the suggested word.
function select(word) {
var input = this;
var value = input.val();
if (word) {
input.val(
value
+ word.substr(value.split(/ |\n/).pop().length)
);
// Select hint.
input[0].selectionStart = value.length;
}
}
})(jQuery);

View File

@ -61,14 +61,14 @@ button {
background: #323841; background: #323841;
color: #fff; color: #fff;
} }
#channels { #networks {
min-height: 100%; min-height: 100%;
padding: 30px 40px 80px; padding: 30px 40px 80px;
} }
#channels .network + .network { #networks .network + .network {
margin-top: 30px; margin-top: 30px;
} }
#channels .chan { #networks .chan {
display: block; display: block;
margin: 1px -10px; margin: 1px -10px;
padding: 6px 10px 8px; padding: 6px 10px 8px;
@ -77,12 +77,12 @@ button {
transition: all .2s; transition: all .2s;
width: 160px; width: 160px;
} }
#channels .chan:first-child { #networks .chan:first-child {
color: #84d1ff; color: #84d1ff;
font-size: 15px; font-size: 15px;
font-weight: bold; font-weight: bold;
} }
#channels .badge { #networks .badge {
background: rgba(255, 255, 255, .06); background: rgba(255, 255, 255, .06);
border-radius: 3px; border-radius: 3px;
color: #afb6c0; color: #afb6c0;
@ -93,6 +93,9 @@ button {
right: 10px; right: 10px;
transition: all .1s; transition: all .1s;
} }
#networks .badge:empty {
display: none;
}
#footer { #footer {
height: 80px; height: 80px;
line-height: 80px; line-height: 80px;
@ -163,17 +166,10 @@ button {
position: relative; position: relative;
width: 100%; width: 100%;
} }
#chat form {
bottom: 0;
height: 40px;
left: 0;
position: absolute;
right: 180px;
}
#chat button:hover { #chat button:hover {
opacity: .6; opacity: .6;
} }
#chat .chat { #chat .window {
bottom: 40px; bottom: 40px;
left: 0; left: 0;
position: absolute; position: absolute;
@ -220,17 +216,30 @@ button {
} }
#messages .from { #messages .from {
background: #f9f9f9; background: #f9f9f9;
color: #33b0f7; color: #ddd;
padding-right: 10px; padding-right: 10px;
text-align: right; text-align: right;
width: 134px; width: 134px;
} }
#messages .from button {
color: #33b0f7;
}
#messages .text { #messages .text {
padding-left: 10px; padding-left: 10px;
padding-right: 6px; padding-right: 6px;
} }
#messages .type { #messages .type {
color: #ccc; color: #ccc;
display: none;
}
#messages .join .type,
#messages .part .type,
#messages .mode .type,
#messages .nick .type,
#messages .kick .type,
#messages .quit .type,
#messages .quit .type {
display: inline;
} }
#meta { #meta {
border-bottom: 1px solid #e9ecef; border-bottom: 1px solid #e9ecef;
@ -244,6 +253,9 @@ button {
#meta .count { #meta .count {
color: #ccc; color: #ccc;
} }
#meta .type {
text-transform: capitalize;
}
#users { #users {
bottom: 0; bottom: 0;
overflow: auto; overflow: auto;
@ -256,7 +268,14 @@ button {
display: block; display: block;
line-height: 1.6em; line-height: 1.6em;
} }
#input { #form {
bottom: 0;
height: 40px;
left: 0;
position: absolute;
right: 180px;
}
#form input {
border: 0; border: 0;
border-top: 1px solid #e9ecef; border-top: 1px solid #e9ecef;
height: 100%; height: 100%;

View File

@ -18,20 +18,7 @@
<body> <body>
<aside id="sidebar"> <aside id="sidebar">
<div id="channels"> <div id="networks"></div>
<section class="network">
<button class="chan">
Network
</button>
<button class="chan active">
#channel
</button>
<button class="chan">
<span class="badge">16</span>
#chan
</button>
</section>
</div>
<footer id="footer"> <footer id="footer">
<button id="connect" class="active"></button> <button id="connect" class="active"></button>
<button id="settings"></button> <button id="settings"></button>
@ -44,52 +31,18 @@
<h1>#channel</h1> <h1>#channel</h1>
</header> </header>
<div id="windows"> <div id="windows">
<div id="chat"> <div id="chat"></div>
<div class="chat"> <form id="form" action="">
<div id="messages">
<div class="msg">
<span class="time">00:00</span>
<span class="from">
<button>foo</button>
</span>
<span class="text">
<em class="type">join</em>
</span>
</div>
<div class="msg">
<span class="time">00:00</span>
<span class="from">
<button>foo</button>
</span>
<span class="text">
<em class="type"></em>
Hello, world!
</span>
</div>
</div>
</div>
<aside class="sidebar">
<div id="meta">
<h1>#channel</h1>
<div class="count">2 users</div>
</div>
<div id="users">
<button>foo</button>
<button>bar</button>
</div>
</aside>
<form action="">
<input id="submit" tabindex="-1" type="submit"> <input id="submit" tabindex="-1" type="submit">
<input id="input"> <input id="input">
</form> </form>
</div> </div>
</div> </div>
</div>
<div id="templates"> <div id="templates">
<script type="text/html" class="networks"> <script type="text/html" class="networks">
{{#each networks}} {{#each networks}}
<section class="network"> <section class="network" data-id="{{id}}">
{{partial "channels"}} {{partial "channels"}}
</section> </section>
{{/each}} {{/each}}
@ -97,19 +50,35 @@
<script type="text/html" class="channels"> <script type="text/html" class="channels">
{{#each channels}} {{#each channels}}
<button class="chan"> <button class="chan" data-id="{{id}}" data-type="{{type}}">
<span class="badge"></span> <span class="badge"></span>
Network {{name}}
</button> </button>
{{/each}} {{/each}}
</script> </script>
<script type="text/html" class="chat">
<div class="window">
<div id="messages">
{{partial "messages"}}
</div>
</div>
<aside class="sidebar">
{{partial "users"}}
</aside>
</script>
<script type="text/html" class="users"> <script type="text/html" class="users">
<div id="meta"> <div id="meta">
<h1>{{name}}</h1> <h1>{{name}}</h1>
<div class="count"> <div class="count">
{{users.length}} {{#if users.length}}
users {{users.length}} users
{{else}}
<span class="type">
{{type}}
</span>
{{/if}}
</div> </div>
</div> </div>
<div id="users"> <div id="users">
@ -121,12 +90,16 @@
<script type="text/html" class="messages"> <script type="text/html" class="messages">
{{#each messages}} {{#each messages}}
<div class="msg"> <div class="msg {{type}}">
<span class="time"> <span class="time">
{{time}} {{time}}
</span> </span>
<span class="from"> <span class="from">
{{#if from}}
<button>{{from}}</button> <button>{{from}}</button>
{{else}}
//
{{/if}}
</span> </span>
<span class="text"> <span class="text">
<em class="type">{{type}}</em> <em class="type">{{type}}</em>
@ -138,7 +111,7 @@
</div> </div>
<script src="js/components.min.js"></script> <script src="js/components.min.js"></script>
<script src="js/shout.js"></script> <script src="js/chat.js"></script>
</body> </body>
</html> </html>

200
client/js/chat.js Normal file
View File

@ -0,0 +1,200 @@
$(function() {
var socket = io();
var commands = [
"/ame",
"/amsg",
"/close",
"/connect",
"/deop",
"/devoice",
"/disconnect",
"/invite",
"/join",
"/kick",
"/leave",
"/mode",
"/msg",
"/nick",
"/notice",
"/op",
"/part",
"/partall",
"/query",
"/quit",
"/raw",
"/say",
"/send",
"/server",
"/slap",
"/topic",
"/voice",
"/whoami",
"/whois"
];
var chat = $("#chat");
var networks = $("#networks");
var channels = [];
var activeChannel = null;
var tpl = [];
function render(name, data) {
tpl[name] = tpl[name] || Handlebars.compile($("#templates ." + name).html());
return tpl[name](data);
}
socket.on("auth", function(data) {
console.log(data);
});
socket.on("init", function(data) {
networks.empty()
channels = $.map(data.networks, function(n) {
return n.channels;
});
networks.append(
render("networks", {
networks: data.networks
})
);
networks.find(".chan")
.eq(0)
.trigger("click");
});
socket.on("join", function(data) {
channels.push(data.chan);
var network = networks
.find(".network[data-id='" + data.network + "']")
.eq(0);
network.append(
render("channels", {
channels: [data.chan]
})
);
});
socket.on("msg", function(data) {
var chan = find(data.chan);
if (typeof chan !== "undefined") {
chan.messages.push(data.msg);
if (isActive(chan)) {
chat.find("#messages").append(
render("messages", {
messages: [data.msg]
})
);
}
}
});
socket.on("network", function(data) {
networks.append(
render("networks", {
networks: [data.network]
})
);
});
socket.on("nick", function(data) {
console.log(data);
});
socket.on("part", function(data) {
console.log(data);
});
socket.on("quit", function(data) {
console.log(data);
});
socket.on("users", function(data) {
var chan = find(data.chan);
if (typeof chan !== "undefined") {
chan.users = data.users;
if (isActive(chan)) {
chat.find(".sidebar")
.html(render("users", chan));
}
}
});
networks.on("click", ".chan", function() {
var id = $(this).data("id");
var chan = find(id);
if (typeof chan !== "undefined") {
activeChannel = chan;
chat.html(
render("chat", chan)
);
}
});
var input = $("#input").tab(complete, {
hint: false
});
var form = $("#form").on("submit", function(e) {
e.preventDefault();
var value = input.val();
input.val("");
socket.emit("input", {
// ..
});
});
function isActive(chan) {
return activeChannel !== null && chan == activeChannel;
}
function find(id) {
return $.grep(channels, function(c) {
return c.id == id;
})[0];
}
function complete(word) {
return $.grep(
commands,
function(w) {
return !w.toLowerCase().indexOf(word.toLowerCase());
}
);
}
function escape(text) {
var e = {
"<": "&lt;",
">": "&gt;"
};
return text.replace(/[<>]/g, function (c) {
return e[c];
});
}
Handlebars.registerHelper(
"partial", function(id) {
return new Handlebars.SafeString(render(id, this));
}
);
Handlebars.registerHelper(
"uri", function(text) {
var urls = [];
text = URI.withinString(text, function(url) {
urls.push(url);
return "$(" + (urls.length - 1) + ")";
});
text = escape(text);
for (var i in urls) {
var url = escape(urls[i]);
text = text.replace(
"$(" + i + ")",
"<a href='" + url.replace(/^www/, "//www") + "' target='_blank'>" + url + "</a>"
);
}
return text;
}
);
});

File diff suppressed because one or more lines are too long

View File

@ -1,116 +0,0 @@
$(function() {
new Shout();
});
function Shout() {
var client = this;
var socket = io();
var events = [
"auth",
"init",
"join",
"msg",
"network",
"nick",
"part",
"quit",
"users"
].forEach(function(e) {
client[e].call(client, socket);
});
}
Shout.prototype.auth = function(socket) {
socket.on("auth", function(data) {
console.log(data);
});
};
Shout.prototype.init = function(socket) {
socket.on("init", function(data) {
console.log(data);
});
};
Shout.prototype.join = function(socket) {
socket.on("join", function(data) {
console.log(data);
});
};
Shout.prototype.msg = function(socket) {
socket.on("msg", function(data) {
console.log(data);
});
};
Shout.prototype.network = function(socket) {
socket.on("network", function(data) {
console.log(data);
});
};
Shout.prototype.nick = function(socket) {
socket.on("nick", function(data) {
console.log(data);
});
};
Shout.prototype.part = function(socket) {
socket.on("part", function(data) {
console.log(data);
});
};
Shout.prototype.quit = function(socket) {
socket.on("quit", function(data) {
console.log(data);
});
};
Shout.prototype.users = function(socket) {
socket.on("users", function(data) {
console.log(data);
});
};
var tpl = [];
function render(name, data) {
tpl[name] = tpl[name] || Handlebars.compile($("#templates ." + name).html());
return tpl[name](data);
}
function escape(text) {
var e = {
"<": "&lt;",
">": "&gt;"
};
return text.replace(/[<>]/g, function (c) {
return e[c];
});
}
Handlebars.registerHelper(
"partial", function(id) {
return new Handlebars.SafeString(render(id, this));
}
);
Handlebars.registerHelper(
"uri", function(text) {
var urls = [];
text = URI.withinString(text, function(url) {
urls.push(url);
return "$(" + (urls.length - 1) + ")";
});
text = escape(text);
for (var i in urls) {
var url = escape(urls[i]);
text = text.replace(
"$(" + i + ")",
"<a href='" + url.replace(/^www/, "//www") + "' target='_blank'>" + url + "</a>"
);
}
return text;
}
);

View File

@ -17,7 +17,7 @@ function Chan(attr) {
name: "", name: "",
messages: [], messages: [],
users: [] users: []
})); }, attr));
} }
Chan.prototype.sortUsers = function() { Chan.prototype.sortUsers = function() {

View File

@ -3,12 +3,13 @@ var Msg = require("../../models/msg");
module.exports = function(irc, network) { module.exports = function(irc, network) {
var client = this; var client = this;
irc.on("errors", function(data) { irc.on("errors", function(data) {
var lobby = network.channels[0];
var msg = new Msg({ var msg = new Msg({
type: Msg.Type.ERROR, type: Msg.Type.ERROR,
from: "*",
text: data.message, text: data.message,
}); });
client.emit("msg", { client.emit("msg", {
chan: lobby.id,
msg: msg msg: msg
}); });
if (!network.connected) { if (!network.connected) {

View File

@ -1,4 +1,5 @@
var _ = require("lodash"); var _ = require("lodash");
var Chan = require("../../models/chan");
var Msg = require("../../models/msg"); var Msg = require("../../models/msg");
var User = require("../../models/user"); var User = require("../../models/user");
@ -29,7 +30,7 @@ module.exports = function(irc, network) {
}); });
chan.messages.push(msg); chan.messages.push(msg);
client.emit("msg", { client.emit("msg", {
id: chan.id, chan: chan.id,
msg: msg msg: msg
}); });
}); });

View File

@ -1,4 +1,5 @@
var _ = require("lodash"); var _ = require("lodash");
var Msg = require("../../models/msg");
module.exports = function(irc, network) { module.exports = function(irc, network) {
var client = this; var client = this;

View File

@ -3,18 +3,17 @@ var Msg = require("../../models/msg");
module.exports = function(irc, network) { module.exports = function(irc, network) {
var client = this; var client = this;
irc.on("motd", function(data) { irc.on("motd", function(data) {
var lobby = network.channels[0]; //var lobby = network.channels[0];
data.motd.forEach(function(text) { //data.motd.forEach(function(text) {
var msg = new Msg({ // var msg = new Msg({
type: Msg.Type.MOTD, // type: Msg.Type.MOTD,
from: "*", // text: text
text: text // });
}); // lobby.messages.push(msg);
lobby.messages.push(msg); // client.emit("msg", {
client.emit("msg", { // chan: lobby.id,
chan: lobby.id, // msg: msg
msg: msg // });
}); //});
});
}); });
}; };

View File

@ -7,7 +7,6 @@ module.exports = function(irc, network) {
if (data["new"] == irc.me) { if (data["new"] == irc.me) {
var lobby = network.channels[0]; var lobby = network.channels[0];
var msg = new Msg({ var msg = new Msg({
from: "*",
text: "You're now known as " + data["new"], text: "You're now known as " + data["new"],
}); });
chan.messages.push(msg); chan.messages.push(msg);

View File

@ -4,9 +4,9 @@ module.exports = function(irc, network) {
var client = this; var client = this;
irc.on("notice", function(data) { irc.on("notice", function(data) {
var lobby = network.channels[0]; var lobby = network.channels[0];
var from = data.from || "*"; var from = data.from || "";
if (data.to == "*" || data.from.indexOf(".") !== -1) { if (data.to == "*" || data.from.indexOf(".") !== -1) {
from = "*"; from = "";
} }
var msg = new Msg({ var msg = new Msg({
type: Msg.Type.NOTICE, type: Msg.Type.NOTICE,

View File

@ -7,7 +7,6 @@ module.exports = function(irc, network) {
irc.write("PING " + network.host); irc.write("PING " + network.host);
var lobby = network.channels[0]; var lobby = network.channels[0];
var msg = new Msg({ var msg = new Msg({
from: "*",
text: "You're now known as " + data text: "You're now known as " + data
}); });
lobby.messages.push(msg); lobby.messages.push(msg);

View File

@ -29,7 +29,7 @@ module.exports = function(irc, network) {
var i = 0; var i = 0;
for (var k in data) { for (var k in data) {
var key = prefix[k]; var key = prefix[k];
if (!key || data[k].toString() == "") { if (!key || data[k].toString() === "") {
continue; continue;
} }
var msg = new Msg({ var msg = new Msg({

View File

@ -49,9 +49,7 @@ module.exports = function() {
sockets = io(http().use(http.static("client")).listen(config.port || 9000)); sockets = io(http().use(http.static("client")).listen(config.port || 9000));
sockets.on("connection", function(socket) { sockets.on("connection", function(socket) {
if (config.public) { if (config.public) {
var client = new Client({sockets: sockets}); auth.call(socket);
init(socket, client);
connect(client, {host: "irc.rizon.net"});
} else { } else {
init(socket); init(socket);
} }
@ -66,13 +64,29 @@ function init(socket, client) {
socket.on("input", function(data) { input(client, data); }); socket.on("input", function(data) { input(client, data); });
socket.join(client.id); socket.join(client.id);
socket.emit("init", { socket.emit("init", {
init: client.networks networks: client.networks
}); });
} }
} }
function auth() { function auth(data) {
var socket = this; var socket = this;
if (config.public) {
// Temporary:
var client = clients[0];
if (clients.length === 0) {
var client = new Client({sockets: sockets});
clients.push(client);
connect(client, {
host: "irc.freenode.org"
});
}
init(socket, client);
} else {
if (false) {
// ..
}
}
} }
function connect(client, args) { function connect(client, args) {
@ -80,25 +94,35 @@ function connect(client, args) {
host: args.host, host: args.host,
port: args.port || 6667 port: args.port || 6667
}; };
var stream = args.tls ? tls.connect(options) : net.connect(options); var stream = args.tls ? tls.connect(options) : net.connect(options);
stream.on("error", function(e) { stream.on("error", function(e) {
console.log(e); console.log(e);
}); });
var irc = slate(stream); var irc = slate(stream);
irc.nick("shout"); irc.nick("shout");
irc.user("shout", "Shout User"); irc.user("shout", "Shout User");
client.nick = "shout"; client.nick = "shout";
var network = new Network({ var network = new Network({
host: options.host, host: options.host,
irc: irc irc: irc
}); });
client.networks.push(network); client.networks.push(network);
client.emit("network", { client.emit("network", {
network: network network: network
}); });
events.forEach(function(plugin) { events.forEach(function(plugin) {
require("./plugins/irc-events/" + plugin).apply(client, [irc, network]); require("./plugins/irc-events/" + plugin).apply(client, [irc, network]);
}); });
irc.on("welcome", function() {
irc.join("#shout-test");
});
} }
function input(client, data) { function input(client, data) {