"use strict"; var mv = function(mv) { mv.connect = { connect: connect, join: join, part: part }; /* If we're updating due to something we received, then don't broadcast back. */ var netrendering = false; /* Our ID */ var id = 0; /* List of all users */ var users = {}; /* The user status box */ var usersStatus = d3.select("#users-status"); /* io.socket's socket */ var socket; function connect() { socket = io.connect(); /* These are still useful to troubleshoot */ socket.on("connect", function() { console.log("CONNECT", arguments); }); socket.on("disconnect", function() { console.log("DISCONNECT", arguments); d3.select("#connection-warning").style("display", "block"); }); /* We're evidently operating online, so show the status */ d3.select("#connect-status").style("display", "block"); /* Tell the server our starting filters */ socket.emit("filter", { filters: mv.charter.filters() }); /* * Protocol: * selflogin -> id (I) * login -> id (I), nick (S) * nickset -> id (I), nick (S) * join -> id (I), channel (I) * part -> id (I) * filterset -> id (I), filters ({dim (S) -> filter (*)}) * users -> { id -> { nick (S), channel (I), filters ({dim (S) -> filter (*)}) } } * logout -> id (I) */ socket.on('selflogin', function(d) { /* Acknowledged that we logged in */ /* Take note of our ID. */ id = d.id; }); socket.on('login', function(d) { /* Someone else logging in */ users[d.id] = { nick: d.nick, filters: {} }; updateUsersStatus(); }); socket.on('users', function(d) { /* We've got a list of all users. */ /* The server is always right. */ users = d.users; updateUsersStatus(); }); socket.on('nickset', function(d) { /* Someone, possibly us, changed their nick. */ users[d.id].nick = d.nick; updateUsersStatus(); }); socket.on('join', function(d) { users[d.id].channel = d.channel; updateUsersStatus(); }); socket.on('part', function(d) { delete users[d.id].channel; updateUsersStatus(); }); socket.on('filterset', function(d) { /* Someone changed their filter */ /* Ignore server sourced (ID: 0) changes */ if (d.id) { users[d.id].filters = d.filters; } /* Use the variable netrendering to denote that we're rendering due to a change received from the network. */ netrendering = true; setOwnFilters(d.filters); netrendering = false; }); socket.on('logout', function(d) { /* Someone disconnected, take them off the list. */ delete users[d.id]; updateUsersStatus(); }); /* * Remember the old renderlet * FIXME: Find a more elegant way to do this */ var f = dc.renderlet(); dc.renderlet(function() { /* Hook a listener into dc's rendering routine. If it rerenders, broadcast the change. */ /* Call the old renderlet */ f(); if (netrendering) { /* If we rendered due a change we received, don't broadcast it again. That would be A Bad Thing. */ return; } socket.emit("filter", { filters: mv.charter.filters() }); }); } function setOwnFilters(filters) { /* See if there's any difference - if there isn't, don't update. */ var change = false; var key; function filterCompare(key, l, r) { return ("filterCompare" in mv.charts[key]) ? mv.charts[key].filterCompare(l, r) : l == r; } /* Check for keys in the filters to apply which are not in our charts. */ for (key in filters) { if (!(key in mv.charts)) continue; /* The two filters to compare. */ var l = mv.charts[key].filter(); var r = filters[key]; if (l instanceof Array) { /* Crossfilter uses arrays to filter ranges. Exactly the first two elements are significant. */ if ((r instanceof Array) && filterCompare(key, l[0], r[0]) && filterCompare(key, l[1], r[1])) { continue; } } else if (!(r instanceof Array) && filterCompare(key, l, r)) { /* Exact filter */ continue; } /* This filter differs. Apply it. */ change = true; mv.charts[key].filter(r); } /* Check for keys in our charts which are not in the filters to apply. */ for (key in mv.charts) { if (mv.charts[key].filter() != null) { if (key in filters) { /* This has already been handled above */ continue; } /* There is no longer a filter applying here, clear it. */ change = true; mv.charts[key].filterAll(); } } if (change) { dc.redrawAll(); } } function join(channel) { if (channel != null) { socket.emit("join", channel); } else { socket.emit("join"); } } function part() { socket.emit("part"); } function updateUsersStatus() { /* Convert the user list to a form suitable for a d3 selection */ var groups = []; /* Track the groupless people separately. They should come at the end. */ var unchannelled = []; var channelById = {}; var channelNames = []; for (var uid in users) { users[uid].id = uid; if ("channel" in users[uid]) { if (!(users[uid].channel in channelById)) { groups.push(channelById[users[uid].channel] = []); channelNames.push(users[uid].channel); } channelById[users[uid].channel].push(users[uid]); } else { unchannelled.push(users[uid]); } } groups.push(unchannelled); var createpart = usersStatus.select(".createpart"); /* Link to part a channel we're in, or create a channel if we're not in one */ if (createpart.empty()) { createpart = usersStatus.append("a") .attr("class", "createpart") ; } if ("channel" in users[id]) { createpart .attr("href", "javascript:mv.connect.part();") .text("Part channel") ; } else { createpart .attr("href", "javascript:mv.connect.join();") .text("Create channel") ; } /* List of groups, with unchanneled users at the end */ var grouplist = usersStatus.selectAll(".group") .data(groups, function(d, i) { return channelNames[i]; }) ; grouplist .enter().append("div").attr("class", "group") ; /* Update */ grouplist .each(function(d, i) { var group = d3.select(this); var ul = group.select("ul"); if (ul.empty()) { ul = group.append("ul"); } /* * Hacky way to check if these are the users without a channel. * Feel free to FIXME! */ if (i != groups.length - 1) { group .attr("class", "group channel") ; var join = group.select(".join"); if (join.empty()) { join = group.append("a") .attr("class", "join") .text("Join channel") ; } if (users[id].channel == channelNames[i]) { /* We're in this channel */ join.style("display", "none"); } else { join .style("display", "inline") .attr("href", "javascript:mv.connect.join(" + channelNames[i] + ");") ; } } else { var join = group.select(".join"); group .attr("class", "group") ; if (join.empty()) { join.remove(); } } /* Update the list of users in this group */ var userlist = ul.selectAll(".user") .data(d, function(d) { return d.id; }) ; userlist .enter().append("li").attr("class", "user") ; userlist .each(function(d,i) { var elm = d3.select(this); var name = elm.select(".name"); var nick = d.nick == null ? "Anonymous User " + d.id : d.nick; if (d.id == id) { /* This is us! We can edit our name. */ if (name.empty()) { name = elm.append("input").attr("class", "name") .attr("type", "text") .attr("placeholder", "Enter name here") .on("change", function () { /* A d3 select must be dereferenced twice to access the DOM entity */ socket.emit("nick", { nick: name[0][0].value }); }) ; } name.attr("value", nick); } else { /* This is someone else. We can't edit their name. */ if (name.empty()) { name = elm.append("p").attr("class", "name"); } name.text(nick); } }) ; userlist .exit().remove() ; }) ; grouplist .exit().remove() ; } return mv; }(mv || {});