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