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>
12 var ircConnections = {};
14 // The server & channel configutation data is stored in localStorage.servers.
15 // These are setters and getters for this structure.
17 return JSON.parse(localStorage.servers || "[]");
19 function setServers(servers) {
20 localStorage.servers = JSON.stringify(servers);
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() {
27 servers().forEach(function(server) {
28 server.channels = server.channels || [];
29 server.channels.forEach(function(channel) {
30 channelList.push(server.name + channel);
38 window.onload = function() {
39 // Setup notifications.
40 window.onfocus = function() {
41 windowHasFocus = true;
44 window.onblur = function() {
45 windowHasFocus = false;
50 // Setup channel navigation and message entry.
51 function handleBodyKeyDown(event) {
52 switch (event.keyCode) {
53 case 37: // left arrow
56 case 39: // right arrow
61 document.body.addEventListener('keydown', handleBodyKeyDown, false);
63 // We don't want left & right arrow inside the text entry to move the channel
65 $('typingDiv').addEventListener('keydown', function(event) {
66 event.stopPropagation();
68 $('entryText').addEventListener('keydown', function(event) {
69 if (event.keyCode == 13) { // RETURN key.
70 processEntryMessage();
74 servers().forEach(addServerConnection);
77 window.onunload = function() {
78 ircConnections.forEach(function(irc) {
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);
93 irc.onConnect = function(message) {
94 server.channels.forEach(function(channel) {
95 ircConnections[server.name].joinChannel(channel);
98 irc.onDisconnect = function(message) {
100 irc.onText = function(channel, nick, message) {
101 checkForNickReference(server, channel, nick, message);
102 addMessage(server.name, channel, nick, new Date(), message);
105 ircConnections[server.name] = irc;
108 function joinChannel(serverName, channelName) {
109 ircConnections[serverName].joinChannel(channelName);
112 function removeChannelListener(channelName) {
113 return function(event) {
114 event.stopPropagation();
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);
134 function syncChannelList() {
135 var channels = channelList();
136 var channelSlides = $('channelSlides');
137 var channelSlideProto = $('channelSlideProto');
139 var channelIndex = 0;
142 while(channelIndex < channels.length ||
143 channels.length != channelSlides.children.length) {
144 var channel = channels[channelIndex];
145 var slide = channelSlides.children[slideIndex];
147 if (slideIndex == channelSlides.children.length ||
148 channel < slideChannel(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);
157 channelSlides.insertBefore(newSlide, slide);
159 newSlide.addEventListener('click', onClickMoveSlide);
160 childNodeWithClass(newSlide, "removeButton")
161 .addEventListener('click', removeChannelListener(channel));
164 } else if (!channel || channel > slideChannel(slide)) {
165 // Delete a removed slide.
167 // If the removed slide is the current slide, we have to pick a new
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]);
179 channelSlides.removeChild(slide);
185 slide.setAttribute("slide", "" + slideIndex - 1);
191 function processEntryMessage() {
192 var message = $('entryText').value;
193 $('entryText').value = "";
195 if (!localStorage.currentSlide) {
196 alert('No current channel');
203 servers().forEach(function(s) {
204 if (localStorage.currentSlide.indexOf(s.name) == 0) {
207 s.channels.forEach(function(c) {
208 if (localStorage.currentSlide == s.name + c) {
215 addMessage(server, channel, nick, new Date(), message);
216 ircConnections[server].sendMessage([channel], message);
219 function addMessage(server, channel, nick, time, body) {
220 messageLine = childNodeWithClass($('channelSlideProto'), "messageLine");
221 var newMessageLine = messageLine.cloneNode(true);
223 jstProcess(new JsEvalContext({
228 newMessageLine.style.display = "";
231 childNodeWithClass($("channel-" + server + channel), "messageList");
232 messageList.appendChild(newMessageLine);
235 function formatTime(time) {
243 // Returns the server#channel string value for a given |slide| element.
244 function slideChannel(slide) {
245 return childNodeWithClass(slide, "channel").innerText;
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);
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) {
264 if (localStorage.currentSlide) {
265 slide = document.getElementById("channel-" + localStorage.currentSlide);
267 slideNumber = parseInt(slide.getAttribute("slide"));
270 if (isNaN(slideNumber) || !slide) {
273 if (typeof(slideDelta) == "number") {
274 slideNumber += slideDelta;
277 var slides = document.getElementsByClassName("channelSlide");
278 if (slideNumber < 0 || slideNumber == slides.length - 1) {
282 for (var i = 0; i < slides.length; i++) {
283 var slide = slides[i];
284 var slideIndex = parseInt(slide.getAttribute("slide")) - slideNumber;
286 if (slideIndex <= -2) {
287 slide.className = "channelSlide far-left";
289 if (slideIndex >= 2) {
290 slide.className = "channelSlide far-right";
295 slide.className = "channelSlide left";
298 slide.className = "channelSlide center";
299 localStorage.currentSlide = slideChannel(slide);
302 slide.className = "channelSlide right";
307 clearNotifications();
313 var windowHasFocus = false;
314 var notifications = {};
316 function clearNotifications() {
317 for (property in notifications) {
318 notifications[property].cancel();
324 function checkForNickReference(server, channel, nick, message) {
325 if (windowHasFocus || !message || message.indexOf(server.nick) < 0) {
329 // Notifications will be enabled by the app install. Otherwise, don't notity.
330 if (window.webkitNotifications.checkPermission() != 0) {
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();
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);
346 var n = window.webkitNotifications.createHTMLNotification(url);
347 n.ondisplay = function() {};
348 n.onclose = function() {
349 delete notifications[server.name + channel];
353 notifications[server.name + channel] = n;
359 <div id="channelSlideProto" style="display:none" class="channelSlide">
360 <div class="channelControls">
361 <div jscontent="name" class="channel">
363 <div class="removeButton">
367 <div class="channelSlideContainer">
368 <div class="messageListContainer">
369 <div class="messageList">
370 <div jsselect="messages">
371 <div class="messageLine">
372 <div jscontent="nick" class="messageSender"></div>:
373 <div jscontent="body" class="messageBody"></div>
378 <div class="messageListSpacer">.</div>
382 <div id="pageContainer">
383 <div id="headerContainer">
384 <div id="pageControls">
385 <div onclick="window.open('addServer.html');">
386 <div class="addControlLabel">
389 <div class="addButton">
393 <div onclick=" window.open('addChannel.html');">
394 <div class="addControlLabel">
397 <div class="addButton">
403 <div id="slideContainer">
405 <input type="text" id="entryText" value="">
407 <div style="" id="channelSlides">