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
12var ircConnections = {};
13
14// The server & channel configutation data is stored in localStorage.servers.
15// These are setters and getters for this structure.
16function servers() {
17  return JSON.parse(localStorage.servers || "[]");
18}
19function 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.
25function 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
38window.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
77window.onunload = function() {
78  ircConnections.forEach(function(irc) {
79    irc.disconnect();  
80  });
81}
82
83function 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
108function joinChannel(serverName, channelName) {
109  ircConnections[serverName].joinChannel(channelName);
110}
111
112function 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
134function 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
191function 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
219function 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
235function formatTime(time) {
236  return "";
237}
238
239/**
240 * Slide Navigation. 
241 */
242 
243// Returns the server#channel string value for a given |slide| element.
244function slideChannel(slide) {
245  return childNodeWithClass(slide, "channel").innerText;
246}
247
248// Handler for clicking on the visible portions of the previous & next slides.
249function 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.
260function 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 */
313var windowHasFocus = false;
314var notifications = {};
315
316function clearNotifications() {
317  for (property in notifications) {
318    notifications[property].cancel();
319  }
320
321  notifications = {};
322}
323
324function 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 (Notification.permission != "granted") {
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].close();
337  }
338
339  var n = new Notification("On " + server.name + channel, {
340    icon: "https://www.google.com/favicon.ico",
341    body: nick + ": " + message,
342  });
343
344  n.onshow = function() {};
345  n.onclose = function() {
346    delete notifications[server.name + channel];
347  };
348
349  notifications[server.name + channel] = n;
350}
351    </script>
352  </head>
353  <body>
354    <!--  TEMPLATES -->
355    <div id="channelSlideProto" style="display:none" class="channelSlide">
356      <div class="channelControls">
357        <div jscontent="name" class="channel">
358        </div>
359        <div class="removeButton">
360          x
361        </div>
362      </div>
363      <div class="channelSlideContainer">
364        <div class="messageListContainer">
365          <div class="messageList">
366            <div jsselect="messages">
367              <div class="messageLine">
368                <div jscontent="nick" class="messageSender"></div>:
369                <div jscontent="body" class="messageBody"></div>
370              </div>
371            </div>
372          </div>
373        </div>
374        <div class="messageListSpacer">.</div>
375      </div>
376    </div>
377    
378    <div id="pageContainer">
379      <div id="headerContainer">
380        <div id="pageControls">
381          <div onclick="window.open('addServer.html');">
382            <div class="addControlLabel">
383              add server
384            </div>
385            <div class="addButton">
386              +
387            </div>
388          </div>
389          <div onclick=" window.open('addChannel.html');">
390            <div class="addControlLabel">
391              add channel
392            </div>
393            <div class="addButton">
394              +
395            </div>
396          </div>
397        </div>
398      </div>
399      <div id="slideContainer">
400        <div id="typingDiv">
401          <input type="text" id="entryText" value="">
402        </div>
403        <div style="" id="channelSlides">
404        </div>
405      </div>
406    </div>
407  </body>
408</html>
409