diff --git a/CHANGELOG.md b/CHANGELOG.md
index bd43be2d1..733165a61 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,15 @@
+Index navigation improvements:
+ - You can now refresh the index page you are on with the refresh shortcut in the header bar or the same keybind for refreshing threads.
+ - You can now switch between paged and all-threads index modes via the "Index Navigation" header sub-menu:
+ 
+ - Threads in the index can now be sorted by:
+ - Bump order
+ - Last reply
+ - Creation date
+ - Reply count
+ - File count
+ - Navigating across index pages is now instantaneous.
+
Added a keybind to open the catalog search field on index pages.
### 3.11.5 - *2013-10-03*
diff --git a/Gruntfile.coffee b/Gruntfile.coffee
index e7226450a..9f48729d7 100644
--- a/Gruntfile.coffee
+++ b/Gruntfile.coffee
@@ -1,11 +1,17 @@
module.exports = (grunt) ->
+ importHTML = (filename) ->
+ "\"\"\"#{grunt.file.read("html/#{filename}.html").replace(/^\s+|\s+$ grunt.config 'pkg'
+ get: ->
+ pkg = grunt.config 'pkg'
+ pkg.importHTML = importHTML
+ pkg
enumerable: true
)
coffee:
@@ -17,6 +23,7 @@ module.exports = (grunt) ->
'src/General/Header.coffee'
'src/General/Notice.coffee'
'src/General/Settings.coffee'
+ 'src/General/Index.coffee'
'src/General/Get.coffee'
'src/General/Build.coffee'
# Features -->
@@ -78,9 +85,10 @@ module.exports = (grunt) ->
stdout: true
stderr: true
failOnError: true
+ checkout:
+ command: 'git checkout <%= pkg.meta.mainBranch %>'
commit:
command: """
- git checkout <%= pkg.meta.mainBranch %>
git commit -am "Release <%= pkg.meta.name %> v<%= pkg.version %>."
git tag -a <%= pkg.version %> -m "<%= pkg.meta.name %> v<%= pkg.version %>."
git tag -af stable-v3 -m "<%= pkg.meta.name %> v<%= pkg.version %>."
@@ -144,9 +152,9 @@ module.exports = (grunt) ->
]
grunt.registerTask 'release', ['shell:commit', 'shell:push', 'build-crx', 'compress:crx']
- grunt.registerTask 'patch', ['bump', 'updcl:3', 'release']
- grunt.registerTask 'minor', ['bump:minor', 'updcl:2', 'release']
- grunt.registerTask 'major', ['bump:major', 'updcl:1', 'release']
+ grunt.registerTask 'patch', ['shell:checkout', 'bump', 'updcl:3', 'release']
+ grunt.registerTask 'minor', ['shell:checkout', 'bump:minor', 'updcl:2', 'release']
+ grunt.registerTask 'major', ['shell:checkout', 'bump:major', 'updcl:1', 'release']
grunt.registerTask 'updcl', 'Update the changelog', (headerLevel) ->
headerPrefix = new Array(+headerLevel + 1).join '#'
diff --git a/css/style.css b/css/style.css
index a67c82043..4bebe551d 100644
--- a/css/style.css
+++ b/css/style.css
@@ -362,6 +362,15 @@ a[href="javascript:;"] {
overflow: hidden;
}
+/* Index */
+:root.index-loading .board,
+:root.index-loading .pagelist {
+ display: none;
+}
+.summary {
+ text-decoration: none;
+}
+
/* Announcement Hiding */
:root.hide-announcement #globalMessage,
:root.hide-announcement-enabled #toggleMsgBtn {
diff --git a/html/General/Index-pagelist.html b/html/General/Index-pagelist.html
new file mode 100644
index 000000000..e6923bb7a
--- /dev/null
+++ b/html/General/Index-pagelist.html
@@ -0,0 +1,11 @@
+
+
+
diff --git a/html/General/Settings-section-Filter-guide.html b/html/General/Settings-section-Filter-guide.html
index ae73a66da..1656d0cb8 100644
--- a/html/General/Settings-section-Filter-guide.html
+++ b/html/General/Settings-section-Filter-guide.html
@@ -23,7 +23,7 @@
For example: highlight; or highlight:wallpaper;.
- Highlighted OPs will have their threads put on top of board pages by default.
+ Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
diff --git a/html/Monitoring/ThreadUpdater.html b/html/Monitoring/ThreadUpdater.html
index 04a6a17b2..32b13b497 100644
--- a/html/Monitoring/ThreadUpdater.html
+++ b/html/Monitoring/ThreadUpdater.html
@@ -13,5 +13,5 @@
-
+
diff --git a/img/changelog/3.12.0/0.png b/img/changelog/3.12.0/0.png
new file mode 100644
index 000000000..cb17e29c0
Binary files /dev/null and b/img/changelog/3.12.0/0.png differ
diff --git a/lib/$.coffee b/lib/$.coffee
index 8b2202304..53ede1802 100644
--- a/lib/$.coffee
+++ b/lib/$.coffee
@@ -45,7 +45,7 @@ $.ajax = do ->
type or= form and 'post' or 'get'
r.open type, url, !sync
if whenModified
- r.setRequestHeader 'If-Modified-Since', lastModified[url] or '0'
+ r.setRequestHeader 'If-Modified-Since', lastModified[url] if url of lastModified
$.on r, 'load', -> lastModified[url] = r.getResponseHeader 'Last-Modified'
$.extend r, options
$.extend r.upload, upCallbacks
@@ -104,7 +104,7 @@ $.rm = do ->
(el) -> el.parentNode?.removeChild el
$.rmAll = (root) ->
# jsperf.com/emptify-element
- while node = root.firstChild
+ for node in [root.childNodes...]
# HTMLSelectElement.remove !== Element.remove
root.removeChild node
return
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 1f1387dba..eee0dfae9 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -110,13 +110,8 @@ Filter =
# Highlight
$.addClass @nodes.root, result.class
- if !@isReply and result.top and g.VIEW is 'index'
- # Put the highlighted OPs' thread on top of the board page...
- thisThread = @nodes.root.parentNode
- # ...before the first non highlighted thread.
- if firstThread = $ 'div[class="postContainer opContainer"]'
- unless firstThread is @nodes.root
- $.before firstThread.parentNode, [thisThread, thisThread.nextElementSibling]
+ if !@isReply and result.top
+ @thread.isOnTop = true
name: (post) ->
if 'name' of post.info
diff --git a/src/Filtering/ThreadHiding.coffee b/src/Filtering/ThreadHiding.coffee
index 21bd4c211..80c11b1d2 100644
--- a/src/Filtering/ThreadHiding.coffee
+++ b/src/Filtering/ThreadHiding.coffee
@@ -4,6 +4,7 @@ ThreadHiding =
@db = new DataBoard 'hiddenThreads'
@syncCatalog()
+ $.on d, 'IndexRefresh', @onrefresh
Thread.callbacks.push
name: 'Thread Hiding'
cb: @node
@@ -14,6 +15,15 @@ ThreadHiding =
return unless Conf['Thread Hiding']
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
+ onrefresh: ->
+ for threadID, thread of g.BOARD.threads when thread.isHidden
+ root = thread.OP.nodes.root.parentNode
+ if thread.stub
+ $.prepend root, thread.stub
+ else
+ threadRoot.nextElementSibling.hidden = true
+ return
+
syncCatalog: ->
# Sync hidden threads from the catalog into the index.
hiddenThreads = ThreadHiding.db.get
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index 783a774f5..c621afa3c 100644
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -60,6 +60,10 @@ Build =
isOP = postID is threadID
staticPath = '//static.4chan.org/image/'
+ gifIcon = if window.devicePixelRatio >= 2
+ '@2x.gif'
+ else
+ '.gif'
if email
emailStart = ''
@@ -82,21 +86,21 @@ Build =
capcodeClass = " capcodeAdmin"
capcodeStart = " ## Admin "
- capcode = " "
when 'mod'
capcodeClass = " capcodeMod"
capcodeStart = " ## Mod "
- capcode = " "
when 'developer'
capcodeClass = " capcodeDeveloper"
capcodeStart = " ## Developer "
- capcode = " "
else
@@ -114,11 +118,11 @@ Build =
if file?.isDeleted
fileHTML = if isOP
"
" +
- " " +
+ " " +
" "
else
"" +
- " " +
+ " " +
" "
else if file
ext = file.name[-3..]
@@ -178,11 +182,16 @@ Build =
''
sticky = if isSticky
- " "
+ " "
else
''
closed = if isClosed
- " "
+ " "
+ else
+ ''
+
+ replyLink = if isOP and g.VIEW is 'index'
+ " [Reply ] "
else
''
@@ -222,7 +231,7 @@ Build =
"" +
emailStart +
"#{name or ''} " + tripcode +
- capcodeStart + emailEnd + capcode + userID + flag + sticky + closed +
+ capcodeStart + emailEnd + capcode + userID + flag +
' ' +
"#{date} " +
"" +
@@ -233,6 +242,7 @@ Build =
else
"/#{boardID}/res/#{threadID}#q#{postID}"
}' title='Quote this post'>#{postID} " +
+ sticky + closed + replyLink +
'' +
'' +
@@ -248,3 +258,29 @@ Build =
quote.href = "/#{boardID}/res/#{href}" # Fix pathnames
container
+
+ summary: (boardID, threadID, posts, files) ->
+ text = []
+ text.push "#{posts} post#{if posts > 1 then 's' else ''}"
+ text.push "and #{files} image repl#{if files > 1 then 'ies' else 'y'}" if files
+ text.push 'omitted.'
+ $.el 'a',
+ className: 'summary'
+ textContent: text.join ' '
+ href: "/#{boardID}/res/#{threadID}"
+ thread: (board, data) ->
+ Build.spoilerRange[board] = data.custom_spoiler
+
+ if (OP = board.posts[data.no]) and root = OP.nodes.root.parentNode
+ $.rmAll root
+ else
+ root = $.el 'div',
+ className: 'thread'
+ id: "t#{data.no}"
+
+ nodes = [if OP then OP.nodes.root else Build.postFromObject data, board.ID]
+ if data.omitted_posts
+ nodes.push Build.summary board.ID, data.no, data.omitted_posts, data.omitted_images
+
+ $.add root, nodes
+ root
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 1421ff806..715822858 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -9,7 +9,6 @@ Config =
'Time Formatting': [true, 'Localize and format timestamps.']
'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.']
'File Info Formatting': [true, 'Reformat the file information.']
- 'Comment Expansion': [true, 'Add buttons to expand too long comments.']
'Thread Expansion': [true, 'Add buttons to expand threads.']
'Index Navigation': [false, 'Add buttons to navigate between threads.']
'Reply Navigation': [false, 'Add buttons to navigate to top / bottom of thread.']
@@ -140,6 +139,9 @@ Config =
#//archive.installgentoo.net/%board/image/%MD5;text:View same on installgentoo /%board/
"""
'Custom CSS': false
+ Index:
+ 'Index Mode': 'paged'
+ 'Index Sort': 'bump'
Header:
'Header auto-hide': false
'Bottom header': false
@@ -170,9 +172,9 @@ Config =
'Eqn tags': ['Alt+e', 'Insert eqn tags.']
'Math tags': ['Alt+m', 'Insert math tags.']
'Submit QR': ['Alt+s', 'Submit post.']
- # Thread related
+ # Index/Thread related
+ 'Update': ['r', 'Refresh the index/thread.']
'Watch': ['w', 'Watch thread.']
- 'Update': ['r', 'Update the thread.']
# Images
'Expand image': ['Shift+e', 'Expand selected image.']
'Expand images': ['e', 'Expand all images.']
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index e16001638..10359d471 100644
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -2,12 +2,11 @@ Header =
init: ->
headerEl = $.el 'div',
id: 'header'
- innerHTML: """
- <%= grunt.file.read('html/General/Header.html').replace(/>\s+<').trim() %>
- """
+ innerHTML: <%= importHTML('General/Header') %>
@bar = $ '#header-bar', headerEl
@toggle = $ '#toggle-header-bar', @bar
+ @noticesRoot = $ '#notifications', headerEl
@menu = new UI.Menu 'header'
menuButton = $.el 'a',
@@ -244,32 +243,48 @@ Header =
$('input[name=boardnav]', settings).focus()
hashScroll: ->
- return unless (hash = @location.hash[1..]) and post = $.id hash
+ hash = @location.hash[1..]
+ return unless /^p\d+$/.test(hash) and post = $.id hash
return if (Get.postFromRoot post).isHidden
- Header.scrollToPost post
- scrollToPost: (post) ->
- {top} = post.getBoundingClientRect()
+ Header.scrollTo post
+ scrollTo: (root, down, needed) ->
+ if down
+ x = Header.getBottomOf root
+ window.scrollBy 0, -x unless needed and x >= 0
+ else
+ x = Header.getTopOf root
+ window.scrollBy 0, x unless needed and x >= 0
+ scrollToIfNeeded: (root, down) ->
+ Header.scrollTo root, down, true
+ getTopOf: (root) ->
+ {top} = root.getBoundingClientRect()
unless Conf['Bottom header']
headRect = Header.toggle.getBoundingClientRect()
- top -= headRect.top + headRect.height
- window.scrollBy 0, top
+ top -= headRect.top + headRect.height
+ top
+ getBottomOf: (root) ->
+ {clientHeight} = doc
+ bottom = clientHeight - root.getBoundingClientRect().bottom
+ if Conf['Bottom header']
+ headRect = Header.toggle.getBoundingClientRect()
+ bottom -= clientHeight - headRect.bottom + headRect.height
+ bottom
addShortcut: (el, index) ->
shortcut = $.el 'span',
className: 'shortcut'
+ shortcut.dataset.index = index
$.add shortcut, el
shortcuts = $ '#shortcuts', Header.bar
- nodes = [shortcuts.childNodes...]
- nodes.splice index, 0, shortcut
- $.add shortcuts, nodes
+ $.add shortcuts, [shortcuts.childNodes...].concat(shortcut).sort (a, b) -> a.dataset.index - b.dataset.index
menuToggle: (e) ->
Header.menu.toggle e, @, g
createNotification: (e) ->
{type, content, lifetime, cb} = e.detail
- notif = new Notice type, content, lifetime
- cb notif if cb
+ notice = new Notice type, content, lifetime
+ cb notice if cb
areNotificationsEnabled: false
enableDesktopNotifications: ->
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
new file mode 100644
index 000000000..5e0713f62
--- /dev/null
+++ b/src/General/Index.coffee
@@ -0,0 +1,281 @@
+Index =
+ init: ->
+ return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
+
+ @button = $.el 'a',
+ className: 'index-refresh-shortcut fa fa-refresh'
+ title: 'Refresh Index'
+ href: 'javascript:;'
+ $.on @button, 'click', @update
+ Header.addShortcut @button, 1
+
+ modeEntry =
+ el: $.el 'span', textContent: 'Index mode'
+ subEntries: [
+ { el: $.el 'label', innerHTML: ' Paged' }
+ { el: $.el 'label', innerHTML: ' All threads' }
+ ]
+ for label in modeEntry.subEntries
+ input = label.el.firstChild
+ input.checked = Conf['Index Mode'] is input.value
+ $.on input, 'change', $.cb.value
+ $.on input, 'change', @cb.mode
+
+ sortEntry =
+ el: $.el 'span', textContent: 'Sort by'
+ subEntries: [
+ { el: $.el 'label', innerHTML: ' Bump order' }
+ { el: $.el 'label', innerHTML: ' Last reply' }
+ { el: $.el 'label', innerHTML: ' Creation date' }
+ { el: $.el 'label', innerHTML: ' Reply count' }
+ { el: $.el 'label', innerHTML: ' File count' }
+ ]
+ for label in sortEntry.subEntries
+ input = label.el.firstChild
+ input.checked = Conf['Index Sort'] is input.value
+ $.on input, 'change', $.cb.value
+ $.on input, 'change', @cb.sort
+
+ $.event 'AddMenuEntry',
+ type: 'header'
+ el: $.el 'span',
+ textContent: 'Index Navigation'
+ order: 90
+ subEntries: [modeEntry, sortEntry]
+
+ $.addClass doc, 'index-loading'
+ @update()
+ @root = $.el 'div', className: 'board'
+ @pagelist = $.el 'div',
+ className: 'pagelist'
+ hidden: true
+ innerHTML: <%= importHTML('General/Index-pagelist') %>
+ @currentPage = @getCurrentPage()
+ $.on window, 'popstate', @cb.popstate
+ $.on @pagelist, 'click', @cb.pageNav
+ $.asap (-> $('.pagelist', doc) or d.readyState isnt 'loading'), ->
+ $.replace $('.board'), Index.root
+ $.replace $('.pagelist'), Index.pagelist
+ $.rmClass doc, 'index-loading'
+
+ cb:
+ mode: ->
+ Index.togglePagelist()
+ Index.buildIndex()
+ sort: ->
+ Index.sort()
+ Index.buildIndex()
+ popstate: (e) ->
+ pageNum = Index.getCurrentPage()
+ Index.pageLoad pageNum if Index.currentPage isnt pageNum
+ pageNav: (e) ->
+ return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
+ switch e.target.nodeName
+ when 'BUTTON'
+ a = e.target.parentNode
+ when 'A'
+ a = e.target
+ else
+ return
+ e.preventDefault()
+ Index.pageNav +a.pathname.split('/')[2]
+
+ scrollToIndex: ->
+ Header.scrollToIfNeeded Index.root
+
+ getCurrentPage: ->
+ +window.location.pathname.split('/')[2]
+ pageNav: (pageNum) ->
+ return if Index.currentPage is pageNum
+ history.pushState null, '', if pageNum is 0 then './' else pageNum
+ Index.pageLoad pageNum
+ pageLoad: (pageNum) ->
+ Index.currentPage = pageNum
+ return if Conf['Index Mode'] isnt 'paged'
+ Index.buildIndex()
+ Index.setPage()
+ Index.scrollToIndex()
+
+ togglePagelist: ->
+ Index.pagelist.hidden = Conf['Index Mode'] isnt 'paged'
+ buildPagelist: ->
+ pagesRoot = $ '.pages', Index.pagelist
+ if pagesRoot.childElementCount isnt Index.pagesNum
+ nodes = []
+ for i in [0..Index.pagesNum - 1]
+ a = $.el 'a',
+ textContent: i
+ href: if i then i else './'
+ nodes.push $.tn('['), a, $.tn '] '
+ $.rmAll pagesRoot
+ $.add pagesRoot, nodes
+ Index.setPage()
+ Index.togglePagelist()
+ setPage: ->
+ pageNum = Index.getCurrentPage()
+ pagesRoot = $ '.pages', Index.pagelist
+ # Previous/Next buttons
+ prev = pagesRoot.previousSibling.firstChild
+ next = pagesRoot.nextSibling.firstChild
+ href = Math.max pageNum - 1, 0
+ prev.href = if href is 0 then './' else href
+ prev.firstChild.disabled = href is pageNum
+ href = Math.min pageNum + 1, Index.pagesNum - 1
+ next.href = if href is 0 then './' else href
+ next.firstChild.disabled = href is pageNum
+ # current page
+ if strong = $ 'strong', pagesRoot
+ return if +strong.textContent is pageNum
+ $.replace strong, strong.firstChild
+ else
+ strong = $.el 'strong'
+ a = pagesRoot.children[pageNum]
+ $.before a, strong
+ $.add strong, a
+
+ update: ->
+ return unless navigator.onLine
+ Index.req?.abort()
+ Index.notice?.close()
+ Index.notice = new Notice 'info', 'Refreshing index...'
+ Index.req = $.ajax "//api.4chan.org/#{g.BOARD}/catalog.json",
+ onabort: Index.load
+ onloadend: Index.load
+ ,
+ whenModified: true
+ $.addClass Index.button, 'fa-spin'
+ load: (e) ->
+ $.rmClass Index.button, 'fa-spin'
+ {req, notice} = Index
+ delete Index.req
+ delete Index.notice
+
+ if e.type is 'abort'
+ req.onloadend = null
+ notice.close()
+ return
+
+ try
+ Index.parse JSON.parse req.response if req.status is 200
+ catch err
+ c.error 'Index failure:', err.stack
+ # network error or non-JSON content for example.
+ notice.setType 'error'
+ notice.el.lastElementChild.textContent = 'Index refresh failed.'
+ setTimeout notice.close, 2 * $.SECOND
+ return
+
+ notice.setType 'success'
+ notice.el.lastElementChild.textContent = 'Index refreshed!'
+ setTimeout notice.close, $.SECOND
+
+ Index.scrollToIndex()
+ parse: (pages) ->
+ Index.parseThreadList pages
+ Index.buildThreads()
+ Index.sort()
+ Index.buildIndex()
+ Index.buildPagelist()
+ parseThreadList: (pages) ->
+ Index.pagesNum = pages.length
+ Index.threadsNumPerPage = pages[0].threads.length
+ Index.liveThreadData = pages.reduce ((arr, next) -> arr.concat next.threads), []
+ Index.liveThreadIDs = Index.liveThreadData.map (data) -> data.no
+ for threadID, thread of g.BOARD.threads when thread.ID not in Index.liveThreadIDs
+ thread.collect()
+ return
+ buildThreads: ->
+ Index.nodes = []
+ threads = []
+ posts = []
+ for threadData in Index.liveThreadData
+ threadRoot = Build.thread g.BOARD, threadData
+ Index.nodes.push threadRoot, $.el 'hr'
+ if thread = g.BOARD.threads[threadData.no]
+ thread.setStatus 'Sticky', !!threadData.sticky
+ thread.setStatus 'Closed', !!threadData.closed
+ else
+ thread = new Thread threadData.no, g.BOARD
+ threads.push thread
+ # postRoots = $$ '.thread > .postContainer', threadRoot
+ # for postRoot in postRoots when postRoot.id.match(/\d+/)[0] not of thread.posts
+ OPRoot = $ '.opContainer', threadRoot
+ continue if OPRoot.id.match(/\d+/)[0] of thread.posts
+ try
+ posts.push new Post OPRoot, thread, g.BOARD
+ catch err
+ # Skip posts that we failed to parse.
+ Main.handleErrors
+ message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
+ error: err
+
+ # Add the threads and s in a container to make sure all features work.
+ $.nodes Index.nodes
+ Main.callbackNodes Thread, threads
+ Main.callbackNodes Post, posts
+ buildReplies: (threadRoots) ->
+ posts = []
+ for threadRoot in threadRoots by 2
+ thread = Get.threadFromRoot threadRoot
+ i = Index.liveThreadIDs.indexOf thread.ID
+ continue unless lastReplies = Index.liveThreadData[i].last_replies
+ nodes = []
+ for data in lastReplies
+ if post = thread.posts[data.no]
+ nodes.push post.nodes.root
+ continue
+ nodes.push node = Build.postFromObject data, thread.board.ID
+ try
+ posts.push new Post node, thread, thread.board
+ catch err
+ # Skip posts that we failed to parse.
+ errors = [] unless errors
+ errors.push
+ message: "Parsing of Post No.#{postRoot.id.match /\d+/} failed. Post will be skipped."
+ error: err
+ $.add threadRoot, nodes
+
+ Main.handleErrors errors if errors
+ Main.callbackNodes Post, posts
+ sort: ->
+ switch Conf['Index Sort']
+ when 'bump'
+ sortedThreadIDs = Index.liveThreadIDs
+ when 'lastreply'
+ sortedThreadIDs = [Index.liveThreadData...].sort((a, b) ->
+ a = a.last_replies[a.last_replies.length - 1] if 'last_replies' of a
+ b = b.last_replies[b.last_replies.length - 1] if 'last_replies' of b
+ b.no - a.no
+ ).map (data) -> data.no
+ when 'birth'
+ sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
+ when 'replycount'
+ sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.replies - a.replies).map (data) -> data.no
+ when 'filecount'
+ sortedThreadIDs = [Index.liveThreadData...].sort((a, b) -> b.images - a.images).map (data) -> data.no
+ Index.sortedNodes = []
+ for threadID in sortedThreadIDs
+ i = Index.liveThreadIDs.indexOf(threadID) * 2
+ Index.sortedNodes.push Index.nodes[i], Index.nodes[i + 1]
+ # Put the sticky threads on top of the index.
+ offset = 0
+ for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isSticky
+ Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
+ return unless Conf['Filter']
+ # Put the highlighted thread & on top of the index
+ # while keeping the original order they appear in.
+ offset = 0
+ for threadRoot, i in Index.sortedNodes by 2 when Get.threadFromRoot(threadRoot).isOnTop
+ Index.sortedNodes.splice offset++ * 2, 0, Index.sortedNodes.splice(i, 2)...
+ return
+ buildIndex: ->
+ if Conf['Index Mode'] is 'paged'
+ pageNum = Index.getCurrentPage()
+ nodesPerPage = Index.threadsNumPerPage * 2
+ nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
+ else
+ nodes = Index.sortedNodes
+ $.rmAll Index.root
+ Index.buildReplies nodes
+ $.event 'IndexRefresh'
+ $.add Index.root, nodes
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 9410365c0..e0b9abc8b 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -69,6 +69,7 @@ Main =
initFeature 'Polyfill', Polyfill
initFeature 'Header', Header
initFeature 'Settings', Settings
+ initFeature 'Index Generator', Index
initFeature 'Announcement Hiding', PSAHiding
initFeature 'Fourchan thingies', Fourchan
initFeature 'Custom CSS', CustomCSS
@@ -105,7 +106,6 @@ Main =
initFeature 'Reveal Spoilers', RevealSpoilers
initFeature 'Auto-GIF', AutoGIF
initFeature 'Image Hover', ImageHover
- initFeature 'Comment Expansion', ExpandComment
initFeature 'Thread Expansion', ExpandThread
initFeature 'Thread Excerpt', ThreadExcerpt
initFeature 'Favicon', Favicon
@@ -169,26 +169,21 @@ Main =
# Something might have gone wrong!
Main.initStyle()
- if board = $ '.board'
- threads = []
- posts = []
-
- for threadRoot in $$ '.board > .thread', board
- thread = new Thread +threadRoot.id[1..], g.BOARD
- threads.push thread
- for postRoot in $$ '.thread > .postContainer', threadRoot
- try
- posts.push new Post postRoot, 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
+ if g.VIEW is 'thread' and threadRoot = $ '.thread'
+ thread = new Thread +threadRoot.id[1..], g.BOARD
+ posts = []
+ for postRoot in $$ '.thread > .postContainer', threadRoot
+ try
+ posts.push new Post postRoot, thread, g.BOARD
+ catch err
+ # Skip posts that we failed to parse.
+ errors = [] unless 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 Thread, [thread]
Main.callbackNodes Post, posts
if $.hasClass d.body, 'fourchan_x'
@@ -207,7 +202,7 @@ Main =
try
localStorage.getItem '4chan-settings'
catch err
- new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to properly function.', 30
+ new Notice 'warning', 'Cookies need to be enabled on 4chan for <%= meta.name %> to operate properly.', 30
Main.disableReports = true
$.event '4chanXInitFinished'
diff --git a/src/General/Notice.coffee b/src/General/Notice.coffee
index 60b46ff77..90c2e504b 100644
--- a/src/General/Notice.coffee
+++ b/src/General/Notice.coffee
@@ -19,7 +19,7 @@ class Notice
$.on d, 'visibilitychange', @add
return
$.off d, 'visibilitychange', @add
- $.add $.id('notifications'), @el
+ $.add Header.noticesRoot, @el
@el.clientHeight # force reflow
@el.style.opacity = 1
setTimeout @close, @timeout * $.SECOND if @timeout
diff --git a/src/General/Post.coffee b/src/General/Post.coffee
index 27811afa5..901405495 100644
--- a/src/General/Post.coffee
+++ b/src/General/Post.coffee
@@ -193,6 +193,13 @@ class Post
quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
$.rmClass quotelink, 'deadlink'
return
+
+ collect: ->
+ @kill()
+ delete g.posts[@fullID]
+ delete @thread.posts[@]
+ delete @board.posts[@]
+
addClone: (context) ->
new Clone @, context
rmClone: (index) ->
diff --git a/src/General/Settings.coffee b/src/General/Settings.coffee
index 17cf7595d..4c0499e4c 100644
--- a/src/General/Settings.coffee
+++ b/src/General/Settings.coffee
@@ -61,9 +61,7 @@ Settings =
return if Settings.dialog
$.event 'CloseMenu'
- html = """
- <%= grunt.file.read('html/General/Settings.html').replace(/>\s+<').trim() %>
- """
+ html = <%= importHTML('General/Settings') %>
Settings.dialog = overlay = $.el 'div',
id: 'overlay'
@@ -113,9 +111,7 @@ Settings =
section.scrollTop = 0
main: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Main.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Main') %>
$.on $('.export', section), 'click', Settings.export
$.on $('.import', section), 'click', Settings.import
$.on $('input', section), 'change', Settings.onImport
@@ -142,7 +138,7 @@ Settings =
return
div = $.el 'div',
- innerHTML: ": Clear manually-hidden threads and posts on all boards. Refresh the page to apply."
+ innerHTML: ": Clear manually-hidden threads and posts on all boards. Reload the page to apply."
button = $ 'button', div
hiddenNum = 0
$.get 'hiddenThreads', boards: {}, (item) ->
@@ -205,7 +201,7 @@ Settings =
try
data = JSON.parse e.target.result
Settings.loadSettings data
- if confirm 'Import successful. Refresh now?'
+ if confirm 'Import successful. Reload now?'
window.location.reload()
catch err
output.textContent = 'Import failed due to an error.'
@@ -290,9 +286,7 @@ Settings =
data
filter: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Filter.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Filter') %>
select = $ 'select', section
$.on select, 'change', Settings.selectFilter
Settings.selectFilter.call select
@@ -309,32 +303,24 @@ Settings =
$.on ta, 'change', $.cb.value
$.add div, ta
return
- div.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Filter-guide.html').replace(/>\s+<').trim() %>
- """
+ div.innerHTML = <%= importHTML('General/Settings-section-Filter-guide') %>
qr: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-QR.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-QR') %>
ta = $ 'textarea', section
$.get 'QR.personas', Conf['QR.personas'], (item) ->
ta.value = item['QR.personas']
$.on ta, 'change', $.cb.value
sauce: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Sauce.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Sauce') %>
ta = $ 'textarea', section
$.get 'sauces', Conf['sauces'], (item) ->
ta.value = item['sauces']
$.on ta, 'change', $.cb.value
rice: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Rice.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Rice') %>
items = {}
inputs = {}
for name in ['boardnav', 'time', 'backlink', 'fileInfo', 'favicon', 'usercss']
@@ -395,9 +381,7 @@ Settings =
CustomCSS.update()
archives: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Archives.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Archives') %>
showLastUpdateTime = (time) ->
$('time', section).textContent = new Date(time).toLocaleString()
@@ -470,9 +454,7 @@ Settings =
$.set 'selectedArchives', selectedArchives
keybinds: (section) ->
- section.innerHTML = """
- <%= grunt.file.read('html/General/Settings-section-Keybinds.html').replace(/>\s+<').trim() %>
- """
+ section.innerHTML = <%= importHTML('General/Settings-section-Keybinds') %>
tbody = $ 'tbody', section
items = {}
inputs = {}
diff --git a/src/General/Thread.coffee b/src/General/Thread.coffee
index 1f15bf714..a02f4bc72 100644
--- a/src/General/Thread.coffee
+++ b/src/General/Thread.coffee
@@ -3,11 +3,41 @@ class Thread
toString: -> @ID
constructor: (@ID, @board) ->
- @fullID = "#{@board}.#{@ID}"
- @posts = {}
+ @fullID = "#{@board}.#{@ID}"
+ @posts = {}
+ @isSticky = false
+ @isClosed = false
+ @postLimit = false
+ @fileLimit = false
g.threads[@fullID] = board.threads[@] = @
+ setStatus: (type, status) ->
+ name = "is#{type}"
+ return if @[name] is status
+ @[name] = status
+ return unless @OP
+ typeLC = type.toLowerCase()
+ unless status
+ $.rm $ ".#{typeLC}Icon", @OP.nodes.info
+ return
+ icon = $.el 'img',
+ src: "//static.4chan.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
+ alt: type
+ title: type
+ className: "#{typeLC}Icon"
+ root = if type is 'Closed' and @isSticky
+ $ '.stickyIcon', @OP.nodes.info
+ else
+ $ '[title="Quote this post"]', @OP.nodes.info
+ $.after root, [$.tn(' '), icon]
+
kill: ->
@isDead = true
@timeOfDeath = Date.now()
+
+ collect: ->
+ for postID, post in @posts
+ post.collect()
+ delete g.threads[@fullID]
+ delete @board.threads[@]
diff --git a/src/Images/ImageExpand.coffee b/src/Images/ImageExpand.coffee
index 17b8066d3..60382bb73 100644
--- a/src/Images/ImageExpand.coffee
+++ b/src/Images/ImageExpand.coffee
@@ -7,7 +7,7 @@ ImageExpand =
title: 'Expand All Images'
href: 'javascript:;'
$.on @EAI, 'click', ImageExpand.cb.toggleAll
- Header.addShortcut @EAI, 2
+ Header.addShortcut @EAI, 3
Post.callbacks.push
name: 'Image Expansion'
@@ -45,7 +45,7 @@ ImageExpand =
continue unless file and file.isImage and doc.contains post.nodes.root
if ImageExpand.on and
(!Conf['Expand spoilers'] and file.isSpoiler or
- Conf['Expand from here'] and file.thumb.getBoundingClientRect().top < 0)
+ Conf['Expand from here'] and Header.getTopOf(file.thumb) < 0)
continue
$.queueTask func, post
return
@@ -60,13 +60,10 @@ ImageExpand =
# Scroll back to the thumbnail when contracting the image
# to avoid being left miles away from the relevant post.
- rect = post.nodes.root.getBoundingClientRect()
- if rect.top < 0
- y = rect.top
- unless Conf['Bottom header']
- headRect = Header.toggle.getBoundingClientRect()
- y -= headRect.top + headRect.height
- if rect.left < 0
+ top = Header.getTopOf post.nodes.root
+ if top < 0
+ y = top
+ if post.nodes.root.getBoundingClientRect().left < 0
x = -window.scrollX
window.scrollBy x, y if x or y
ImageExpand.contract post
@@ -104,13 +101,12 @@ ImageExpand =
$.addClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding'
return
- prev = post.nodes.root.getBoundingClientRect()
+ {bottom} = post.nodes.root.getBoundingClientRect()
$.queueTask ->
$.addClass post.nodes.root, 'expanded-image'
$.rmClass post.file.thumb, 'expanding'
- return unless prev.top + prev.height <= 0
- curr = post.nodes.root.getBoundingClientRect()
- window.scrollBy 0, curr.height - prev.height + curr.top - prev.top
+ return unless bottom <= 0
+ window.scrollBy 0, post.nodes.root.getBoundingClientRect().bottom - bottom
error: ->
post = Get.postFromNode @
diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee
deleted file mode 100644
index 103c891e5..000000000
--- a/src/Miscellaneous/ExpandComment.coffee
+++ /dev/null
@@ -1,71 +0,0 @@
-ExpandComment =
- init: ->
- return if g.VIEW isnt 'index' or !Conf['Comment Expansion']
-
- Post.callbacks.push
- name: 'Comment Expansion'
- cb: @node
- node: ->
- if a = $ '.abbr > a:not([onclick])', @nodes.comment
- $.on a, 'click', ExpandComment.cb
- cb: (e) ->
- e.preventDefault()
- ExpandComment.expand Get.postFromNode @
- expand: (post) ->
- if post.nodes.longComment and !post.nodes.longComment.parentNode
- $.replace post.nodes.shortComment, post.nodes.longComment
- post.nodes.comment = post.nodes.longComment
- return
- return unless a = $ '.abbr > a', post.nodes.comment
- a.textContent = "Post No.#{post} Loading..."
- $.cache "//api.4chan.org#{a.pathname}.json", -> ExpandComment.parse @, a, post
- contract: (post) ->
- return unless post.nodes.shortComment
- a = $ '.abbr > a', post.nodes.shortComment
- a.textContent = 'here'
- $.replace post.nodes.longComment, post.nodes.shortComment
- post.nodes.comment = post.nodes.shortComment
- parse: (req, a, post) ->
- {status} = req
- if status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{status})"
- return
-
- posts = JSON.parse(req.response).posts
- if spoilerRange = posts[0].custom_spoiler
- Build.spoilerRange[g.BOARD] = spoilerRange
-
- for postObj in posts
- break if postObj.no is post.ID
- if postObj.no isnt post.ID
- a.textContent = "Post No.#{post} not found."
- return
-
- {comment} = post.nodes
- clone = comment.cloneNode false
- clone.innerHTML = postObj.com
- for quote in $$ '.quotelink', clone
- href = quote.getAttribute 'href'
- continue if href[0] is '/' # Cross-board quote, or board link
- quote.href = "/#{post.board}/res/#{href}" # Fix pathnames
- post.nodes.shortComment = comment
- $.replace comment, clone
- post.nodes.comment = post.nodes.longComment = clone
- post.parseComment()
- post.parseQuotes()
- if Conf['Resurrect Quotes']
- Quotify.node.call post
- if Conf['Quote Previewing']
- QuotePreview.node.call post
- if Conf['Quote Inlining']
- QuoteInline.node.call post
- if Conf['Mark OP Quotes']
- QuoteOP.node.call post
- if Conf['Mark Cross-thread Quotes']
- QuoteCT.node.call post
- if g.BOARD.ID is 'g'
- Fourchan.code.call post
- if g.BOARD.ID is 'sci'
- Fourchan.math.call post
- if Conf['Linkify']
- Linkify.node.call post
diff --git a/src/Miscellaneous/ExpandThread.coffee b/src/Miscellaneous/ExpandThread.coffee
index fe2305bdc..d9efaa2b0 100644
--- a/src/Miscellaneous/ExpandThread.coffee
+++ b/src/Miscellaneous/ExpandThread.coffee
@@ -1,19 +1,26 @@
ExpandThread =
init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Expansion']
-
+ @statuses = {}
+ $.on d, 'IndexRefresh', @onIndexRefresh
Thread.callbacks.push
name: 'Thread Expansion'
cb: @node
node: ->
- return unless span = $.x 'following-sibling::span[contains(@class,"summary")][1]', @OP.nodes.root
- [posts, files] = span.textContent.match /\d+/g
- a = $.el 'a',
- textContent: ExpandThread.text '+', posts, files
- className: 'summary'
- href: 'javascript:;'
+ ExpandThread.setButton @
+
+ setButton: (thread) ->
+ return unless a = $.x 'following-sibling::a[contains(@class,"summary")][1]', thread.OP.nodes.root
+ a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)...
$.on a, 'click', ExpandThread.cbToggle
- $.replace span, a
+
+ onIndexRefresh: ->
+ for threadID, status of ExpandThread.statuses
+ status.req?.abort()
+ delete ExpandThread.statuses[threadID]
+ for threadID, thread of g.BOARD.threads
+ ExpandThread.setButton thread
+ return
text: (status, posts, files) ->
text = [status]
@@ -22,95 +29,77 @@ ExpandThread =
text.push if status is '-' then 'shown' else 'omitted'
text.join(' ') + '.'
- cbToggle: ->
+ cbToggle: (e) ->
+ return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
+ e.preventDefault()
ExpandThread.toggle Get.threadFromNode @
toggle: (thread) ->
threadRoot = thread.OP.nodes.root.parentNode
- a = $ '.summary', threadRoot
-
- switch thread.isExpanded
- when false, undefined
- for post in $$ '.thread > .postContainer', threadRoot
- ExpandComment.expand Get.postFromRoot post
- unless a
- thread.isExpanded = true
- return
- thread.isExpanded = 'loading'
- [posts, files] = a.textContent.match /\d+/g
- a.textContent = ExpandThread.text '...', posts, files
- $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
- ExpandThread.parse @, thread, a
-
- when 'loading'
- thread.isExpanded = false
- return unless a
- [posts, files] = a.textContent.match /\d+/g
- a.textContent = ExpandThread.text '+', posts, files
-
- when true
- thread.isExpanded = false
- #goddamit moot
- num = if thread.isSticky
- 1
- else switch g.BOARD.ID
- # XXX boards config
- when 'b', 'vg' then 3
- when 't' then 1
- else 5
- posts = $$ ".thread > .replyContainer", threadRoot
- for post in [thread.OP.nodes.root].concat posts[-num..]
- ExpandComment.contract Get.postFromRoot post
- return unless a
- postsCount = 0
- filesCount = 0
- for reply in posts[...-num]
- if Conf['Quote Inlining']
- # rm clones
- inlined.click() while inlined = $ '.inlined', reply
- postsCount++
- filesCount++ if 'file' of Get.postFromRoot reply
- $.rm reply
- a.textContent = ExpandThread.text '+', postsCount, filesCount
- return
-
- parse: (req, thread, a) ->
- return if a.textContent[0] is '+'
- if req.status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{req.status})"
- $.off a, 'click', ExpandThread.cbToggle
+ return unless a = $ '.summary', threadRoot
+ if thread.ID of ExpandThread.statuses
+ ExpandThread.contract thread, a, threadRoot
+ else
+ ExpandThread.expand thread, a, threadRoot
+ expand: (thread, a, threadRoot) ->
+ ExpandThread.statuses[thread] = status = {}
+ a.textContent = ExpandThread.text '...', a.textContent.match(/\d+/g)...
+ status.req = $.cache "//api.4chan.org/#{thread.board}/res/#{thread}.json", ->
+ delete status.req
+ ExpandThread.parse @, thread, a
+ contract: (thread, a, threadRoot) ->
+ status = ExpandThread.statuses[thread]
+ delete ExpandThread.statuses[thread]
+ if status.req
+ status.req.abort()
+ a.textContent = ExpandThread.text '+', a.textContent.match(/\d+/g)... if a
return
- thread.isExpanded = true
+ num = if thread.isSticky
+ 1
+ else switch g.BOARD.ID
+ # XXX boards config
+ when 'b', 'vg' then 3
+ when 't' then 1
+ else 5
+ postsCount = 0
+ filesCount = 0
+ for reply in $$('.thread > .replyContainer', threadRoot)[...-num]
+ # rm clones
+ inlined.click() while inlined = $ '.inlined', reply if Conf['Quote Inlining']
+ postsCount++
+ filesCount++ if 'file' of Get.postFromRoot reply
+ $.rm reply
+ a.textContent = ExpandThread.text '+', postsCount, filesCount
+ parse: (req, thread, a) ->
+ if req.status not in [200, 304]
+ a.textContent = "Error #{req.statusText} (#{req.status})"
+ return
- {posts} = JSON.parse req.response
- if spoilerRange = posts.shift().custom_spoiler
- Build.spoilerRange[thread.board] = spoilerRange
+ data = JSON.parse(req.response).posts
+ Build.spoilerRange[thread.board] = data.shift().custom_spoiler
- postsObj = []
+ posts = []
postsRoot = []
filesCount = 0
- for reply in posts
- if post = thread.posts[reply.no]
+ for postData in data
+ if post = thread.posts[postData.no]
filesCount++ if 'file' of post
postsRoot.push post.nodes.root
continue
- root = Build.postFromObject reply, thread.board.ID
+ root = Build.postFromObject postData, thread.board.ID
post = new Post root, thread, thread.board
- link = $ 'a[title="Highlight this post"]', root
- link.href = "res/#{thread}#p#{post}"
- link.nextSibling.href = "res/#{thread}#q#{post}"
filesCount++ if 'file' of post
- postsObj.push post
+ posts.push post
postsRoot.push root
- Main.callbackNodes Post, postsObj
+ Main.callbackNodes Post, posts
$.after a, postsRoot
- postsCount = postsRoot.length
+ postsCount = postsRoot.length
a.textContent = ExpandThread.text '-', postsCount, filesCount
# Enable 4chan features.
if Conf['Enable 4chan\'s Extension']
- $.globalEval "Parser.parseThread(#{thread.ID}, 1, #{postsCount})"
+ $.globalEval "Parser.parseThread(#{thread}, 1, #{postsCount})"
else
Fourchan.parseThread thread.ID, 1, postsCount
diff --git a/src/Miscellaneous/Fourchan.coffee b/src/Miscellaneous/Fourchan.coffee
index 64b1f4562..6f290d662 100644
--- a/src/Miscellaneous/Fourchan.coffee
+++ b/src/Miscellaneous/Fourchan.coffee
@@ -6,8 +6,9 @@ Fourchan =
if board is 'g'
$.globalEval """
window.addEventListener('prettyprint', function(e) {
- var pre = e.detail;
- pre.innerHTML = prettyPrintOne(pre.innerHTML);
+ window.dispatchEvent(new CustomEvent('prettyprint:cb', {
+ detail: prettyPrintOne(e.detail)
+ }));
}, false);
"""
Post.callbacks.push
@@ -32,9 +33,11 @@ Fourchan =
cb: @math
code: ->
return if @isClone
+ apply = (e) -> pre.innerHTML = e.detail
+ $.on window, 'prettyprint:cb', apply
for pre in $$ '.prettyprint:not(.prettyprinted)', @nodes.comment
- $.event 'prettyprint', pre, window
- $.addClass pre, 'prettyprinted'
+ $.event 'prettyprint', pre.innerHTML, window
+ $.off window, 'prettyprint:cb', apply
return
math: ->
return if @isClone or !$ '.math', @nodes.comment
diff --git a/src/Miscellaneous/Keybinds.coffee b/src/Miscellaneous/Keybinds.coffee
index a06a6043b..00ee147cd 100644
--- a/src/Miscellaneous/Keybinds.coffee
+++ b/src/Miscellaneous/Keybinds.coffee
@@ -7,7 +7,7 @@ Keybinds =
init = ->
$.off d, '4chanXInitFinished', init
- $.on d, 'keydown', Keybinds.keydown
+ $.on d, 'keydown', Keybinds.keydown
for node in $$ '[accesskey]'
node.removeAttribute 'accesskey'
return
@@ -58,11 +58,15 @@ Keybinds =
Keybinds.tags 'math', target
when Conf['Submit QR']
QR.submit() if QR.nodes and !QR.status()
- # Thread related
+ # Index/Thread related
+ when Conf['Update']
+ switch g.VIEW
+ when 'thread'
+ ThreadUpdater.update()
+ when 'index'
+ Index.update()
when Conf['Watch']
ThreadWatcher.toggle thread
- when Conf['Update']
- ThreadUpdater.update()
# Images
when Conf['Expand image']
Keybinds.img threadRoot
@@ -70,15 +74,18 @@ Keybinds =
Keybinds.img threadRoot, true
# Board Navigation
when Conf['Front page']
- window.location = "/#{g.BOARD}/0#delform"
+ if g.VIEW is 'index'
+ Index.pageNav 0
+ else
+ window.location = "/#{g.BOARD}/"
when Conf['Open front page']
- $.open "/#{g.BOARD}/#delform"
+ $.open "/#{g.BOARD}/"
when Conf['Next page']
- if form = $ '.next form'
- window.location = form.action
+ return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
+ $('.next button', Index.pagelist).click()
when Conf['Previous page']
- if form = $ '.prev form'
- window.location = form.action
+ return unless g.VIEW is 'index' and Conf['Index Mode'] is 'paged'
+ $('.prev button', Index.pagelist).click()
when Conf['Search form']
$.id('search-btn').click()
# Thread Navigation
@@ -176,43 +183,31 @@ Keybinds =
location.href = url
hl: (delta, thread) ->
+ postEl = $ '.reply.highlight', thread
+
unless delta
- if postEl = $ '.reply.highlight', thread
- $.rmClass postEl, 'highlight'
+ $.rmClass postEl, 'highlight' if postEl
return
- if Conf['Bottom header']
- topMargin = 0
- else
- headRect = Header.toggle.getBoundingClientRect()
- topMargin = headRect.top + headRect.height
- if postEl = $ '.reply.highlight', thread
- $.rmClass postEl, 'highlight'
- rect = postEl.getBoundingClientRect()
- if rect.bottom >= topMargin and rect.top <= doc.clientHeight # We're at least partially visible
+
+ if postEl
+ {height} = postEl.getBoundingClientRect()
+ if Header.getTopOf(postEl) >= -height and Header.getBottomOf(postEl) >= -height # We're at least partially visible
root = postEl.parentNode
axe = if delta is +1
'following'
else
'preceding'
- next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
- unless next
- @focus postEl
- return
- return unless g.VIEW is 'thread' or $.x('ancestor::div[parent::div[@class="board"]]', next) is thread
- rect = next.getBoundingClientRect()
- if rect.top < 0 or rect.bottom > doc.clientHeight
- if delta is -1
- window.scrollBy 0, rect.top - topMargin
- else
- next.scrollIntoView false
+ return unless next = $.x "#{axe}-sibling::div[contains(@class,'replyContainer')][1]/child::div[contains(@class,'reply')]", root
+ Header.scrollToIfNeeded next, delta is +1
@focus next
+ $.rmClass postEl, 'highlight'
return
+ $.rmClass postEl, 'highlight'
replies = $$ '.reply', thread
replies.reverse() if delta is -1
for reply in replies
- rect = reply.getBoundingClientRect()
- if delta is +1 and rect.top >= topMargin or delta is -1 and rect.bottom <= doc.clientHeight
+ if delta is +1 and Header.getTopOf(reply) > 0 or delta is -1 and Header.getBottomOf(reply) > 0
@focus reply
return
diff --git a/src/Miscellaneous/Nav.coffee b/src/Miscellaneous/Nav.coffee
index 2b7e23354..3620080f0 100644
--- a/src/Miscellaneous/Nav.coffee
+++ b/src/Miscellaneous/Nav.coffee
@@ -38,29 +38,24 @@ Nav =
else
Nav.scroll +1
- getThread: (full) ->
- if Conf['Bottom header']
- topMargin = 0
- else
- headRect = Header.toggle.getBoundingClientRect()
- topMargin = headRect.top + headRect.height
- threads = $$('.thread').filter (thread) ->
- thread = Get.threadFromRoot thread
- !(thread.isHidden and !thread.stub)
- for thread, i in threads
- rect = thread.getBoundingClientRect()
- if rect.bottom > topMargin # not scrolled past
- return if full then [threads, thread, i, rect, topMargin] else thread
+ getThread: ->
+ for threadRoot in $$ '.thread'
+ thread = Get.threadFromRoot threadRoot
+ continue if thread.isHidden and !thread.stub
+ if Header.getTopOf(threadRoot) >= -threadRoot.getBoundingClientRect().height # not scrolled past
+ return threadRoot
return $ '.board'
scroll: (delta) ->
- [threads, thread, i, rect, topMargin] = Nav.getThread true
- top = rect.top - topMargin
-
- # unless we're not at the beginning of the current thread
- # (and thus wanting to move to beginning)
- # or we're above the first thread and don't want to skip it
- if (delta is -1 and top > -5) or (delta is +1 and top < 5)
- top = threads[i + delta]?.getBoundingClientRect().top - topMargin
-
- window.scrollBy 0, top
+ thread = Nav.getThread()
+ axe = if delta is +1
+ 'following'
+ else
+ 'preceding'
+ if next = $.x "#{axe}-sibling::div[contains(@class,'thread')][1]", thread
+ # Unless we're not at the beginning of the current thread,
+ # and thus wanting to move to beginning,
+ # or we're above the first thread and don't want to skip it.
+ top = Header.getTopOf thread
+ thread = next if delta is +1 and top < 5 or delta is -1 and top > -5
+ Header.scrollTo thread
diff --git a/src/Monitoring/Favicon.coffee b/src/Monitoring/Favicon.coffee
index 806134034..6085a6f2c 100644
--- a/src/Monitoring/Favicon.coffee
+++ b/src/Monitoring/Favicon.coffee
@@ -45,5 +45,5 @@ Favicon =
Favicon.unread = Favicon.unreadNSFW
Favicon.unreadY = Favicon.unreadNSFWY
- dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>'
- logo: 'data:image/png;base64,<%= grunt.file.read("img/icon128.png", {encoding: "base64"}) %>'
+ dead: 'data:image/gif;base64,<%= grunt.file.read("img/favicons/dead.gif", {encoding: "base64"}) %>'
+ logo: 'data:image/png;base64,<%= grunt.file.read("img/icon128.png", {encoding: "base64"}) %>'
diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee
index f80154cae..89d1be0c8 100644
--- a/src/Monitoring/ThreadStats.coffee
+++ b/src/Monitoring/ThreadStats.coffee
@@ -1,9 +1,7 @@
ThreadStats =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Stats']
- @dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', """
- <%= grunt.file.read('html/Monitoring/ThreadStats.html').replace(/>\s+<').trim() %>
- """
+ @dialog = UI.dialog 'thread-stats', 'bottom: 0; left: 0;', <%= importHTML('Monitoring/ThreadStats') %>
@postCountEl = $ '#post-count', @dialog
@fileCountEl = $ '#file-count', @dialog
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index 677db825d..0ef1d04bf 100644
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -2,14 +2,19 @@ ThreadUpdater =
init: ->
return if g.VIEW isnt 'thread' or !Conf['Thread Updater']
+ @button = $.el 'a',
+ className: 'thread-refresh-shortcut fa fa-refresh'
+ title: 'Refresh Thread'
+ href: 'javascript:;'
+ $.on @button, 'click', @update
+ Header.addShortcut @button, 1
+
html = ''
for name, conf of Config.updater.checkbox
checked = if Conf[name] then 'checked' else ''
html += " #{name}
"
- html = """
- <%= grunt.file.read('html/Monitoring/ThreadUpdater.html').replace(/>\s+<').trim() %>
- """
+ html = <%= importHTML('Monitoring/ThreadUpdater') %>
@dialog = UI.dialog 'updater', 'bottom: 0; right: 0;', html
@timer = $ '#update-timer', @dialog
@@ -81,6 +86,7 @@ ThreadUpdater =
ThreadUpdater.interval = @value = val
$.cb.value.call @ if e
load: (e) ->
+ $.rmClass ThreadUpdater.button, 'fa-spin'
{req} = ThreadUpdater
delete ThreadUpdater.req
if e.type isnt 'loadend' # timeout or abort
@@ -143,9 +149,10 @@ ThreadUpdater =
update: ->
return unless navigator.onLine
+ $.addClass ThreadUpdater.button, 'fa-spin'
ThreadUpdater.count()
ThreadUpdater.set 'timer', '...'
- ThreadUpdater.req.abort() if ThreadUpdater.req
+ ThreadUpdater.req?.abort()
url = "//api.4chan.org/#{ThreadUpdater.thread.board}/res/#{ThreadUpdater.thread}.json"
ThreadUpdater.req = $.ajax url,
onabort: ThreadUpdater.cb.load
@@ -155,38 +162,27 @@ ThreadUpdater =
,
whenModified: true
- updateThreadStatus: (title, OP) ->
- titleLC = title.toLowerCase()
- return if ThreadUpdater.thread["is#{title}"] is !!OP[titleLC]
- unless ThreadUpdater.thread["is#{title}"] = !!OP[titleLC]
- message = if title is 'Sticky'
- 'The thread is not a sticky anymore.'
+ updateThreadStatus: (type, status) ->
+ return unless hasChanged = ThreadUpdater.thread["is#{type}"] isnt status
+ ThreadUpdater.thread.setStatus type, status
+ change = if type is 'Sticky'
+ if status
+ 'now a sticky'
else
- 'The thread is not closed anymore.'
- new Notice 'info', message, 30
- $.rm $ ".#{titleLC}Icon", ThreadUpdater.thread.OP.nodes.info
- return
- message = if title is 'Sticky'
- 'The thread is now a sticky.'
+ 'not a sticky anymore'
else
- 'The thread is now closed.'
- new Notice 'info', message, 30
- icon = $.el 'img',
- src: "//static.4chan.org/image/#{titleLC}.gif"
- alt: title
- title: title
- className: "#{titleLC}Icon"
- root = $ '[title="Quote this post"]', ThreadUpdater.thread.OP.nodes.info
- if title is 'Closed'
- root = $('.stickyIcon', ThreadUpdater.thread.OP.nodes.info) or root
- $.after root, [$.tn(' '), icon]
+ if status
+ 'now closed'
+ else
+ 'not closed anymore'
+ new Notice 'info', "The thread is #{change}.", 30
parse: (postObjects) ->
OP = postObjects[0]
Build.spoilerRange[ThreadUpdater.thread.board] = OP.custom_spoiler
- ThreadUpdater.updateThreadStatus 'Sticky', OP
- ThreadUpdater.updateThreadStatus 'Closed', OP
+ ThreadUpdater.updateThreadStatus 'Sticky', !!OP.sticky
+ ThreadUpdater.updateThreadStatus 'Closed', !!OP.closed
ThreadUpdater.thread.postLimit = !!OP.bumplimit
ThreadUpdater.thread.fileLimit = !!OP.imagelimit
@@ -250,15 +246,14 @@ ThreadUpdater =
ThreadUpdater.lastPost = posts[count - 1].ID
Main.callbackNodes Post, posts
- scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and
- ThreadUpdater.root.getBoundingClientRect().bottom - doc.clientHeight < 25
+ scroll = Conf['Auto Scroll'] and ThreadUpdater.scrollBG() and Header.getBottomOf(ThreadUpdater.root) > -25
$.add ThreadUpdater.root, nodes
sendEvent()
if scroll
if Conf['Bottom Scroll']
window.scrollTo 0, d.body.clientHeight
else
- Header.scrollToPost nodes[0]
+ Header.scrollTo nodes[0]
# Enable 4chan features.
threadID = ThreadUpdater.thread.ID
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 54bfb9435..3b021d5a7 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -3,15 +3,17 @@ ThreadWatcher =
return if !Conf['Thread Watcher']
@db = new DataBoard 'watchedThreads', @refresh, true
- @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', """
- <%= grunt.file.read('html/Monitoring/ThreadWatcher.html').replace(/>\s+<').trim() %>
- """
+ @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %>
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild
$.on d, 'QRPostSuccessful', @cb.post
- $.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on d, '4chanXInitFinished', @ready
+ switch g.VIEW
+ when 'index'
+ $.on d, 'IndexRefresh', @cb.onIndexRefresh
+ when 'thread'
+ $.on d, 'ThreadUpdate', @cb.onThreadRefresh
now = Date.now()
if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
@@ -69,7 +71,17 @@ ThreadWatcher =
$.set 'AutoWatch', threadID
else if Conf['Auto Watch Reply']
ThreadWatcher.add board.threads[threadID]
- threadUpdate: (e) ->
+ onIndexRefresh: ->
+ {db} = ThreadWatcher
+ boardID = g.BOARD.ID
+ for threadID, data of db.data.boards[boardID] when not data.isDead and threadID not of g.BOARD.threads
+ if Conf['Auto Prune']
+ ThreadWatcher.db.delete {boardID, threadID}
+ else
+ data.isDead = true
+ ThreadWatcher.db.set {boardID, threadID, val: data}
+ ThreadWatcher.refresh()
+ onThreadRefresh: (e) ->
{thread} = e.detail
return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
# Update 404 status.
@@ -100,7 +112,7 @@ ThreadWatcher =
ThreadWatcher.status.textContent = status
return if @status isnt 404
if Conf['Auto Prune']
- ThreadWatcher.rm boardID, threadID
+ ThreadWatcher.db.delete {boardID, threadID}
else
data.isDead = true
ThreadWatcher.db.set {boardID, threadID, val: data}
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index fc7e2940e..8e13c463e 100644
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -42,18 +42,16 @@ Unread =
while root = $.x 'preceding-sibling::div[contains(@class,"replyContainer")][1]', post.nodes.root
break unless (post = Get.postFromRoot root).isHidden
return unless root
- onload = -> root.scrollIntoView false if checkPosition root
+ down = true
else
# Scroll to the last read post.
posts = Object.keys Unread.thread.posts
{root} = Unread.thread.posts[posts[posts.length - 1]].nodes
- onload = -> Header.scrollToPost root if checkPosition root
- checkPosition = (target) ->
- # Scroll to the target unless we scrolled past it.
- target.getBoundingClientRect().bottom > doc.clientHeight
# Prevent the browser to scroll back to
# the previous scroll location on page load.
- $.on window, 'load', onload
+ $.on window, 'load', ->
+ # Scroll to the target unless we scrolled past it.
+ Header.scrollTo root, down if Header.getBottomOf(root) < 0
sync: ->
lastReadPost = Unread.db.get
@@ -102,7 +100,7 @@ Unread =
body: post.info.comment
icon: Favicon.logo
notif.onclick = ->
- Header.scrollToPost post.nodes.root
+ Header.scrollToIfNeeded post.nodes.root, true
window.focus()
notif.onshow = ->
setTimeout ->
@@ -132,9 +130,8 @@ Unread =
read: (e) ->
return if d.hidden or !Unread.posts.length
- height = doc.clientHeight
for post, i in Unread.posts
- break if post.nodes.root.getBoundingClientRect().bottom > height # post is not completely read
+ break if Header.getBottomOf(post.nodes.root) < -1 # post is not completely read
return unless i
Unread.lastReadPost = Unread.posts.splice(0, i)[i - 1].ID
diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee
index d0be4c445..7a8f829ea 100644
--- a/src/Posting/QR.coffee
+++ b/src/Posting/QR.coffee
@@ -26,7 +26,7 @@ QR =
$.event 'CloseMenu'
QR.open()
QR.nodes.com.focus()
- Header.addShortcut sc, 1
+ Header.addShortcut sc, 2
$.on d, 'QRGetSelectedPost', ({detail: cb}) ->
cb QR.selected
@@ -39,11 +39,15 @@ QR =
$.on d, 'dragover', QR.dragOver
$.on d, 'drop', QR.dropFile
$.on d, 'dragstart dragend', QR.drag
- $.on d, 'ThreadUpdate', ->
- if g.DEAD
- QR.abort()
- else
- QR.status()
+ switch g.VIEW
+ when 'index'
+ $.on d, 'IndexRefresh', QR.generatePostableThreadsList
+ when 'thread'
+ $.on d, 'ThreadUpdate', ->
+ if g.DEAD
+ QR.abort()
+ else
+ QR.status()
QR.persist() if Conf['Persistent QR']
@@ -697,7 +701,7 @@ QR =
imgContainer = $.el 'div',
className: 'captcha-img'
- title: 'Reload'
+ title: 'Reload reCAPTCHA'
innerHTML: ' '
input = $.el 'input',
className: 'captcha-input field'
@@ -796,10 +800,27 @@ QR =
return
e.preventDefault()
+ generatePostableThreadsList: ->
+ return unless QR.nodes
+ list = QR.nodes.thread
+ options = [list.firstChild]
+ for thread of g.BOARD.threads
+ options.push $.el 'option',
+ value: thread
+ textContent: "Thread No.#{thread}"
+ val = list.value
+ $.rmAll list
+ $.add list, options
+ list.value = val
+ return unless list.value
+ # Fix the value if the option disappeared.
+ list.value = if g.VIEW is 'thread'
+ g.THREADID
+ else
+ 'new'
+
dialog: ->
- dialog = UI.dialog 'qr', 'top:0;right:0;', """
- <%= grunt.file.read('html/Posting/QR.html').replace(/>\s+<').trim() %>
- """
+ dialog = UI.dialog 'qr', 'top:0;right:0;', <%= importHTML('Posting/QR') %>
QR.nodes = nodes =
el: dialog
@@ -867,12 +888,6 @@ QR =
nodes.flag.dataset.default = '0'
$.add nodes.form, nodes.flag
- # Make a list of threads.
- for thread of g.BOARD.threads
- $.add nodes.thread, $.el 'option',
- value: thread
- textContent: "Thread No.#{thread}"
-
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
for elm in $$ '*', QR.nodes.el
@@ -906,6 +921,7 @@ QR =
$.set 'QR Size', @style.cssText
<% } %>
+ QR.generatePostableThreadsList()
QR.persona.init()
new QR.post true
QR.status()