diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee index bcbd0e3602..17745f2fc7 100644 --- a/src/Filtering/Filter.coffee +++ b/src/Filtering/Filter.coffee @@ -78,10 +78,12 @@ Filter = else types = ['subject', 'name', 'filename', 'comment'] + watch = /(?:^|;)\s*watch/.test filter + # Hide the post (default case). - hide = !(hl or noti) + hide = !(hl or noti or watch) - filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti} + filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti, watch} if key is 'general' for type in types (@filters[type] or= []).push filter @@ -119,11 +121,12 @@ Filter = test: (post, hideable=true) -> return post.filterResults if post.filterResults - hide = false - stub = true - hl = undefined - top = false - noti = false + hide = false + stub = true + hl = undefined + top = false + noti = false + watch = false if QuoteYou.isYou(post) hideable = false mask = (if post.isReply then 2 else 1) @@ -149,14 +152,16 @@ Filter = top or= filter.top if filter.noti noti = true + if filter.watch + watch = true if hide {hide, stub} else - {hl, top, noti} + {hl, top, noti, watch} node: -> return if @isClone - {hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index')) + {hide, stub, hl, top, noti, watch} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index')) if hide if @isReply PostHiding.hide @, stub @@ -168,6 +173,9 @@ Filter = $.addClass @nodes.root, hl... if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@) Unread.openNotification @, ' triggered a notification filter' + if watch and !ThreadWatcher.isWatched(@thread) + ThreadWatcher.add(@thread) + catalog: -> return unless (url = g.SITE.urls.catalogJSON?(g.BOARD)) diff --git a/src/General/Settings/Filter-guide.html b/src/General/Settings/Filter-guide.html index 02bba58050..477ef4f4f0 100644 --- a/src/General/Settings/Filter-guide.html +++ b/src/General/Settings/Filter-guide.html @@ -43,6 +43,12 @@ Show a desktop notification instead of hiding.
For example: notify;. +
  • + Add thread to thread watcher instead of hiding.
    + For example: watch;
    + If you specify boards with this flag, the boards provided will be periodically scanned in the background for new threads.
    + If you do not specify boards, then it will scan every page you visit for matching threads/comments. +
  • Filters in the "General" section apply to multiple fields, by default subject,name,filename,comment.
    The fields can be specified with the type option, separated by commas.
    diff --git a/src/Monitoring/AutoWatcher.coffee b/src/Monitoring/AutoWatcher.coffee new file mode 100644 index 0000000000..68aa3e474e --- /dev/null +++ b/src/Monitoring/AutoWatcher.coffee @@ -0,0 +1,89 @@ +AutoWatcher = + init: -> + return unless Conf['Filter'] + + AutoWatcher.periodicScan() + + periodicScan: -> + clearTimeout AutoWatcher.timeout + + interval = 5 * $.MINUTE + + now = Date.now() + + unless Conf['autoWatchLastScan'] and (now - interval < Conf['autoWatchLastScan']) + AutoWatcher.scan() + $.set 'autoWatchLastScan', now + AutoWatcher.timeout = setTimeout AutoWatcher.periodicScan, interval + + scan: -> + sitesAndBoards = (for own _, filters of Filter.filters + Object.keys(filter.boards) for filter in filters when filter.watch and filter.boards + ).flat(2).reduce (acc, i) -> + [_, k, v] = i.match(/(.*)\/(.*)/) + acc[k] ?= [] + acc[k].push(v) + acc + , {} + for own rawSite, boards of sitesAndBoards + break unless site = g.sites[rawSite] + for boardID in boards + AutoWatcher.fetchCatalog(boardID, site, AutoWatcher.parseCatalog) + + fetchCatalog: (boardID, site, cb) -> + return unless url = site.urls['catalogJSON']?({boardID}) + + ajax = if site.ID is g.SITE.ID then $.ajax else CrossOrigin.ajax + + onLoadEnd = -> + cb.apply @, [site, boardID] + + $.whenModified( + url, + 'AutoWatcher' + onLoadEnd, + {timeout: $.MINUTE, ajax} + ) + + parseCatalog: (site, boardID) -> + addedThreads = false + rawCatalog = @.response.reduce ((acc, i, idx) -> + threads = for thread in i.threads + thread.extraData = { + page: idx + 1, + modified: thread.last_modified, + replies: thread.replies, + unread: thread.replies + } + if thread.last_replies + thread.extraData.last = thread.last_replies[thread.last_replies.length - 1].no + thread + acc.concat(threads) + ), [] + for thread in rawCatalog + continue if ThreadWatcher.isWatchedRaw(boardID, thread.no) + parsedThread = site.Build.parseJSON(thread, {siteID: site.ID, boardID}) + + # Hacks for ThreadWatcher + parsedThread.isDead = false + parsedThread.board = {ID: boardID} + Object.assign(parsedThread, thread.extraData) + + # I wish destructuring was actually pattern matching + {watch} = Filter.test(parsedThread) + continue unless watch + + excerptName = ( + parsedThread?.info?.subject or + parsedThread?.info?.comment.replace(/\n+/g, ' // ') or + parsedThread?.file?.name or + "No.#{parsedThread.ID}" + ) + excerpt = "/#{boardID}/ - #{excerptName}" + excerpt = "#{excerpt[...70]}..." if excerpt.length > 73 + data = Object.assign(thread.extraData, {excerpt}) + ThreadWatcher.addRaw(boardID, parsedThread.ID, data, null, true) + addedThreads = true + # Check to see if we added any threads. If so, trigger a refresh AFTER we're done adding them all, to avoid spamming the API + # We already give the ThreadWatcher most of what it needs, this is just to get things like lastPage coloring + ThreadWatcher.buttonFetchAll() if addedThreads diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index 187f15dfb9..de77c42bf6 100644 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -551,7 +551,7 @@ ThreadWatcher = data.excerpt = Get.threadExcerpt thread if thread.OP ThreadWatcher.addRaw boardID, threadID, data, cb - addRaw: (boardID, threadID, data, cb) -> + addRaw: (boardID, threadID, data, cb, skipRefresh = false) -> oldData = ThreadWatcher.db.get {boardID, threadID, defaultValue: $.dict()} delete oldData.last delete oldData.modified @@ -559,6 +559,7 @@ ThreadWatcher = ThreadWatcher.db.set {boardID, threadID, val: oldData}, cb ThreadWatcher.refresh() thread = {siteID: g.SITE.ID, boardID, threadID, data, force: true} + return if skipRefresh if Conf['Show Page'] and !data.isDead ThreadWatcher.fetchBoard [thread] else if ThreadWatcher.unreadEnabled and Conf['Show Unread Count'] diff --git a/src/main/Main.coffee b/src/main/Main.coffee index 0e79a5d736..2429a5324b 100644 --- a/src/main/Main.coffee +++ b/src/main/Main.coffee @@ -72,6 +72,7 @@ Main = Conf['archives'] = Redirect.archives Conf['selectedArchives'] = $.dict() Conf['cooldowns'] = $.dict() + Conf['autoWatchLastScan'] = null Conf['Index Sort'] = $.dict() Conf["Last Long Reply Thresholds #{i}"] = $.dict() for i in [0...2] Conf['siteProperties'] = $.dict() @@ -521,7 +522,7 @@ Main = unless nodes[i] (cb() if cb) return - setTimeout softTask, 0 + setTimeout softTask, 0 softTask() @@ -698,6 +699,7 @@ Main = ['Thread Updater', ThreadUpdater] ['Thread Watcher', ThreadWatcher] ['Thread Watcher (Menu)', ThreadWatcher.menu] + ['Auto Watcher', AutoWatcher] ['Mark New IPs', MarkNewIPs] ['Index Navigation', Nav] ['Keybinds', Keybinds]