diff --git a/4chan_x.user.js b/4chan_x.user.js index 3c38f41f7..17a38ce88 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -8,11 +8,16 @@ // @match *://boards.4chan.org/* // @match *://images.4chan.org/* // @match *://sys.4chan.org/* +// @match *://api.4chan.org/* // @match *://*.foolz.us/api/* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// @grant GM_openInTab // @run-at document-start // @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js // @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @icon http://mayhemydg.github.com/4chan-x/favicon.gif +// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7 // ==/UserScript== /* LICENSE @@ -81,6 +86,7 @@ Config = { main: { Enhancing: { + 'Disable 4chan\'s extension': [true, 'Avoid conflicts between 4chan X and 4chan\'s inline extension.'], '404 Redirect': [true, 'Redirect dead threads and images.'], 'Keybinds': [true, 'Bind actions to keyboard shortcuts.'], 'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'], @@ -937,6 +943,9 @@ return (_ref2 = $.id('boardNavDesktopFoot')) != null ? _ref2.hidden = true : void 0; }, initFeatures: function() { + if (Conf['Disable 4chan\'s extension']) { + localStorage.setItem('4chan-settings', '{"disableAll":true}'); + } if (Conf['Resurrect Quotes']) { try { Quotify.init(); @@ -1086,7 +1095,7 @@ return $.on(d, 'DOMNodeInserted', Main.addStyle); } }, - css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n z-index: 1;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* quote */\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n position: fixed;\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n outline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n position: fixed;\n padding-bottom: 16px;\n}" + css: "/* general */\n.dialog.reply {\n display: block;\n border: 1px solid rgba(0, 0, 0, .25);\n padding: 0;\n}\n.move {\n cursor: move;\n}\nlabel {\n cursor: pointer;\n}\na[href=\"javascript:;\"] {\n text-decoration: none;\n}\n.warning {\n color: red;\n}\n\n/* 4chan style fixes */\n.opContainer, .op {\n display: block !important;\n}\n.post {\n overflow: visible !important;\n}\n\n/* header */\nbody.fourchan_x {\n margin-top: 2.5em;\n}\n#boardNavDesktop.reply {\n border-width: 0 0 1px;\n padding: 4px;\n position: fixed;\n top: 0;\n right: 0;\n left: 0;\n transition: opacity .1s ease-in-out;\n -o-transition: opacity .1s ease-in-out;\n -moz-transition: opacity .1s ease-in-out;\n -webkit-transition: opacity .1s ease-in-out;\n z-index: 1;\n}\n#boardNavDesktop.reply:not(:hover) {\n opacity: .4;\n transition: opacity 1.5s .5s ease-in-out;\n -o-transition: opacity 1.5s .5s ease-in-out;\n -moz-transition: opacity 1.5s .5s ease-in-out;\n -webkit-transition: opacity 1.5s .5s ease-in-out;\n}\n#boardNavDesktop.reply a {\n margin: -1px;\n}\n#settings {\n float: right;\n}\n\n/* quote */\n.quotelink.deadlink {\n text-decoration: underline !important;\n}\n.inlined {\n opacity: .5;\n}\n#qp input, .forwarded {\n display: none;\n}\n.quotelink.forwardlink,\n.backlink.forwardlink {\n text-decoration: none;\n border-bottom: 1px dashed;\n}\n.inline {\n border: 1px solid rgba(128, 128, 128, .5);\n display: table;\n margin: 2px 0;\n}\n.inline .post {\n border: 0 !important;\n display: table !important;\n margin: 0 !important;\n padding: 1px 2px !important;\n}\n#qp {\n position: fixed;\n padding: 2px 2px 5px;\n}\n#qp .post {\n border: none;\n margin: 0;\n padding: 0;\n}\n#qp img {\n max-height: 300px;\n max-width: 500px;\n}\n.qphl {\n outline: 2px solid rgba(216, 94, 49, .7);\n}\n\n/* file */\n.fileText:hover .fntrunc,\n.fileText:not(:hover) .fnfull {\n display: none;\n}\n#ihover {\n box-sizing: border-box;\n -moz-box-sizing: border-box;\n max-height: 100%;\n max-width: 75%;\n position: fixed;\n padding-bottom: 16px;\n}" }; Redirect = { @@ -1230,6 +1239,7 @@ }; Build = { + spoilerRange: {}, shortFilename: function(filename, isReply) { var threshold; threshold = isReply ? 30 : 40; @@ -1239,120 +1249,139 @@ return filename; } }, - post: function(o) { - var board, bq, capcode, comment, container, date, dateUTC, email, file, fl, flag, flagTitle, html, isOP, name, pi, post, postID, subject, threadID, tripcode, uniqueID; - postID = o.postID, threadID = o.threadID, board = o.board, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flag = o.flag, flagTitle = o.flagTitle, date = o.date, dateUTC = o.dateUTC, comment = o.comment, file = o.file; + postFromObject: function(data, board) { + var o; + o = { + postID: data.no, + threadID: data.resto || data.no, + board: board, + name: data.name, + capcode: data.capcode, + tripcode: data.trip, + uniqueID: data.id, + email: data.email ? encodeURIComponent(data.email) : '', + subject: data.sub, + flagCode: data.country, + flagName: data.country_name, + date: data.now, + dateUTC: data.time, + comment: data.com, + isSticky: !!data.sticky, + isClosed: !!data.closed + }; + if (data.ext || data.filedeleted) { + o.file = { + name: data.filename + data.ext, + timestamp: "" + data.tim + data.ext, + url: "//images.4chan.org/" + board + "/src/" + data.tim + data.ext, + height: data.h, + width: data.w, + MD5: data.md5, + size: data.fsize, + turl: "//thumbs.4chan.org/" + board + "/thumb/" + data.tim + "s.jpg", + theight: data.tn_h, + twidth: data.tn_w, + isSpoiler: !!data.spoiler, + isDeleted: !!data.filedeleted + }; + } + return Build.post(o); + }, + post: function(o, isArchived) { + /* + This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS). + @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE + */ + + var a, board, capcode, capcodeClass, capcodeStart, closed, comment, container, date, dateUTC, email, emailEnd, emailStart, ext, file, fileDims, fileHTML, fileInfo, fileSize, fileThumb, filename, flag, flagCode, flagName, href, imgSrc, isClosed, isOP, isSticky, name, postID, quote, shortFilename, spoilerRange, staticPath, sticky, subject, threadID, tripcode, uniqueID, userID, _i, _len, _ref; + postID = o.postID, threadID = o.threadID, board = o.board, name = o.name, capcode = o.capcode, tripcode = o.tripcode, uniqueID = o.uniqueID, email = o.email, subject = o.subject, flagCode = o.flagCode, flagName = o.flagName, date = o.date, dateUTC = o.dateUTC, isSticky = o.isSticky, isClosed = o.isClosed, comment = o.comment, file = o.file; isOP = postID === threadID; - html = []; - html.push(" "); - html.push("" + subject + " "); - html.push(""); + staticPath = '//static.4chan.org'; if (email) { - html.push(""); - } - html.push("" + name + ""); - if (tripcode) { - html.push(" " + tripcode + ""); - } - if (uniqueID) { - html.push(" (ID: " + uniqueID + ")"); + emailStart = ''; + emailEnd = ''; + } else { + emailStart = ''; + emailEnd = ''; } + subject = subject ? "" + subject + "" : ''; + userID = !capcode && uniqueID ? (" (ID: ") + ("" + uniqueID + ") ") : ''; switch (capcode) { - case 'M': - html.push(' ## Mod'); - html.push(' This user is a 4chan Moderator.'); + case 'admin': + case 'admin_highlight': + capcodeClass = " capcodeAdmin"; + capcodeStart = " ## Admin"; + capcode = (" "; break; - case 'A': - html.push(' ## Admin'); - html.push(' This user is the 4chan Administrator.'); + case 'mod': + capcodeClass = " capcodeMod"; + capcodeStart = " ## Moderator"; + capcode = (" "; break; - case 'D': - html.push(' ## Mod'); - html.push(' This user is a 4chan Developer.'); + case 'developer': + capcodeClass = " capcodeDeveloper"; + capcodeStart = " ## Developer"; + capcode = (" "; + break; + default: + capcodeClass = ''; + capcodeStart = ''; + capcode = ''; } - if (email) { - html.push(''); - } - if (flag) { - html.push("  + flag + "); - } - html.push(' '); - html.push("" + date + " "); - html.push(''); - html.push("No."); - html.push("" + postID + ""); - html.push(''); - pi = $.el('div', { - id: "pi" + postID, - className: 'postInfo desktop', - innerHTML: html.join('') - }); - bq = $.el('blockquote', { - id: "m" + postID, - className: 'postMessage', - innerHTML: comment - }); - if (file.name) { - html = []; - html.push('
'); - html.push("File: "); - html.push("" + file.origin + ""); - html.push('-('); + flag = flagCode ? ("  + flagCode + ") : ''; + if (file != null ? file.isDeleted : void 0) { + fileHTML = isOP ? ("
") + ("File deleted.") + "
" : ("
") + ("File deleted.") + "
"; + } else if (file) { + ext = file.name.slice(-3); + if (!file.twidth && !file.theight && ext === 'gif') { + file.twidth = file.width; + file.theight = file.height; + } + fileSize = $.bytesToString(file.size); + fileThumb = file.turl; if (file.isSpoiler) { - html.push('Spoiler Image, '); + fileSize = "Spoiler Image, " + fileSize; + if (!isArchived) { + fileThumb = '//static.4chan.org/image/spoiler'; + if (spoilerRange = Build.spoilerRange[board]) { + fileThumb += ("-" + board) + Math.floor(1 + spoilerRange * Math.random()); + } + fileThumb += '.png'; + file.twidth = file.theight = 100; + } } - html.push("" + ($.bytesToString(file.size)) + ", "); - html.push(/\.pdf$/i.test(file.name) ? "PDF" : "" + file.width + "x" + file.height); - if (!file.isSpoiler) { - html.push(", " + (Build.shortFilename(file.name)) + ""); - } - html.push(")
"); - html.push(""); - html.push("" + (file.isSpoiler ? "); - html.push(''); - fl = $.el('div', { - id: "f" + postID, - className: 'file', - innerHTML: html.join('') + imgSrc = ("") + ("" + fileSize + ""); + a = $.el('a', { + innerHTML: file.name }); + filename = a.textContent.replace(/%22/g, '"'); + a.textContent = Build.shortFilename(filename); + shortFilename = a.innerHTML; + a.textContent = filename; + filename = a.innerHTML.replace(/'/g, '''); + fileDims = ext === 'pdf' ? 'PDF' : "" + file.width + "x" + file.height; + fileInfo = ("File: " + file.timestamp + "") + ("-(" + fileSize + ", " + fileDims + (file.isSpoiler ? '' : ", " + shortFilename + "")) + ")"; + fileHTML = "
" + fileInfo + "
" + imgSrc + "
"; + } else { + fileHTML = ''; } - post = $.el('div', { - id: "p" + postID, - className: "post " + (isOP ? 'op' : 'reply') - }); - if (fl && isOP) { - $.add(post, fl); - } - $.add(post, pi); - if (fl && !isOP) { - $.add(post, fl); - } - $.add(post, bq); + tripcode = tripcode ? " " + tripcode + "" : ''; + sticky = isSticky ? ' Sticky' : ''; + closed = isClosed ? ' Closed' : ''; container = $.el('div', { id: "pc" + postID, - className: "postContainer " + (isOP ? 'op' : 'reply') + "Container" + className: "postContainer " + (isOP ? 'op' : 'reply') + "Container", + innerHTML: (isOP ? '' : "
>>
") + ("
") + ("
") + ("") + ("" + (name || '') + "") + tripcode + capcodeStart + capcode + userID + flag + sticky + closed + ("
" + subject) + ("
" + date) + '
' + ("No.") + ("" + postID + "") + '
' + '
' + (isOP ? fileHTML : '') + ("
") + (" ") + ("" + subject + " ") + ("") + emailStart + ("" + (name || '') + "") + tripcode + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed + ' ' + ("" + date + " ") + "" + ("No.") + ("" + postID + "") + '' + '
' + (isOP ? '' : fileHTML) + ("
" + (comment || '') + "
") + '
' }); - if (!isOP) { - $.add(container, $.el('div', { - id: "sa" + postID, - className: 'sideArrows', - textContent: '>>' - })); + _ref = $$('.quotelink', container); + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + quote = _ref[_i]; + href = quote.getAttribute('href'); + if (href[0] === '/') { + continue; + } + quote.href = "/" + board + "/res/" + href; } - $.add(container, post); return container; } }; @@ -1397,7 +1426,7 @@ } root.textContent = "Loading post No." + postID + "..."; if (threadID) { - return $.cache("/" + board + "/res/" + threadID, function() { + return $.cache("//api.4chan.org/" + board + "/res/" + threadID + ".json", function() { return Get.fetchedPost(this, board, threadID, postID, root, context); }); } else if (url = Redirect.post(board, postID)) { @@ -1420,7 +1449,7 @@ return $.add(root, nodes.root); }, fetchedPost: function(req, board, threadID, postID, root, context) { - var doc, href, link, pc, post, quote, status, thread, url, _i, _len, _ref; + var post, posts, spoilerRange, status, thread, url, _i, _len; if (post = g.posts["" + board + "." + postID]) { Get.insert(post, root, context); return; @@ -1429,48 +1458,44 @@ if (status !== 200) { if (url = Redirect.post(board, postID)) { $.cache(url, function() { - return Get.archivedPost(this, board, postID, root); + return Get.archivedPost(this, board, postID, root, context); }); } else { $.addClass(root, 'warning'); - root.textContent = status === 404 ? "Thread No." + threadID + " has not been found." : "Error " + req.status + ": " + req.statusText + "."; + root.textContent = status === 404 ? "Thread No." + threadID + " 404'd." : "Error " + req.status + ": " + req.statusText + "."; } return; } - doc = d.implementation.createHTMLDocument(''); - doc.documentElement.innerHTML = req.response; - if (!(pc = doc.getElementById("pc" + postID))) { - if (url = Redirect.post(board, postID)) { - $.cache(url, function() { - return Get.archivedPost(this, board, postID, root); - }); - } else { - $.addClass(root, 'warning'); - root.textContent = "Post No." + postID + " has not been found."; - } - return; + posts = JSON.parse(req.response).posts; + if (spoilerRange = posts[0].custom_spoiler) { + Build.spoilerRange[board] = spoilerRange; } - pc = d.importNode(pc, true); - _ref = $$('.quotelink', pc); - for (_i = 0, _len = _ref.length; _i < _len; _i++) { - quote = _ref[_i]; - href = quote.getAttribute('href'); - if (href[0] === '/') { - continue; + postID = +postID; + for (_i = 0, _len = posts.length; _i < _len; _i++) { + post = posts[_i]; + if (post.no === postID) { + break; + } + if (post.no > postID) { + if (url = Redirect.post(board, postID)) { + $.cache(url, function() { + return Get.parseArchivedPost(this, board, postID, root, context); + }); + } else { + $.addClass(root, 'warning'); + root.textContent = "Post No." + postID + " was not found."; + } + return; } - quote.href = "/" + board + "/res/" + href; } - link = $('a[title="Highlight this post"]', pc); - link.href = "/" + board + "/res/" + threadID + "#p" + postID; - link.nextSibling.href = "/" + board + "/res/" + threadID + "#q" + postID; board = g.boards[board] || new Board(board); thread = g.threads["" + board + "." + threadID] || new Thread(threadID, board); - post = new Post(pc, thread, board); + post = new Post(Build.postFromObject(post, board), thread, board); Main.callbackNodes(Post, [post]); return Get.insert(post, root, context); }, archivedPost: function(req, board, postID, root, context) { - var bq, comment, data, post, postContainer, thread, threadID; + var bq, comment, data, o, post, thread, threadID; if (post = g.posts["" + board + "." + postID]) { Get.insert(post, root, context); return; @@ -1512,37 +1537,49 @@ }); comment = bq.innerHTML.replace(/(^|>)(>[^<$]+)(<|$)/g, '$1$2$3'); threadID = data.thread_num; - postContainer = Build.post({ + o = { postID: postID, threadID: threadID, board: board, - name: data.name, - capcode: data.capcode, + name: data.name_processed, + capcode: (function() { + switch (data.capcode) { + case 'M': + return 'mod'; + case 'A': + return 'admin'; + case 'D': + return 'developer'; + } + })(), tripcode: data.trip, uniqueID: data.poster_hash, - email: data.email, - subject: data.title, - flag: data.poster_country, + email: encodeURIComponent(data.email), + subject: data.title_processed, + flagCode: data.poster_country, + flagName: data.poster_country_name_processed, date: data.fourchan_date, dateUTC: data.timestamp, - comment: comment, - file: { - name: data.media_filename, - origin: data.media_orig, + comment: comment + }; + if (data.media_filename) { + o.file = { + name: data.media_filename_processed, + timestamp: data.media_orig, url: data.media_link || data.remote_media_link, height: data.media_h, width: data.media_w, - isSpoiler: data.spoiler === '1', MD5: data.media_hash, size: data.media_size, turl: data.thumb_link || ("//thumbs.4chan.org/" + board + "/thumb/" + data.preview_orig), theight: data.preview_h, - twidth: data.preview_w - } - }); + twidth: data.preview_w, + isSpoiler: data.spoiler === '1' + }; + } board = g.boards[board] || new Board(board); thread = g.threads["" + board + "." + threadID] || new Thread(threadID, board); - post = new Post(postContainer, thread, board, { + post = new Post(Build.post(o, true), thread, board, { isArchived: true }); Main.callbackNodes(Post, [post]); diff --git a/Cakefile b/Cakefile index 68a89ad75..a87b4cb0b 100644 --- a/Cakefile +++ b/Cakefile @@ -19,11 +19,16 @@ HEADER = """ // @match *://boards.4chan.org/* // @match *://images.4chan.org/* // @match *://sys.4chan.org/* +// @match *://api.4chan.org/* // @match *://*.foolz.us/api/* +// @grant GM_getValue +// @grant GM_setValue +// @grant GM_deleteValue +// @grant GM_openInTab // @run-at document-start // @updateURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js // @downloadURL https://github.com/MayhemYDG/4chan-x/raw/stable/4chan_x.user.js -// @icon http://mayhemydg.github.com/4chan-x/favicon.gif +// @icon data:image/gif;base64,R0lGODlhEAAQAKECAAAAAGbMM////////yH5BAEKAAIALAAAAAAQABAAAAIxlI+pq+D9DAgUoFkPDlbs7lGiI2bSVnKglnJMOL6omczxVZK3dH/41AG6Lh7i6qUoAAA7 // ==/UserScript== /* LICENSE diff --git a/changelog b/changelog index e17a90f9b..77e861dc3 100644 --- a/changelog +++ b/changelog @@ -4,6 +4,22 @@ alpha Fix Quote Backlinks not affecting inlined quotes. Fix Quote Highlighting not affecting inlined quotes. +master +- Mayhem + Use 4chan's API to fetch posts for: + - Thread Updater. + - Quote Inlining. + - Quote Previewing. + - Thread Expansion. + - Comment Expansion. + This will make fetching faster, and reduce bandwidth usage. + Add an option to disable 4chan's inline extension. Enabled by default. + Fix compatibility with Scriptish's auto-udpater. + +2.34.10 +- Mayhem + Fix 4chan X. Blame moot. + 2.34.9 - Mayhem Add /g/, /k/, /w/, /an/, /cgl/, /ck/, /lit/, /toy/ and /x/ archived image redirection. diff --git a/latest.js b/latest.js index 0335ecfb8..d90eff7b7 100644 --- a/latest.js +++ b/latest.js @@ -1 +1 @@ -postMessage({version:'2.34.9'},'*') \ No newline at end of file +postMessage({version:'2.34.10'},'*') \ No newline at end of file diff --git a/script.coffee b/script.coffee index 430d97430..20b574f44 100644 --- a/script.coffee +++ b/script.coffee @@ -1,6 +1,7 @@ Config = main: Enhancing: + 'Disable 4chan\'s extension': [true, 'Avoid conflicts between 4chan X and 4chan\'s inline extension.'] '404 Redirect': [true, 'Redirect dead threads and images.'] 'Keybinds': [true, 'Bind actions to keyboard shortcuts.'] 'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'] @@ -741,6 +742,9 @@ Main = $.id('boardNavDesktopFoot')?.hidden = true initFeatures: -> + if Conf['Disable 4chan\'s extension'] + localStorage.setItem '4chan-settings', '{"disableAll":true}' + if Conf['Resurrect Quotes'] try Quotify.init() @@ -935,6 +939,9 @@ body.fourchan_x { } /* quote */ +.quotelink.deadlink { + text-decoration: underline !important; +} .inlined { opacity: .5; } @@ -1060,6 +1067,7 @@ Redirect = url or '' Build = + spoilerRange: {} shortFilename: (filename, isReply) -> # FILENAME SHORTENING SCIENCE: # OPs have a +10 characters threshold. @@ -1069,117 +1077,249 @@ Build = "#{filename[...threshold - 5]}(...).#{filename[-3..]}" else filename - post: (o) -> + postFromObject: (data, board) -> + o = + # id + postID: data.no + threadID: data.resto or data.no + board: board + # info + name: data.name + capcode: data.capcode + tripcode: data.trip + uniqueID: data.id + email: if data.email then encodeURIComponent data.email else '' + subject: data.sub + flagCode: data.country + flagName: data.country_name + date: data.now + dateUTC: data.time + comment: data.com + # thread status + isSticky: !!data.sticky + isClosed: !!data.closed + # file + if data.ext or data.filedeleted + o.file = + name: data.filename + data.ext + timestamp: "#{data.tim}#{data.ext}" + url: "//images.4chan.org/#{board}/src/#{data.tim}#{data.ext}" + height: data.h + width: data.w + MD5: data.md5 + size: data.fsize + turl: "//thumbs.4chan.org/#{board}/thumb/#{data.tim}s.jpg" + theight: data.tn_h + twidth: data.tn_w + isSpoiler: !!data.spoiler + isDeleted: !!data.filedeleted + Build.post o + post: (o, isArchived) -> + ### + This function contains code from 4chan-JS (https://github.com/4chan/4chan-JS). + @license: https://github.com/4chan/4chan-JS/blob/master/LICENSE + ### { postID, threadID, board - name, capcode, tripcode, uniqueID, email, subject, flag, flagTitle, date, dateUTC, comment + name, capcode, tripcode, uniqueID, email, subject, flagCode, flagName, date, dateUTC + isSticky, isClosed + comment file } = o isOP = postID is threadID - # post info - html = [] - # input - html.push " " - # subject - html.push "#{subject} " - # name block - html.push "" - # mail start - html.push "" if email - # name - html.push "#{name}" - # tripcode - html.push " #{tripcode}" if tripcode - # user id - html.push " (ID: #{uniqueID})" if uniqueID - # capcode - switch capcode - when 'M' - html.push ' ## Mod' - html.push ' This user is a 4chan Moderator.' - when 'A' - html.push ' ## Admin' - html.push ' This user is the 4chan Administrator.' - when 'D' - html.push ' ## Mod' - html.push ' This user is a 4chan Developer.' - # mail end - html.push '' if email - # flag - # XXX what about troll flags in /pol/? - html.push " #{flag}" if flag - # end name block - html.push ' ' - # date - html.push "#{date} " - # post num - html.push '' - html.push "No." - html.push "' + emailEnd = '' + else + emailStart = '' + emailEnd = '' + + subject = + if subject + "#{subject}" else - "/#{board}/res/#{threadID}#q#{postID}" - }\" title='Quote this post'>#{postID}" - # XXX closed/sticky? - html.push '' - pi = $.el 'div', - id: "pi#{postID}" - className: 'postInfo desktop' - innerHTML: html.join '' + '' - bq = $.el 'blockquote', - id: "m#{postID}" - className: 'postMessage' - innerHTML: comment + userID = + if !capcode and uniqueID + " (ID: " + + "#{uniqueID}) " + else + '' - # file - if file.name # XXX need to fix support of Flash - html = [] - html.push '
' - html.push "File: " - html.push "#{file.origin}" - html.push '-(' - html.push 'Spoiler Image, ' if file.isSpoiler - html.push "#{$.bytesToString file.size}, " - html.push if /\.pdf$/i.test file.name - "PDF" + switch capcode + when 'admin', 'admin_highlight' + capcodeClass = " capcodeAdmin" + capcodeStart = " ## Admin" + capcode = " " + when 'mod' + capcodeClass = " capcodeMod" + capcodeStart = " ## Moderator" + capcode = " " + when 'developer' + capcodeClass = " capcodeDeveloper" + capcodeStart = " ## Developer" + capcode = " " + else + capcodeClass = '' + capcodeStart = '' + capcode = '' + + flag = + if flagCode + " #{flagCode}" + else + '' + + if file?.isDeleted + fileHTML = + if isOP + "
" + + "File deleted." + + "
" else - "#{file.width}x#{file.height}" - html.push ", #{Build.shortFilename file.name}" unless file.isSpoiler - html.push ")
" - html.push "" - html.push "#{if file.isSpoiler then " - html.push '' - fl = $.el 'div', - id: "f#{postID}" - className: 'file' - innerHTML: html.join '' + "
" + + "File deleted." + + "
" + else if file + ext = file.name[-3..] + if !file.twidth and !file.theight and ext is 'gif' # wtf ? + file.twidth = file.width + file.theight = file.height - post = $.el 'div', - id: "p#{postID}" - className: "post #{if isOP then 'op' else 'reply'}" - $.add post, fl if fl and isOP - $.add post, pi - $.add post, fl if fl and !isOP - $.add post, bq + fileSize = $.bytesToString file.size + + fileThumb = file.turl + if file.isSpoiler + fileSize = "Spoiler Image, #{fileSize}" + unless isArchived + fileThumb = '//static.4chan.org/image/spoiler' + if spoilerRange = Build.spoilerRange[board] + # Randomize the spoiler image. + fileThumb += "-#{board}" + Math.floor 1 + spoilerRange * Math.random() + fileThumb += '.png' + file.twidth = file.theight = 100 + + imgSrc = "" + + "#{fileSize}" + + # Ha Ha filenames. + # html -> text, translate WebKit's %22s into "s + a = $.el 'a', innerHTML: file.name + filename = a.textContent.replace /%22/g, '"' + + # shorten filename, get html + a.textContent = Build.shortFilename filename + shortFilename = a.innerHTML + + # get html + a.textContent = filename + filename = a.innerHTML.replace /'/g, ''' + + fileDims = if ext is 'pdf' then 'PDF' else "#{file.width}x#{file.height}" + fileInfo = "File: #{file.timestamp}" + + "-(#{fileSize}, #{fileDims}#{ + if file.isSpoiler + '' + else + ", #{shortFilename}" + }" + ")" + + fileHTML = "
#{fileInfo}
#{imgSrc}
" + else + fileHTML = '' + + tripcode = + if tripcode + " #{tripcode}" + else + '' + + sticky = + if isSticky + ' Sticky' + else + '' + closed = + if isClosed + ' Closed' + else + '' container = $.el 'div', id: "pc#{postID}" className: "postContainer #{if isOP then 'op' else 'reply'}Container" - unless isOP - $.add container, $.el 'div', - id: "sa#{postID}" - className: 'sideArrows' - textContent: '>>' - $.add container, post + innerHTML: \ + (if isOP then '' else "
>>
") + + "
" + + + "
" + + "" + + "#{name or ''}" + tripcode + + capcodeStart + capcode + userID + flag + sticky + closed + + "
#{subject}" + + "
#{date}" + + '
' + + "No." + + "#{postID}" + + '
' + + '
' + + + (if isOP then fileHTML else '') + + + "
" + + " " + + "#{subject} " + + "" + + emailStart + + "#{name or ''}" + tripcode + + capcodeStart + emailEnd + capcode + userID + flag + sticky + closed + + ' ' + + "#{date} " + + "" + + "No." + + "#{postID}" + + '' + + '
' + + + (if isOP then '' else fileHTML) + + + "
#{comment or ''}
" + + + '
' + + for quote in $$ '.quotelink', container + href = quote.getAttribute 'href' + continue if href[0] is '/' # Cross-board quote, or board link + quote.href = "/#{board}/res/#{href}" # Fix pathnames container @@ -1213,7 +1353,7 @@ Get = root.textContent = "Loading post No.#{postID}..." if threadID - $.cache "/#{board}/res/#{threadID}", -> + $.cache "//api.4chan.org/#{board}/res/#{threadID}.json", -> Get.fetchedPost @, board, threadID, postID, root, context else if url = Redirect.post board, postID $.cache url, -> @@ -1242,43 +1382,37 @@ Get = # The thread can die by the time we check a quote. if url = Redirect.post board, postID $.cache url, -> - Get.archivedPost @, board, postID, root + Get.archivedPost @, board, postID, root, context else $.addClass root, 'warning' root.textContent = if status is 404 - "Thread No.#{threadID} has not been found." + "Thread No.#{threadID} 404'd." else "Error #{req.status}: #{req.statusText}." return - doc = d.implementation.createHTMLDocument '' - doc.documentElement.innerHTML = req.response - - unless pc = doc.getElementById "pc#{postID}" - # The post can be deleted by the time we check a quote. - if url = Redirect.post board, postID - $.cache url, -> - Get.archivedPost @, board, postID, root - else - $.addClass root, 'warning' - root.textContent = "Post No.#{postID} has not been found." - return - pc = d.importNode pc, true - - for quote in $$ '.quotelink', pc - href = quote.getAttribute 'href' - continue if href[0] is '/' # Cross-board quote, or board link - quote.href = "/#{board}/res/#{href}" # Fix pathnames - link = $ 'a[title="Highlight this post"]', pc - link.href = "/#{board}/res/#{threadID}#p#{postID}" - link.nextSibling.href = "/#{board}/res/#{threadID}#q#{postID}" + posts = JSON.parse(req.response).posts + if spoilerRange = posts[0].custom_spoiler + Build.spoilerRange[board] = spoilerRange + postID = +postID + for post in posts + break if post.no is postID # we found it! + if post.no > postID + # The post can be deleted by the time we check a quote. + if url = Redirect.post board, postID + $.cache url, -> + Get.parseArchivedPost @, board, postID, root, context + else + $.addClass root, 'warning' + root.textContent = "Post No.#{postID} was not found." + return board = g.boards[board] or new Board board thread = g.threads["#{board}.#{threadID}"] or new Thread threadID, board - post = new Post pc, thread, board + post = new Post Build.postFromObject(post, board), thread, board Main.callbackNodes Post, [post] Get.insert post, root, context archivedPost: (req, board, postID, root, context) -> @@ -1332,42 +1466,46 @@ Get = comment = bq.innerHTML.replace /(^|>)(>[^<$]+)(<|$)/g, '$1$2$3' threadID = data.thread_num - postContainer = Build.post + o = # id - postID: postID - threadID: threadID - board: board + postID: postID + threadID: threadID + board: board # info - name: data.name - capcode: data.capcode - tripcode: data.trip - uniqueID: data.poster_hash - email: data.email - subject: data.title - flag: data.poster_country - # XXX flagTitle: data.??? - date: data.fourchan_date - dateUTC: data.timestamp - comment: comment + name: data.name_processed + capcode: switch data.capcode + when 'M' then 'mod' + when 'A' then 'admin' + when 'D' then 'developer' + tripcode: data.trip + uniqueID: data.poster_hash + email: encodeURIComponent data.email + subject: data.title_processed + flagCode: data.poster_country + flagName: data.poster_country_name_processed + date: data.fourchan_date + dateUTC: data.timestamp + comment: comment # file - file: - name: data.media_filename - origin: data.media_orig + if data.media_filename + o.file = + name: data.media_filename_processed + timestamp: data.media_orig url: data.media_link or data.remote_media_link height: data.media_h width: data.media_w - isSpoiler: data.spoiler is '1' MD5: data.media_hash size: data.media_size turl: data.thumb_link or "//thumbs.4chan.org/#{board}/thumb/#{data.preview_orig}" theight: data.preview_h twidth: data.preview_w + isSpoiler: data.spoiler is '1' board = g.boards[board] or new Board board thread = g.threads["#{board}.#{threadID}"] or new Thread threadID, board - post = new Post postContainer, thread, board, + post = new Post Build.post(o, true), thread, board, isArchived: true Main.callbackNodes Post, [post] Get.insert post, root, context