logbot.js 22 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713
  1. $(function() {
  2. 'use strict';
  3. var initialising = true;
  4. var current_channel = $('#channel').data('name');
  5. // cookie helper
  6. function set_cookie(name, value) {
  7. document.cookie = name + "=" + (value ? '1' : '0') + '; expires=Thu, 31 Dec 2037 23:59:58 GMT; path=/';
  8. }
  9. // always collapse the sidebar on tiny screens
  10. var is_tiny_screen = $('#not-tiny-screen:visible').length === 0;
  11. if (is_tiny_screen) {
  12. $('body').addClass('menu-c');
  13. set_cookie('menu-c', true);
  14. }
  15. // keyboard shortcuts
  16. $('body')
  17. .keyup(function(e) {
  18. // if a text input field has focus
  19. if (document.activeElement.nodeName === 'INPUT' &&
  20. document.activeElement.getAttribute('type') === 'text'
  21. ) {
  22. if (e.which === 27) {
  23. // escape should clear field
  24. $(document.activeElement).val('').keyup();
  25. }
  26. // and other shortcuts shouldn't work
  27. return;
  28. }
  29. if (e.which === 27) { // esc
  30. if ($('#settings-dialog:visible').length) {
  31. // close settings
  32. $('#settings-close').click();
  33. } else {
  34. // toggle sidebar
  35. $('#collapse-sidebar').click();
  36. }
  37. } else if (e.key === '#') {
  38. // # --> show channel list
  39. if (!$('body').hasClass('list')) {
  40. $('#channel-list-action').click();
  41. }
  42. } else if (e.key === 'ArrowLeft') {
  43. // left-arrow --> previous date
  44. if (document.activeElement.nodeName !== 'BODY') {
  45. return;
  46. }
  47. if ($('#skip-prev').length) {
  48. $('#skip-prev')[0].click();
  49. } else if ($('#date-prev:not(.hidden)').length) {
  50. $('#date-prev')[0].click();
  51. }
  52. } else if (e.key === 'ArrowRight') {
  53. // right-arrow --> next date
  54. if (document.activeElement.nodeName !== 'BODY') {
  55. return;
  56. }
  57. if ($('#skip-next').length) {
  58. $('#skip-next')[0].click();
  59. } else if ($('#date-next:not(.hidden)').length) {
  60. $('#date-next')[0].click();
  61. }
  62. }
  63. });
  64. // collapse sidebar
  65. function set_sidebar_collapse_title() {
  66. $('#collapse-sidebar').attr('title',
  67. $('body').hasClass('menu-c') ?
  68. 'Show Channels (Esc)' : 'Hide Channels (Esc)'
  69. );
  70. }
  71. $('#collapse-sidebar')
  72. .click(function() {
  73. $('body').toggleClass('menu-c');
  74. set_sidebar_collapse_title();
  75. set_cookie('menu-c', is_tiny_screen || $('body').hasClass('menu-c'));
  76. });
  77. set_sidebar_collapse_title();
  78. // highlight active channel
  79. if ($('body').hasClass('logs')) {
  80. var ch = current_channel.substring(1);
  81. var $ch = $('#ch-' + ch);
  82. if ($ch.length) {
  83. $ch.addClass('is-active');
  84. } else {
  85. // hidden channel, add to top of channel list
  86. var $li = $('#channel-menu li:first');
  87. if ($li.length) {
  88. $li = $li.clone();
  89. $li.find('a')
  90. .addClass('is-active')
  91. .attr('id', 'ch-' + ch)
  92. .attr('href', '/' + ch)
  93. .attr('title', $('#topic').text().trim())
  94. .text(current_channel);
  95. $('#channel-menu').prepend($li);
  96. }
  97. }
  98. }
  99. // about
  100. $('#about')
  101. .click(function() {
  102. set_cookie('menu-c', false);
  103. });
  104. // nav - date
  105. function nav_to_date(ymd) {
  106. document.location = '/' + encodeURIComponent(current_channel.substring(1)) + '/' + ymd;
  107. }
  108. if (document.getElementById('date-icon')) {
  109. var nav_pika_config = {
  110. field: document.getElementById('date-icon'),
  111. maxDate: new Date(),
  112. onSelect: function(date) {
  113. var mm = (date.getMonth() + 1).toString();
  114. var dd = date.getDate().toString();
  115. var ymd = [date.getFullYear(), mm.length === 2 ? '' : '0', mm, dd.length === 2 ? '' : '0', dd].join('');
  116. nav_to_date(ymd);
  117. }
  118. };
  119. var ymd = $('#date').data('ymd') + '';
  120. if (ymd) {
  121. nav_pika_config.defaultDate = new Date(ymd.substr(0, 4), ymd.substr(4, 2) - 1, ymd.substr(6, 2));
  122. nav_pika_config.setDefaultDate = true;
  123. }
  124. new Pikaday(nav_pika_config);
  125. }
  126. // nav - last message
  127. $('#channel-end')
  128. .click(function(e) {
  129. e.preventDefault();
  130. $('html, body').animate({
  131. scrollTop: $(document).height()
  132. }, 250);
  133. });
  134. $(document).on('setting:hide-b', function(e, enabled) {
  135. if ($('.no-events:visible').length) {
  136. $('#channel-end')
  137. .attr('disabled', true)
  138. .attr('href', '');
  139. } else {
  140. $('#channel-end')
  141. .attr('disabled', undefined)
  142. .attr('href', '#end');
  143. }
  144. });
  145. // nav - network
  146. $('#current-network')
  147. .click(function(e) {
  148. e.preventDefault();
  149. if ($('#networks').hasClass('collapsed')) {
  150. $('#networks').removeClass('collapsed');
  151. } else {
  152. $('#networks').addClass('collapsed');
  153. }
  154. });
  155. // nav - topic
  156. function set_topic_visible(is_visible) {
  157. if (is_visible) {
  158. $('#channel-topic').attr('title', 'Hide Channel Topic');
  159. $('body').addClass('topic');
  160. $('#topic').show();
  161. set_cookie('topic', true);
  162. } else {
  163. $('#channel-topic').attr('title', 'Show Channel Topic');
  164. $('body').removeClass('topic');
  165. $('#topic').hide();
  166. set_cookie('topic', false);
  167. }
  168. }
  169. set_topic_visible($('body').hasClass('topic'));
  170. $('#channel-topic')
  171. .click(function(e) {
  172. e.preventDefault();
  173. set_topic_visible(!$('body').hasClass('topic'));
  174. });
  175. // search
  176. $('#search-submit')
  177. .click(function() {
  178. document.forms['nav-search'].submit();
  179. });
  180. if ($('body').hasClass('search')) {
  181. $('#search-nav')
  182. .prop('disabled', true);
  183. $('#search-query')
  184. .focus()
  185. .select();
  186. var search_pika_config = {
  187. maxDate: new Date(),
  188. keyboardInput: false,
  189. toString: function(d) {
  190. return [
  191. d.getFullYear(),
  192. ('0' + (d.getMonth() + 1)).slice(-2),
  193. ('0' + d.getDate()).slice(-2)
  194. ].join('-');
  195. }
  196. };
  197. search_pika_config.field = document.getElementById('search-when-from');
  198. new Pikaday(search_pika_config);
  199. search_pika_config.field = document.getElementById('search-when-to');
  200. new Pikaday(search_pika_config);
  201. $(window).on('pageshow', function(e) {
  202. // replace name attributes that are cleared on submit
  203. $('#search-form input').each(function() {
  204. var $this = $(this);
  205. if ($this.data('name')) {
  206. $this.attr('name', $this.data('name'));
  207. }
  208. });
  209. });
  210. }
  211. $('#search-channels')
  212. .chosen({
  213. no_results_text: 'No channels matching',
  214. placeholder_text_multiple: 'All channels',
  215. search_contains: true,
  216. display_selected_options: false
  217. });
  218. $('#search-channel-all, #search-channel-single, #search-channel-custom')
  219. .change(function() {
  220. if (!$(this).prop('checked')) {
  221. return;
  222. }
  223. if ($(this).attr('id') === 'search-channel-custom') {
  224. $('#search-channels').attr('disabled', false);
  225. $('#search-channel-multi').show();
  226. if (!initialising) {
  227. $('#search-channels').trigger('chosen:open');
  228. }
  229. } else {
  230. $('#search-channel-multi').hide();
  231. $('#search-channels').attr('disabled', true);
  232. }
  233. })
  234. .change();
  235. $('#search-when-all, #search-when-recently, #search-when-custom')
  236. .change(function() {
  237. if (!$(this).prop('checked')) {
  238. return;
  239. }
  240. if ($(this).attr('id') === 'search-when-custom') {
  241. $('#search-when-from').attr('disabled', false);
  242. $('#search-when-to').attr('disabled', false);
  243. $('#search-when-range').show();
  244. } else {
  245. $('#search-when-range').hide();
  246. $('#search-when-from').attr('disabled', true);
  247. $('#search-when-to').attr('disabled', true);
  248. }
  249. })
  250. .change();
  251. $('#search-form-submit')
  252. .click(function() {
  253. $('#search-channel-all, #search-channel-custom, #search-when-recently, #search-using-ft')
  254. .each(function() {
  255. var $this = $(this);
  256. if ($this.prop('checked')) {
  257. $this.data('name', $this.attr('name'));
  258. $this.attr('name', '');
  259. }
  260. });
  261. var $who = $('#search-who');
  262. if ($who.val() === '') {
  263. $who.data('name', $who.attr('name'));
  264. $who.attr('name', '');
  265. }
  266. });
  267. // channel list filter
  268. function update_filtered() {
  269. var filter = $('#filter').val().trim().toLowerCase();
  270. var filter_words = filter.split(/ +/);
  271. $('#channel-list li').each(function() {
  272. var $this = $(this);
  273. var this_text = $this.data('text');
  274. var match = true;
  275. for (var i = 0, l = filter_words.length; i < l; i++) {
  276. if (this_text.indexOf(filter_words[i]) === -1) {
  277. match = false;
  278. break;
  279. }
  280. }
  281. if (match) {
  282. $this.addClass('match');
  283. } else {
  284. $this.removeClass('match');
  285. }
  286. });
  287. var active_count = $('#active-channels li.match').length;
  288. var archived_count = $('#archived-channels li.match').length;
  289. if (active_count === 0) {
  290. $('#active-channels').hide();
  291. } else {
  292. $('#active-channels').show();
  293. }
  294. if (archived_count === 0) {
  295. $('#archived-channels').hide();
  296. } else {
  297. $('#archived-channels').show();
  298. }
  299. if (active_count === 0 && archived_count === 0) {
  300. $('#no-results').show();
  301. } else {
  302. $('#no-results').hide();
  303. }
  304. }
  305. function init_list() {
  306. $('#filter')
  307. .focus()
  308. .select()
  309. .keyup(update_filtered)
  310. .keypress(function(e) {
  311. if (e.which === 13) {
  312. e.preventDefault();
  313. var filter = $('#filter').val().trim().toLowerCase().replace(/^#/, '');
  314. var exact_match = $('#active-channels a.channel[href="/' + CSS.escape(filter) + '"]');
  315. if (exact_match.length) {
  316. document.location = exact_match.attr('href');
  317. } else if ($('#channel-list li.match').length === 1) {
  318. document.location = $('#channel-list li.match a').attr('href');
  319. }
  320. }
  321. });
  322. $(window).on('pageshow', function(e) {
  323. update_filtered();
  324. $('#filter').focus();
  325. });
  326. }
  327. $(document).on('list-preloaded', function() {
  328. $('#channel-list-action').on('click', function(e) {
  329. if (e.metaKey) {
  330. return;
  331. }
  332. e.preventDefault();
  333. e.stopPropagation();
  334. $('#main').html(localStorage.getItem('list-html'));
  335. $('body')
  336. .addClass('list')
  337. .removeClass('search');
  338. history.pushState({}, '', this.href);
  339. init_list();
  340. });
  341. });
  342. if ($('body').hasClass('list')) {
  343. init_list();
  344. } else {
  345. try {
  346. var list_id = $('#list-id').data('id');
  347. if (localStorage.getItem('list-id') !== list_id) {
  348. $.ajax({
  349. url: '/_channels_body',
  350. method: 'GET',
  351. dataType: 'html',
  352. success: function(html) {
  353. localStorage.setItem('list-html', html);
  354. localStorage.setItem('list-id', list_id);
  355. $('body').trigger('list-preloaded');
  356. }
  357. });
  358. } else {
  359. $('body').trigger('list-preloaded');
  360. }
  361. } catch (e) {
  362. console.error(e);
  363. }
  364. }
  365. // highlight
  366. function highlight($start, $end) {
  367. // clear highlights
  368. $('li.hl').removeClass('hl');
  369. var start_id = $start.attr('id');
  370. var end_id = $end.attr('id');
  371. // single row
  372. if (!end_id || start_id === end_id) {
  373. $start.addClass('hl');
  374. return [start_id];
  375. }
  376. // swap positions if end is before start
  377. if ($start.prevAll().filter('#' + end_id).length) {
  378. var tmp = end_id;
  379. end_id = start_id;
  380. start_id = tmp;
  381. tmp = $end;
  382. $start = $end;
  383. $end = tmp;
  384. }
  385. // iterate from start to end
  386. var id = start_id;
  387. var $el = $start;
  388. while (id !== end_id) {
  389. $el.addClass('hl');
  390. $el = $el.next();
  391. id = $el.attr('id');
  392. if (!id) {
  393. return [start_id];
  394. }
  395. }
  396. $el.addClass('hl');
  397. return [start_id, end_id];
  398. }
  399. function highlight_from_anchor(anchor) {
  400. var hl_hash = anchor.match('^#(c[0-9]+)(-(c[0-9]+))?$');
  401. if (!hl_hash) {
  402. return;
  403. }
  404. var $li = $('#' + hl_hash[1]);
  405. if ($li.length) {
  406. var range = highlight($li, $('#' + hl_hash[3]));
  407. var offset = $li.offset();
  408. $hl_anchor = $('#' + range[0]);
  409. $('html, body').animate({
  410. scrollTop: $li.offset().top - 40
  411. }, 50);
  412. }
  413. }
  414. var $hl_anchor = false;
  415. $('#logs .time')
  416. .click(function(e) {
  417. e.preventDefault();
  418. var $li = $(this).parent('li');
  419. var $current = $('li.hl');
  420. // if shift key is down highlight range
  421. if (e.shiftKey && $hl_anchor) {
  422. var range = highlight($hl_anchor, $li);
  423. var anchor = '#' + range[0] + (range[1] ? '-' + range[1] : '');
  424. history.pushState('', document.title, document.location.pathname + document.location.search + anchor);
  425. return;
  426. }
  427. if ($li.hasClass('hl')) {
  428. // deselect
  429. $current.removeClass('hl');
  430. $hl_anchor = false;
  431. history.pushState('', document.title, document.location.pathname + document.location.search);
  432. } else {
  433. // highlight just this row
  434. $current.removeClass('hl');
  435. $li.addClass('hl');
  436. $hl_anchor = $li;
  437. history.pushState('', document.title, document.location.pathname + document.location.search + '#' + $li.attr('id'));
  438. }
  439. });
  440. highlight_from_anchor(document.location.hash);
  441. $('#logs .text a, #logs .action a')
  442. .click(function(e) {
  443. if (this.hash && this.href.startsWith($('#logs').data('url')) && $(this.hash).length) {
  444. e.preventDefault();
  445. highlight_from_anchor(this.hash);
  446. }
  447. });
  448. // no-event messages
  449. $(document).on('setting:hide-b', function(e, enabled) {
  450. if (enabled) {
  451. $('#no-visible-events').show();
  452. } else {
  453. $('#no-visible-events').hide();
  454. }
  455. });
  456. // settings dialog
  457. function toggle_setting($setting, name) {
  458. var enabled = $('body')
  459. .toggleClass(name)
  460. .hasClass(name);
  461. set_cookie(name, enabled);
  462. $setting
  463. .find('input')
  464. .prop('checked', enabled);
  465. $(document).trigger('setting:' + name, [enabled]);
  466. }
  467. $('#settings')
  468. .click(function() {
  469. $('#settings-dialog').addClass('is-active');
  470. });
  471. $('#settings-dialog .modal-background, #settings-dialog .modal-close, #settings-close')
  472. .click(function(e) {
  473. e.preventDefault();
  474. $('#settings-dialog').removeClass('is-active');
  475. });
  476. $('.setting')
  477. .click(function() {
  478. var $this = $(this);
  479. toggle_setting($this, $this.data('setting'));
  480. });
  481. $('#settings-container .setting')
  482. .hover(
  483. function() {
  484. var $this = $(this);
  485. if (!$this.hasClass('not-implemented')) {
  486. $(this).find('input').addClass('hover');
  487. }
  488. },
  489. function() {
  490. $(this).find('input').removeClass('hover');
  491. }
  492. );
  493. // relative time
  494. function time_ago(ss) {
  495. var mm = Math.round(ss / 60),
  496. hh = Math.round(mm / 60),
  497. dd = Math.round(hh / 24),
  498. mo = Math.round(dd / 30),
  499. yy = Math.round(mo / 12);
  500. if (ss < 10) return 'just now';
  501. if (ss < 45) return ss + ' seconds ago';
  502. if (ss < 90) return 'a minute ago';
  503. if (mm < 45) return mm + ' minutes ago';
  504. if (mm < 90) return 'an hour ago';
  505. if (hh < 24) return hh + ' hours ago';
  506. if (hh < 36) return 'a day ago';
  507. if (dd < 30) return dd + ' days ago';
  508. if (dd < 45) return 'a month ago';
  509. if (mo < 12) return mo + ' months ago';
  510. if (mo < 18) return 'a year ago';
  511. return yy + ' years ago';
  512. }
  513. function relative_timer() {
  514. var now = Math.floor(new Date().getTime() / 1000);
  515. $('.rel-time').each(function() {
  516. $(this).text(time_ago(now - $(this).data('time')));
  517. });
  518. }
  519. function handle_visibility_change() {
  520. if (document[hidden_event]) {
  521. relative_timer();
  522. if (!relative_timer_id) {
  523. relative_timer_id = window.setInterval(relative_timer, relative_timer_duration);
  524. }
  525. } else {
  526. relative_timer();
  527. if (!relative_timer_id) {
  528. relative_timer_id = window.setInterval(relative_timer, relative_timer_duration);
  529. }
  530. }
  531. }
  532. if ($('.rel-time').length) {
  533. var relative_timer_duration = 60000;
  534. var relative_timer_id = window.setInterval(relative_timer, relative_timer_duration);
  535. var hidden_event, visibility_change;
  536. if (typeof document.hidden !== "undefined") {
  537. hidden_event = "hidden";
  538. visibility_change = "visibilitychange";
  539. } else if (typeof document.webkitHidden !== "undefined") {
  540. hidden_event = "webkitHidden";
  541. visibility_change = "webkitvisibilitychange";
  542. }
  543. if (hidden_event) {
  544. document.addEventListener(visibility_change, handle_visibility_change);
  545. }
  546. }
  547. // channel stats
  548. function update_hours_plot() {
  549. $.ajax({
  550. url: base + 'hours',
  551. method: 'GET',
  552. dataType: 'json',
  553. success: function(series) {
  554. var $container = $('#hours-plot');
  555. $container
  556. .text('')
  557. .removeClass('loading');
  558. var plot = $.plot(
  559. $container, [series], {
  560. xaxis: {
  561. ticks: 24,
  562. tickFormatter: function(h) {
  563. if (h == 0) return '12am';
  564. if (h < 12) return h + 'am';
  565. h -= 12;
  566. if (h == 0) return '12pm';
  567. return h + 'pm';
  568. }
  569. },
  570. yaxis: {
  571. show: false
  572. },
  573. grid: {
  574. borderWidth: 0
  575. },
  576. colors: [$('#loc-container').css('background-color')]
  577. }
  578. );
  579. var current_hh = $container.data('hh') * 1;
  580. var current_mm = $container.data('mm') * 1;
  581. var offset = plot.pointOffset({
  582. x: current_hh + (current_mm / 60),
  583. y: plot.getAxes().yaxis.datamax
  584. });
  585. var style = 'position:absolute;height:' + plot.height() + 'px;top:8px;left:' + offset.left + 'px';
  586. $container.append('<div id="hours-now" style="' + style + '">Now</div>');
  587. }
  588. });
  589. }
  590. if ($('body').hasClass('stats')) {
  591. var channel = $('#stats').data('channel');
  592. var base = channel ? 'stats/' : '_stats/';
  593. $.getJSON(base + 'meta', function(data) {
  594. $.each(data, function(name, value) {
  595. $('#' + name)
  596. .text(value)
  597. .removeClass('loading');
  598. });
  599. $('.loading-hide').removeClass('loading-hide');
  600. });
  601. update_hours_plot();
  602. $(document).on('setting:dark', update_hours_plot);
  603. if (channel) {
  604. $.ajax({
  605. url: base + 'nicks',
  606. method: 'GET',
  607. dataType: 'html',
  608. success: function(html) {
  609. $('#nicks-plot')
  610. .text('')
  611. .removeClass('loading')
  612. .html(html);
  613. }
  614. });
  615. }
  616. }
  617. $('.setting').each(function() {
  618. var $this = $(this);
  619. var name = $this.data('setting');
  620. $(document).trigger('setting:' + name, [$('body').hasClass(name)]);
  621. });
  622. initialising = false;
  623. });