1 <html> 2 <head> 3 <title>ChromiumIRC</title> 4 <link rel="stylesheet" type="text/css" href="styles.css"> 5 <script src="jstemplate/util.js" type="text/javascript"></script> 6 <script src="jstemplate/jsevalcontext.js" type="text/javascript"></script> 7 <script src="jstemplate/jstemplate.js" type="text/javascript"></script> 8 <script src="util.js" type="text/javascript"></script> 9 <script lang="JavaScript" src="irc.js"></script> 10 <script> 11 12 var ircConnections = {}; 13 14 // The server & channel configutation data is stored in localStorage.servers. 15 // These are setters and getters for this structure. 16 function servers() { 17 return JSON.parse(localStorage.servers || "[]"); 18 } 19 function setServers(servers) { 20 localStorage.servers = JSON.stringify(servers); 21 } 22 23 // Channel list is a sorted list of "server#channel" strings. This maps to 24 // channel slides as represented in the UI. 25 function channelList() { 26 var channelList = []; 27 servers().forEach(function(server) { 28 server.channels = server.channels || []; 29 server.channels.forEach(function(channel) { 30 channelList.push(server.name + channel); 31 }); 32 }); 33 34 channelList.sort(); 35 return channelList; 36 } 37 38 window.onload = function() { 39 // Setup notifications. 40 window.onfocus = function() { 41 windowHasFocus = true; 42 clearNotifications(); 43 } 44 window.onblur = function() { 45 windowHasFocus = false; 46 } 47 48 syncChannelList(); 49 50 // Setup channel navigation and message entry. 51 function handleBodyKeyDown(event) { 52 switch (event.keyCode) { 53 case 37: // left arrow 54 slideTo(-1); 55 break; 56 case 39: // right arrow 57 slideTo(1); 58 break; 59 } 60 } 61 document.body.addEventListener('keydown', handleBodyKeyDown, false); 62 63 // We don't want left & right arrow inside the text entry to move the channel 64 // slides. 65 $('typingDiv').addEventListener('keydown', function(event) { 66 event.stopPropagation(); 67 }); 68 $('entryText').addEventListener('keydown', function(event) { 69 if (event.keyCode == 13) { // RETURN key. 70 processEntryMessage(); 71 } 72 }); 73 74 servers().forEach(addServerConnection); 75 }; 76 77 window.onunload = function() { 78 ircConnections.forEach(function(irc) { 79 irc.disconnect(); 80 }); 81 } 82 83 function addServerConnection(server) { 84 var ws = new WebSocket("ws://" + location.host + "/ws"); 85 var irc = new IRCConnection(server.name, server.port, server.nick, 86 ws.send.bind(ws), // sendFunc 87 ws.close.bind(ws)); // closeFunc 88 ws.onopen = irc.onOpened.bind(irc); 89 ws.onclose = irc.onClosed.bind(irc); 90 ws.onmessage = function(message) { 91 irc.onMessage(message.data); 92 }; 93 irc.onConnect = function(message) { 94 server.channels.forEach(function(channel) { 95 ircConnections[server.name].joinChannel(channel); 96 }); 97 }; 98 irc.onDisconnect = function(message) { 99 }; 100 irc.onText = function(channel, nick, message) { 101 checkForNickReference(server, channel, nick, message); 102 addMessage(server.name, channel, nick, new Date(), message); 103 }; 104 105 ircConnections[server.name] = irc; 106 } 107 108 function joinChannel(serverName, channelName) { 109 ircConnections[serverName].joinChannel(channelName); 110 } 111 112 function removeChannelListener(channelName) { 113 return function(event) { 114 event.stopPropagation(); 115 116 var servers = servers(); 117 servers.forEach(function(server) { 118 if (channelName.indexOf(server.name) == 0) { 119 for (var i = 0; server.channels.length; i++) { 120 if (channelName == server.name + server.channels[i]) { 121 ircConnections[server.name].quitChannel(server.channels[i]); 122 server.channels.splice(i, 1); 123 break; 124 } 125 } 126 } 127 }); 128 129 setServers(servers); 130 syncChannelList(); 131 }; 132 } 133 134 function syncChannelList() { 135 var channels = channelList(); 136 var channelSlides = $('channelSlides'); 137 var channelSlideProto = $('channelSlideProto'); 138 139 var channelIndex = 0; 140 var slideIndex = 0; 141 142 while(channelIndex < channels.length || 143 channels.length != channelSlides.children.length) { 144 var channel = channels[channelIndex]; 145 var slide = channelSlides.children[slideIndex]; 146 147 if (slideIndex == channelSlides.children.length || 148 channel < slideChannel(slide)) { 149 // Add a new slide. 150 var newSlide = channelSlideProto.cloneNode(true); 151 jstProcess(new JsEvalContext({ name: channel }), newSlide); 152 newSlide.setAttribute("id", "channel-" + channel); 153 newSlide.style.display = ""; 154 if (slideIndex == channelSlides.children.length) { 155 channelSlides.appendChild(newSlide); 156 } else { 157 channelSlides.insertBefore(newSlide, slide); 158 } 159 newSlide.addEventListener('click', onClickMoveSlide); 160 childNodeWithClass(newSlide, "removeButton") 161 .addEventListener('click', removeChannelListener(channel)); 162 163 slide = newSlide; 164 } else if (!channel || channel > slideChannel(slide)) { 165 // Delete a removed slide. 166 167 // If the removed slide is the current slide, we have to pick a new 168 // current slide. 169 if (localStorage.currentSlide == slideChannel(slide)) { 170 if (slide.nextSibling) { 171 localStorage.currentSlide = slideChannel(slide.nextSibling); 172 } else if (channels.length == 0) { 173 localStorage.currentSlide = ""; 174 } else if (slideIndex < channelSlides.children.length) { 175 localStorage.currentSlide = 176 slideChannel(channelSlides.children[slideIndex - 1]); 177 } 178 } 179 channelSlides.removeChild(slide); 180 } else { 181 channelIndex++; 182 slideIndex++; 183 } 184 185 slide.setAttribute("slide", "" + slideIndex - 1); 186 } 187 188 slideTo(); 189 } 190 191 function processEntryMessage() { 192 var message = $('entryText').value; 193 $('entryText').value = ""; 194 195 if (!localStorage.currentSlide) { 196 alert('No current channel'); 197 return; 198 } 199 200 var server; 201 var channel; 202 var nick; 203 servers().forEach(function(s) { 204 if (localStorage.currentSlide.indexOf(s.name) == 0) { 205 server = s.name; 206 nick = s.nick; 207 s.channels.forEach(function(c) { 208 if (localStorage.currentSlide == s.name + c) { 209 channel = c; 210 } 211 }); 212 } 213 }); 214 215 addMessage(server, channel, nick, new Date(), message); 216 ircConnections[server].sendMessage([channel], message); 217 } 218 219 function addMessage(server, channel, nick, time, body) { 220 messageLine = childNodeWithClass($('channelSlideProto'), "messageLine"); 221 var newMessageLine = messageLine.cloneNode(true); 222 223 jstProcess(new JsEvalContext({ 224 'nick': nick, 225 'time': time, 226 'body': body 227 }), newMessageLine); 228 newMessageLine.style.display = ""; 229 230 var messageList = 231 childNodeWithClass($("channel-" + server + channel), "messageList"); 232 messageList.appendChild(newMessageLine); 233 } 234 235 function formatTime(time) { 236 return ""; 237 } 238 239 /** 240 * Slide Navigation. 241 */ 242 243 // Returns the server#channel string value for a given |slide| element. 244 function slideChannel(slide) { 245 return childNodeWithClass(slide, "channel").innerText; 246 } 247 248 // Handler for clicking on the visible portions of the previous & next slides. 249 function onClickMoveSlide() { 250 if (localStorage.currentSlide != slideChannel(this)) { 251 localStorage.currentSlide = slideChannel(this); 252 slideTo(); 253 } 254 } 255 256 // Handles navigating between the channel slides. If |slideDelta| is given, 257 // it should specify the number of slides to move left (negative value) or right 258 // positive value. If |slideDelta| is not provided, It ensures that 259 // |localStorage.currentSlide| is navigated to. 260 function slideTo(slideDelta) { 261 var slide; 262 var slideNumber; 263 264 if (localStorage.currentSlide) { 265 slide = document.getElementById("channel-" + localStorage.currentSlide); 266 if (slide) { 267 slideNumber = parseInt(slide.getAttribute("slide")); 268 } 269 } 270 if (isNaN(slideNumber) || !slide) { 271 slideNumber = 0; 272 } 273 if (typeof(slideDelta) == "number") { 274 slideNumber += slideDelta; 275 } 276 277 var slides = document.getElementsByClassName("channelSlide"); 278 if (slideNumber < 0 || slideNumber == slides.length - 1) { 279 return; 280 } 281 282 for (var i = 0; i < slides.length; i++) { 283 var slide = slides[i]; 284 var slideIndex = parseInt(slide.getAttribute("slide")) - slideNumber; 285 286 if (slideIndex <= -2) { 287 slide.className = "channelSlide far-left"; 288 } 289 if (slideIndex >= 2) { 290 slide.className = "channelSlide far-right"; 291 } 292 293 switch(slideIndex) { 294 case -1: 295 slide.className = "channelSlide left"; 296 break; 297 case 0: 298 slide.className = "channelSlide center"; 299 localStorage.currentSlide = slideChannel(slide); 300 break; 301 case 1: 302 slide.className = "channelSlide right"; 303 break; 304 } 305 } 306 307 clearNotifications(); 308 } 309 310 /** 311 * Notifications 312 */ 313 var windowHasFocus = false; 314 var notifications = {}; 315 316 function clearNotifications() { 317 for (property in notifications) { 318 notifications[property].cancel(); 319 } 320 321 notifications = {}; 322 } 323 324 function checkForNickReference(server, channel, nick, message) { 325 if (windowHasFocus || !message || message.indexOf(server.nick) < 0) { 326 return; 327 } 328 329 // Notifications will be enabled by the app install. Otherwise, don't notity. 330 if (window.webkitNotifications.checkPermission() != 0) { 331 return; 332 } 333 334 // Remove a previous notification from the same channel. Show the newer one. 335 if (notifications[server.name + channel]) { 336 notifications[server.name + channel].cancel(); 337 } 338 339 var title = "On " + server.name + channel; 340 var icon = "http://www.google.com/favicon.ico"; 341 var text = nick + ": " + message; 342 var url = location.protocol + "//" + location.host + "/notification.html"; 343 url += "?title=" + encodeURIComponent(title) + 344 "&content=" + encodeURIComponent(text); 345 346 var n = window.webkitNotifications.createHTMLNotification(url); 347 n.ondisplay = function() {}; 348 n.onclose = function() { 349 delete notifications[server.name + channel]; 350 }; 351 n.show(); 352 353 notifications[server.name + channel] = n; 354 } 355 356 357 358 359 381 382 411 412