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) {
var components = "";
var files = [
"./lib/**/*.js",
"./client/js/shout.js"
@ -14,7 +15,10 @@ module.exports = function(grunt) {
uglify: {
js: {
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;
color: #fff;
}
#channels {
#networks {
min-height: 100%;
padding: 30px 40px 80px;
}
#channels .network + .network {
#networks .network + .network {
margin-top: 30px;
}
#channels .chan {
#networks .chan {
display: block;
margin: 1px -10px;
padding: 6px 10px 8px;
@ -77,12 +77,12 @@ button {
transition: all .2s;
width: 160px;
}
#channels .chan:first-child {
#networks .chan:first-child {
color: #84d1ff;
font-size: 15px;
font-weight: bold;
}
#channels .badge {
#networks .badge {
background: rgba(255, 255, 255, .06);
border-radius: 3px;
color: #afb6c0;
@ -93,6 +93,9 @@ button {
right: 10px;
transition: all .1s;
}
#networks .badge:empty {
display: none;
}
#footer {
height: 80px;
line-height: 80px;
@ -163,17 +166,10 @@ button {
position: relative;
width: 100%;
}
#chat form {
bottom: 0;
height: 40px;
left: 0;
position: absolute;
right: 180px;
}
#chat button:hover {
opacity: .6;
}
#chat .chat {
#chat .window {
bottom: 40px;
left: 0;
position: absolute;
@ -220,17 +216,30 @@ button {
}
#messages .from {
background: #f9f9f9;
color: #33b0f7;
color: #ddd;
padding-right: 10px;
text-align: right;
width: 134px;
}
#messages .from button {
color: #33b0f7;
}
#messages .text {
padding-left: 10px;
padding-right: 6px;
}
#messages .type {
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 {
border-bottom: 1px solid #e9ecef;
@ -244,6 +253,9 @@ button {
#meta .count {
color: #ccc;
}
#meta .type {
text-transform: capitalize;
}
#users {
bottom: 0;
overflow: auto;
@ -256,7 +268,14 @@ button {
display: block;
line-height: 1.6em;
}
#input {
#form {
bottom: 0;
height: 40px;
left: 0;
position: absolute;
right: 180px;
}
#form input {
border: 0;
border-top: 1px solid #e9ecef;
height: 100%;

View File

@ -18,20 +18,7 @@
<body>
<aside id="sidebar">
<div id="channels">
<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>
<div id="networks"></div>
<footer id="footer">
<button id="connect" class="active"></button>
<button id="settings"></button>
@ -44,52 +31,18 @@
<h1>#channel</h1>
</header>
<div id="windows">
<div id="chat">
<div class="chat">
<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="input">
</form>
</div>
<div id="chat"></div>
<form id="form" action="">
<input id="submit" tabindex="-1" type="submit">
<input id="input">
</form>
</div>
</div>
<div id="templates">
<script type="text/html" class="networks">
{{#each networks}}
<section class="network">
<section class="network" data-id="{{id}}">
{{partial "channels"}}
</section>
{{/each}}
@ -97,19 +50,35 @@
<script type="text/html" class="channels">
{{#each channels}}
<button class="chan">
<button class="chan" data-id="{{id}}" data-type="{{type}}">
<span class="badge"></span>
Network
{{name}}
</button>
{{/each}}
</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">
<div id="meta">
<h1>{{name}}</h1>
<div class="count">
{{users.length}}
users
{{#if users.length}}
{{users.length}} users
{{else}}
<span class="type">
{{type}}
</span>
{{/if}}
</div>
</div>
<div id="users">
@ -121,12 +90,16 @@
<script type="text/html" class="messages">
{{#each messages}}
<div class="msg">
<div class="msg {{type}}">
<span class="time">
{{time}}
</span>
<span class="from">
{{#if from}}
<button>{{from}}</button>
{{else}}
//
{{/if}}
</span>
<span class="text">
<em class="type">{{type}}</em>
@ -138,7 +111,7 @@
</div>
<script src="js/components.min.js"></script>
<script src="js/shout.js"></script>
<script src="js/chat.js"></script>
</body>
</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: "",
messages: [],
users: []
}));
}, attr));
}
Chan.prototype.sortUsers = function() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -29,7 +29,7 @@ module.exports = function(irc, network) {
var i = 0;
for (var k in data) {
var key = prefix[k];
if (!key || data[k].toString() == "") {
if (!key || data[k].toString() === "") {
continue;
}
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.on("connection", function(socket) {
if (config.public) {
var client = new Client({sockets: sockets});
init(socket, client);
connect(client, {host: "irc.rizon.net"});
auth.call(socket);
} else {
init(socket);
}
@ -66,13 +64,29 @@ function init(socket, client) {
socket.on("input", function(data) { input(client, data); });
socket.join(client.id);
socket.emit("init", {
init: client.networks
networks: client.networks
});
}
}
function auth() {
function auth(data) {
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) {
@ -80,25 +94,35 @@ function connect(client, args) {
host: args.host,
port: args.port || 6667
};
var stream = args.tls ? tls.connect(options) : net.connect(options);
stream.on("error", function(e) {
console.log(e);
});
var irc = slate(stream);
irc.nick("shout");
irc.user("shout", "Shout User");
client.nick = "shout";
var network = new Network({
host: options.host,
irc: irc
});
client.networks.push(network);
client.emit("network", {
network: network
});
events.forEach(function(plugin) {
require("./plugins/irc-events/" + plugin).apply(client, [irc, network]);
});
irc.on("welcome", function() {
irc.join("#shout-test");
});
}
function input(client, data) {