ThreadWatcher = init: -> return if !Conf['Thread Watcher'] @shortcut = sc = $.el 'a', id: 'watcher-link' textContent: 'Watcher' title: 'Thread Watcher' href: 'javascript:;' className: 'disabled fa fa-eye' @db = new DataBoard 'watchedThreads', @refresh, true @dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= importHTML('Monitoring/ThreadWatcher') %> @status = $ '#watcher-status', @dialog @list = @dialog.lastElementChild @refreshButton = $ '.refresh', @dialog @unreaddb = Unread.db or new DataBoard 'lastReadPosts' $.on d, 'QRPostSuccessful', @cb.post $.on sc, 'click', @toggleWatcher $.on @refreshButton, 'click', @fetchAllStatus $.on $('.move > .close', @dialog), 'click', @toggleWatcher $.on d, '4chanXInitFinished', @ready switch g.VIEW when 'index' $.on d, 'IndexRefresh', @cb.onIndexRefresh when 'thread' $.on d, 'ThreadUpdate', @cb.onThreadRefresh if Conf['Toggleable Thread Watcher'] Header.addShortcut sc $.addClass doc, 'fixed-watcher' ThreadWatcher.fetchAuto() if g.VIEW is 'index' and Conf['JSON Navigation'] and Conf['Menu'] and g.BOARD.ID isnt 'f' Menu.menu.addEntry el: $.el 'a', href: 'javascript:;' order: 6 open: ({thread}) -> return false if Conf['Index Mode'] isnt 'catalog' @el.textContent = if ThreadWatcher.isWatched thread 'Unwatch thread' else 'Watch thread' $.off @el, 'click', @cb if @cb @cb = -> $.event 'CloseMenu' ThreadWatcher.toggle thread $.on @el, 'click', @cb true Post.callbacks.push name: 'Thread Watcher' cb: @node CatalogThread.callbacks.push name: 'Thread Watcher' cb: @catalogNode isWatched: (thread) -> ThreadWatcher.db?.get {boardID: thread.board.ID, threadID: thread.ID} node: -> return if @isReply if @isClone toggler = $ '.watch-thread-link', @nodes.post else toggler = $.el 'a', href: 'javascript:;' className: 'watch-thread-link' $.before $('input', @nodes.post), toggler $.on toggler, 'click', ThreadWatcher.cb.toggle catalogNode: -> $.addClass @nodes.root, 'watched' if ThreadWatcher.isWatched @thread $.on @nodes.thumb.parentNode, 'click', (e) => return unless e.button is 0 and e.altKey ThreadWatcher.toggle @thread e.preventDefault() ready: -> $.off d, '4chanXInitFinished', ThreadWatcher.ready return unless Main.isThisPageLegit() ThreadWatcher.refresh() $.add d.body, ThreadWatcher.dialog if Conf['Toggleable Thread Watcher'] ThreadWatcher.dialog.hidden = true return unless Conf['Auto Watch'] $.get 'AutoWatch', 0, ({AutoWatch}) -> return unless thread = g.BOARD.threads[AutoWatch] ThreadWatcher.add thread $.delete 'AutoWatch' toggleWatcher: -> $.toggleClass ThreadWatcher.shortcut, 'disabled' ThreadWatcher.dialog.hidden = !ThreadWatcher.dialog.hidden cb: openAll: -> return if $.hasClass @, 'disabled' for a in $$ 'a[title]', ThreadWatcher.list $.open a.href $.event 'CloseMenu' pruneDeads: -> return if $.hasClass @, 'disabled' ThreadWatcher.db.forceSync() 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() $.event 'CloseMenu' toggle: -> {thread} = Get.postFromNode @ Index.followedThreadID = thread.ID ThreadWatcher.toggle thread delete Index.followedThreadID rm: -> [boardID, threadID] = @parentNode.dataset.fullID.split '.' ThreadWatcher.rm boardID, +threadID post: (e) -> {boardID, threadID, postID} = e.detail if postID is threadID if Conf['Auto Watch'] $.set 'AutoWatch', threadID else if Conf['Auto Watch Reply'] ThreadWatcher.add g.threads[boardID + '.' + threadID] onIndexRefresh: -> {db} = ThreadWatcher boardID = g.BOARD.ID db.forceSync() for threadID, data of db.data.boards[boardID] when not data?.isDead and threadID not of g.BOARD.threads if Conf['Auto Prune'] or not (data and typeof data is 'object') ThreadWatcher.db.delete {boardID, threadID} else data.isDead = true ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.refresh() onThreadRefresh: (e) -> thread = g.threads[e.detail.threadID] return unless e.detail[404] and ThreadWatcher.db.get {boardID: thread.board.ID, threadID: thread.ID} # Update dead status. ThreadWatcher.add thread fetchCount: fetched: 0 fetching: 0 fetchAuto: -> clearTimeout ThreadWatcher.timeout return unless Conf['Auto Update Thread Watcher'] {db} = ThreadWatcher interval = if Conf['Show Unread Count'] then 5 * $.MINUTE else 2 * $.HOUR now = Date.now() if now >= (db.data.lastChecked or 0) + interval db.data.lastChecked = now ThreadWatcher.fetchAllStatus() db.save() ThreadWatcher.timeout = setTimeout ThreadWatcher.fetchAuto, interval fetchAllStatus: -> ThreadWatcher.db.forceSync() ThreadWatcher.unreaddb.forceSync() QR.db?.forceSync() return unless (threads = ThreadWatcher.getAll()).length for thread in threads ThreadWatcher.fetchStatus thread return fetchStatus: (thread, force) -> {boardID, threadID, data} = thread return if data.isDead and not force {fetchCount} = ThreadWatcher if fetchCount.fetching is 0 ThreadWatcher.status.textContent = '...' $.addClass ThreadWatcher.refreshButton, 'fa-spin' fetchCount.fetching++ $.ajax "//a.4cdn.org/#{boardID}/thread/#{threadID}.json", onloadend: -> ThreadWatcher.parseStatus.call @, thread parseStatus: ({boardID, threadID, data}) -> {fetchCount} = ThreadWatcher fetchCount.fetched++ if fetchCount.fetched is fetchCount.fetching fetchCount.fetched = 0 fetchCount.fetching = 0 status = '' $.rmClass ThreadWatcher.refreshButton, 'fa-spin' else status = "#{Math.round fetchCount.fetched / fetchCount.fetching * 100}%" ThreadWatcher.status.textContent = status if @status is 200 and @response isDead = !!@response.posts[0].archived if isDead and Conf['Auto Prune'] ThreadWatcher.db.delete {boardID, threadID} ThreadWatcher.refresh() return lastReadPost = ThreadWatcher.unreaddb.get boardID: boardID threadID: threadID defaultValue: 0 unread = quotingYou = 0 for postObj in @response.posts continue unless postObj.no > lastReadPost continue if QR.db?.get {boardID, threadID, postID: postObj.no} unread++ continue unless QR.db and postObj.com regexp = /]*\bhref="(?:\/([^\/]+)\/thread\/(\d+))?(?:#p(\d+))?"/g while match = regexp.exec postObj.com if QR.db.get { boardID: match[1] or boardID threadID: match[2] or threadID postID: match[3] or match[2] or threadID } quotingYou++ continue if isDead isnt data.isDead or unread isnt data.unread or quotingYou isnt data.quotingYou data.isDead = isDead data.unread = unread data.quotingYou = quotingYou ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.refresh() else if @status is 404 if Conf['Auto Prune'] ThreadWatcher.db.delete {boardID, threadID} else data.isDead = true delete data.unread delete data.quotingYou ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.refresh() 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 when data and typeof data is 'object' all.push {boardID, threadID, data} all makeLine: (boardID, threadID, data) -> x = $.el 'a', className: 'fa fa-times' href: 'javascript:;' $.on x, 'click', ThreadWatcher.cb.rm link = $.el 'a', href: "/#{boardID}/thread/#{threadID}" title: data.excerpt className: 'watcher-link' if Conf['Show Unread Count'] and data.unread? count = $.el 'span', textContent: "(#{data.unread})" className: 'watcher-unread' $.add link, count title = $.el 'span', textContent: data.excerpt className: 'watcher-title' $.add link, title 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 if Conf['Show Unread Count'] $.addClass div, 'replies-unread' if data.unread $.addClass div, 'replies-quoting-you' if data.quotingYou $.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 g.threads.forEach (thread) -> helper = if ThreadWatcher.isWatched thread then ['addClass', 'Unwatch'] else ['rmClass', 'Watch'] if thread.OP for post in [thread.OP, thread.OP.clones...] toggler = $ '.watch-thread-link', post.nodes.post $[helper[0]] toggler, 'watched' toggler.title = "#{helper[1]} Thread" $[helper[0]] thread.catalogView.nodes.root, 'watched' if thread.catalogView ThreadWatcher.refreshIcon() for refresher in ThreadWatcher.menu.refreshers refresher() if Index.nodes and Conf['Pin Watched Threads'] Index.sort() Index.buildIndex() refreshIcon: -> for className in ['replies-unread', 'replies-quoting-you'] ThreadWatcher.shortcut.classList.toggle className, !!$(".#{className}", ThreadWatcher.dialog) return update: (boardID, threadID, newData) -> return unless data = ThreadWatcher.db?.get {boardID, threadID} if newData.isDead and Conf['Auto Prune'] ThreadWatcher.db.delete {boardID, threadID} ThreadWatcher.refresh() return n = 0 n++ for key, val of newData when data[key] isnt val return unless n ThreadWatcher.db.forceSync() return unless data = ThreadWatcher.db.get {boardID, threadID} $.extend data, newData ThreadWatcher.db.set {boardID, threadID, val: data} if line = $ "#watched-threads > [data-full-i-d='#{boardID}.#{threadID}']", ThreadWatcher.dialog newLine = ThreadWatcher.makeLine boardID, threadID, data $.replace line, newLine ThreadWatcher.refreshIcon() else ThreadWatcher.refresh() set404: (boardID, threadID, cb) -> return cb() unless data = ThreadWatcher.db?.get {boardID, threadID} if Conf['Auto Prune'] ThreadWatcher.db.delete {boardID, threadID} return cb() return cb() if data.isDead and not (data.unread? or data.quotingYou?) data.isDead = true delete data.unread delete data.quotingYou ThreadWatcher.db.set {boardID, threadID, val: data}, cb toggle: (thread) -> boardID = thread.board.ID threadID = thread.ID if ThreadWatcher.db.get {boardID, threadID} ThreadWatcher.rm boardID, threadID else ThreadWatcher.add thread add: (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 data.excerpt = Get.threadExcerpt thread ThreadWatcher.db.set {boardID, threadID, val: data} ThreadWatcher.refresh() if Conf['Show Unread Count'] ThreadWatcher.fetchStatus {boardID, threadID, data}, true rm: (boardID, threadID) -> ThreadWatcher.db.delete {boardID, threadID} ThreadWatcher.refresh() convert: (oldFormat) -> newFormat = {} for boardID, threads of oldFormat for threadID, data of threads (newFormat[boardID] or= {})[threadID] = excerpt: data.textContent newFormat menu: refreshers: [] init: -> return if !Conf['Thread Watcher'] menu = @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:;' Header.menu.addEntry 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: el: $.el 'a', textContent: 'Open all threads' refresh: -> (if ThreadWatcher.list.firstElementChild then $.rmClass else $.addClass) @el, 'disabled' # `Prune dead threads` entry entries.push cb: ThreadWatcher.cb.pruneDeads entry: el: $.el 'a', textContent: 'Prune dead 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: 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 @menu.addEntry entry return createSubEntry: (name, desc) -> entry = type: 'thread watcher' el: UI.checkbox name, name.replace(' Thread Watcher', '') entry.el.title = desc input = entry.el.firstElementChild $.on input, 'change', $.cb.checked $.on input, 'change', ThreadWatcher.refresh if name in ['Current Board', 'Show Unread Count'] $.on input, 'change', ThreadWatcher.fetchAuto if name in ['Show Unread Count', 'Auto Update Thread Watcher'] entry