-
-
+
diff --git a/img/changelog/3.16.0/0.png b/img/changelog/3.16.0/0.png
new file mode 100644
index 000000000..c7fe125f4
Binary files /dev/null and b/img/changelog/3.16.0/0.png differ
diff --git a/json/archives.json b/json/archives.json
index a19808354..0a8e29d4e 100644
--- a/json/archives.json
+++ b/json/archives.json
@@ -5,8 +5,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["a", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
- "files": ["a", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
+ "boards": ["a", "biz", "co", "gd", "jp", "m", "sp", "tg", "tv", "v", "vg", "vp", "vr", "wsg"],
+ "files": ["a", "biz", "gd", "jp", "m", "tg", "vg", "vp", "vr", "wsg"]
}, {
"uid": 1,
"name": "NSFW Foolz",
@@ -86,8 +86,8 @@
"http": false,
"https": true,
"software": "fuuka",
- "boards": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
- "files": ["3", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
+ "boards": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"],
+ "files": ["3", "biz", "cgl", "ck", "fa", "ic", "jp", "lit", "tg", "vr"]
}, {
"uid": 15,
"name": "fgts",
@@ -95,8 +95,8 @@
"http": true,
"https": true,
"software": "foolfuuka",
- "boards": ["soc"],
- "files": ["soc"]
+ "boards": ["r", "soc"],
+ "files": ["r", "soc"]
}, {
"uid": 16,
"name": "maware",
@@ -123,6 +123,6 @@
"https": true,
"withCredentials": true,
"software": "foolfuuka",
- "boards": ["a", "co", "gd", "jp", "m", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
- "files": ["a", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
+ "boards": ["a", "biz", "co", "d", "gd", "jp", "m", "mlp", "s4s", "sp", "tg", "tv", "u", "v", "vg", "vp", "vr", "wsg"],
+ "files": ["a", "biz", "d", "gd", "jp", "m", "s4s", "tg", "u", "vg", "vp", "vr", "wsg"]
}]
diff --git a/lib/$.coffee b/lib/$.coffee
index c50412cbf..939c1577e 100644
--- a/lib/$.coffee
+++ b/lib/$.coffee
@@ -99,17 +99,11 @@ $.rmClass = (el, className...) ->
el.classList.remove className...
$.hasClass = (el, className) ->
el.classList.contains className
-$.rm = do ->
- if 'remove' of Element.prototype
- (el) -> el.remove()
- else
- (el) -> el.parentNode?.removeChild el
+$.rm = (el) ->
+ el.remove()
$.rmAll = (root) ->
- # jsperf.com/emptify-element
- for node in [root.childNodes...]
- # HTMLSelectElement.remove !== Element.remove
- root.removeChild node
- return
+ # https://gist.github.com/MayhemYDG/8646194
+ root.textContent = null
$.tn = (s) ->
d.createTextNode s
$.nodes = (nodes) ->
@@ -234,81 +228,91 @@ $.localKeys = [
'usercss'
]
# https://developer.chrome.com/extensions/storage.html
-$.delete = (keys) ->
- chrome.storage.sync.remove keys
-$.get = (key, val, cb) ->
- if typeof cb is 'function'
- items = $.item key, val
- else
- items = key
- cb = val
-
- localItems = null
- syncItems = null
- for key, val of items
- if key in $.localKeys
- (localItems or= {})[key] = val
- else
- (syncItems or= {})[key] = val
-
- count = 0
- done = (item) ->
- if chrome.runtime.lastError
- c.error chrome.runtime.lastError.message
- $.extend items, item
- cb items unless --count
-
- if localItems
- count++
- chrome.storage.local.get localItems, done
- if syncItems
- count++
- chrome.storage.sync.get syncItems, done
-$.set = do ->
+do ->
items =
- sync: {}
local: {}
- timeout = {}
+ sync: {}
+ $.delete = (keys) ->
+ if typeof keys is 'string'
+ keys = [keys]
+ for key in keys
+ delete items.local[key]
+ delete items.sync[key]
+ chrome.storage.sync.remove keys
+
+ $.get = (key, val, cb) ->
+ if typeof cb is 'function'
+ data = $.item key, val
+ else
+ data = key
+ cb = val
+
+ localItems = null
+ syncItems = null
+ for key, val of data
+ if key in $.localKeys
+ (localItems or= {})[key] = val
+ else
+ (syncItems or= {})[key] = val
+
+ count = 0
+ done = (result) ->
+ if chrome.runtime.lastError
+ c.error chrome.runtime.lastError.message
+ $.extend data, result
+ cb data unless --count
+
+ if localItems
+ count++
+ chrome.storage.local.get localItems, done
+ if syncItems
+ count++
+ chrome.storage.sync.get syncItems, done
+
+ timeout = {}
setArea = (area) ->
data = items[area]
- return if !Object.keys(data).length or timeout[area]
- items[area] = {}
+ return if !Object.keys(data).length or timeout[area] > Date.now()
chrome.storage[area].set data, ->
if chrome.runtime.lastError
c.error chrome.runtime.lastError.message
for key, val of data when key not of items[area]
+ if area is 'sync' and chrome.storage.sync.QUOTA_BYTES_PER_ITEM < JSON.stringify(val).length + key.length
+ c.error chrome.runtime.lastError.message, key, val
+ continue
items[area][key] = val
- timeout[area] = setTimeout setArea, $.MINUTE, area
+ setTimeout setArea, $.MINUTE, area
+ timeout[area] = Date.now() + $.MINUTE
return
delete timeout[area]
+ items[area] = {}
- setAll = $.debounce $.SECOND, ->
- for key in $.localKeys
- if key of items.sync
- items.local[key] = items.sync[key]
- delete items.sync[key]
- try
- setArea 'local'
- setArea 'sync'
- catch err
- c.error err.stack
+ setSync = $.debounce $.SECOND, ->
+ setArea 'sync'
- (key, val) ->
+ $.set = (key, val) ->
if typeof key is 'string'
items.sync[key] = val
else
$.extend items.sync, key
- setAll()
-$.clear = (cb) ->
- count = 2
- done = ->
- if chrome.runtime.lastError
- c.error chrome.runtime.lastError.message
- return
- cb?() unless --count
- chrome.storage.local.clear done
- chrome.storage.sync.clear done
+ for key in $.localKeys when key of items.sync
+ items.local[key] = items.sync[key]
+ delete items.sync[key]
+ setArea 'local'
+ setSync()
+
+ $.clear = (cb) ->
+ items.local = {}
+ items.sync = {}
+ count = 2
+ done = ->
+ if chrome.runtime.lastError
+ c.error chrome.runtime.lastError.message
+ return
+ cb?() unless --count
+ chrome.storage.local.clear done
+ chrome.storage.sync.clear done
<% } else { %>
# http://wiki.greasespot.net/Main_Page
$.sync = do ->
diff --git a/package.json b/package.json
index 6babef4e9..737696770 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "4chan-X",
- "version": "3.15.2",
+ "version": "3.18.0",
"description": "Cross-browser extension for productive lurking on 4chan.",
"meta": {
"name": "4chan X",
@@ -26,13 +26,13 @@
"grunt-bump": "~0.0.13",
"grunt-concurrent": "~0.4.3",
"grunt-contrib-clean": "~0.5.0",
- "grunt-contrib-coffee": "~0.8.2",
+ "grunt-contrib-coffee": "~0.10.0",
"grunt-contrib-compress": "~0.6.0",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-copy": "~0.5.0",
"grunt-contrib-watch": "~0.5.3",
"grunt-shell": "~0.6.4",
- "load-grunt-tasks": "~0.2.1"
+ "load-grunt-tasks": "~0.3.0"
},
"repository": {
"type": "git",
diff --git a/src/Archive/Redirect.coffee b/src/Archive/Redirect.coffee
index 67cf6d575..25a773fc8 100644
--- a/src/Archive/Redirect.coffee
+++ b/src/Archive/Redirect.coffee
@@ -45,7 +45,7 @@ Redirect =
cb?()
to: (dest, data) ->
- archive = (if dest is 'search' then Redirect.data.thread else Redirect.data[dest])[data.boardID]
+ archive = (if dest in ['search', 'board'] then Redirect.data.thread else Redirect.data[dest])[data.boardID]
return '' unless archive
Redirect[dest] archive, data
@@ -80,6 +80,9 @@ Redirect =
file: (archive, {boardID, filename}) ->
"#{Redirect.protocol archive}#{archive.domain}/#{boardID}/full_image/#{filename}"
+ board: (archive, {boardID}) ->
+ "#{Redirect.protocol archive}#{archive.domain}/#{boardID}/"
+
search: (archive, {boardID, type, value}) ->
type = if type is 'name'
'username'
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index eee0dfae9..62abf61c3 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -1,7 +1,7 @@
Filter =
filters: {}
init: ->
- return if g.VIEW is 'catalog' or !Conf['Filter']
+ return if !Conf['Filter']
for key of Config.filter
@filters[key] = []
@@ -110,6 +110,8 @@ Filter =
# Highlight
$.addClass @nodes.root, result.class
+ unless @highlights and result.class in @highlights
+ (@highlights or= []).push result.class
if !@isReply and result.top
@thread.isOnTop = true
@@ -164,7 +166,7 @@ Filter =
menu:
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Filter']
+ return if !Conf['Menu'] or !Conf['Filter']
div = $.el 'div',
textContent: 'Filter'
diff --git a/src/Filtering/PostHiding.coffee b/src/Filtering/PostHiding.coffee
index 122a7c8c4..ed621d194 100644
--- a/src/Filtering/PostHiding.coffee
+++ b/src/Filtering/PostHiding.coffee
@@ -1,6 +1,6 @@
PostHiding =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link']
+ return if !Conf['Reply Hiding'] and !Conf['Reply Hiding Link']
@db = new DataBoard 'hiddenPosts'
Post.callbacks.push
@@ -20,7 +20,7 @@ PostHiding =
menu:
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link']
+ return if !Conf['Menu'] or !Conf['Reply Hiding Link']
# Hide
div = $.el 'div',
diff --git a/src/Filtering/Recursive.coffee b/src/Filtering/Recursive.coffee
index da5496ceb..51d7a406b 100644
--- a/src/Filtering/Recursive.coffee
+++ b/src/Filtering/Recursive.coffee
@@ -1,8 +1,6 @@
Recursive =
recursives: {}
init: ->
- return if g.VIEW is 'catalog'
-
Post.callbacks.push
name: 'Recursive'
cb: @node
diff --git a/src/Filtering/ThreadHiding.coffee b/src/Filtering/ThreadHiding.coffee
index 6d5635201..b1daef857 100644
--- a/src/Filtering/ThreadHiding.coffee
+++ b/src/Filtering/ThreadHiding.coffee
@@ -1,10 +1,9 @@
ThreadHiding =
init: ->
- return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link']
+ return if g.VIEW isnt 'index'
@db = new DataBoard 'hiddenThreads'
- @syncCatalog()
- $.on d, 'IndexBuild', @onIndexBuild
+ $.on d, 'IndexRefresh', @onIndexRefresh
Thread.callbacks.push
name: 'Thread Hiding'
cb: @node
@@ -15,61 +14,17 @@ ThreadHiding =
return unless Conf['Thread Hiding']
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
- onIndexBuild: ({detail: nodes}) ->
- for root, i in nodes by 2
+ onIndexRefresh: ->
+ for root, i in Index.nodes by 2
thread = Get.threadFromRoot root
continue unless thread.isHidden
unless thread.stub
- nodes[i + 1].hidden = true
+ Index.nodes[i + 1].hidden = true
else unless root.contains thread.stub
# When we come back to a page, the stub is already there.
ThreadHiding.makeStub thread, root
return
- syncCatalog: ->
- # Sync hidden threads from the catalog into the index.
- hiddenThreads = ThreadHiding.db.get
- boardID: g.BOARD.ID
- defaultValue: {}
- hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
-
- # Add threads that were hidden in the catalog.
- for threadID of hiddenThreadsOnCatalog
- unless threadID of hiddenThreads
- hiddenThreads[threadID] = {}
-
- # Remove threads that were un-hidden in the catalog.
- for threadID of hiddenThreads
- unless threadID of hiddenThreadsOnCatalog
- delete hiddenThreads[threadID]
-
- if (ThreadHiding.db.data.lastChecked or 0) > Date.now() - $.MINUTE
- # Was cleaned just now.
- ThreadHiding.cleanCatalog hiddenThreadsOnCatalog
-
- unless Object.keys(hiddenThreads).length
- ThreadHiding.db.delete boardID: g.BOARD.ID
- return
- ThreadHiding.db.set
- boardID: g.BOARD.ID
- val: hiddenThreads
-
- cleanCatalog: (hiddenThreadsOnCatalog) ->
- # We need to clean hidden threads on the catalog ourselves,
- # otherwise if we don't visit the catalog regularly
- # it will pollute the localStorage and our data.
- $.cache "//a.4cdn.org/#{g.BOARD}/threads.json", ->
- return unless @status is 200
- threads = {}
- for page in @response
- for thread in page.threads
- if thread.no of hiddenThreadsOnCatalog
- threads[thread.no] = hiddenThreadsOnCatalog[thread.no]
- if Object.keys(threads).length
- localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify threads
- else
- localStorage.removeItem "4chan-hide-t-#{g.BOARD}"
-
menu:
init: ->
return if g.VIEW isnt 'index' or !Conf['Menu'] or !Conf['Thread Hiding Link']
@@ -91,7 +46,7 @@ ThreadHiding =
el: div
order: 20
open: ({thread, isReply}) ->
- if isReply or thread.isHidden
+ if isReply or thread.isHidden or Conf['Index Mode'] is 'catalog'
return false
ThreadHiding.menu.thread = thread
true
@@ -130,19 +85,15 @@ ThreadHiding =
$.prepend root, thread.stub
saveHiddenState: (thread, makeStub) ->
- hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
if thread.isHidden
ThreadHiding.db.set
boardID: thread.board.ID
threadID: thread.ID
val: {makeStub}
- hiddenThreadsOnCatalog[thread] = true
else
ThreadHiding.db.delete
boardID: thread.board.ID
threadID: thread.ID
- delete hiddenThreadsOnCatalog[thread]
- localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog
toggle: (thread) ->
unless thread instanceof Thread
@@ -157,6 +108,7 @@ ThreadHiding =
return if thread.isHidden
threadRoot = thread.OP.nodes.root.parentNode
thread.isHidden = true
+ Index.updateHideLabel()
unless makeStub
threadRoot.hidden = threadRoot.nextElementSibling.hidden = true #
@@ -171,3 +123,4 @@ ThreadHiding =
threadRoot = thread.OP.nodes.root.parentNode
threadRoot.nextElementSibling.hidden =
threadRoot.hidden = thread.isHidden = false
+ Index.updateHideLabel()
diff --git a/src/General/Build.coffee b/src/General/Build.coffee
index 6fa1278b8..5cb9d1dca 100644
--- a/src/General/Build.coffee
+++ b/src/General/Build.coffee
@@ -110,18 +110,18 @@ Build =
flag = unless flagCode
''
else if boardID is 'pol'
- "
"
+ "
"
else
"
"
if file?.isDeleted
fileHTML = if isOP
"
" +
- " " +
+ " " +
" "
else
"
" +
- " " +
+ " " +
" "
else if file
fileSize = $.bytesToString file.size
@@ -167,16 +167,16 @@ Build =
fileHTML = ''
sticky = if isSticky
- "
"
+ "
"
else
''
closed = if isClosed
- "
"
+ "
"
else
''
if isOP and g.VIEW is 'index'
- pageNum = Math.floor Index.liveThreadIDs.indexOf(postID) / Index.threadsNumPerPage
+ pageNum = Index.liveThreadIDs.indexOf(postID) // Index.threadsNumPerPage
pageIcon = "
Page #{pageNum} "
replyLink = "
[Reply ] "
else
@@ -254,9 +254,74 @@ Build =
[posts, files] = if Conf['Show Replies']
[data.omitted_posts, data.omitted_images]
else
- # XXX data.images is not accurate.
- [data.replies, data.omitted_images + data.last_replies.filter((data) -> !!data.ext).length]
+ [data.replies, data.images]
nodes.push Build.summary board.ID, data.no, posts, files
$.add root, nodes
root
+ catalogThread: (thread) ->
+ {staticPath, gifIcon} = Build
+ data = Index.liveThreadData[Index.liveThreadIDs.indexOf thread.ID]
+
+ postCount = data.replies + 1
+ fileCount = data.images + !!data.ext
+ pageCount = Index.liveThreadIDs.indexOf(thread.ID) // Index.threadsNumPerPage
+
+ subject = if thread.OP.info.subject
+ "
#{thread.OP.info.subject}
"
+ else
+ ''
+ comment = thread.OP.nodes.comment.innerHTML.replace /(
\s*){2,}/g, '
'
+
+ root = $.el 'div',
+ className: 'catalog-thread'
+ innerHTML: <%= importHTML('General/Thread-catalog-view') %>
+
+ root.dataset.fullID = thread.fullID
+ $.addClass root, 'pinned' if thread.isPinned
+ $.addClass root, thread.OP.highlights... if thread.OP.highlights
+
+ thumb = root.firstElementChild
+ if data.spoiler and !Conf['Reveal Spoilers']
+ src = "#{staticPath}spoiler"
+ if spoilerRange = Build.spoilerRange[thread.board]
+ # Randomize the spoiler image.
+ src += "-#{thread.board}" + Math.floor 1 + spoilerRange * Math.random()
+ src += '.png'
+ $.addClass thumb, 'spoiler-file'
+ else if data.filedeleted
+ src = "#{staticPath}filedeleted-res#{gifIcon}"
+ $.addClass thumb, 'deleted-file'
+ else if thread.OP.file
+ src = thread.OP.file.thumbURL
+ thumb.dataset.width = data.tn_w
+ thumb.dataset.height = data.tn_h
+ else
+ src = "#{staticPath}nofile.png"
+ $.addClass thumb, 'no-file'
+ thumb.style.backgroundImage = "url(#{src})"
+ if Conf['Open threads in a new tab']
+ thumb.target = '_blank'
+
+ for quotelink in $$ '.quotelink', root.lastElementChild
+ $.replace quotelink, [quotelink.childNodes...]
+ for pp in $$ '.prettyprint', root.lastElementChild
+ $.replace pp, $.tn pp.textContent
+
+ if thread.isSticky
+ $.add $('.thread-icons', root), $.el 'img',
+ src: "#{staticPath}sticky#{gifIcon}"
+ className: 'stickyIcon'
+ title: 'Sticky'
+ if thread.isClosed
+ $.add $('.thread-icons', root), $.el 'img',
+ src: "#{staticPath}closed#{gifIcon}"
+ className: 'closedIcon'
+ title: 'Closed'
+
+ if data.bumplimit
+ $.addClass $('.post-count', root), 'warning'
+ if data.imagelimit
+ $.addClass $('.file-count', root), 'warning'
+
+ root
diff --git a/src/General/CatalogThread.coffee b/src/General/CatalogThread.coffee
new file mode 100644
index 000000000..a7bebe7bc
--- /dev/null
+++ b/src/General/CatalogThread.coffee
@@ -0,0 +1,15 @@
+class CatalogThread
+ @callbacks = []
+ toString: -> @ID
+
+ constructor: (root, @thread) ->
+ @ID = @thread.ID
+ @board = @thread.board
+ @nodes =
+ root: root
+ thumb: $ '.thumb', root
+ icons: $ '.thread-icons', root
+ postCount: $ '.post-count', root
+ fileCount: $ '.file-count', root
+ pageCount: $ '.page-count', root
+ @thread.catalogView = @
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 14b665337..426c17b37 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -6,6 +6,7 @@ Config =
'Announcement Hiding': [true, 'Add button to hide 4chan announcements.']
'404 Redirect': [true, 'Redirect dead threads and images.']
'Keybinds': [true, 'Bind actions to keyboard shortcuts.']
+ 'Linkify': [true, 'Convert text links into hyperlinks.']
'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.']
@@ -24,11 +25,9 @@ Config =
'Auto-GIF': [false, 'Animate GIF thumbnails (disabled on /gif/, /wsg/).']
'Image Expansion': [true, 'Expand images inline.']
'Image Hover': [false, 'Show a floating expanded image on hover.']
+ 'Image Hover in Catalog': [false, 'Show a floating expanded image on hover in the catalog.']
'Sauce': [true, 'Add sauce links to images.']
'Reveal Spoilers': [false, 'Reveal spoiler thumbnails.']
- 'Linkification':
- 'Linkify': [true, 'Convert text links into hyperlinks.']
- 'Clean Links': [true, 'Remove spoiler and code tags commonly used to bypass blocked links.']
'Menu':
'Menu': [true, 'Add a drop-down menu to posts.']
'Report Link': [true, 'Add a report link to the menu.']
@@ -62,7 +61,6 @@ Config =
'Remember Spoiler': [false, 'Remember the spoiler state, instead of resetting after posting.']
'Hide Original Post Form': [true, 'Hide the normal post form.']
'Cooldown': [true, 'Indicate the remaining time before posting again.']
- 'Cooldown Prediction': [true, 'Decrease the cooldown time by taking into account upload speed. Disable it if it\'s inaccurate for you.']
<% if (type === 'crx') { %>
'Tab to Choose Files First': [false, 'Tab to the file input before the submit button.']
<% } %>
@@ -74,9 +72,7 @@ Config =
'Quote Previewing': [true, 'Show quoted post on hover.']
'Quote Highlighting': [true, 'Highlight the previewed post.']
'Resurrect Quotes': [true, 'Link dead quotes to the archives.']
- 'Mark Quotes of You': [true, 'Add \'(You)\' to quotes linking to your posts.']
- 'Mark OP Quotes': [true, 'Add \'(OP)\' to OP quotes.']
- 'Mark Cross-thread Quotes': [true, 'Add \'(Cross-thread)\' to cross-threads quotes.']
+ 'Quote Markers': [true, 'Add "(You)", "(OP)", "(Cross-thread)", "(Dead)" markers to quote links.']
imageExpansion:
'Fit width': [true, '']
'Fit height': [false, '']
@@ -141,7 +137,11 @@ Config =
'Custom CSS': false
Index:
'Index Mode': 'paged'
+ 'Previous Index Mode': 'paged'
'Index Sort': 'bump'
+ 'Index Size': 'small'
+ 'Threads per Page': 0
+ 'Open threads in a new tab': false
'Show Replies': true
'Anchor Hidden Threads': true
'Refreshed Navigation': false
@@ -149,7 +149,6 @@ Config =
'Header auto-hide': false
'Header auto-hide on scroll': false
'Bottom header': false
- 'Header catalog links': false
'Top Board List': false
'Bottom Board List': false
'Custom Board Navigation': true
@@ -188,6 +187,10 @@ Config =
'Next page': ['Right', 'Jump to the next page.']
'Previous page': ['Left', 'Jump to the previous page.']
'Search form': ['Ctrl+Alt+s', 'Focus the search field on the board index.']
+ 'Paged mode': ['Ctrl+1', 'Sets the index mode to paged.']
+ 'All pages mode': ['Ctrl+2', 'Sets the index mode to all threads.']
+ 'Catalog mode': ['Ctrl+3', 'Sets the index mode to catalog.']
+ 'Cycle sort type': ['Ctrl+x', 'Cycle through index sort types.']
# Thread Navigation
'Next thread': ['Down', 'See next thread.']
'Previous thread': ['Up', 'See previous thread.']
diff --git a/src/General/DataBoard.coffee b/src/General/DataBoard.coffee
index aba14d68d..d82ff1b65 100644
--- a/src/General/DataBoard.coffee
+++ b/src/General/DataBoard.coffee
@@ -1,5 +1,5 @@
class DataBoard
- @keys = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
+ @keys = ['pinnedThreads', 'hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts', 'watchedThreads']
constructor: (@key, sync, dontClean) ->
@data = Conf[key]
@@ -58,30 +58,31 @@ class DataBoard
val or defaultValue
clean: ->
- for boardID, val of @data.boards
- @deleteIfEmpty {boardID}
-
now = Date.now()
- if (@data.lastChecked or 0) < now - 2 * $.HOUR
- @data.lastChecked = now
- for boardID of @data.boards
- @ajaxClean boardID
+ return if (@data.lastChecked or 0) > now - 2 * $.HOUR
+ for boardID of @data.boards
+ @deleteIfEmpty {boardID}
+ @ajaxClean boardID if boardID of @data.boards
+
+ @data.lastChecked = now
@save()
ajaxClean: (boardID) ->
$.cache "//a.4cdn.org/#{boardID}/threads.json", (e) =>
if e.target.status isnt 200
- @delete boardID if e.target.status is 404
+ @delete {boardID} if e.target.status is 404
return
board = @data.boards[boardID]
threads = {}
for page in e.target.response
- for thread in page.threads
- if thread.no of board
- threads[thread.no] = board[thread.no]
- @data.boards[boardID] = threads
- @deleteIfEmpty {boardID}
- @save()
+ for thread in page.threads when thread.no of board
+ threads[thread.no] = board[thread.no]
+ count = Object.keys(threads).length
+ return if count is Object.keys(board).length # Nothing changed.
+ if count
+ @set {boardID, val: threads}
+ else
+ @delete {boardID}
onSync: (data) =>
@data = data or boards: {}
diff --git a/src/General/Get.coffee b/src/General/Get.coffee
index 9fe328a13..0490b8403 100644
--- a/src/General/Get.coffee
+++ b/src/General/Get.coffee
@@ -188,7 +188,7 @@ Get =
comment = bq.innerHTML
# greentext
- .replace(/(^|>)(>[^<$]*)(<|$)/g, '$1
$2 $3')
+ .replace /(^|>)(>[^<$]*)(<|$)/g, '$1
$2 $3'
# quotes
.replace /((>){2}(>\/[a-z\d]+\/)?\d+)/g, '
$1 '
@@ -233,5 +233,6 @@ Get =
thread = g.threads["#{boardID}.#{threadID}"] or
new Thread threadID, board
post = new Post Build.post(o, true), thread, board, {isArchived: true}
+ $('.page-num', post.nodes.info).hidden = true
Main.callbackNodes Post, [post]
Get.insert post, root, context
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index e97fcdf0f..74eb9648a 100644
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -25,8 +25,6 @@ Header =
innerHTML: '
Auto-hide header on scroll'
barPositionToggler = $.el 'label',
innerHTML: '
Bottom header'
- catalogToggler = $.el 'label',
- innerHTML: '
Use catalog board links'
topBoardToggler = $.el 'label',
innerHTML: '
Top original board list'
botBoardToggler = $.el 'label',
@@ -40,7 +38,6 @@ Header =
@headerToggler = headerToggler.firstElementChild
@scrollHeaderToggler = scrollHeaderToggler.firstElementChild
@barPositionToggler = barPositionToggler.firstElementChild
- @catalogToggler = catalogToggler.firstElementChild
@topBoardToggler = topBoardToggler.firstElementChild
@botBoardToggler = botBoardToggler.firstElementChild
@customNavToggler = customNavToggler.firstElementChild
@@ -48,7 +45,6 @@ Header =
$.on @headerToggler, 'change', @toggleBarVisibility
$.on @scrollHeaderToggler, 'change', @toggleHideBarOnScroll
$.on @barPositionToggler, 'change', @toggleBarPosition
- $.on @catalogToggler, 'change', @toggleCatalogLinks
$.on @topBoardToggler, 'change', @toggleOriginalBoardList
$.on @botBoardToggler, 'change', @toggleOriginalBoardList
$.on @customNavToggler, 'change', @toggleCustomNav
@@ -74,7 +70,6 @@ Header =
{el: headerToggler}
{el: scrollHeaderToggler}
{el: barPositionToggler}
- {el: catalogToggler}
{el: topBoardToggler}
{el: botBoardToggler}
{el: customNavToggler}
@@ -92,9 +87,6 @@ Header =
if a = $ "a[href*='/#{g.BOARD}/']", $.id 'boardNavDesktopFoot'
a.className = 'current'
- Header.setCatalogLinks Conf['Header catalog links']
- $.sync 'Header catalog links', Header.setCatalogLinks
-
@enableDesktopNotifications()
setBoardList: ->
@@ -120,10 +112,12 @@ Header =
list = $ '#custom-board-list', Header.bar
$.rmAll list
return unless text
- as = $$ '#full-board-list a[title]', Header.bar
- nodes = text.match(/[\w@]+(-(all|title|replace|full|index|catalog|text:"[^"]+"))*|[^\w@]+/g).map (t) ->
+ as = $$ '.boardList a[title]', Header.bar
+ re = /[\w@]+(-(all|title|replace|full|archive|(mode|sort|text):"[^"]+"))*|[^\w@]+/g
+ nodes = text.match(re).map (t) ->
if /^[^\w@]/.test t
return $.tn t
+
if /^toggle-all/.test t
a = $.el 'a',
className: 'show-board-list-button'
@@ -131,31 +125,47 @@ Header =
href: 'javascript:;'
$.on a, 'click', Header.toggleBoardList
return a
- board = if /^current/.test t
- g.BOARD.ID
+
+ boardID = t.split('-')[0]
+ boardID = g.BOARD.ID if boardID is 'current'
+ for a in as when a.textContent is boardID
+ a = a.cloneNode()
+ break
+ return $.tn boardID if a.parentNode # Not a clone.
+
+ a.textContent = if /-title/.test(t) or /-replace/.test(t) and boardID is g.BOARD.ID
+ a.title
+ else if /-full/.test t
+ "/#{boardID}/ - #{a.title}"
+ else if m = t.match /-text:"([^"]+)"/
+ m[1]
else
- t.match(/^[^-]+/)[0]
- for a in as
- if a.textContent is board
- a = a.cloneNode true
+ boardID
- a.textContent = if /-title/.test(t) or /-replace/.test(t) and $.hasClass a, 'current'
- a.title
- else if /-full/.test t
- "/#{board}/ - #{a.title}"
- else if m = t.match /-text:"(.+)"/
- m[1]
- else
- a.textContent
+ if /-archive/.test t
+ if href = Redirect.to 'board', {boardID}
+ a.href = href
+ else
+ return a.firstChild # Its text node.
- if m = t.match /-(index|catalog)/
- a.dataset.only = m[1]
- a.href = "//boards.4chan.org/#{board}/"
- a.href += 'catalog' if m[1] is 'catalog'
+ if m = t.match /-mode:"([^"]+)"/
+ type = m[1].toLowerCase()
+ a.dataset.indexMode = switch type
+ when 'all threads' then 'all pages'
+ when 'paged', 'catalog' then type
+ else 'paged'
+ if m = t.match /-sort:"([^"]+)"/
+ type = m[1].toLowerCase()
+ a.dataset.indexSort = switch type
+ when 'bump order' then 'bump'
+ when 'last reply' then 'lastreply'
+ when 'creation date' then 'birth'
+ when 'reply count' then 'replycount'
+ when 'file count' then 'filecount'
+ else 'bump'
- $.addClass a, 'navSmall' if board is '@'
- return a
- $.tn t
+ $.addClass a, 'navSmall' if boardID is '@'
+ a
$.add list, nodes
toggleBoardList: ->
@@ -219,21 +229,6 @@ Header =
$.cb.checked.call @
Header.setBarPosition @checked
- setCatalogLinks: (useCatalog) ->
- Header.catalogToggler.checked = useCatalog
- as = $$ [
- '#board-list a'
- '#boardNavDesktop a'
- '#boardNavDesktopFoot a'
- ].join ', '
- path = if useCatalog then 'catalog' else ''
- for a in as when a.hostname is 'boards.4chan.org' and not a.dataset.only
- a.pathname = "/#{a.pathname.split('/')[1]}/#{path}"
- return
- toggleCatalogLinks: ->
- $.cb.checked.call @
- Header.setCatalogLinks @checked
-
setTopBoardList: (show) ->
Header.topBoardToggler.checked = show
if show
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 79a935099..2e794b116 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -1,6 +1,18 @@
Index =
+ showHiddenThreads: false
init: ->
- return if g.VIEW isnt 'index' or g.BOARD.ID is 'f'
+ if g.VIEW isnt 'index'
+ $.ready @setupNavLinks
+ return
+ return if g.BOARD.ID is 'f'
+
+ @db = new DataBoard 'pinnedThreads'
+ Thread.callbacks.push
+ name: 'Thread Pinning'
+ cb: @threadNode
+ CatalogThread.callbacks.push
+ name: 'Catalog Features'
+ cb: @catalogNode
@button = $.el 'a',
className: 'index-refresh-shortcut fa fa-refresh'
@@ -9,33 +21,20 @@ Index =
$.on @button, 'click', @update
Header.addShortcut @button, 1
- modeEntry =
- el: $.el 'span', textContent: 'Index mode'
+ threadNumEntry =
+ el: $.el 'span', textContent: 'Threads per page'
subEntries: [
- { el: $.el 'label', innerHTML: '
Paged' }
- { el: $.el 'label', innerHTML: '
All threads' }
+ { el: $.el 'label', innerHTML: '
', title: 'Use 0 for default value' }
]
- 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
+ threadsNumInput = threadNumEntry.subEntries[0].el.firstChild
+ threadsNumInput.value = Conf['Threads per Page']
+ $.on threadsNumInput, 'change', $.cb.value
+ $.on threadsNumInput, 'change', @cb.threadsNum
+ targetEntry =
+ el: $.el 'label',
+ innerHTML: '
Open threads in a new tab'
+ title: 'Catalog-only setting.'
repliesEntry =
el: $.el 'label',
innerHTML: '
Show replies'
@@ -47,12 +46,14 @@ Index =
el: $.el 'label',
innerHTML: '
Refreshed navigation'
title: 'Refresh index when navigating through pages.'
- for label in [repliesEntry, anchorEntry, refNavEntry]
+ for label in [targetEntry, repliesEntry, anchorEntry, refNavEntry]
input = label.el.firstChild
{name} = input
input.checked = Conf[name]
$.on input, 'change', $.cb.checked
switch name
+ when 'Open threads in a new tab'
+ $.on input, 'change', @cb.target
when 'Show Replies'
$.on input, 'change', @cb.replies
when 'Anchor Hidden Threads'
@@ -63,24 +64,41 @@ Index =
el: $.el 'span',
textContent: 'Index Navigation'
order: 90
- subEntries: [modeEntry, sortEntry, repliesEntry, anchorEntry, refNavEntry]
+ subEntries: [threadNumEntry, targetEntry, repliesEntry, anchorEntry, refNavEntry]
$.addClass doc, 'index-loading'
@update()
+
+ @navLinks = $.el 'div',
+ id: 'nav-links'
+ innerHTML: <%= importHTML('General/Index-navlinks') %>
+ @searchInput = $ '#index-search', @navLinks
+ @hideLabel = $ '#hidden-label', @navLinks
+ @selectMode = $ '#index-mode', @navLinks
+ @selectSort = $ '#index-sort', @navLinks
+ @selectSize = $ '#index-size', @navLinks
+ $.on @searchInput, 'input', @onSearchInput
+ $.on $('#index-search-clear', @navLinks), 'click', @clearSearch
+ $.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
+ for select in [@selectMode, @selectSort, @selectSize]
+ select.value = Conf[select.name]
+ $.on select, 'change', $.cb.value
+ $.on @selectMode, 'change', @cb.mode
+ $.on @selectSort, 'change', @cb.sort
+ $.on @selectSize, 'change', @cb.size
+
@root = $.el 'div', className: 'board'
@pagelist = $.el 'div',
className: 'pagelist'
hidden: true
innerHTML: <%= importHTML('General/Index-pagelist') %>
- @navLinks = $.el 'div',
- className: 'navLinks'
- innerHTML: <%= importHTML('General/Index-navlinks') %>
- @searchInput = $ '#index-search', @navLinks
@currentPage = @getCurrentPage()
$.on window, 'popstate', @cb.popstate
$.on @pagelist, 'click', @cb.pageNav
- $.on @searchInput, 'input', @onSearchInput
- $.on $('#index-search-clear', @navLinks), 'click', @clearSearch
+ $.on $('#custom-board-list', Header.bar), 'click', @cb.headerNav
+
+ @cb.toggleCatalogMode()
+
$.asap (-> $('.board', doc) or d.readyState isnt 'loading'), ->
board = $ '.board'
$.replace board, Index.root
@@ -95,18 +113,192 @@ Index =
for navLink in $$ '.navLinks'
$.rm navLink
- $.after $.x('child::form/preceding-sibling::hr[1]'), Index.navLinks
+ $.before $.id('delform'), [Index.navLinks, $.x 'child::form/preceding-sibling::hr[1]']
$.rmClass doc, 'index-loading'
$.asap (-> $('.pagelist') or d.readyState isnt 'loading'), ->
$.replace $('.pagelist'), Index.pagelist
+ menu:
+ init: ->
+ return if g.VIEW isnt 'index' or !Conf['Menu'] or g.BOARD.ID is 'f'
+
+ $.event 'AddMenuEntry',
+ type: 'post'
+ el: $.el 'a', href: 'javascript:;'
+ order: 5
+ open: ({thread}) ->
+ return false if Conf['Index Mode'] isnt 'catalog'
+ @el.textContent = if thread.isHidden
+ 'Unhide thread'
+ else
+ 'Hide thread'
+ $.off @el, 'click', @cb if @cb
+ @cb = ->
+ $.event 'CloseMenu'
+ Index.toggleHide thread
+ $.on @el, 'click', @cb
+ true
+
+ $.event 'AddMenuEntry',
+ type: 'post'
+ el: $.el 'a', href: 'javascript:;'
+ order: 6
+ open: ({thread}) ->
+ return false if Conf['Index Mode'] isnt 'catalog'
+ @el.textContent = if thread.isPinned
+ 'Unpin thread'
+ else
+ 'Pin thread'
+ $.off @el, 'click', @cb if @cb
+ @cb = ->
+ $.event 'CloseMenu'
+ Index.togglePin thread
+ $.on @el, 'click', @cb
+ true
+
+ threadNode: ->
+ return unless Index.db.get {boardID: @board.ID, threadID: @ID}
+ @pin()
+ catalogNode: ->
+ $.on @nodes.thumb, 'click', Index.onClick
+ return if Conf['Image Hover in Catalog']
+ $.on @nodes.thumb, 'mouseover', Index.onOver
+ onClick: (e) ->
+ return if e.button isnt 0
+ thread = g.threads[@parentNode.dataset.fullID]
+ if e.shiftKey
+ Index.toggleHide thread
+ else if e.altKey
+ Index.togglePin thread
+ else
+ return
+ e.preventDefault()
+ onOver: (e) ->
+ # 4chan's less than stellar CSS forces us to include a .post and .postInfo
+ # in order to have proper styling for the .nameBlock's content.
+ {nodes} = g.threads[@parentNode.dataset.fullID].OP
+ el = $.el 'div',
+ innerHTML: '
'
+ className: 'thread-info'
+ hidden: true
+ $.add el.firstElementChild.firstElementChild, [
+ $('.nameBlock', nodes.info).cloneNode true
+ $.tn ' '
+ nodes.date.cloneNode true
+ ]
+ $.add d.body, el
+ UI.hover
+ root: @
+ el: el
+ latestEvent: e
+ endEvents: 'mouseout'
+ offsetX: 15
+ offsetY: -20
+ setTimeout (-> el.hidden = false if el.parentNode), .25 * $.SECOND
+ toggleHide: (thread) ->
+ $.rm thread.catalogView.nodes.root
+ if Index.showHiddenThreads
+ ThreadHiding.show thread
+ return unless ThreadHiding.db.get {boardID: thread.board.ID, threadID: thread.ID}
+ # Don't save when un-hiding filtered threads.
+ else
+ ThreadHiding.hide thread
+ ThreadHiding.saveHiddenState thread
+ togglePin: (thread) ->
+ data =
+ boardID: thread.board.ID
+ threadID: thread.ID
+ if thread.isPinned
+ thread.unpin()
+ Index.db.delete data
+ else
+ thread.pin()
+ data.val = true
+ Index.db.set data
+ Index.sort()
+ Index.buildIndex()
+ setIndexMode: (mode) ->
+ Index.selectMode.value = mode
+ $.event 'change', null, Index.selectMode
+ cycleSortType: ->
+ types = [Index.selectSort.options...].filter (option) -> !option.disabled
+ for type, i in types
+ break if type.selected
+ types[(i + 1) % types.length].selected = true
+ $.event 'change', null, Index.selectSort
+ addCatalogSwitch: ->
+ a = $.el 'a',
+ href: 'javascript:;'
+ textContent: 'Switch to <%= meta.name %>\'s catalog'
+ className: 'btn-wrap'
+ $.on a, 'click', ->
+ $.set 'Index Mode', 'catalog'
+ window.location = './'
+ $.add $.id('info'), a
+ setupNavLinks: ->
+ for el in $$ '.navLinks.desktop > a'
+ if el.getAttribute('href') is '.././catalog'
+ el.href = '.././'
+ $.on el, 'click', ->
+ switch @textContent
+ when 'Return'
+ $.set 'Index Mode', Conf['Previous Index Mode']
+ when 'Catalog'
+ $.set 'Index Mode', 'catalog'
+ return
cb:
- mode: ->
- Index.togglePagelist()
- Index.buildIndex()
- sort: ->
+ toggleCatalogMode: ->
+ if Conf['Index Mode'] is 'catalog'
+ $.addClass doc, 'catalog-mode'
+ else
+ $.rmClass doc, 'catalog-mode'
+ Index.cb.size()
+ toggleHiddenThreads: ->
+ $('#hidden-toggle a', Index.navLinks).textContent = if Index.showHiddenThreads = !Index.showHiddenThreads
+ 'Hide'
+ else
+ 'Show'
Index.sort()
Index.buildIndex()
+ mode: (e) ->
+ Index.cb.toggleCatalogMode()
+ Index.togglePagelist()
+ Index.buildIndex() if e
+ mode = Conf['Index Mode']
+ if mode not in ['catalog', Conf['Previous Index Mode']]
+ Conf['Previous Index Mode'] = mode
+ $.set 'Previous Index Mode', mode
+ return unless QR.nodes
+ if mode is 'catalog'
+ QR.hide()
+ else
+ QR.unhide()
+ sort: (e) ->
+ Index.sort()
+ Index.buildIndex() if e
+ size: (e) ->
+ if Conf['Index Mode'] isnt 'catalog'
+ $.rmClass Index.root, 'catalog-small'
+ $.rmClass Index.root, 'catalog-large'
+ else if Conf['Index Size'] is 'small'
+ $.addClass Index.root, 'catalog-small'
+ $.rmClass Index.root, 'catalog-large'
+ else
+ $.addClass Index.root, 'catalog-large'
+ $.rmClass Index.root, 'catalog-small'
+ Index.buildIndex() if e
+ threadsNum: ->
+ return unless Conf['Index Mode'] is 'paged'
+ Index.buildPagelist()
+ Index.buildIndex()
+ target: ->
+ for threadID, thread of g.BOARD.threads when thread.catalogView
+ {thumb} = thread.catalogView.nodes
+ if Conf['Open threads in a new tab']
+ thumb.target = '_blank'
+ else
+ thumb.removeAttribute 'target'
+ return
replies: ->
Index.buildThreads()
Index.sort()
@@ -123,12 +315,40 @@ Index =
a = e.target
else
return
- return if a.textContent is 'Catalog'
e.preventDefault()
+ return if Index.cb.indexNav a, true
Index.userPageNav +a.pathname.split('/')[2]
+ headerNav: (e) ->
+ a = e.target
+ return if e.button isnt 0 or a.nodeName isnt 'A' or a.hostname isnt 'boards.4chan.org'
+ # Save settings
+ onSameBoard = a.pathname.split('/')[1] is g.BOARD.ID
+ Index.cb.indexNav a, onSameBoard
+ # Do nav if this isn't a simple click, or different board.
+ return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or !onSameBoard
+ e.preventDefault()
+ indexNav: (a, onSameBoard) ->
+ {indexMode, indexSort} = a.dataset
+ if indexMode
+ $.set 'Index Mode', indexMode
+ Conf['Index Mode'] = indexMode
+ if g.VIEW is 'index' and onSameBoard
+ Index.selectMode.value = indexMode
+ Index.cb.mode()
+ if indexSort
+ $.set 'Index Sort', indexSort
+ Conf['Index Sort'] = indexSort
+ if g.VIEW is 'index' and onSameBoard
+ Index.selectSort.value = indexSort
+ Index.cb.sort()
+ if g.VIEW is 'index' and onSameBoard and (indexMode or indexSort)
+ Index.buildIndex()
+ Index.scrollToIndex()
+ return true
+ false
scrollToIndex: ->
- Header.scrollToIfNeeded Index.root
+ Header.scrollToIfNeeded Index.navLinks
getCurrentPage: ->
+window.location.pathname.split('/')[2]
@@ -148,11 +368,17 @@ Index =
Index.setPage()
Index.scrollToIndex()
- getPagesNum: ->
- if Index.isSearching
- Math.ceil (Index.sortedNodes.length / 2) / Index.threadsNumPerPage
+ getThreadsNumPerPage: ->
+ if Conf['Threads per Page'] > 0
+ +Conf['Threads per Page']
else
- Index.pagesNum
+ Index.threadsNumPerPage
+ getPagesNum: ->
+ numThreads = if Index.isSearching
+ Index.sortedNodes.length / 2
+ else
+ Index.liveThreadIDs.length
+ Math.ceil numThreads / Index.getThreadsNumPerPage()
getMaxPageNum: ->
Math.max 0, Index.getPagesNum() - 1
togglePagelist: ->
@@ -193,6 +419,20 @@ Index =
$.before a, strong
$.add strong, a
+ updateHideLabel: ->
+ hiddenCount = 0
+ for threadID, thread of g.BOARD.threads when thread.isHidden
+ hiddenCount++ if thread.ID in Index.liveThreadIDs
+ unless hiddenCount
+ Index.hideLabel.hidden = true
+ Index.cb.toggleHiddenThreads() if Index.showHiddenThreads
+ return
+ Index.hideLabel.hidden = false
+ $('#hidden-count', Index.navLinks).textContent = if hiddenCount is 1
+ '1 hidden thread'
+ else
+ "#{hiddenCount} hidden threads"
+
update: (pageNum) ->
return unless navigator.onLine
Index.req?.abort()
@@ -273,7 +513,6 @@ Index =
Index.buildIndex()
Index.setPage()
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
@@ -288,7 +527,9 @@ Index =
threadRoot = Build.thread g.BOARD, threadData
Index.nodes.push threadRoot, $.el 'hr'
if thread = g.BOARD.threads[threadData.no]
- thread.setPage Math.floor i / Index.threadsNumPerPage
+ thread.setPage i // Index.threadsNumPerPage
+ thread.setCount 'post', threadData.replies + 1, threadData.bumplimit
+ thread.setCount 'file', threadData.images + !!threadData.ext, threadData.imagelimit
thread.setStatus 'Sticky', !!threadData.sticky
thread.setStatus 'Closed', !!threadData.closed
else
@@ -309,6 +550,7 @@ Index =
$.nodes Index.nodes
Main.callbackNodes Thread, threads
Main.callbackNodes Post, posts
+ Index.updateHideLabel()
$.event 'IndexRefresh'
buildReplies: (threadRoots) ->
posts = []
@@ -334,16 +576,37 @@ Index =
Main.handleErrors errors if errors
Main.callbackNodes Post, posts
+ buildCatalogViews: ->
+ threads = Index.sortedNodes
+ .filter (n, i) -> !(i % 2)
+ .map (threadRoot) -> Get.threadFromRoot threadRoot
+ .filter (thread) -> !thread.isHidden isnt Index.showHiddenThreads
+ catalogThreads = []
+ for thread in threads when !thread.catalogView
+ catalogThreads.push new CatalogThread Build.catalogThread(thread), thread
+ Main.callbackNodes CatalogThread, catalogThreads
+ threads.map (thread) -> thread.catalogView.nodes.root
+ sizeCatalogViews: (nodes) ->
+ # XXX When browsers support CSS3 attr(), use it instead.
+ size = if Conf['Index Size'] is 'small' then 150 else 250
+ for node in nodes
+ thumb = node.firstElementChild
+ {width, height} = thumb.dataset
+ continue unless width
+ ratio = size / Math.max width, height
+ thumb.style.width = width * ratio + 'px'
+ thumb.style.height = height * ratio + 'px'
+ return
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
+ sortedThreadIDs = [Index.liveThreadData...].sort (a, b) ->
+ [..., a] = a.last_replies if 'last_replies' of a
+ [..., b] = b.last_replies if 'last_replies' of b
b.no - a.no
- ).map (data) -> data.no
+ .map (data) -> data.no
when 'birth'
sortedThreadIDs = [Index.liveThreadIDs...].sort (a, b) -> b - a
when 'replycount'
@@ -359,7 +622,7 @@ Index =
# Sticky threads
Index.sortOnTop (thread) -> thread.isSticky
# Highlighted threads
- Index.sortOnTop((thread) -> thread.isOnTop) if Conf['Filter']
+ Index.sortOnTop (thread) -> thread.isOnTop or thread.isPinned
# Non-hidden threads
Index.sortOnTop((thread) -> !thread.isHidden) if Conf['Anchor Hidden Threads']
sortOnTop: (match) ->
@@ -368,16 +631,20 @@ Index =
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
+ switch Conf['Index Mode']
+ when 'paged'
+ pageNum = Index.getCurrentPage()
+ nodesPerPage = Index.getThreadsNumPerPage() * 2
+ nodes = Index.sortedNodes[nodesPerPage * pageNum ... nodesPerPage * (pageNum + 1)]
+ when 'catalog'
+ nodes = Index.buildCatalogViews()
+ Index.sizeCatalogViews nodes
+ else
+ nodes = Index.sortedNodes
$.rmAll Index.root
- Index.buildReplies nodes if Conf['Show Replies']
- $.event 'IndexBuild', nodes
+ Index.buildReplies nodes if Conf['Show Replies'] and Conf['Index Mode'] isnt 'catalog'
$.add Index.root, nodes
+ $.event 'IndexBuild', nodes
isSearching: false
clearSearch: ->
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index 254d2cba4..6f7a455ea 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -11,6 +11,9 @@ Main =
'catalog'
else
'index'
+ if g.VIEW is 'catalog'
+ $.ready Index.addCatalogSwitch
+ return
if g.VIEW is 'thread'
g.THREADID = +pathname[3]
@@ -82,6 +85,7 @@ Main =
initFeature 'Strike-through Quotes', QuoteStrikeThrough
initFeature 'Quick Reply', QR
initFeature 'Menu', Menu
+ initFeature 'Index Generator (Menu)', Index.menu
initFeature 'Report Link', ReportLink
initFeature 'Thread Hiding (Menu)', ThreadHiding.menu
initFeature 'Reply Hiding (Menu)', PostHiding.menu
@@ -92,9 +96,7 @@ Main =
initFeature 'Quote Inlining', QuoteInline
initFeature 'Quote Previewing', QuotePreview
initFeature 'Quote Backlinks', QuoteBacklink
- initFeature 'Mark Quotes of You', QuoteYou
- initFeature 'Mark OP Quotes', QuoteOP
- initFeature 'Mark Cross-thread Quotes', QuoteCT
+ initFeature 'Quote Markers', QuoteMarkers
initFeature 'Anonymize', Anonymize
initFeature 'Color User IDs', IDColor
initFeature 'Time Formatting', Time
@@ -131,10 +133,6 @@ Main =
$.addClass doc, 'fourchan-x', '<% if (type === 'crx') { %>blink<% } else { %>gecko<% } %>'
$.addStyle Main.css
- if g.VIEW is 'catalog'
- $.addClass doc, $.id('base-css').href.match(/catalog_(\w+)/)[1].replace('_new', '').replace /_+/g, '-'
- return
-
style = 'yotsuba-b'
mainStyleSheet = $ 'link[title=switch]', d.head
styleSheets = $$ 'link[rel="alternate stylesheet"]', d.head
diff --git a/src/General/Post.coffee b/src/General/Post.coffee
index f3b69895b..5d5b63ed1 100644
--- a/src/General/Post.coffee
+++ b/src/General/Post.coffee
@@ -52,6 +52,9 @@ class Post
@parseQuotes()
@parseFile that
+ @isDead = false
+ @isHidden = false
+
@clones = []
g.posts[@fullID] = thread.posts[@] = board.posts[@] = @
@kill() if that.isArchived
@@ -62,7 +65,6 @@ class Post
# Get the comment's text.
#
-> \n
# Remove:
- # 'Comment too long'...
# EXIF data. (/p/)
# Rolls. (/tg/)
# Preceding and following new lines.
@@ -149,17 +151,14 @@ class Post
$.rmClass node, 'desktop'
return
- kill: (file, now) ->
- now or= new Date()
+ kill: (file) ->
if file
return if @file.isDead
@file.isDead = true
- @file.timeOfDeath = now
$.addClass @nodes.root, 'deleted-file'
else
return if @isDead
@isDead = true
- @timeOfDeath = now
$.addClass @nodes.root, 'deleted-post'
unless strong = $ 'strong.warning', @nodes.info
@@ -171,20 +170,20 @@ class Post
return if @isClone
for clone in @clones
- clone.kill file, now
+ clone.kill file
return if file
# Get quotelinks/backlinks to this post
# and paint them (Dead).
for quotelink in Get.allQuotelinksLinkingTo @ when not $.hasClass quotelink, 'deadlink'
- $.add quotelink, $.tn '\u00A0(Dead)'
$.addClass quotelink, 'deadlink'
+ continue unless Conf['Quote Markers']
+ QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
return
# XXX tmp fix for 4chan's racing condition
# giving us false-positive dead posts.
resurrect: ->
delete @isDead
- delete @timeOfDeath
$.rmClass @nodes.root, 'deleted-post'
strong = $ 'strong.warning', @nodes.info
# no false-positive files
@@ -197,10 +196,10 @@ class Post
for clone in @clones
clone.resurrect()
- for quotelink in Get.allQuotelinksLinkingTo @
- if $.hasClass quotelink, 'deadlink'
- quotelink.textContent = quotelink.textContent.replace '\u00A0(Dead)', ''
- $.rmClass quotelink, 'deadlink'
+ for quotelink in Get.allQuotelinksLinkingTo @ when $.hasClass quotelink, 'deadlink'
+ $.rmClass quotelink, 'deadlink'
+ continue unless Conf['Quote Markers']
+ QuoteMarkers.parseQuotelink Get.postFromNode(quotelink), quotelink, true
return
collect: ->
diff --git a/src/General/Settings.coffee b/src/General/Settings.coffee
index 0d85a8d44..86c027c4e 100644
--- a/src/General/Settings.coffee
+++ b/src/General/Settings.coffee
@@ -133,10 +133,7 @@ Settings =
button.textContent = "Hidden: #{hiddenNum}"
$.on button, 'click', ->
@textContent = 'Hidden: 0'
- $.get 'hiddenThreads', {}, ({hiddenThreads}) ->
- for boardID of hiddenThreads.boards
- localStorage.removeItem "4chan-hide-t-#{boardID}"
- $.delete ['hiddenThreads', 'hiddenPosts']
+ $.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: ->
# Make sure to export the most recent data.
@@ -195,8 +192,8 @@ Settings =
'Remember QR size': ''
'Quote Inline': 'Quote Inlining'
'Quote Preview': 'Quote Previewing'
- 'Indicate OP quote': 'Mark OP Quotes'
- 'Indicate Cross-thread Quotes': 'Mark Cross-thread Quotes'
+ 'Indicate OP quote': ''
+ 'Indicate Cross-thread Quotes': ''
# filter
'uniqueid': 'uniqueID'
'mod': 'capcode'
diff --git a/src/General/Thread.coffee b/src/General/Thread.coffee
index 410a4288a..b930661c8 100644
--- a/src/General/Thread.coffee
+++ b/src/General/Thread.coffee
@@ -5,18 +5,30 @@ class Thread
constructor: (@ID, @board) ->
@fullID = "#{@board}.#{@ID}"
@posts = {}
+ @isDead = false
+ @isHidden = false
+ @isOnTop = false
+ @isPinned = false
@isSticky = false
@isClosed = false
@postLimit = false
@fileLimit = false
+ @OP = null
+ @catalogView = null
+
g.threads[@fullID] = board.threads[@] = @
setPage: (pageNum) ->
- icon = $ '.page-num', @OP.nodes.post
+ icon = $ '.page-num', @OP.nodes.info
for key in ['title', 'textContent']
icon[key] = icon[key].replace /\d+/, pageNum
- return
+ @catalogView.nodes.pageCount.textContent = pageNum if @catalogView
+ setCount: (type, count, reachedLimit) ->
+ return unless @catalogView
+ el = @catalogView.nodes["#{type}Count"]
+ el.textContent = count
+ (if reachedLimit then $.addClass else $.rmClass) el, 'warning'
setStatus: (type, status) ->
name = "is#{type}"
return if @[name] is status
@@ -25,23 +37,33 @@ class Thread
typeLC = type.toLowerCase()
unless status
$.rm $ ".#{typeLC}Icon", @OP.nodes.info
+ $.rm $ ".#{typeLC}Icon", @catalogView.nodes.icons if @catalogView
return
+
icon = $.el 'img',
- src: "//s.4cdn.org/image/#{typeLC}#{if window.devicePixelRatio >= 2 then '@2x' else ''}.gif"
- alt: type
+ src: "#{Build.staticPath}#{typeLC}#{Build.gifIcon}"
title: type
className: "#{typeLC}Icon"
root = if type is 'Closed' and @isSticky
$ '.stickyIcon', @OP.nodes.info
else if g.VIEW is 'index'
- $ '.page-num', @OP.nodes.info
+ $ '.page-num', @OP.nodes.info
else
$ '[title="Quote this post"]', @OP.nodes.info
$.after root, [$.tn(' '), icon]
+ return unless @catalogView
+ (if type is 'Sticky' and @isClosed then $.prepend else $.add) @catalogView.nodes.icons, icon.cloneNode()
+
+ pin: ->
+ @isPinned = true
+ $.addClass @catalogView.nodes.root, 'pinned' if @catalogView
+ unpin: ->
+ @isPinned = false
+ $.rmClass @catalogView.nodes.root, 'pinned' if @catalogView
+
kill: ->
@isDead = true
- @timeOfDeath = Date.now()
collect: ->
for postID, post of @posts
diff --git a/src/General/UI.coffee b/src/General/UI.coffee
index e52774aea..d3dd11c1c 100644
--- a/src/General/UI.coffee
+++ b/src/General/UI.coffee
@@ -47,6 +47,7 @@ UI = do ->
menu = @makeMenu()
currentMenu = menu
lastToggledButton = button
+ $.addClass button, 'open'
for entry in @entries
@insertEntry entry, menu, data
@@ -99,6 +100,7 @@ UI = do ->
close: =>
$.rm currentMenu
+ $.rmClass lastToggledButton, 'open'
currentMenu = null
lastToggledButton = null
$.off d, 'click CloseMenu', @close
@@ -191,7 +193,7 @@ UI = do ->
# prevent text selection
e.preventDefault()
if isTouching = e.type is 'touchstart'
- e = e.changedTouches[e.changedTouches.length - 1]
+ [..., e] = e.changedTouches
# distance from pointer to el edge is constant; calculate it here.
el = $.x 'ancestor::div[contains(@class,"dialog")][1]', @
rect = el.getBoundingClientRect()
@@ -271,7 +273,7 @@ UI = do ->
$.off d, 'mouseup', @up
$.set "#{@id}.position", @style.cssText
- hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb}) ->
+ hoverstart = ({root, el, latestEvent, endEvents, asapTest, cb, offsetX, offsetY}) ->
o = {
root
el
@@ -281,14 +283,17 @@ UI = do ->
latestEvent
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
+ offsetX: offsetX or 45
+ offsetY: offsetY or -120
}
o.hover = hover.bind o
o.hoverend = hoverend.bind o
- $.asap ->
- !el.parentNode or asapTest()
- , ->
- o.hover o.latestEvent if el.parentNode
+ if asapTest
+ $.asap ->
+ !el.parentNode or asapTest()
+ , ->
+ o.hover o.latestEvent if el.parentNode
$.on root, endEvents, o.hoverend
$.on root, 'mousemove', o.hover
@@ -302,7 +307,7 @@ UI = do ->
height = @el.offsetHeight
{clientX, clientY} = e
- top = clientY - 120
+ top = clientY + @offsetY
top = if @clientHeight <= height or top <= 0
0
else if top + height >= @clientHeight
@@ -310,10 +315,10 @@ UI = do ->
else
top
- [left, right] = if clientX <= @clientWidth - 400
- [clientX + 45 + 'px', null]
+ [left, right] = if clientX <= @clientWidth / 2
+ [clientX + @offsetX + 'px', null]
else
- [null, @clientWidth - clientX + 45 + 'px']
+ [null, @clientWidth - clientX + @offsetX + 'px']
{style} = @
style.top = top + 'px'
diff --git a/src/Images/AutoGIF.coffee b/src/Images/AutoGIF.coffee
index 71ccc7d8e..c47a388bb 100644
--- a/src/Images/AutoGIF.coffee
+++ b/src/Images/AutoGIF.coffee
@@ -1,10 +1,13 @@
AutoGIF =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg']
+ return if !Conf['Auto-GIF'] or g.BOARD.ID in ['gif', 'wsg']
Post.callbacks.push
name: 'Auto-GIF'
cb: @node
+ CatalogThread.callbacks.push
+ name: 'Auto-GIF'
+ cb: @catalogNode
node: ->
return if @isClone or @isHidden or @thread.isHidden or !@file?.isImage
{thumb, URL} = @file
@@ -13,8 +16,20 @@ AutoGIF =
# Revealed spoilers do not have height/width set, this fixes auto-gifs dimensions.
{style} = thumb
style.maxHeight = style.maxWidth = if @isReply then '125px' else '250px'
+ AutoGIF.replaceThumbnail thumb, URL
+ catalogNode: ->
+ {OP} = @thread
+ return unless OP.file?.isImage
+ {URL} = OP.file
+ return unless /gif$/.test URL
+ AutoGIF.replaceThumbnail @nodes.thumb, URL, true
+ replaceThumbnail: (thumb, URL, isBackground) ->
gif = $.el 'img'
$.on gif, 'load', ->
# Replace the thumbnail once the GIF has finished loading.
- thumb.src = URL
+ if isBackground
+ thumb.style.backgroundImage = "url(#{URL})"
+ else
+ thumb.src = URL
gif.src = URL
+
diff --git a/src/Images/ImageExpand.coffee b/src/Images/ImageExpand.coffee
index 13816680c..9d754da91 100644
--- a/src/Images/ImageExpand.coffee
+++ b/src/Images/ImageExpand.coffee
@@ -1,6 +1,6 @@
ImageExpand =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Expansion']
+ return if !Conf['Image Expansion']
@EAI = $.el 'a',
className: 'expand-all-shortcut fa fa-expand'
@@ -133,7 +133,7 @@ ImageExpand =
timeoutID = setTimeout ImageExpand.expand, 10000, post
<% if (type === 'crx') { %>
- $.ajax @src,
+ $.ajax post.file.URL,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID
@@ -156,7 +156,7 @@ ImageExpand =
menu:
init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Expansion']
+ return if !Conf['Image Expansion']
el = $.el 'span',
textContent: 'Image Expansion'
diff --git a/src/Images/ImageHover.coffee b/src/Images/ImageHover.coffee
index 9494b0c71..97e44971a 100644
--- a/src/Images/ImageHover.coffee
+++ b/src/Images/ImageHover.coffee
@@ -1,15 +1,24 @@
ImageHover =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Image Hover']
-
- Post.callbacks.push
- name: 'Image Hover'
- cb: @node
+ if Conf['Image Hover']
+ Post.callbacks.push
+ name: 'Image Hover'
+ cb: @node
+ if Conf['Image Hover in Catalog']
+ CatalogThread.callbacks.push
+ name: 'Image Hover'
+ cb: @catalogNode
node: ->
return unless @file?.isImage
$.on @file.thumb, 'mouseover', ImageHover.mouseover
+ catalogNode: ->
+ return unless @thread.OP.file?.isImage
+ $.on @nodes.thumb, 'mouseover', ImageHover.mouseover
mouseover: (e) ->
- post = Get.postFromNode @
+ post = if $.hasClass @, 'thumb'
+ g.posts[@parentNode.dataset.fullID]
+ else
+ Get.postFromNode @
el = $.el 'img',
id: 'ihover'
src: post.file.URL
@@ -39,7 +48,7 @@ ImageHover =
timeoutID = setTimeout (=> @src = post.file.URL + '?' + Date.now()), 3000
<% if (type === 'crx') { %>
- $.ajax @src,
+ $.ajax post.file.URL,
onloadend: ->
return if @status isnt 404
clearTimeout timeoutID
diff --git a/src/Images/RevealSpoilers.coffee b/src/Images/RevealSpoilers.coffee
index 52f7dffe6..1d925ce91 100644
--- a/src/Images/RevealSpoilers.coffee
+++ b/src/Images/RevealSpoilers.coffee
@@ -1,6 +1,6 @@
RevealSpoilers =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Reveal Spoilers']
+ return if !Conf['Reveal Spoilers']
Post.callbacks.push
name: 'Reveal Spoilers'
diff --git a/src/Images/Sauce.coffee b/src/Images/Sauce.coffee
index b7ce89779..e2854b23c 100644
--- a/src/Images/Sauce.coffee
+++ b/src/Images/Sauce.coffee
@@ -1,6 +1,6 @@
Sauce =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Sauce']
+ return if !Conf['Sauce']
links = []
for link in Conf['sauces'].split '\n'
@@ -15,7 +15,7 @@ Sauce =
name: 'Sauce'
cb: @node
createSauceLink: (link) ->
- link = link.replace /%(T?URL|MD5|board)/g, (parameter) ->
+ link = link.replace /%(T?URL|MD5|board|name)/g, (parameter) ->
switch parameter
when '%TURL'
"' + encodeURIComponent(post.file.thumbURL) + '"
@@ -25,6 +25,8 @@ Sauce =
"' + encodeURIComponent(post.file.MD5) + '"
when '%board'
"' + encodeURIComponent(post.board) + '"
+ when '%name'
+ "' + encodeURIComponent(post.file.name) + '"
else
parameter
text = if m = link.match(/;text:(.+)$/) then m[1] else link.match(/(\w+)\.\w+\//)[1]
diff --git a/src/Linkification/Linkify.coffee b/src/Linkification/Linkify.coffee
index fd90879e6..fce9bbe53 100644
--- a/src/Linkification/Linkify.coffee
+++ b/src/Linkification/Linkify.coffee
@@ -1,6 +1,6 @@
Linkify =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Linkify']
+ return if !Conf['Linkify']
# gruber revised + magnet support
# http://df4.us/fv9
@@ -60,11 +60,12 @@ Linkify =
# Replace already-linkified links,
# f.e.: https://boards.4chan.org/b/%
$.replace parent, anchor
- Linkify.cleanLink anchor, link if Conf['Clean Links']
+ Linkify.cleanLink anchor, link
walker.currentNode = anchor.lastChild
else
walker.previousNode()
range.detach()
+ @nodes.comment.normalize()
find: (link, walker) ->
# Walk through the nodes until we find the entire link.
@@ -134,6 +135,8 @@ Linkify =
cleanLink: (anchor, link) ->
{length} = link
+ for node in $$ 'wbr', anchor
+ $.rm node
for node in $$ 's, .prettyprint', anchor
$.replace node, [node.childNodes...] if length > node.textContent.length
return
diff --git a/src/Menu/ArchiveLink.coffee b/src/Menu/ArchiveLink.coffee
index f691d6f95..88ecc21b5 100644
--- a/src/Menu/ArchiveLink.coffee
+++ b/src/Menu/ArchiveLink.coffee
@@ -1,6 +1,6 @@
ArchiveLink =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Archive Link']
+ return if !Conf['Menu'] or !Conf['Archive Link']
div = $.el 'div',
textContent: 'Archive'
diff --git a/src/Menu/DeleteLink.coffee b/src/Menu/DeleteLink.coffee
index 1805047f8..c79f24c98 100644
--- a/src/Menu/DeleteLink.coffee
+++ b/src/Menu/DeleteLink.coffee
@@ -1,6 +1,6 @@
DeleteLink =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Delete Link']
+ return if !Conf['Menu'] or !Conf['Delete Link']
div = $.el 'div',
className: 'delete-link'
diff --git a/src/Menu/DownloadLink.coffee b/src/Menu/DownloadLink.coffee
index 4dcc043f0..a93cfd8c0 100644
--- a/src/Menu/DownloadLink.coffee
+++ b/src/Menu/DownloadLink.coffee
@@ -1,6 +1,6 @@
DownloadLink =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Download Link']
+ return if !Conf['Menu'] or !Conf['Download Link']
a = $.el 'a',
className: 'download-link'
diff --git a/src/Menu/Menu.coffee b/src/Menu/Menu.coffee
index 23fb2ce91..5803ed0ed 100644
--- a/src/Menu/Menu.coffee
+++ b/src/Menu/Menu.coffee
@@ -1,36 +1,42 @@
Menu =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu']
+ return if !Conf['Menu']
+
+ a = $.el 'a',
+ className: 'menu-button'
+ innerHTML: '
'
+ href: 'javascript:;'
+ @frag = $.nodes [$.tn(' '), a]
@menu = new UI.Menu 'post'
Post.callbacks.push
name: 'Menu'
cb: @node
+ CatalogThread.callbacks.push
+ name: 'Menu'
+ cb: @catalogNode
node: ->
if @isClone
$.on $('.menu-button', @nodes.info), 'click', Menu.toggle
return
$.add @nodes.info, Menu.makeButton()
+ catalogNode: ->
+ $.add @nodes.thumb, Menu.makeButton()
- makeButton: do ->
- frag = null
- ->
- unless frag
- a = $.el 'a',
- className: 'menu-button'
- innerHTML: '[
]'
- href: 'javascript:;'
- frag = $.nodes [$.tn(' '), a]
- clone = frag.cloneNode true
- $.on clone.lastElementChild, 'click', Menu.toggle
- clone
+ makeButton: ->
+ clone = Menu.frag.cloneNode true
+ $.on clone.lastElementChild, 'click', Menu.toggle
+ clone
toggle: (e) ->
try
# Posts, inlined posts, hidden replies.
post = Get.postFromNode @
catch
- # Hidden threads.
- post = Get.threadFromNode(@).OP
+ post = if fullID = @parentNode.parentNode.dataset.fullID
+ g.threads[fullID].OP
+ else
+ # Hidden threads.
+ Get.threadFromNode(@).OP
Menu.menu.toggle e, @, post
diff --git a/src/Menu/ReportLink.coffee b/src/Menu/ReportLink.coffee
index 435006c6d..c246c903f 100644
--- a/src/Menu/ReportLink.coffee
+++ b/src/Menu/ReportLink.coffee
@@ -1,6 +1,6 @@
ReportLink =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Report Link']
+ return if !Conf['Menu'] or !Conf['Report Link']
a = $.el 'a',
className: 'report-link'
diff --git a/src/Miscellaneous/Anonymize.coffee b/src/Miscellaneous/Anonymize.coffee
index 414c428e5..5b9f4e6a1 100644
--- a/src/Miscellaneous/Anonymize.coffee
+++ b/src/Miscellaneous/Anonymize.coffee
@@ -1,6 +1,6 @@
Anonymize =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Anonymize']
+ return if !Conf['Anonymize']
Post.callbacks.push
name: 'Anonymize'
diff --git a/src/Miscellaneous/Dice.coffee b/src/Miscellaneous/Dice.coffee
index d02698344..8729d9faf 100644
--- a/src/Miscellaneous/Dice.coffee
+++ b/src/Miscellaneous/Dice.coffee
@@ -1,6 +1,6 @@
Dice =
init: ->
- return if g.BOARD.ID isnt 'tg' or g.VIEW is 'catalog' or !Conf['Show Dice Roll']
+ return if g.BOARD.ID isnt 'tg' or !Conf['Show Dice Roll']
Post.callbacks.push
name: 'Show Dice Roll'
cb: @node
diff --git a/src/Miscellaneous/ExpandThread.coffee b/src/Miscellaneous/ExpandThread.coffee
index d7c6a6d59..8cfb24411 100644
--- a/src/Miscellaneous/ExpandThread.coffee
+++ b/src/Miscellaneous/ExpandThread.coffee
@@ -92,12 +92,4 @@ ExpandThread =
postsRoot.push root
Main.callbackNodes Post, posts
$.after a, postsRoot
-
- postsCount = postsRoot.length
- a.textContent = ExpandThread.text '-', postsCount, filesCount
-
- # Enable 4chan features.
- if Conf['Enable 4chan\'s Extension']
- $.globalEval "Parser.parseThread(#{thread}, 1, #{postsCount})"
- else
- Fourchan.parseThread thread.ID, 1, postsCount
+ a.textContent = ExpandThread.text '-', postsRoot.length, filesCount
diff --git a/src/Miscellaneous/FileInfo.coffee b/src/Miscellaneous/FileInfo.coffee
index e58f9faef..0534b2c33 100644
--- a/src/Miscellaneous/FileInfo.coffee
+++ b/src/Miscellaneous/FileInfo.coffee
@@ -1,6 +1,6 @@
FileInfo =
init: ->
- return if g.VIEW is 'catalog' or !Conf['File Info Formatting']
+ return if !Conf['File Info Formatting']
@funk = @createFunc Conf['fileInfo']
Post.callbacks.push
diff --git a/src/Miscellaneous/Fourchan.coffee b/src/Miscellaneous/Fourchan.coffee
index 6f290d662..a0da2637d 100644
--- a/src/Miscellaneous/Fourchan.coffee
+++ b/src/Miscellaneous/Fourchan.coffee
@@ -1,7 +1,5 @@
Fourchan =
init: ->
- return if g.VIEW is 'catalog'
-
board = g.BOARD.ID
if board is 'g'
$.globalEval """
@@ -42,10 +40,3 @@ Fourchan =
math: ->
return if @isClone or !$ '.math', @nodes.comment
$.event 'jsmath', @nodes.post, window
- parseThread: (threadID, offset, limit) ->
- # Fix /sci/
- # Fix /g/
- $.event '4chanParsingDone',
- threadId: threadID
- offset: offset
- limit: limit
diff --git a/src/Miscellaneous/IDColor.coffee b/src/Miscellaneous/IDColor.coffee
index 1fbdd5718..4ce769ad7 100644
--- a/src/Miscellaneous/IDColor.coffee
+++ b/src/Miscellaneous/IDColor.coffee
@@ -1,6 +1,6 @@
IDColor =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Color User IDs']
+ return if !Conf['Color User IDs']
@ids = {}
Post.callbacks.push
diff --git a/src/Miscellaneous/Keybinds.coffee b/src/Miscellaneous/Keybinds.coffee
index c158cb183..8bb3e466b 100644
--- a/src/Miscellaneous/Keybinds.coffee
+++ b/src/Miscellaneous/Keybinds.coffee
@@ -1,6 +1,6 @@
Keybinds =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Keybinds']
+ return if !Conf['Keybinds']
for hotkey of Conf.hotkeys
$.sync hotkey, Keybinds.sync
@@ -88,6 +88,17 @@ Keybinds =
$('.prev button', Index.pagelist).click()
when Conf['Search form']
Index.searchInput.focus()
+ when Conf['Paged mode']
+ return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'paged'
+ Index.setIndexMode 'paged'
+ when Conf['All pages mode']
+ return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'all pages'
+ Index.setIndexMode 'all pages'
+ when Conf['Catalog mode']
+ return unless g.VIEW is 'index' and Conf['Index Mode'] isnt 'catalog'
+ Index.setIndexMode 'catalog'
+ when Conf['Cycle sort type']
+ Index.cycleSortType()
# Thread Navigation
when Conf['Next thread']
return if g.VIEW isnt 'index'
diff --git a/src/Miscellaneous/Nav.coffee b/src/Miscellaneous/Nav.coffee
index 659cce4e1..0c93cc2d5 100644
--- a/src/Miscellaneous/Nav.coffee
+++ b/src/Miscellaneous/Nav.coffee
@@ -5,8 +5,6 @@ Nav =
return unless Conf['Index Navigation']
when 'thread'
return unless Conf['Reply Navigation']
- else # catalog
- return
span = $.el 'span',
id: 'navlinks'
diff --git a/src/Miscellaneous/Time.coffee b/src/Miscellaneous/Time.coffee
index 60239f627..4bb2b4919 100644
--- a/src/Miscellaneous/Time.coffee
+++ b/src/Miscellaneous/Time.coffee
@@ -1,6 +1,6 @@
Time =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Time Formatting']
+ return if !Conf['Time Formatting']
@funk = @createFunc Conf['time']
Post.callbacks.push
diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee
index 028a4ccd9..784c4738f 100644
--- a/src/Monitoring/ThreadUpdater.coffee
+++ b/src/Monitoring/ThreadUpdater.coffee
@@ -254,11 +254,3 @@ ThreadUpdater =
window.scrollTo 0, d.body.clientHeight
else
Header.scrollTo nodes[0]
-
- # Enable 4chan features.
- threadID = ThreadUpdater.thread.ID
- {length} = $$ '.thread > .postContainer', ThreadUpdater.root
- if Conf['Enable 4chan\'s Extension']
- $.globalEval "Parser.parseThread(#{threadID}, #{-count})"
- else
- Fourchan.parseThread threadID, length - count, length
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 3eac7ae4c..ab570ebb0 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -72,9 +72,8 @@ ThreadWatcher =
else if Conf['Auto Watch Reply']
ThreadWatcher.add board.threads[threadID]
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
+ for threadID, data of ThreadWatcher.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
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index 98c4b62fb..067f617a9 100644
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -139,7 +139,7 @@ Unread =
Unread.readArray Unread.postsQuotingYou
Unread.update() if e
- saveLastReadPost: ->
+ saveLastReadPost: <% if (type === 'crx') { %>$.debounce 5 * $.SECOND,<% } %> ->
return if Unread.thread.isDead
Unread.db.set
boardID: Unread.thread.board.ID
@@ -158,7 +158,7 @@ Unread =
count = Unread.posts.length
if Conf['Unread Count']
- d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then "/#{g.BOARD}/ - 404" else "#{Unread.title}"}"
+ d.title = "#{if count or !Conf['Hide Unread Count at (0)'] then "(#{count}) " else ''}#{if g.DEAD then Unread.title.replace '-', '- 404 -' else Unread.title}"
return unless Conf['Unread Tab Icon']
diff --git a/src/Posting/QR.captcha.coffee b/src/Posting/QR.captcha.coffee
index c19e319ed..16d533f81 100644
--- a/src/Posting/QR.captcha.coffee
+++ b/src/Posting/QR.captcha.coffee
@@ -1,38 +1,25 @@
QR.captcha =
init: ->
return if d.cookie.indexOf('pass_enabled=1') >= 0
- return unless @isEnabled = !!$.id 'captchaFormPart'
- $.asap (-> $.id 'recaptcha_challenge_field_holder'), @ready.bind @
- ready: ->
- setLifetime = (e) => @lifetime = e.detail
- $.on window, 'captcha:timeout', setLifetime
- $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
- $.off window, 'captcha:timeout', setLifetime
+ container = $.id 'captchaContainer'
+ return unless @isEnabled = !!container
imgContainer = $.el 'div',
className: 'captcha-img'
title: 'Reload reCAPTCHA'
innerHTML: '
'
+ hidden: true
input = $.el 'input',
className: 'captcha-input field'
title: 'Verification'
+ placeholder: 'Focus to load reCAPTCHA'
autocomplete: 'off'
spellcheck: false
@nodes =
- challenge: $.id 'recaptcha_challenge_field_holder'
- img: imgContainer.firstChild
- input: input
+ img: imgContainer.firstChild
+ input: input
- new MutationObserver(@load.bind @).observe @nodes.challenge,
- childList: true
-
- $.on imgContainer, 'click', @reload.bind @
- $.on input, 'keydown', @keydown.bind @
- $.get 'captchas', [], ({captchas}) =>
- @sync captchas
- $.sync 'captchas', @sync
- # start with an uncached captcha
- @reload()
+ $.on input, 'focus', @setup
<% if (type === 'userscript') { %>
# XXX Firefox lacks focusin/focusout support.
@@ -42,6 +29,36 @@ QR.captcha =
$.addClass QR.nodes.el, 'has-captcha'
$.after QR.nodes.com.parentNode, [imgContainer, input]
+
+ @setupObserver = new MutationObserver @afterSetup
+ @setupObserver.observe container, childList: true
+ @afterSetup() # reCAPTCHA might have loaded before the QR.
+ setup: ->
+ $.globalEval 'loadRecaptcha()'
+ afterSetup: ->
+ return unless challenge = $.id 'recaptcha_challenge_field_holder'
+ QR.captcha.setupObserver.disconnect()
+ delete QR.captcha.setupObserver
+
+ setLifetime = (e) -> QR.captcha.lifetime = e.detail
+ $.on window, 'captcha:timeout', setLifetime
+ $.globalEval 'window.dispatchEvent(new CustomEvent("captcha:timeout", {detail: RecaptchaState.timeout}))'
+ $.off window, 'captcha:timeout', setLifetime
+
+ {img, input} = QR.captcha.nodes
+ img.parentNode.hidden = false
+ $.off input, 'focus', QR.captcha.setup
+ $.on input, 'keydown', QR.captcha.keydown.bind QR.captcha
+ $.on img.parentNode, 'click', QR.captcha.reload.bind QR.captcha
+
+ $.get 'captchas', [], ({captchas}) ->
+ QR.captcha.sync captchas
+ $.sync 'captchas', QR.captcha.sync
+
+ QR.captcha.nodes.challenge = challenge
+ new MutationObserver(QR.captcha.load.bind QR.captcha).observe challenge,
+ childList: true
+ QR.captcha.load()
sync: (captchas) ->
QR.captcha.captchas = captchas
QR.captcha.count()
@@ -70,6 +87,7 @@ QR.captcha =
@reload()
$.set 'captchas', @captchas
clear: ->
+ return unless @captchas # not loaded yet.
now = Date.now()
for captcha, i in @captchas
break if captcha.timeout > now
@@ -87,7 +105,7 @@ QR.captcha =
@nodes.input.value = null
@clear()
count: ->
- count = @captchas.length
+ count = if @captchas then @captchas.length else 0
@nodes.input.placeholder = switch count
when 0
'Verification (Shift + Enter to cache)'
diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee
index 2eed475ff..337ac7c59 100644
--- a/src/Posting/QR.coffee
+++ b/src/Posting/QR.coffee
@@ -50,14 +50,15 @@ QR =
else
QR.status()
- QR.persist() if Conf['Persistent QR']
+ return unless Conf['Persistent QR']
+ QR.open()
+ QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'index' and Conf['Index Mode'] is 'catalog'
node: ->
+ if QR.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
+ $.addClass @nodes.root, 'your-post'
$.on $('a[title="Quote this post"]', @nodes.info), 'click', QR.quote
- persist: ->
- QR.open()
- QR.hide() if Conf['Auto-Hide QR'] or g.VIEW is 'catalog'
open: ->
if QR.nodes
QR.nodes.el.hidden = false
diff --git a/src/Posting/QR.cooldown.coffee b/src/Posting/QR.cooldown.coffee
index ce0fa7960..4a5f3f08c 100644
--- a/src/Posting/QR.cooldown.coffee
+++ b/src/Posting/QR.cooldown.coffee
@@ -7,16 +7,13 @@ QR.cooldown =
$.off window, 'cooldown:timers', setTimers
for type of QR.cooldown.types
QR.cooldown.types[type] = +QR.cooldown.types[type]
- QR.cooldown.upSpd = 0
- QR.cooldown.upSpdAccuracy = .5
key = "cooldown.#{g.BOARD}"
$.get key, {}, (item) ->
QR.cooldown.cooldowns = item[key]
QR.cooldown.start()
$.sync key, QR.cooldown.sync
start: ->
- return unless Conf['Cooldown']
- return if QR.cooldown.isCounting
+ return if QR.cooldown.isCounting or !Object.keys(QR.cooldown.cooldowns).length
QR.cooldown.isCounting = true
QR.cooldown.count()
sync: (cooldowns) ->
@@ -32,10 +29,6 @@ QR.cooldown =
if delay
cooldown = {delay}
else
- if post.file
- upSpd = post.file.size / ((start - req.uploadStartTime) / $.SECOND)
- QR.cooldown.upSpdAccuracy = ((upSpd > QR.cooldown.upSpd * .9) + QR.cooldown.upSpdAccuracy) / 2
- QR.cooldown.upSpd = upSpd
cooldown = {isReply, threadID}
QR.cooldown.cooldowns[start] = cooldown
$.set "cooldown.#{g.BOARD}", QR.cooldown.cooldowns
@@ -48,7 +41,7 @@ QR.cooldown =
$.delete "cooldown.#{g.BOARD}"
count: ->
unless Object.keys(QR.cooldown.cooldowns).length
- $.delete "#{g.BOARD}.cooldown"
+ $.delete "cooldown.#{g.BOARD}"
delete QR.cooldown.isCounting
delete QR.cooldown.seconds
QR.status()
@@ -62,9 +55,10 @@ QR.cooldown =
isReply = post.thread isnt 'new'
hasFile = !!post.file
seconds = null
- {types, cooldowns, upSpd, upSpdAccuracy} = QR.cooldown
+ {types, cooldowns} = QR.cooldown
for start, cooldown of cooldowns
+ start = +start
if 'delay' of cooldown
if cooldown.delay
seconds = Math.max seconds, cooldown.delay--
@@ -76,8 +70,10 @@ QR.cooldown =
if isReply is cooldown.isReply
# Only cooldowns relevant to this post can set the seconds variable:
# reply cooldown with a reply, thread cooldown with a thread
- elapsed = Math.floor (now - start) / $.SECOND
- continue if elapsed < 0 # clock changed since then?
+ elapsed = (now - start) // $.SECOND
+ if elapsed < 0 # clock changed since then?
+ QR.cooldown.unset start
+ continue
type = unless isReply
'thread'
else if hasFile
@@ -90,9 +86,6 @@ QR.cooldown =
type += '_intra' if isReply and +post.thread is cooldown.threadID
seconds = Math.max seconds, types[type] - elapsed
- if seconds and Conf['Cooldown Prediction'] and hasFile and upSpd
- seconds -= Math.floor post.file.size / upSpd * upSpdAccuracy
- seconds = Math.max seconds, 0
# Update the status when we change posting type.
# Don't get stuck at some random number.
# Don't interfere with progress status updates.
diff --git a/src/Posting/QR.post.coffee b/src/Posting/QR.post.coffee
index 06194bbe3..275406e7e 100644
--- a/src/Posting/QR.post.coffee
+++ b/src/Posting/QR.post.coffee
@@ -35,7 +35,7 @@ QR.post = class
else
'new'
- prev = QR.posts[QR.posts.length - 1]
+ [..., prev] = QR.posts
QR.posts.push @
@nodes.spoiler.checked = @spoiler = if prev and Conf['Remember Spoiler']
prev.spoiler
@@ -153,7 +153,10 @@ QR.post = class
@filesize = $.bytesToString file.size
@nodes.label.hidden = false if QR.spoiler
URL.revokeObjectURL @URL
- @showFileData() if @ is QR.selected
+ if @ is QR.selected
+ @showFileData()
+ else
+ @updateFilename()
unless /^image/.test file.type
@nodes.el.style.backgroundImage = null
return
diff --git a/src/Quotelinks/QuoteBacklink.coffee b/src/Quotelinks/QuoteBacklink.coffee
index cb407e50e..f04c0a555 100644
--- a/src/Quotelinks/QuoteBacklink.coffee
+++ b/src/Quotelinks/QuoteBacklink.coffee
@@ -3,19 +3,19 @@ QuoteBacklink =
# - previous, same, and following posts.
# - existing and yet-to-exist posts.
# - newly fetched posts.
- # - in copies.
+ # - clones.
# XXX what about order for fetched posts?
#
- # First callback creates backlinks and add them to relevant containers.
- # Second callback adds relevant containers into posts.
- # This is is so that fetched posts can get their backlinks,
- # and that as much backlinks are appended in the background as possible.
+ # First callback creates a map of quoted -> [quoters],
+ # and append backlinks to posts that already have containers.
+ # Second callback creates, fill and append containers.
init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Backlinks']
+ return if !Conf['Quote Backlinks']
format = Conf['backlink'].replace /%id/g, "' + id + '"
@funk = Function 'id', "return '#{format}'"
- @containers = {}
+ @frag = $.nodes [$.tn(' '), $.el 'a', className: 'backlink']
+ @map = {}
Post.callbacks.push
name: 'Quote Backlinking Part 1'
cb: @firstNode
@@ -23,35 +23,42 @@ QuoteBacklink =
name: 'Quote Backlinking Part 2'
cb: @secondNode
firstNode: ->
- return if @isClone or !@quotes.length
- a = $.el 'a',
- href: "/#{@board}/res/#{@thread}#p#{@}"
- className: if @isHidden then 'filtered backlink' else 'backlink'
- textContent: QuoteBacklink.funk @ID
- for quote in @quotes
- containers = [QuoteBacklink.getContainer quote]
- if (post = g.posts[quote]) and post.nodes.backlinkContainer
- # Don't add OP clones when OP Backlinks is disabled,
- # as the clones won't have the backlink containers.
- for clone in post.clones
- containers.push clone.nodes.backlinkContainer
- for container in containers
- link = a.cloneNode true
- if Conf['Quote Previewing']
- $.on link, 'mouseover', QuotePreview.mouseover
- if Conf['Quote Inlining']
- $.on link, 'click', QuoteInline.toggle
- $.add container, [$.tn(' '), link]
+ return if @isClone
+ for quoteID in @quotes
+ (QuoteBacklink.map[quoteID] or= []).push @fullID
+ continue unless (post = g.posts[quoteID]) and container = post?.nodes.backlinkContainer
+ for post in [post].concat post.clones
+ $.add post.nodes.backlinkContainer, QuoteBacklink.buildBacklink post, @
return
secondNode: ->
- if @isClone and (@origin.isReply or Conf['OP Backlinks'])
- @nodes.backlinkContainer = $ '.container', @nodes.info
- return
# Don't backlink the OP.
return unless @isReply or Conf['OP Backlinks']
- container = QuoteBacklink.getContainer @fullID
- @nodes.backlinkContainer = container
+ if @isClone
+ @nodes.backlinkContainer = $ '.backlink-container', @nodes.info
+ return unless Conf['Quote Markers']
+ for backlink in @nodes.backlinks
+ QuoteMarkers.parseQuotelink @, backlink, true, QuoteBacklink.funk Get.postDataFromLink(backlink).postID
+ return
+ @nodes.backlinkContainer = container = $.el 'span',
+ className: 'backlink-container'
+ if @fullID of QuoteBacklink.map
+ for quoteID in QuoteBacklink.map[@fullID]
+ if post = g.posts[quoteID] # Post hasn't been collected since.
+ $.add container, QuoteBacklink.buildBacklink @, post
$.add @nodes.info, container
- getContainer: (id) ->
- @containers[id] or=
- $.el 'span', className: 'container'
+ buildBacklink: (quoted, quoter) ->
+ frag = QuoteBacklink.frag.cloneNode true
+ a = frag.lastElementChild
+ a.href = "/#{quoter.board}/res/#{quoter.thread}#p#{quoter}"
+ a.textContent = text = QuoteBacklink.funk quoter.ID
+ if quoter.isDead
+ $.addClass a, 'deadlink'
+ if quoter.isHidden
+ $.addClass a, 'filtered'
+ if Conf['Quote Markers']
+ QuoteMarkers.parseQuotelink quoted, a, false, text
+ if Conf['Quote Previewing']
+ $.on a, 'mouseover', QuotePreview.mouseover
+ if Conf['Quote Inlining']
+ $.on a, 'click', QuoteInline.toggle
+ frag
diff --git a/src/Quotelinks/QuoteCT.coffee b/src/Quotelinks/QuoteCT.coffee
deleted file mode 100644
index 9ba959acd..000000000
--- a/src/Quotelinks/QuoteCT.coffee
+++ /dev/null
@@ -1,22 +0,0 @@
-QuoteCT =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark Cross-thread Quotes']
-
- # \u00A0 is nbsp
- @text = '\u00A0(Cross-thread)'
- Post.callbacks.push
- name: 'Mark Cross-thread Quotes'
- cb: @node
- node: ->
- # Stop there if it's a clone of a post in the same thread.
- return if @isClone and @thread is @context.thread
-
- {board, thread} = if @isClone then @context else @
- for quotelink in @nodes.quotelinks
- {boardID, threadID} = Get.postDataFromLink quotelink
- continue unless threadID # deadlink
- if @isClone
- quotelink.textContent = quotelink.textContent.replace QuoteCT.text, ''
- if boardID is board.ID and threadID isnt thread.ID
- $.add quotelink, $.tn QuoteCT.text
- return
diff --git a/src/Quotelinks/QuoteInline.coffee b/src/Quotelinks/QuoteInline.coffee
index 526bf75fc..b61aa5af3 100644
--- a/src/Quotelinks/QuoteInline.coffee
+++ b/src/Quotelinks/QuoteInline.coffee
@@ -1,6 +1,6 @@
QuoteInline =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Inlining']
+ return if !Conf['Quote Inlining']
Post.callbacks.push
name: 'Quote Inlining'
diff --git a/src/Quotelinks/QuoteMarkers.coffee b/src/Quotelinks/QuoteMarkers.coffee
new file mode 100644
index 000000000..fe6a66d4a
--- /dev/null
+++ b/src/Quotelinks/QuoteMarkers.coffee
@@ -0,0 +1,39 @@
+QuoteMarkers =
+ init: ->
+ return if !Conf['Quote Markers']
+
+ Post.callbacks.push
+ name: 'Quote Markers'
+ cb: @node
+ node: ->
+ for quotelink in @nodes.quotelinks
+ QuoteMarkers.parseQuotelink @, quotelink, !!@isClone
+ return
+ parseQuotelink: (post, quotelink, mayReset, customText) ->
+ {board, thread} = if post.isClone then post.context else post
+ markers = []
+ {boardID, threadID, postID} = Get.postDataFromLink quotelink
+
+ if QR.db?.get {boardID, threadID, postID}
+ markers.push 'You'
+
+ if board.ID is boardID
+ if thread.ID is postID
+ markers.push 'OP'
+
+ if threadID and threadID isnt thread.ID # threadID is 0 for deadlinks
+ markers.push 'Cross-thread'
+
+ if $.hasClass quotelink, 'deadlink'
+ markers.push 'Dead'
+
+ text = if customText
+ customText
+ else if boardID is post.board.ID
+ ">>#{postID}"
+ else
+ ">>>/#{boardID}/#{postID}"
+ if markers.length
+ quotelink.textContent = "#{text}\u00A0(#{markers.join '/'})"
+ else if mayReset
+ quotelink.textContent = text
diff --git a/src/Quotelinks/QuoteOP.coffee b/src/Quotelinks/QuoteOP.coffee
deleted file mode 100644
index 4e80520ff..000000000
--- a/src/Quotelinks/QuoteOP.coffee
+++ /dev/null
@@ -1,29 +0,0 @@
-QuoteOP =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark OP Quotes']
-
- # \u00A0 is nbsp
- @text = '\u00A0(OP)'
- Post.callbacks.push
- name: 'Mark OP Quotes'
- cb: @node
- node: ->
- # Stop there if it's a clone of a post in the same thread.
- return if @isClone and @thread is @context.thread
- # Stop there if there's no quotes in that post.
- return unless (quotes = @quotes).length
- {quotelinks} = @nodes
-
- # rm (OP) from cross-thread quotes.
- if @isClone and @thread.fullID in quotes
- for quotelink in quotelinks
- quotelink.textContent = quotelink.textContent.replace QuoteOP.text, ''
-
- {fullID} = (if @isClone then @context else @).thread
- # add (OP) to quotes quoting this context's OP.
- return unless fullID in quotes
- for quotelink in quotelinks
- {boardID, postID} = Get.postDataFromLink quotelink
- if "#{boardID}.#{postID}" is fullID
- $.add quotelink, $.tn QuoteOP.text
- return
diff --git a/src/Quotelinks/QuotePreview.coffee b/src/Quotelinks/QuotePreview.coffee
index 1b1694344..d138cdd81 100644
--- a/src/Quotelinks/QuotePreview.coffee
+++ b/src/Quotelinks/QuotePreview.coffee
@@ -1,6 +1,6 @@
QuotePreview =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Quote Previewing']
+ return if !Conf['Quote Previewing']
Post.callbacks.push
name: 'Quote Previewing'
diff --git a/src/Quotelinks/QuoteStrikeThrough.coffee b/src/Quotelinks/QuoteStrikeThrough.coffee
index b24273322..bf3120516 100644
--- a/src/Quotelinks/QuoteStrikeThrough.coffee
+++ b/src/Quotelinks/QuoteStrikeThrough.coffee
@@ -1,6 +1,6 @@
QuoteStrikeThrough =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter']
+ return if !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] and !Conf['Filter']
Post.callbacks.push
name: 'Strike-through Quotes'
diff --git a/src/Quotelinks/QuoteYou.coffee b/src/Quotelinks/QuoteYou.coffee
deleted file mode 100644
index 4f7219149..000000000
--- a/src/Quotelinks/QuoteYou.coffee
+++ /dev/null
@@ -1,14 +0,0 @@
-QuoteYou =
- init: ->
- return if g.VIEW is 'catalog' or !Conf['Mark Quotes of You'] or !Conf['Quick Reply']
-
- # \u00A0 is nbsp
- @text = '\u00A0(You)'
- Post.callbacks.push
- name: 'Mark Quotes of You'
- cb: @node
- node: ->
- return if @isClone
- for quotelink in @nodes.quotelinks when QR.db.get Get.postDataFromLink quotelink
- $.add quotelink, $.tn QuoteYou.text
- return
diff --git a/src/Quotelinks/Quotify.coffee b/src/Quotelinks/Quotify.coffee
index 0b7503b94..333f47ec1 100644
--- a/src/Quotelinks/Quotify.coffee
+++ b/src/Quotelinks/Quotify.coffee
@@ -1,6 +1,6 @@
Quotify =
init: ->
- return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes']
+ return if !Conf['Resurrect Quotes']
Post.callbacks.push
name: 'Resurrect Quotes'
@@ -37,28 +37,20 @@ Quotify =
quoteID = "#{boardID}.#{postID}"
if post = g.posts[quoteID]
- unless post.isDead
- # Don't (Dead) when quotifying in an archived post,
- # and we know the post still exists.
- a = $.el 'a',
- href: "/#{boardID}/res/#{post.thread}#p#{postID}"
- className: 'quotelink'
- textContent: quote
- else
- # Replace the .deadlink span if we can redirect.
- a = $.el 'a',
- href: "/#{boardID}/res/#{post.thread}#p#{postID}"
- className: 'quotelink deadlink'
- target: '_blank'
- textContent: "#{quote}\u00A0(Dead)"
- $.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
- else if redirect = Redirect.to 'thread', {boardID, threadID: 0, postID}
+ # Don't add 'deadlink' when quotifying in an archived post,
+ # and we don't know if the post died yet.
+ a = $.el 'a',
+ href: "/#{boardID}/res/#{post.thread}#p#{postID}"
+ className: if post.isDead then 'quotelink deadlink' else 'quotelink'
+ textContent: quote
+ $.extend a.dataset, {boardID, threadID: post.thread.ID, postID}
+ else if redirect = Redirect.to 'thread', {boardID, postID}
# Replace the .deadlink span if we can redirect.
a = $.el 'a',
href: redirect
className: 'deadlink'
+ textContent: quote
target: '_blank'
- textContent: "#{quote}\u00A0(Dead)"
if Redirect.to 'post', {boardID, postID}
# Make it function as a normal quote if we can fetch the post.
$.addClass a, 'quotelink'
@@ -68,7 +60,7 @@ Quotify =
@quotes.push quoteID
unless a
- deadlink.textContent = "#{quote}\u00A0(Dead)"
+ deadlink.textContent = "#{quote}\u00A0(Dead)" if Conf['Quote Markers']
return
$.replace deadlink, a