diff --git a/CHANGELOG.md b/CHANGELOG.md
index 002e98a51..883682af1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,10 +1,11 @@
-- **Thread Watcher** rewrite:
+- **Thread Watcher** improvements:
- It is now possible to open all watched threads via the `Open all threads` button in the Thread Watcher's menu.
- Added the `Current Board` setting to switch between showing watched threads from the current board or all boards, disabled by default.
- About dead (404'd) threads:
- Dead threads will be typographically indicated with a strikethrough.
- - Dead threads will directly link to the corresponding archive when possible.
+ - Dead threads will directly link to the corresponding archive when available.
- A button to prune all 404'd threads from the list is now available.
+ - Added the `Auto Prune` setting to automatically prune 404'd threads, disabled by default.
- The current thread is now highlighted in the list of watched threads.
- Watching the current thread can be done in the Header's menu too.
- Removed the `Check for Updates` setting:
diff --git a/src/General/Config.coffee b/src/General/Config.coffee
index 23e36d461..2270444ec 100644
--- a/src/General/Config.coffee
+++ b/src/General/Config.coffee
@@ -80,6 +80,7 @@ Config =
'Current Board': [false, 'Only show watched threads from the current board.']
'Auto Watch': [true, 'Automatically watch threads you start.']
'Auto Watch Reply': [false, 'Automatically watch threads you reply to.']
+ 'Auto Prune': [false, 'Automatically prune 404\'d threads.']
filter:
name: """
# Filter any namefags:
diff --git a/src/General/Main.coffee b/src/General/Main.coffee
index eb10afe7f..039ad745d 100644
--- a/src/General/Main.coffee
+++ b/src/General/Main.coffee
@@ -112,6 +112,7 @@ Main =
initFeature 'Thread Stats', ThreadStats
initFeature 'Thread Updater', ThreadUpdater
initFeature 'Thread Watcher', ThreadWatcher
+ initFeature 'Thread Watcher (Menu)', ThreadWatcher.menu
initFeature 'Index Navigation', Nav
initFeature 'Keybinds', Keybinds
initFeature 'Show Dice Roll', Dice
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 1629209d9..98193166e 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -8,14 +8,16 @@ ThreadWatcher =
"""
@list = @dialog.lastElementChild
- @menuInit()
- @addHeaderMenuEntry()
-
- $.on $('.menu-button', @dialog), 'click', @cb.menuToggle
$.on d, 'QRPostSuccessful', @cb.post
$.on d, 'ThreadUpdate', @cb.threadUpdate if g.VIEW is 'thread'
$.on d, '4chanXInitFinished', @ready
+ now = Date.now()
+ if (@db.data.lastChecked or 0) < now - 2 * $.HOUR
+ @db.data.lastChecked = now
+ ThreadWatcher.fetchAllStatus()
+ @db.save()
+
# XXX tmp conversion from old to new format
$.get 'WatchedThreads', null, ({WatchedThreads}) ->
return unless WatchedThreads
@@ -27,97 +29,36 @@ ThreadWatcher =
Thread::callbacks.push
name: 'Thread Watcher'
cb: @node
-
node: ->
toggler = $.el 'img',
className: 'watcher-toggler'
$.on toggler, 'click', ThreadWatcher.cb.toggle
$.before $('input', @OP.nodes.post), toggler
-
- return if g.VIEW isnt 'thread' or !Conf['Auto Watch']
- $.get 'AutoWatch', 0, ({AutoWatch}) =>
- return if AutoWatch isnt @ID
- ThreadWatcher.add @
- $.delete 'AutoWatch'
-
- menuInit: ->
- ThreadWatcher.menu = new UI.Menu 'thread watcher'
-
- # `Open all` entry
- entry =
- type: 'thread watcher'
- el: $.el 'a',
- textContent: 'Open all threads'
- href: 'javascript:;'
- open: ->
- (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
- true
- $.event 'AddMenuEntry', entry
- $.on entry.el, 'click', ThreadWatcher.cb.openAll
-
- # `Prune 404'd threads` entry
- entry =
- type: 'thread watcher'
- el: $.el 'a',
- textContent: 'Prune 404\'d threads'
- href: 'javascript:;'
- open: ->
- (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
- true
- $.event 'AddMenuEntry', entry
- $.on entry.el, 'click', ThreadWatcher.cb.pruneDeads
-
- # `Settings` entries:
- subEntries = []
- for name, conf of Config.threadWatcher
- subEntries.push ThreadWatcher.createSubEntry name, conf[1]
- $.event 'AddMenuEntry',
- type: 'thread watcher'
- el: $.el 'span', textContent: 'Settings'
- subEntries: subEntries
- createSubEntry: (name, desc) ->
- entry =
- type: 'thread watcher'
- el: $.el 'label',
- innerHTML: " #{name}"
- title: desc
- input = entry.el.firstElementChild
- input.checked = Conf[name]
- $.on input, 'change', $.cb.checked
- $.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
- entry
- addHeaderMenuEntry: ->
- return if g.VIEW isnt 'thread'
- ThreadWatcher.entryEl = $.el 'a', href: 'javascript:;'
- entry =
- type: 'header'
- el: ThreadWatcher.entryEl
- order: 60
- $.event 'AddMenuEntry', entry
- $.on entry.el, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
-
ready: ->
$.off d, '4chanXInitFinished', ThreadWatcher.ready
return unless Main.isThisPageLegit()
ThreadWatcher.refresh()
$.add d.body, ThreadWatcher.dialog
+ return unless Conf['Auto Watch']
+ $.get 'AutoWatch', 0, ({AutoWatch}) ->
+ return unless thread = g.BOARD.threads[AutoWatch]
+ ThreadWatcher.add thread
+ $.delete 'AutoWatch'
+
cb:
- menuToggle: (e) ->
- ThreadWatcher.menu.toggle e, @, ThreadWatcher
openAll: ->
return if $.hasClass @, 'disabled'
for a in $$ 'a[title]', ThreadWatcher.list
$.open a.href
$.event 'CloseMenu'
+ checkThreads: ->
+ return if $.hasClass @, 'disabled'
+ ThreadWatcher.fetchAllStatus()
pruneDeads: ->
return if $.hasClass @, 'disabled'
- for boardID, threads of ThreadWatcher.db.data.boards
- if Conf['Current Board'] and boardID isnt g.BOARD.ID
- continue
- for threadID, data of threads
- continue unless data.isDead
- delete threads[threadID]
+ for {boardID, threadID, data} in ThreadWatcher.getAll() when data.isDead
+ delete ThreadWatcher.db.data.boards[boardID][threadID]
ThreadWatcher.db.deleteIfEmpty {boardID}
ThreadWatcher.db.save()
ThreadWatcher.refresh()
@@ -125,7 +66,7 @@ ThreadWatcher =
toggle: ->
ThreadWatcher.toggle Get.postFromNode(@).thread
rm: ->
- [boardID, threadID] = @parentNode.dataset.fullid.split '.'
+ [boardID, threadID] = @parentNode.dataset.fullID.split '.'
ThreadWatcher.rm boardID, +threadID
post: (e) ->
{board, postID, threadID} = e.detail
@@ -135,57 +76,77 @@ ThreadWatcher =
else if Conf['Auto Watch Reply']
ThreadWatcher.add board.threads[threadID]
threadUpdate: (e) ->
- # Update 404 status.
- return unless e.detail[404]
{thread} = e.detail
- return unless ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
+ return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID}
+ # Update 404 status.
ThreadWatcher.add thread
- refresh: ->
- nodes = []
+ fetchAllStatus: ->
+ # XXX need visual feedback
+ for thread in ThreadWatcher.getAll()
+ ThreadWatcher.fetchStatus thread
+ return
+ fetchStatus: ({boardID, threadID, data}) ->
+ return if data.isDead
+ $.ajax "//api.4chan.org/#{boardID}/res/#{threadID}.json",
+ onload: ->
+ return if @status isnt 404
+ if Conf['Auto Prune']
+ ThreadWatcher.rm boardID, threadID
+ else
+ data.isDead = true
+ ThreadWatcher.db.set {boardID, threadID, val: data}
+ ThreadWatcher.refresh()
+ ,
+ type: 'head'
+
+ getAll: ->
+ all = []
for boardID, threads of ThreadWatcher.db.data.boards
if Conf['Current Board'] and boardID isnt g.BOARD.ID
continue
for threadID, data of threads
- x = $.el 'a',
- textContent: '×'
- href: 'javascript:;'
- $.on x, 'click', ThreadWatcher.cb.rm
+ all.push {boardID, threadID, data}
+ all
- if data.isDead
- href = Redirect.to 'thread', {boardID, threadID}
- link = $.el 'a',
- href: href or "/#{boardID}/res/#{threadID}"
- textContent: data.excerpt
- title: data.excerpt
+ makeLine: (boardID, threadID, data) ->
+ x = $.el 'a',
+ textContent: '×'
+ href: 'javascript:;'
+ $.on x, 'click', ThreadWatcher.cb.rm
- nodes.push div = $.el 'div'
- div.setAttribute 'data-fullid', "#{boardID}.#{threadID}"
- $.addClass div, 'dead-thread' if data.isDead
- $.add div, [x, $.tn(' '), link]
+ if data.isDead
+ href = Redirect.to 'thread', {boardID, threadID}
+ link = $.el 'a',
+ href: href or "/#{boardID}/res/#{threadID}"
+ textContent: data.excerpt
+ title: data.excerpt
- list = ThreadWatcher.dialog.lastElementChild
+ div = $.el 'div'
+ fullID = "#{boardID}.#{threadID}"
+ div.dataset.fullID = fullID
+ $.addClass div, 'current' if g.VIEW is 'thread' and fullID is "#{g.BOARD}.#{g.THREADID}"
+ $.addClass div, 'dead-thread' if data.isDead
+ $.add div, [x, $.tn(' '), link]
+ div
+ refresh: ->
+ nodes = []
+ for {boardID, threadID, data} in ThreadWatcher.getAll()
+ nodes.push ThreadWatcher.makeLine boardID, threadID, data
+
+ {list} = ThreadWatcher
$.rmAll list
$.add list, nodes
- if g.VIEW is 'thread'
- {entryEl} = ThreadWatcher
- if div = $ "div[data-fullid='#{g.BOARD}.#{g.THREADID}']", list
- $.addClass div, 'current'
- $.addClass entryEl, 'unwatch-thread'
- $.rmClass entryEl, 'watch-thread'
- entryEl.textContent = 'Unwatch thread'
- else
- $.addClass entryEl, 'watch-thread'
- $.rmClass entryEl, 'unwatch-thread'
- entryEl.textContent = 'Watch thread'
-
for threadID, thread of g.BOARD.threads
toggler = $ '.watcher-toggler', thread.OP.nodes.post
toggler.src = if ThreadWatcher.db.get {boardID: thread.board.ID, threadID}
Favicon.default
else
Favicon.empty
+
+ for refresher in ThreadWatcher.menu.refreshers
+ refresher()
return
toggle: (thread) ->
@@ -196,13 +157,16 @@ ThreadWatcher =
else
ThreadWatcher.add thread
add: (thread) ->
- data = excerpt: Get.threadExcerpt thread
+ data = {}
+ boardID = thread.board.ID
+ threadID = thread.ID
if thread.isDead
+ if Conf['Auto Prune'] and ThreadWatcher.db.get {boardID, threadID}
+ ThreadWatcher.rm boardID, threadID
+ return
data.isDead = true
- ThreadWatcher.db.set
- boardID: thread.board.ID
- threadID: thread.ID
- val: data
+ data.excerpt = Get.threadExcerpt thread
+ ThreadWatcher.db.set {boardID, threadID, val: data}
ThreadWatcher.refresh()
rm: (boardID, threadID) ->
ThreadWatcher.db.delete {boardID, threadID}
@@ -214,3 +178,89 @@ ThreadWatcher =
for threadID, data of threads
(newFormat[boardID] or= {})[threadID] = excerpt: data.textContent
newFormat
+
+ menu:
+ refreshers: []
+ init: ->
+ return if !Conf['Thread Watcher']
+ menu = new UI.Menu 'thread watcher'
+ $.on $('.menu-button', ThreadWatcher.dialog), 'click', (e) ->
+ menu.toggle e, @, ThreadWatcher
+ @addHeaderMenuEntry()
+ @addMenuEntries()
+
+ addHeaderMenuEntry: ->
+ return if g.VIEW isnt 'thread'
+ entryEl = $.el 'a',
+ href: 'javascript:;'
+ $.event 'AddMenuEntry',
+ type: 'header'
+ el: entryEl
+ order: 60
+ $.on entryEl, 'click', -> ThreadWatcher.toggle g.threads["#{g.BOARD}.#{g.THREADID}"]
+ @refreshers.push ->
+ [addClass, rmClass, text] = if $ '.current', ThreadWatcher.list
+ ['unwatch-thread', 'watch-thread', 'Unwatch thread']
+ else
+ ['watch-thread', 'unwatch-thread', 'Watch thread']
+ $.addClass entryEl, addClass
+ $.rmClass entryEl, rmClass
+ entryEl.textContent = text
+
+ addMenuEntries: ->
+ entries = []
+
+ # `Open all` entry
+ entries.push
+ cb: ThreadWatcher.cb.openAll
+ entry:
+ type: 'thread watcher'
+ el: $.el 'a',
+ textContent: 'Open all threads'
+ refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled'
+
+ # `Check 404'd threads` entry
+ entries.push
+ cb: ThreadWatcher.cb.checkThreads
+ entry:
+ type: 'thread watcher'
+ el: $.el 'a',
+ textContent: 'Check 404\'d threads'
+ refresh: -> (if $('div:not(.dead-thread)', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
+
+ # `Prune 404'd threads` entry
+ entries.push
+ cb: ThreadWatcher.cb.pruneDeads
+ entry:
+ type: 'thread watcher'
+ el: $.el 'a',
+ textContent: 'Prune 404\'d threads'
+ refresh: -> (if $('.dead-thread', ThreadWatcher.list) then $.rmClass else $.addClass) @el, 'disabled'
+
+ # `Settings` entries:
+ subEntries = []
+ for name, conf of Config.threadWatcher
+ subEntries.push @createSubEntry name, conf[1]
+ entries.push
+ entry:
+ type: 'thread watcher'
+ el: $.el 'span',
+ textContent: 'Settings'
+ subEntries: subEntries
+
+ for {entry, cb, refresh} in entries
+ entry.el.href = 'javascript:;' if entry.el.nodeName is 'A'
+ $.on entry.el, 'click', cb if cb
+ @refreshers.push refresh.bind entry if refresh
+ $.event 'AddMenuEntry', entry
+ createSubEntry: (name, desc) ->
+ entry =
+ type: 'thread watcher'
+ el: $.el 'label',
+ innerHTML: " #{name}"
+ title: desc
+ input = entry.el.firstElementChild
+ input.checked = Conf[name]
+ $.on input, 'change', $.cb.checked
+ $.on input, 'change', ThreadWatcher.refresh if name is 'Current Board'
+ entry