From 966a07ca202d2b4f0c2d0d48497d2b6f23bd0a05 Mon Sep 17 00:00:00 2001 From: Zixaphir Date: Sat, 19 Oct 2013 19:41:49 -0700 Subject: [PATCH] Infinite Scrolling --- LICENSE | 2 +- builds/appchan-x.user.js | 188 +++++++++++++++++++++++++- builds/crx/script.js | 188 +++++++++++++++++++++++++- src/General/Config.coffee | 4 + src/General/Main.coffee | 7 +- src/Miscellaneous/InfiniScroll.coffee | 139 +++++++++++++++++++ 6 files changed, 515 insertions(+), 13 deletions(-) create mode 100644 src/Miscellaneous/InfiniScroll.coffee diff --git a/LICENSE b/LICENSE index d360a8692..b292df3a6 100644 --- a/LICENSE +++ b/LICENSE @@ -1,5 +1,5 @@ /* -* appchan x - Version 2.4.1 - 2013-10-17 +* appchan x - Version 2.4.1 - 2013-10-19 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE diff --git a/builds/appchan-x.user.js b/builds/appchan-x.user.js index 3a938d385..2819c8819 100644 --- a/builds/appchan-x.user.js +++ b/builds/appchan-x.user.js @@ -20,7 +20,7 @@ // ==/UserScript== /* -* appchan x - Version 2.4.1 - 2013-10-17 +* appchan x - Version 2.4.1 - 2013-10-19 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -108,7 +108,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __slice = [].slice, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }, @@ -133,7 +133,8 @@ 'Show Dice Roll': [true, 'Show dice that were entered into the email field.'], 'Color User IDs': [false, 'Assign unique colors to user IDs on boards that use them'], 'Remove Spoilers': [false, 'Remove all spoilers in text.'], - 'Reveal Spoilers': [false, 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'] + 'Reveal Spoilers': [false, 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'], + 'Infinite Scrolling': [false, 'Add new posts to the board index upon reaching the bottom of the board.'] }, 'Linkification': { 'Linkify': [true, 'Convert text into links where applicable.'], @@ -13789,6 +13790,184 @@ } }; + InfiniScroll = { + init: function() { + if (!(Conf['Infinite Scrolling'] && g.VIEW === 'index' && g.BOARD !== 'f')) { + return; + } + this.threads = g.threads; + return $.on(d, '4chanXInitFinished', this.ready); + }, + ready: function() { + $.off(d, '4chanXInitFinished', InfiniScroll.ready); + $.on(d, 'scroll', InfiniScroll.scroll); + return InfiniScroll.scroll(); + }, + scroll: $.debounce(100, function() { + var url; + + if (InfiniScroll.isFetching || (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight))) { + return; + } + if (InfiniScroll.isDead) { + return InfiniScroll.notice(); + } + if (InfiniScroll.cache && InfiniScroll.cache.time > Date.now() - $.MINUTE) { + return InfiniScroll.parse(InfiniScroll.cache); + } + new Notice('info', "Fetching next page.", 2); + InfiniScroll.isFetching = true; + url = "//api.4chan.org/" + g.BOARD + "/catalog.json"; + return $.ajax(url, { + onloadend: InfiniScroll.cb.load + }, { + whenModified: true + }); + }), + parse: function(response) { + var botPostForm, el, nodes, omitted_images, omitted_posts, op, post, postlink, posts, replylink, thread, threadID, threadNodes, threads, _i, _j, _len, _len1, _ref; + + threads = InfiniScroll.parsePages(response); + threadNodes = []; + nodes = []; + if (!threads.length) { + InfiniScroll.notice(); + return InfiniScroll.isDead = true; + } + for (_i = 0, _len = threads.length; _i < _len; _i++) { + thread = threads[_i]; + posts = []; + omitted_posts = thread.omitted_posts, omitted_images = thread.omitted_images; + threadID = thread.no; + el = $.el('div', { + className: 'thread', + id: "t" + threadID + }); + op = Build.postFromObject(thread, g.BOARD); + posts.push(op); + replylink = $.el('a', { + href: "res/" + threadID, + className: 'replylink', + textContent: 'Reply' + }); + postlink = $.el('div', { + className: "postLink mobile", + innerHTML: "View Thread" + }); + if (omitted_posts) { + posts.push($.el('span', { + className: 'summary desktop', + innerHTML: "" + omitted_posts + " posts " + (omitted_images ? "and " + omitted_images + " image replies" : void 0) + " omitted. Click here to view." + })); + $.prepend(postlink, $.el('span', { + className: 'info', + innerHTML: "" + omitted_posts + " posts omitted" + (omitted_images ? "
(" + omitted_images + " have images)" : "") + })); + } + $.add($('.postInfo', op), [$.tn('\u00A0\u00A0\u00A0['), replylink, $.tn(']\u00A0')]); + $.add(op, postlink); + if (thread.last_replies) { + _ref = thread.last_replies; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + post = _ref[_j]; + posts.push(Build.postFromObject(post, g.BOARD)); + } + } + $.add(el, posts); + threadNodes.push(el); + nodes.push(el); + nodes.push($.el('hr')); + } + InfiniScroll.features(threadNodes); + if (botPostForm = $('.board > .mobile.center')) { + return $.before(botPostForm, nodes); + } + }, + parsePages: function(response) { + var newThreads, number, page, pages, thread, threads, _i, _len; + + pages = JSON.parse(response); + newThreads = []; + for (number in pages) { + page = pages[number]; + if (!(pages.hasOwnProperty(number))) { + continue; + } + threads = page.threads; + for (_i = 0, _len = threads.length; _i < _len; _i++) { + thread = threads[_i]; + if (g.threads["" + g.BOARD + "." + thread.no]) { + continue; + } + newThreads.push(thread); + if (newThreads.length === 15) { + return newThreads; + } + } + } + return newThreads; + }, + features: function(threadNodes) { + var err, errors, post, posts, thread, threadRoot, threads, _i, _j, _len, _len1, _ref; + + posts = []; + threads = []; + for (_i = 0, _len = threadNodes.length; _i < _len; _i++) { + threadRoot = threadNodes[_i]; + thread = new Thread(+threadRoot.id.slice(1), g.BOARD); + threads.push(thread); + _ref = $$('.thread > .postContainer', threadRoot); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + post = _ref[_j]; + try { + posts.push(new Post(post, thread, g.BOARD)); + } catch (_error) { + err = _error; + if (!errors) { + errors = []; + } + errors.push({ + message: "Parsing of Post No." + (postRoot.id.match(/\d+/)) + " failed. Post will be skipped.", + error: err + }); + } + } + } + if (errors) { + Main.handleErrors(errors); + } + Main.callbackNodes(Thread, threads); + return Main.callbackNodes(Post, posts); + }, + notice: (function() { + var notify, reset; + + notify = false; + reset = function() { + return notify = false; + }; + return function() { + if (notify) { + return; + } + notify = true; + new Notice('info', "Last page reached.", 2); + return setTimeout(reset, 3 * $.SECOND); + }; + })(), + cb: { + load: function() { + InfiniScroll.isFetching = false; + if (this.status !== 200) { + return; + } + InfiniScroll.cache = new String(this.response); + InfiniScroll.cache.time = Date.now(); + return InfiniScroll.parse(this.response); + } + } + }; + Keybinds = { init: function() { var init; @@ -15849,7 +16028,8 @@ 'Thread Watcher (Menu)': ThreadWatcher.menu, 'Index Navigation': Nav, 'Keybinds': Keybinds, - 'Show Dice Roll': Dice + 'Show Dice Roll': Dice, + 'Infinite Scrolling': InfiniScroll }); $.on(d, 'AddCallback', Main.addCallback); return $.ready(Main.initReady); diff --git a/builds/crx/script.js b/builds/crx/script.js index 677dfe550..a8da4c636 100644 --- a/builds/crx/script.js +++ b/builds/crx/script.js @@ -1,6 +1,6 @@ // Generated by CoffeeScript /* -* appchan x - Version 2.4.1 - 2013-10-17 +* appchan x - Version 2.4.1 - 2013-10-19 * * Licensed under the MIT license. * https://github.com/zixaphir/appchan-x/blob/master/LICENSE @@ -88,7 +88,7 @@ 'use strict'; (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Banner, Board, Build, CatalogLinks, Clone, Conf, Config, CustomCSS, DataBoard, DeleteLink, Dice, DownloadLink, Emoji, ExpandComment, ExpandThread, FappeTyme, Favicon, FileInfo, Filter, Fourchan, Gallery, Get, GlobalMessage, Header, IDColor, ImageExpand, ImageHover, ImageLoader, InfiniScroll, JSColor, Keybinds, Linkify, Main, MascotTools, Mascots, Menu, Nav, Notice, PSAHiding, Polyfill, Post, PostHiding, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, QuoteStrikeThrough, QuoteThreading, QuoteYou, Quotify, Recursive, Redirect, RelativeDates, RemoveSpoilers, Report, ReportLink, RevealSpoilers, Rice, Sauce, Settings, Style, ThemeTools, Themes, Thread, ThreadExcerpt, ThreadHiding, ThreadStats, ThreadUpdater, ThreadWatcher, Time, UI, Unread, c, d, doc, editMascot, editTheme, g, userNavigation, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __slice = [].slice, __hasProp = {}.hasOwnProperty, @@ -114,7 +114,8 @@ 'Show Dice Roll': [true, 'Show dice that were entered into the email field.'], 'Color User IDs': [false, 'Assign unique colors to user IDs on boards that use them'], 'Remove Spoilers': [false, 'Remove all spoilers in text.'], - 'Reveal Spoilers': [false, 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'] + 'Reveal Spoilers': [false, 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.'], + 'Infinite Scrolling': [false, 'Add new posts to the board index upon reaching the bottom of the board.'] }, 'Linkification': { 'Linkify': [true, 'Convert text into links where applicable.'], @@ -13785,6 +13786,184 @@ } }; + InfiniScroll = { + init: function() { + if (!(Conf['Infinite Scrolling'] && g.VIEW === 'index' && g.BOARD !== 'f')) { + return; + } + this.threads = g.threads; + return $.on(d, '4chanXInitFinished', this.ready); + }, + ready: function() { + $.off(d, '4chanXInitFinished', InfiniScroll.ready); + $.on(d, 'scroll', InfiniScroll.scroll); + return InfiniScroll.scroll(); + }, + scroll: $.debounce(100, function() { + var url; + + if (InfiniScroll.isFetching || (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight))) { + return; + } + if (InfiniScroll.isDead) { + return InfiniScroll.notice(); + } + if (InfiniScroll.cache && InfiniScroll.cache.time > Date.now() - $.MINUTE) { + return InfiniScroll.parse(InfiniScroll.cache); + } + new Notice('info', "Fetching next page.", 2); + InfiniScroll.isFetching = true; + url = "//api.4chan.org/" + g.BOARD + "/catalog.json"; + return $.ajax(url, { + onloadend: InfiniScroll.cb.load + }, { + whenModified: true + }); + }), + parse: function(response) { + var botPostForm, el, nodes, omitted_images, omitted_posts, op, post, postlink, posts, replylink, thread, threadID, threadNodes, threads, _i, _j, _len, _len1, _ref; + + threads = InfiniScroll.parsePages(response); + threadNodes = []; + nodes = []; + if (!threads.length) { + InfiniScroll.notice(); + return InfiniScroll.isDead = true; + } + for (_i = 0, _len = threads.length; _i < _len; _i++) { + thread = threads[_i]; + posts = []; + omitted_posts = thread.omitted_posts, omitted_images = thread.omitted_images; + threadID = thread.no; + el = $.el('div', { + className: 'thread', + id: "t" + threadID + }); + op = Build.postFromObject(thread, g.BOARD); + posts.push(op); + replylink = $.el('a', { + href: "res/" + threadID, + className: 'replylink', + textContent: 'Reply' + }); + postlink = $.el('div', { + className: "postLink mobile", + innerHTML: "View Thread" + }); + if (omitted_posts) { + posts.push($.el('span', { + className: 'summary desktop', + innerHTML: "" + omitted_posts + " posts " + (omitted_images ? "and " + omitted_images + " image replies" : void 0) + " omitted. Click here to view." + })); + $.prepend(postlink, $.el('span', { + className: 'info', + innerHTML: "" + omitted_posts + " posts omitted" + (omitted_images ? "
(" + omitted_images + " have images)" : "") + })); + } + $.add($('.postInfo', op), [$.tn('\u00A0\u00A0\u00A0['), replylink, $.tn(']\u00A0')]); + $.add(op, postlink); + if (thread.last_replies) { + _ref = thread.last_replies; + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + post = _ref[_j]; + posts.push(Build.postFromObject(post, g.BOARD)); + } + } + $.add(el, posts); + threadNodes.push(el); + nodes.push(el); + nodes.push($.el('hr')); + } + InfiniScroll.features(threadNodes); + if (botPostForm = $('.board > .mobile.center')) { + return $.before(botPostForm, nodes); + } + }, + parsePages: function(response) { + var newThreads, number, page, pages, thread, threads, _i, _len; + + pages = JSON.parse(response); + newThreads = []; + for (number in pages) { + page = pages[number]; + if (!(pages.hasOwnProperty(number))) { + continue; + } + threads = page.threads; + for (_i = 0, _len = threads.length; _i < _len; _i++) { + thread = threads[_i]; + if (g.threads["" + g.BOARD + "." + thread.no]) { + continue; + } + newThreads.push(thread); + if (newThreads.length === 15) { + return newThreads; + } + } + } + return newThreads; + }, + features: function(threadNodes) { + var err, errors, post, posts, thread, threadRoot, threads, _i, _j, _len, _len1, _ref; + + posts = []; + threads = []; + for (_i = 0, _len = threadNodes.length; _i < _len; _i++) { + threadRoot = threadNodes[_i]; + thread = new Thread(+threadRoot.id.slice(1), g.BOARD); + threads.push(thread); + _ref = $$('.thread > .postContainer', threadRoot); + for (_j = 0, _len1 = _ref.length; _j < _len1; _j++) { + post = _ref[_j]; + try { + posts.push(new Post(post, thread, g.BOARD)); + } catch (_error) { + err = _error; + if (!errors) { + errors = []; + } + errors.push({ + message: "Parsing of Post No." + (postRoot.id.match(/\d+/)) + " failed. Post will be skipped.", + error: err + }); + } + } + } + if (errors) { + Main.handleErrors(errors); + } + Main.callbackNodes(Thread, threads); + return Main.callbackNodes(Post, posts); + }, + notice: (function() { + var notify, reset; + + notify = false; + reset = function() { + return notify = false; + }; + return function() { + if (notify) { + return; + } + notify = true; + new Notice('info', "Last page reached.", 2); + return setTimeout(reset, 3 * $.SECOND); + }; + })(), + cb: { + load: function() { + InfiniScroll.isFetching = false; + if (this.status !== 200) { + return; + } + InfiniScroll.cache = new String(this.response); + InfiniScroll.cache.time = Date.now(); + return InfiniScroll.parse(this.response); + } + } + }; + Keybinds = { init: function() { var init; @@ -15837,7 +16016,8 @@ 'Thread Watcher (Menu)': ThreadWatcher.menu, 'Index Navigation': Nav, 'Keybinds': Keybinds, - 'Show Dice Roll': Dice + 'Show Dice Roll': Dice, + 'Infinite Scrolling': InfiniScroll }); $.on(d, 'AddCallback', Main.addCallback); return $.ready(Main.initReady); diff --git a/src/General/Config.coffee b/src/General/Config.coffee index d1763b1bd..3f1376b96 100755 --- a/src/General/Config.coffee +++ b/src/General/Config.coffee @@ -69,6 +69,10 @@ Config = false 'Indicate spoilers if Remove Spoilers is enabled, or make the text appear hovered if Remove Spoiler is disabled.' ] + 'Infinite Scrolling': [ + false + 'Add new posts to the board index upon reaching the bottom of the board.' + ] 'Linkification': 'Linkify': [ diff --git a/src/General/Main.coffee b/src/General/Main.coffee index e898542a5..45e236cf3 100644 --- a/src/General/Main.coffee +++ b/src/General/Main.coffee @@ -172,6 +172,7 @@ Main = 'Index Navigation': Nav 'Keybinds': Keybinds 'Show Dice Roll': Dice + 'Infinite Scrolling': InfiniScroll # c.timeEnd 'All initializations' @@ -242,8 +243,7 @@ Main = try callback.cb.call node catch err - unless errors - errors = [] + errors = [] unless errors errors.push message: "\"#{callback.name}\" crashed on #{klass.name} No.#{node} (/#{node.board}/)." error: err @@ -259,8 +259,7 @@ Main = try callback.cb.call node catch err - unless errors - errors = [] + errors = [] unless errors errors.push message: "\"#{callback.name}\" crashed on #{klass.name} No.#{node} (/#{node.board}/)." error: err diff --git a/src/Miscellaneous/InfiniScroll.coffee b/src/Miscellaneous/InfiniScroll.coffee new file mode 100644 index 000000000..ec2bcfe40 --- /dev/null +++ b/src/Miscellaneous/InfiniScroll.coffee @@ -0,0 +1,139 @@ +InfiniScroll = + init: -> + return unless Conf['Infinite Scrolling'] and g.VIEW is 'index' and g.BOARD isnt 'f' + + @threads = g.threads + + $.on d, '4chanXInitFinished', @ready + + ready: -> + $.off d, '4chanXInitFinished', InfiniScroll.ready + $.on d, 'scroll', InfiniScroll.scroll + InfiniScroll.scroll() + + scroll: $.debounce 100, -> + return if InfiniScroll.isFetching or (doc.scrollTop <= doc.scrollHeight - (300 + window.innerHeight)) + return InfiniScroll.notice() if InfiniScroll.isDead + + # For once, lets respect 4chan's API rules. + if InfiniScroll.cache and InfiniScroll.cache.time > Date.now() - $.MINUTE + return InfiniScroll.parse InfiniScroll.cache + + new Notice 'info', "Fetching next page.", 2 + + InfiniScroll.isFetching = true + url = "//api.4chan.org/#{g.BOARD}/catalog.json" + $.ajax url, onloadend: InfiniScroll.cb.load, + whenModified: true + + parse: (response) -> + threads = InfiniScroll.parsePages response + threadNodes = [] + nodes = [] + + return unless threads.length + InfiniScroll.notice() + InfiniScroll.isDead = true + + for thread in threads + posts = [] + {omitted_posts, omitted_images} = thread + threadID = thread.no + + el = $.el 'div', + className: 'thread' + id: "t#{threadID}" + + op = Build.postFromObject thread, g.BOARD + posts.push op + + replylink = $.el 'a', + href: "res/#{threadID}" + className: 'replylink' + textContent: 'Reply' + + postlink = $.el 'div', + className: "postLink mobile" + innerHTML: """View Thread""" + + if omitted_posts + posts.push $.el 'span', + className: 'summary desktop' + innerHTML: """ + #{omitted_posts} posts #{if omitted_images then "and " + omitted_images + " image replies"} omitted. Click here to view. + """ + + $.prepend postlink, $.el 'span', + className: 'info' + innerHTML: """ + #{omitted_posts} posts omitted#{if omitted_images then "
(#{omitted_images} have images)" else ""} + """ + + $.add $('.postInfo', op), [$.tn('\u00A0\u00A0\u00A0['), replylink, $.tn(']\u00A0')] + $.add op, postlink + + if thread.last_replies then posts.push Build.postFromObject post, g.BOARD for post in thread.last_replies + + $.add el, posts + + threadNodes.push el + nodes.push el + nodes.push $.el 'hr' + + InfiniScroll.features threadNodes + + $.before botPostForm, nodes if botPostForm = $ '.board > .mobile.center' + + parsePages: (response) -> + pages = JSON.parse response + newThreads = [] + + for number, page of pages when pages.hasOwnProperty number + {threads} = page + for thread in threads + continue if g.threads["#{g.BOARD}.#{thread.no}"] + + newThreads.push thread + + return newThreads if newThreads.length is 15 + + return newThreads + + features: (threadNodes) -> + posts = [] + threads = [] + for threadRoot in threadNodes + thread = new Thread +threadRoot.id[1..], g.BOARD + threads.push thread + for post in $$ '.thread > .postContainer', threadRoot + try + posts.push new Post post, thread, g.BOARD + catch err + # Skip posts that we failed to parse. + unless errors + errors = [] + errors.push + message: "Parsing of Post No.#{postRoot.id.match(/\d+/)} failed. Post will be skipped." + error: err + Main.handleErrors errors if errors + + Main.callbackNodes Thread, threads + Main.callbackNodes Post, posts + + notice: do -> + notify = false + reset = -> notify = false + return -> + return if notify + notify = true + new Notice 'info', "Last page reached.", 2 + setTimeout reset, 3 * $.SECOND + + cb: + load: -> + InfiniScroll.isFetching = false + return unless @status is 200 + + InfiniScroll.cache = new String @response + InfiniScroll.cache.time = Date.now() + InfiniScroll.parse @response \ No newline at end of file