Subway
Overview
Throughout college my friends and I opted into using IRC as our primary means of communication. It was great for figuring out how to setup a client and to get it to run on a server, but the desktop clients felt clunky and weren't easily accessible from different machines.
These were my nascent days as a web developer and I wanted to see if I
could leverage the
This combined with the encouragement of Paul Irish led to me kicking off this project with ambitious goals: persistent message history, a plugin ecosystem, desktop notifications, and a modern web interface.
Sporadically over two years I built a working IRC client that not only persisted messages among different browser sessions but pioneered many patterns now common in modern chat applications. It featured dynamic plugin loading, sophisticated message rendering optimization, and comprehensive notification systems—all cutting-edge for web applications in 2012-2014.
Details
Interface



Interactive examples of the user interface and real-time chat functionality. Try clicking and interacting with the elements below.
Real-time Chat Interface
Live IRC chat with proper message formatting, timestamps, and user highlighting. Watch messages appear in real-time.
Channel Management
Multi-server support with channel lists, user counts, and private message indicators. Click channels to switch and watch for new messages.
Desktop Notifications
HTML5 notifications for mentions and private messages. Click to simulate a mention notification.
Interactive Code Examples
Explore the implementation details with interactive code examples. Click to expand sections, run simulations, and see the code in action.
Socket.io Connection Management
IRC Protocol Message Handling
Plugin Loading System
Message Rendering Queue
Technical Innovation
This implementation predated many modern web development patterns. The real-time messaging, plugin architecture, and performance optimizations demonstrated early adoption of techniques that later became standard in applications like Discord and Slack.
Code Implementation
Here are the actual code implementations for the key features demonstrated above. Click on line numbers to highlight specific sections and see detailed explanations.
1 | // Initial state of our app |
2 | app.initialized = false; |
3 | app.irc = new app.models.App(); |
4 | |
5 | // Display startup menu and handle connection |
6 | app.io.on("connect", function() { |
7 | var menu = new app.components.startMenu(); |
8 | menu.show(); |
9 | |
10 | // Restore previous session if user exists |
11 | if(app.user) { |
12 | $.post('restore_connection/', {socketid: app.io.io.engine.id}); |
Tip Session Restoration: Automatically reconnects users to their previous IRC sessions using the socket ID. This provided seamless experience across browser refreshes. | |
13 | } |
14 | |
15 | // Load configured plugins and apply highlighting |
16 | util.loadPlugins(app.settings.plugins); |
17 | util.highlightCss(); |
18 | }); |
19 | |
20 | app.io.on("restore_connection", function(data) { |
21 | app.initialized = true; |
22 | app.irc.set(_.omit(data, "connections")); |
23 | app.irc.set({ |
24 | connections: new app.collections.Connections(data.connections) |
Important State Management: Uses Backbone.js models to manage application state. The connection data is restored and IRC channels are rebuilt from server data. | |
25 | }); |
26 | }); |
1 | util.handle_irc = function(message, irc, app_ref) { |
2 | var conn = irc.get("connections"); |
3 | var server = conn.get(message.client_server); |
4 | |
5 | switch (message.rawCommand) { |
6 | case "PRIVMSG": |
7 | if (message.args[0][0] === "#") { |
8 | // Channel message |
9 | server.addMessage(message.args[0], { |
10 | from: message.nick, |
11 | text: message.args[1], |
12 | type: "PRIVMSG" |
13 | }); |
14 | } else { |
15 | // Private message |
16 | server.addMessage(message.nick, { |
17 | from: message.nick, |
18 | text: message.args[1], |
19 | type: "PRIVMSG" |
20 | }); |
21 | } |
Note IRC PRIVMSG Handling: Distinguishes between channel messages (starting with #) and private messages. This logic mirrors how traditional IRC clients work. | |
22 | break; |
23 | |
24 | case "JOIN": |
25 | if(message.nick === app.irc.getActiveNick()) { |
26 | server.addChannel(message.args[0]); |
27 | app.irc.set("active_channel", message.args[0]); |
28 | } else { |
29 | server.addMessage(message.args[0], {type: "JOIN", nick: message.nick}); |
30 | var channel = server.get("channels").get(message.args[0]); |
31 | channel.get("users").add({nick: message.nick}); |
32 | } |
Tip Channel Join Logic: When a user joins a channel, the client updates its local state. If it's the current user, it switches to that channel automatically. | |
33 | break; |
34 | |
35 | case "PART": |
36 | if(message.nick === server.get("nick")) { |
37 | server.get("channels").remove(message.args[0]); |
38 | app.irc.set("active_channel", "status"); |
39 | } else { |
40 | server.addMessage(message.args[0], {type: "PART", nick: message.nick}); |
41 | var channel = server.get("channels").get(message.args[0]); |
42 | channel.get("users").remove(message.nick); |
43 | } |
Important User Management: Tracks when users leave channels and updates the user lists accordingly. This maintains accurate channel state. | |
44 | break; |
45 | } |
46 | } |
1 | util.loadPlugin = function(plugin, cb) { |
2 | var gist_id = plugin.split("/")[1]; |
3 | var base_url = "plugin_cache/" + gist_id + "/"; |
4 | |
5 | $.get(base_url + "plugin.json", function(data) { |
6 | // Dynamically inject plugin JavaScript and CSS |
7 | util.embedJs(base_url + "plugin.js"); |
8 | util.embedCss(base_url + "plugin.css"); |
9 | |
10 | // Register plugin in the system |
11 | app.plugin_data[data.pluginId] = data; |
12 | app.settings.active_plugins.push(data.pluginId); |
13 | |
14 | if (cb) cb.call(this); |
15 | }); |
Note GitHub Gist Integration: Plugins are hosted as GitHub Gists, making them easy to share and update. The system fetches metadata and injects the code dynamically. | |
16 | }; |
17 | |
18 | util.applyPlugins = function(text) { |
19 | var listeners = []; |
20 | _.each(app.settings.active_plugins, function(pluginName) { |
21 | var pluginMethod = app.plugins[pluginName]; |
22 | var args = pluginMethod(text); |
23 | |
24 | if (typeof args === "string") { |
25 | text = args; |
26 | } else { |
27 | text = args.text; |
28 | if (args.listener) listeners.push(args.listener); |
29 | } |
30 | }); |
Tip Plugin Pipeline: Messages pass through all active plugins in sequence. Each plugin can transform text and register event listeners for interactive features. | |
31 | return {text: text, listeners: listeners}; |
32 | }; |
33 | |
34 | util.embedJs = function(js, isRaw) { |
35 | var output_js = ""; |
36 | if(isRaw) { |
37 | output_js = '<script>' + js + "</script>"; |
38 | } else { |
39 | output_js = '<script src="' + js + '"></script>'; |
40 | } |
Warning Dynamic Code Injection: JavaScript and CSS are injected directly into the page DOM. This was an innovative approach for 2012 web development. | |
41 | $("body").append(output_js); |
42 | }; |
1 | util.renderQueue = { |
2 | queue: [], |
3 | |
4 | pushQueue: function(message) { |
5 | this.queue.push(message); |
6 | |
Note Queue Management: Messages are added to a queue instead of being rendered immediately. This prevents UI freezing during high-volume chat periods. | |
7 | if(this.queueInt === undefined) { |
8 | this.queueInt = setInterval(function() { |
9 | // Process messages in batches to prevent UI blocking |
10 | for(x=0; x<10; x++) { |
11 | if(_this.queue.length === 0) break; |
12 | |
13 | var entry = _this.queue.pop(); |
14 | var processedText = entry.getModel().getText(); |
15 | |
16 | // Preserve scroll position for users at bottom |
17 | var mess = document.getElementsByClassName("messages")[0]; |
18 | var is_at_bottom = mess.scrollTop + mess.offsetHeight === mess.scrollHeight; |
19 | |
20 | $(entry.getDOMNode()).find(".messageText").html(processedText.text); |
21 | |
22 | if(is_at_bottom) { |
23 | mess.scrollTop = mess.scrollHeight; |
24 | } |
25 | |
26 | entry.attachListeners(processedText); |
27 | } |
Tip Batch Processing: Processes 10 messages every 50ms. This balance prevents UI blocking while maintaining responsive message delivery - a sophisticated optimization for 2012. | |
28 | }, 50); // 50ms intervals |
29 | } |
30 | }, |
31 | |
32 | clearQueue: function() { |
33 | clearInterval(this.queueInt); |
34 | this.queueInt = undefined; |
35 | this.queue = []; |
36 | } |
37 | }; |
Socket.io Event Handling: The application listens for connection events and initializes the UI. This pattern was cutting-edge in 2012 when WebSocket support was limited.