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 I was excited about to build a web-based chat client that could rival traditional desktop IRC applications.

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

Time
2012-2014
Source
Status
Archived

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.

14:23alice:
14:24bob:

Channel Management

Multi-server support with channel lists, user counts, and private message indicators. Click channels to switch and watch for new messages.

freenode
#javascript(45)3
#node.js(23)
#webdev(67)1
@alice

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.

app.js - Socket.io Connection Management
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();
Note

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.

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 });
util.js - IRC Protocol Message Handler
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 }
util.js - Dynamic Plugin System
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 };
util.js - Performance-Optimized Rendering
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 };