From 54c5b599468aef5454b5ccc0221a9c1fe96d503b Mon Sep 17 00:00:00 2001 From: Zixaphir Date: Sat, 10 Jan 2015 02:28:35 -0700 Subject: [PATCH] More fixes Still no header or posts, but no errors either. :D --- builds/appchan-x.user.js | 271 ++++++++++++------------- builds/crx/script.js | 271 ++++++++++++------------- src/General/Build.coffee | 6 +- src/General/Header.coffee | 2 +- src/General/UI.coffee | 89 ++++---- src/General/eventPage/eventPage.coffee | 28 +++ src/General/lib/databoard.class | 50 ++--- src/General/lib/notice.class | 2 +- src/Monitoring/ThreadWatcher.coffee | 1 + src/Posting/QR.coffee | 2 +- 10 files changed, 374 insertions(+), 348 deletions(-) create mode 100644 src/General/eventPage/eventPage.coffee diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index de2e61d9f..2ea12029b 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -3872,15 +3872,22 @@ }; DataBoard.prototype["delete"] = function(_arg) { - var boardID, postID, threadID; + var boardID, postID, threadID, _ref; boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID; + $.forceSync(this.key); if (postID) { + if (!((_ref = this.data.boards[boardID]) != null ? _ref[threadID] : void 0)) { + return; + } delete this.data.boards[boardID][threadID][postID]; this.deleteIfEmpty({ boardID: boardID, threadID: threadID }); } else if (threadID) { + if (!this.data.boards[boardID]) { + return; + } delete this.data.boards[boardID][threadID]; this.deleteIfEmpty({ boardID: boardID @@ -3894,6 +3901,7 @@ DataBoard.prototype.deleteIfEmpty = function(_arg) { var boardID, threadID; boardID = _arg.boardID, threadID = _arg.threadID; + $.forceSync(this.key); if (threadID) { if (!Object.keys(this.data.boards[boardID][threadID]).length) { delete this.data.boards[boardID][threadID]; @@ -3909,6 +3917,7 @@ DataBoard.prototype.set = function(_arg) { var boardID, postID, threadID, val, _base, _base1, _base2; boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID, val = _arg.val; + $.forceSync(this.key); if (postID !== void 0) { ((_base = ((_base1 = this.data.boards)[boardID] || (_base1[boardID] = {})))[threadID] || (_base[threadID] = {}))[postID] = val; } else if (threadID !== void 0) { @@ -3942,70 +3951,47 @@ return val || defaultValue; }; + DataBoard.prototype.forceSync = function() { + return $.forceSync(this.key); + }; + DataBoard.prototype.clean = function() { - var boardID, keys, now, _i, _len; - now = Date.now(); - if ((this.data.lastChecked || 0) > now - 2 * $.HOUR) { - return; - } - keys = Object.keys(this.data.boards); - if (!keys.length) { - return; - } - for (_i = 0, _len = keys.length; _i < _len; _i++) { - boardID = keys[_i]; + var boardID, now, threadID, val, _ref; + $.forceSync(this.key); + _ref = this.data.boards; + for (boardID in _ref) { + val = _ref[boardID]; this.deleteIfEmpty({ boardID: boardID }); - if (boardID in this.data.boards) { - this.ajaxClean(boardID); + } + now = Date.now(); + if ((this.data.lastChecked || 0) < now - 2 * $.HOUR) { + this.data.lastChecked = now; + for (boardID in this.data.boards) { + for (threadID in this.data.boards[boardID]) { + this.ajaxClean(boardID, threadID); + } } } - this.data.lastChecked = now; return this.save(); }; - DataBoard.prototype.ajaxClean = function(boardID) { - return $.cache("//a.4cdn.org/" + boardID + "/threads.json", (function(_this) { - return function(e) { - var board, count, page, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - if (e.target.status !== 200) { + DataBoard.prototype.ajaxClean = function(boardID, threadID) { + return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { + onloadend: (function(_this) { + return function(e) { if (e.target.status === 404) { - _this["delete"]({ - boardID: boardID + return _this["delete"]({ + boardID: boardID, + threadID: threadID }); } - return; - } - board = _this.data.boards[boardID]; - threads = {}; - _ref = e.target.response; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - page = _ref[_i]; - _ref1 = page.threads; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - thread = _ref1[_j]; - if (thread.no in board) { - threads[thread.no] = board[thread.no]; - } - } - } - count = Object.keys(threads).length; - if (count === Object.keys(board).length) { - return; - } - if (count) { - return _this.set({ - boardID: boardID, - val: threads - }); - } else { - return _this["delete"]({ - boardID: boardID - }); - } - }; - })(this)); + }; + })(this) + }, { + type: 'head' + }); }; DataBoard.prototype.onSync = function(data) { @@ -4053,7 +4039,7 @@ return; } $.off(d, 'visibilitychange', this.add); - $.add(Header.noticesRoot, this.el); + $.add(doc, this.el); this.el.clientHeight; this.el.style.opacity = 1; if (this.timeout) { @@ -4319,7 +4305,7 @@ className: 'menu-button a-icon', id: 'main-menu' }); - box = UI.checkbox.bind(UI); + box = UI.checkbox; barFixedToggler = box('Fixed Header', 'Fixed Header'); headerToggler = box('Header auto-hide', ' Auto-hide header'); scrollHeaderToggler = box('Header auto-hide on scroll', ' Auto-hide header on scroll'); @@ -6211,43 +6197,21 @@ }; /* File Info */ - if (file != null ? file.isDeleted : void 0) { - fileCont = { - innerHTML: "\"File" - }; - } else if (file && boardID === 'f') { - fileCont = { - innerHTML: "
File: " + E(file.name) + "-(" + E($.bytesToString(file.size)) + ", " + E(file.width) + "x" + E(file.height) + ", " + E(file.tag) + ")
" - }; - } else if (file) { - if (file.isSpoiler) { - shortFilename = 'Spoiler Image'; - if (spoilerRange = Build.spoilerRange[boardID]) { - fileThumb = "//s.4cdn.org/image/spoiler-" + boardID + (Math.floor(1 + spoilerRange * Math.random())) + ".png"; - } else { - fileThumb = '//s.4cdn.org/image/spoiler.png'; - } - file.twidth = file.theight = 100; - } else { - shortFilename = Build.shortFilename(file.name, !isOP); - fileThumb = file.turl; - } - fileSize = $.bytesToString(file.size); - fileDims = file.url.slice(-4) === '.pdf' ? 'PDF' : "" + file.width + "x" + file.height; - fileLink = file.isSpoiler || file.name === shortFilename ? { - innerHTML: "" + E(shortFilename) + "" - } : { - innerHTML: "" + E(shortFilename) + "" - }; - fileText = file.isSpoiler ? { - innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" - } : { - innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" - }; - ({ - innerHTML: fileText.innerHTML + "\""" - }); - } + fileCont = (file != null ? file.isDeleted : void 0) ? { + innerHTML: "\"File" + } : file && boardID === 'f' ? { + innerHTML: "
File: " + E(file.name) + "-(" + E($.bytesToString(file.size)) + ", " + E(file.width) + "x" + E(file.height) + ", " + E(file.tag) + ")
" + } : file ? (file.isSpoiler ? (shortFilename = 'Spoiler Image', (spoilerRange = Build.spoilerRange[boardID]) ? fileThumb = "//s.4cdn.org/image/spoiler-" + boardID + (Math.floor(1 + spoilerRange * Math.random())) + ".png" : fileThumb = '//s.4cdn.org/image/spoiler.png', file.twidth = file.theight = 100) : (shortFilename = Build.shortFilename(file.name, !isOP), fileThumb = file.turl), fileSize = $.bytesToString(file.size), fileDims = file.url.slice(-4) === '.pdf' ? 'PDF' : "" + file.width + "x" + file.height, fileLink = file.isSpoiler || file.name === shortFilename ? { + innerHTML: "" + E(shortFilename) + "" + } : { + innerHTML: "" + E(shortFilename) + "" + }, fileText = file.isSpoiler ? { + innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" + } : { + innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" + }, { + innerHTML: fileText.innerHTML + "\""" + }) : void 0; fileBlock = file ? { innerHTML: "
" + fileCont.innerHTML + "
" } : { @@ -6765,20 +6729,30 @@ }; UI = (function() { - var Menu, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; - dialog = function(id, position, html) { - var el, move; + var Menu, checkbox, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; + dialog = function(id, position, properties) { + var child, el, move, _i, _len, _ref; el = $.el('div', { className: 'dialog', - innerHTML: html, id: id }); + $.extend(el, properties); el.style.cssText = position; $.get("" + id + ".position", position, function(item) { return el.style.cssText = item["" + id + ".position"]; }); move = $('.move', el); $.on(move, 'touchstart mousedown', dragstart); + _ref = move.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (!child.tagName) { + continue; + } + $.on(child, 'touchstart mousedown', function(e) { + return e.stopPropagation(); + }); + } return el; }; Menu = (function() { @@ -6788,11 +6762,24 @@ lastToggledButton = null; - function Menu() { + function Menu(type) { + this.type = type; this.rmEntry = __bind(this.rmEntry, this); + this.addEntry = __bind(this.addEntry, this); this.onFocus = __bind(this.onFocus, this); this.keybinds = __bind(this.keybinds, this); this.close = __bind(this.close, this); + $.on(d, 'AddMenuEntry', (function(_this) { + return function(_arg) { + var detail; + detail = _arg.detail; + if (detail.type !== _this.type) { + return; + } + delete detail.open; + return _this.addEntry(detail); + }; + })(this)); this.entries = []; } @@ -6832,7 +6819,6 @@ menu = this.makeMenu(); currentMenu = menu; lastToggledButton = button; - $.addClass(button, 'open'); this.entries.sort(function(first, second) { return first.order - second.order; }); @@ -6870,12 +6856,7 @@ Menu.prototype.insertEntry = function(entry, parent, data) { var subEntry, submenu, _i, _len, _ref; if (typeof entry.open === 'function') { - if (!entry.open(data, (function(_this) { - return function(subEntry) { - _this.parseEntry(subEntry); - return entry.subEntries.push(subEntry); - }; - })(this))) { + if (!entry.open(data)) { return; } } @@ -6899,7 +6880,7 @@ Menu.prototype.close = function() { $.rm(currentMenu); - $.rmClass(lastToggledButton, 'open'); + $.rmClass(lastToggledButton, 'active'); currentMenu = null; lastToggledButton = null; return $.off(d, 'click CloseMenu', this.close); @@ -7014,7 +6995,6 @@ return; } $.addClass(el, 'has-submenu'); - $.addClass(el, 'pfa'); for (_i = 0, _len = subEntries.length; _i < _len; _i++) { subEntry = subEntries[_i]; this.parseEntry(subEntry); @@ -7025,13 +7005,13 @@ })(); dragstart = function(e) { - var el, isTouching, o, rect, screenHeight, screenWidth, _ref, _ref1; + var el, isTouching, o, rect, screenHeight, screenWidth, _ref; if (e.type === 'mousedown' && e.button !== 0) { return; } e.preventDefault(); if (isTouching = e.type === 'touchstart') { - _ref = e.changedTouches, e = _ref[_ref.length - 1]; + e = e.changedTouches[e.changedTouches.length - 1]; } el = $.x('ancestor::div[contains(@class,"dialog")][1]', this); rect = el.getBoundingClientRect(); @@ -7048,7 +7028,7 @@ screenWidth: screenWidth, isTouching: isTouching }; - _ref1 = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? [0, 0] : Conf['Bottom Header'] ? [0, Header.bar.getBoundingClientRect().height] : [Header.bar.getBoundingClientRect().height, 0], o.topBorder = _ref1[0], o.bottomBorder = _ref1[1]; + _ref = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? [0, 0] : Conf['Bottom Header'] ? [0, Header.bar.getBoundingClientRect().height] : [Header.bar.getBoundingClientRect().height, 0], o.topBorder = _ref[0], o.bottomBorder = _ref[1]; if (isTouching) { o.identifier = e.identifier; o.move = touchmove.bind(o); @@ -7110,33 +7090,29 @@ return $.set("" + this.id + ".position", this.style.cssText); }; hoverstart = function(_arg) { - var asapTest, cb, el, endEvents, latestEvent, noRemove, o, offsetX, offsetY, root; - root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, cb = _arg.cb, offsetX = _arg.offsetX, offsetY = _arg.offsetY, noRemove = _arg.noRemove; + var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, root, _ref; + root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, height = _arg.height, cb = _arg.cb, noRemove = _arg.noRemove; o = { root: root, el: el, style: el.style, + isImage: (_ref = el.nodeName) === 'IMG' || _ref === 'VIDEO', cb: cb, - close: close, endEvents: endEvents, latestEvent: latestEvent, clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, - offsetX: offsetX || 45, - offsetY: offsetY || -120, noRemove: noRemove }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); - if (asapTest) { - $.asap(function() { - return !el.parentNode || asapTest(); - }, function() { - if (el.parentNode) { - return o.hover(o.latestEvent); - } - }); - } + $.asap(function() { + return !el.parentNode || asapTest(); + }, function() { + if (el.parentNode) { + return o.hover(o.latestEvent); + } + }); $.on(root, endEvents, o.hoverend); if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) { $.on(d, 'keydown', o.hoverend); @@ -7150,22 +7126,28 @@ return $.on(doc, 'mousemove', o.workaround); }; hover = function(e) { - var clientX, clientY, height, left, right, top, _ref; + var clientX, clientY, height, left, right, style, threshold, top, _ref; this.latestEvent = e; - height = this.el.offsetHeight + 25; + height = this.height || this.el.offsetHeight; clientX = e.clientX, clientY = e.clientY; - top = clientY + this.offsetY; - top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top; - _ref = clientX <= this.clientWidth / 2 ? [clientX + this.offsetX + 'px', null] : [null, this.clientWidth - clientX + this.offsetX + 'px'], left = _ref[0], right = _ref[1]; - this.style.top = top + 'px'; - this.style.left = left; - return this.style.right = right; + top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); + threshold = this.clientWidth / 2; + if (!this.isImage) { + threshold = Math.max(threshold, this.clientWidth - 400); + } + _ref = clientX <= threshold ? [clientX + 45 + 'px', null] : [null, this.clientWidth - clientX + 45 + 'px'], left = _ref[0], right = _ref[1]; + style = this.style; + style.top = top + 'px'; + style.left = left; + return style.right = right; }; hoverend = function(e) { if (e.type === 'keydown' && e.keyCode !== 13 || e.target.nodeName === "TEXTAREA") { return; } - $.rm(this.el); + if (!this.noRemove) { + $.rm(this.el); + } $.off(this.root, this.endEvents, this.hoverend); $.off(d, 'keydown', this.hoverend); $.off(this.root, 'mousemove', this.hover); @@ -7174,10 +7156,25 @@ return this.cb.call(this); } }; + checkbox = function(name, text, checked) { + var input, label; + if (checked == null) { + checked = Conf[name]; + } + label = $.el('label'); + input = $.el('input', { + type: 'checkbox', + name: name, + checked: checked + }); + $.add(label, [input, $.tn(text)]); + return label; + }; return { dialog: dialog, Menu: Menu, - hover: hoverstart + hover: hoverstart, + checkbox: checkbox }; })(); @@ -9730,11 +9727,10 @@ dialog: function() { var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { - el: dialog = UI.dialog('qr', 'top:0;right:0;') + el: dialog = UI.dialog('qr', 'top:0;right:0;', { + innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" + }) }; - $.extend(dialog, { - innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" - }); setNode = function(name, query) { return nodes[name] = $(query, dialog); }; @@ -14099,6 +14095,7 @@ this.status = $('#watcher-status', this.dialog); this.list = this.dialog.lastElementChild; this.refreshButton = $('.refresh', this.dialog); + this.unreaddb = Unread.db || new DataBoard('lastReadPosts'); $.on(d, 'QRPostSuccessful', this.cb.post); if (g.VIEW === 'thread') { $.on(d, 'ThreadUpdate', this.cb.threadUpdate); diff --git a/builds/crx/script.js b/builds/crx/script.js index 842fdf724..b2addcf5f 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -3899,15 +3899,22 @@ }; DataBoard.prototype["delete"] = function(_arg) { - var boardID, postID, threadID; + var boardID, postID, threadID, _ref; boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID; + $.forceSync(this.key); if (postID) { + if (!((_ref = this.data.boards[boardID]) != null ? _ref[threadID] : void 0)) { + return; + } delete this.data.boards[boardID][threadID][postID]; this.deleteIfEmpty({ boardID: boardID, threadID: threadID }); } else if (threadID) { + if (!this.data.boards[boardID]) { + return; + } delete this.data.boards[boardID][threadID]; this.deleteIfEmpty({ boardID: boardID @@ -3921,6 +3928,7 @@ DataBoard.prototype.deleteIfEmpty = function(_arg) { var boardID, threadID; boardID = _arg.boardID, threadID = _arg.threadID; + $.forceSync(this.key); if (threadID) { if (!Object.keys(this.data.boards[boardID][threadID]).length) { delete this.data.boards[boardID][threadID]; @@ -3936,6 +3944,7 @@ DataBoard.prototype.set = function(_arg) { var boardID, postID, threadID, val, _base, _base1, _base2; boardID = _arg.boardID, threadID = _arg.threadID, postID = _arg.postID, val = _arg.val; + $.forceSync(this.key); if (postID !== void 0) { ((_base = ((_base1 = this.data.boards)[boardID] || (_base1[boardID] = {})))[threadID] || (_base[threadID] = {}))[postID] = val; } else if (threadID !== void 0) { @@ -3969,70 +3978,47 @@ return val || defaultValue; }; + DataBoard.prototype.forceSync = function() { + return $.forceSync(this.key); + }; + DataBoard.prototype.clean = function() { - var boardID, keys, now, _i, _len; - now = Date.now(); - if ((this.data.lastChecked || 0) > now - 2 * $.HOUR) { - return; - } - keys = Object.keys(this.data.boards); - if (!keys.length) { - return; - } - for (_i = 0, _len = keys.length; _i < _len; _i++) { - boardID = keys[_i]; + var boardID, now, threadID, val, _ref; + $.forceSync(this.key); + _ref = this.data.boards; + for (boardID in _ref) { + val = _ref[boardID]; this.deleteIfEmpty({ boardID: boardID }); - if (boardID in this.data.boards) { - this.ajaxClean(boardID); + } + now = Date.now(); + if ((this.data.lastChecked || 0) < now - 2 * $.HOUR) { + this.data.lastChecked = now; + for (boardID in this.data.boards) { + for (threadID in this.data.boards[boardID]) { + this.ajaxClean(boardID, threadID); + } } } - this.data.lastChecked = now; return this.save(); }; - DataBoard.prototype.ajaxClean = function(boardID) { - return $.cache("//a.4cdn.org/" + boardID + "/threads.json", (function(_this) { - return function(e) { - var board, count, page, thread, threads, _i, _j, _len, _len1, _ref, _ref1; - if (e.target.status !== 200) { + DataBoard.prototype.ajaxClean = function(boardID, threadID) { + return $.ajax("//a.4cdn.org/" + boardID + "/thread/" + threadID + ".json", { + onloadend: (function(_this) { + return function(e) { if (e.target.status === 404) { - _this["delete"]({ - boardID: boardID + return _this["delete"]({ + boardID: boardID, + threadID: threadID }); } - return; - } - board = _this.data.boards[boardID]; - threads = {}; - _ref = e.target.response; - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - page = _ref[_i]; - _ref1 = page.threads; - for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { - thread = _ref1[_j]; - if (thread.no in board) { - threads[thread.no] = board[thread.no]; - } - } - } - count = Object.keys(threads).length; - if (count === Object.keys(board).length) { - return; - } - if (count) { - return _this.set({ - boardID: boardID, - val: threads - }); - } else { - return _this["delete"]({ - boardID: boardID - }); - } - }; - })(this)); + }; + })(this) + }, { + type: 'head' + }); }; DataBoard.prototype.onSync = function(data) { @@ -4080,7 +4066,7 @@ return; } $.off(d, 'visibilitychange', this.add); - $.add(Header.noticesRoot, this.el); + $.add(doc, this.el); this.el.clientHeight; this.el.style.opacity = 1; if (this.timeout) { @@ -4348,7 +4334,7 @@ className: 'menu-button a-icon', id: 'main-menu' }); - box = UI.checkbox.bind(UI); + box = UI.checkbox; barFixedToggler = box('Fixed Header', 'Fixed Header'); headerToggler = box('Header auto-hide', ' Auto-hide header'); scrollHeaderToggler = box('Header auto-hide on scroll', ' Auto-hide header on scroll'); @@ -6240,43 +6226,21 @@ }; /* File Info */ - if (file != null ? file.isDeleted : void 0) { - fileCont = { - innerHTML: "\"File" - }; - } else if (file && boardID === 'f') { - fileCont = { - innerHTML: "
File: " + E(file.name) + "-(" + E($.bytesToString(file.size)) + ", " + E(file.width) + "x" + E(file.height) + ", " + E(file.tag) + ")
" - }; - } else if (file) { - if (file.isSpoiler) { - shortFilename = 'Spoiler Image'; - if (spoilerRange = Build.spoilerRange[boardID]) { - fileThumb = "//s.4cdn.org/image/spoiler-" + boardID + (Math.floor(1 + spoilerRange * Math.random())) + ".png"; - } else { - fileThumb = '//s.4cdn.org/image/spoiler.png'; - } - file.twidth = file.theight = 100; - } else { - shortFilename = Build.shortFilename(file.name, !isOP); - fileThumb = file.turl; - } - fileSize = $.bytesToString(file.size); - fileDims = file.url.slice(-4) === '.pdf' ? 'PDF' : "" + file.width + "x" + file.height; - fileLink = file.isSpoiler || file.name === shortFilename ? { - innerHTML: "" + E(shortFilename) + "" - } : { - innerHTML: "" + E(shortFilename) + "" - }; - fileText = file.isSpoiler ? { - innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" - } : { - innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" - }; - ({ - innerHTML: fileText.innerHTML + "\""" - }); - } + fileCont = (file != null ? file.isDeleted : void 0) ? { + innerHTML: "\"File" + } : file && boardID === 'f' ? { + innerHTML: "
File: " + E(file.name) + "-(" + E($.bytesToString(file.size)) + ", " + E(file.width) + "x" + E(file.height) + ", " + E(file.tag) + ")
" + } : file ? (file.isSpoiler ? (shortFilename = 'Spoiler Image', (spoilerRange = Build.spoilerRange[boardID]) ? fileThumb = "//s.4cdn.org/image/spoiler-" + boardID + (Math.floor(1 + spoilerRange * Math.random())) + ".png" : fileThumb = '//s.4cdn.org/image/spoiler.png', file.twidth = file.theight = 100) : (shortFilename = Build.shortFilename(file.name, !isOP), fileThumb = file.turl), fileSize = $.bytesToString(file.size), fileDims = file.url.slice(-4) === '.pdf' ? 'PDF' : "" + file.width + "x" + file.height, fileLink = file.isSpoiler || file.name === shortFilename ? { + innerHTML: "" + E(shortFilename) + "" + } : { + innerHTML: "" + E(shortFilename) + "" + }, fileText = file.isSpoiler ? { + innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" + } : { + innerHTML: "
File: " + fileLink.innerHTML + " (" + E(fileSize) + ", " + E(fileDims) + ")
" + }, { + innerHTML: fileText.innerHTML + "\""" + }) : void 0; fileBlock = file ? { innerHTML: "
" + fileCont.innerHTML + "
" } : { @@ -6794,20 +6758,30 @@ }; UI = (function() { - var Menu, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; - dialog = function(id, position, html) { - var el, move; + var Menu, checkbox, dialog, drag, dragend, dragstart, hover, hoverend, hoverstart, touchend, touchmove; + dialog = function(id, position, properties) { + var child, el, move, _i, _len, _ref; el = $.el('div', { className: 'dialog', - innerHTML: html, id: id }); + $.extend(el, properties); el.style.cssText = position; $.get("" + id + ".position", position, function(item) { return el.style.cssText = item["" + id + ".position"]; }); move = $('.move', el); $.on(move, 'touchstart mousedown', dragstart); + _ref = move.children; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + child = _ref[_i]; + if (!child.tagName) { + continue; + } + $.on(child, 'touchstart mousedown', function(e) { + return e.stopPropagation(); + }); + } return el; }; Menu = (function() { @@ -6817,11 +6791,24 @@ lastToggledButton = null; - function Menu() { + function Menu(type) { + this.type = type; this.rmEntry = __bind(this.rmEntry, this); + this.addEntry = __bind(this.addEntry, this); this.onFocus = __bind(this.onFocus, this); this.keybinds = __bind(this.keybinds, this); this.close = __bind(this.close, this); + $.on(d, 'AddMenuEntry', (function(_this) { + return function(_arg) { + var detail; + detail = _arg.detail; + if (detail.type !== _this.type) { + return; + } + delete detail.open; + return _this.addEntry(detail); + }; + })(this)); this.entries = []; } @@ -6861,7 +6848,6 @@ menu = this.makeMenu(); currentMenu = menu; lastToggledButton = button; - $.addClass(button, 'open'); this.entries.sort(function(first, second) { return first.order - second.order; }); @@ -6899,12 +6885,7 @@ Menu.prototype.insertEntry = function(entry, parent, data) { var subEntry, submenu, _i, _len, _ref; if (typeof entry.open === 'function') { - if (!entry.open(data, (function(_this) { - return function(subEntry) { - _this.parseEntry(subEntry); - return entry.subEntries.push(subEntry); - }; - })(this))) { + if (!entry.open(data)) { return; } } @@ -6928,7 +6909,7 @@ Menu.prototype.close = function() { $.rm(currentMenu); - $.rmClass(lastToggledButton, 'open'); + $.rmClass(lastToggledButton, 'active'); currentMenu = null; lastToggledButton = null; return $.off(d, 'click CloseMenu', this.close); @@ -7043,7 +7024,6 @@ return; } $.addClass(el, 'has-submenu'); - $.addClass(el, 'pfa'); for (_i = 0, _len = subEntries.length; _i < _len; _i++) { subEntry = subEntries[_i]; this.parseEntry(subEntry); @@ -7054,13 +7034,13 @@ })(); dragstart = function(e) { - var el, isTouching, o, rect, screenHeight, screenWidth, _ref, _ref1; + var el, isTouching, o, rect, screenHeight, screenWidth, _ref; if (e.type === 'mousedown' && e.button !== 0) { return; } e.preventDefault(); if (isTouching = e.type === 'touchstart') { - _ref = e.changedTouches, e = _ref[_ref.length - 1]; + e = e.changedTouches[e.changedTouches.length - 1]; } el = $.x('ancestor::div[contains(@class,"dialog")][1]', this); rect = el.getBoundingClientRect(); @@ -7077,7 +7057,7 @@ screenWidth: screenWidth, isTouching: isTouching }; - _ref1 = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? [0, 0] : Conf['Bottom Header'] ? [0, Header.bar.getBoundingClientRect().height] : [Header.bar.getBoundingClientRect().height, 0], o.topBorder = _ref1[0], o.bottomBorder = _ref1[1]; + _ref = Conf['Header auto-hide'] || !Conf['Fixed Header'] ? [0, 0] : Conf['Bottom Header'] ? [0, Header.bar.getBoundingClientRect().height] : [Header.bar.getBoundingClientRect().height, 0], o.topBorder = _ref[0], o.bottomBorder = _ref[1]; if (isTouching) { o.identifier = e.identifier; o.move = touchmove.bind(o); @@ -7139,33 +7119,29 @@ return $.set("" + this.id + ".position", this.style.cssText); }; hoverstart = function(_arg) { - var asapTest, cb, el, endEvents, latestEvent, noRemove, o, offsetX, offsetY, root; - root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, cb = _arg.cb, offsetX = _arg.offsetX, offsetY = _arg.offsetY, noRemove = _arg.noRemove; + var asapTest, cb, el, endEvents, height, latestEvent, noRemove, o, root, _ref; + root = _arg.root, el = _arg.el, latestEvent = _arg.latestEvent, endEvents = _arg.endEvents, asapTest = _arg.asapTest, height = _arg.height, cb = _arg.cb, noRemove = _arg.noRemove; o = { root: root, el: el, style: el.style, + isImage: (_ref = el.nodeName) === 'IMG' || _ref === 'VIDEO', cb: cb, - close: close, endEvents: endEvents, latestEvent: latestEvent, clientHeight: doc.clientHeight, clientWidth: doc.clientWidth, - offsetX: offsetX || 45, - offsetY: offsetY || -120, noRemove: noRemove }; o.hover = hover.bind(o); o.hoverend = hoverend.bind(o); - if (asapTest) { - $.asap(function() { - return !el.parentNode || asapTest(); - }, function() { - if (el.parentNode) { - return o.hover(o.latestEvent); - } - }); - } + $.asap(function() { + return !el.parentNode || asapTest(); + }, function() { + if (el.parentNode) { + return o.hover(o.latestEvent); + } + }); $.on(root, endEvents, o.hoverend); if ($.x('ancestor::div[contains(@class,"inline")][1]', root)) { $.on(d, 'keydown', o.hoverend); @@ -7173,22 +7149,28 @@ return $.on(root, 'mousemove', o.hover); }; hover = function(e) { - var clientX, clientY, height, left, right, top, _ref; + var clientX, clientY, height, left, right, style, threshold, top, _ref; this.latestEvent = e; - height = this.el.offsetHeight + 25; + height = this.height || this.el.offsetHeight; clientX = e.clientX, clientY = e.clientY; - top = clientY + this.offsetY; - top = this.clientHeight <= height || top <= 0 ? 0 : top + height >= this.clientHeight ? this.clientHeight - height : top; - _ref = clientX <= this.clientWidth / 2 ? [clientX + this.offsetX + 'px', null] : [null, this.clientWidth - clientX + this.offsetX + 'px'], left = _ref[0], right = _ref[1]; - this.style.top = top + 'px'; - this.style.left = left; - return this.style.right = right; + top = this.isImage ? Math.max(0, clientY * (this.clientHeight - height) / this.clientHeight) : Math.max(0, Math.min(this.clientHeight - height, clientY - 120)); + threshold = this.clientWidth / 2; + if (!this.isImage) { + threshold = Math.max(threshold, this.clientWidth - 400); + } + _ref = clientX <= threshold ? [clientX + 45 + 'px', null] : [null, this.clientWidth - clientX + 45 + 'px'], left = _ref[0], right = _ref[1]; + style = this.style; + style.top = top + 'px'; + style.left = left; + return style.right = right; }; hoverend = function(e) { if (e.type === 'keydown' && e.keyCode !== 13 || e.target.nodeName === "TEXTAREA") { return; } - $.rm(this.el); + if (!this.noRemove) { + $.rm(this.el); + } $.off(this.root, this.endEvents, this.hoverend); $.off(d, 'keydown', this.hoverend); $.off(this.root, 'mousemove', this.hover); @@ -7196,10 +7178,25 @@ return this.cb.call(this); } }; + checkbox = function(name, text, checked) { + var input, label; + if (checked == null) { + checked = Conf[name]; + } + label = $.el('label'); + input = $.el('input', { + type: 'checkbox', + name: name, + checked: checked + }); + $.add(label, [input, $.tn(text)]); + return label; + }; return { dialog: dialog, Menu: Menu, - hover: hoverstart + hover: hoverstart, + checkbox: checkbox }; })(); @@ -9784,11 +9781,10 @@ dialog: function() { var dialog, event, i, items, match_max, match_min, name, node, nodes, rules, save, setNode; QR.nodes = nodes = { - el: dialog = UI.dialog('qr', 'top:0;right:0;') + el: dialog = UI.dialog('qr', 'top:0;right:0;', { + innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" + }) }; - $.extend(dialog, { - innerHTML: "
\r\r
\r\r
\r\\uf00d\r
\r
\r
\r\r\r \r
\r
\r\r\r
\r
\r
\r+\r
\r
\r\rNo selected file\r\r\r\rSpoiler\r\\uf0c1\rPost from URL\r+\rDump\r\\uf00d\rRemove File\r\r\r\r
\r\r
\r\r\r\r" - }); setNode = function(name, query) { return nodes[name] = $(query, dialog); }; @@ -14126,6 +14122,7 @@ this.status = $('#watcher-status', this.dialog); this.list = this.dialog.lastElementChild; this.refreshButton = $('.refresh', this.dialog); + this.unreaddb = Unread.db || new DataBoard('lastReadPosts'); $.on(d, 'QRPostSuccessful', this.cb.post); if (g.VIEW === 'thread') { $.on(d, 'ThreadUpdate', this.cb.threadUpdate); diff --git a/src/General/Build.coffee b/src/General/Build.coffee index 3175af075..775fd3067 100755 --- a/src/General/Build.coffee +++ b/src/General/Build.coffee @@ -181,14 +181,14 @@ Build = ### File Info ### - if file?.isDeleted - fileCont = <%= html( + fileCont = if file?.isDeleted + <%= html( '' + 'File deleted.' + '' ) %> else if file and boardID is 'f' - fileCont = <%= html( + <%= html( '
' + 'File: ${file.name}' + '-(${$.bytesToString(file.size)}, ${file.width}x${file.height}, ${file.tag})' + diff --git a/src/General/Header.coffee b/src/General/Header.coffee index 56c47d6f3..07a01d55f 100644 --- a/src/General/Header.coffee +++ b/src/General/Header.coffee @@ -6,7 +6,7 @@ Header = className: 'menu-button a-icon' id: 'main-menu' - box = UI.checkbox.bind UI + box = UI.checkbox barFixedToggler = box 'Fixed Header', 'Fixed Header' headerToggler = box 'Header auto-hide', ' Auto-hide header' diff --git a/src/General/UI.coffee b/src/General/UI.coffee index 7e7af73cf..877632bcb 100755 --- a/src/General/UI.coffee +++ b/src/General/UI.coffee @@ -1,15 +1,19 @@ UI = do -> - dialog = (id, position, html) -> + dialog = (id, position, properties) -> el = $.el 'div', className: 'dialog' - innerHTML: html id: id + $.extend el, properties el.style.cssText = position $.get "#{id}.position", position, (item) -> el.style.cssText = item["#{id}.position"] move = $ '.move', el $.on move, 'touchstart mousedown', dragstart + for child in move.children + continue unless child.tagName + $.on child, 'touchstart mousedown', (e) -> + e.stopPropagation() el @@ -17,7 +21,12 @@ UI = do -> currentMenu = null lastToggledButton = null - constructor: -> + constructor: (@type) -> + # XXX AddMenuEntry event is deprecated + $.on d, 'AddMenuEntry', ({detail}) => + return if detail.type isnt @type + delete detail.open + @addEntry detail @entries = [] makeMenu: -> @@ -47,7 +56,6 @@ UI = do -> menu = @makeMenu() currentMenu = menu lastToggledButton = button - $.addClass button, 'open' @entries.sort (first, second) -> first.order - second.order @@ -95,9 +103,7 @@ UI = do -> insertEntry: (entry, parent, data) -> if typeof entry.open is 'function' - return unless entry.open data, (subEntry) => - @parseEntry subEntry - entry.subEntries.push subEntry + return unless entry.open data $.add parent, entry.el return unless entry.subEntries @@ -113,7 +119,7 @@ UI = do -> close: => $.rm currentMenu - $.rmClass lastToggledButton, 'open' + $.rmClass lastToggledButton, 'active' currentMenu = null lastToggledButton = null $.off d, 'click CloseMenu', @close @@ -184,7 +190,7 @@ UI = do -> style.left = left style.right = right - addEntry: (entry) -> + addEntry: (entry) => @parseEntry entry @entries.push entry @@ -199,7 +205,6 @@ UI = do -> el.style.order = entry.order or 100 return unless subEntries $.addClass el, 'has-submenu' - $.addClass el, 'pfa' for subEntry in subEntries @parseEntry subEntry return @@ -209,7 +214,7 @@ UI = do -> # prevent text selection e.preventDefault() if isTouching = e.type is 'touchstart' - [..., e] = e.changedTouches + e = e.changedTouches[e.changedTouches.length - 1] # distance from pointer to el edge is constant; calculate it here. el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @ rect = el.getBoundingClientRect() @@ -302,76 +307,80 @@ UI = do -> $.off d, 'mouseup', @up $.set "#{@id}.position", @style.cssText - hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY, noRemove}) -> + hoverstart = ({root, el, latestEvent, endEvents, asapTest, height, cb, noRemove}) -> o = { root el style: el.style + isImage: el.nodeName in ['IMG', 'VIDEO'] cb - close: close endEvents latestEvent clientHeight: doc.clientHeight clientWidth: doc.clientWidth - offsetX: offsetX or 45 - offsetY: offsetY or -120 noRemove } o.hover = hover.bind o o.hoverend = hoverend.bind o - if asapTest - $.asap -> - !el.parentNode or asapTest() - , -> - o.hover o.latestEvent if el.parentNode + $.asap -> + !el.parentNode or asapTest() + , -> + o.hover o.latestEvent if el.parentNode $.on root, endEvents, o.hoverend if $.x 'ancestor::div[contains(@class,"inline")][1]', root $.on d, 'keydown', o.hoverend $.on root, 'mousemove', o.hover <% if (type === 'userscript') { %> - # Workaround for https://github.com/MayhemYDG/4chan-x/issues/377 + # Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 o.workaround = (e) -> o.hoverend(e) unless root.contains e.target $.on doc, 'mousemove', o.workaround <% } %> hover = (e) -> @latestEvent = e - height = @el.offsetHeight + 25 + height = @height or @el.offsetHeight {clientX, clientY} = e - - top = clientY + @offsetY - top = if @clientHeight <= height or top <= 0 - 0 - else if top + height >= @clientHeight - @clientHeight - height + top = if @isImage + Math.max 0, clientY * (@clientHeight - height) / @clientHeight else - top + Math.max 0, Math.min(@clientHeight - height, clientY - 120) - [left, right] = if clientX <= @clientWidth / 2 - [clientX + @offsetX + 'px', null] + threshold = @clientWidth / 2 + threshold = Math.max threshold, @clientWidth - 400 unless @isImage + [left, right] = if clientX <= threshold + [clientX + 45 + 'px', null] else - [null, @clientWidth - clientX + @offsetX + 'px'] + [null, @clientWidth - clientX + 45 + 'px'] - @style.top = top + 'px' - @style.left = left - @style.right = right + {style} = @ + style.top = top + 'px' + style.left = left + style.right = right hoverend = (e) -> return if e.type is 'keydown' and e.keyCode isnt 13 or e.target.nodeName is "TEXTAREA" - $.rm @el + $.rm @el unless @noRemove $.off @root, @endEvents, @hoverend $.off d, 'keydown', @hoverend $.off @root, 'mousemove', @hover <% if (type === 'userscript') { %> - # Workaround for https://github.com/MayhemYDG/4chan-x/issues/377 + # Workaround for https://bugzilla.mozilla.org/show_bug.cgi?id=674955 $.off doc, 'mousemove', @workaround <% } %> @cb.call @ if @cb + checkbox = (name, text, checked) -> + checked = Conf[name] unless checked? + label = $.el 'label' + input = $.el 'input', {type: 'checkbox', name, checked} + $.add label, [input, $.tn text] + label + return { - dialog: dialog - Menu: Menu - hover: hoverstart + dialog: dialog + Menu: Menu + hover: hoverstart + checkbox: checkbox } diff --git a/src/General/eventPage/eventPage.coffee b/src/General/eventPage/eventPage.coffee new file mode 100644 index 000000000..e5a107dc0 --- /dev/null +++ b/src/General/eventPage/eventPage.coffee @@ -0,0 +1,28 @@ +requestID = 0 + +chrome.runtime.onMessage.addListener (request, sender, sendResponse) -> + id = requestID + requestID++ + sendResponse id + + xhr = new XMLHttpRequest() + xhr.open 'GET', request.url, true + xhr.responseType = request.responseType + xhr.addEventListener 'load', -> + if @readyState is @DONE && xhr.status is 200 + contentType = @getResponseHeader 'Content-Type' + contentDisposition = @getResponseHeader 'Content-Disposition' + {response} = @ + if request.responseType is 'arraybuffer' + response = [new Uint8Array(response)...] + chrome.tabs.sendMessage sender.tab.id, {id, response, contentType, contentDisposition} + else + chrome.tabs.sendMessage sender.tab.id, {id, error: true} + , false + xhr.addEventListener 'error', -> + chrome.tabs.sendMessage sender.tab.id, {id, error: true} + , false + xhr.addEventListener 'abort', -> + chrome.tabs.sendMessage sender.tab.id, {id, error: true} + , false + xhr.send() diff --git a/src/General/lib/databoard.class b/src/General/lib/databoard.class index a93e90e1a..ec0c31099 100755 --- a/src/General/lib/databoard.class +++ b/src/General/lib/databoard.class @@ -16,10 +16,13 @@ class DataBoard save: -> $.set @key, @data delete: ({boardID, threadID, postID}) -> + $.forceSync @key if postID + return unless @data.boards[boardID]?[threadID] delete @data.boards[boardID][threadID][postID] @deleteIfEmpty {boardID, threadID} else if threadID + return unless @data.boards[boardID] delete @data.boards[boardID][threadID] @deleteIfEmpty {boardID} else @@ -27,6 +30,7 @@ class DataBoard @save() deleteIfEmpty: ({boardID, threadID}) -> + $.forceSync @key if threadID unless Object.keys(@data.boards[boardID][threadID]).length delete @data.boards[boardID][threadID] @@ -35,6 +39,7 @@ class DataBoard delete @data.boards[boardID] set: ({boardID, threadID, postID, val}) -> + $.forceSync @key if postID isnt undefined ((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val else if threadID isnt undefined @@ -60,39 +65,28 @@ class DataBoard thread val or defaultValue + forceSync: -> + $.forceSync @key clean: -> - now = Date.now() - return if (@data.lastChecked or 0) > now - 2 * $.HOUR - - # Don't even bother writing unless we have data to write - keys = Object.keys @data.boards - return unless keys.length - - # Since we generated the keys anyways, use them to avoid the slow ``Object::hasOwnProperty`` method - # Not that ``Object.keys`` is fast - for boardID in keys + $.forceSync @key + for boardID, val of @data.boards @deleteIfEmpty {boardID} - @ajaxClean boardID if boardID of @data.boards - @data.lastChecked = now + now = Date.now() + if (@data.lastChecked or 0) < now - 2 * $.HOUR + @data.lastChecked = now + for boardID of @data.boards + for threadID of @data.boards[boardID] + @ajaxClean boardID, threadID @save() - ajaxClean: (boardID) -> - $.cache "//a.4cdn.org/#{boardID}/threads.json", (e) => - if e.target.status isnt 200 - @delete {boardID} if e.target.status is 404 - return - board = @data.boards[boardID] - threads = {} - for page in e.target.response - for thread in page.threads when thread.no of board - threads[thread.no] = board[thread.no] - count = Object.keys(threads).length - return if count is Object.keys(board).length # Nothing changed. - if count - @set {boardID, val: threads} - else - @delete {boardID} + ajaxClean: (boardID, threadID) -> + $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json", + onloadend: (e) => + if e.target.status is 404 + @delete {boardID, threadID} + , + type: 'head' onSync: (data) => @data = data or boards: {} diff --git a/src/General/lib/notice.class b/src/General/lib/notice.class index c2545ce39..4eae06879 100644 --- a/src/General/lib/notice.class +++ b/src/General/lib/notice.class @@ -19,7 +19,7 @@ class Notice $.on d, 'visibilitychange', @add return $.off d, 'visibilitychange', @add - $.add Header.noticesRoot, @el + $.add doc, @el @el.clientHeight # force reflow @el.style.opacity = 1 setTimeout @close, @timeout * $.SECOND if @timeout diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index da3e10425..b95a7b5a9 100755 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -8,6 +8,7 @@ ThreadWatcher = @status = $ '#watcher-status', @dialog @list = @dialog.lastElementChild @refreshButton = $ '.refresh', @dialog + @unreaddb = Unread.db or new DataBoard 'lastReadPosts' $.on d, 'QRPostSuccessful', @cb.post $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread' diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index e4e1783b7..1ae45e1f7 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -387,7 +387,7 @@ QR = dialog: -> QR.nodes = nodes = el: dialog = UI.dialog 'qr', 'top:0;right:0;', - $.extend dialog, <%= importHTML('Features/QuickReply') %> + <%= importHTML('Features/QuickReply') %> setNode = (name, query) -> nodes[name] = $ query, dialog