/ js / extension-test-sync2.js
extension-test-sync2.js
   1  /********************************
   2   *                              *
   3   *        4chan Extension       *
   4   *                              *
   5   ********************************/
   6  
   7  /**
   8   * Helpers
   9   */
  10  $ = {};
  11  
  12  $.id = function(id) {
  13    return document.getElementById(id);
  14  };
  15  
  16  $.cls = function(klass, root) {
  17    return (root || document).getElementsByClassName(klass);
  18  };
  19  
  20  $.byName = function(name) {
  21    return document.getElementsByName(name);
  22  };
  23  
  24  $.tag = function(tag, root) {
  25    return (root || document).getElementsByTagName(tag);
  26  };
  27  
  28  $.qs = function(sel, root) {
  29    return (root || document).querySelector(sel);
  30  };
  31  
  32  $.extend = function(destination, source) {
  33    for (var key in source) {
  34      destination[key] = source[key];
  35    }
  36  };
  37  
  38  if (!document.documentElement.classList) {
  39    $.hasClass = function(el, klass) {
  40      return (' ' + el.className + ' ').indexOf(' ' + klass + ' ') != -1;
  41    };
  42    
  43    $.addClass = function(el, klass) {
  44      el.className = (el.className == '') ? klass : el.className + ' ' + klass;
  45    };
  46    
  47    $.removeClass = function(el, klass) {
  48      el.className = (' ' + el.className + ' ').replace(' ' + klass + ' ', '');
  49    };
  50  }
  51  else {
  52    $.hasClass = function(el, klass) {
  53      return el.classList.contains(klass);
  54    };
  55    
  56    $.addClass = function(el, klass) {
  57      el.classList.add(klass);
  58    };
  59    
  60    $.removeClass = function(el, klass) {
  61      el.classList.remove(klass);
  62    };
  63  }
  64  
  65  $.get = function(url, callbacks, headers) {
  66    var key, xhr;
  67    
  68    xhr = new XMLHttpRequest();
  69    xhr.open('GET', url, true);
  70    if (callbacks) {
  71      for (key in callbacks) {
  72        xhr[key] = callbacks[key];
  73      }
  74    }
  75    if (headers) {
  76      for (key in headers) {
  77        xhr.setRequestHeader(key, headers[key]);
  78      }
  79    }
  80    xhr.send(null);
  81    return xhr;
  82  };
  83  
  84  $.hash = function(str) {
  85    var i, j, msg = 0;
  86    for (i = 0, j = str.length; i < j; ++i) {
  87      msg = ((msg << 5) - msg) + str.charCodeAt(i);
  88    }
  89    return msg;
  90  };
  91  
  92  $.prettySeconds = function(fs) {
  93    var m, s;
  94    
  95    m = Math.floor(fs / 60);
  96    s = Math.round(fs - m * 60);
  97    
  98    return [ m, s ];
  99  };
 100  
 101  $.docEl = document.documentElement;
 102  
 103  $.cache = {};
 104  
 105  /**
 106   * Parser
 107   */
 108  var Parser = {};
 109  
 110  Parser.init = function() {
 111    var o, a, h, m, tail, staticPath, tracked, el;
 112    
 113    if (Config.filter || Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) {
 114      this.needMsg = true;
 115    }
 116    
 117    staticPath = '//s.4cdn.org/image/';
 118    
 119    tail = window.devicePixelRatio >= 2 ? '@2x.gif' : '.gif';
 120    
 121    this.icons = {
 122      admin: staticPath + 'adminicon' + tail,
 123      mod: staticPath + 'modicon' + tail,
 124      dev: staticPath + 'developericon' + tail,
 125      manager: staticPath + 'managericon' + tail,
 126      del: staticPath + 'filedeleted-res' + tail
 127    };
 128    
 129    this.prettify = typeof prettyPrint == 'function';
 130    
 131    this.customSpoiler = {};
 132    
 133    if (Config.localTime) {
 134      if (o = (new Date).getTimezoneOffset()) {
 135        a = Math.abs(o);
 136        h = (0 | (a / 60));
 137        
 138        this.utcOffset = 'Timezone: UTC' + (o < 0 ? '+' : '-')
 139          + h + ((m = a - h * 60) ? (':' + m) : '');
 140      }
 141      else {
 142        this.utcOffset = 'Timezone: UTC';
 143      }
 144      
 145      this.weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
 146    }
 147    
 148    if (Main.tid) {
 149      this.trackedReplies = this.getTrackedReplies(Main.tid) || {};
 150    }
 151  };
 152  
 153  Parser.getTrackedReplies = function(tid) {
 154    var tracked = null;
 155    
 156    if (tracked = sessionStorage.getItem('4chan-track-' + Main.board + '-' + tid)) {
 157      tracked = JSON.parse(tracked);
 158    }
 159    
 160    return tracked;
 161  };
 162  
 163  Parser.saveTrackedReplies = function(tid, replies) {
 164    sessionStorage.setItem(
 165      '4chan-track-' + Main.board + '-' + tid,
 166      JSON.stringify(replies)
 167    );
 168  };
 169  
 170  Parser.parseThreadJSON = function(data) {
 171    var thread;
 172    
 173    try {
 174      thread = JSON.parse(data).posts;
 175    }
 176    catch (e) {
 177      console.log(e);
 178      thread = [];
 179    }
 180    
 181    return thread;
 182  };
 183  
 184  Parser.parseCatalogJSON = function(data) {
 185    var catalog;
 186    
 187    try {
 188      catalog = JSON.parse(data);
 189    }
 190    catch (e) {
 191      console.log(e);
 192      catalog = [];
 193    }
 194    
 195    return catalog;
 196  };
 197  
 198  Parser.setCustomSpoiler = function(board, val) {
 199    var s;
 200    if (!this.customSpoiler[board] && (val = parseInt(val))) {
 201      if (board == Main.board && (s = $.cls('imgspoiler')[0])) {
 202        this.customSpoiler[board] =
 203          s.firstChild.src.match(/spoiler(-[a-z0-9]+)\.png$/)[1];
 204      }
 205      else {
 206        this.customSpoiler[board] = '-' + board
 207          + (Math.floor(Math.random() * val) + 1);
 208      }
 209    }
 210  };
 211  
 212  Parser.buildPost = function(thread, board, pid) {
 213    var i, j, el = null;
 214    
 215    for (i = 0; j = thread[i]; ++i) {
 216      if (j.no != pid) {
 217        continue;
 218      }
 219      
 220      if (!Config.revealSpoilers && thread[0].custom_spoiler) {
 221        Parser.setCustomSpoiler(board, thread[0].custom_spoiler);
 222      }
 223      
 224      el = Parser.buildHTMLFromJSON(j, board, false, true).lastElementChild;
 225      
 226      if (Config.IDColor && (uid = $.cls('posteruid', el)[Main.hasMobileLayout ? 0 : 1])) {
 227        IDColor.applyRemote(uid.firstElementChild);
 228      }
 229    }
 230    
 231    return el;
 232  };
 233  
 234  Parser.decodeSpecialChars = function(str) {
 235    return str.replace(/&amp;/g, '&')
 236      .replace(/&quot;/g, '"')
 237      .replace(/&#039;/g, "'")
 238      .replace(/&lt;/g, '<')
 239      .replace(/&gt;/g, '>');
 240  };
 241  
 242  Parser.encodeSpecialChars = function(str) {
 243    return str.replace(/&/g, '&amp;')
 244      .replace(/"/g, '&quot;')
 245      .replace(/'/g, '&#039;')
 246      .replace(/</g, '&lt;')
 247      .replace(/>/g, '&gt;');
 248  };
 249  
 250  Parser.buildHTMLFromJSON = function(data, board, standalone, fromQuote) {
 251    var
 252      container = document.createElement('div'),
 253      isOP = false,
 254      
 255      userId,
 256      fileDims = '',
 257      imgSrc = '',
 258      fileInfo = '',
 259      fileHtml = '',
 260      fileThumb,
 261      filePath,
 262      fileName,
 263      fileSpoilerTip = '"',
 264      size = '',
 265      fileClass = '',
 266      shortFile = '',
 267      longFile = '',
 268      tripcode = '',
 269      capcodeStart = '',
 270      capcodeClass = '',
 271      capcode = '',
 272      flag,
 273      highlight = '',
 274      emailStart = '',
 275      emailEnd = '',
 276      name,
 277      subject,
 278      noLink,
 279      quoteLink,
 280      replySpan = '',
 281      noFilename,
 282      decodedFilename,
 283      mobileLink = '',
 284      postType = 'reply',
 285      summary = '',
 286      postCountStr,
 287      resto,
 288      capcode_replies = '',
 289      threadIcons = '',
 290      needFileTip = false,
 291      
 292      i, q, href, quotes,
 293      
 294      imgDir = '//i.4cdn.org/' + board;
 295    
 296    if (data.resto == 0) {
 297      isOP = true;
 298      
 299      if (standalone) {
 300        mobileLink = '<div class="postLink mobile"><span class="info"></span><a href="'
 301          + 'thread/' + data.no + '" class="button">View Thread</a></div>';
 302        postType = 'op';
 303        replySpan = '&nbsp; <span>[<a href="'
 304          + 'thread/' + data.no + (data.semantic_url ? ('/' + data.semantic_url) : '')
 305          + '" class="replylink" rel="canonical">Reply</a>]</span>'
 306      }
 307      
 308      resto = data.no;
 309    }
 310    else {
 311      resto = data.resto;
 312    }
 313    
 314    
 315    if (!Main.tid || board != Main.board) {
 316      noLink = 'thread/' + resto + '#p' + data.no;
 317      quoteLink = 'thread/' + resto + '#q' + data.no;
 318    }
 319    else {
 320      noLink = '#p' + data.no;
 321      quoteLink = 'javascript:quote(\'' + data.no + '\')';
 322    }
 323    
 324    if (!data.capcode && data.id) {
 325      userId = ' <span class="posteruid id_'
 326        + data.id + '">(ID: <span class="hand" title="Highlight posts by this ID">'
 327        + data.id + '</span>)</span> ';
 328    }
 329    else {
 330      userId = '';
 331    }
 332    
 333    switch (data.capcode) {
 334      case 'admin_highlight':
 335        highlight = ' highlightPost';
 336      case 'admin':
 337        capcodeStart = ' <strong class="capcode hand id_admin"'
 338          + 'title="Highlight posts by the Administrator">## Admin</strong>';
 339        capcodeClass = ' capcodeAdmin';
 340        
 341        capcode = ' <img src="' + Parser.icons.admin + '" '
 342          + 'alt="This user is the 4chan Administrator." '
 343          + 'title="This user is the 4chan Administrator." class="identityIcon">';
 344        break;
 345      case 'mod':
 346        capcodeStart = ' <strong class="capcode hand id_mod" '
 347          + 'title="Highlight posts by Moderators">## Mod</strong>';
 348        capcodeClass = ' capcodeMod';
 349        
 350        capcode = ' <img src="' + Parser.icons.mod + '" '
 351          + 'alt="This user is a 4chan Moderator." '
 352          + 'title="This user is a 4chan Moderator." class="identityIcon">';
 353        break;
 354      case 'developer':
 355        capcodeStart = ' <strong class="capcode hand id_developer" '
 356          + 'title="Highlight posts by Developers">## Developer</strong>';
 357        capcodeClass = ' capcodeDeveloper';
 358        
 359        capcode = ' <img src="' + Parser.icons.dev + '" '
 360          + 'alt="This user is a 4chan Developer." '
 361          + 'title="This user is a 4chan Developer." class="identityIcon">';
 362        break;
 363      case 'manager':
 364        capcodeStart = ' <strong class="capcode hand id_manager" '
 365          + 'title="Highlight posts by Managers">## Manager</strong>';
 366        capcodeClass = ' capcodeManager';
 367        
 368        capcode = ' <img src="' + Parser.icons.manager + '" '
 369          + 'alt="This user is a 4chan Manager." '
 370          + 'title="This user is a 4chan Manager." class="identityIcon">';
 371        break;
 372    }
 373    
 374    if (data.email) {
 375      emailStart = '<a href="mailto:' + data.email.replace(/ /g, '%20') + '" class="useremail">';
 376      emailEnd = '</a>';
 377    }
 378    
 379    if (data.country) {
 380      if (board == 'pol') {
 381        flag = ' <img src="//s.4cdn.org/image/country/troll/'
 382          + data.country.toLowerCase() + '.gif" alt="'
 383          + data.country + '" title="' + data.country_name + '" class="countryFlag">';
 384      }
 385      else {
 386        flag = ' <span title="' + data.country_name + '" class="flag flag-'
 387          + data.country.toLowerCase() + '"></span>';
 388      }
 389    }
 390    else {
 391      flag = '';
 392    }
 393    
 394    if (data.filedeleted) {
 395      fileHtml = '<div id="f' + data.no + '" class="file"><span class="fileThumb"><img src="'
 396        + Parser.icons.del + '" class="fileDeletedRes" alt="File deleted."></span></div>';
 397    }
 398    else if (data.ext) {
 399      decodedFilename = Parser.decodeSpecialChars(data.filename);
 400      
 401      shortFile = longFile = data.filename + data.ext;
 402      
 403      if (decodedFilename.length > (isOP ? 40 : 30)) {
 404        shortFile = Parser.encodeSpecialChars(
 405          decodedFilename.slice(0, isOP ? 35 : 25)
 406        ) + '(...)' + data.ext;
 407        
 408        needFileTip = true;
 409      }
 410      
 411      if (!data.tn_w && !data.tn_h && data.ext == '.gif') {
 412        data.tn_w = data.w;
 413        data.tn_h = data.h;
 414      }
 415      if (data.fsize >= 1048576) {
 416        size = ((0 | (data.fsize / 1048576 * 100 + 0.5)) / 100) + ' M';
 417      }
 418      else if (data.fsize > 1024) {
 419        size = (0 | (data.fsize / 1024 + 0.5)) + ' K';
 420      }
 421      else {
 422        size = data.fsize + ' ';
 423      }
 424      
 425      if (data.spoiler) {
 426        if (!Config.revealSpoilers) {
 427          fileName = 'Spoiler Image';
 428          fileSpoilerTip = '" title="' + longFile + '"';
 429          fileClass = ' imgspoiler';
 430          
 431          fileThumb = '//s.4cdn.org/image/spoiler'
 432            + (Parser.customSpoiler[board] || '') + '.png';
 433          data.tn_w = 100;
 434          data.tn_h = 100;
 435          
 436          noFilename = true;
 437        }
 438        else {
 439          fileName = shortFile;
 440        }
 441      }
 442      else {
 443        fileName = shortFile;
 444      }
 445      
 446      if (!fileThumb) {
 447        fileThumb = '//0.t.4cdn.org/' + board + '/' + data.tim + 's.jpg';
 448      }
 449      
 450      fileDims = data.ext == '.pdf' ? 'PDF' : data.w + 'x' + data.h;
 451      
 452      if (board != 'f') {
 453        filePath = imgDir + '/' + data.tim + data.ext;
 454        
 455        imgSrc = '<a class="fileThumb' + fileClass + '" href="' + filePath
 456          + '" target="_blank"><img src="' + fileThumb
 457          + '" alt="' + size + 'B" data-md5="' + data.md5
 458          + '" style="height: ' + data.tn_h + 'px; width: '
 459          + data.tn_w + 'px;">'
 460          + '<div class="mFileInfo mobile">' + size + 'B '
 461          + data.ext.slice(1).toUpperCase()
 462          + '</div></a>';
 463        
 464        fileInfo = '<div class="fileText" id="fT' + data.no + fileSpoilerTip
 465          + '>File: <a' + (needFileTip ? (' title="' + longFile + '"') : '')
 466          + ' href="' + filePath + '" target="_blank">'
 467          + fileName + '</a> (' + size + 'B, ' + fileDims + ')</div>';
 468      }
 469      else {
 470        filePath = imgDir + '/' + data.filename + data.ext;
 471        
 472        fileDims += ', ' + data.tag;
 473        
 474        fileInfo = '<div class="fileText" id="fT' + data.no + '"'
 475          + '>File: <a href="' + filePath + '" target="_blank">'
 476          + data.filename + '.swf</a> (' + size + 'B, ' + fileDims + ')</div>';
 477      }
 478      
 479      fileHtml = '<div id="f' + data.no + '" class="file">'
 480        + fileInfo + imgSrc + '</div>';
 481    }
 482    
 483    if (data.trip) {
 484      tripcode = ' <span class="postertrip">' + data.trip + '</span>';
 485    }
 486    
 487    name = data.name || '';
 488    
 489    
 490    if (isOP) {
 491      if (data.capcode_replies) {
 492        capcode_replies = Parser.buildCapcodeReplies(data.capcode_replies, board, data.no);
 493      }
 494      
 495      if (fromQuote && data.replies) {
 496        postCountStr = data.replies + ' post' + (data.replies > 1 ? 's' : '');
 497        
 498        if (data.images) {
 499          postCountStr += ' and ' + data.images + ' image repl' +
 500            (data.images > 1 ? 'ies' : 'y');
 501        }
 502        
 503        summary = '<span class="summary preview-summary">' + postCountStr + '.</span>';
 504      }
 505      
 506      if (data.sticky) {
 507        threadIcons += '<img class="stickyIcon retina" title="Sticky" alt="Sticky" src="'
 508          + Main.icons2.sticky + '"> ';
 509      }
 510      
 511      if (data.closed) {
 512        if (data.archived) {
 513          threadIcons += '<img class="archivedIcon retina" title="Archived" alt="Archived" src="'
 514            + Main.icons2.archived + '"> ';
 515        }
 516        else {
 517          threadIcons += '<img class="closedIcon retina" title="Closed" alt="Closed" src="'
 518            + Main.icons2.closed + '"> ';
 519        }
 520      }
 521      
 522      subject = '<span class="subject">' + (data.sub || '') + '</span> ';
 523    }
 524    else {
 525      subject = '';
 526    }
 527    
 528    container.className = 'postContainer ' + postType + 'Container';
 529    container.id = 'pc' + data.no;
 530    
 531    container.innerHTML =
 532      (isOP ? '' : '<div class="sideArrows" id="sa' + data.no + '">&gt;&gt;</div>') +
 533      '<div id="p' + data.no + '" class="post ' + postType + highlight + '">' +
 534        '<div class="postInfoM mobile" id="pim' + data.no + '">' +
 535          '<span class="nameBlock' + capcodeClass + '">' +
 536          '<span class="name">' + name + '</span>' + tripcode +
 537          capcodeStart + capcode + userId + flag +
 538          '<br>' + subject +
 539          '</span><span class="dateTime postNum" data-utc="' + data.time + '">' +
 540          data.now + ' <a href="' + data.no + '#p' + data.no + '" title="Link to this post">No.</a>' +
 541          '<a href="javascript:quote(\'' + data.no + '\');" title="Reply to this post">' +
 542          data.no + '</a></span>' +
 543        '</div>' +
 544        (isOP ? fileHtml : '') +
 545        '<div class="postInfo desktop" id="pi' + data.no + '"' +
 546          (board != Main.board ? (' data-board="' + board + '"') : '') + '>' +
 547          '<input type="checkbox" name="' + data.no + '" value="delete"> ' +
 548          subject +
 549          '<span class="nameBlock' + capcodeClass + '">' + emailStart +
 550            '<span class="name">' + name + '</span>' +
 551            tripcode + capcodeStart + emailEnd + capcode + userId + flag +
 552          ' </span> ' +
 553          '<span class="dateTime" data-utc="' + data.time + '">' + data.now + '</span> ' +
 554          '<span class="postNum desktop">' +
 555            '<a href="' + noLink + '" title="Link to this post">No.</a><a href="' +
 556            quoteLink + '" title="Reply to this post">' + data.no + '</a> '
 557              + threadIcons + replySpan +
 558          '</span>' +
 559        '</div>' +
 560        (isOP ? '' : fileHtml) +
 561        '<blockquote class="postMessage" id="m' + data.no + '">'
 562        + (data.com || '') + capcode_replies + summary + '</blockquote> ' +
 563      '</div>' + mobileLink;
 564    
 565    if (!Main.tid || board != Main.board) {
 566      quotes = container.getElementsByClassName('quotelink');
 567      for (i = 0; q = quotes[i]; ++i) {
 568        href = q.getAttribute('href');
 569        if (href.charAt(0) != '/') {
 570          q.href = '/' + board + '/thread/' + resto + href;
 571        }
 572      }
 573    }
 574    
 575    return container;
 576  };
 577  
 578  Parser.buildCapcodeReplies = function(replies, board, tid) {
 579    var i, capcode, id, html, map, post_ids, prelink, pretext;
 580    
 581    map = {
 582      admin: 'Administrator',
 583      mod: 'Moderator',
 584      developer: 'Developer',
 585      manager: 'Manager'
 586    };
 587    
 588    if (board != Main.board) {
 589      prelink = '/' + board + '/thread/';
 590      pretext = '&gt;&gt;&gt;/' + board + '/';
 591    }
 592    else {
 593      prelink = '';
 594      pretext = '&gt;&gt;';
 595    }
 596    
 597    html = '<br><br><span class="capcodeReplies"><span class="smaller">';
 598    
 599    for (capcode in replies) {
 600      html += '<span class="bold">' + map[capcode] + ' Replies:</span> ';
 601      
 602      post_ids = replies[capcode];
 603      
 604      for (i = 0; id = post_ids[i]; ++i) {
 605        html += '<a class="quotelink" href="'
 606          + prelink + tid + '#p' + id + '">' + pretext + id + '</a> ';
 607      }
 608    }
 609    
 610    return html + '</span></span>';
 611  };
 612  
 613  Parser.parseBoard = function() {
 614    var i, threads = document.getElementsByClassName('thread');
 615    
 616    for (i = 0; threads[i]; ++i) {
 617      Parser.parseThread(threads[i].id.slice(1));
 618    }
 619  };
 620  
 621  Parser.parseThread = function(tid, offset, limit) {
 622    var i, j, thread, posts, pi, el, frag, summary, omitted, key, filtered, cnt,
 623      frag;
 624    
 625    thread = $.id('t' + tid);
 626    posts = thread.getElementsByClassName('post');
 627    
 628    if (!offset) {
 629      pi = document.getElementById('pi' + tid);
 630      
 631      if (!Main.tid) {
 632        if (Config.filter) {
 633          filtered = Filter.exec(
 634            thread,
 635            pi, 
 636            document.getElementById('m' + tid),
 637            tid
 638          );
 639        }
 640        
 641        if (Config.threadHiding && !filtered) {
 642          if (Main.hasMobileLayout) {
 643            el = document.createElement('a');
 644            el.href = 'javascript:;';
 645            el.setAttribute('data-cmd', 'hide');
 646            el.setAttribute('data-id', tid);
 647            el.className = 'mobileHideButton button';
 648            el.textContent = 'Hide';
 649            posts[0].nextElementSibling.appendChild(el);
 650          }
 651          else {
 652            el = document.createElement('span');
 653            el.innerHTML = '<img alt="H" class="extButton threadHideButton"'
 654              + 'data-cmd="hide" data-id="' + tid + '" src="'
 655              + Main.icons.minus + '" title="Hide thread">';
 656            posts[0].insertBefore(el, posts[0].firstChild);
 657          }
 658          el.id = 'sa' + tid;
 659          if (ThreadHiding.hidden[tid]) {
 660            ThreadHiding.hidden[tid] = Main.now;
 661            ThreadHiding.hide(tid);
 662          }
 663        }
 664        
 665        if (ThreadExpansion.enabled
 666            && (summary = $.cls('summary', thread)[0])) {
 667          frag = document.createDocumentFragment();
 668          
 669          omitted = summary.cloneNode(true);
 670          omitted.className = '';
 671          summary.textContent = '';
 672          
 673          el = document.createElement('img');
 674          el.className = 'extButton expbtn';
 675          el.title = 'Expand thread';
 676          el.alt = '+';
 677          el.setAttribute('data-cmd', 'expand');
 678          el.setAttribute('data-id', tid);
 679          el.src = Main.icons.plus;
 680          frag.appendChild(el);
 681          
 682          frag.appendChild(omitted);
 683          
 684          el = document.createElement('span');
 685          el.style.display = 'none';
 686          el.textContent = 'Showing all replies.'
 687          frag.appendChild(el);
 688          
 689          summary.appendChild(frag);
 690        }
 691      }
 692      
 693      if (Main.tid && Config.threadWatcher && (cnt = $.cls('navLinksBot')[0])) {
 694        el = document.createElement('img');
 695        
 696        if (ThreadWatcher.watched[key = tid + '-' + Main.board]) {
 697          el.src = Main.icons.watched;
 698          el.setAttribute('data-active', '1');
 699        }
 700        else {
 701          el.src = Main.icons.notwatched;
 702        }
 703        
 704        el.className = 'extButton wbtn wbtn-' + key;
 705        el.setAttribute('data-cmd', 'watch');
 706        el.setAttribute('data-id', tid);
 707        el.alt = 'W';
 708        el.title = 'Add to watch list';
 709        
 710        frag = document.createDocumentFragment();
 711        frag.appendChild(document.createTextNode('['));
 712        frag.appendChild(el.cloneNode(true));
 713        frag.appendChild(document.createTextNode('] '));
 714        cnt.insertBefore(frag, cnt.firstChild);
 715      }
 716    }
 717    
 718    j = offset ? offset < 0 ? posts.length + offset : offset : 0;
 719    limit = limit ? j + limit : posts.length;
 720    
 721    if (Main.isMobileDevice && Config.quotePreview) {
 722      for (i = j; i < limit; ++i) {
 723        Parser.parseMobileQuotelinks(posts[i]);
 724      }
 725    }
 726    
 727    if (Parser.trackedReplies) {
 728      for (i = j; i < limit; ++i) {
 729        Parser.parseTrackedReplies(posts[i]);
 730      }
 731    }
 732    
 733    for (i = j; i < limit; ++i) {
 734      Parser.parsePost(posts[i].id.slice(1), tid);
 735    }
 736    
 737    if (offset) {
 738      if (Parser.prettify) {
 739        for (i = j; i < limit; ++i) {
 740          Parser.parseMarkup(posts[i]);
 741        }
 742      }
 743      if (window.jsMath) {
 744        if (window.jsMath.loaded) {
 745          for (i = j; i < limit; ++i) {
 746            window.jsMath.ProcessBeforeShowing(posts[i]);
 747          }
 748        }
 749        else {
 750          Parser.loadJSMath();
 751        }
 752      }
 753    }
 754    
 755    UA.dispatchEvent('4chanParsingDone', { threadId: tid, offset: j, limit: limit });
 756  };
 757  
 758  Parser.loadJSMath = function(root) {
 759    if ($.cls('math', root)[0]) {
 760      window.jsMath.Autoload.Script.Push('ProcessBeforeShowing', [ null ]);
 761      window.jsMath.Autoload.LoadJsMath();
 762    }
 763  };
 764  
 765  Parser.parseMathOne = function(node) {
 766    if (window.jsMath.loaded) {
 767      window.jsMath.ProcessBeforeShowing(node);
 768    }
 769    else {
 770      Parser.loadJSMath(node);
 771    }
 772  };
 773  
 774  Parser.parseTrackedReplies = function(post) {
 775    var i, link, quotelinks;
 776    
 777    quotelinks = $.cls('quotelink', post);
 778    
 779    for (i = 0; link = quotelinks[i]; ++i) {
 780      if (Parser.trackedReplies[link.textContent]) {
 781        link.textContent += ' (You)';
 782        Parser.hasYouMarkers = true;
 783      }
 784    }
 785  };
 786  
 787  Parser.parseMobileQuotelinks = function(post) {
 788    var i, link, quotelinks, t, el;
 789    
 790    quotelinks = $.cls('quotelink', post);
 791    
 792    for (i = 0; link = quotelinks[i]; ++i) {
 793      t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/);
 794      
 795      if (!t) {
 796        continue;
 797      }
 798      
 799      el = document.createElement('a');
 800      el.href = link.href;
 801      el.textContent = ' #';
 802      el.className = 'quoteLink';
 803      
 804      link.parentNode.insertBefore(el, link.nextSibling);
 805    }
 806  };
 807  
 808  Parser.parseMarkup = function(post) {
 809    var i, pre, el;
 810    
 811    if ((pre = post.getElementsByClassName('prettyprint'))[0]) {
 812      for (i = 0; el = pre[i]; ++i) {
 813        el.innerHTML = prettyPrintOne(el.innerHTML);
 814      }
 815    }
 816  };
 817  
 818  Parser.parsePost = function(pid, tid) {
 819    var hasMobileLayout, cnt, el, pi, href, img, file, msg, filtered, html, filename, txt, finfo, isOP, uid;
 820    
 821    hasMobileLayout = Main.hasMobileLayout;
 822    
 823    if (!tid) {
 824      pi = pid.getElementsByClassName('postInfo')[0];
 825      pid = pi.id.slice(2);
 826    }
 827    else {
 828      pi = document.getElementById('pi' + pid);
 829    }
 830    
 831    if (Parser.needMsg) {
 832      msg = document.getElementById('m' + pid);
 833    }
 834    
 835    if (hasMobileLayout) {
 836      if (Config.reportButton) {
 837        el = document.createElement('span');
 838        el.className = 'mobile mobile-report';
 839        el.setAttribute('data-cmd', 'report');
 840        el.setAttribute('data-id', pid);
 841        el.textContent = 'Report';
 842        pi.parentNode.appendChild(el);
 843      }
 844    }
 845    else {
 846      el = document.createElement('a');
 847      el.href = '#';
 848      el.className = 'postMenuBtn';
 849      el.title = 'Post menu';
 850      el.setAttribute('data-cmd', 'post-menu');
 851      el.textContent = 'โ–ถ';
 852      pi.appendChild(el);
 853    }
 854    
 855    if (tid && pid != tid) {
 856      if (Config.filter) {
 857        filtered = Filter.exec(pi.parentNode, pi, msg);
 858      }
 859      
 860      if (!filtered && ReplyHiding.hidden[pid]) {
 861        ReplyHiding.hidden[pid] = Main.now;
 862        ReplyHiding.hide(pid);
 863      }
 864      
 865      if (Config.backlinks) {
 866        Parser.parseBacklinks(pid, tid);
 867      }
 868    }
 869    
 870    if (IDColor.enabled && (uid = $.cls('posteruid', pi.parentNode)[hasMobileLayout ? 0 : 1])) {
 871      IDColor.apply(uid.firstElementChild);
 872    }
 873    
 874    if (Config.embedSoundCloud) {
 875      Media.parseSoundCloud(msg);
 876    }
 877    
 878    if (Config.embedYouTube) {
 879      Media.parseYouTube(msg);
 880    }
 881    
 882    if (Config.embedVocaroo) {
 883      Media.parseVocaroo(msg);
 884    }
 885    
 886    if (Config.revealSpoilers
 887        && (file = document.getElementById('f' + pid))
 888        && (file = file.children[1])
 889      ) {
 890      if ($.hasClass(file, 'imgspoiler')) {
 891        img = file.firstChild;
 892        file.removeChild(img);
 893        img.removeAttribute('style');
 894        isOP = $.hasClass(pi.parentNode, 'op');
 895        img.style.maxWidth = img.style.maxHeight = isOP ? '250px' : '125px';
 896        img.src = '//0.t.4cdn.org'
 897          + (file.pathname.replace(/([0-9]+).+$/, '/$1s.jpg'));
 898        
 899        filename = file.previousElementSibling;
 900        finfo = filename.title.split('.');
 901        
 902        if (finfo[0].length > (isOP ? 40 : 30)) {
 903          txt = finfo[0].slice(0, isOP ? 35 : 25) + '(...)' + finfo[1];
 904        }
 905        else {
 906          txt = filename.title;
 907          filename.removeAttribute('title');
 908        }
 909        
 910        filename.firstElementChild.innerHTML = txt;
 911        file.insertBefore(img, file.firstElementChild);
 912      }
 913    }
 914    
 915    if (Config.localTime) {
 916      if (hasMobileLayout) {
 917        el = pi.parentNode.getElementsByClassName('dateTime')[0];
 918        el.firstChild.nodeValue
 919          = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000)) + ' ';
 920      }
 921      else {
 922        el = pi.getElementsByClassName('dateTime')[0];
 923        el.title = this.utcOffset;
 924        el.textContent
 925          = Parser.getLocaleDate(new Date(el.getAttribute('data-utc') * 1000));
 926      }
 927    }
 928    
 929  };
 930  
 931  Parser.getLocaleDate = function(date) {
 932    return ('0' + (1 + date.getMonth())).slice(-2) + '/'
 933      + ('0' + date.getDate()).slice(-2) + '/'
 934      + ('0' + date.getFullYear()).slice(-2) + '('
 935      + this.weekdays[date.getDay()] + ')'
 936      + ('0' + date.getHours()).slice(-2) + ':'
 937      + ('0' + date.getMinutes()).slice(-2) + ':'
 938      + ('0' + date.getSeconds()).slice(-2);
 939  };
 940  
 941  Parser.parseBacklinks = function(pid, tid) {
 942    var i, j, msg, backlinks, linklist, ids, target, bid, html, bl, el, href;
 943    
 944    msg = document.getElementById('m' + pid);
 945    
 946    if (!(backlinks = msg.getElementsByClassName('quotelink'))) {
 947      return;
 948    }
 949    
 950    linklist = {};
 951    
 952    for (i = 0; j = backlinks[i]; ++i) {
 953      // [tid, pid]
 954      ids = j.getAttribute('href').split('#p');
 955      
 956      if (!ids[1]) {
 957        continue;
 958      }
 959      
 960      if (ids[1] == tid) {
 961        j.textContent += ' (OP)';
 962      }
 963      
 964      if (!(target = document.getElementById('pi' + ids[1]))) {
 965        if (Main.tid && j.textContent.charAt(2) != '>' ) {
 966          j.textContent += ' โ†’';
 967        }
 968        continue;
 969      }
 970      
 971      // Already processed?
 972      if (linklist[ids[1]]) {
 973        continue;
 974      }
 975      
 976      linklist[ids[1]] = true;
 977      
 978      // Backlink node
 979      bl = document.createElement('span');
 980      
 981      if (!Main.tid) {
 982        href = 'thread/' + tid + '#p' + pid;
 983      }
 984      else {
 985        href = '#p' + pid;
 986      }
 987      
 988      if (!Main.hasMobileLayout) {
 989        bl.innerHTML = '<a href="' + href + '" class="quotelink">&gt;&gt;' + pid + '</a> ';
 990      }
 991      else {
 992        bl.innerHTML = '<a href="' + href + '" class="quotelink">&gt;&gt;' + pid
 993          + '</a><a href="' + href + '" class="quoteLink"> #</a> ';
 994      }
 995      
 996      // Backlinks container
 997      if (!(el = document.getElementById('bl_' + ids[1]))) {
 998        el = document.createElement('div');
 999        el.id = 'bl_' + ids[1];
1000        el.className = 'backlink';
1001        
1002        if (Main.hasMobileLayout) {
1003          el.className = 'backlink mobile';
1004          target = document.getElementById('p' + ids[1]);
1005        }
1006        
1007        target.appendChild(el);
1008      }
1009      
1010      el.appendChild(bl);
1011    }
1012  };
1013  
1014  Parser.buildSummary = function(tid, oRep, oImg) {
1015    if (oRep) {
1016      oRep = oRep + ' post' + (oRep > 1 ? 's' : '');
1017    }
1018    else {
1019      return null;
1020    }
1021    
1022    if (oImg) {
1023      oImg = ' and ' + oImg + ' image repl' + (oImg > 1 ? 'ies' : 'y');
1024    }
1025    else {
1026      oImg = '';
1027    }
1028    
1029    el = document.createElement('span');
1030    el.className = 'summary desktop';
1031    el.innerHTML = oRep + oImg
1032      + ' omitted. <a href="thread/'
1033      + tid + '" class="replylink">Click here</a> to view.';
1034    
1035    return el;
1036  };
1037  
1038  /**
1039   * Sync
1040   */
1041  var UserSync = {
1042    url: 'https://sys.4chan.org/sync',
1043    timeout: null,
1044    processing: false,
1045    maxDelay: 3600000,
1046    queue: {}
1047  };
1048  
1049  UserSync.onEnable = function() {
1050    var tkn = Math.random().toString(16).substring(2)
1051      + Math.random().toString(16).substring(2);
1052    
1053    Main.setCookie('sync', tkn, '4chan.org');
1054  };
1055  
1056  UserSync.onDisable = function() {
1057    Main.removeCookie('sync', '4chan.org');
1058    localStorage.removeItem('4chan-sync-ts');
1059  };
1060  
1061  UserSync.onSyncNowClick = function() {
1062    UserSync.syncStatus(true);
1063  };
1064  
1065  UserSync.purgeSync = function() {
1066    var xhr, tkn;
1067    
1068    tkn = Main.getCookie('sync');
1069    
1070    if (!tkn) {
1071      alert("Syncing doesn't seem to be enabled on this machine");
1072      return;
1073    }
1074    
1075    if (!confirm('All data associated with this sync key will be deleted from the server.')) {
1076      return;
1077    }
1078    
1079    xhr = new XMLHttpRequest();
1080    xhr.open('POST', UserSync.url + '?action=purge');
1081    xhr.onload = UserSync.onPurgeSyncLoaded;
1082    xhr.onerror = UserSync.onSyncError;
1083    xhr.withCredentials = true;
1084    xhr.withFeedback = true;
1085    
1086    //Feedback.notify('Processingโ€ฆ', false);
1087    
1088    xhr.send(JSON.stringify({tkn: tkn}));
1089  };
1090  
1091  UserSync.onPurgeSyncLoaded = function() {
1092    var resp = JSON.parse(this.responseText);
1093    
1094    if (resp.error) {
1095      return Feedback.error(resp.error);
1096    }
1097    
1098    //Feedback.notify('Done');
1099    
1100    UserSync.onDisable();
1101  };
1102  
1103  UserSync.syncStatus = function(withFeedback) {
1104    var xhr;
1105    
1106    if (UserSync.processing) {
1107      console.log('Sync: Already syncing');
1108      return;
1109    }
1110    
1111    UserSync.processing = true;
1112    
1113    if (withFeedback) {
1114      //Feedback.notify('Syncingโ€ฆ', false);
1115    }
1116    
1117    xhr = new XMLHttpRequest();
1118    xhr.open('GET', UserSync.url + '?action=status');
1119    xhr.withCredentials = true;
1120    xhr.withFeedback = withFeedback;
1121    xhr.onerror = UserSync.onSyncError;
1122    xhr.onload = UserSync.onSyncStatusLoaded;
1123    xhr.send(null);
1124  };
1125  
1126  UserSync.onSyncStatusLoaded = function() {
1127    var i, key, item, items, get, set, req, xhr, local_ts, remote_ts, data, syncTs, tkn;
1128    
1129    UserSync.processing = false;
1130    
1131    items = JSON.parse(this.responseText);
1132    
1133    if (items.error) {
1134      console.log('Sync: ' + items.error);
1135      return;
1136    }
1137    
1138    syncTs = UserSync.getSyncTs();
1139    syncTs.ts = Date.now();
1140    
1141    get = [];
1142    set = {};
1143    
1144    for (key in items) {
1145      local_ts = syncTs[key] || 0;
1146      remote_ts = items[key] || 0;
1147      
1148      if (remote_ts > local_ts) {
1149        get.push(key);
1150      }
1151      else if (local_ts > remote_ts) {
1152        data = localStorage.getItem(key);
1153        
1154        if (data) {
1155          set[key] = {
1156            ts: local_ts,
1157            data: JSON.parse(data)
1158          };
1159        }
1160        else {
1161          delete syncTs[key];
1162        }
1163      }
1164    }
1165    
1166    UserSync.setSyncTs(syncTs);
1167    
1168    req = {};
1169    
1170    if (get.length) {
1171      req['get'] = get;
1172    }
1173    
1174    for (i in set) {
1175      req['set'] = set;
1176      break;
1177    }
1178    
1179    if (!req['get'] && !req['set']) {
1180      if (this.withFeedback) {
1181        //Feedback.notify('Done');
1182      }
1183      if (Config.threadWatcher) {
1184        ThreadWatcher.onUserSyncLoaded();
1185      }
1186      return;
1187    }
1188    
1189    tkn = Main.getCookie('sync');
1190    
1191    if (!tkn) {
1192      return;
1193    }
1194    
1195    req.tkn = tkn;
1196    
1197    xhr = new XMLHttpRequest();
1198    xhr.open('POST', UserSync.url + '?action=sync');
1199    xhr.withCredentials = true;
1200    xhr.withFeedback = this.withFeedback;
1201    xhr.onload = UserSync.onSyncLoaded;
1202    xhr.onerror = UserSync.onSyncError;
1203    xhr.send(JSON.stringify(req));
1204  };
1205  
1206  UserSync.onSyncError = function() {
1207    var msg = 'Sync: Connection Error';
1208    
1209    UserSync.processing = false;
1210    
1211    UserSync.resetSyncTs();
1212    
1213    console.log(msg);
1214  };
1215  
1216  UserSync.getSyncTs = function() {
1217    var data = localStorage.getItem('4chan-sync-ts');
1218    
1219    return data ? JSON.parse(data) : {};
1220  };
1221  
1222  UserSync.setSyncTs = function(data) {
1223    return localStorage.setItem('4chan-sync-ts', JSON.stringify(data));
1224  };
1225  
1226  UserSync.resetSyncTs = function() {
1227    var tsData = UserSync.getSyncTs();
1228    delete tsData.ts;
1229    UserSync.setSyncTs(tsData);
1230  };
1231  
1232  UserSync.onSyncLoaded = function() {
1233    var items, key, value, local_ts, syncTs;
1234    
1235    items = JSON.parse(this.responseText);
1236    
1237    if (items.error) {
1238      console.log('Sync: ' + items.error);
1239      return;
1240    }
1241    
1242    if (this.withFeedback) {
1243      //Feedback.notify('Done');
1244    }
1245    
1246    syncTs = UserSync.getSyncTs();
1247    syncTs.ts = Date.now();
1248    
1249    for (key in items) {
1250      value = items[key];
1251      
1252      local_ts = syncTs[key] || 0;
1253      
1254      if (+local_ts > +value['ts']) {
1255        continue;
1256      }
1257      
1258      localStorage.setItem(key, JSON.stringify(value['data']));
1259      
1260      syncTs[key] = value['ts'];
1261    }
1262    
1263    UserSync.setSyncTs(syncTs);
1264    
1265    if (Config.threadWatcher) {
1266      ThreadWatcher.onUserSyncLoaded();
1267    }
1268  };
1269    
1270  UserSync.onQueueProcessed = function() {
1271    var items;
1272    
1273    items = JSON.parse(this.responseText);
1274    
1275    if (items.error) {
1276      console.log('Sync: ' + items.error);
1277    }
1278  }
1279    
1280  UserSync.syncPush = function(key) {
1281    var ts, tsData;
1282    
1283    ts = Math.round(Date.now() / 1000);
1284    
1285    tsData = UserSync.getSyncTs();
1286    tsData[key] = ts;
1287    UserSync.setSyncTs(tsData);
1288    
1289    UserSync.queue[key] = ts;
1290    
1291    if (UserSync.timeout) {
1292      clearTimeout(UserSync.timeout);
1293    }
1294    
1295    UserSync.timeout = setTimeout(UserSync.syncProcessQueue, 1000);
1296  };
1297    
1298  UserSync.syncProcessQueue = function() {
1299    var set, xhr, key, tkn;
1300    
1301    tkn = Main.getCookie('sync');
1302    
1303    if (!tkn) {
1304      UserSync.queue = {};
1305      return;
1306    }
1307    
1308    set = {};
1309    
1310    for (key in UserSync.queue) {
1311      set[key] = {
1312        ts: UserSync.queue[key],
1313        data: JSON.parse(localStorage.getItem(key))
1314      }
1315    }
1316    
1317    UserSync.queue = {};
1318    
1319    xhr = new XMLHttpRequest();
1320    xhr.open('POST', UserSync.url + '?action=sync');
1321    xhr.withCredentials = true;
1322    xhr.onload = UserSync.onQueueProcessed;
1323    xhr.onerror = UserSync.onSyncError;
1324    xhr.send(JSON.stringify({
1325      tkn: tkn,
1326      set: set
1327    }));
1328  };
1329  
1330  
1331  /**
1332   * Post Menu
1333   */
1334  var PostMenu = {
1335    activeBtn: null
1336  };
1337  
1338  PostMenu.open = function(btn) {
1339    var div, html, pid, board, btnPos, txt, el, href, left, limit, isOP;
1340    
1341    PostMenu.close();
1342    
1343    pid = btn.parentNode.id.split('pi')[1];
1344    
1345    board = btn.parentNode.getAttribute('data-board');
1346    
1347    isOP = !board && !!$.id('t' + pid);
1348    
1349    html = '<ul><li data-cmd="report" data-id="' + pid
1350      + (board ? ('" data-board="' + board + '"') : '"')
1351      + '">Report post</li>';
1352    
1353    if (isOP) {
1354      if (!Main.tid) {
1355        html += '<li data-cmd="hide" data-id="' + pid + '">'
1356          + ($.hasClass($.id('t' + pid), 'post-hidden') ? 'Unhide' : 'Hide')
1357          + ' thread</li>';
1358      }
1359      if (Config.threadWatcher) {
1360        html += '<li data-cmd="watch" data-id="' + pid + '">'
1361          + (ThreadWatcher.watched[pid + '-' + Main.board] ? 'Remove from' : 'Add to')
1362          + ' watch list</li>';
1363      }
1364    }
1365    else if (el = $.id('pc' + pid)) {
1366      html += '<li data-cmd="hide-r" data-id="' + pid + '">'
1367        + ($.hasClass(el, 'post-hidden') ? 'Unhide' : 'Hide')
1368        + ' post</li>';
1369    }
1370    
1371    if (file = $.id('fT' + pid)) {
1372      el = $.cls('fileThumb', file.parentNode)[0];
1373      
1374      if (el) {
1375        if (/\.(png|jpg)$/.test(el.href)) {
1376          href = el.href;
1377        }
1378        else {
1379          href = 'http://0.t.4cdn.org/' + Main.board + '/'
1380            + el.href.match(/\/([0-9]+)\..+$/)[1] + 's.jpg';
1381        }
1382        
1383        html += '<li><ul>'
1384          + '<li><a href="//www.google.com/searchbyimage?image_url=' + href
1385          + '" target="_blank">Google</a></li>'
1386          + '<li><a href="http://iqdb.org/?url='
1387          + href + '" target="_blank">iqdb</a></li></ul>Image search &raquo</li>';
1388      }
1389    }
1390    
1391    if (Config.filter) {
1392      html += '<li><a href="#" data-cmd="filter-sel">Filter selected text</a></li>';
1393    }
1394    
1395    div = document.createElement('div');
1396    div.id = 'post-menu';
1397    div.className = 'dd-menu';
1398    div.innerHTML = html + '</ul>';
1399    
1400    btnPos = btn.getBoundingClientRect();
1401    
1402    div.style.top = btnPos.bottom + 3 + window.pageYOffset + 'px';
1403    
1404    document.addEventListener('click', PostMenu.close, false);
1405    
1406    $.addClass(btn, 'menuOpen');
1407    PostMenu.activeBtn = btn;
1408    
1409    UA.dispatchEvent('4chanPostMenuReady', { postId: pid, isOP: isOP, node: div.firstElementChild });
1410    
1411    document.body.appendChild(div);
1412    
1413    left = btnPos.left + window.pageXOffset;
1414    limit = $.docEl.clientWidth - div.offsetWidth;
1415    
1416    if (left > (limit - 75)) {
1417      div.className += ' dd-menu-left';
1418    }
1419    
1420    if (left > limit) {
1421      left = limit;
1422    }
1423    
1424    div.style.left = left + 'px';
1425  };
1426  
1427  PostMenu.close = function() {
1428    var el;
1429    
1430    if (el = $.id('post-menu')) {
1431      el.parentNode.removeChild(el);
1432      document.removeEventListener('click', PostMenu.close, false);
1433      $.removeClass(PostMenu.activeBtn, 'menuOpen');
1434      PostMenu.activeBtn = null;
1435    }
1436  };
1437  
1438  /**
1439   * Depager
1440   */
1441  var Depager = {};
1442  
1443  Depager.init = function() {
1444    var el, el2, cnt;
1445    
1446    this.isLoading = false;
1447    this.isEnabled = false;
1448    this.isComplete = false;
1449    this.threadsLoaded = false;
1450    this.threadQueue = [];
1451    this.debounce = 100;
1452    this.threshold = 350;
1453    
1454    this.adId = 'azk53379';
1455    this.adZones = [ 16258, 16260 ];
1456    
1457    this.boardHasAds = !!$.id(this.adId);
1458    
1459    if (this.boardHasAds) {
1460      el = $.cls('ad-plea');
1461      this.adPlea = el[el.length - 1];
1462    }
1463    
1464    if (el = $.cls('prev')[0]) {
1465      el.innerHTML = '[<a title="Toggle infinite scroll" '
1466        + 'class="depagelink" href="" data-cmd="depage">All</a>]';
1467      el = el.firstElementChild;
1468    }
1469    else {
1470      return;
1471    }
1472    
1473    if (Config.alwaysDepage) {
1474      this.isEnabled = true;
1475      el.parentNode.parentNode.className += ' depagerEnabled';
1476      Depager.bindHandlers();
1477      
1478      if (cnt = $.cls('board')[0]) {
1479        el2 = document.createElement('span');
1480        el2.className = 'depageNumber';
1481        el2.textContent = 'Page 1';
1482        cnt.insertBefore(el2, cnt.firstElementChild);
1483      }
1484    }
1485    else {
1486      el.setAttribute('data-cmd', 'depage');
1487    }
1488  };
1489  
1490  Depager.onScroll = function() {
1491    if (document.documentElement.scrollHeight
1492        <= (window.innerHeight + window.pageYOffset + Depager.threshold)) {
1493      if (Depager.threadsLoaded) {
1494        Depager.renderNext();
1495      }
1496      else {
1497        Depager.depage();
1498      }
1499    }
1500  };
1501  
1502  Depager.trackPageview = function(pageId) {
1503    var url;
1504    
1505    try {
1506      if (window._gat) {
1507        url = '/' + Main.board + '/' + pageId;
1508        window._gat._getTrackerByName()._trackPageview(url);
1509      }
1510      
1511      if (window.__qc) {
1512        window.__qc.qpixelsent = [];
1513        window._qevents.push({ qacct: window.__qc.qopts.qacct });
1514        window.__qc.firepixels();
1515      }
1516    }
1517    catch(e) {
1518      console.log(e);
1519    }
1520  };
1521  
1522  Depager.insertAd = function(pageId, frag, zone, isLastPage) {
1523    var wrap, cnt, nodes;
1524    
1525    if (!Depager.boardHasAds || !window.ados_add_placement) {
1526      return;
1527    }
1528    
1529    if (isLastPage) {
1530      nodes = $.cls('bottomad');
1531      wrap = nodes[nodes.length - 1];
1532      cnt = document.createElement('div');
1533      cnt.id = 'azkDepage' + pageId;
1534      wrap.appendChild(cnt);
1535      window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone);
1536    }
1537    else {
1538      wrap = document.createElement('div');
1539      wrap.className = 'bottomad center';
1540      
1541      if (pageId == 2) {
1542        cnt = $.id(Depager.adId);
1543      }
1544      else {
1545        cnt = document.createElement('div');
1546        cnt.id = 'azkDepage' + pageId;
1547      }
1548      
1549      wrap.appendChild(cnt);
1550      frag.appendChild(wrap);
1551      
1552      if (Depager.adPlea) {
1553        frag.appendChild(Depager.adPlea.cloneNode(true));
1554      }
1555      
1556      frag.appendChild(document.createElement('hr'));
1557      
1558      if (pageId != 2) {
1559        window.ados_add_placement(3536, 18130, cnt.id, 4).setZone(zone);
1560      }
1561    }
1562  };
1563  
1564  Depager.loadAds = function() {
1565    if (!Depager.boardHasAds || !window.ados_load) {
1566      return;
1567    }
1568    
1569    window.ados_load();
1570  };
1571  
1572  Depager.renderNext = function() {
1573    var el, frag, i, j, k, threads, op, summary, cnt, reply, parseList, scroll,
1574      lastReplies, pageId, data, isLastPage, html;
1575    
1576    parseList = [];
1577      
1578    scroll = window.pageYOffset;
1579    
1580    frag = document.createDocumentFragment();
1581    
1582    data = Depager.threadQueue.shift();
1583    
1584    if (!data) {
1585      return;
1586    }
1587    
1588    threads = data.threads;
1589    pageId = data.page;
1590    
1591    isLastPage = !Depager.threadQueue.length;
1592    
1593    Depager.insertAd(pageId, frag, data.adZone, isLastPage);
1594    
1595    el = document.createElement('span');
1596    el.className = 'depageNumber';
1597    el.textContent = 'Page ' + pageId;
1598    frag.appendChild(el);
1599    
1600    for (j = 0; op = threads[j]; ++j) {
1601      if ($.id('t' + op.no)) {
1602        continue;
1603      }
1604      
1605      cnt = document.createElement('div');
1606      cnt.id = 't' + op.no;
1607      cnt.className = 'thread';
1608      
1609      cnt.appendChild(Parser.buildHTMLFromJSON(op, Main.board, true));
1610      
1611      if (summary = Parser.buildSummary(op.no, op.omitted_posts, op.omitted_images)) {
1612        cnt.appendChild(summary);
1613      }
1614      
1615      if (op.replies) {
1616        last_replies = op.last_replies;
1617        
1618        for (k = 0; reply = last_replies[k]; ++k) {
1619          cnt.appendChild(Parser.buildHTMLFromJSON(reply, Main.board));
1620        }
1621      }
1622      
1623      frag.appendChild(cnt);
1624      
1625      frag.appendChild(document.createElement('hr'));
1626      
1627      parseList.push(op.no);
1628    }
1629    
1630    if (isLastPage) {
1631      Depager.unbindHandlers();
1632      Depager.isComplete = true;
1633      Depager.setStatus('disabled');
1634    }
1635    
1636    boardDiv = $.cls('board')[0];
1637    boardDiv.insertBefore(frag, boardDiv.lastElementChild);
1638    
1639    Depager.trackPageview(pageId);
1640    
1641    Depager.loadAds();
1642    
1643    for (i = 0; op = parseList[i]; ++i) {
1644      Parser.parseThread(op);
1645    }
1646    
1647    window.scrollTo(0, scroll);
1648  };
1649  
1650  Depager.bindHandlers = function() {
1651    window.addEventListener('scroll', Depager.onScroll, false);
1652    window.addEventListener('resize', Depager.onScroll, false);
1653  };
1654  
1655  Depager.unbindHandlers = function() {
1656    window.removeEventListener('scroll', Depager.onScroll, false);
1657    window.removeEventListener('resize', Depager.onScroll, false);
1658  };
1659  
1660  Depager.setStatus = function(type) {
1661    var i, el, links, p;
1662    
1663    links = $.cls('depagelink');
1664    
1665    if (!links.length) {
1666      return;
1667    }
1668    
1669    if (type == 'enabled') {
1670      for (i = 0; el = links[i]; ++i) {
1671        el.textContent = 'All';
1672        p = el.parentNode.parentNode;
1673        if (!$.hasClass(p, 'depagerEnabled')) {
1674          $.addClass(p,'depagerEnabled');
1675        }
1676      }
1677    }
1678    else if (type == 'loading') {
1679      for (i = 0; el = links[i]; ++i) {
1680        el.textContent = 'Loadingโ€ฆ';
1681      }
1682    }
1683    else if (type == 'disabled') {
1684      for (i = 0; el = links[i]; ++i) {
1685        el.textContent = 'All';
1686        $.removeClass(el.parentNode.parentNode,'depagerEnabled');
1687      }
1688    }
1689    else if (type == 'error') {
1690      for (i = 0; el = links[i]; ++i) {
1691        el.textContent = 'Error';
1692        el.removeAttribute('title');
1693        el.removeAttribute('data-cmd');
1694        $.removeClass(el.parentNode.parentNode, 'depagerEnabled');
1695      }
1696    }
1697  };
1698  
1699  Depager.toggle = function() {
1700    if (Depager.isLoading || Depager.isComplete) {
1701      return;
1702    }
1703    
1704    if (Depager.isEnabled) {
1705      Depager.disable();
1706    }
1707    else {
1708      Depager.enable();
1709    }
1710    
1711    Depager.isEnabled = !Depager.isEnabled;
1712  };
1713  
1714  Depager.enable = function() {
1715    Depager.bindHandlers();
1716    Depager.setStatus('enabled');
1717    Depager.onScroll();
1718  };
1719  
1720  Depager.disable = function() {
1721    Depager.unbindHandlers();
1722    Depager.setStatus('disabled');
1723  };
1724  
1725  Depager.depage = function() {
1726    if (Depager.isLoading) {
1727      return;
1728    }
1729    
1730    Depager.isLoading = true;
1731    
1732    $.get('//a.4cdn.org/' + Main.board + '/catalog.json', {
1733      onload: Depager.onLoad,
1734      onerror: Depager.onError
1735    });
1736    
1737    Depager.setStatus('loading');
1738  };
1739  
1740  Depager.onLoad = function() {
1741    var catalog, i, page, queue, adZone;
1742    
1743    Depager.isLoading = false;
1744    Depager.threadsLoaded = true;
1745    
1746    if (this.status == 200) {
1747      Depager.setStatus('enabled');
1748      
1749      if (!Config.alwaysDepage) {
1750        Depager.bindHandlers();
1751      }
1752      
1753      catalog = Parser.parseCatalogJSON(this.responseText);
1754      
1755      queue = Depager.threadQueue;
1756      
1757      adZone = 0;
1758      for (i = 1; page = catalog[i]; ++i) {
1759        page.adZone = adZone;
1760        queue.push(page);
1761        adZone = adZone ? 0 : 1;
1762      }
1763      
1764      Depager.renderNext();
1765    }
1766    else if (this.status == 404) {
1767      Depager.unbindHandlers();
1768      Depager.setStatus('error');
1769    }
1770    else {
1771      Depager.unbindHandlers();
1772      console.log('Error: ' + this.status);
1773      Depager.setStatus('error');
1774    }
1775  };
1776  
1777  Depager.onError = function() {
1778    Depager.isLoading = false;
1779    Depager.unbindHandlers();
1780    console.log('Error: ' + this.status);
1781    Depager.setStatus('error');
1782  };
1783  
1784  /**
1785   * Quote inlining
1786   */
1787  var QuoteInline = {};
1788  
1789  QuoteInline.isSelfQuote = function(node, pid, board) {
1790    var cnt;
1791    
1792    if (board && board != Main.board) {
1793      return false;
1794    }
1795    
1796    node = node.parentNode;
1797    
1798    if ((node.nodeName == 'BLOCKQUOTE' && node.id.split('m')[1] == pid)
1799        || node.parentNode.id.split('_')[1] == pid) {
1800      return true;
1801    }
1802    
1803    return false;
1804  };
1805  
1806  QuoteInline.toggle = function(link, e) {
1807    var t, pfx, src, el, count;
1808    
1809    t = link.getAttribute('href').match(/^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/);
1810    
1811    if (!t || t[1] == 'rs' || QuoteInline.isSelfQuote(link, t[3], t[1])) {
1812      return;
1813    }
1814    
1815    e && e.preventDefault();
1816    
1817    if (pfx = link.getAttribute('data-pfx')) {
1818      link.removeAttribute('data-pfx');
1819      $.removeClass(link, 'linkfade');
1820      
1821      el = $.id(pfx + 'p' + t[3]);
1822      el.parentNode.removeChild(el);
1823      
1824      if (link.parentNode.parentNode.className == 'backlink') {
1825        el = $.id('pc' + t[3]);
1826        count = +el.getAttribute('data-inline-count') - 1;
1827        if (count == 0) {
1828          el.style.display = '';
1829          el.removeAttribute('data-inline-count');
1830        }
1831        else {
1832          el.setAttribute('data-inline-count', count);
1833        }
1834      }
1835      
1836      return;
1837    }
1838    
1839    if (src = $.id('p' + t[3])) {
1840      QuoteInline.inline(link, src, t[3]);
1841    }
1842    else {
1843      QuoteInline.inlineRemote(link, t[1] || Main.board, t[2], t[3]);
1844    }
1845  };
1846  
1847  QuoteInline.inlineRemote = function(link, board, tid, pid) {
1848    var xhr, onload, onerror, cached, key, el, dummy;
1849    
1850    if (link.hasAttribute('data-loading')) {
1851      return;
1852    }
1853    
1854    key = board + '-' + tid;
1855    
1856    if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) {
1857      Parser.parsePost(el);
1858      QuoteInline.inline(link, el);
1859      return;
1860    }
1861    
1862    if ((dummy = link.nextElementSibling) && $.hasClass(dummy, 'spinner')) {
1863      dummy.parentNode.removeChild(dummy);
1864      return;
1865    }
1866    else {
1867      dummy = document.createElement('div');
1868    }
1869    
1870    dummy.className = 'preview spinner inlined';
1871    dummy.textContent = 'Loading...';
1872    link.parentNode.insertBefore(dummy, link.nextSibling);
1873    
1874    onload = function() {
1875      var el, thread;
1876      
1877      link.removeAttribute('data-loading');
1878      
1879      if (this.status == 200 || this.status == 304 || this.status == 0) {
1880        thread = Parser.parseThreadJSON(this.responseText);
1881        
1882        $.cache[key] = thread;
1883        
1884        if (el = Parser.buildPost(thread, board, pid)) {
1885          dummy.parentNode && dummy.parentNode.removeChild(dummy);
1886          Parser.parsePost(el);
1887          QuoteInline.inline(link, el);
1888        }
1889        else {
1890          $.addClass(link, 'deadlink');
1891          dummy.textContent = 'This post doesn\'t exist anymore';
1892        }
1893      }
1894      else if (this.status == 404) {
1895        $.addClass(link, 'deadlink');
1896        dummy.textContent = 'This thread doesn\'t exist anymore';
1897      }
1898      else {
1899        this.onerror();
1900      }
1901    };
1902    
1903    onerror = function() {
1904      dummy.textContent = 'Error: ' + this.statusText + ' (' + this.status + ')';
1905      link.removeAttribute('data-loading');
1906    };
1907    
1908    link.setAttribute('data-loading', '1');
1909    
1910    $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json',
1911      {
1912        onload: onload,
1913        onerror: onerror
1914      }
1915    );
1916  };
1917  
1918  QuoteInline.inline = function(link, src, id) {
1919    var i, j, now, el, blcnt, isBl, inner, tblcnt, pfx, dest, count, cnt;
1920    
1921    now = Date.now();
1922    
1923    if (id) {
1924      if ((blcnt = link.parentNode.parentNode).className == 'backlink') {
1925        el = blcnt.parentNode.parentNode.parentNode;
1926        isBl = true;
1927      }
1928      else {
1929        el = blcnt.parentNode;
1930      }
1931      
1932      while (el.parentNode !== document) {
1933        if (el.id.split('m')[1] == id) {
1934          return;
1935        }
1936        el = el.parentNode;
1937      }
1938    }
1939    
1940    link.className += ' linkfade';
1941    link.setAttribute('data-pfx', now);
1942    
1943    el = src.cloneNode(true);
1944    el.id = now + el.id;
1945    el.setAttribute('data-pfx', now);
1946    el.className += ' preview inlined';
1947    $.removeClass(el, 'highlight');
1948    $.removeClass(el, 'highlight-anti');
1949    
1950    if ((inner = $.cls('inlined', el))[0]) {
1951      while (j = inner[0]) {
1952        j.parentNode.removeChild(j);
1953      }
1954      inner = $.cls('quotelink', el);
1955      for (i = 0; j = inner[i]; ++i) {
1956        j.removeAttribute('data-pfx');
1957        $.removeClass(j, 'linkfade');
1958      }
1959    }
1960    
1961    for (i = 0; j = el.children[i]; ++i) {
1962      j.id = now + j.id;
1963    }
1964    
1965    if (tblcnt = $.cls('backlink', el)[0]) {
1966      tblcnt.id = now + tblcnt.id;
1967    }
1968    
1969    if (isBl) {
1970      pfx = blcnt.parentNode.parentNode.getAttribute('data-pfx') || '';
1971      dest = $.id(pfx + 'm' + blcnt.id.split('_')[1]);
1972      dest.insertBefore(el, dest.firstChild);
1973      if (count = src.parentNode.getAttribute('data-inline-count')) {
1974        count = +count + 1;
1975      }
1976      else {
1977        count = 1;
1978        src.parentNode.style.display = 'none';
1979      }
1980      src.parentNode.setAttribute('data-inline-count', count);
1981    }
1982    else {
1983      if ($.hasClass(link.parentNode, 'quote')) {
1984        link = link.parentNode;
1985        cnt = link.parentNode;
1986      }
1987      else {
1988        cnt = link.parentNode;
1989      }
1990      cnt.insertBefore(el, link.nextSibling);
1991    }
1992  };
1993  
1994  /**
1995   * Quote preview
1996   */
1997  var QuotePreview = {};
1998  
1999  QuotePreview.init = function() {
2000    var thread;
2001    
2002    this.regex = /^(?:\/([^\/]+)\/)?(?:thread\/)?([0-9]+)?#p([0-9]+)$/;
2003    this.highlight = null;
2004    this.highlightAnti = null;
2005    this.out = true;
2006  };
2007  
2008  QuotePreview.resolve = function(link) {
2009    var self, t, post, ids, offset, pfx;
2010    
2011    self = QuotePreview;
2012    self.out = false;
2013    
2014    t = link.getAttribute('href').match(self.regex);
2015    
2016    if (!t) {
2017      return;
2018    }
2019    
2020    // Quoted post in scope
2021    pfx = link.getAttribute('data-pfx') || '';
2022    
2023    if (post = document.getElementById(pfx + 'p' + t[3])) {
2024      // Visible and not filtered out?
2025      offset = post.getBoundingClientRect();
2026      if (offset.top > 0
2027          && offset.bottom < document.documentElement.clientHeight
2028          && !$.hasClass(post.parentNode, 'post-hidden')) {
2029        if (!$.hasClass(post, 'highlight') && location.hash.slice(1) != post.id) {
2030          self.highlight = post;
2031          $.addClass(post, 'highlight');
2032        }
2033        else if (!$.hasClass(post, 'op')) {
2034          self.highlightAnti = post;
2035          $.addClass(post, 'highlight-anti');
2036        }
2037        return;
2038      }
2039      // Nope
2040      self.show(link, post);
2041    }
2042    // Quoted post out of scope
2043    else {
2044      if (!UA.hasCORS) {
2045        return;
2046      }
2047      self.showRemote(link, t[1] || Main.board, t[2], t[3]);
2048    }
2049  };
2050  
2051  QuotePreview.showRemote = function(link, board, tid, pid) {
2052    var xhr, onload, onerror, el, cached, key;
2053    
2054    key = board + '-' + tid;
2055    
2056    if ((cached = $.cache[key]) && (el = Parser.buildPost(cached, board, pid))) {
2057      QuotePreview.show(link, el);
2058      return;
2059    }
2060    
2061    link.style.cursor = 'wait';
2062    
2063    onload = function() {
2064      var el, thread;
2065      
2066      link.style.cursor = '';
2067      
2068      if (this.status == 200 || this.status == 304 || this.status == 0) {
2069        thread = Parser.parseThreadJSON(this.responseText);
2070        
2071        $.cache[key] = thread;
2072        
2073        if ($.id('quote-preview') || QuotePreview.out) {
2074          return;
2075        }
2076        
2077        if (el = Parser.buildPost(thread, board, pid)) {
2078          el.className = 'post preview';
2079          el.style.display = 'none';
2080          el.id = 'quote-preview';
2081          document.body.appendChild(el);
2082          QuotePreview.show(link, el, true);
2083        }
2084        else {
2085          $.addClass(link, 'deadlink');
2086        }
2087      }
2088      else if (this.status == 404) {
2089        $.addClass(link, 'deadlink');
2090      }
2091    };
2092    
2093    onerror = function() {
2094      link.style.cursor = '';
2095    };
2096    
2097    $.get('//a.4cdn.org/' + board + '/thread/' + tid + '.json',
2098      {
2099        onload: onload,
2100        onerror: onerror
2101      }
2102    );
2103  };
2104  
2105  QuotePreview.show = function(link, post, remote) {
2106    var rect, postHeight, postWidth, doc, docWidth, style, pos, quotes, i, j, qid,
2107      top, scrollTop, margin, img;
2108    
2109    if (remote) {
2110      Parser.parsePost(post);
2111      post.style.display = '';
2112    }
2113    else {
2114      post = post.cloneNode(true);
2115      if (location.hash && location.hash == ('#' + post.id)) {
2116        post.className += ' highlight';
2117      }
2118      post.id = 'quote-preview';
2119      post.className += ' preview';
2120      
2121      if (Config.imageExpansion && (img = $.cls('expanded-thumb', post)[0])) {
2122        ImageExpansion.contract(img);
2123      }
2124    }
2125    
2126    if (!link.parentNode.className) {
2127      quotes = post.querySelectorAll(
2128        '#' + $.cls('postMessage', post)[0].id + ' > .quotelink'
2129      );
2130      if (quotes[1]) {
2131        qid = '>>' + link.parentNode.parentNode.id.split('_')[1];
2132        for (i = 0; j = quotes[i]; ++i) {
2133          if (j.textContent == qid) {
2134            $.addClass(j, 'dotted');
2135            break;
2136          }
2137        }
2138      }
2139    }
2140    
2141    rect = link.getBoundingClientRect();
2142    doc = document.documentElement;
2143    docWidth = doc.offsetWidth;
2144    style = post.style;
2145    
2146    document.body.appendChild(post);
2147    
2148    if (Main.isMobileDevice) {
2149      style.top = rect.top + link.offsetHeight + window.pageYOffset + 'px';
2150      
2151      if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) {
2152        style.right = docWidth - rect.right + 'px';
2153      }
2154      else {
2155        style.left = rect.left + 'px';
2156      }
2157    }
2158    else {
2159      if ((docWidth - rect.right) < (0 | (docWidth * 0.3))) {
2160        pos = docWidth - rect.left;
2161        style.right = pos + 5 + 'px';
2162      }
2163      else {
2164        pos = rect.left + rect.width;
2165        style.left = pos + 5 + 'px';
2166      }
2167      
2168      top = rect.top + link.offsetHeight + window.pageYOffset
2169        - post.offsetHeight / 2 - rect.height / 2;
2170      
2171      postHeight = post.getBoundingClientRect().height;
2172      
2173      if (doc.scrollTop != document.body.scrollTop) {
2174        scrollTop = doc.scrollTop + document.body.scrollTop;
2175      } else {
2176        scrollTop = document.body.scrollTop;
2177      }
2178      
2179      if (top < scrollTop) {
2180        style.top = scrollTop + 'px';
2181      }
2182      else if (top + postHeight > scrollTop + doc.clientHeight) {
2183        style.top = scrollTop + doc.clientHeight - postHeight + 'px';
2184      }
2185      else {
2186        style.top = top + 'px';
2187      }
2188    }
2189  };
2190  
2191  QuotePreview.remove = function(el) {
2192    var self, cnt;
2193    
2194    self = QuotePreview;
2195    self.out = true;
2196    
2197    if (self.highlight) {
2198      $.removeClass(self.highlight, 'highlight');
2199      self.highlight = null;
2200    }
2201    else if (self.highlightAnti) {
2202      $.removeClass(self.highlightAnti, 'highlight-anti');
2203      self.highlightAnti = null
2204    }
2205    
2206    if (el) {
2207      el.style.cursor = '';
2208    }
2209    
2210    if (cnt = $.id('quote-preview')) {
2211      document.body.removeChild(cnt);
2212    }
2213  };
2214  
2215  /**
2216   * Image expansion
2217   */
2218  var ImageExpansion = {
2219    activeVideos: [],
2220    timeout: null
2221  };
2222  
2223  ImageExpansion.expand = function(thumb) {
2224    var img, el, href, ext;
2225    
2226    if (Config.imageHover) {
2227      ImageHover.hide();
2228    }
2229    
2230    href = thumb.parentNode.getAttribute('href');
2231    
2232    if (ext = href.match(/\.(?:webm|pdf)$/)) {
2233      if (!Main.hasMobileLayout && ext[0] == '.webm') {
2234        return ImageExpansion.expandWebm(thumb);
2235      }
2236      return false;
2237    }
2238    
2239    thumb.setAttribute('data-expanding', '1');
2240    
2241    img = document.createElement('img');
2242    img.alt = 'Image';
2243    img.setAttribute('src', href);
2244    img.className = 'expanded-thumb';
2245    img.style.display = 'none';
2246    img.onerror = this.onError;
2247    
2248    thumb.parentNode.insertBefore(img, thumb.nextElementSibling);
2249    
2250    if (UA.hasCORS) {
2251      thumb.style.opacity = '0.75';
2252      this.timeout = this.checkLoadStart(img, thumb);
2253    }
2254    else {
2255      this.onLoadStart(img, thumb);
2256    }
2257    
2258    return true;
2259  };
2260  
2261  ImageExpansion.contract = function(img) {
2262    var cnt, p;
2263    
2264    clearTimeout(this.timeout);
2265    
2266    p = img.parentNode;
2267    cnt = p.parentNode.parentNode;
2268    
2269    $.removeClass(p.parentNode, 'image-expanded');
2270    
2271    if (Config.centeredThreads) {
2272      $.removeClass(cnt.parentNode, 'centre-exp');
2273      cnt.parentNode.style.marginLeft = '';
2274    }
2275    
2276    if (!Main.tid && Config.threadHiding) {
2277      $.removeClass(p, 'image-expanded-anti');
2278    }
2279    
2280    p.firstChild.style.display = '';
2281    
2282    p.removeChild(img);
2283    
2284    if (cnt.offsetTop < window.pageYOffset) {
2285      cnt.scrollIntoView();
2286    }
2287  };
2288  
2289  ImageExpansion.toggle = function(t) {
2290    if (t.hasAttribute('data-md5')) {
2291      if (!t.hasAttribute('data-expanding')) {
2292        return ImageExpansion.expand(t);
2293      }
2294    }
2295    else {
2296      ImageExpansion.contract(t);
2297    }
2298    
2299    return true;
2300  };
2301  
2302  ImageExpansion.expandWebm = function(thumb) {
2303    var el, link, fileText, left, width, href, maxWidth, self;
2304    
2305    self = ImageExpansion;
2306    
2307    if (el = document.getElementById('image-hover')) {
2308      document.body.removeChild(el);
2309    }
2310    
2311    link = thumb.parentNode;
2312    
2313    href = link.getAttribute('href');
2314    
2315    left = link.getBoundingClientRect().left;
2316    maxWidth = document.documentElement.clientWidth - left - 25;
2317    
2318    el = document.createElement('video');
2319    el.muted = true;
2320    el.controls = true;
2321    el.loop = true;
2322    el.autoplay = true;
2323    el.className = 'expandedWebm';
2324    el.onloadedmetadata = ImageExpansion.fitWebm;
2325    el.onplay = ImageExpansion.onWebmPlay;
2326    el.src = href;
2327    
2328    link.style.display = 'none';
2329    link.parentNode.appendChild(el);
2330    
2331    fileText = thumb.parentNode.previousElementSibling;
2332    
2333    el = document.createElement('span');
2334    el.className = 'collapseWebm';
2335    el.innerHTML = '-[<a href="#">Close</a>]';
2336    el.firstElementChild.addEventListener('click', self.collapseWebm, false);
2337    
2338    fileText.appendChild(el);
2339    
2340    return true;
2341  };
2342  
2343  ImageExpansion.fitWebm = function() {
2344    var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, cntEl,
2345      centerWidth, ofs;
2346    
2347    if (Config.centeredThreads) {
2348      centerWidth = $.cls('opContainer')[0].offsetWidth;
2349      cntEl = this.parentNode.parentNode.parentNode;
2350      $.addClass(cntEl, 'centre-exp')
2351    }
2352    
2353    left = this.getBoundingClientRect().left;
2354    
2355    maxWidth = document.documentElement.clientWidth - left - 25;
2356    maxHeight = document.documentElement.clientHeight;
2357    
2358    imgWidth = this.videoWidth;
2359    imgHeight = this.videoHeight;
2360    
2361    if (imgWidth > maxWidth) {
2362      ratio = maxWidth / imgWidth;
2363      imgWidth = maxWidth;
2364      imgHeight = imgHeight * ratio;
2365    }
2366    
2367    if (Config.fitToScreenExpansion && imgHeight > maxHeight) {
2368      ratio = maxHeight / imgHeight;
2369      imgHeight = maxHeight;
2370      imgWidth = imgWidth * ratio;
2371    }
2372    
2373    this.style.maxWidth = imgWidth + 'px';
2374    this.style.maxHeight = imgHeight + 'px';
2375    
2376    if (Config.centeredThreads) {
2377      left = this.getBoundingClientRect().left;
2378      ofs = this.offsetWidth + left * 2;
2379      if (ofs > centerWidth) {
2380        left = Math.floor(($.docEl.clientWidth - ofs) / 2);
2381        
2382        if (left > 0) {
2383          cntEl.style.marginLeft = left + 'px';
2384        }
2385      }
2386      else {
2387        $.removeClass(cntEl, 'centre-exp')
2388      }
2389    }
2390  };
2391  
2392  ImageExpansion.onWebmPlay = function(e) {
2393    var self = ImageExpansion;
2394    
2395    if (!self.activeVideos.length) {
2396      document.addEventListener('scroll', self.onScroll, false);
2397    }
2398    
2399    self.activeVideos.push(this);
2400  };
2401  
2402  ImageExpansion.collapseWebm = function(e) {
2403    var cnt, el, el2;
2404    
2405    e.preventDefault();
2406    
2407    this.removeEventListener('click', ImageExpansion.collapseWebm, false);
2408    
2409    cnt = this.parentNode;
2410    el = cnt.parentNode.parentNode.getElementsByClassName('expandedWebm')[0];
2411    
2412    if (Config.centeredThreads) {
2413      el2 = el.parentNode.parentNode.parentNode;
2414      $.removeClass(el2, 'centre-exp')
2415      el2.style.marginLeft = '';
2416    }
2417    
2418    el.previousElementSibling.style.display = '';
2419    el.parentNode.removeChild(el);
2420    cnt.parentNode.removeChild(cnt);
2421  };
2422  
2423  ImageExpansion.onScroll = function(e) {
2424    clearTimeout(ImageExpansion.timeout);
2425    ImageExpansion.timeout = setTimeout(ImageExpansion.pauseVideos, 500);
2426  };
2427  
2428  ImageExpansion.pauseVideos = function() {
2429    var self, i, el, pos, min, max, nodes;
2430    
2431    self = ImageExpansion;
2432    
2433    nodes = [];
2434    min = window.pageYOffset;
2435    max = window.pageYOffset + $.docEl.clientHeight;
2436    
2437    for (i = 0; el = self.activeVideos[i]; ++i) {
2438      pos = el.getBoundingClientRect();
2439      if (pos.top + window.pageYOffset > max || pos.bottom + window.pageYOffset < min) {
2440        el.pause();
2441      }
2442      else if (!el.paused){
2443        nodes.push(el);
2444      }
2445    }
2446    
2447    if (!nodes.length) {
2448      document.removeEventListener('scroll', self.onScroll, false);
2449    }
2450    
2451    self.activeVideos = nodes;
2452  };
2453  
2454  ImageExpansion.onError = function(e) {
2455    var thumb, img;
2456    
2457    img = e.target;
2458    thumb = $.qs('img[data-expanding]', img.parentNode);
2459    
2460    img.parentNode.removeChild(img);
2461    thumb.style.opacity = '';
2462    thumb.removeAttribute('data-expanding');
2463  };
2464  
2465  ImageExpansion.onLoadStart = function(img, thumb) {
2466    var imgWidth, imgHeight, maxWidth, maxHeight, ratio, left, fileEl, cntEl,
2467      centerWidth, ofs;
2468    
2469    thumb.removeAttribute('data-expanding');
2470    
2471    fileEl = thumb.parentNode.parentNode;
2472    
2473    if (Config.centeredThreads) {
2474      cntEl = fileEl.parentNode.parentNode;
2475      centerWidth = $.cls('opContainer')[0].offsetWidth;
2476      $.addClass(cntEl, 'centre-exp');
2477    }
2478    
2479    left = thumb.getBoundingClientRect().left;
2480    
2481    maxWidth = $.docEl.clientWidth - left - 25;
2482    maxHeight = $.docEl.clientHeight;
2483    
2484    imgWidth = img.naturalWidth;
2485    imgHeight = img.naturalHeight;
2486    
2487    if (imgWidth > maxWidth) {
2488      ratio = maxWidth / imgWidth;
2489      imgWidth = maxWidth;
2490      imgHeight = imgHeight * ratio;
2491    }
2492    
2493    if (Config.fitToScreenExpansion && imgHeight > maxHeight) {
2494      ratio = maxHeight / imgHeight;
2495      imgHeight = maxHeight;
2496      imgWidth = imgWidth * ratio;
2497    }
2498    
2499    img.style.maxWidth = imgWidth + 'px';
2500    img.style.maxHeight = imgHeight + 'px';
2501    
2502    $.addClass(fileEl, 'image-expanded');
2503    
2504    if (!Main.tid && Config.threadHiding) {
2505      $.addClass(thumb.parentNode, 'image-expanded-anti');
2506    }
2507    
2508    img.style.display = '';
2509    thumb.style.display = 'none';
2510    
2511    if (Config.centeredThreads) {
2512      left = img.getBoundingClientRect().left;
2513      ofs = img.offsetWidth + left * 2;
2514      if (ofs > centerWidth) {
2515        left = Math.floor(($.docEl.clientWidth - ofs) / 2);
2516        
2517        if (left > 0) {
2518          cntEl.style.marginLeft = left + 'px';
2519        }
2520      }
2521      else {
2522        $.removeClass(cntEl, 'centre-exp');
2523      }
2524    }
2525  };
2526  
2527  ImageExpansion.checkLoadStart = function(img, thumb) {
2528    if (img.naturalWidth) {
2529      ImageExpansion.onLoadStart(img, thumb);
2530      thumb.style.opacity = '';
2531    }
2532    else {
2533      return setTimeout(ImageExpansion.checkLoadStart, 15, img, thumb);
2534    }
2535  };
2536  
2537  /**
2538   * Image hover
2539   */
2540  var ImageHover = {};
2541  
2542  ImageHover.show = function(thumb) {
2543    var el, href, ext;
2544    
2545    href = thumb.parentNode.getAttribute('href');
2546    
2547    if (ext = href.match(/\.(?:webm|pdf)$/)) {
2548      if (ext[0] == '.webm') {
2549         ImageHover.showWebm(thumb);
2550      }
2551      return;
2552    }
2553    
2554    el = document.createElement('img');
2555    el.id = 'image-hover';
2556    el.alt = 'Image';
2557    el.setAttribute('src', href);
2558    
2559    document.body.appendChild(el);
2560    
2561    if (UA.hasCORS) {
2562      el.style.display = 'none';
2563      this.timeout = ImageHover.checkLoadStart(el, thumb);
2564    }
2565    else {
2566      el.style.left = thumb.getBoundingClientRect().right + 10 + 'px';
2567    }
2568  };
2569  
2570  ImageHover.hide = function() {
2571    var img;
2572    clearTimeout(this.timeout);
2573    if (img = $.id('image-hover')) {
2574      if (img.play) {
2575        Tip.hide();
2576      }
2577      document.body.removeChild(img);
2578    }
2579  };
2580  
2581  ImageHover.showWebm = function(thumb) {
2582    var dims, el, bounds, limit, width;
2583    
2584    dims = thumb.parentNode.previousElementSibling.textContent.match(/, ([0-9]+)x[0-9]+/);
2585    width = +dims[1];
2586    
2587    el = document.createElement('video');
2588    el.id = 'image-hover';
2589    el.src = thumb.parentNode.getAttribute('href');
2590    el.loop = true;
2591    el.muted = true;
2592    el.autoplay = true;
2593    el.onloadedmetadata = function() { ImageHover.showWebMDuration(this, thumb); };
2594    
2595    bounds = thumb.getBoundingClientRect();
2596    limit = window.innerWidth - bounds.right - 20;
2597    
2598    if (width > limit) {
2599      el.style.maxWidth = limit + 'px';
2600    }
2601    
2602    document.body.appendChild(el);
2603  };
2604  
2605  ImageHover.showWebMDuration = function(el, thumb) {
2606    if (!el.parentNode) {
2607      return;
2608    }
2609    
2610    var ms = $.prettySeconds(el.duration);
2611    
2612    Tip.show(thumb, ms[0] + ':' + ('0' + ms[1]).slice(-2));
2613  };
2614  
2615  ImageHover.onLoadStart = function(img, thumb) {
2616    var bounds, limit;
2617    
2618    bounds = thumb.getBoundingClientRect();
2619    limit = window.innerWidth - bounds.right - 20;
2620    
2621    if (img.naturalWidth > limit) {
2622      img.style.maxWidth = limit + 'px';
2623    }
2624    
2625    img.style.display = '';
2626  };
2627  
2628  ImageHover.checkLoadStart = function(img, thumb) {
2629    if (img.naturalWidth) {
2630      ImageHover.onLoadStart(img, thumb);
2631    }
2632    else {
2633      return setTimeout(ImageHover.checkLoadStart, 15, img, thumb);
2634    }
2635  };
2636  
2637  /**
2638   * Quick reply
2639   */
2640  var QR = {};
2641  
2642  QR.init = function() {
2643    var item;
2644    
2645    if (!UA.hasFormData) {
2646      return;
2647    }
2648    
2649    this.enabled = true;
2650    this.currentTid = null;
2651    this.cooldown = null;
2652    this.timestamp = null;
2653    this.auto = false;
2654    
2655    this.btn = null;
2656    this.comField = null;
2657    this.comLength = window.comlen;
2658    this.lenCheckTimeout = null;
2659    
2660    this.preuploadSizeLimit = Main.hasMobileLayout ? 0 : 204800;
2661    
2662    this.cdElapsed = 0;
2663    this.activeDelay = 0;
2664    
2665    this.cooldowns = {};
2666    
2667    for (item in window.cooldowns) {
2668      this.cooldowns[item] = window.cooldowns[item] * 1000;
2669    }
2670    
2671    this.captchaDelay = 240500;
2672    this.captchaInterval = null;
2673    this.pulse = null;
2674    this.xhr = null;
2675    
2676    this.fileDisabled = !!window.imagelimit;
2677    
2678    this.tracked = {};
2679    
2680    this.lastTid = localStorage.getItem('4chan-cd-' + Main.board + '-tid');
2681    
2682    if (Main.tid && !Main.hasMobileLayout && !Main.threadClosed) {
2683      QR.addReplyLink();
2684    }
2685    
2686    window.addEventListener('storage', this.syncStorage, false);
2687  };
2688  
2689  QR.addReplyLink = function() {
2690    var cnt, el;
2691    
2692    cnt = $.cls('navLinks')[2];
2693    
2694    el = document.createElement('div');
2695    el.className = 'open-qr-wrap';
2696    el.innerHTML = '[<a href="#" class="open-qr-link" data-cmd="open-qr">Post a Reply</a>]';
2697    
2698    cnt.insertBefore(el, cnt.firstChild);
2699  };
2700  
2701  QR.lock = function() {
2702    QR.showPostError('This thread is closed.', 'closed', true);
2703  };
2704  
2705  QR.unlock = function() {
2706    QR.hidePostError('closed');
2707  };
2708  
2709  QR.syncStorage = function(e) {
2710    var key;
2711    
2712    if (!e.key) {
2713      return;
2714    }
2715    
2716    key = e.key.split('-');
2717    
2718    if (key[0] != '4chan') {
2719      return;
2720    }
2721    
2722    if (key[1] == 'cd' && e.newValue && Main.board == key[2]) {
2723      if (key[3] == 'tid') {
2724        QR.lastTid = e.newValue;
2725      }
2726      else {
2727        QR.startCooldown();
2728      }
2729    }
2730  };
2731  
2732  QR.quotePost = function(tid, pid) {
2733    if (!QR.noCooldown
2734        && (Main.threadClosed || (!Main.tid && Main.isThreadClosed(tid)))) {
2735      alert('This thread is closed');
2736      return;
2737    }
2738    QR.show(tid);
2739    QR.addQuote(pid);
2740  };
2741  
2742  QR.addQuote = function(pid) {
2743    var q, pos, sel, ta;
2744    
2745    ta = $.tag('textarea', document.forms.qrPost)[0];
2746    
2747    pos = ta.selectionStart;
2748    
2749    sel = UA.getSelection();
2750    
2751    if (pid) {
2752      q = '>>' + pid + '\n';
2753    }
2754    else {
2755      q = '';
2756    }
2757    
2758    if (sel) {
2759      q += '>' + sel.trim().replace(/[\r\n]+/g, '\n>') + '\n';
2760    }
2761    
2762    if (ta.value) {
2763      ta.value = ta.value.slice(0, pos)
2764        + q + ta.value.slice(ta.selectionEnd);
2765    }
2766    else {
2767      ta.value = q;
2768    }
2769    if (UA.isOpera) {
2770      pos += q.split('\n').length;
2771    }
2772    
2773    ta.selectionStart = ta.selectionEnd = pos + q.length;
2774    
2775    if (ta.selectionStart == ta.value.length) {
2776      ta.scrollTop = ta.scrollHeight;
2777    }
2778    ta.focus();
2779  };
2780  
2781  QR.show = function(tid) {
2782    var i, j, cnt, postForm, form, qrForm, fields, row, spoiler, file,
2783      el, el2, placeholder, cd, qrError, cookie;
2784    
2785    if (QR.currentTid) {
2786      if (!Main.tid && QR.currentTid != tid) {
2787        $.id('qrTid').textContent = $.id('qrResto').value = QR.currentTid = tid;
2788        $.byName('com')[1].value = '';
2789        
2790        QR.startCooldown();
2791      }
2792      
2793      if (Main.hasMobileLayout) {
2794        $.id('quickReply').style.top = window.pageYOffset + 25 + 'px';
2795      }
2796      
2797      return;
2798    }
2799    
2800    QR.currentTid = tid;
2801    
2802    postForm = $.id('postForm');
2803    
2804    cnt = document.createElement('div');
2805    cnt.id = 'quickReply';
2806    cnt.className = 'extPanel reply';
2807    cnt.setAttribute('data-trackpos', 'QR-position');
2808    
2809    if (Main.hasMobileLayout) {
2810      cnt.style.top = window.pageYOffset + 28 + 'px';
2811    }
2812    else if (Config['QR-position']) {
2813      cnt.style.cssText = Config['QR-position'];
2814    }
2815    else {
2816      cnt.style.right = '0px';
2817      cnt.style.top = '10%';
2818    }
2819    
2820    cnt.innerHTML =
2821      '<div id="qrHeader" class="drag postblock">Reply to Thread No.<span id="qrTid">'
2822      + tid + '</span><img alt="X" src="' + Main.icons.cross + '" id="qrClose" '
2823      + 'class="extButton" title="Close Window"></div>';
2824    
2825    form = postForm.parentNode.cloneNode(false);
2826    form.setAttribute('name', 'qrPost');
2827    form.innerHTML =
2828      '<input type="hidden" value="'
2829      + $.byName('MAX_FILE_SIZE')[0].value + '" name="MAX_FILE_SIZE">'
2830      + '<input type="hidden" value="regist" name="mode">'
2831      + '<input id="qrResto" type="hidden" value="' + tid + '" name="resto">';
2832    
2833    qrForm = document.createElement('div');
2834    qrForm.id = 'qrForm';
2835    
2836    fields = postForm.firstElementChild.children;
2837    for (i = 0, j = fields.length - 1; i < j; ++i) {
2838      row = document.createElement('div');
2839      if (fields[i].id == 'captchaFormPart') {
2840        if (QR.noCaptcha) {
2841          continue;
2842        }
2843        row.id = 'qrCaptchaContainer';
2844      }
2845      else {
2846        placeholder = fields[i].getAttribute('data-type');
2847        if (placeholder == 'Password' || placeholder == 'Spoilers') {
2848          continue;
2849        }
2850        else if (placeholder == 'File') {
2851          file = fields[i].children[1].firstChild.cloneNode(false);
2852          file.tabIndex += 20;
2853          file.id = 'qrFile';
2854          file.size = '19';
2855          file.addEventListener('change', QR.onFileChange, false);
2856          row.appendChild(file);
2857          
2858          if (UA.hasDragAndDrop) {
2859            $.addClass(file, 'qrRealFile');
2860            
2861            file = document.createElement('div');
2862            file.id = 'qrDummyFile';
2863            
2864            el = document.createElement('button');
2865            el.id = 'qrDummyFileButton';
2866            el.type = 'button';
2867            el.textContent = 'Browseโ€ฆ';
2868            file.appendChild(el);
2869            
2870            el = document.createElement('span');
2871            el.id = 'qrDummyFileLabel';
2872            el.textContent = 'No file selected.';
2873            file.appendChild(el);
2874            
2875            row.appendChild(file);
2876          }
2877          
2878          file.title = 'Shift + Click to remove the file';
2879        }
2880        else {
2881          row.innerHTML = fields[i].children[1].innerHTML;
2882          if (row.firstChild.type == 'hidden') {
2883            el = row.lastChild.previousSibling;
2884          }
2885          else {
2886            el = row.firstChild;
2887          }
2888          if (el.tabIndex > 0) {
2889            el.tabIndex += 20;
2890          }
2891          if (el.nodeName == 'INPUT' || el.nodeName == 'TEXTAREA') {
2892            if (el.name == 'name') {
2893              if (cookie = Main.getCookie('4chan_name')) {
2894                el.value = cookie;
2895              }
2896            }
2897            else if (el.name == 'email') {
2898              el.id = 'qrEmail';
2899            }
2900            else if (el.name == 'com') {
2901              QR.comField = el;
2902              el.addEventListener('keydown', QR.onKeyDown, false);
2903              el.addEventListener('paste', QR.onKeyDown, false);
2904              el.addEventListener('cut', QR.onKeyDown, false);
2905              if (row.children[1]) {
2906                row.removeChild(el.nextSibling);
2907              }
2908            }
2909            else if (el.name == 'sub') {
2910              continue;
2911            }
2912            if (placeholder !== null) {
2913              el.setAttribute('placeholder', placeholder);
2914            }
2915          }
2916          else if ((el.name == 'flag')) {
2917            if (el2 = el.querySelector('option[selected]')) {
2918              el2.removeAttribute('selected');
2919            }
2920            if ((cookie = Main.getCookie('4chan_flag')) &&
2921              (el2 = el.querySelector('option[value="' + cookie + '"]'))) {
2922              el2.setAttribute('selected', 'selected');
2923            }
2924          }
2925        }
2926      }
2927      qrForm.appendChild(row);
2928    }
2929    
2930    this.btn = qrForm.querySelector('input[type="submit"]');
2931    this.btn.previousSibling.className = 'presubmit';
2932    this.btn.tabIndex += 20;
2933    
2934    if (el = postForm.querySelector('.desktop > label > input[name="spoiler"]')) {
2935      spoiler = document.createElement('span');
2936      spoiler.id = 'qrSpoiler';
2937      spoiler.innerHTML = '<label>[<input type="checkbox" tabindex="'
2938        + (el.tabIndex + 20) + '" value="on" name="spoiler">Spoiler?]</label>';
2939      file.parentNode.insertBefore(spoiler, file.nextSibling);
2940    }
2941    
2942    form.appendChild(qrForm);
2943    cnt.appendChild(form);
2944    
2945    qrError = document.createElement('div');
2946    qrError.id = 'qrError';
2947    cnt.appendChild(qrError);
2948    
2949    cnt.addEventListener('click', QR.onClick, false);
2950    
2951    document.body.appendChild(cnt);
2952    
2953    QR.startCooldown();
2954    
2955    if (Main.threadClosed) {
2956      QR.lock();
2957    }
2958    
2959    if (!window.passEnabled) {
2960      if (window.captchaReady) {
2961        if (QR.captchaInterval === null) {
2962          QR.onCaptchaReady();
2963        }
2964        else {
2965          QR.reloadCaptcha();
2966        }
2967      }
2968      else {
2969        window.loadRecaptcha();
2970      }
2971    }
2972    
2973    if (!Main.hasMobileLayout) {
2974      Draggable.set($.id('qrHeader'));
2975    }
2976  };
2977  
2978  QR.onCaptchaReady = function() {
2979    if (!$.id('qrCaptchaContainer')) {
2980      QR.captchaInterval = 1;
2981      return;
2982    }
2983    
2984    QR.pollCaptcha();
2985  };
2986  
2987  QR.onFileChange = function(e) {
2988    var fsize, maxFilesize;
2989    
2990    QR.needPreuploadCaptcha = false;
2991    
2992    if (this.value) {
2993      maxFilesize = window.maxFilesize;
2994      
2995      if (this.files) {
2996        fsize = this.files[0].size;
2997        if (this.files[0].type == 'video/webm' && window.maxWebmFilesize) {
2998          maxFilesize = window.maxWebmFilesize;
2999        }
3000      }
3001      else {
3002        fsize = 0;
3003      }
3004      
3005      if (QR.fileDisabled) {
3006        QR.showPostError('Image limit reached.', 'imagelimit', true);
3007      }
3008      else if (fsize > maxFilesize) {
3009        QR.showPostError('Error: Maximum file size allowed is '
3010          + Math.floor(maxFilesize / 1048576) + ' MB', 'filesize', true);
3011      }
3012      else {
3013        QR.hidePostError();
3014      }
3015      
3016      if (fsize >= QR.preuploadSizeLimit) {
3017        QR.needPreuploadCaptcha = true;
3018      }
3019    }
3020    else {
3021      QR.hidePostError();
3022    }
3023    
3024    QR.startCooldown();
3025  };
3026  
3027  QR.onKeyDown = function(e) {
3028    if (e.ctrlKey && e.keyCode == 83) {
3029      var ta, start, end, spoiler;
3030      
3031      e.stopPropagation();
3032      e.preventDefault();
3033      
3034      ta = e.target;
3035      start = ta.selectionStart;
3036      end = ta.selectionEnd;
3037    
3038      if (ta.value) {
3039        spoiler = '[spoiler]' + ta.value.slice(start, end) + '[/spoiler]';
3040        ta.value = ta.value.slice(0, start) + spoiler + ta.value.slice(end);
3041        ta.setSelectionRange(end + 19, end + 19);
3042      }
3043      else {
3044        ta.value = '[spoiler][/spoiler]';
3045        ta.setSelectionRange(9, 9);
3046      }
3047    }
3048    else if (e.keyCode == 27 && !e.ctrlKey && !e.altKey && !e.shiftKey && !e.metaKey) {
3049      QR.close();
3050      return;
3051    }
3052    
3053    clearTimeout(QR.lenCheckTimeout);
3054    QR.lenCheckTimeout = setTimeout(QR.checkComLength, 500);
3055  };
3056  
3057  QR.checkComLength = function() {
3058    var byteLength, qrError;
3059    
3060    if (QR.comLength) {
3061      byteLength = encodeURIComponent(QR.comField.value).split(/%..|./).length - 1;
3062      
3063      if (byteLength > QR.comLength) {
3064        QR.showPostError('Error: Comment too long ('
3065          + byteLength + '/' + QR.comLength + ').', 'length', true);
3066      }
3067      else {
3068        QR.hidePostError('length');
3069      }
3070    }
3071  };
3072  
3073  QR.close = function() {
3074    var el, cnt = $.id('quickReply');
3075    
3076    QR.comField = null;
3077    QR.currentTid = null;
3078    
3079    clearInterval(QR.captchaInterval);
3080    clearInterval(QR.pulse);
3081    
3082    if (QR.xhr) {
3083      QR.xhr.abort();
3084      QR.xhr = null;
3085    }
3086    
3087    cnt.removeEventListener('click', QR.onClick, false);
3088    
3089    (el = $.id('qrFile')) && el.removeEventListener('change', QR.startCooldown, false);
3090    (el = $.id('qrEmail')) && el.removeEventListener('change', QR.startCooldown, false);
3091    $.tag('textarea', cnt)[0].removeEventListener('keydown', QR.onKeyDown, false);
3092    
3093    Draggable.unset($.id('qrHeader'));
3094    
3095    if (window.RecaptchaState) {
3096      Recaptcha.destroy();
3097      window.captchaReady = false;
3098      if (el = $.id('captchaContainer')) {
3099        el.innerHTML = '<div class="placeholder">'
3100          + el.getAttribute('data-placeholder') + '</div>';
3101      }
3102    }
3103    
3104    document.body.removeChild(cnt);
3105  };
3106  
3107  QR.cloneCaptcha = function() {
3108    var row = $.id('qrCaptchaContainer');
3109    
3110    if (!row) {
3111      return false;
3112    }
3113    
3114    row.innerHTML = '<img id="qrCaptcha" title="Reload" width="300" height="57" src="'
3115      + $.id('recaptcha_challenge_image').src + '" alt="reCAPTCHA challenge image">'
3116      + (window.preupload_captcha ? '<input id="qrCapToken" type="hidden" name="captcha_token" disabled>' : '')
3117      + '<input id="qrCapField" tabindex="25" name="recaptcha_response_field" '
3118      + 'placeholder="Type the text (Required)" '
3119      + 'type="text" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false">'
3120      + '<input id="qrChallenge" name="recaptcha_challenge_field" type="hidden" value="'
3121      + $.id('recaptcha_challenge_field').value + '">';
3122    
3123    return true;
3124  };
3125  
3126  QR.reloadCaptcha = function(focus) {
3127    var pulse, poll;
3128    
3129    if (QR.noCaptcha || !$.id('recaptcha_image') || !window.RecaptchaState) {
3130      return;
3131    }
3132    
3133    poll = function() {
3134      var el;
3135      clearTimeout(pulse);
3136      if (el = $.id('recaptcha_challenge_image')) {
3137        QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay);
3138        QR.cloneCaptcha();
3139        if (focus) {
3140          $.id('qrCapField').focus();
3141        }
3142      }
3143      else {
3144        pulse = setTimeout(poll, 100);
3145      }
3146    };
3147    clearInterval(QR.captchaInterval);
3148    Recaptcha.destroy();
3149    window.loadRecaptcha();
3150    pulse = setTimeout(poll, 100);
3151  };
3152  
3153  QR.pollCaptcha = function() {
3154    clearTimeout(QR.captchaPollTimeout);
3155    
3156    if ($.id('recaptcha_challenge_image')) {
3157      QR.captchaInterval = setInterval(QR.cloneCaptcha, QR.captchaDelay);
3158      QR.cloneCaptcha();
3159    }
3160    else {
3161      QR.captchaPollTimeout = setTimeout(QR.pollCaptcha, 100);
3162    }
3163  };
3164  
3165  QR.onClick = function(e) {
3166    var t = e.target;
3167    
3168    if (t.type == 'submit') {
3169      e.preventDefault();
3170      QR.submit(e.shiftKey);
3171    }
3172    else {
3173      switch (t.id) {
3174        case 'qrFile':
3175          if (e.shiftKey) {
3176            e.preventDefault();
3177            QR.resetFile();
3178          }
3179          break;
3180        case 'qrDummyFile':
3181        case 'qrDummyFileButton':
3182        case 'qrDummyFileLabel':
3183          e.preventDefault();
3184          if (e.shiftKey) {
3185            QR.resetFile();
3186          }
3187          else {
3188            $.id('qrFile').click();
3189          }
3190          break;
3191        case 'qrCaptcha':
3192          QR.reloadCaptcha(true);
3193          break;
3194        case 'qrClose':
3195          QR.close();
3196          break;
3197      }    
3198    }
3199  };
3200  
3201  QR.submit = function(force) {
3202    if (force) {
3203      QR.submitDirect(true);
3204    }
3205    else if (!QR.noCaptcha && window.preupload_captcha && QR.needPreuploadCaptcha) {
3206      QR.submitPreupload();
3207    }
3208    else {
3209      QR.submitDirect();
3210    }
3211  };
3212  
3213  QR.showPostError = function(msg, type, silent) {
3214    var qrError;
3215    
3216    qrError = $.id('qrError');
3217    
3218    if (!qrError) {
3219      return;
3220    }
3221    
3222    qrError.innerHTML = msg;
3223    qrError.style.display = 'block';
3224    
3225    qrError.setAttribute('data-type', type || '');
3226    
3227    if (!silent && (document.hidden
3228      || document.mozHidden
3229      || document.webkitHidden
3230      || document.msHidden)) {
3231      alert('Posting Error');
3232    }
3233  };
3234  
3235  QR.hidePostError = function(type) {
3236    var el = $.id('qrError');
3237    
3238    if (!el.hasAttribute('style')) {
3239      return;
3240    }
3241    
3242    if (!type || el.getAttribute('data-type') == type) {
3243      el.removeAttribute('style');
3244    }
3245  };
3246  
3247  QR.resetFile = function() {
3248    var file, el;
3249    
3250    el = document.createElement('input');
3251    el.id = 'qrFile';
3252    el.type = 'file';
3253    el.size = '19';
3254    el.name = 'upfile';
3255    el.addEventListener('change', QR.onFileChange, false);
3256    
3257    file = $.id('qrFile');
3258    file.removeEventListener('change', QR.onFileChange, false);
3259    
3260    file.parentNode.replaceChild(el, file);
3261    
3262    QR.hidePostError('imagelimit');
3263    
3264    QR.needPreuploadCaptcha = false;
3265    
3266    QR.startCooldown();
3267  };
3268  
3269  QR.submitPreupload = function() {
3270    var token, challenge, response, data;
3271    
3272    if (!QR.presubmitChecks()) {
3273      return;
3274    }
3275    
3276    challenge = $.id('qrChallenge');
3277    response = $.id('qrCapField');
3278    
3279    if (response.value == '') {
3280      QR.showPostError('You forgot to type in the CAPTCHA.');
3281      response.focus();
3282      return;
3283    }
3284    
3285    data = new FormData();
3286    data.append('mode', 'checkcaptcha');
3287    data.append('challenge', challenge.value);
3288    data.append('response', response.value);
3289    
3290    QR.xhr = new XMLHttpRequest();
3291    
3292    QR.xhr.open('POST', document.forms.post.action, true);
3293    
3294    QR.xhr.onerror = function() {
3295      QR.xhr = null;
3296      QR.submitDirect();
3297    };
3298    
3299    QR.xhr.onload = function() {
3300      var el, resp;
3301      
3302      QR.xhr = null;
3303      
3304      try {
3305        resp = JSON.parse(this.responseText);
3306      }
3307      catch(e) {
3308        console.log("Couldn't verify captcha.");
3309        QR.submitDirect();
3310        return;
3311      }
3312      
3313      if (resp.token) {
3314        el = $.id('qrCapToken');
3315        el.value = resp.token;
3316        el.removeAttribute('disabled');
3317        
3318        QR.submitDirect();
3319      }
3320      else if (resp.error) {
3321        QR.reloadCaptcha();
3322        QR.btn.value = 'Post';
3323        QR.showPostError(resp.error);
3324      }
3325      else {
3326        if (resp.fail) {
3327          console.log(resp.fail);
3328        }
3329        QR.submitDirect();
3330      }
3331    };
3332    
3333    token = $.id('qrCapToken');
3334    token.value = '';
3335    token.setAttribute('disabled', '1');
3336    
3337    QR.btn.value = 'Sending';
3338    
3339    QR.xhr.send(data);
3340  };
3341  
3342  QR.submitDirect = function(force) {
3343    var field, formdata, file;
3344    
3345    QR.hidePostError();
3346    
3347    if (!QR.presubmitChecks(force)) {
3348      return;
3349    }
3350    
3351    QR.auto = false;
3352    
3353    if (!force && (field = $.id('qrCapField')) && field.value == '') {
3354      QR.showPostError('You forgot to type in the CAPTCHA.');
3355      field.focus();
3356      return;
3357    }
3358    
3359    QR.xhr = new XMLHttpRequest();
3360    
3361    QR.xhr.open('POST', document.forms.qrPost.action, true);
3362    
3363    QR.xhr.withCredentials = true;
3364    
3365    QR.xhr.upload.onprogress = function(e) {
3366      if (e.loaded >= e.total) {
3367        QR.btn.value = '100%';
3368      }
3369      else {
3370        QR.btn.value = (0 | (e.loaded / e.total * 100)) + '%';
3371      }
3372    };
3373    
3374    QR.xhr.onerror = function() {
3375      QR.xhr = null;
3376      QR.showPostError('Connection error.');
3377    };
3378    
3379    QR.xhr.onload = function() {
3380      var resp, el, hasFile, ids, tid, pid, tracked;
3381      
3382      QR.xhr = null;
3383      
3384      QR.btn.value = 'Post';
3385      
3386      if (this.status == 200) {
3387        if (resp = this.responseText.match(/"errmsg"[^>]*>(.*?)<\/span/)) {
3388          QR.reloadCaptcha(true);
3389          QR.showPostError(resp[1]);
3390          return;
3391        }
3392        
3393        if (ids = this.responseText.match(/<!-- thread:([0-9]+),no:([0-9]+) -->/)) {
3394          tid = ids[1];
3395          pid = ids[2];
3396          
3397          QR.lastTid = tid;
3398          
3399          localStorage.setItem('4chan-cd-' + Main.board + '-tid', tid);
3400          
3401          hasFile = (el = $.id('qrFile')) && el.value;
3402          
3403          QR.setPostTime();
3404          
3405          if (Config.persistentQR) {
3406            $.byName('com')[1].value = '';
3407            
3408            if (el = $.byName('spoiler')[2]) {
3409              el.checked = false;
3410            }
3411            
3412            QR.reloadCaptcha();
3413            
3414            if (hasFile) {
3415              QR.resetFile();
3416            }
3417            
3418            QR.startCooldown();
3419          }
3420          else {
3421            QR.close();
3422          }
3423          
3424          if (Main.tid) {
3425            if (Config.threadWatcher) {
3426              ThreadWatcher.setLastRead(pid, tid);
3427            }
3428            QR.lastReplyId = +pid;
3429            Parser.trackedReplies['>>' + pid] = 1;
3430            Parser.saveTrackedReplies(tid, Parser.trackedReplies);
3431          }
3432          else {
3433            tracked = Parser.getTrackedReplies(tid) || {};
3434            tracked['>>' + pid] = 1;
3435            Parser.saveTrackedReplies(tid, tracked);
3436          }
3437          
3438          UA.dispatchEvent('4chanQRPostSuccess', { threadId: tid, postId: pid });
3439        }
3440        
3441        if (ThreadUpdater.enabled) {
3442          setTimeout(ThreadUpdater.forceUpdate, 500);
3443        }
3444      }
3445      else {
3446        QR.showPostError('Error: ' + this.status + ' ' + this.statusText);
3447      }
3448    };
3449    
3450    formdata = new FormData(document.forms.qrPost);
3451    
3452    clearInterval(QR.pulse);
3453    
3454    QR.btn.value = 'Sending';
3455    
3456    QR.xhr.send(formdata);
3457  };
3458  
3459  QR.presubmitChecks = function(force) {
3460    if (QR.xhr) {
3461      QR.xhr.abort();
3462      QR.xhr = null;
3463      QR.showPostError('Aborted.');
3464      QR.btn.value = 'Post';
3465      return false;
3466    }
3467    
3468    if (!force && QR.cooldown) {
3469      if (QR.auto = !QR.auto) {
3470        QR.btn.value = QR.cooldown + 's (auto)';
3471      }
3472      else {
3473        QR.btn.value = QR.cooldown + 's';
3474      }
3475      return false;
3476    }
3477    
3478    return true;
3479  };
3480  
3481  QR.getCooldown = function(type) {
3482    if (QR.currentTid != QR.lastTid) {
3483      return QR.cooldowns[type];
3484    }
3485    else {
3486      return QR.cooldowns[type + '_intra'];
3487    }
3488  };
3489  
3490  QR.setPostTime = function() {
3491    return localStorage.setItem('4chan-cd-' + Main.board, Date.now());
3492  };
3493  
3494  QR.getPostTime = function() {
3495    return localStorage.getItem('4chan-cd-' + Main.board);
3496  };
3497  
3498  QR.removePostTime = function() {
3499    return localStorage.removeItem('4chan-cd-' + Main.board);
3500  };
3501  
3502  QR.startCooldown = function() {
3503    var type, el, time;
3504    
3505    if (QR.noCooldown || !$.id('quickReply') || QR.xhr) {
3506      return;
3507    }
3508    
3509    clearInterval(QR.pulse);
3510    
3511    type = ((el = $.id('qrFile')) && el.value) ? 'image' : 'reply';
3512    
3513    time = QR.getPostTime(type);
3514    
3515    if (!time) {
3516      QR.btn.value = 'Post';
3517      return;
3518    }
3519    
3520    QR.timestamp = parseInt(time, 10);
3521    
3522    QR.activeDelay = QR.getCooldown(type);
3523    
3524    QR.cdElapsed = Date.now() - QR.timestamp;
3525    
3526    QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000);
3527    
3528    if (QR.cooldown <= 0 || QR.cdElapsed < 0) {
3529      QR.cooldown = false;
3530      QR.removePostTime(type);
3531      return;
3532    }
3533    
3534    QR.btn.value = QR.cooldown + 's';
3535    
3536    QR.pulse = setInterval(QR.onPulse, 1000);
3537  };
3538  
3539  QR.onPulse = function() {
3540    QR.cdElapsed = Date.now() - QR.timestamp;
3541    QR.cooldown = Math.floor((QR.activeDelay - QR.cdElapsed) / 1000);
3542    if (QR.cooldown <= 0) {
3543      clearInterval(QR.pulse);
3544      QR.btn.value = 'Post';
3545      QR.cooldown = false;
3546      if (QR.auto) {
3547        QR.submit();
3548      }
3549    }
3550    else {
3551      QR.btn.value = QR.cooldown + (QR.auto ? 's (auto)' : 's');
3552    }
3553  };
3554  
3555  /**
3556   * Thread hiding
3557   */
3558  var ThreadHiding = {};
3559  
3560  ThreadHiding.init = function() {
3561    this.threshold = 43200000; // 12 hours
3562    
3563    this.hidden = {};
3564    
3565    this.load();
3566    
3567    this.purge();
3568  };
3569  
3570  ThreadHiding.clear = function(silent) {
3571    var i, id, key, msg;
3572    
3573    this.load();
3574    
3575    i = 0;
3576    
3577    for (id in this.hidden) {
3578      ++i;
3579    }
3580    
3581    key = '4chan-hide-t-' + Main.board;
3582    
3583    if (!silent) {
3584      if (!i) {
3585        alert("You don't have any hidden threads on /" + Main.board + '/');
3586        return;
3587      }
3588      
3589      msg = 'This will unhide ' + i + ' thread' + (i > 1 ? 's' : '') + ' on /' + Main.board + '/';
3590      
3591      if (!confirm(msg)) {
3592        return;
3593      }
3594      
3595      localStorage.removeItem(key);
3596    }
3597    else {
3598      localStorage.removeItem(key);
3599    }
3600  };
3601  
3602  ThreadHiding.isHidden = function(tid) {
3603    var sa = $.id('sa' + tid);
3604    
3605    return !sa || sa.hasAttribute('data-hidden');
3606  };
3607  
3608  ThreadHiding.toggle = function(tid) {
3609    if (this.isHidden(tid)) {
3610      this.show(tid);
3611    }
3612    else {
3613      this.hide(tid);
3614    }
3615    this.save();
3616  };
3617  
3618  ThreadHiding.show = function(tid) {
3619    var sa, th;
3620    
3621    th = $.id('t' + tid);
3622    
3623    sa = $.id('sa' + tid);
3624    sa.removeAttribute('data-hidden');
3625    
3626    if (Main.hasMobileLayout) {
3627      sa.textContent = 'Hide';
3628      $.removeClass(sa, 'mobile-tu-show');
3629      $.cls('postLink', th)[0].appendChild(sa);
3630      
3631      th.style.display = null;
3632      $.removeClass(th.nextElementSibling, 'mobile-hr-hidden');
3633    }
3634    else {
3635      sa.firstChild.src = Main.icons.minus;
3636      $.removeClass(th, 'post-hidden');
3637    }
3638    
3639    delete this.hidden[tid];
3640  };
3641  
3642  ThreadHiding.hide = function(tid) {
3643    var sa, th;
3644    
3645    th = $.id('t' + tid);
3646    
3647    if (Main.hasMobileLayout) {
3648      th.style.display = 'none';
3649      $.addClass(th.nextElementSibling, 'mobile-hr-hidden');
3650      
3651      sa = $.id('sa' + tid);
3652      sa.setAttribute('data-hidden', tid);
3653      sa.textContent = 'Show Hidden Thread';
3654      $.addClass(sa, 'mobile-tu-show');
3655      
3656      th.parentNode.insertBefore(sa, th);
3657    }
3658    else {
3659      if (Config.hideStubs && !$.cls('stickyIcon', th)[0]) {
3660        th.style.display = th.nextElementSibling.style.display = 'none';
3661      }
3662      else {
3663        sa = $.id('sa' + tid);
3664        sa.setAttribute('data-hidden', tid);
3665        sa.firstChild.src = Main.icons.plus;
3666        th.className += ' post-hidden';
3667      }
3668    }
3669    
3670    this.hidden[tid] = Date.now();
3671  };
3672  
3673  ThreadHiding.load = function() {
3674    var storage;
3675    
3676    if (storage = localStorage.getItem('4chan-hide-t-' + Main.board)) {
3677      this.hidden = JSON.parse(storage);
3678    }
3679  };
3680  
3681  ThreadHiding.purge = function() {
3682    var i, hasHidden, lastPurged, key;
3683    
3684    key = '4chan-purge-t-' + Main.board;
3685    
3686    lastPurged = localStorage.getItem(key);
3687    
3688    for (i in this.hidden) {
3689      hasHidden = true;
3690      break;
3691    }
3692    
3693    if (!hasHidden) {
3694      return;
3695    }
3696    
3697    if (!lastPurged || lastPurged < Date.now() - this.threshold) {
3698      $.get('//a.4cdn.org/' + Main.board + '/threads.json',
3699      {
3700        onload: function() {
3701          var i, j, t, p, pages, threads, alive;
3702          
3703          if (this.status == 200) {
3704            alive = {};
3705            pages = JSON.parse(this.responseText);
3706            for (i = 0; p = pages[i]; ++i) {
3707              threads = p.threads;
3708              for (j = 0; t = threads[j]; ++j) {
3709                if (ThreadHiding.hidden[t.no]) {
3710                  alive[t.no] = 1;
3711                }
3712              }
3713            }
3714            ThreadHiding.hidden = alive;
3715            ThreadHiding.save();
3716            localStorage.setItem(key, Date.now());
3717          }
3718          else {
3719            console.log('Bad status code while purging threads');
3720          }
3721        },
3722        onerror: function() {
3723          console.log('Error while purging hidden threads');
3724        }
3725      });
3726    }
3727  };
3728  
3729  ThreadHiding.save = function() {
3730    for (var i in this.hidden) {
3731      localStorage.setItem('4chan-hide-t-' + Main.board,
3732        JSON.stringify(this.hidden)
3733      );
3734      return;
3735    }
3736    localStorage.removeItem('4chan-hide-t-' + Main.board);
3737  };
3738  
3739  /**
3740   * Reply hiding
3741   */
3742  var ReplyHiding = {};
3743  
3744  ReplyHiding.init = function() {
3745    this.threshold = 7 * 86400000;
3746    this.hidden = {};
3747    this.load();
3748  };
3749  
3750  ReplyHiding.isHidden = function(pid) {
3751    var sa = $.id('sa' + pid);
3752    
3753    return !sa || sa.hasAttribute('data-hidden');
3754  };
3755  
3756  ReplyHiding.toggle = function(pid) {
3757    if (this.isHidden(pid)) {
3758      this.show(pid);
3759    }
3760    else {
3761      this.hide(pid);
3762    }
3763    this.save();
3764  };
3765  
3766  ReplyHiding.show = function(pid) {
3767    var post, sa;
3768    
3769    post = $.id('pc' + pid);
3770    
3771    $.removeClass(post, 'post-hidden');
3772    
3773    sa = $.id('sa' + pid);
3774    sa.removeAttribute('data-hidden');
3775    sa.firstChild.src = Main.icons.minus;
3776    
3777    delete this.hidden[pid];
3778  };
3779  
3780  ReplyHiding.hide = function(pid) {
3781    var post, sa;
3782    
3783    post = $.id('pc' + pid);
3784    post.className += ' post-hidden';
3785    
3786    sa = $.id('sa' + pid);
3787    sa.setAttribute('data-hidden', pid);
3788    sa.firstChild.src = Main.icons.plus;
3789    
3790    this.hidden[pid] = Date.now();
3791  };
3792  
3793  ReplyHiding.load = function() {
3794    var storage;
3795    
3796    if (storage = localStorage.getItem('4chan-hide-r-' + Main.board)) {
3797      this.hidden = JSON.parse(storage);
3798    }
3799  };
3800  
3801  ReplyHiding.purge = function() {
3802    var tid, now;
3803    
3804    now = Date.now();
3805    
3806    for (tid in this.hidden) {
3807      if (now - this.hidden[tid] > this.threshold) {
3808        delete this.hidden[tid];
3809      }
3810    }
3811    this.save();
3812  };
3813  
3814  ReplyHiding.save = function() {
3815    for (var i in this.hidden) {
3816      localStorage.setItem('4chan-hide-r-' + Main.board,
3817        JSON.stringify(this.hidden)
3818      );
3819      return;
3820    }
3821    localStorage.removeItem('4chan-hide-r-' + Main.board);
3822  };
3823  
3824  /**
3825   * Thread watcher
3826   */
3827  var ThreadWatcher = {};
3828  
3829  ThreadWatcher.init = function() {
3830    var cnt, jumpTo, rect, el;
3831    
3832    this.listNode = null;
3833    this.charLimit = 45;
3834    this.watched = {};
3835    this.isRefreshing = false;
3836    
3837    if (Main.hasMobileLayout) {
3838      el = document.createElement('a');
3839      el.href = '#';
3840      el.textContent = 'TW';
3841      el.addEventListener('click', ThreadWatcher.toggleList, false);
3842      cnt = $.id('settingsWindowLinkMobile');
3843      cnt.parentNode.insertBefore(el, cnt);
3844      cnt.parentNode.insertBefore(document.createTextNode(' '), cnt);
3845    }
3846    
3847    if (location.hash && (jumpTo = location.hash.split('lr')[1])) {
3848      if (jumpTo = $.id('pc' + jumpTo)) {
3849        if (jumpTo.nextElementSibling) {
3850          jumpTo = jumpTo.nextElementSibling;
3851          if (el = $.id('p' + jumpTo.id.slice(2))) {
3852            $.addClass(el, 'highlight');
3853          }
3854        }
3855        
3856        rect = jumpTo.getBoundingClientRect();
3857        
3858        if (rect.top < 0 || rect.bottom > document.documentElement.clientHeight) {
3859          window.scrollBy(0, rect.top);
3860        }
3861      }
3862      
3863      if (window.history && history.replaceState) {
3864        history.replaceState(null, '', location.href.split('#', 1)[0]);
3865      }
3866    }
3867    
3868    cnt = document.createElement('div');
3869    cnt.id = 'threadWatcher';
3870    cnt.className = 'extPanel reply';
3871    cnt.setAttribute('data-trackpos', 'TW-position');
3872    
3873    if (Main.hasMobileLayout) {
3874      cnt.style.display = 'none';
3875    }
3876    else {
3877      if (Config['TW-position']) {
3878        cnt.style.cssText = Config['TW-position'];
3879      }
3880      else {
3881        cnt.style.left = '10px';
3882        cnt.style.top = '380px';
3883      }
3884      
3885      if (Config.fixedThreadWatcher) {
3886        cnt.style.position = 'fixed';
3887      }
3888      else {
3889        cnt.style.position = '';
3890      }
3891    }
3892    
3893    cnt.innerHTML = '<div class="drag" id="twHeader">'
3894      + (Main.hasMobileLayout ? ('<img id="twClose" class="pointer" src="'
3895      + Main.icons.cross + '" alt="X">') : '')
3896      + 'Thread Watcher'
3897      + (UA.hasCORS ? ('<img id="twPrune" class="pointer right" src="'
3898      + Main.icons.refresh + '" alt="R" title="Refresh"></div>') : '</div>');
3899    
3900    this.listNode = document.createElement('ul');
3901    this.listNode.id = 'watchList';
3902    
3903    this.load();
3904    
3905    if (Main.tid) {
3906      this.refreshCurrent();
3907    }
3908    
3909    this.build();
3910    
3911    cnt.appendChild(this.listNode);
3912    document.body.appendChild(cnt);
3913    cnt.addEventListener('mouseup', this.onClick, false);
3914    Draggable.set($.id('twHeader'));
3915    window.addEventListener('storage', this.syncStorage, false);
3916    
3917    if (Main.hasMobileLayout) {
3918      if (Main.tid) {
3919        ThreadWatcher.initMobileButtons();
3920      }
3921    }
3922    else if (!Main.tid && this.canAutoRefresh()) {
3923      this.refresh();
3924    }
3925  };
3926  
3927  ThreadWatcher.toggleList = function(e) {
3928    var el = $.id('threadWatcher');
3929    
3930    e && e.preventDefault();
3931    
3932    if (!Main.tid && ThreadWatcher.canAutoRefresh()) {
3933      ThreadWatcher.refresh();
3934    }
3935    
3936    if (el.style.display == 'none') {
3937      el.style.top = (window.pageYOffset + 30) + 'px';
3938      el.style.display = '';
3939    }
3940    else {
3941      el.style.display = 'none';
3942    }
3943  };
3944  
3945  ThreadWatcher.syncStorage = function(e) {
3946    var key;
3947    
3948    if (!e.key) {
3949      return;
3950    }
3951    
3952    key = e.key.split('-');
3953    
3954    if (key[0] == '4chan' && key[1] == 'watch' && e.newValue != e.oldValue) {
3955      ThreadWatcher.watched = JSON.parse(e.newValue);
3956      ThreadWatcher.build(true);
3957    }
3958  };
3959  
3960  ThreadWatcher.load = function() {
3961    if (storage = localStorage.getItem('4chan-watch')) {
3962      this.watched = JSON.parse(storage);
3963    }
3964  };
3965  
3966  ThreadWatcher.build = function(rebuildButtons) {
3967    var i, html, tuid, key, nodes, cls;
3968    
3969    html = '';
3970    
3971    for (key in this.watched) {
3972      tuid = key.split('-');
3973      html += '<li id="watch-' + key
3974        + '"><span class="pointer" data-cmd="unwatch" data-id="'
3975        + tuid[0] + '" data-board="' + tuid[1] + '">&times;</span> <a href="'
3976        + Main.linkToThread(tuid[0], tuid[1]) + '#lr' + this.watched[key][1] + '"';
3977      
3978      if (this.watched[key][1] == -1) {
3979        html += ' class="deadlink">';
3980      }
3981      else {
3982        if (this.watched[key][3]) {
3983          cls = 'archivelink';
3984        }
3985        else {
3986          cls = false;
3987        }
3988        if (this.watched[key][2]) {
3989          html += ' class="' + (cls ? (cls + ' ') : '')
3990            + 'hasNewReplies">(' + this.watched[key][2] + ') ';
3991        }
3992        else {
3993          html += (cls ? ('class="' + cls + '"') : '') + '>';
3994        }
3995      }
3996      
3997      html += '/' + tuid[1] + '/ - ' + this.watched[key][0] + '</a></li>';
3998    }
3999    
4000    if (rebuildButtons) {
4001      ThreadWatcher.rebuildButtons();
4002    }
4003    
4004    ThreadWatcher.listNode.innerHTML = html;
4005  };
4006  
4007  ThreadWatcher.rebuildButtons = function() {
4008    var i, buttons, key;
4009    
4010    buttons = $.cls('wbtn');
4011    
4012    for (i = 0; btn = buttons[i]; ++i) {
4013      key = btn.getAttribute('data-id') + '-' + Main.board;
4014      if (ThreadWatcher.watched[key]) {
4015        if (!btn.hasAttribute('data-active')) {
4016          btn.src = Main.icons.watched;
4017          btn.setAttribute('data-active', '1');
4018        }
4019      }
4020      else {
4021        if (btn.hasAttribute('data-active')) {
4022          btn.src = Main.icons.notwatched;
4023          btn.removeAttribute('data-active');
4024        }
4025      }
4026    }
4027  };
4028  
4029  ThreadWatcher.initMobileButtons = function() {
4030    var el, cnt, key, ref;
4031    
4032    el = document.createElement('img');
4033    
4034    key = Main.tid + '-' + Main.board;
4035    
4036    if (ThreadWatcher.watched[key]) {
4037      el.src = Main.icons.watched;
4038      el.setAttribute('data-active', '1');
4039    }
4040    else {
4041      el.src = Main.icons.notwatched;
4042    }
4043    
4044    el.className = 'extButton wbtn wbtn-' + key;
4045    el.setAttribute('data-cmd', 'watch');
4046    el.setAttribute('data-id', Main.tid);
4047    el.alt = 'W';
4048    
4049    cnt = document.createElement('span');
4050    cnt.className = 'mobileib button';
4051    
4052    cnt.appendChild(el);
4053    
4054    if (ref = $.cls('navLinks')[0]) {
4055      ref.appendChild(document.createTextNode(' '));
4056      ref.appendChild(cnt);
4057    }
4058    
4059    if (ref = $.cls('navLinks')[3]) {
4060      ref.appendChild(document.createTextNode(' '));
4061      ref.appendChild(cnt.cloneNode(true));
4062    }
4063  };
4064  
4065  ThreadWatcher.onClick = function(e) {
4066    var t = e.target;
4067    
4068    if (t.hasAttribute('data-id')) {
4069      ThreadWatcher.toggle(
4070        t.getAttribute('data-id'),
4071        t.getAttribute('data-board')
4072      );
4073    }
4074    else if (t.id == 'twPrune' && !ThreadWatcher.isRefreshing) {
4075      ThreadWatcher.refresh();
4076    }
4077    else if (t.id == 'twClose') {
4078      ThreadWatcher.toggleList();
4079    }
4080  };
4081  
4082  ThreadWatcher.toggle = function(tid, board, synced) {
4083    var i, key, label, lastReply, thread;
4084    
4085    key = tid + '-' + (board || Main.board);
4086    
4087    if (this.watched[key]) {
4088      delete this.watched[key];
4089    }
4090    else {
4091      if (label = $.cls('subject', $.id('pi' + tid))[0].textContent) {
4092        label = label.slice(0, this.charLimit);
4093      }
4094      else if (label = $.id('m' + tid).innerHTML) {
4095        label = label.replace(/(?:<br>)+/g, ' ')
4096          .replace(/<[^>]*?>/g, '').slice(0, this.charLimit);
4097      }
4098      else {
4099        label = 'No.' + tid;
4100      }
4101      
4102      if ((thread = $.id('t' + tid)).children[1]) {
4103        lastReply = thread.lastElementChild.id.slice(2);
4104      }
4105      else {
4106        lastReply = tid;
4107      }
4108      
4109      this.watched[key] = [ label, lastReply, 0 ];
4110    }
4111    this.save();
4112    this.load();
4113    this.build(true);
4114  };
4115  
4116  ThreadWatcher.save = function() {
4117    ThreadWatcher.sortByBoard();
4118    
4119    localStorage.setItem('4chan-watch', JSON.stringify(ThreadWatcher.watched));
4120  };
4121  
4122  ThreadWatcher.sortByBoard = function() {
4123    var i, self, key, sorted, keys;
4124    
4125    self = ThreadWatcher;
4126    
4127    sorted = {};
4128    keys = [];
4129    
4130    for (key in self.watched) {
4131      keys.push(key);
4132    }
4133    
4134    keys.sort(function(a, b) {
4135      a = a.split('-')[1];
4136      b = b.split('-')[1];
4137      
4138      if (a < b) {
4139        return -1;
4140      }
4141      if (a > b) {
4142        return 1;
4143      }
4144      return 0;
4145    });
4146    
4147    for (i = 0; key = keys[i]; ++i) {
4148      sorted[key] = self.watched[key];
4149    }
4150    
4151    self.watched = sorted;
4152  };
4153  
4154  ThreadWatcher.canAutoRefresh = function() {
4155    var time;
4156    
4157    if (time = localStorage.getItem('4chan-tw-timestamp')) {
4158      return Date.now() - (+time) >= 60000;
4159    }
4160    return false;
4161  };
4162  
4163  ThreadWatcher.setRefreshTimestamp = function() {
4164    localStorage.setItem('4chan-tw-timestamp', Date.now());
4165  };
4166  
4167  ThreadWatcher.refresh = function() {
4168    var i, to, key, total, img;
4169    
4170    if (total = $.id('watchList').children.length) {
4171      i = to = 0;
4172      img = $.id('twPrune');
4173      img.src = Main.icons.rotate;
4174      ThreadWatcher.isRefreshing = true;
4175      ThreadWatcher.setRefreshTimestamp();
4176      for (key in ThreadWatcher.watched) {
4177        setTimeout(ThreadWatcher.fetch, to, key, ++i == total ? img : null);
4178        to += 200;
4179      }
4180    }
4181  };
4182  
4183  ThreadWatcher.refreshCurrent = function(rebuild) {
4184    var key, thread, lastReply;
4185    
4186    key = Main.tid + '-' + Main.board;
4187    
4188    if (this.watched[key]) {
4189      if ((thread = $.id('t' + Main.tid)).children[1]) {
4190        lastReply = thread.lastElementChild.id.slice(2);
4191      }
4192      else {
4193        lastReply = Main.tid;
4194      }
4195      if (this.watched[key][1] < lastReply) {
4196        this.watched[key][1] = lastReply;
4197      }
4198      
4199      this.watched[key][2] = 0;
4200      this.save();
4201      
4202      if (rebuild) {
4203        this.build();
4204      }
4205    }
4206  };
4207  
4208  ThreadWatcher.setLastRead = function(pid, tid) {
4209    var key = tid + '-' + Main.board;
4210    
4211    if (this.watched[key]) {
4212      this.watched[key][1] = pid;
4213      this.watched[key][2] = 0;
4214      this.save();
4215      this.build();
4216    }
4217  };
4218  
4219  ThreadWatcher.onRefreshEnd = function(img) {
4220    img.src = Main.icons.refresh;
4221    this.isRefreshing = false;
4222    this.save();
4223    this.load();
4224    this.build();
4225  };
4226  
4227  ThreadWatcher.fetch = function(key, img) {
4228    var tuid, xhr, li, method;
4229    
4230    li = $.id('watch-' + key);
4231    
4232    if (ThreadWatcher.watched[key][1] == -1) {
4233      delete ThreadWatcher.watched[key];
4234      li.parentNode.removeChild(li);
4235      if (img) {
4236        ThreadWatcher.onRefreshEnd(img);
4237      }
4238      return;
4239    }
4240    
4241    tuid = key.split('-'); // tid, board
4242    
4243    xhr = new XMLHttpRequest();
4244    xhr.onload = function() {
4245      var i, newReplies, posts, lastReply;
4246      if (this.status == 200) {
4247        posts = Parser.parseThreadJSON(this.responseText);
4248        lastReply = ThreadWatcher.watched[key][1];
4249        newReplies = 0;
4250        for (i = posts.length - 1; i >= 1; i--) {
4251          if (posts[i].no <= lastReply) {
4252            break;
4253          }
4254          ++newReplies;
4255        }
4256        if (newReplies > ThreadWatcher.watched[key][2]) {
4257          ThreadWatcher.watched[key][2] = newReplies;
4258        }
4259        if (posts[0].archived) {
4260          ThreadWatcher.watched[key][3] = 1;
4261        }
4262      }
4263      else if (this.status == 404) {
4264        ThreadWatcher.watched[key][1] = -1;
4265      }
4266      if (img) {
4267        ThreadWatcher.onRefreshEnd(img);
4268      }
4269    };
4270    if (img) {
4271      xhr.onerror = xhr.onload;
4272    }
4273    xhr.open('GET', '//a.4cdn.org/' + tuid[1] + '/thread/' + tuid[0] + '.json');
4274    xhr.send(null);
4275  };
4276  
4277  /**
4278   * Thread expansion
4279   */
4280  var ThreadExpansion = {};
4281  
4282  ThreadExpansion.init = function() {
4283    this.enabled = UA.hasCORS;
4284  };
4285  
4286  ThreadExpansion.expandComment = function(link) {
4287    var ids, tid, pid, abbr;
4288    
4289    if (!(ids = link.getAttribute('href').match(/^(?:thread\/)([0-9]+)#p([0-9]+)$/))) {
4290      return;
4291    }
4292    
4293    tid = ids[1];
4294    pid = ids[2];
4295    
4296    abbr = link.parentNode;
4297    abbr.textContent = 'Loading...';
4298    
4299    $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json',
4300      {
4301        onload: function() {
4302          var i, msg, com, post, posts;
4303          
4304          if (this.status == 200) {
4305            msg = $.id('m' + pid);
4306            
4307            posts = Parser.parseThreadJSON(this.responseText);
4308            
4309            if (tid == pid) {
4310              post = posts[0];
4311            }
4312            else {
4313              for (i = posts.length - 1; i > 0; i--) {
4314                if (posts[i].no == pid) {
4315                  post = posts[i];
4316                  break;
4317                }
4318              }
4319            }
4320            
4321            if (post) {
4322              post = Parser.buildHTMLFromJSON(post, Main.board);
4323              
4324              msg.innerHTML = $.cls('postMessage', post)[0].innerHTML;
4325              
4326              if (Parser.prettify) {
4327                Parser.parseMarkup(msg);
4328              }
4329              if (window.jsMath) {
4330                Parser.parseMathOne(msg);
4331              }
4332            }
4333            else {
4334              abbr.textContent = "This post doesn't exist anymore.";
4335            }
4336          }
4337          else if (this.status == 404) {
4338            abbr.textContent = "This thread doesn't exist anymore.";
4339          }
4340          else {
4341            abbr.textContent = 'Connection Error';
4342            console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText);
4343          }
4344        },
4345        onerror: function() {
4346          abbr.textContent = 'Connection Error';
4347          console.log('ThreadExpansion: xhr failed');
4348        }
4349      }
4350    );
4351  };
4352  
4353  ThreadExpansion.toggle = function(tid) {
4354    var thread, msg, expmsg, summary, tmp;
4355    
4356    thread = $.id('t' + tid);
4357    summary = thread.children[1];
4358    if (thread.hasAttribute('data-truncated')) {
4359      msg = $.id('m' + tid);
4360      expmsg = msg.nextSibling;
4361    }
4362    
4363    if ($.hasClass(thread, 'tExpanded')) {
4364      thread.className = thread.className.replace(' tExpanded', ' tCollapsed');
4365      summary.children[0].src = Main.icons.plus;
4366      summary.children[1].style.display = 'inline';
4367      summary.children[2].style.display = 'none';
4368      if (msg) {
4369        tmp = msg.innerHTML;
4370        msg.innerHTML = expmsg.textContent;
4371        expmsg.textContent = tmp;
4372      }
4373    }
4374    else if ($.hasClass(thread, 'tCollapsed')) {
4375      thread.className = thread.className.replace(' tCollapsed', ' tExpanded');
4376      summary.children[0].src = Main.icons.minus;
4377      summary.children[1].style.display = 'none';
4378      summary.children[2].style.display = 'inline';
4379      if (msg) {
4380        tmp = msg.innerHTML;
4381        msg.innerHTML = expmsg.textContent;
4382        expmsg.textContent = tmp;
4383      }
4384    }
4385    else {
4386      summary.children[0].src = Main.icons.rotate;
4387      ThreadExpansion.fetch(tid);
4388    }
4389  };
4390  
4391  ThreadExpansion.fetch = function(tid) {
4392    $.get('//a.4cdn.org/' + Main.board + '/thread/' + tid + '.json',
4393      {
4394        onload: function() {
4395          var i, p, n, frag, thread, tail, posts, count, msg, metacap,
4396            expmsg, summary, abbr;
4397          
4398          thread = $.id('t' + tid);
4399          summary = thread.children[1];
4400          
4401          if (this.status == 200) {
4402            tail = $.cls('reply', thread);
4403            
4404            posts = Parser.parseThreadJSON(this.responseText);
4405            
4406            if (!Config.revealSpoilers && posts[0].custom_spoiler) {
4407              Parser.setCustomSpoiler(Main.board, posts[0].custom_spoiler);
4408            }
4409            
4410            frag = document.createDocumentFragment();
4411            
4412            if (tail[0]) {
4413              tail = +tail[0].id.slice(1);
4414              
4415              for (i = 1; p = posts[i]; ++i) {
4416                if (p.no < tail) {
4417                  n = Parser.buildHTMLFromJSON(p, Main.board);
4418                  n.className += ' rExpanded';
4419                  frag.appendChild(n);
4420                }
4421                else {
4422                  break;
4423                }
4424              }
4425            }
4426            else {
4427              for (i = 1; p = posts[i]; ++i) {
4428                n = Parser.buildHTMLFromJSON(p, Main.board);
4429                n.className += ' rExpanded';
4430                frag.appendChild(n);
4431              }
4432            }
4433            
4434            msg = $.id('m' + tid);
4435            if ((abbr = $.cls('abbr', msg)[0])
4436              && /^Comment/.test(abbr.textContent)) {
4437              thread.setAttribute('data-truncated', '1');
4438              expmsg = document.createElement('div');
4439              expmsg.style.display = 'none';
4440              expmsg.textContent = msg.innerHTML;
4441              msg.parentNode.insertBefore(expmsg, msg.nextSibling);
4442              if (metacap = $.cls('capcodeReplies', msg)[0]) {
4443                msg.innerHTML = posts[0].com + '<br><br>';
4444                msg.appendChild(metacap);
4445              }
4446              else {
4447                msg.innerHTML = posts[0].com;
4448              }
4449              if (Parser.prettify) {
4450                Parser.parseMarkup(msg);
4451              }
4452              if (window.jsMath) {
4453                Parser.parseMathOne(msg);
4454              }
4455            }
4456            
4457            thread.insertBefore(frag, summary.nextSibling);
4458            Parser.parseThread(tid, 1, i - 1);
4459            
4460            thread.className += ' tExpanded';
4461            summary.children[0].src = Main.icons.minus;
4462            summary.children[1].style.display = 'none';
4463            summary.children[2].style.display = 'inline';
4464          }
4465          else if (this.status == 404) {
4466            summary.children[0].src = Main.icons.plus;
4467            summary.children[0].display = 'none';
4468            summary.children[1].textContent = "This thread doesn't exist anymore.";
4469          }
4470          else {
4471            summary.children[0].src = Main.icons.plus;
4472            console.log('ThreadExpansion: ' + this.status + ' ' + this.statusText);
4473          }
4474        },
4475        onerror: function() {
4476          $.id('t' + tid).children[1].children[0].src = Main.icons.plus;
4477          console.log('ThreadExpansion: xhr failed');
4478        }
4479      }
4480    );
4481  };
4482  
4483  /**
4484   * Thread updater
4485   */
4486  var ThreadUpdater = {};
4487  
4488  ThreadUpdater.init = function() {
4489    if (!UA.hasCORS) {
4490      return;
4491    }
4492    
4493    this.enabled = true;
4494    
4495    this.pageTitle = document.title;
4496    
4497    this.unreadCount = 0;
4498    this.auto = this.hadAuto = false;
4499    
4500    this.delayId = 0;
4501    this.delayIdHidden = 4;
4502    this.delayRange = [ 10, 15, 20, 30, 60, 90, 120, 180, 240, 300 ];
4503    this.timeLeft = 0;
4504    this.interval = null;
4505    
4506    this.lastModified = '0';
4507    this.lastReply = null;
4508    
4509    this.currentIcon = null;
4510    this.iconPath = '//s.4cdn.org/image/';
4511    this.iconNode = document.head.querySelector('link[rel="shortcut icon"]');
4512    this.iconNode.type = 'image/x-icon';
4513    this.defaultIcon = this.iconNode.getAttribute('href').replace(this.iconPath, '');
4514    
4515    this.deletionQueue = {};
4516    
4517    if (Config.updaterSound) {
4518      this.audioEnabled = false;
4519      this.audio = document.createElement('audio');
4520      this.audio.src = '//s.4cdn.org/media/beep.ogg';
4521    }
4522    
4523    this.hidden = 'hidden';
4524    this.visibilitychange = 'visibilitychange';
4525    
4526    this.adRefreshDelay = 1000;
4527    this.adDebounce = 0;
4528    this.ads = {};
4529    
4530    if (typeof document.hidden === 'undefined') {
4531      if ('mozHidden' in document) {
4532        this.hidden = 'mozHidden';
4533        this.visibilitychange = 'mozvisibilitychange';
4534      }
4535      else if ('webkitHidden' in document) {
4536        this.hidden = 'webkitHidden';
4537        this.visibilitychange = 'webkitvisibilitychange';
4538      }
4539      else if ('msHidden' in document) {
4540        this.hidden = 'msHidden';
4541        this.visibilitychange = 'msvisibilitychange';
4542      }
4543    }
4544    
4545    this.initAds();
4546    this.initControls();
4547    
4548    document.addEventListener('scroll', this.onScroll, false);
4549    
4550    if (Config.alwaysAutoUpdate || sessionStorage.getItem('4chan-auto-' + Main.tid)) {
4551      this.start();
4552    }
4553  };
4554  
4555  ThreadUpdater.buildMobileControl = function(el, bottom) {
4556    var wrap, cnt, ctrl, cb, label, oldBtn, btn;
4557    
4558    bottom = (bottom ? 'Bot' : '');
4559    
4560    wrap = document.createElement('div');
4561    wrap.className = 'btn-row';
4562    
4563    // Update button
4564    oldBtn = el.parentNode;
4565    
4566    btn = oldBtn.cloneNode(true);
4567    btn.textContent = 'Update';
4568    btn.setAttribute('data-cmd', 'update');
4569    
4570    wrap.appendChild(btn);
4571    cnt = el.parentNode.parentNode;
4572    ctrl = document.createElement('span');
4573    ctrl.className = 'mobileib button';
4574    
4575    // Auto checkbox
4576    label = document.createElement('label');
4577    cb = document.createElement('input');
4578    cb.type = 'checkbox';
4579    cb.setAttribute('data-cmd', 'auto');
4580    this['autoNode' + bottom] = cb;
4581    label.appendChild(cb);
4582    label.appendChild(document.createTextNode('Auto'));
4583    ctrl.appendChild(label);
4584    wrap.appendChild(document.createTextNode(' '));
4585    wrap.appendChild(ctrl);
4586    
4587    // Status label
4588    label = document.createElement('div');
4589    label.className = 'mobile-tu-status';
4590    
4591    wrap.appendChild(this['statusNode' + bottom] = label);
4592    
4593    cnt.appendChild(wrap);
4594    
4595    // Remove Update button
4596    oldBtn.parentNode.removeChild(oldBtn);
4597    
4598    $.id('mpostform').parentNode.style.marginTop = '';
4599  };
4600  
4601  ThreadUpdater.buildDesktopControl = function(bottom) {
4602    var frag, el, label, navlinks;
4603    
4604    bottom = (bottom ? 'Bot' : '');
4605    
4606    frag = document.createDocumentFragment();
4607    
4608    // Update button
4609    frag.appendChild(document.createTextNode(' ['));
4610    el = document.createElement('a');
4611    el.href = '';
4612    el.textContent = 'Update';
4613    el.setAttribute('data-cmd', 'update');
4614    frag.appendChild(el);
4615    frag.appendChild(document.createTextNode(']'));
4616    
4617    // Auto checkbox
4618    frag.appendChild(document.createTextNode(' ['));
4619    label = document.createElement('label');
4620    el = document.createElement('input');
4621    el.type = 'checkbox';
4622    el.title = 'Fetch new replies automatically';
4623    el.setAttribute('data-cmd', 'auto');
4624    this['autoNode' + bottom] = el;
4625    label.appendChild(el);
4626    label.appendChild(document.createTextNode('Auto'));
4627    frag.appendChild(label);
4628    frag.appendChild(document.createTextNode('] '));
4629    
4630    if (Config.updaterSound) {
4631      // Sound checkbox
4632      frag.appendChild(document.createTextNode(' ['));
4633      label = document.createElement('label');
4634      el = document.createElement('input');
4635      el.type = 'checkbox';
4636      el.title = 'Play a sound on new replies to your posts';
4637      el.setAttribute('data-cmd', 'sound');
4638      this['soundNode' + bottom] = el;
4639      label.appendChild(el);
4640      label.appendChild(document.createTextNode('Sound'));
4641      frag.appendChild(label);
4642      frag.appendChild(document.createTextNode('] '));
4643    }
4644    
4645    // Status label
4646    frag.appendChild(
4647      this['statusNode' + bottom] = document.createElement('span')
4648    );
4649    
4650    if (bottom) {
4651      navlinks = $.cls('navLinks' + bottom)[0];
4652    }
4653    else {
4654      navlinks = $.cls('navLinks')[1];
4655    }
4656    
4657    if (navlinks) {
4658      navlinks.appendChild(frag);
4659    }
4660  };
4661  
4662  ThreadUpdater.initControls = function() {
4663    var i, j, frag, el, label, navlinks;
4664    
4665    // Mobile
4666    if (Main.hasMobileLayout) {
4667      this.buildMobileControl($.id('refresh_top'));
4668      this.buildMobileControl($.id('refresh_bottom'), true);
4669    }
4670    // Desktop
4671    else {
4672      this.buildDesktopControl();
4673      this.buildDesktopControl(true);
4674    }
4675  };
4676  
4677  ThreadUpdater.start = function() {
4678    this.auto = this.hadAuto = true;
4679    this.autoNode.checked = this.autoNodeBot.checked = true;
4680    this.force = this.updating = false;
4681    this.lastUpdated = Date.now();
4682    if (this.hidden) {
4683      document.addEventListener(this.visibilitychange,
4684        this.onVisibilityChange, false);
4685    }
4686    this.delayId = 0;
4687    this.timeLeft = this.delayRange[0];
4688    this.pulse();
4689    sessionStorage.setItem('4chan-auto-' + Main.tid, 1);
4690  };
4691  
4692  ThreadUpdater.stop = function(manual) {
4693    clearTimeout(this.interval);
4694    this.auto = this.updating = this.force = false;
4695    this.autoNode.checked = this.autoNodeBot.checked = false;
4696    if (this.hidden) {
4697      document.removeEventListener(this.visibilitychange,
4698        this.onVisibilityChange, false);
4699    }
4700    if (manual) {
4701      this.setStatus('');
4702      this.setIcon(null);
4703    }
4704    sessionStorage.removeItem('4chan-auto-' + Main.tid);
4705  };
4706  
4707  ThreadUpdater.pulse = function() {
4708    var self = ThreadUpdater;
4709    
4710    if (self.timeLeft == 0) {
4711      self.update();
4712    }
4713    else {
4714      self.setStatus(self.timeLeft--);
4715      self.interval = setTimeout(self.pulse, 1000);
4716    }
4717  };
4718  
4719  ThreadUpdater.adjustDelay = function(postCount)
4720  {
4721    if (postCount == 0) {
4722      if (!this.force) {
4723        if (this.delayId < this.delayRange.length - 1) {
4724          ++this.delayId;
4725        }
4726      }
4727    }
4728    else {
4729      this.delayId = document[this.hidden] ? this.delayIdHidden : 0;
4730    }
4731    this.timeLeft = this.delayRange[this.delayId];
4732    if (this.auto) {
4733      this.pulse();
4734    }
4735  };
4736  
4737  ThreadUpdater.onVisibilityChange = function(e) {
4738    var self = ThreadUpdater;
4739    
4740    if (document[self.hidden] && self.delayId < self.delayIdHidden) {
4741      self.delayId = self.delayIdHidden;
4742    }
4743    else {
4744      self.delayId = 0;
4745      self.refreshAds();
4746    }
4747    
4748    self.timeLeft = self.delayRange[0];
4749    self.lastUpdated = Date.now();
4750    clearTimeout(self.interval);
4751    self.pulse();
4752  };
4753  
4754  ThreadUpdater.onScroll = function(e) {
4755    if (ThreadUpdater.hadAuto &&
4756        (document.documentElement.scrollHeight
4757        <= (window.innerHeight + window.pageYOffset)
4758        && !document[ThreadUpdater.hidden])) {
4759      ThreadUpdater.clearUnread();
4760    }
4761    
4762    ThreadUpdater.refreshAds();
4763  };
4764  
4765  ThreadUpdater.clearUnread = function() {
4766    if (!this.dead) {
4767      this.setIcon(null);
4768    }
4769    if (this.lastReply) {
4770      this.unreadCount = 0;
4771      document.title = this.pageTitle;
4772      $.removeClass(this.lastReply, 'newPostsMarker');
4773      this.lastReply = null;
4774    }
4775  };
4776  
4777  ThreadUpdater.forceUpdate = function() {
4778    ThreadUpdater.force = true;
4779    ThreadUpdater.update();
4780  };
4781  
4782  ThreadUpdater.toggleAuto = function() {
4783    if (this.updating) {
4784      return;
4785    }
4786    this.auto ? this.stop(true) : this.start();
4787  };
4788  
4789  ThreadUpdater.toggleSound = function() {
4790    this.soundNode.checked = this.soundNodeBot.checked =
4791      this.audioEnabled = !this.audioEnabled;
4792  };
4793  
4794  ThreadUpdater.update = function() {
4795    var self, now = Date.now();
4796    
4797    self = ThreadUpdater;
4798    
4799    if (self.updating) {
4800      return;
4801    }
4802    
4803    clearTimeout(self.interval);
4804    
4805    self.updating = true;
4806    
4807    self.setStatus('Updating...');
4808    
4809    $.get('//a.4cdn.org/' + Main.board + '/thread/' + Main.tid + '.json',
4810      {
4811        onload: self.onload,
4812        onerror: self.onerror
4813      },
4814      {
4815        'If-Modified-Since': self.lastModified
4816      }
4817    );
4818  };
4819  
4820  ThreadUpdater.initAds = function() {
4821    var i, id, adIds = [ '_top_ad', '_middle_ad', '_bottom_ad' ];
4822    
4823    for (i = 0; id = adIds[i]; ++i) {
4824      ThreadUpdater.ads[id] = {
4825        time: 0,
4826        seenOnce: false,
4827        isStale: false
4828      };
4829    }
4830  };
4831  
4832  ThreadUpdater.invalidateAds = function() {
4833    var id, self = ThreadUpdater;
4834    
4835    for (id in self.ads) {
4836      meta = self.ads[id];
4837      if (meta.seenOnce) {
4838        meta.isStale = true;
4839      }
4840    }
4841  };
4842  
4843  ThreadUpdater.refreshAds = function() {
4844    var self, now, el, id, ad, meta, hidden, docHeight, offset;
4845    
4846    self = ThreadUpdater;
4847    
4848    now = Date.now();
4849    
4850    if (now - self.adDebounce < 100) {
4851      return;
4852    }
4853    
4854    self.adDebounce = now;
4855    
4856    hidden = document[self.hidden];
4857    docHeight = document.documentElement.clientHeight;
4858    
4859    for (id in self.ads) {
4860      meta = self.ads[id];
4861      
4862      if (hidden) {
4863        continue;
4864      }
4865      
4866      ad = window[id];
4867      
4868      if (!ad) {
4869        continue;
4870      }
4871      
4872      el = $.id(ad.D);
4873      
4874      if (!el) {
4875        continue;
4876      }
4877      
4878      offset = el.getBoundingClientRect();
4879      
4880      if (offset.top < 0 || offset.bottom > docHeight) {
4881        continue;
4882      }
4883      
4884      meta.seenOnce = true;
4885      
4886      if (!meta.isStale || now - meta.time < self.adRefreshDelay) {
4887        continue;
4888      }
4889      
4890      meta.time = now;
4891      meta.isStale = false;
4892      
4893      ados_refresh(ad, 0, false);
4894    }
4895  };
4896  
4897  ThreadUpdater.markDeletedReplies = function(newposts) {
4898    var i, j, posthash, oldposts, el;
4899    
4900    posthash = {};
4901    for (i = 0; j = newposts[i]; ++i) {
4902      posthash['pc' + j.no] = 1;
4903    }
4904    
4905    oldposts = $.cls('replyContainer');
4906    for (i = 0; j = oldposts[i]; ++i) {
4907      if (!posthash[j.id] && !$.hasClass(j, 'deleted')) {
4908        if (this.deletionQueue[j.id]) {
4909          el = document.createElement('img');
4910          el.src = Main.icons2.trash;
4911          el.className = 'trashIcon';
4912          el.title = 'This post has been deleted';
4913          $.addClass(j, 'deleted');
4914          $.cls('postNum', j)[1].appendChild(el);
4915          delete this.deletionQueue[j.id];
4916        }
4917        else {
4918          this.deletionQueue[j.id] = 1;
4919        }
4920      }
4921    }
4922  };
4923  
4924  ThreadUpdater.onload = function() {
4925    var i, el, state, self, nodes, thread, newposts, frag, lastrep, lastid,
4926      spoiler, op, doc, autoscroll, count, fromQR, lastRepPos;
4927    
4928    self = ThreadUpdater;
4929    nodes = [];
4930    
4931    self.setStatus('');
4932    
4933    if (this.status == 200) {
4934      self.lastModified = this.getResponseHeader('Last-Modified');
4935      
4936      thread = $.id('t' + Main.tid);
4937      
4938      lastrep = thread.children[thread.childElementCount - 1];
4939      lastid = +lastrep.id.slice(2);
4940      
4941      newposts = Parser.parseThreadJSON(this.responseText);
4942      
4943      state = !!newposts[0].archived;
4944      if (window.thread_archived !== undefined && state != window.thread_archived) {
4945        QR.enabled && $.id('quickReply') && QR.lock();
4946        Main.setThreadState('archived', state);
4947      }
4948      
4949      state = !!newposts[0].closed;
4950      if (state != Main.threadClosed) {
4951        if (newposts[0].archived) {
4952          state = false;
4953        }
4954        else if (QR.enabled && $.id('quickReply')) {
4955          if (state) {
4956            QR.lock();
4957          }
4958          else {
4959            QR.unlock();
4960          }
4961        }
4962        Main.setThreadState('closed', state);
4963      }
4964      
4965      state = !!newposts[0].sticky;
4966      if (state != Main.threadSticky) {
4967        Main.setThreadState('sticky', state);
4968      }
4969      
4970      state = !!newposts[0].imagelimit;
4971      if (QR.enabled && state != QR.fileDisabled) {
4972        QR.fileDisabled = state;
4973      }
4974      
4975      if (!Config.revealSpoilers && newposts[0].custom_spoiler) {
4976        Parser.setCustomSpoiler(Main.board, newposts[0].custom_spoiler);
4977      }
4978      
4979      for (i = newposts.length - 1; i >= 0; i--) {
4980        if (newposts[i].no <= lastid) {
4981          break;
4982        }
4983        nodes.push(newposts[i]);
4984      }
4985      
4986      count = nodes.length;
4987      
4988      if (count == 1 && QR.lastReplyId == nodes[0].no) {
4989        fromQR = true;
4990        QR.lastReplyId = null;
4991      }
4992      
4993      if (!fromQR) {
4994        self.markDeletedReplies(newposts);
4995      }
4996      
4997      if (count) {
4998        doc = document.documentElement;
4999        
5000        autoscroll = (
5001          Config.autoScroll
5002          && document[self.hidden]
5003          && doc.scrollHeight == (window.innerHeight + window.pageYOffset)
5004        );
5005        
5006        frag = document.createDocumentFragment();
5007        for (i = nodes.length - 1; i >= 0; i--) {
5008          frag.appendChild(Parser.buildHTMLFromJSON(nodes[i], Main.board));
5009        }
5010        thread.appendChild(frag);
5011        
5012        lastRepPos = lastrep.offsetTop;
5013        
5014        Parser.hasYouMarkers = false;
5015        Parser.hasHighlightedPosts = false;
5016        Parser.parseThread(thread.id.slice(1), -nodes.length);
5017        
5018        if (lastRepPos != lastrep.offsetTop) {
5019          window.scrollBy(0, lastrep.offsetTop - lastRepPos);
5020        }
5021        
5022        if (!fromQR) {
5023          if (!self.force && doc.scrollHeight > window.innerHeight) {
5024            if (!self.lastReply && lastid != Main.tid) {
5025              (self.lastReply = lastrep.lastChild).className += ' newPostsMarker';
5026            }
5027            if (Parser.hasYouMarkers) {
5028              self.setIcon('rep');
5029              if (self.audioEnabled && document[self.hidden]) {
5030                self.audio.play();
5031              }
5032            }
5033            else if (Parser.hasHighlightedPosts && self.currentIcon !== 'rep') {
5034              self.setIcon('hl');
5035            }
5036            else if (self.unreadCount == 0) {
5037              self.setIcon('new');
5038            }
5039            self.unreadCount += count;
5040            document.title = '(' + self.unreadCount + ') ' + self.pageTitle;
5041          }
5042          else {
5043            self.setStatus(count + ' new post' + (count > 1 ? 's' : ''));
5044          }
5045        }
5046        
5047        if (autoscroll) {
5048          window.scrollTo(0, document.documentElement.scrollHeight);
5049        }
5050        
5051        if (Config.threadWatcher) {
5052          ThreadWatcher.refreshCurrent(true);
5053        }
5054        
5055        if (Config.threadStats) {
5056          op = newposts[0];
5057          ThreadStats.update(op.replies, op.images, op.bumplimit, op.imagelimit);
5058        }
5059        
5060        self.invalidateAds();
5061        self.refreshAds();
5062        
5063        UA.dispatchEvent('4chanThreadUpdated', { count: count });
5064      }
5065      else {
5066        self.setStatus('No new posts');
5067      }
5068      
5069      if (newposts[0].archived) {
5070        self.setError('This thread is archived');
5071        if (!self.dead) {
5072          self.setIcon('dead');
5073          window.thread_archived = true;
5074          self.dead = true;
5075          self.stop();
5076        }
5077      }
5078    }
5079    else if (this.status == 304 || this.status == 0) {
5080      self.setStatus('No new posts');
5081    }
5082    else if (this.status == 404) {
5083      self.setIcon('dead');
5084      self.setError('This thread has been pruned or deleted');
5085      self.dead = true;
5086      self.stop();
5087      return;
5088    }
5089    
5090    self.lastUpdated = Date.now();
5091    self.adjustDelay(nodes.length);
5092    self.updating = self.force = false;
5093  };
5094  
5095  ThreadUpdater.onerror = function() {
5096    var self = ThreadUpdater;
5097    
5098    if (UA.isOpera && !this.statusText && this.status == 0) {
5099      self.setStatus('No new posts');
5100    }
5101    else {
5102      self.setError('Connection Error');
5103    }
5104    
5105    self.lastUpdated = Date.now();
5106    self.adjustDelay(0);
5107    self.updating = self.force = false;
5108  };
5109  
5110  ThreadUpdater.setStatus = function(msg) {
5111    this.statusNode.textContent = this.statusNodeBot.textContent = msg;
5112  };
5113  
5114  ThreadUpdater.setError = function(msg) {
5115    this.statusNode.innerHTML
5116      = this.statusNodeBot.innerHTML
5117      = '<span class="tu-error">' + msg + '</span>';
5118  };
5119  
5120  ThreadUpdater.setIcon = function(type) {
5121    var icon;
5122    
5123    if (type === null) {
5124      icon = this.defaultIcon;
5125    }
5126    else {
5127      icon = this.icons[Main.type + type];
5128    }
5129    
5130    this.currentIcon = type;
5131    this.iconNode.href = this.iconPath + icon;
5132    document.head.appendChild(this.iconNode);
5133  };
5134  
5135  ThreadUpdater.icons = {
5136    wsnew: 'favicon-ws-newposts.ico',
5137    nwsnew: 'favicon-nws-newposts.ico',
5138    wsrep: 'favicon-ws-newreplies.ico',
5139    nwsrep: 'favicon-nws-newreplies.ico',
5140    wsdead: 'favicon-ws-deadthread.ico',
5141    nwsdead: 'favicon-nws-deadthread.ico',
5142    wshl: 'favicon-ws-newfilters.ico',
5143    nwshl: 'favicon-nws-newfilters.ico'
5144  };
5145  
5146  /**
5147   * Thread stats
5148   */
5149  var ThreadStats = {};
5150  
5151  ThreadStats.init = function() {
5152    var i, cnt;
5153    
5154    this.nodeTop = document.createElement('div');
5155    this.nodeTop.className = 'thread-stats';
5156    this.nodeBot = this.nodeTop.cloneNode(false);
5157    
5158    cnt = $.cls('navLinks');
5159    cnt[1] && cnt[1].appendChild(this.nodeTop);
5160    cnt[2] && cnt[2].appendChild(this.nodeBot);
5161    
5162    this.pageNumber = null;
5163    this.update(null, null, window.bumplimit, window.imagelimit);
5164    
5165    if (!window.thread_archived) {
5166      this.updatePageNumber();
5167      this.pageInterval = setInterval(this.updatePageNumber, 3 * 60000);
5168    }
5169  };
5170  
5171  ThreadStats.update = function(replies, images, isBumpFull, isImageFull) {
5172    var stats, repStr, imgStr, pageStr, stateStr;
5173    
5174    if (replies === null) {
5175      replies = $.cls('replyContainer').length;
5176      images = $.cls('fileText').length - ($.id('fT' + Main.tid) ? 1 : 0);
5177    }
5178    
5179    stats = [];
5180    
5181    if (Main.threadSticky) {
5182      stats.push('Sticky');
5183    }
5184    
5185    if (window.thread_archived) {
5186      stats.push('Archived');
5187    }
5188    else if (Main.threadClosed) {
5189      stats.push('Closed');
5190    }
5191    
5192    if (isBumpFull) {
5193      stats.push('<em data-tip="Bump limit reached">' + replies + '</em>');
5194    }
5195    else {
5196      stats.push('<span data-tip="Replies">' + replies + '</span>');
5197    }
5198    
5199    if (isImageFull) {
5200      stats.push('<em data-tip="Image limit reached">' + images + '</em>');
5201    }
5202    else {
5203      stats.push('<span data-tip="Images">' + images + '</span>');
5204    }
5205    
5206    if (!window.thread_archived) {
5207      stats.push('<span data-tip="Page" class="ts-page">' + (this.pageNumber || '?') + '</span>');
5208    }
5209    
5210    this.nodeTop.innerHTML = this.nodeBot.innerHTML
5211      = stats.join(' / ');
5212  };
5213  
5214  ThreadStats.updatePageNumber = function() {
5215    $.get('//a.4cdn.org/' + Main.board + '/threads.json',
5216      {
5217        onload: ThreadStats.onCatalogLoad,
5218        onerror: ThreadStats.onCatalogError
5219      }
5220    );
5221  };
5222  
5223  ThreadStats.onCatalogLoad = function() {
5224    var self, i, j, page, post, threads, catalog, tid, nodes;
5225    
5226    self = ThreadStats;
5227    
5228    if (this.status == 200) {
5229      tid = +Main.tid;
5230      catalog = JSON.parse(this.responseText);
5231      for (i = 0; page = catalog[i]; ++i) {
5232        threads = page.threads;
5233        for (j = 0; post = threads[j]; ++j) {
5234          if (post.no == tid) {
5235            nodes = $.cls('ts-page');
5236            nodes[0].textContent = nodes[1].textContent = page.page
5237            self.pageNumber = page.page;
5238            return;
5239          }
5240        }
5241      }
5242      clearInterval(self.pageInterval);
5243    }
5244    else {
5245      ThreadStats.onCatalogError();
5246    }
5247  };
5248  
5249  ThreadStats.onCatalogError = function() {
5250    console.log('ThreadStats: couldn\'t get the catalog (' + this.status + ')');
5251  };
5252  
5253  /**
5254   * Filter
5255   */
5256  var Filter = {};
5257  
5258  Filter.init = function() {
5259    this.entities = document.createElement('div');
5260    Filter.load();
5261  };
5262  
5263  Filter.onClick = function(e) {
5264    var cmd;
5265    
5266    if (cmd = e.target.getAttribute('data-cmd')) {
5267      switch (cmd) {
5268        case 'filters-add':
5269          Filter.add();
5270          break;
5271        case 'filters-save':
5272          Filter.save();
5273          Filter.close();
5274          break;
5275        case 'filters-close':
5276          Filter.close();
5277          break;
5278        case 'filters-palette':
5279          Filter.openPalette(e.target);
5280          break;
5281        case 'filters-palette-close':
5282          Filter.closePalette();
5283          break;
5284        case 'filters-palette-clear':
5285          Filter.clearPalette();
5286          break;
5287        case 'filters-up':
5288          Filter.moveUp(e.target.parentNode.parentNode);
5289          break;
5290        case 'filters-del':
5291          Filter.remove(e.target.parentNode.parentNode);
5292          break;
5293        case 'filters-help-open':
5294          Filter.openHelp();
5295          break;
5296        case 'filters-help-close':
5297          Filter.closeHelp();
5298          break;
5299      }
5300    }
5301  };
5302  
5303  Filter.onPaletteClick = function(e) {
5304    var cmd;
5305    
5306    if (cmd = e.target.getAttribute('data-cmd')) {
5307      switch (cmd) {
5308        case 'palette-pick':
5309          Filter.pickColor(e.target);
5310          break;
5311        case 'palette-clear':
5312          Filter.pickColor(e.target, true);
5313          break;
5314        case 'palette-close':
5315          Filter.closePalette();
5316          break;
5317      }
5318    }
5319  };
5320  
5321  Filter.exec = function(cnt, pi, msg, tid) {
5322    var trip, name, com, uid, sub, fname, f, filters, hit, currentBoard;
5323    
5324    if (Parser.trackedReplies && Parser.trackedReplies['>>' + pi.id.slice(2)]) {
5325      return false;
5326    }
5327    
5328    currentBoard = Main.board;
5329    filters = Filter.activeFilters;
5330    hit = false;
5331    
5332    for (i = 0; f = filters[i]; ++i) {
5333      // boards
5334      if (f.boards && !f.boards[currentBoard]) {
5335        continue;
5336      }
5337      // tripcode
5338      if (f.type == 0) {
5339        if ((trip !== undefined || (trip = pi.getElementsByClassName('postertrip')[0])
5340          ) && f.pattern == trip.textContent) {
5341          hit = true;
5342          break;
5343        }
5344      }
5345      // name
5346      else if (f.type == 1) {
5347        if ((name || (name = pi.getElementsByClassName('name')[0]))
5348          && f.pattern == name.textContent) {
5349          hit = true;
5350          break;
5351        }
5352      }
5353      // comment
5354      else if (f.type == 2) {
5355        if (com === undefined) {
5356          this.entities.innerHTML
5357            = msg.innerHTML.replace(/<br>/g, '\n').replace(/[<[^>]+>/g, '');
5358          com = this.entities.textContent;
5359        }
5360        if (f.pattern.test(com)) {
5361          hit = true;
5362          break;
5363        }
5364      }
5365      // user id
5366      else if (f.type == 4) {
5367        if ((uid ||
5368            ((uid = pi.getElementsByClassName('posteruid')[0])
5369              && (uid = uid.firstElementChild.textContent)
5370            )
5371          ) && f.pattern == uid) {
5372          hit = true;
5373          break;
5374        }
5375      }
5376      // subject
5377      else if (!Main.tid && f.type == 5) {
5378        if ((sub ||
5379            ((sub = pi.getElementsByClassName('subject')[0])
5380              && (sub = sub.textContent)
5381            )
5382          ) && f.pattern.test(sub)) {
5383          hit = true;
5384          break;
5385        }
5386      }
5387      // filename
5388      else if (f.type == 6) {
5389        if (fname === undefined) {
5390          if ((fname = pi.parentNode.getElementsByClassName('fileText')[0])) {
5391            fname = fname.firstElementChild.textContent;
5392          }
5393          else {
5394            fname = '';
5395          }
5396        }
5397        if (f.pattern.test(fname)) {
5398          hit = true;
5399          break;
5400        }
5401      }
5402    }
5403    
5404    if (hit) {
5405      if (f.hide) {
5406        cnt.className += ' post-hidden';
5407        el = document.createElement('span');
5408        if (!tid) {
5409          el.textContent = '[View]';
5410          el.setAttribute('data-filtered', '1');
5411        }
5412        else {
5413          el.innerHTML = '[<a data-filtered="1" href="thread/' + tid + '">View</a>]';
5414        }
5415        el.className = 'filter-preview';
5416        pi.appendChild(el);
5417        return true;
5418      }
5419      else {
5420        cnt.className += ' filter-hl';
5421        cnt.style.boxShadow = '-3px 0 ' + f.color;
5422        Parser.hasHighlightedPosts = true;
5423      }
5424    }
5425    return false;
5426  };
5427  
5428  Filter.load = function() {
5429    var i, j, w, f, rawFilters, rawPattern, fid, regexEscape, regexType,
5430      wordSepS, wordSepE, words, inner, regexWildcard, replaceWildcard;
5431    
5432    this.activeFilters = [];
5433    
5434    if (!(rawFilters = localStorage.getItem('4chan-filters'))) {
5435      return;
5436    }
5437    
5438    rawFilters = JSON.parse(rawFilters);
5439    
5440    regexEscape = new RegExp('(\\'
5441      + ['/', '.', '*', '+', '?', '(', ')', '[', ']', '{', '}', '\\', '^', '$' ].join('|\\')
5442      + ')', 'g');
5443    regexType = /^\/(.*)\/(i?)$/;
5444    wordSepS = '(?=.*\\b';
5445    wordSepE = '\\b)';
5446    regexWildcard = /\\\*/g;
5447    replaceWildcard = '[^\\s]*';
5448    
5449    try {
5450      for (fid = 0; f = rawFilters[fid]; ++fid) {
5451        if (f.active && f.pattern != '') {
5452          // Boards
5453          if (f.boards) {
5454            tmp = f.boards.split(/[^a-z0-9]+/i);
5455            boards = {};
5456            for (i = 0; j = tmp[i]; ++i) {
5457              boards[j] = true;
5458            }
5459          }
5460          else {
5461            boards = false;
5462          }
5463          
5464          rawPattern = f.pattern;
5465          // Name, Tripcode or ID, string comparison
5466          if (!f.type || f.type == 1 || f.type == 4) {
5467            pattern = rawPattern;
5468          }
5469          // /RegExp/
5470          else if (match = rawPattern.match(regexType)) {
5471            pattern = new RegExp(match[1], match[2]);
5472          }
5473          // "Exact match"
5474          else if (rawPattern[0] == '"' && rawPattern[rawPattern.length - 1] == '"') {
5475            pattern = new RegExp(rawPattern.slice(1, -1).replace(regexEscape, '\\$1'));
5476          }
5477          // Full words, AND operator
5478          else {
5479            words = rawPattern.split(' ');
5480            pattern = '';
5481            for (i = 0, j = words.length; i < j; ++i) {
5482              inner = words[i]
5483                .replace(regexEscape, '\\$1')
5484                .replace(regexWildcard, replaceWildcard);
5485              pattern += wordSepS + inner + wordSepE;
5486            }
5487            pattern = new RegExp('^' + pattern, 'im');
5488          }
5489          //console.log('Resulting pattern: ' + pattern);
5490          this.activeFilters.push({
5491            type: f.type,
5492            pattern: pattern,
5493            boards: boards,
5494            color: f.color,
5495            hide: f.hide
5496          });
5497        }
5498      }
5499    }
5500    catch (e) {
5501      alert('There was an error processing one of the filters: '
5502        + e + ' in: ' + rawPattern);
5503    }
5504  };
5505  
5506  Filter.addSelection = function() {
5507    var text, type, node, sel = UA.getSelection(true);
5508    
5509    if (Filter.open() === false) {
5510      return;
5511    }
5512    
5513    if (typeof sel == 'string') {
5514      text = sel.trim();
5515    }
5516    else {
5517      node = sel.anchorNode.parentNode;
5518      text = sel.toString().trim();
5519      
5520      if ($.hasClass(node, 'name')) {
5521        type = 1;
5522      }
5523      else if ($.hasClass(node, 'postertrip')) {
5524        type = 0;
5525      }
5526      else if ($.hasClass(node, 'subject')) {
5527        type = 5;
5528      }
5529      else if ($.hasClass(node, 'posteruid') || $.hasClass(node, 'hand')) {
5530        type = 4;
5531      }
5532      else if ($.hasClass(node, 'fileText')) {
5533        type = 6;
5534      }
5535      else {
5536        type = 2;
5537      }
5538    }
5539    
5540    Filter.add(text, type);
5541  };
5542  
5543  Filter.openHelp = function() {
5544    var cnt;
5545    
5546    if ($.id('filtersHelp')) {
5547      return;
5548    }
5549    
5550    cnt = document.createElement('div');
5551    cnt.id = 'filtersHelp';
5552    cnt.className = 'UIPanel';
5553    cnt.setAttribute('data-cmd', 'filters-help-close');
5554    cnt.innerHTML = '\
5555  <div class="extPanel reply"><div class="panelHeader">Filters &amp; Highlights Help\
5556  <span><img alt="Close" title="Close" class="pointer" data-cmd="filters-help-close" src="'
5557  + Main.icons.cross + '"></span></div>\
5558  <h4>Tripcode, Name and ID filters:</h4>\
5559  <ul><li>Those use simple string comparison.</li>\
5560  <li>Type them exactly as they appear on 4chan, including the exclamation mark for tripcode filters.</li>\
5561  <li>Example: <code>!Ep8pui8Vw2</code></li></ul>\
5562  <h4>Comment, Subject and E-mail filters:</h4>\
5563  <ul><li><strong>Matching whole words:</strong></li>\
5564  <li><code>feel</code> &mdash; will match <em>"feel"</em> but not <em>"feeling"</em>. This search is case-insensitive.</li></ul>\
5565  <ul><li><strong>AND operator:</strong></li>\
5566  <li><code>feel girlfriend</code> &mdash; will match <em>"feel"</em> AND <em>"girlfriend"</em> in any order.</li></ul>\
5567  <ul><li><strong>Exact match:</strong></li>\
5568  <li><code>"that feel when"</code> &mdash; place double quotes around the pattern to search for an exact string</li></ul>\
5569  <ul><li><strong>Wildcards:</strong></li>\
5570  <li><code>feel*</code> &mdash; matches expressions such as <em>"feel"</em>, <em>"feels"</em>, <em>"feeling"</em>, <em>"feeler"</em>, etcโ€ฆ</li>\
5571  <li><code>idolm*ster</code> &mdash; this can match <em>"idolmaster"</em> or <em>"idolm@ster"</em>, etcโ€ฆ</li></ul>\
5572  <ul><li><strong>Regular expressions:</strong></li>\
5573  <li><code>/feel when no (girl|boy)friend/i</code></li>\
5574  <li><code>/^(?!.*touhou).*$/i</code> &mdash; NOT operator.</li>\
5575  <li><code>/^>/</code> &mdash; comments starting with a quote.</li>\
5576  <li><code>/^$/</code> &mdash; comments with no text.</li></ul>\
5577  <h4>Colors:</h4>\
5578  <ul><li>The color field can accept any valid CSS color:</li>\
5579  <li><code>red</code>, <code>#0f0</code>, <code>#00ff00</code>, <code>rgba( 34, 12, 64, 0.3)</code>, etcโ€ฆ</li></ul>\
5580  <h4>Boards:</h4>\
5581  <ul><li>A space separated list of boards on which the filter will be active. Leave blank to apply to all boards.</li></ul>\
5582  <h4>Shortcut:</h4>\
5583  <ul><li>If you have <code>Keyboard shortcuts</code> enabled, pressing <kbd>F</kbd> will add the selected text to your filters.</li></ul>';
5584  
5585    document.body.appendChild(cnt);
5586    cnt.addEventListener('click', this.onClick, false);
5587  };
5588  
5589  Filter.closeHelp = function() {
5590    var cnt;
5591    
5592    if (cnt = $.id('filtersHelp')) {
5593      cnt.removeEventListener('click', this.onClick, false);
5594      document.body.removeChild(cnt);
5595    }
5596  };
5597  
5598  Filter.open = function() {
5599    var i, f, cnt, menu, html, rawFilters, filterId, filterList;
5600    
5601    if ($.id('filtersMenu')) {
5602      return false;
5603    }
5604    
5605    cnt = document.createElement('div');
5606    cnt.id = 'filtersMenu';
5607    cnt.className = 'UIPanel';
5608    cnt.style.display = 'none';
5609    cnt.setAttribute('data-cmd', 'filters-close');
5610    cnt.innerHTML = '\
5611  <div class="extPanel reply"><div class="panelHeader">Filters &amp; Highlights\
5612  <span><img alt="Help" class="pointer" title="Help" data-cmd="filters-help-open" src="'
5613  + Main.icons.help
5614  + '"><img alt="Close" title="Close" class="pointer" data-cmd="filters-close" src="'
5615  + Main.icons.cross + '"></span></div>\
5616  <table><thead><tr>\
5617  <th>Order</th>\
5618  <th>On</th>\
5619  <th>Pattern</th>\
5620  <th>Boards</th>\
5621  <th>Type</th>\
5622  <th>Color</th>\
5623  <th>Hide</th>\
5624  <th>Del</th>\
5625  </tr></thead><tbody id="filter-list"></tbody><tfoot><tr><td colspan="8">\
5626  <button data-cmd="filters-add">Add</button>\
5627  <button class="right" data-cmd="filters-save">Save</button>\
5628  </td></tr></tfoot></table></div>';
5629    
5630    document.body.appendChild(cnt);
5631    cnt.addEventListener('click', this.onClick, false);
5632    
5633    filterList = $.id('filter-list');
5634    
5635    if (rawFilters = localStorage.getItem('4chan-filters')) {
5636      rawFilters = JSON.parse(rawFilters);
5637      for (i = 0; f = rawFilters[i]; ++i) {
5638        filterList.appendChild(this.buildEntry(f, i));
5639      }
5640    }
5641    
5642    cnt.style.display = '';
5643  };
5644  
5645  Filter.close = function() {
5646    var cnt;
5647    
5648    if (cnt = $.id('filtersMenu')) {
5649      this.closePalette();
5650      cnt.removeEventListener('click', this.onClick, false);
5651      document.body.removeChild(cnt);
5652    }
5653  };
5654  
5655  Filter.moveUp = function(el) {
5656    var prev;
5657    
5658    if (prev = el.previousElementSibling) {
5659      el.parentNode.insertBefore(el, prev);
5660    }
5661  };
5662  
5663  Filter.add = function(pattern, type, boards) {
5664    var filter, id, el;
5665    
5666    filter = {
5667      active: true,
5668      type: type || 0,
5669      pattern: pattern || '',
5670      boards: boards || '',
5671      color: '',
5672      hide: false
5673    };
5674    
5675    id = this.getNextFilterId();
5676    el = this.buildEntry(filter, id);
5677    
5678    $.id('filter-list').appendChild(el);
5679    $.cls('fPattern', el)[0].focus();
5680  };
5681  
5682  Filter.remove = function(tr) {
5683    $.id('filter-list').removeChild(tr);
5684  };
5685  
5686  Filter.save = function() {
5687    var i, rawFilters, entries, tr, f, color, type;
5688    
5689    rawFilters = [];
5690    entries = $.id('filter-list').children;
5691    
5692    for (i = 0; tr = entries[i]; ++i) {
5693      type = tr.children[4].firstChild;
5694      f = {
5695        active: tr.children[1].firstChild.checked,
5696        pattern: tr.children[2].firstChild.value,
5697        boards: tr.children[3].firstChild.value,
5698        type: +type.options[type.selectedIndex].value,
5699        hide: tr.children[6].firstChild.checked
5700      }
5701      
5702      color = tr.children[5].firstChild;
5703      
5704      if (!color.hasAttribute('data-nocolor')) {
5705        f.color = color.style.backgroundColor;
5706      }
5707      
5708      rawFilters.push(f);
5709    }
5710    
5711    if (rawFilters[0]) {
5712      localStorage.setItem('4chan-filters', JSON.stringify(rawFilters));
5713    }
5714    else {
5715      localStorage.removeItem('4chan-filters');
5716    }
5717  };
5718  
5719  Filter.getNextFilterId = function() {
5720    var i, j, max, entries = $.id('filter-list').children;
5721    
5722    if (!entries.length) {
5723      return 0;
5724    }
5725    else {
5726      max = 0;
5727      for (i = 0; j = entries[i]; ++i) {
5728        j = +j.id.slice(7);
5729        if (j > max) {
5730          max = j;
5731        }
5732      }
5733      return max + 1;
5734    }
5735  };
5736  
5737  Filter.buildEntry = function(filter, id) {
5738    var tr, html, sel;
5739    
5740    tr = document.createElement('tr');
5741    tr.id = 'filter-' + id;
5742    
5743    html = '';
5744    
5745    // Move up
5746    html += '<td><span data-cmd="filters-up" class="pointer">&uarr;</span></td>';
5747    
5748    // On
5749    html += '<td><input type="checkbox"'
5750      + (filter.active ? ' checked="checked"></td>' : '></td>');
5751    
5752    // Pattern
5753    html += '<td><input class="fPattern" type="text" value="'
5754      + filter.pattern.replace(/"/g, '&quot;') + '"></td>';
5755    
5756    // Boards
5757    html += '<td><input class="fBoards" type="text" value="'
5758      + (filter.boards !== undefined ? filter.boards : '') + '"></td>';
5759    
5760    // FIXME
5761    if (filter.type === 3) {
5762      filter.type = 4;
5763    }
5764    
5765    // Type
5766    sel = [ '', '', '', '', '', '', '' ];
5767    sel[filter.type] = ' selected="selected"';
5768    
5769    html += '<td><select size="1"><option value="0"'
5770      + sel[0] + '>Tripcode</option><option value="1"'
5771      + sel[1] + '>Name</option><option value="2"'
5772      + sel[2] + '>Comment</option><option value="4"'
5773      + sel[4] + '>ID</option><option value="5"'
5774      + sel[5] + '>Subject</option><option value="6"'
5775      + sel[6] + '>Filename</option></select></td>';
5776    
5777    // Color
5778    html += '<td><span data-cmd="filters-palette" title="Change Color" class="colorbox fColor" ';
5779    
5780    if (!filter.color) {
5781      html += ' data-nocolor="1">&#x2215;';
5782    }
5783    else {
5784      html += ' style="background-color:' + filter.color + '">';
5785    }
5786    html += '</span></td>';
5787    
5788    // Hide
5789    html += '<td><input type="checkbox"'
5790      + (filter.hide ? ' checked="checked"></td>' : '></td>');
5791    
5792    // Del
5793    html += '<td><span data-cmd="filters-del" class="pointer fDel">&times;</span></td>';
5794    
5795    tr.innerHTML = html;
5796    
5797    return tr;
5798  }
5799  
5800  Filter.buildPalette = function(id) {
5801    var i, j, cnt, html, colors, rowCount, colCount;
5802    
5803    colors = [
5804      ['#E0B0FF', '#F2F3F4', '#7DF9FF', '#FFFF00'],
5805      ['#FBCEB1', '#FFBF00', '#ADFF2F', '#0047AB'],
5806      ['#00A550', '#007FFF', '#AF0A0F', '#B5BD68']
5807    ];
5808    
5809    rowCount = colors.length;
5810    colCount = colors[0].length;
5811    
5812    html = '<div id="colorpicker" class="reply extPanel"><table><tbody>';
5813    
5814    for (i = 0; i < rowCount; ++i) {
5815      html += '<tr>'
5816      for (j = 0; j < colCount; ++j) {
5817        html += '<td><div data-cmd="palette-pick" class="colorbox" style="background:'
5818          + colors[i][j] + '"></div></td>';
5819      }
5820      html += '</tr>'
5821    }
5822    
5823    html += '</tbody></table>Custom\
5824  <div id="palette-custom"><input id="palette-custom-input" type="text">\
5825  <div id="palette-custom-ok" data-cmd="palette-pick" title="Select Color" class="colorbox"></div></div>\
5826  [<a href="javascript:;" data-cmd="palette-close">Close</a>]\
5827  [<a href="javascript:;" data-cmd="palette-clear">Clear</a>]</div>';
5828    
5829    cnt = document.createElement('div');
5830    cnt.id = 'filter-palette';
5831    cnt.setAttribute('data-target', id);
5832    cnt.setAttribute('data-cmd', 'palette-close');
5833    cnt.className = 'UIMenu';
5834    cnt.innerHTML = html;
5835    
5836    return cnt;
5837  };
5838  
5839  Filter.openPalette = function(target) {
5840    var el, pos, id, picker;
5841    
5842    Filter.closePalette();
5843    
5844    pos = target.getBoundingClientRect();
5845    id = target.parentNode.parentNode.id.slice(7);
5846    
5847    el = Filter.buildPalette(id);
5848    document.body.appendChild(el);
5849    
5850    $.id('filter-palette').addEventListener('click', Filter.onPaletteClick, false);
5851    $.id('palette-custom-input').addEventListener('keyup', Filter.setCustomColor, false);
5852    
5853    picker = el.firstElementChild;
5854    picker.style.cssText = 'top:' + pos.top + 'px;left:'
5855      + (pos.left - picker.clientWidth - 10) + 'px;';
5856  };
5857  
5858  Filter.closePalette = function() {
5859    var el;
5860    
5861    if (el = $.id('filter-palette')) {
5862      $.id('filter-palette').removeEventListener('click', Filter.onPaletteClick, false);
5863      $.id('palette-custom-input').removeEventListener('keyup', Filter.setCustomColor, false);
5864      el.parentNode.removeChild(el);
5865    }
5866  };
5867  
5868  Filter.pickColor = function(el, clear) {
5869    var id, target;
5870    
5871    id = $.id('filter-palette').getAttribute('data-target');
5872    target = $.id('filter-' + id);
5873    
5874    if (!target) {
5875      return;
5876    }
5877    
5878    target = $.cls('colorbox', target)[0];
5879    
5880    if (clear === true) {
5881      target.setAttribute('data-nocolor', '1');
5882      target.innerHTML = '&#x2215;';
5883      target.style.background = '';
5884    }
5885    else {
5886      target.removeAttribute('data-nocolor');
5887      target.innerHTML = '';
5888      target.style.background = el.style.backgroundColor;
5889    }
5890    
5891    Filter.closePalette();
5892  };
5893  
5894  Filter.setCustomColor = function() {
5895    var input, box;
5896    
5897    input = $.id('palette-custom-input');
5898    box = $.id('palette-custom-ok');
5899    
5900    box.style.backgroundColor = input.value;
5901  };
5902  
5903  /**
5904   * ID colors
5905   */
5906  var IDColor = {
5907    css: 'padding: 0 5px; border-radius: 6px; font-size: 0.8em;',
5908    ids: {}
5909  };
5910  
5911  IDColor.init = function() {
5912    var style;
5913    
5914    if (window.user_ids) {
5915      this.enabled = true;
5916      
5917      style = document.createElement('style');
5918      style.setAttribute('type', 'text/css');
5919      style.textContent = '.posteruid .hand {' + this.css + '}';
5920      document.head.appendChild(style);
5921    }
5922  };
5923  
5924  IDColor.compute = function(str) {
5925    var rgb, hash;
5926    
5927    rgb = [];
5928    hash = $.hash(str);
5929    
5930    rgb[0] = (hash >> 24) & 0xFF;
5931    rgb[1] = (hash >> 16) & 0xFF;
5932    rgb[2] = (hash >> 8) & 0xFF;
5933    rgb[3] = ((rgb[0] * 0.299) + (rgb[1] * 0.587) + (rgb[2] * 0.114)) > 125;
5934    
5935    this.ids[str] = rgb;
5936    
5937    return rgb;
5938  };
5939  
5940  IDColor.apply = function(uid) {
5941    var rgb;
5942    
5943    rgb = IDColor.ids[uid.textContent] || IDColor.compute(uid.textContent);
5944    uid.style.cssText = '\
5945      background-color: rgb(' + rgb[0] + ',' + rgb[1] + ',' + rgb[2] + ');\
5946      color: ' + (rgb[3] ? 'black;' : 'white;');
5947  };
5948  
5949  IDColor.applyRemote = function(uid) {
5950    this.apply(uid);
5951    uid.style.cssText += this.css;
5952  };
5953  
5954  /**
5955   * SWF embed
5956   */
5957  var SWFEmbed = {};
5958  
5959  SWFEmbed.init = function() {
5960    if (Main.tid) {
5961      this.processThread();
5962    }
5963    else {
5964      this.processIndex();
5965    }
5966  };
5967  
5968  SWFEmbed.processThread = function() {
5969    var fileText, el;
5970    
5971    fileText = $.id('fT' + Main.tid);
5972    
5973    if (!fileText) {
5974      return;
5975    }
5976    
5977    el = document.createElement('a');
5978    el.href = 'javascript:;';
5979    el.textContent = 'Embed';
5980    el.addEventListener('click', SWFEmbed.toggleThread, false);
5981    
5982    fileText.appendChild(document.createTextNode('-['));
5983    fileText.appendChild(el);
5984    fileText.appendChild(document.createTextNode(']'));
5985  };
5986  
5987  SWFEmbed.processIndex = function() {
5988    var i, tr, el, cnt, nodes, srcIndex, src;
5989    
5990    srcIndex = 2;
5991    
5992    cnt = $.cls('postblock')[0];
5993    
5994    if (!cnt) {
5995      return;
5996    }
5997    
5998    tr = cnt.parentNode;
5999    
6000    el = document.createElement('td');
6001    el.className = 'postblock';
6002    tr.insertBefore(el, tr.children[srcIndex].nextElementSibling);
6003    
6004    cnt = $.cls('flashListing')[0];
6005    
6006    if (!cnt) {
6007      return;
6008    }
6009    
6010    nodes = $.tag('tr', cnt);
6011    
6012    for (i = 1; tr = nodes[i]; ++i) {
6013      src = tr.children[srcIndex].firstElementChild;
6014      el = document.createElement('td');
6015      el.innerHTML = '[<a href="' + src.href + '">Embed</a>]';
6016      el.firstElementChild.addEventListener('click', SWFEmbed.embedIndex, false);
6017      tr.insertBefore(el, tr.children[srcIndex].nextElementSibling);
6018    };
6019  };
6020  
6021  SWFEmbed.toggleThread = function(e) {
6022    var cnt, link, el, post, maxWidth, ratio, width, height;
6023    
6024    if (cnt = $.id('swf-embed')) {
6025      cnt.parentNode.removeChild(cnt);
6026      e.target.textContent = 'Embed';
6027      return;
6028    }
6029    
6030    link = $.tag('a', e.target.parentNode)[0];
6031    
6032    maxWidth = document.documentElement.clientWidth - 100;
6033    
6034    width = +link.getAttribute('data-width');
6035    height = +link.getAttribute('data-height');
6036    
6037    if (width > maxWidth) {
6038      ratio = width / height;
6039      width = maxWidth;
6040      height = Math.round(maxWidth / ratio);
6041    }
6042    
6043    cnt = document.createElement('div');
6044    cnt.id = 'swf-embed';
6045    
6046    el = document.createElement('embed');
6047    el.setAttribute('allowScriptAccess', 'never');
6048    el.type = 'application/x-shockwave-flash';
6049    el.width = width;
6050    el.height = height;
6051    el.src = link.href;
6052    
6053    cnt.appendChild(el);
6054    
6055    post = $.id('m' + Main.tid);
6056    post.insertBefore(cnt, post.firstChild);
6057    
6058    $.cls('thread')[0].scrollIntoView(true);
6059    
6060    e.target.textContent = 'Remove';
6061  };
6062  
6063  SWFEmbed.embedIndex = function(e) {
6064    var el, cnt, header, icon, backdrop, width, height, cntWidth, cntHeight,
6065      maxWidth, maxHeight, docWidth, docHeight, margins, headerHeight, fileName;
6066    
6067    e.preventDefault();
6068    
6069    margins = 10;
6070    headerHeight = 20;
6071    
6072    el = e.target.parentNode.parentNode.children[2].firstElementChild;
6073    
6074    fileName = el.getAttribute('title') || el.textContent;
6075    
6076    cntWidth = width = +el.getAttribute('data-width');
6077    cntHeight = height = +el.getAttribute('data-height');
6078    
6079    docWidth = document.documentElement.clientWidth;
6080    docHeight = document.documentElement.clientHeight;
6081    
6082    maxWidth = docWidth - margins;
6083    maxHeight = docHeight - margins - headerHeight;
6084    
6085    ratio = width / height;
6086    
6087    if (cntWidth > maxWidth) {
6088      cntWidth = maxWidth;
6089      cntHeight = Math.round(maxWidth / ratio);
6090    }
6091    
6092    if (cntHeight > maxHeight) {
6093      cntHeight = maxHeight;
6094      cntWidth = Math.round(maxHeight * ratio);
6095    }
6096    
6097    el = document.createElement('embed');
6098    el.setAttribute('allowScriptAccess', 'never');
6099    el.src = e.target.href;
6100    el.width = '100%';
6101    el.height = '100%';
6102    
6103    cnt = document.createElement('div');
6104    cnt.style.position = 'fixed';
6105    cnt.style.width = cntWidth + 'px';
6106    cnt.style.height = cntHeight + 'px';
6107    cnt.style.top = '50%';
6108    cnt.style.left = '50%';
6109    cnt.style.marginTop = (-cntHeight / 2 - headerHeight / 2) + 'px';
6110    cnt.style.marginLeft = (-cntWidth / 2) + 'px';
6111    cnt.style.background = 'white';
6112    
6113    header = document.createElement('div');
6114    header.id = 'swf-embed-header';
6115    header.className = 'postblock';
6116    header.textContent = fileName + ', ' + width + 'x' + height;
6117    
6118    icon = document.createElement('img');
6119    icon.id = 'swf-embed-close';
6120    icon.className = 'pointer';
6121    icon.src = Main.icons.cross;
6122    
6123    header.appendChild(icon);
6124    
6125    cnt.appendChild(header);
6126    cnt.appendChild(el);
6127    
6128    backdrop = document.createElement('div');
6129    backdrop.id = 'swf-embed';
6130    backdrop.style.cssText = 'width: 100%; height: 100%; position: fixed;\
6131    top: 0; left: 0; background: rgba(128, 128, 128, 0.5)';
6132    
6133    backdrop.appendChild(cnt);
6134    backdrop.addEventListener('click', SWFEmbed.onBackdropClick, false);
6135    
6136    document.body.appendChild(backdrop);
6137  };
6138  
6139  SWFEmbed.onBackdropClick = function(e) {
6140    var backdrop = $.id('swf-embed');
6141    
6142    if (e.target === backdrop || e.target.id == 'swf-embed-close') {
6143      backdrop.removeEventListener('click', SWFEmbed.onBackdropClick, false);
6144      backdrop.parentNode.removeChild(backdrop);
6145    }
6146  };
6147  
6148  /**
6149   * Media
6150   */
6151  var Media = {};
6152  
6153  Media.init = function() {
6154    this.matchSC = /(?:soundcloud\.com|snd\.sc)\/[^\s<]+(?:<wbr>)?[^\s<]*/g;
6155    this.matchYT = /(?:youtube\.com\/watch\?[^\s]*?v=|youtu\.be\/)[^\s<]+(?:<wbr>)?[^\s<]*(?:<wbr>)?[^\s<]*/g;
6156    this.toggleYT = /(?:v=|\.be\/)([a-zA-Z0-9_-]{11})/;
6157    this.timeYT = /#t=([ms0-9]+)/;
6158    this.matchVocaroo = /vocaroo\.com\/i\/([a-z0-9]{12})/gi;
6159    
6160    this.map = {
6161      yt: this.toggleYouTube,
6162      sc: this.toggleSoundCloud,
6163      vocaroo: this.toggleVocaroo
6164    };
6165  };
6166  
6167  Media.parseSoundCloud = function(msg) {
6168    msg.innerHTML = msg.innerHTML.replace(this.matchSC, this.replaceSoundCloud);
6169  };
6170  
6171  Media.replaceSoundCloud = function(link) {
6172    return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="sc">Embed</a>]';
6173  };
6174  
6175  Media.toggleSoundCloud = function(node) {
6176    var xhr, url;
6177    
6178    if (node.textContent == 'Remove') {
6179      node.parentNode.removeChild(node.nextElementSibling);
6180      node.textContent = 'Embed';
6181    }
6182    else if (node.textContent == 'Embed') {
6183      url = node.previousElementSibling.textContent;
6184      
6185      xhr = new XMLHttpRequest();
6186      xhr.open('GET', '//soundcloud.com/oembed?show_artwork=false&'
6187        + 'maxwidth=500px&show_comments=false&format=json&url='
6188        + 'http://' + url);
6189      xhr.onload = function() {
6190        var el;
6191        
6192        if (this.status == 200 || this.status == 304) {
6193          el = document.createElement('div');
6194          el.className = 'media-embed';
6195          el.innerHTML = JSON.parse(this.responseText).html;
6196          node.parentNode.insertBefore(el, node.nextElementSibling);
6197          node.textContent = 'Remove';
6198        }
6199        else {
6200          node.textContent = 'Error';
6201          console.log('SoundCloud Error (HTTP ' + this.status + ')');
6202        }
6203      };
6204      node.textContent = 'Loading...';
6205      xhr.send(null);
6206    }
6207  };
6208  
6209  Media.parseYouTube = function(msg) {
6210    msg.innerHTML = msg.innerHTML.replace(this.matchYT, this.replaceYouTube);
6211  };
6212  
6213  Media.replaceYouTube = function(link) {
6214    return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="yt">Embed</a>]';
6215  };
6216  
6217  Media.showYTPreview = function(link) {
6218    var cnt, img, vid, aabb, x, y, tw, th, pad;
6219    
6220    tw = 320; th = 180; pad = 5;
6221    
6222    aabb = link.getBoundingClientRect();
6223    
6224    vid = link.previousElementSibling.textContent.match(this.toggleYT)[1];
6225    
6226    if (aabb.right + tw + pad > $.docEl.clientWidth) {
6227      x = aabb.left - tw - pad;
6228    }
6229    else {
6230      x = aabb.right + pad;
6231    }
6232    
6233    y = aabb.top - th / 2 + aabb.height / 2;
6234    
6235    img = document.createElement('img');
6236    img.width = tw;
6237    img.height = th;
6238    img.alt = '';
6239    img.src = '//i1.ytimg.com/vi/' + encodeURIComponent(vid) + '/mqdefault.jpg';
6240    
6241    cnt = document.createElement('div');
6242    cnt.id = 'yt-preview';
6243    cnt.className = 'reply';
6244    cnt.style.left = (x + window.pageXOffset) + 'px';
6245    cnt.style.top = (y + window.pageYOffset) + 'px';
6246    
6247    cnt.appendChild(img);
6248    
6249    document.body.appendChild(cnt);
6250  };
6251  
6252  Media.removeYTPreview = function() {
6253    var el;
6254    
6255    if (el = $.id('yt-preview')) {
6256      document.body.removeChild(el);
6257    }
6258  }
6259  
6260  Media.toggleYouTube = function(node) {
6261    var vid, time, el, url;
6262    
6263    if (node.textContent == 'Remove') {
6264      node.parentNode.removeChild(node.nextElementSibling);
6265      node.textContent = 'Embed';
6266    }
6267    else {
6268      url = node.previousElementSibling.textContent;
6269      vid = url.match(this.toggleYT);
6270      time = url.match(this.timeYT);
6271      
6272      if (vid && (vid = vid[1])) {
6273        vid = encodeURIComponent(vid);
6274        
6275        if (time && (time = time[1])) {
6276          vid += '#t=' + encodeURIComponent(time);
6277        }
6278        
6279        el = document.createElement('div');
6280        el.className = 'media-embed';
6281        el.innerHTML = '<iframe src="//www.youtube.com/embed/'
6282          + vid
6283          + '" width="640" height="360" frameborder="0"></iframe>'
6284        
6285        node.parentNode.insertBefore(el, node.nextElementSibling);
6286        
6287        node.textContent = 'Remove';
6288      }
6289      else {
6290        node.textContent = 'Error';
6291      }
6292    }
6293  };
6294  
6295  Media.parseVocaroo = function(msg) {
6296    msg.innerHTML = msg.innerHTML.replace(this.matchVocaroo, this.replaceVocaroo);
6297  };
6298  
6299  Media.replaceVocaroo = function(link) {
6300    return '<span>' + link + '</span> [<a href="javascript:;" data-cmd="embed" data-type="vocaroo">Embed</a>]';
6301  };
6302  
6303  Media.toggleVocaroo = function(node) {
6304    var vid, time, el, url;
6305    
6306    if (node.textContent == 'Remove') {
6307      node.parentNode.removeChild(node.nextElementSibling);
6308      node.textContent = 'Embed';
6309    }
6310    else {
6311      url = node.previousElementSibling.textContent;
6312      vid = url.match(Media.matchVocaroo);
6313      
6314      if (vid && (vid = vid[0].split('/').pop())) {
6315        vid = encodeURIComponent(vid);
6316        
6317        el = document.createElement('div');
6318        el.className = 'media-embed';
6319        el.innerHTML = '<embed width="220" height="140" class="media-embed" '
6320          + 'src="//vocaroo.com/mediafoo.swf?playMediaID=' + vid + '&autoplay=0">';
6321        
6322        node.parentNode.insertBefore(el, node.nextElementSibling);
6323        
6324        node.textContent = 'Remove';
6325      }
6326      else {
6327        node.textContent = 'Error';
6328      }
6329    }
6330  };
6331  
6332  Media.toggleEmbed = function(node) {
6333    var fn, type = node.getAttribute('data-type');
6334    
6335    if (type && (fn = Media.map[type])) {
6336      fn.call(this, node);
6337    }
6338  };
6339  
6340  /**
6341   * Custom CSS
6342   */
6343  var CustomCSS = {};
6344  
6345  CustomCSS.init = function() {
6346    var style, css;
6347    if (css = localStorage.getItem('4chan-css')) {
6348      style = document.createElement('style');
6349      style.id = 'customCSS';
6350      style.setAttribute('type', 'text/css');
6351      style.textContent = css;
6352      document.head.appendChild(style);
6353    }
6354  };
6355  
6356  CustomCSS.open = function() {
6357    var cnt, ta, data;
6358    
6359    if ($.id('customCSSMenu')) {
6360      return;
6361    }
6362    
6363    cnt = document.createElement('div');
6364    cnt.id = 'customCSSMenu';
6365    cnt.className = 'UIPanel';
6366    cnt.setAttribute('data-cmd', 'css-close');
6367    cnt.innerHTML = '\
6368  <div class="extPanel reply"><div class="panelHeader">Custom CSS\
6369  <span><img alt="Close" title="Close" class="pointer" data-cmd="css-close" src="'
6370  + Main.icons.cross + '"></span></div>\
6371  <textarea id="customCSSBox"></textarea>\
6372  <div class="center"><button data-cmd="css-save">Save CSS</button></div>\
6373  </td></tr></tfoot></table></div>';
6374    
6375    document.body.appendChild(cnt);
6376    
6377    cnt.addEventListener('click', this.onClick, false);
6378    
6379    ta = $.id('customCSSBox');
6380    
6381    if (data = localStorage.getItem('4chan-css')) {
6382      ta.textContent = data;
6383    }
6384    
6385    ta.focus();
6386  };
6387  
6388  CustomCSS.save = function() {
6389    var ta, style;
6390    
6391    if (ta = $.id('customCSSBox')) {
6392      localStorage.setItem('4chan-css', ta.value);
6393      if (Config.customCSS && (style = $.id('customCSS'))) {
6394        document.head.removeChild(style);
6395        CustomCSS.init();
6396      }
6397    }
6398  };
6399  
6400  CustomCSS.close = function() {
6401    var cnt;
6402    
6403    if (cnt = $.id('customCSSMenu')) {
6404      cnt.removeEventListener('click', this.onClick, false);
6405      document.body.removeChild(cnt);
6406    }
6407  };
6408  
6409  CustomCSS.onClick = function(e) {
6410    var cmd;
6411    
6412    if (cmd = e.target.getAttribute('data-cmd')) {
6413      switch (cmd) {
6414        case 'css-close':
6415          CustomCSS.close();
6416          break;
6417        case 'css-save':
6418          CustomCSS.save();
6419          CustomCSS.close();
6420          break;
6421      }
6422    }
6423  };
6424  
6425  /**
6426   * Keyboard shortcuts
6427   */
6428  var Keybinds = {};
6429  
6430  Keybinds.init = function() {
6431    this.map = {
6432      // A
6433      65: function() {
6434        if (ThreadUpdater.enabled) ThreadUpdater.toggleAuto();
6435      },
6436      // F
6437      70: function() {
6438        if (Config.filter) {
6439          Filter.addSelection();
6440        }
6441      },
6442      // Q
6443      81: function() {
6444        if (QR.enabled && Main.tid) {
6445          QR.quotePost(Main.tid);
6446        }
6447      },
6448      // R
6449      82: function() {
6450        if (ThreadUpdater.enabled) ThreadUpdater.forceUpdate();
6451      },
6452      // W
6453      87: function() {
6454        if (Config.threadWatcher && Main.tid) ThreadWatcher.toggle(Main.tid);
6455      },
6456      // B
6457      66: function() {
6458        var el;
6459        (el = $.cls('prev')[0]) && (el = $.tag('form', el)[0]) && el.submit();
6460      },
6461      // C
6462      67: function() {
6463        location.href = '/' + Main.board + '/catalog';
6464      },
6465      // N
6466      78: function() {
6467        var el;
6468        (el = $.cls('next')[0]) && (el = $.tag('form', el)[0]) && el.submit();
6469      },
6470      // I
6471      73: function() {
6472        location.href = '/' + Main.board + '/';
6473      }
6474    };
6475    
6476    document.addEventListener('keydown', this.resolve, false);
6477  };
6478  
6479  Keybinds.resolve = function(e) {
6480    var bind, el = e.target;
6481    
6482    if (el.nodeName == 'TEXTAREA' || el.nodeName == 'INPUT') {
6483      return;
6484    }
6485    
6486    bind = Keybinds.map[e.keyCode];
6487    
6488    if (bind && !e.altKey && !e.shiftKey && !e.ctrlKey && !e.metaKey) {
6489      e.preventDefault();
6490      e.stopPropagation();
6491      bind();
6492    }
6493  };
6494  
6495  Keybinds.open = function() {
6496    var cnt;
6497    
6498    if ($.id('keybindsHelp')) {
6499      return;
6500    }
6501    
6502    cnt = document.createElement('div');
6503    cnt.id = 'keybindsHelp';
6504    cnt.className = 'UIPanel';
6505    cnt.setAttribute('data-cmd', 'keybinds-close');
6506    cnt.innerHTML = '\
6507  <div class="extPanel reply"><div class="panelHeader">Keyboard Shortcuts\
6508  <span><img data-cmd="keybinds-close" class="pointer" alt="Close" title="Close" src="'
6509  + Main.icons.cross + '"></span></div>\
6510  <ul>\
6511  <li><strong>Global</strong></li>\
6512  <li><kbd>A</kbd> &mdash; Toggle auto-updater</li>\
6513  <li><kbd>Q</kbd> &mdash; Open Quick Reply</li>\
6514  <li><kbd>R</kbd> &mdash; Update thread</li>\
6515  <li><kbd>W</kbd> &mdash; Watch/Unwatch thread</li>\
6516  <li><kbd>B</kbd> &mdash; Previous page</li>\
6517  <li><kbd>N</kbd> &mdash; Next page</li>\
6518  <li><kbd>I</kbd> &mdash; Return to index</li>\
6519  <li><kbd>C</kbd> &mdash; Open catalog</li>\
6520  <li><kbd>F</kbd> &mdash; Filter selected text</li>\
6521  </ul><ul>\
6522  <li><strong>Quick Reply (always enabled)</strong></li>\
6523  <li><kbd>Ctrl + Click</kbd> the post number &mdash; Quote without linking</li>\
6524  <li><kbd>Ctrl + S</kbd> &mdash; Spoiler tags</li>\
6525  <li><kbd>Esc</kbd> &mdash; Close the Quick Reply</li>\
6526  </ul>';
6527  
6528    document.body.appendChild(cnt);
6529    cnt.addEventListener('click', this.onClick, false);
6530  };
6531  
6532  Keybinds.close = function() {
6533    var cnt;
6534    
6535    if (cnt = $.id('keybindsHelp')) {
6536      cnt.removeEventListener('click', this.onClick, false);
6537      document.body.removeChild(cnt);
6538    }
6539  };
6540  
6541  Keybinds.onClick = function(e) {
6542    var cmd;
6543    
6544    if ((cmd = e.target.getAttribute('data-cmd')) && cmd == 'keybinds-close') {
6545      Keybinds.close();
6546    }
6547  };
6548  
6549  /**
6550   * Reporting
6551   */
6552  var Report = {
6553    init: function() {
6554      window.addEventListener('message', Report.onMessage, false);
6555    }
6556  };
6557  
6558  Report.onMessage = function(e) {
6559    var id;
6560    
6561    if (e.origin === 'https://sys.4chan.org' && /^done-report/.test(e.data)) {
6562      id = e.data.split('-')[2];
6563      
6564      if (Config.threadHiding && $.id('t' + id)) {
6565        if (!ThreadHiding.isHidden(id)) {
6566          ThreadHiding.hide(id);
6567          ThreadHiding.save();
6568        }
6569        
6570        return;
6571      }
6572      
6573      if ($.id('p' + id)) {
6574        if (!ReplyHiding.isHidden(id)) {
6575          ReplyHiding.hide(id);
6576          ReplyHiding.save();
6577        }
6578        
6579        return;
6580      }
6581    }
6582  };
6583  
6584  Report.open = function(pid, board) {
6585    window.open('https://sys.4chan.org/'
6586      + (board || Main.board) + '/imgboard.php?mode=report&no=' + pid
6587      , Date.now(),
6588      "toolbar=0,scrollbars=0,location=0,status=1,menubar=0,resizable=1,width=600,height=170");
6589  };
6590  
6591  /**
6592   * Custom Menu
6593   */
6594  var CustomMenu = {};
6595  
6596  CustomMenu.reset = function() {
6597    var i, el, full, custom, navs;
6598    
6599    full = $.cls('boardList');
6600    custom = $.cls('customBoardList');
6601    navs = $.cls('show-all-boards');
6602    
6603    for (i = 0; el = navs[i]; ++i) {
6604      el.removeEventListener('click', CustomMenu.reset, false);
6605    }
6606    
6607    for (i = custom.length - 1; el = custom[i]; i--) {
6608      full[i].style.display = null;
6609      el.parentNode.removeChild(el);
6610    }
6611  };
6612  
6613  CustomMenu.apply = function(str) {
6614    var i, j, el, cntBottom, board, navs, boardList, more;
6615    
6616    if (!str) {
6617      return;
6618    }
6619    
6620    boardList = str.split(/[^0-9a-z]/i);
6621    
6622    cnt = document.createElement('span');
6623    cnt.className = 'customBoardList';
6624    
6625    for (i = 0; board = boardList[i]; ++i) {
6626      if (i) {
6627        cnt.appendChild(document.createTextNode(' / '));
6628      }
6629      else {
6630        cnt.appendChild(document.createTextNode('['));
6631      }
6632      el = document.createElement('a');
6633      el.textContent = board;
6634      el.href = '//boards.4chan.org/' + board + '/';
6635      cnt.appendChild(el);
6636    }
6637    
6638    cnt.appendChild(document.createTextNode(']'));
6639    
6640    cnt.appendChild(document.createTextNode(' ['));
6641    el = document.createElement('a');
6642    el.textContent = 'โ€ฆ';
6643    el.title = 'Show all';
6644    el.className = 'show-all-boards pointer';
6645    cnt.appendChild(el);
6646    cnt.appendChild(document.createTextNode('] '));
6647    
6648    cntBottom = cnt.cloneNode(true);
6649    
6650    navs = $.cls('boardList');
6651    
6652    for (i = 0; el = navs[i]; ++i) {
6653      el.style.display = 'none';
6654      el.parentNode.insertBefore(i ? cntBottom : cnt, el);
6655    }
6656    
6657    navs = $.cls('show-all-boards');
6658    
6659    for (i = 0; el = navs[i]; ++i) {
6660      el.addEventListener('click', CustomMenu.reset, false);
6661    }
6662  };
6663  
6664  CustomMenu.onClick = function(e) {
6665    var t;
6666    
6667    if ((t = e.target) == document) {
6668      return;
6669    }
6670  
6671    if (t.hasAttribute('data-close')) {
6672      CustomMenu.closeEditor();
6673    }
6674    else if (t.hasAttribute('data-save')) {
6675      CustomMenu.save();
6676    }
6677  };
6678  
6679  CustomMenu.showEditor = function() {
6680    var cnt;
6681    
6682    cnt = document.createElement('div');
6683    cnt.id = 'customMenu';
6684    cnt.className = 'UIPanel';
6685    cnt.setAttribute('data-close', '1');
6686    cnt.innerHTML = '\
6687  <div class="extPanel reply"><div class="panelHeader">Custom Board List\
6688  <span><img alt="Close" title="Close" class="pointer" data-close="1" src="'
6689  + Main.icons.cross + '"></a></span></div>\
6690  <input placeholder="Example: jp tg mu" id="customMenuBox" type="text" value="">\
6691  <div class="center"><button data-save="1">Save</button></div></div>';
6692  
6693    document.body.appendChild(cnt);
6694    
6695    if (Config.customMenuList) {
6696      $.id('customMenuBox').value = Config.customMenuList;
6697    }
6698    
6699    cnt.addEventListener('click', CustomMenu.onClick, false);
6700  };
6701  
6702  CustomMenu.closeEditor = function() {
6703    var el;
6704    
6705    if (el = $.id('customMenu')) {
6706      el.removeEventListener('click', CustomMenu.onClick, false);
6707      document.body.removeChild(el);
6708    }
6709  };
6710  
6711  CustomMenu.save = function() {
6712    var input;
6713  
6714    if (input = $.id('customMenuBox')) {
6715      Config.customMenuList = input.value;
6716    }
6717    
6718    CustomMenu.closeEditor();
6719  };
6720  
6721  /**
6722   * Draggable helper
6723   */
6724  var Draggable = {
6725    el: null,
6726    key: null,
6727    scrollX: null,
6728    scrollY: null,
6729    dx: null, dy: null, right: null, bottom: null,
6730    
6731    set: function(handle) {
6732      handle.addEventListener('mousedown', Draggable.startDrag, false);
6733    },
6734    
6735    unset: function(handle) {
6736      handle.removeEventListener('mousedown', Draggable.startDrag, false);
6737    },
6738    
6739    startDrag: function(e) {
6740      var self, doc, offs;
6741      
6742      if (this.parentNode.hasAttribute('data-shiftkey') && !e.shiftKey) {
6743        return;
6744      }
6745      
6746      e.preventDefault();
6747      
6748      self = Draggable;
6749      doc = document.documentElement;
6750      
6751      self.el = this.parentNode;
6752      
6753      self.key = self.el.getAttribute('data-trackpos');
6754      offs = self.el.getBoundingClientRect();
6755      self.dx = e.clientX - offs.left;
6756      self.dy = e.clientY - offs.top;
6757      self.right = doc.clientWidth - offs.width;
6758      self.bottom = doc.clientHeight - offs.height;
6759      
6760      if (getComputedStyle(self.el, null).position != 'fixed') {
6761        self.scrollX = window.pageXOffset;
6762        self.scrollY = window.pageYOffset;
6763      }
6764      else {
6765        self.scrollX = self.scrollY = 0;
6766      }
6767      
6768      document.addEventListener('mouseup', self.endDrag, false);
6769      document.addEventListener('mousemove', self.onDrag, false);
6770    },
6771    
6772    endDrag: function(e) {
6773      document.removeEventListener('mouseup', Draggable.endDrag, false);
6774      document.removeEventListener('mousemove', Draggable.onDrag, false);
6775      if (Draggable.key) {
6776        Config[Draggable.key] = Draggable.el.style.cssText;
6777        Config.save();
6778      }
6779      delete Draggable.el;
6780    },
6781    
6782    onDrag: function(e) {
6783      var left, top, style;
6784      
6785      left = e.clientX - Draggable.dx + Draggable.scrollX;
6786      top = e.clientY - Draggable.dy + Draggable.scrollY;
6787      style = Draggable.el.style;
6788      if (left < 1) {
6789        style.left = '0';
6790        style.right = '';
6791      }
6792      else if (Draggable.right < left) {
6793        style.left = '';
6794        style.right = '0';
6795      }
6796      else {
6797        style.left = (left / document.documentElement.clientWidth * 100) + '%';
6798        style.right = '';
6799      }
6800      if (top < 1) {
6801        style.top = '0';
6802        style.bottom = '';
6803      }
6804      else if (Draggable.bottom < top) {
6805        style.bottom = '0';
6806        style.top = '';
6807      }
6808      else {
6809        style.top = (top / document.documentElement.clientHeight * 100) + '%';
6810        style.bottom = '';
6811      }
6812    }
6813  };
6814  
6815  /**
6816   * User Agent
6817   */
6818  var UA = {};
6819  
6820  UA.init = function() {
6821    document.head = document.head || $.tag('head')[0];
6822    
6823    this.isOpera = Object.prototype.toString.call(window.opera) == '[object Opera]';
6824    
6825    this.hasCORS = 'withCredentials' in new XMLHttpRequest;
6826    
6827    this.hasFormData = 'FormData' in window;
6828    
6829    this.hasDragAndDrop = false; /*'draggable' in document.createElement('div');*/
6830  };
6831  
6832  UA.dispatchEvent = function(name, detail) {
6833    var e = document.createEvent('Event');
6834    e.initEvent(name, false, false);
6835    if (detail) {
6836      e.detail = detail;
6837    }
6838    document.dispatchEvent(e);
6839  };
6840  
6841  UA.getSelection = function(raw) {
6842    var sel;
6843    
6844    if (UA.isOpera && typeof (sel = document.getSelection()) == 'string') {}
6845    else {
6846      sel = window.getSelection();
6847      
6848      if (!raw) {
6849        sel = sel.toString();
6850      }
6851    }
6852    
6853    return sel;
6854  };
6855  
6856  /**
6857   * Config
6858   */
6859  var Config = {
6860    quotePreview: true,
6861    backlinks: true,
6862    quickReply: true,
6863    threadUpdater: true,
6864    threadHiding: true,
6865    
6866    alwaysAutoUpdate: false,
6867    topPageNav: false,
6868    threadWatcher: false,
6869    imageExpansion: true,
6870    fitToScreenExpansion: false,
6871    threadExpansion: true,
6872    alwaysDepage: false,
6873    localTime: true,
6874    stickyNav: false,
6875    keyBinds: false,
6876    inlineQuotes: false,
6877  
6878    filter: false,
6879    revealSpoilers: false,
6880    imageHover: false,
6881    threadStats: true,
6882    IDColor: true,
6883    noPictures: false,
6884    embedYouTube: true,
6885    embedSoundCloud: false,
6886    updaterSound: false,
6887  
6888    customCSS: false,
6889    autoScroll: false,
6890    hideStubs: false,
6891    compactThreads: false,
6892    centeredThreads: false,
6893    dropDownNav: false,
6894    classicNav: false,
6895    fixedThreadWatcher: false,
6896    persistentQR: false,
6897    forceHTTPS: false,
6898    reportButton: false,
6899    
6900    disableAll: false
6901  };
6902  
6903  var ConfigMobile = {
6904    embedYouTube: false,
6905    compactThreads: false
6906  };
6907  
6908  Config.load = function() {
6909    if (storage = localStorage.getItem('4chan-settings')) {
6910      storage = JSON.parse(storage);
6911      $.extend(Config, storage);
6912      
6913      if (Main.getCookie('https') === '1') {
6914        Config.forceHTTPS = true;
6915      }
6916      else {
6917        Config.forceHTTPS = false;
6918      }
6919    }
6920    else {
6921      Main.firstRun = true;
6922    }
6923  };
6924  
6925  Config.loadFromURL = function() {
6926    var cmd, data;
6927    
6928    cmd = location.href.split('=', 2);
6929    
6930    if (/#cfg$/.test(cmd[0])) {
6931      try {
6932        data = JSON.parse(decodeURIComponent(cmd[1]));
6933        
6934        history.replaceState(null, '', location.href.split('#', 1)[0]);
6935        
6936        $.extend(Config, JSON.parse(data.settings));
6937        
6938        Config.save();
6939        
6940        if (data.filters) {
6941          localStorage.setItem('4chan-filters', data.filters);
6942        }
6943        
6944        if (data.css) {
6945          localStorage.setItem('4chan-css', data.css);
6946        }
6947        
6948        if (data.catalogFilters) {
6949          localStorage.setItem('catalog-filters', data.catalogFilters);
6950        }
6951        
6952        if (data.catalogSettings) {
6953          localStorage.setItem('catalog-settings', data.catalogSettings);
6954        }
6955        
6956        return true;
6957      }
6958      catch (e) {
6959        console.log(e);
6960      }
6961    }
6962    
6963    return false;
6964  };
6965  
6966  Config.toURL = function() {
6967    var data, cfg = {};
6968    
6969    cfg.settings = localStorage.getItem('4chan-settings');
6970    
6971    if (data = localStorage.getItem('4chan-filters')) {
6972      cfg.filters = data;
6973    }
6974    
6975    if (data = localStorage.getItem('4chan-css')) {
6976      cfg.css = data;
6977    }
6978    
6979    if (data = localStorage.getItem('catalog-filters')) {
6980      cfg.catalogFilters = data;
6981    }
6982    
6983    if (data = localStorage.getItem('catalog-settings')) {
6984      cfg.catalogSettings = data;
6985    }
6986    
6987    return encodeURIComponent(JSON.stringify(cfg));
6988  };
6989  
6990  Config.save = function() {
6991    localStorage.setItem('4chan-settings', JSON.stringify(Config));
6992    
6993    if (Config.forceHTTPS) {
6994      Main.setCookie('https', 1);
6995    }
6996    else {
6997      Main.removeCookie('https');
6998    }
6999  };
7000  
7001  /**
7002   * Settings menu
7003   */
7004  var SettingsMenu = {};
7005  
7006  // [ Name, Subtitle, available on mobile?, is sub-option?, is mobile only? ]
7007  SettingsMenu.options = {
7008    'Quotes &amp; Replying': {
7009      quotePreview: [ 'Quote preview', 'Show post when mousing over post links', true ],
7010      backlinks: [ 'Backlinks', 'Show who has replied to a post', true ],
7011      inlineQuotes: [ 'Inline quote links', 'Clicking quote links will inline expand the quoted post, Shift-click to bypass inlining' ],
7012      quickReply: [ 'Quick Reply', 'Quickly respond to a post by clicking its post number', true ],
7013      persistentQR: [ 'Persistent Quick Reply', 'Keep Quick Reply window open after posting' ]
7014    },
7015    'Monitoring': {
7016      threadUpdater: [ 'Thread updater', 'Append new posts to bottom of thread without refreshing the page', true ],
7017      alwaysAutoUpdate:[ 'Auto-update by default', 'Always auto-update threads', true ],
7018      threadWatcher: [ 'Thread Watcher', 'Keep track of threads you\'re watching and see when they receive new posts', true ],
7019      autoScroll: [ 'Auto-scroll with auto-updated posts', 'Automatically scroll the page as new posts are added' ],
7020      updaterSound: [ 'Sound notification', 'Play a sound when somebody replies to your post(s)' ],
7021      fixedThreadWatcher: [ 'Pin Thread Watcher to the page', 'Thread Watcher will scroll with you' ],
7022      threadStats: [ 'Thread statistics', 'Display post and image counts on the right of the page, <em>italics</em> signify bump/image limit has been met' ],
7023    },
7024    'Filters &amp; Post Hiding': {
7025      filter: [ 'Filter and highlight specific threads/posts [<a href="javascript:;" data-cmd="filters-open">Edit</a>]', 'Enable pattern-based filters' ],
7026      threadHiding: [ 'Thread hiding [<a href="javascript:;" data-cmd="thread-hiding-clear">Clear History</a>]', 'Hide entire threads by clicking the minus button', true ],
7027      hideStubs: [ 'Hide thread stubs', "Don't display stubs of hidden threads" ]
7028    },
7029    'Navigation': {
7030      threadExpansion: [ 'Thread expansion', 'Expand threads inline on board indexes', true ],
7031      dropDownNav: [ 'Use persistent drop-down navigation bar', '' ],
7032      classicNav: [ 'Use traditional board list', '', false, true ],
7033      customMenu: [ 'Custom board list [<a href="javascript:;" data-cmd="custom-menu-edit">Edit</a>]', 'Only show selected boards in top and bottom board lists' ],
7034      alwaysDepage: [ 'Always use infinite scroll', 'Enable infinite scroll by default, so reaching the bottom of the board index will load subsequent pages' ],
7035      topPageNav: [ 'Page navigation at top of page', 'Show the page switcher at the top of the page, hold Shift and drag to move' ],
7036      stickyNav: [ 'Navigation arrows', 'Show top and bottom navigation arrows, hold Shift and drag to move' ],
7037      keyBinds: [ 'Use keyboard shortcuts [<a href="javascript:;" data-cmd="keybinds-open">Show</a>]', 'Enable handy keyboard shortcuts for common actions' ]
7038    },
7039    'Images &amp; Media': {
7040      imageExpansion: [ 'Image expansion', 'Enable inline image expansion, limited to browser width', true ],
7041      fitToScreenExpansion: [ 'Fit expanded images to screen', 'Limit expanded images to both browser width and height' ],
7042      imageHover: [ 'Image hover', 'Mouse over images to view full size, limited to browser size' ],
7043      revealSpoilers: [ "Don't spoiler images", 'Show image thumbnail and original filename instead of spoiler placeholders' ],
7044      noPictures: [ 'Hide thumbnails', 'Don\'t display thumbnails while browsing', true ],
7045      embedYouTube: [ 'Embed YouTube links', 'Embed YouTube player into replies' ],
7046      embedSoundCloud: [ 'Embed SoundCloud links', 'Embed SoundCloud player into replies' ],
7047      embedVocaroo: [ 'Embed Vocaroo links', 'Embed Vocaroo player into replies' ]
7048    },
7049    'Miscellaneous': {
7050      customCSS: [ 'Custom CSS [<a href="javascript:;" data-cmd="css-open">Edit</a>]', 'Include your own CSS rules', true ],
7051      IDColor: [ 'Color user IDs', 'Assign unique colors to user IDs on boards that use them', true ],
7052      compactThreads: [ 'Force long posts to wrap', 'Long posts will wrap at 75% browser width' ],
7053      centeredThreads: [ 'Center threads', 'Align threads to the center of page', false ],
7054      reportButton: [ 'Report button', 'Add a report button next to posts for easy reporting', true, false, true ],
7055      localTime: [ 'Convert dates to local time', 'Convert 4chan server time (US Eastern Time) to your local time', true ],
7056      forceHTTPS: [ 'Always use HTTPS', 'Rewrite 4chan URLs to always use HTTPS', true ]
7057    }
7058  };
7059  
7060  SettingsMenu.save = function() {
7061    var i, options, el, key;
7062    
7063    options = $.id('settingsMenu').getElementsByClassName('menuOption');
7064    
7065    for (i = 0; el = options[i]; ++i) {
7066      key = el.getAttribute('data-option');
7067      Config[key] = el.type == 'checkbox' ? el.checked : el.value;
7068    }
7069    
7070    Config.save();
7071    
7072    SettingsMenu.close();
7073    location.href = location.href.replace(/#.+$/, '');
7074  };
7075  
7076  SettingsMenu.toggle = function() {
7077    if ($.id('settingsMenu')) {
7078      SettingsMenu.close();
7079    }
7080    else {
7081      SettingsMenu.open();
7082    }
7083  };
7084  
7085  SettingsMenu.open = function() {
7086    var i, cat, categories, key, html, cnt, opts, mobileOpts, el;
7087    
7088    if (Main.firstRun) {
7089      if (el = $.id('settingsTip')) {
7090        el.parentNode.removeChild(el);
7091      }
7092      if (el = $.id('settingsTipBottom')) {
7093        el.parentNode.removeChild(el);
7094      }
7095      Config.save();
7096    }
7097    
7098    cnt = document.createElement('div');
7099    cnt.id = 'settingsMenu';
7100    cnt.className = 'UIPanel';
7101    
7102    html = '<div class="extPanel reply"><div class="panelHeader">Settings'
7103      + '<span><img alt="Close" title="Close" class="pointer" data-cmd="settings-toggle" src="'
7104      + Main.icons.cross + '"></a>'
7105      + '</span></div><ul>';
7106    
7107    html += '<ul><li id="settings-exp-all">[<a href="#" data-cmd="settings-exp-all">Expand All Settings</a>]</li></ul>';
7108    
7109    if (Main.hasMobileLayout) {
7110      categories = {};
7111      for (cat in SettingsMenu.options) {
7112        mobileOpts = {};
7113        opts = SettingsMenu.options[cat];
7114        for (key in opts) {
7115          if (opts[key][2]) {
7116            mobileOpts[key] = opts[key];
7117          }
7118        }
7119        for (i in mobileOpts) {
7120          categories[cat] = mobileOpts;
7121          break;
7122        }
7123      }
7124    }
7125    else {
7126      categories = SettingsMenu.options;
7127    }
7128    
7129    for (cat in categories) {
7130      opts = categories[cat];
7131      html += '<ul><li class="settings-cat-lbl">'
7132        + '<img alt="" class="settings-expand" src="' + Main.icons.plus + '">'
7133        + '<span class="settings-expand pointer">'
7134        + cat + '</span></li><ul class="settings-cat">';
7135      for (key in opts) {
7136        // Mobile layout only?
7137        if (opts[key][4] && !Main.hasMobileLayout) {
7138          continue;
7139        }
7140        html += '<li' + (opts[key][3] ? ' class="settings-sub">' : '>')
7141          + '<label><input type="checkbox" class="menuOption" data-option="'
7142          + key + '"' + (Config[key] ? ' checked="checked">' : '>')
7143          + opts[key][0] + '</label>'
7144          + (opts[key][1] !== false ? '</li><li class="settings-tip'
7145          + (opts[key][3] ? ' settings-sub">' : '">') + opts[key][1] : '')
7146          + '</li>';
7147      }
7148      html += '</ul></ul>';
7149    }
7150    
7151    html += '</ul><ul><li class="settings-off">'
7152      + '<label title="Completely disable the native extension (overrides any checked boxes)">'
7153      + '<input type="checkbox" class="menuOption" data-option="disableAll"'
7154      + (Config.disableAll ? ' checked="checked">' : '>')
7155      + 'Disable the native extension</label></li></ul>'
7156      + '<div class="center"><button data-cmd="settings-export">Export Settings</button>'
7157      + '<button data-cmd="settings-save">Save Settings</button></div>';
7158    
7159    cnt.innerHTML = html;
7160    cnt.addEventListener('click', SettingsMenu.onClick, false);
7161    document.body.appendChild(cnt);
7162    
7163    if (Main.firstRun) {
7164      SettingsMenu.expandAll();
7165    }
7166    
7167    (el = $.cls('menuOption', cnt)[0]) && el.focus();
7168  };
7169  
7170  SettingsMenu.showExport = function() {
7171    var cnt, str, el;
7172    
7173    if ($.id('exportSettings')) {
7174      return;
7175    }
7176    
7177    str = location.href.replace(location.hash, '') + '#cfg=' + Config.toURL();
7178    
7179    cnt = document.createElement('div');
7180    cnt.id = 'exportSettings';
7181    cnt.className = 'UIPanel';
7182    cnt.setAttribute('data-cmd', 'export-close');
7183    cnt.innerHTML = '\
7184  <div class="extPanel reply"><div class="panelHeader">Export Settings\
7185  <span><img data-cmd="export-close" class="pointer" alt="Close" title="Close" src="'
7186  + Main.icons.cross + '"></span></div>\
7187  <p class="center">Copy and save the URL below, and visit it from another \
7188  browser or computer to restore your extension and catalog settings.</p>\
7189  <p class="center">\
7190  <input class="export-field" type="text" readonly="readonly" value="' + str + '"></p>\
7191  <p style="margin-top:15px" class="center">Alternatively, you can drag the link below into your \
7192  bookmarks bar and click it to restore.</p>\
7193  <p class="center">[<a target="_blank" href="'
7194  + str + '">Restore 4chan Settings</a>]</p>';
7195  
7196    document.body.appendChild(cnt);
7197    cnt.addEventListener('click', this.onExportClick, false);
7198    el = $.cls('export-field', cnt)[0];
7199    el.focus();
7200    el.select();
7201  };
7202  
7203  SettingsMenu.closeExport = function() {
7204    var cnt;
7205    
7206    if (cnt = $.id('exportSettings')) {
7207      cnt.removeEventListener('click', this.onExportClick, false);
7208      document.body.removeChild(cnt);
7209    }
7210  };
7211  
7212  SettingsMenu.onExportClick = function(e) {
7213    var el;
7214    
7215    if (e.target.id == 'exportSettings') {
7216      e.preventDefault();
7217      e.stopPropagation();
7218      SettingsMenu.closeExport();
7219    }
7220  };
7221  
7222  SettingsMenu.expandAll = function() {
7223    var i, el, nodes = $.cls('settings-expand');
7224    
7225    for (i = 0; el = nodes[i]; ++i) {
7226      el.src = Main.icons.minus;
7227      el.parentNode.nextElementSibling.style.display = 'block';
7228    }
7229  };
7230  
7231  SettingsMenu.toggleCat = function(t) {
7232    var icon, disp, el = t.parentNode.nextElementSibling;
7233    
7234    if (!el.style.display) {
7235      disp = 'block';
7236      icon = 'minus';
7237    }
7238    else {
7239      disp = '';
7240      icon = 'plus';
7241    }
7242    
7243    el.style.display = disp;
7244    t.parentNode.firstElementChild.src = Main.icons[icon];
7245  };
7246  
7247  SettingsMenu.onClick = function(e) {
7248    var el, t, i, j;
7249    
7250    t = e.target;
7251    
7252    if ($.hasClass(t, 'settings-expand')) {
7253      SettingsMenu.toggleCat(t);
7254    }
7255    else if (t.getAttribute('data-cmd') == 'settings-exp-all') {
7256      e.preventDefault();
7257      SettingsMenu.expandAll();
7258    }
7259    else if (t.id == 'settingsMenu' && (el = $.id('settingsMenu'))) {
7260      e.preventDefault();
7261      SettingsMenu.close(el);
7262    }
7263  };
7264  
7265  SettingsMenu.close = function(el) {
7266    if (el = (el || $.id('settingsMenu'))) {
7267      el.removeEventListener('click', SettingsMenu.onClick, false);
7268      document.body.removeChild(el);
7269    }
7270  };
7271  
7272  /**
7273   * Main
7274   */
7275  var Main = {};
7276  
7277  Main.addTooltip = function(link, message, id) {
7278    var el, pos;
7279    
7280    el = document.createElement('div');
7281    el.className = 'click-me';
7282    if (id) {
7283      el.id = id;
7284    }
7285    el.innerHTML = message || 'Change your settings';
7286    link.parentNode.appendChild(el);
7287    
7288    pos = (link.offsetWidth - el.offsetWidth + link.offsetLeft - el.offsetLeft) / 2;
7289    el.style.marginLeft = pos + 'px';
7290    
7291    return el;
7292  };
7293  
7294  Main.init = function() {
7295    var params;
7296    
7297    document.addEventListener('DOMContentLoaded', Main.run, false);
7298    
7299    Main.now = Date.now();
7300    
7301    UA.init();
7302    
7303    Config.load();
7304    
7305    if (Config.forceHTTPS && location.protocol != 'https:') {
7306      location.href = location.href.replace(/^http:/, 'https:');
7307      return;
7308    }
7309    
7310    if (Main.firstRun && Config.loadFromURL()) {
7311      Main.firstRun = false;
7312    }
7313    
7314    if (Main.stylesheet = Main.getCookie(style_group)) {
7315      Main.stylesheet = Main.stylesheet.toLowerCase().replace(/ /g, '_');
7316    }
7317    else {
7318      Main.stylesheet =
7319        style_group == 'nws_style' ? 'yotsuba_new' : 'yotsuba_b_new';
7320    }
7321    
7322    Main.passEnabled = Main.getCookie('pass_enabled');
7323    QR.noCaptcha = QR.noCaptcha || Main.passEnabled;
7324    
7325    Main.initIcons();
7326    
7327    Main.addCSS();
7328    
7329    Main.type = style_group.split('_')[0];
7330    
7331    params = location.pathname.split(/\//);
7332    Main.board = params[1];
7333    Main.page = params[2];
7334    Main.tid = params[3];
7335    
7336    Report.init();
7337    
7338    if (Config.IDColor) {
7339      IDColor.init();
7340    }
7341    
7342    if (Config.customCSS) {
7343      CustomCSS.init();
7344    }
7345    
7346    if (Config.keyBinds) {
7347      Keybinds.init();
7348    }
7349    
7350    UA.dispatchEvent('4chanMainInit');
7351  };
7352  
7353  Main.initPersistentNav = function() {
7354    var el, top, bottom;
7355    
7356    top = $.id('boardNavDesktop');
7357    bottom = $.id('boardNavDesktopFoot');
7358    
7359    if (Config.classicNav) {
7360      el = document.createElement('div');
7361      el.className = 'pageJump';
7362      el.innerHTML = '<a href="#bottom">&#9660;</a>'
7363        + '<a href="javascript:void(0);" id="settingsWindowLinkClassic">Settings</a>'
7364        + '<a href="//www.4chan.org" target="_top">Home</a></div>';
7365      
7366      top.appendChild(el);
7367      
7368      $.id('settingsWindowLinkClassic')
7369        .addEventListener('click', SettingsMenu.toggle, false);
7370      
7371      $.addClass(top, 'persistentNav');
7372    }
7373    else {
7374      top.style.display = 'none';
7375      $.removeClass($.id('boardNavMobile'), 'mobile');
7376    }
7377    
7378    bottom.style.display = 'none';
7379    
7380    $.addClass(document.body, 'hasDropDownNav');
7381  };
7382  
7383  Main.checkMobileLayout = function() {
7384    var mobile, desktop;
7385    
7386    if (window.matchMedia) {
7387      return window.matchMedia('(max-width: 480px)').matches
7388        && localStorage.getItem('4chan_never_show_mobile') != 'true';
7389    }
7390    
7391    mobile = $.id('boardNavMobile');
7392    desktop = $.id('boardNavDesktop');
7393    
7394    return mobile && desktop && mobile.offsetWidth > 0 && desktop.offsetWidth == 0;
7395  };
7396  
7397  Main.run = function() {
7398    var thread;
7399    
7400    document.removeEventListener('DOMContentLoaded', Main.run, false);
7401    
7402    document.addEventListener('click', Main.onclick, false);
7403    
7404    $.id('settingsWindowLink').addEventListener('click', SettingsMenu.toggle, false);
7405    $.id('settingsWindowLinkBot').addEventListener('click', SettingsMenu.toggle, false);
7406    $.id('settingsWindowLinkMobile').addEventListener('click', SettingsMenu.toggle, false);
7407    
7408    if (Config.disableAll) {
7409      return;
7410    }
7411    
7412    Main.hasMobileLayout = Main.checkMobileLayout();
7413    Main.isMobileDevice = /Mobile|Android|Dolfin|Opera Mobi|PlayStation Vita|Nintendo DS/.test(navigator.userAgent);
7414    
7415    if (Main.hasMobileLayout) {
7416      $.extend(Config, ConfigMobile);
7417    }
7418    else {
7419      $.id('bottomReportBtn').style.display = 'none';
7420      
7421      if (Main.isMobileDevice) {
7422        $.addClass(document.body, 'isMobileDevice');
7423      }
7424    }
7425    
7426    if (Main.firstRun && Main.isMobileDevice) {
7427      Config.topPageNav = false;
7428      Config.dropDownNav = true;
7429    }
7430    
7431    if (Config.dropDownNav && !Main.hasMobileLayout) {
7432      Main.initPersistentNav();
7433    }
7434    
7435    $.addClass(document.body, Main.stylesheet);
7436    $.addClass(document.body, Main.type);
7437    
7438    if (Config.compactThreads) {
7439      $.addClass(document.body, 'compact');
7440    }
7441    else if (Config.centeredThreads) {
7442      $.addClass(document.body, 'centeredThreads');
7443    }
7444    
7445    if (Config.noPictures) {
7446      $.addClass(document.body, 'noPictures');
7447    }
7448    
7449    if (Config.customMenu) {
7450      CustomMenu.apply(Config.customMenuList);
7451    }
7452    
7453    if (Config.quotePreview || Config.imageHover|| Config.filter) {
7454      thread = $.id('delform');
7455      thread.addEventListener('mouseover', Main.onThreadMouseOver, false);
7456      thread.addEventListener('mouseout', Main.onThreadMouseOut, false);
7457    }
7458    
7459    if (!Main.hasMobileLayout) {
7460      Main.initGlobalMessage();
7461    }
7462    
7463    if (Config.stickyNav) {
7464      Main.setStickyNav();
7465    }
7466    
7467    if (Config.threadExpansion) {
7468      ThreadExpansion.init();
7469    }
7470    
7471    if (Config.threadWatcher) {
7472      ThreadWatcher.init();
7473    }
7474    
7475    if (Config.filter) {
7476      Filter.init();
7477    }
7478    
7479    if (Config.embedSoundCloud || Config.embedYouTube || Config.embedVocaroo) {
7480      Media.init();
7481    }
7482    
7483    ReplyHiding.init();
7484    
7485    if (Config.quotePreview) {
7486      QuotePreview.init();
7487    }
7488    
7489    Parser.init();
7490    
7491    if (Main.tid) {
7492      Main.threadClosed = !document.forms.post;
7493      Main.threadSticky = !!$.cls('stickyIcon', $.id('pi' + Main.tid))[0];
7494      
7495      if (Config.threadStats) {
7496        ThreadStats.init();
7497      }
7498      
7499      Parser.parseThread(Main.tid);
7500      
7501      if (Config.threadUpdater) {
7502        ThreadUpdater.init();
7503      }
7504    }
7505    else {
7506      if (!Main.page) {
7507        Depager.init();
7508      }
7509      
7510      if (Config.topPageNav) {
7511        Main.setPageNav();
7512      }
7513      if (Config.threadHiding) {
7514        ThreadHiding.init();
7515        Parser.parseBoard();
7516      }
7517      else {
7518        Parser.parseBoard();
7519      }
7520    }
7521    
7522    if (Main.board === 'f') {
7523      SWFEmbed.init();
7524    }
7525    
7526    if (Config.quickReply) {
7527      QR.init();
7528    }
7529    
7530    ReplyHiding.purge();
7531  };
7532  
7533  Main.isThreadClosed = function(tid) {
7534    return window.thread_archived || ((el = $.id('pi' + tid)) && $.cls('closedIcon', el)[0])
7535  };
7536  
7537  Main.setThreadState = function(state, mode) {
7538    var cnt, el, ref, cap;
7539    
7540    cap = state.charAt(0).toUpperCase() + state.slice(1);
7541    
7542    if (mode) {
7543      cnt = $.cls('postNum', $.id('pi' + Main.tid))[0];
7544      el = document.createElement('img');
7545      el.className = state + 'Icon retina';
7546      el.title = cap;
7547      el.src = Main.icons2[state];
7548      if (state == 'sticky' && (ref = $.cls('closedIcon', cnt)[0])) {
7549        cnt.insertBefore(el, ref);
7550        cnt.insertBefore(document.createTextNode(' '), ref);
7551      }
7552      else {
7553        cnt.appendChild(document.createTextNode(' '));
7554        cnt.appendChild(el);
7555      }
7556    }
7557    else {
7558      if (el = $.cls(state + 'Icon', $.id('pi' + Main.tid))[0]) {
7559        el.parentNode.removeChild(el.previousSibling);
7560        el.parentNode.removeChild(el);
7561      }
7562    }
7563    
7564    Main['thread' + cap] = mode;
7565  };
7566  
7567  Main.icons = {
7568    up: 'arrow_up.png',
7569    down: 'arrow_down.png',
7570    right: 'arrow_right.png',
7571    download: 'arrow_down2.png',
7572    refresh: 'refresh.png',
7573    cross: 'cross.png',
7574    gis: 'gis.png',
7575    iqdb: 'iqdb.png',
7576    minus: 'post_expand_minus.png',
7577    plus: 'post_expand_plus.png',
7578    rotate: 'post_expand_rotate.gif',
7579    quote: 'quote.png',
7580    report: 'report.png',
7581    notwatched: 'watch_thread_off.png',
7582    watched: 'watch_thread_on.png',
7583    help: 'question.png'
7584  };
7585  
7586  Main.icons2 = {
7587    archived: 'archived.gif',
7588    closed: 'closed.gif',
7589    sticky: 'sticky.gif',
7590    trash: 'trash.gif'
7591  },
7592  
7593  Main.initIcons = function() {
7594    var key, paths, url;
7595    
7596    paths = {
7597      yotsuba_new: 'futaba/',
7598      futaba_new: 'futaba/',
7599      yotsuba_b_new: 'burichan/',
7600      burichan_new: 'burichan/',
7601      tomorrow: 'tomorrow/',
7602      photon: 'photon/'
7603    };
7604    
7605    url = '//s.4cdn.org/image/'
7606    
7607    if (window.devicePixelRatio >= 2) {
7608      for (key in Main.icons) {
7609        Main.icons[key] = Main.icons[key].replace('.', '@2x.');
7610      }
7611      for (key in Main.icons2) {
7612        Main.icons2[key] = Main.icons2[key].replace('.', '@2x.');
7613      }
7614    }
7615    
7616    for (key in Main.icons2) {
7617      Main.icons2[key] = url + Main.icons2[key];
7618    }
7619    
7620    url += 'buttons/' + paths[Main.stylesheet];
7621    for (key in Main.icons) {
7622      Main.icons[key] = url + Main.icons[key];
7623    }
7624  };
7625  
7626  Main.setPageNav = function() {
7627    var el, cnt;
7628    
7629    cnt = document.createElement('div');
7630    cnt.setAttribute('data-shiftkey', '1');
7631    cnt.setAttribute('data-trackpos', 'TN-position');
7632    cnt.className = 'topPageNav';
7633    
7634    if (Config['TN-position']) {
7635      cnt.style.cssText = Config['TN-position'];
7636    }
7637    else {
7638      cnt.style.left = '10px';
7639      cnt.style.top = '50px';
7640    }
7641    
7642    el = $.cls('pagelist')[0]
7643    
7644    if (!el) {
7645      return;
7646    }
7647    
7648    el = el.cloneNode(true);
7649    cnt.appendChild(el);
7650    Draggable.set(el);
7651    document.body.appendChild(cnt);
7652  };
7653  
7654  Main.initGlobalMessage = function() {
7655    var msg, btn, thisTs, oldTs;
7656    
7657    if ((msg = $.id('globalMessage')) && msg.textContent) {
7658      msg.nextElementSibling.style.clear = 'both';
7659      
7660      btn = document.createElement('img');
7661      btn.id = 'toggleMsgBtn';
7662      btn.className = 'extButton';
7663      btn.setAttribute('data-cmd', 'toggleMsg');
7664      btn.alt = 'Toggle';
7665      btn.title = 'Toggle announcement';
7666      
7667      oldTs = localStorage.getItem('4chan-global-msg');
7668      thisTs = msg.getAttribute('data-utc');
7669      
7670      if (oldTs && thisTs <= oldTs) {
7671        msg.style.display = 'none';
7672        btn.style.opacity = '0.5';
7673        btn.src = Main.icons.plus;
7674      }
7675      else {
7676        btn.src = Main.icons.minus;
7677      }
7678      
7679      msg.parentNode.insertBefore(btn, msg);
7680    }
7681  };
7682  
7683  Main.toggleGlobalMessage = function() {
7684    var msg, btn;
7685    
7686    msg = $.id('globalMessage');
7687    btn = $.id('toggleMsgBtn');
7688    if (msg.style.display == 'none') {
7689      msg.style.display = '';
7690      btn.src = Main.icons.minus;
7691      btn.style.opacity = '1';
7692      localStorage.removeItem('4chan-global-msg');
7693    }
7694    else {
7695      msg.style.display = 'none';
7696      btn.src = Main.icons.plus;
7697      btn.style.opacity = '0.5';
7698      localStorage.setItem('4chan-global-msg', msg.getAttribute('data-utc'));
7699    }
7700  };
7701  
7702  Main.setStickyNav = function() {
7703    var cnt, hdr;
7704    
7705    cnt = document.createElement('div');
7706    cnt.id = 'stickyNav';
7707    cnt.className = 'extPanel reply';
7708    cnt.setAttribute('data-shiftkey', '1');
7709    cnt.setAttribute('data-trackpos', 'SN-position');
7710    
7711    if (Config['SN-position']) {
7712      cnt.style.cssText = Config['SN-position'];
7713    }
7714    else {
7715      cnt.style.right = '10px';
7716      cnt.style.top = '50px';
7717    }
7718    
7719    hdr = document.createElement('div');
7720    hdr.innerHTML = '<img class="pointer" src="'
7721      +  Main.icons.up + '" data-cmd="totop" alt="โ–ฒ" title="Top">'
7722      + '<img class="pointer" src="' +  Main.icons.down
7723      + '" data-cmd="tobottom" alt="โ–ผ" title="Bottom">';
7724    Draggable.set(hdr);
7725    
7726    cnt.appendChild(hdr);
7727    document.body.appendChild(cnt);
7728  };
7729  
7730  Main.getCookie = function(name) {
7731    var i, c, ca, key;
7732    
7733    key = name + "=";
7734    ca = document.cookie.split(';');
7735    
7736    for (i = 0; c = ca[i]; ++i) {
7737      while (c.charAt(0) == ' ') {
7738        c = c.substring(1, c.length);
7739      }
7740      if (c.indexOf(key) == 0) {
7741        return decodeURIComponent(c.substring(key.length, c.length));
7742      }
7743    }
7744    return null;
7745  };
7746  
7747  Main.setCookie = function(name, value) {
7748    var date = new Date();
7749    
7750    date.setTime(date.getTime() + (365 * 24 * 60 * 60 * 1000));
7751    
7752    document.cookie = name + '=' + value
7753      + '; expires=' + date.toGMTString()
7754      + '; path=/; domain=boards.4chan.org';
7755  };
7756  
7757  Main.removeCookie = function(name) {
7758    document.cookie = name + '='
7759      + '; expires=Thu, 01 Jan 1970 00:00:01 GMT;'
7760      + '; path=/; domain=boards.4chan.org';
7761  };
7762  
7763  Main.onclick = function(e) {
7764    var t, cmd, tid;
7765    
7766    if ((t = e.target) == document) {
7767      return;
7768    }
7769    
7770    if (cmd = t.getAttribute('data-cmd')) {
7771      id = t.getAttribute('data-id');
7772      switch (cmd) {
7773        case 'update':
7774          e.preventDefault();
7775          ThreadUpdater.forceUpdate();
7776          break;
7777        case 'post-menu':
7778          e.preventDefault();
7779          PostMenu.open(t);
7780          break;
7781        case 'auto':
7782          ThreadUpdater.toggleAuto();
7783          break;
7784        case 'totop':
7785        case 'tobottom':
7786          if (!e.shiftKey) {
7787            location.href = '#' + cmd.slice(2);
7788          }
7789          break;
7790        case 'hide':
7791          ThreadHiding.toggle(id);
7792          break;
7793        case 'watch':
7794          ThreadWatcher.toggle(id);
7795          break;
7796        case 'hide-r':
7797          ReplyHiding.toggle(id);
7798          break;
7799        case 'expand':
7800          ThreadExpansion.toggle(id);
7801          break;
7802        case 'open-qr':
7803          e.preventDefault();
7804          QR.show(Main.tid);
7805          $.tag('textarea', document.forms.qrPost)[0].focus();
7806          break;
7807        case 'depage':
7808          e.preventDefault();
7809          Depager.toggle();
7810          break;
7811        case 'report':
7812          Report.open(id, t.getAttribute('data-board'));
7813          break;
7814        case 'filter-sel':
7815          e.preventDefault();
7816          Filter.addSelection();
7817          break;
7818        case 'embed':
7819          Media.toggleEmbed(t);
7820          break
7821        case 'sound':
7822          ThreadUpdater.toggleSound();
7823          break;
7824        case 'toggleMsg':
7825          Main.toggleGlobalMessage();
7826          break;
7827        case 'settings-toggle':
7828          SettingsMenu.toggle();
7829          break;
7830        case 'settings-save':
7831          SettingsMenu.save();
7832          break;
7833        case 'keybinds-open':
7834          Keybinds.open();
7835          break;
7836        case 'filters-open':
7837          Filter.open();
7838          break;
7839        case 'thread-hiding-clear':
7840          ThreadHiding.clear();
7841          break;
7842        case 'css-open':
7843          CustomCSS.open();
7844          break;
7845        case 'settings-export':
7846          SettingsMenu.showExport();
7847          break;
7848        case 'export-close':
7849          SettingsMenu.closeExport();
7850          break;
7851        case 'custom-menu-edit':
7852          CustomMenu.showEditor();
7853          break;
7854      }
7855    }
7856    else if (!Config.disableAll) {
7857      if (QR.enabled && t.title == 'Reply to this post') {
7858        e.preventDefault();
7859        tid = Main.tid || t.previousElementSibling.getAttribute('href').split('#')[0].split('/')[1];
7860        QR.quotePost(tid, !e.ctrlKey && t.textContent);
7861      }
7862      else if (Config.imageExpansion && e.which == 1 && t.parentNode
7863        && $.hasClass(t.parentNode, 'fileThumb')
7864        && t.parentNode.nodeName == 'A'
7865        && !$.hasClass(t.parentNode, 'deleted')) {
7866        
7867        if (ImageExpansion.toggle(t)) {
7868          e.preventDefault();
7869        }
7870      }
7871      else if (Config.inlineQuotes && e.which == 1 && $.hasClass(t, 'quotelink')) {
7872        if (!e.shiftKey) {
7873          QuoteInline.toggle(t, e);
7874        }
7875        else {
7876          e.preventDefault();
7877          window.location = t.href;
7878        }
7879      }
7880      else if (Config.threadExpansion && t.parentNode && $.hasClass(t.parentNode, 'abbr')) {
7881        e.preventDefault();
7882        ThreadExpansion.expandComment(t);
7883      }
7884      else if (Main.isMobileDevice && Config.quotePreview) {
7885        if ($.hasClass(t, 'quotelink')
7886          && (cmd = t.getAttribute('href').match(QuotePreview.regex))
7887          && cmd[1] != 'rs') {
7888          e.preventDefault();
7889        }
7890      }
7891    }
7892  };
7893  
7894  Main.onThreadMouseOver = function(e) {
7895    var t = e.target;
7896    
7897    if (Config.quotePreview
7898      && $.hasClass(t, 'quotelink')
7899      && !$.hasClass(t, 'deadlink')
7900      && !$.hasClass(t, 'linkfade')) {
7901      QuotePreview.resolve(e.target);
7902    }
7903    else if (Config.imageHover && t.hasAttribute('data-md5')
7904      && !$.hasClass(t.parentNode, 'deleted')) {
7905      ImageHover.show(t);
7906    }
7907    else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) {
7908      Media.showYTPreview(t);
7909    }
7910    else if (Config.filter && t.hasAttribute('data-filtered')) {
7911      QuotePreview.show(t,
7912        t.href ? t.parentNode.parentNode.parentNode : t.parentNode.parentNode);
7913    }
7914  };
7915  
7916  Main.onThreadMouseOut = function(e) {
7917    var t = e.target;
7918    
7919    if (Config.quotePreview && $.hasClass(t, 'quotelink')) {
7920      QuotePreview.remove(t);
7921    }
7922    else if (Config.imageHover && t.hasAttribute('data-md5')) {
7923      ImageHover.hide();
7924    }
7925    else if (Config.embedYouTube && t.getAttribute('data-type') === 'yt' && !Main.hasMobileLayout) {
7926      Media.removeYTPreview();
7927    }
7928    else if (Config.filter && t.hasAttribute('data-filtered')) {
7929      QuotePreview.remove(t);
7930    }
7931  };
7932  
7933  Main.linkToThread = function(tid, board, post) {
7934    return '//' + location.host + '/'
7935      + (board || Main.board) + '/thread/'
7936      + tid + (post > 0 ? ('#p' + post) : '');
7937  };
7938  
7939  Main.addCSS = function() {
7940    var style, css = '\
7941  body.hasDropDownNav {\
7942    margin-top: 45px;\
7943  }\
7944  .extButton.threadHideButton {\
7945    float: left;\
7946    margin-right: 5px;\
7947    margin-top: -1px;\
7948  }\
7949  .extButton.replyHideButton {\
7950    margin-top: 1px;\
7951  }\
7952  div.op > span .postHideButtonCollapsed {\
7953    margin-right: 1px;\
7954  }\
7955  .dropDownNav #boardNavMobile, {\
7956    display: block !important;\
7957  }\
7958  .extPanel {\
7959    border: 1px solid rgba(0, 0, 0, 0.20);\
7960  }\
7961  .tomorrow .extPanel {\
7962    border: 1px solid #111;\
7963  }\
7964  .extButton,\
7965  img.pointer {\
7966    width: 18px;\
7967    height: 18px;\
7968  }\
7969  .extControls {\
7970    display: inline;\
7971    margin-left: 5px;\
7972  }\
7973  .extButton {\
7974    cursor: pointer;\
7975    margin-bottom: -4px;\
7976  }\
7977  .trashIcon {\
7978    width: 16px;\
7979    height: 16px;\
7980    margin-bottom: -2px;\
7981    margin-left: 5px;\
7982  }\
7983  .threadUpdateStatus {\
7984    margin-left: 0.5ex;\
7985  }\
7986  .futaba_new .stub,\
7987  .burichan_new .stub {\
7988    line-height: 1;\
7989    padding-bottom: 1px;\
7990  }\
7991  .stub .extControls,\
7992  .stub .wbtn,\
7993  .stub input {\
7994    display: none;\
7995  }\
7996  .stub .threadHideButton {\
7997    float: none;\
7998    margin-right: 2px;\
7999  }\
8000  div.post div.postInfo {\
8001    width: auto;\
8002    display: inline;\
8003  }\
8004  .right {\
8005    float: right;\
8006  }\
8007  .center {\
8008    display: block;\
8009    margin: auto;\
8010  }\
8011  .pointer {\
8012    cursor: pointer;\
8013  }\
8014  .drag {\
8015    cursor: move !important;\
8016    user-select: none !important;\
8017    -moz-user-select: none !important;\
8018    -webkit-user-select: none !important;\
8019  }\
8020  #quickReport,\
8021  #quickReply {\
8022    display: block;\
8023    position: fixed;\
8024    padding: 2px;\
8025    font-size: 10pt;\
8026  }\
8027  #qrepHeader,\
8028  #qrHeader {\
8029    text-align: center;\
8030    margin-bottom: 1px;\
8031    padding: 0;\
8032    height: 18px;\
8033    line-height: 18px;\
8034  }\
8035  #qrepClose,\
8036  #qrClose {\
8037    float: right;\
8038  }\
8039  #quickReport iframe {\
8040    overflow: hidden;\
8041  }\
8042  #quickReport {\
8043    height: 190px;\
8044  }\
8045  #qrForm > div {\
8046    clear: both;\
8047  }\
8048  #quickReply input[type="text"],\
8049  #quickReply textarea,\
8050  #quickReply #recaptcha_response_field {\
8051    border: 1px solid #aaa;\
8052    font-family: arial,helvetica,sans-serif;\
8053    font-size: 10pt;\
8054    outline: medium none;\
8055    width: 296px;\
8056    padding: 2px;\
8057    margin: 0 0 1px 0;\
8058  }\
8059  #quickReply textarea {\
8060    min-width: 296px;\
8061    float: left;\
8062  }\
8063  #quickReply input::-moz-placeholder,\
8064  #quickReply textarea::-moz-placeholder {\
8065    color: #aaa !important;\
8066    opacity: 1 !important;\
8067  }\
8068  #quickReply input[type="submit"] {\
8069    width: 83px;\
8070    margin: 0;\
8071    font-size: 10pt;\
8072    float: left;\
8073  }\
8074  #quickReply #qrCapField {\
8075    display: block;\
8076    margin-top: 1px;\
8077  }\
8078  #qrCaptcha {\
8079    width: 300px;\
8080    height: 53px;\
8081    cursor: pointer;\
8082    border: 1px solid #aaa;\
8083    display: block;\
8084  }\
8085  #quickReply input.presubmit {\
8086    margin-right: 1px;\
8087    width: 212px;\
8088    float: left;\
8089  }\
8090  #qrFile {\
8091    width: 215px;\
8092    margin-right: 5px;\
8093  }\
8094  .qrRealFile {\
8095    position: absolute;\
8096    left: 0;\
8097    visibility: hidden;\
8098  }\
8099  .yotsuba_new #qrFile {\
8100    color:black;\
8101  }\
8102  #qrSpoiler {\
8103    display: inline;\
8104  }\
8105  #qrError {\
8106    width: 292px;\
8107    display: none;\
8108    font-family: monospace;\
8109    background-color: #E62020;\
8110    font-size: 12px;\
8111    color: white;\
8112    padding: 3px 5px;\
8113    text-shadow: 0 1px rgba(0, 0, 0, 0.20);\
8114    clear: both;\
8115  }\
8116  #qrError a:hover,\
8117  #qrError a {\
8118    color: white !important;\
8119    text-decoration: underline;\
8120  }\
8121  #twHeader {\
8122    font-weight: bold;\
8123    text-align: center;\
8124    height: 17px;\
8125  }\
8126  .futaba_new #twHeader,\
8127  .burichan_new #twHeader {\
8128    line-height: 1;\
8129  }\
8130  #twPrune {\
8131    margin-left: 3px;\
8132    margin-top: -1px;\
8133  }\
8134  #twClose {\
8135    float: left;\
8136    margin-top: -1px;\
8137  }\
8138  #threadWatcher {\
8139    max-width: 265px;\
8140    display: block;\
8141    position: absolute;\
8142    padding: 3px;\
8143  }\
8144  #watchList {\
8145    margin: 0;\
8146    padding: 0;\
8147    user-select: none;\
8148    -moz-user-select: none;\
8149    -webkit-user-select: none;\
8150  }\
8151  #watchList li:first-child {\
8152    margin-top: 3px;\
8153    padding-top: 2px;\
8154    border-top: 1px solid rgba(0, 0, 0, 0.20);\
8155  }\
8156  .photon #watchList li:first-child {\
8157    border-top: 1px solid #ccc;\
8158  }\
8159  .yotsuba_new #watchList li:first-child {\
8160    border-top: 1px solid #d9bfb7;\
8161  }\
8162  .yotsuba_b_new #watchList li:first-child {\
8163    border-top: 1px solid #b7c5d9;\
8164  }\
8165  .tomorrow #watchList li:first-child {\
8166    border-top: 1px solid #111;\
8167  }\
8168  #watchList a {\
8169    text-decoration: none;\
8170  }\
8171  #watchList li {\
8172    overflow: hidden;\
8173    white-space: nowrap;\
8174    text-overflow: ellipsis;\
8175  }\
8176  div.post div.image-expanded {\
8177    display: table;\
8178  }\
8179  div.op div.file .image-expanded-anti {\
8180    margin-left: -3px;\
8181  }\
8182  #quote-preview {\
8183    display: block;\
8184    position: absolute;\
8185    top: 0;\
8186    padding: 3px 6px 6px 3px;\
8187    margin: 0;\
8188  }\
8189  #quote-preview .dateTime {\
8190    white-space: nowrap;\
8191  }\
8192  .yotsuba_new #quote-preview.highlight,\
8193  .yotsuba_b_new #quote-preview.highlight {\
8194    border-width: 1px 2px 2px 1px !important;\
8195    border-style: solid !important;\
8196  }\
8197  .yotsuba_new #quote-preview.highlight {\
8198    border-color: #D99F91 !important;\
8199  }\
8200  .yotsuba_b_new #quote-preview.highlight {\
8201    border-color: #BA9DBF !important;\
8202  }\
8203  .yotsuba_b_new .highlight-anti,\
8204  .burichan_new .highlight-anti {\
8205    border-width: 1px !important;\
8206    background-color: #bfa6ba !important;\
8207  }\
8208  .yotsuba_new .highlight-anti,\
8209  .futaba_new .highlight-anti {\
8210    background-color: #e8a690 !important;\
8211  }\
8212  .tomorrow .highlight-anti {\
8213    background-color: #111 !important;\
8214    border-color: #111;\
8215  }\
8216  .photon .highlight-anti {\
8217    background-color: #bbb !important;\
8218  }\
8219  .op.inlined {\
8220    display: block;\
8221  }\
8222  #quote-preview .inlined,\
8223  #quote-preview .postMenuBtn,\
8224  #quote-preview .extButton,\
8225  #quote-preview .extControls {\
8226    display: none;\
8227  }\
8228  .hasNewReplies {\
8229    font-weight: bold;\
8230  }\
8231  .archivelink {\
8232    opacity: 0.5;\
8233  }\
8234  .deadlink {\
8235    text-decoration: line-through !important;\
8236  }\
8237  div.backlink {\
8238    font-size: 0.8em !important;\
8239    display: inline;\
8240    padding: 0;\
8241    padding-left: 5px;\
8242  }\
8243  .backlink.mobile {\
8244    padding: 3px 5px;\
8245    display: block;\
8246    clear: both;\
8247    line-height: 2;\
8248  }\
8249  .op .backlink.mobile,\
8250  #quote-preview .backlink.mobile {\
8251    display: none !important;\
8252  }\
8253  .backlink.mobile .quoteLink {\
8254    padding-right: 2px;\
8255  }\
8256  .backlink span {\
8257    padding: 0;\
8258  }\
8259  .burichan_new .backlink a,\
8260  .yotsuba_b_new .backlink a {\
8261    color: #34345C !important;\
8262  }\
8263  .burichan_new .backlink a:hover,\
8264  .yotsuba_b_new .backlink a:hover {\
8265    color: #dd0000 !important;\
8266  }\
8267  .expbtn {\
8268    margin-right: 3px;\
8269    margin-left: 2px;\
8270  }\
8271  .tCollapsed .rExpanded {\
8272    display: none;\
8273  }\
8274  #stickyNav {\
8275    position: fixed;\
8276    font-size: 0;\
8277  }\
8278  #stickyNav img {\
8279    vertical-align: middle;\
8280  }\
8281  .tu-error {\
8282    color: red;\
8283  }\
8284  .topPageNav {\
8285    position: absolute;\
8286  }\
8287  .yotsuba_b_new .topPageNav {\
8288    border-top: 1px solid rgba(255, 255, 255, 0.25);\
8289    border-left: 1px solid rgba(255, 255, 255, 0.25);\
8290  }\
8291  .newPostsMarker:not(#quote-preview) {\
8292    box-shadow: 0 3px red;\
8293  }\
8294  #toggleMsgBtn {\
8295    float: left;\
8296    margin-bottom: 6px;\
8297  }\
8298  .panelHeader {\
8299    font-weight: bold;\
8300    font-size: 16px;\
8301    text-align: center;\
8302    margin-bottom: 5px;\
8303    margin-top: 5px;\
8304    padding-bottom: 5px;\
8305    border-bottom: 1px solid rgba(0, 0, 0, 0.20);\
8306  }\
8307  .yotsuba_new .panelHeader {\
8308    border-bottom: 1px solid #d9bfb7;\
8309  }\
8310  .yotsuba_b_new .panelHeader {\
8311    border-bottom: 1px solid #b7c5d9;\
8312  }\
8313  .tomorrow .panelHeader {\
8314    border-bottom: 1px solid #111;\
8315  }\
8316  .panelHeader span {\
8317    position: absolute;\
8318    right: 5px;\
8319    top: 5px;\
8320  }\
8321  .UIMenu,\
8322  .UIPanel {\
8323    position: fixed;\
8324    width: 100%;\
8325    height: 100%;\
8326    z-index: 9002;\
8327    top: 0;\
8328    left: 0;\
8329  }\
8330  .UIPanel {\
8331    line-height: 14px;\
8332    font-size: 14px;\
8333    background-color: rgba(0, 0, 0, 0.25);\
8334  }\
8335  .UIPanel:after {\
8336    display: inline-block;\
8337    height: 100%;\
8338    vertical-align: middle;\
8339    content: "";\
8340  }\
8341  .UIPanel > div {\
8342    -moz-box-sizing: border-box;\
8343    box-sizing: border-box;\
8344    display: inline-block;\
8345    height: auto;\
8346    max-height: 100%;\
8347    position: relative;\
8348    width: 400px;\
8349    left: 50%;\
8350    margin-left: -200px;\
8351    overflow: auto;\
8352    box-shadow: 0 0 5px rgba(0, 0, 0, 0.25);\
8353    vertical-align: middle;\
8354  }\
8355  #settingsMenu > div {\
8356    top: 25px;;\
8357    vertical-align: top;\
8358    max-height: 85%;\
8359  }\
8360  .extPanel input[type="text"],\
8361  .extPanel textarea {\
8362    border: 1px solid #AAA;\
8363    outline: none;\
8364  }\
8365  .UIPanel .center {\
8366    margin-bottom: 5px;\
8367  }\
8368  .UIPanel button {\
8369    display: inline-block;\
8370    margin-right: 5px;\
8371  }\
8372  .UIPanel code {\
8373    background-color: #eee;\
8374    color: #000000;\
8375    padding: 1px 4px;\
8376    font-size: 12px;\
8377  }\
8378  .UIPanel ul {\
8379    list-style: none;\
8380    padding: 0;\
8381    margin: 0 0 10px;\
8382  }\
8383  .UIPanel .export-field {\
8384    width: 385px;\
8385  }\
8386  #settingsMenu label input {\
8387    margin-right: 5px;\
8388  }\
8389  .tomorrow #settingsMenu ul {\
8390    border-bottom: 1px solid #282a2e;\
8391  }\
8392  .settings-off {\
8393    padding-left: 3px;\
8394  }\
8395  .settings-cat-lbl {\
8396    font-weight: bold;\
8397    margin: 10px 0 5px;\
8398    padding-left: 5px;\
8399  }\
8400  .settings-cat-lbl img {\
8401    vertical-align: text-bottom;\
8402    margin-right: 5px;\
8403    cursor: pointer;\
8404    width: 18px;\
8405    height: 18px;\
8406  }\
8407  .settings-tip {\
8408    font-size: 0.85em;\
8409    margin: 2px 0 5px 0;\
8410    padding-left: 23px;\
8411  }\
8412  #settings-exp-all {\
8413    padding-left: 7px;\
8414    text-align: center;\
8415  }\
8416  #settingsMenu .settings-cat {\
8417    display: none;\
8418    margin-left: 3px;\
8419  }\
8420  #customCSSMenu textarea {\
8421    display: block;\
8422    max-width: 100%;\
8423    min-width: 100%;\
8424    -moz-box-sizing: border-box;\
8425    box-sizing: border-box;\
8426    height: 200px;\
8427    margin: 0 0 5px;\
8428    font-family: monospace;\
8429  }\
8430  #customCSSMenu .right,\
8431  #settingsMenu .right {\
8432    margin-top: 2px;\
8433  }\
8434  #settingsMenu label {\
8435    display: inline-block;\
8436    user-select: none;\
8437    -moz-user-select: none;\
8438    -webkit-user-select: none;\
8439  }\
8440  #filtersHelp > div {\
8441    width: 600px;\
8442    left: 50%;\
8443    margin-left: -300px;\
8444  }\
8445  #filtersHelp h4 {\
8446    font-size: 15px;\
8447    margin: 20px 0 0 10px;\
8448  }\
8449  #filtersHelp h4:before {\
8450    content: "ยป";\
8451    margin-right: 3px;\
8452  }\
8453  #filtersHelp ul {\
8454    padding: 0;\
8455    margin: 10px;\
8456  }\
8457  #filtersHelp li {\
8458    padding: 3px 0;\
8459    list-style: none;\
8460  }\
8461  #filtersMenu table {\
8462    width: 100%;\
8463  }\
8464  #filtersMenu th {\
8465    font-size: 12px;\
8466  }\
8467  #filtersMenu tbody {\
8468    text-align: center;\
8469  }\
8470  #filtersMenu select,\
8471  #filtersMenu .fPattern,\
8472  #filtersMenu .fBoards,\
8473  #palette-custom-input {\
8474    padding: 1px;\
8475    font-size: 11px;\
8476  }\
8477  #filtersMenu select {\
8478    width: 75px;\
8479  }\
8480  #filtersMenu tfoot td {\
8481    padding-top: 10px;\
8482  }\
8483  #keybindsHelp li {\
8484    padding: 3px 5px;\
8485  }\
8486  .fPattern {\
8487    width: 110px;\
8488  }\
8489  .fBoards {\
8490    width: 25px;\
8491  }\
8492  .fColor {\
8493    width: 60px;\
8494  }\
8495  .fDel {\
8496    font-size: 16px;\
8497  }\
8498  .filter-preview {\
8499    cursor: default;\
8500    margin-left: 3px;\
8501  }\
8502  #quote-preview iframe,\
8503  #quote-preview .filter-preview {\
8504    display: none;\
8505  }\
8506  .post-hidden .extButton,\
8507  .post-hidden:not(#quote-preview) .postInfo {\
8508    opacity: 0.5;\
8509  }\
8510  .post-hidden:not(.thread) .postInfo {\
8511    padding-left: 5px;\
8512  }\
8513  .post-hidden:not(#quote-preview) input,\
8514  .post-hidden:not(#quote-preview) .replyContainer,\
8515  .post-hidden:not(#quote-preview) .summary,\
8516  .post-hidden:not(#quote-preview) .op .file,\
8517  .post-hidden:not(#quote-preview) .file,\
8518  .post-hidden .wbtn,\
8519  .post-hidden .postNum span,\
8520  .post-hidden:not(#quote-preview) .backlink,\
8521  div.post-hidden:not(#quote-preview) div.file,\
8522  div.post-hidden:not(#quote-preview) blockquote.postMessage {\
8523    display: none;\
8524  }\
8525  .click-me {\
8526    border-radius: 5px;\
8527    margin-top: 5px;\
8528    padding: 2px 5px;\
8529    position: absolute;\
8530    font-weight: bold;\
8531    z-index: 2;\
8532    white-space: nowrap;\
8533  }\
8534  .yotsuba_new .click-me,\
8535  .futaba_new .click-me {\
8536    color: #800000;\
8537    background-color: #F0E0D6;\
8538    border: 2px solid #D9BFB7;\
8539  }\
8540  .yotsuba_b_new .click-me,\
8541  .burichan_new .click-me {\
8542    color: #000;\
8543    background-color: #D6DAF0;\
8544    border: 2px solid #B7C5D9;\
8545  }\
8546  .tomorrow .click-me {\
8547    color: #C5C8C6;\
8548    background-color: #282A2E;\
8549    border: 2px solid #111;\
8550  }\
8551  .photon .click-me {\
8552    color: #333;\
8553    background-color: #ddd;\
8554    border: 2px solid #ccc;\
8555  }\
8556  .click-me:before {\
8557    content: "";\
8558    border-width: 0 6px 6px;\
8559    border-style: solid;\
8560    left: 50%;\
8561    margin-left: -6px;\
8562    position: absolute;\
8563    width: 0;\
8564    height: 0;\
8565    top: -6px;\
8566  }\
8567  .yotsuba_new .click-me:before,\
8568  .futaba_new .click-me:before {\
8569    border-color: #D9BFB7 transparent;\
8570  }\
8571  .yotsuba_b_new .click-me:before,\
8572  .burichan_new .click-me:before {\
8573    border-color: #B7C5D9 transparent;\
8574  }\
8575  .tomorrow .click-me:before {\
8576    border-color: #111 transparent;\
8577  }\
8578  .photon .click-me:before {\
8579    border-color: #ccc transparent;\
8580  }\
8581  .click-me:after {\
8582    content: "";\
8583    border-width: 0 4px 4px;\
8584    top: -4px;\
8585    display: block;\
8586    left: 50%;\
8587    margin-left: -4px;\
8588    position: absolute;\
8589    width: 0;\
8590    height: 0;\
8591  }\
8592  .yotsuba_new .click-me:after,\
8593  .futaba_new .click-me:after {\
8594    border-color: #F0E0D6 transparent;\
8595    border-style: solid;\
8596  }\
8597  .yotsuba_b_new .click-me:after,\
8598  .burichan_new .click-me:after {\
8599    border-color: #D6DAF0 transparent;\
8600    border-style: solid;\
8601  }\
8602  .tomorrow .click-me:after {\
8603    border-color: #282A2E transparent;\
8604    border-style: solid;\
8605  }\
8606  .photon .click-me:after {\
8607    border-color: #DDD transparent;\
8608    border-style: solid;\
8609  }\
8610  #image-hover {\
8611    position: fixed;\
8612    max-width: 100%;\
8613    max-height: 100%;\
8614    top: 0px;\
8615    right: 0px;\
8616    z-index: 9002;\
8617  }\
8618  .thread-stats {\
8619    float: right;\
8620    margin-right: 5px;\
8621    cursor: default;\
8622  }\
8623  .compact .thread {\
8624    max-width: 75%;\
8625  }\
8626  .dotted {\
8627    text-decoration: none;\
8628    border-bottom: 1px dashed;\
8629  }\
8630  .linkfade {\
8631    opacity: 0.5;\
8632  }\
8633  #quote-preview .linkfade {\
8634    opacity: 1.0;\
8635  }\
8636  kbd {\
8637    background-color: #f7f7f7;\
8638    color: black;\
8639    border: 1px solid #ccc;\
8640    border-radius: 3px 3px 3px 3px;\
8641    box-shadow: 0 1px 0 #ccc, 0 0 0 2px #fff inset;\
8642    font-family: monospace;\
8643    font-size: 11px;\
8644    line-height: 1.4;\
8645    padding: 0 5px;\
8646  }\
8647  .deleted {\
8648    opacity: 0.66;\
8649  }\
8650  .noPictures a.fileThumb img:not(.expanded-thumb) {\
8651    opacity: 0;\
8652  }\
8653  .noPictures.futaba_new a.fileThumb,\
8654  .noPictures.yotsuba_new a.fileThumb {\
8655    border: 1px solid #800;\
8656  }\
8657  .noPictures.burichan_new a.fileThumb,\
8658  .noPictures.yotsuba_b_new a.fileThumb {\
8659    border: 1px solid #34345C;\
8660  }\
8661  .noPictures.tomorrow a.fileThumb:not(.expanded-thumb) {\
8662    border: 1px solid #C5C8C6;\
8663  }\
8664  .noPictures.photon a.fileThumb:not(.expanded-thumb) {\
8665    border: 1px solid #004A99;\
8666  }\
8667  .spinner {\
8668    margin-top: 2px;\
8669    padding: 3px;\
8670    display: table;\
8671  }\
8672  #settings-presets {\
8673    position: relative;\
8674    top: -1px;\
8675  }\
8676  #colorpicker { \
8677    position: fixed;\
8678    text-align: center;\
8679  }\
8680  .colorbox {\
8681    font-size: 10px;\
8682    width: 16px;\
8683    height: 16px;\
8684    line-height: 17px;\
8685    display: inline-block;\
8686    text-align: center;\
8687    background-color: #fff;\
8688    border: 1px solid #aaa;\
8689    text-decoration: none;\
8690    color: #000;\
8691    cursor: pointer;\
8692    vertical-align: top;\
8693  }\
8694  #palette-custom-input {\
8695    vertical-align: top;\
8696    width: 45px;\
8697    margin-right: 2px;\
8698  }\
8699  #qrDummyFile {\
8700    float: left;\
8701    margin-right: 5px;\
8702    width: 220px;\
8703    cursor: default;\
8704    -moz-user-select: none;\
8705    -webkit-user-select: none;\
8706    -ms-user-select: none;\
8707    user-select: none;\
8708    white-space: nowrap;\
8709    text-overflow: ellipsis;\
8710    overflow: hidden;\
8711  }\
8712  #qrDummyFileLabel {\
8713    margin-left: 3px;\
8714  }\
8715  .depageNumber {\
8716    position: absolute;\
8717    right: 5px;\
8718  }\
8719  .depagerEnabled .depagelink {\
8720    font-weight: bold;\
8721  }\
8722  .depagerEnabled strong {\
8723    font-weight: normal;\
8724  }\
8725  .depagelink {\
8726    display: inline-block;\
8727    padding: 4px 0;\
8728    cursor: pointer;\
8729    text-decoration: none;\
8730  }\
8731  .burichan_new .depagelink,\
8732  .futaba_new .depagelink {\
8733    text-decoration: underline;\
8734  }\
8735  #customMenuBox {\
8736    margin: 0 auto 5px auto;\
8737    width: 385px;\
8738    display: block;\
8739  }\
8740  .preview-summary {\
8741    display: block;\
8742  }\
8743  #swf-embed-header {\
8744    padding: 0 0 0 3px;\
8745    font-weight: normal;\
8746    height: 20px;\
8747    line-height: 20px;\
8748  }\
8749  .yotsuba_new #swf-embed-header,\
8750  .yotsuba_b_new #swf-embed-header {\
8751    height: 18px;\
8752    line-height: 18px;\
8753  }\
8754  #swf-embed-close {\
8755    position: absolute;\
8756    right: 0;\
8757    top: 1px;\
8758  }\
8759  .open-qr-wrap {\
8760    text-align: center;\
8761    width: 200px;\
8762    position: absolute;\
8763    margin-left: 50%;\
8764    left: -100px;\
8765  }\
8766  .postMenuBtn {\
8767    margin-left: 5px;\
8768    text-decoration: none;\
8769    line-height: 1em;\
8770    display: inline-block;\
8771    -webkit-transition: -webkit-transform 0.1s;\
8772    -moz-transition: -moz-transform 0.1s;\
8773    transition: transform 0.1s;\
8774    width: 1em;\
8775    height: 1em;\
8776    text-align: center;\
8777    outline: none;\
8778    opacity: 0.8;\
8779  }\
8780  .postMenuBtn:hover{\
8781    opacity: 1;\
8782  }\
8783  .yotsuba_new .postMenuBtn,\
8784  .futaba_new .postMenuBtn {\
8785    color: #000080;\
8786  }\
8787  .tomorrow .postMenuBtn {\
8788    color: #5F89AC !important;\
8789  }\
8790  .tomorrow .postMenuBtn:hover {\
8791    color: #81a2be !important;\
8792  }\
8793  .photon .postMenuBtn {\
8794    color: #FF6600 !important;\
8795  }\
8796  .photon .postMenuBtn:hover {\
8797    color: #FF3300 !important;\
8798  }\
8799  .menuOpen {\
8800    -webkit-transform: rotate(90deg);\
8801    -moz-transform: rotate(90deg);\
8802    -ms-transform: rotate(90deg);\
8803    transform: rotate(90deg);\
8804  }\
8805  .settings-sub label:before {\
8806    border-bottom: 1px solid;\
8807    border-left: 1px solid;\
8808    content: " ";\
8809    display: inline-block;\
8810    height: 8px;\
8811    margin-bottom: 5px;\
8812    width: 8px;\
8813  }\
8814  .settings-sub {\
8815    margin-left: 25px;\
8816  }\
8817  .settings-tip.settings-sub {\
8818    padding-left: 32px;\
8819  }\
8820  .centeredThreads .opContainer {\
8821    display: block;\
8822  }\
8823  .centeredThreads .postContainer {\
8824    margin: auto;\
8825    width: 75%;\
8826  }\
8827  .centeredThreads .sideArrows {\
8828    display: none;\
8829  }\
8830  .centre-exp {\
8831    width: auto !important;\
8832    clear: both;\
8833  }\
8834  .centeredThreads .expandedWebm {\
8835    float: none;\
8836  }\
8837  .centeredThreads .summary {\
8838    margin-left: 12.5%;\
8839    display: block;\
8840  }\
8841  .centre-exp div.op{\
8842    display: table;\
8843  }\
8844  #yt-preview { position: absolute; }\
8845  #yt-preview img { display: block; }\
8846  \
8847  @media only screen and (max-width: 480px) {\
8848  #threadWatcher {\
8849    max-width: none;\
8850    padding: 3px 0;\
8851    left: 0;\
8852    width: 100%;\
8853    border-left: none;\
8854    border-right: none;\
8855  }\
8856  #watchList {\
8857    padding: 0 10px;\
8858  }\
8859  .btn-row {\
8860    margin-top: 5px;\
8861  }\
8862  .image-expanded .mFileInfo {\
8863    display: none !important;\
8864  }\
8865  .mobile-report {\
8866    float: right;\
8867    font-size: 11px;\
8868    margin-bottom: 3px;\
8869    margin-left: 10px;\
8870  }\
8871  .mobile-report:after {\
8872    content: "]";\
8873  }\
8874  .mobile-report:before {\
8875    content: "[";\
8876  }\
8877  .nws .mobile-report:after {\
8878    color: #800000;\
8879  }\
8880  .nws .mobile-report:before {\
8881    color: #800000;\
8882  }\
8883  .ws .mobile-report {\
8884    color: #34345C;\
8885  }\
8886  .nws .mobile-report {\
8887    color:#0000EE;\
8888  }\
8889  .reply .mobile-report {\
8890    margin: 5px 5px 0 5px;\
8891  }\
8892  .postLink .mobileHideButton {\
8893    margin-right: 3px;\
8894  }\
8895  .board .mobile-hr-hidden {\
8896    margin-top: 10px !important;\
8897  }\
8898  .board > .mobileHideButton {\
8899    margin-top: -20px !important;\
8900  }\
8901  .board > .mobileHideButton:first-child {\
8902    margin-top: 10px !important;\
8903  }\
8904  .extButton.threadHideButton {\
8905    float: none;\
8906    margin: 0;\
8907    margin-bottom: 5px;\
8908  }\
8909  .mobile-post-hidden {\
8910    display: none;\
8911  }\
8912  #toggleMsgBtn {\
8913    display: none;\
8914  }\
8915  .mobile-tu-status {\
8916    height: 20px;\
8917    line-height: 20px;\
8918  }\
8919  .mobile-tu-show {\
8920    width: 150px;\
8921    margin: auto;\
8922    display: block;\
8923    text-align: center;\
8924  }\
8925  .button input {\
8926    margin: 0 3px 0 0;\
8927    position: relative;\
8928    top: -2px;\
8929    border-radius: 0;\
8930    height: 10px;\
8931    width: 10px;\
8932  }\
8933  .UIPanel > div {\
8934    width: 320px;\
8935    margin-left: -160px;\
8936  }\
8937  .UIPanel .export-field {\
8938    width: 300px;\
8939  }\
8940  .yotsuba_new #quote-preview.highlight,\
8941  #quote-preview {\
8942    border-width: 1px !important;\
8943  }\
8944  .yotsuba_new #quote-preview.highlight {\
8945    border-color: #D9BFB7 !important;\
8946  }\
8947  #quickReply input[type="text"],\
8948  #quickReply textarea,\
8949  .extPanel input[type="text"],\
8950  .extPanel textarea {\
8951    font-size: 16px;\
8952  }\
8953  #quickReply {\
8954    position: absolute;\
8955    left: 50%;\
8956    margin-left: -154px;\
8957  }\
8958  }\
8959  ';
8960    
8961    style = document.createElement('style');
8962    style.setAttribute('type', 'text/css');
8963    style.textContent = css;
8964    document.head.appendChild(style);
8965  };
8966  
8967  Main.init();