From 2003b25c05c7fc418e8966e0d546eaa38634987a Mon Sep 17 00:00:00 2001
From: ebinBuddha <30810167+ebinBuddha@users.noreply.github.com>
Date: Sat, 12 Jan 2019 01:25:41 +0100
Subject: [PATCH 001/131] added desktop notification for filters
---
src/Filtering/Filter.coffee | 32 ++++++++++++++++++++++++++------
1 file changed, 26 insertions(+), 6 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 80df4cffd..bec7324b8 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -63,6 +63,9 @@ Filter =
false
else
Conf['Stubs']
+
+ # Desktop notification
+ noti = /notify/.test filter
# Highlight the post, or hide it.
# If not specified, the highlight class will be filter-highlight.
@@ -82,7 +85,7 @@ Filter =
else
types = ['subject', 'name', 'filename', 'comment']
- filter = @createFilter regexp, boards, excludes, op, stub, hl, top
+ filter = @createFilter regexp, boards, excludes, op, stub, hl, top, noti
if key is 'general'
for type in types
(@filters[type] or= []).push filter
@@ -94,7 +97,7 @@ Filter =
name: 'Filter'
cb: @node
- createFilter: (regexp, boards, excludes, op, stub, hl, top) ->
+ createFilter: (regexp, boards, excludes, op, stub, hl, top, noti) ->
test =
if typeof regexp is 'string'
# MD5 checking
@@ -107,6 +110,7 @@ Filter =
stub: stub
class: hl
top: top
+ noti: noti
(value, boardID, isReply) ->
if boards and boardID not in boards
@@ -125,6 +129,7 @@ Filter =
stub = true
hl = undefined
top = false
+ noti = false
if QuoteYou.isYou(post)
hideable = false
for key of Filter.filters when ((value = Filter[key] post)?)
@@ -138,14 +143,16 @@ Filter =
unless hl and result.class in hl
(hl or= []).push result.class
top or= result.top
+ if result.noti
+ noti = true
if hide
- {hide, stub}
+ {hide, stub, noti}
else
- {hl, top}
+ {hl, top, noti}
node: ->
return if @isClone
- {hide, stub, hl, top} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
+ {hide, stub, hl, top, noti} = Filter.test @, (!@isFetchedQuote and (@isReply or g.VIEW is 'index'))
if hide
if @isReply
PostHiding.hide @, stub
@@ -155,7 +162,20 @@ Filter =
if hl
@highlights = hl
$.addClass @nodes.root, hl...
- return
+ if noti
+ if Header.areNotificationsEnabled
+ if not (Unread.posts is null)
+ if (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
+ notif = new Notification "#{@info.nameBlock} triggered a notification filter",
+ body: @commentDisplay()
+ icon: Favicon.logo
+ notif.onclick = ->
+ Header.scrollToIfNeeded @nodes.bottom, true
+ window.focus()
+ notif.onshow = ->
+ setTimeout ->
+ notif.close()
+ , 7 * $.SECOND
isHidden: (post) ->
!!Filter.test(post).hide
From 28a46edcf3654362d33586b68e5172d0f4b54f07 Mon Sep 17 00:00:00 2001
From: ebinBuddha <30810167+ebinBuddha@users.noreply.github.com>
Date: Thu, 21 Feb 2019 12:10:05 +0100
Subject: [PATCH 002/131] whitespaces
---
src/Filtering/Filter.coffee | 20 ++++++++++----------
1 file changed, 10 insertions(+), 10 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index bec7324b8..28d463389 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -63,9 +63,9 @@ Filter =
false
else
Conf['Stubs']
-
- # Desktop notification
- noti = /notify/.test filter
+
+ # Desktop notification
+ noti = /notify/.test filter
# Highlight the post, or hide it.
# If not specified, the highlight class will be filter-highlight.
@@ -110,7 +110,7 @@ Filter =
stub: stub
class: hl
top: top
- noti: noti
+ noti: noti
(value, boardID, isReply) ->
if boards and boardID not in boards
@@ -129,7 +129,7 @@ Filter =
stub = true
hl = undefined
top = false
- noti = false
+ noti = false
if QuoteYou.isYou(post)
hideable = false
for key of Filter.filters when ((value = Filter[key] post)?)
@@ -143,8 +143,8 @@ Filter =
unless hl and result.class in hl
(hl or= []).push result.class
top or= result.top
- if result.noti
- noti = true
+ if result.noti
+ noti = true
if hide
{hide, stub, noti}
else
@@ -162,9 +162,9 @@ Filter =
if hl
@highlights = hl
$.addClass @nodes.root, hl...
- if noti
- if Header.areNotificationsEnabled
- if not (Unread.posts is null)
+ if noti
+ if Header.areNotificationsEnabled
+ if not (Unread.posts is null)
if (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
notif = new Notification "#{@info.nameBlock} triggered a notification filter",
body: @commentDisplay()
From a94d76d84b3138a142006133c6144ef8d2ec16d4 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 8 Mar 2019 20:45:37 -0800
Subject: [PATCH 003/131] Let the default responseType in $.ajax always be
"json" regardless of URL.
---
src/Miscellaneous/Report.coffee | 1 -
src/platform/$.coffee | 2 +-
src/platform/CrossOrigin.coffee | 2 +-
3 files changed, 2 insertions(+), 3 deletions(-)
diff --git a/src/Miscellaneous/Report.coffee b/src/Miscellaneous/Report.coffee
index 9ae50029e..aad20ea1b 100644
--- a/src/Miscellaneous/Report.coffee
+++ b/src/Miscellaneous/Report.coffee
@@ -77,7 +77,6 @@ Report =
for [name, url] in urls
do (name, url) ->
$.ajax url,
- responseType: 'json'
onloadend: ->
results.push [name, @response or {error: ''}]
if results.length is urls.length
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 78697a9db..abb6f28bc 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -52,7 +52,7 @@ $.ajax = do ->
(url, options={}, extra={}) ->
{type, whenModified, bypassCache, upCallbacks, form} = extra
- options.responseType ?= 'json' if /\.json$/.test url
+ options.responseType ?= 'json'
# XXX https://forums.lanik.us/viewtopic.php?f=64&t=24173&p=78310
url = url.replace /^((?:https?:)?\/\/(?:\w+\.)?4c(?:ha|d)n\.org)\/adv\//, '$1//adv/'
if whenModified
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index 179a90cde..cb9bc899a 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -90,7 +90,7 @@ CrossOrigin =
unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
if bypassCache
$.cleanCache (url2) -> url2 is url
- if (req = $.cache url, cb, responseType: 'json')
+ if (req = $.cache url, cb)
$.on req, 'abort error', -> cb.call({})
else
cb.call {}
From 44feaf4eb700d13b218076bc11188cd5cdeab9f4 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 8 Mar 2019 20:54:41 -0800
Subject: [PATCH 004/131] Eliminate use of onabort and event in Index.coffee.
---
src/General/Index.coffee | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 8acf0e7bc..0b5540c1d 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -573,6 +573,7 @@ Index =
"#{hiddenCount} hidden threads"
update: (firstTime) ->
+ Index.req?.aborted = true
Index.req?.abort()
Index.notice?.close()
@@ -594,13 +595,12 @@ Index =
return
Index.req = $.ajax "#{location.protocol}//a.4cdn.org/#{g.BOARD}/catalog.json",
- onabort: Index.load
onloadend: Index.load
,
whenModified: 'Index'
$.addClass Index.button, 'fa-spin'
- load: (e) ->
+ load: ->
$.rmClass Index.button, 'fa-spin'
{req, notice, nTimeout} = Index
clearTimeout nTimeout if nTimeout
@@ -608,7 +608,7 @@ Index =
delete Index.req
delete Index.notice
- if e.type is 'abort'
+ if req.aborted
req.onloadend = null
notice?.close()
return
From e43d91c9d34b243aba38e780ef634e515ec9fcd6 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 8 Mar 2019 23:38:11 -0800
Subject: [PATCH 005/131] Implement pruning of data for dead threads on vichan
sites with JSON API. #2171 (also work on #525)
---
src/General/BoardConfig.coffee | 5 +++++
src/classes/DataBoard.coffee | 22 ++++++++++++----------
src/site/SW.tinyboard.coffee | 3 +++
src/site/SW.yotsuba.coffee | 2 ++
4 files changed, 22 insertions(+), 10 deletions(-)
diff --git a/src/General/BoardConfig.coffee b/src/General/BoardConfig.coffee
index fb8ee4f06..842cd84da 100644
--- a/src/General/BoardConfig.coffee
+++ b/src/General/BoardConfig.coffee
@@ -48,6 +48,11 @@ BoardConfig =
domain: (board) ->
"boards.#{if BoardConfig.isSFW(board) then '4channel' else '4chan'}.org"
+ isArchived: (board) ->
+ # assume archive exists if no data available to prevent cleaning of archived threads
+ data = (@boards or Conf['boardConfig'].boards)[board]
+ !data or data.is_archived
+
noAudio: (boardID) ->
return false unless Site.software is 'yotsuba'
boards = @boards or Conf['boardConfig'].boards
diff --git a/src/classes/DataBoard.coffee b/src/classes/DataBoard.coffee
index 3a07e3d4c..811dabd81 100644
--- a/src/classes/DataBoard.coffee
+++ b/src/classes/DataBoard.coffee
@@ -116,13 +116,9 @@ class DataBoard
val or defaultValue
clean: ->
- # XXX not yet multisite ready
- return unless Site.software is 'yotsuba'
siteID = Site.hostname
-
for boardID, val of @data[siteID].boards
@deleteIfEmpty {siteID, boardID}
-
now = Date.now()
unless now - 2 * $.HOUR < (@data[siteID].lastChecked or 0) <= now
@data[siteID].lastChecked = now
@@ -131,12 +127,18 @@ class DataBoard
return
ajaxClean: (boardID) ->
- $.cache "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json", (e1) =>
- return unless e1.target.status is 200
- response1 = e1.target.response
- $.cache "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json", (e2) =>
- return unless e2.target.status is 200 or boardID in ['b', 'f', 'trash', 'bant']
- @ajaxCleanParse boardID, response1, e2.target.response
+ that = @
+ siteID = Site.hostname
+ threadsList = Site.urls.threadsListJSON?({siteID, boardID})
+ return unless threadsList
+ $.cache threadsList, ->
+ return unless @status is 200
+ archiveList = Site.urls.archiveListJSON?({siteID, boardID})
+ return that.ajaxCleanParse(boardID, @response) unless archiveList
+ response1 = @response
+ $.cache archiveList, ->
+ return unless @status is 200
+ that.ajaxCleanParse(boardID, response1, @response)
ajaxCleanParse: (boardID, response1, response2) ->
siteID = Site.hostname
diff --git a/src/site/SW.tinyboard.coffee b/src/site/SW.tinyboard.coffee
index 78f03b1ef..412724b39 100644
--- a/src/site/SW.tinyboard.coffee
+++ b/src/site/SW.tinyboard.coffee
@@ -60,6 +60,9 @@ SW.tinyboard =
threadJSON: ({siteID, boardID, threadID}) ->
root = Conf['siteProperties'][siteID]?.root
if root then "#{root}#{boardID}/res/#{threadID}.json" else ''
+ threadsListJSON: ({siteID, boardID}) ->
+ root = Conf['siteProperties'][siteID]?.root
+ if root then "#{root}#{boardID}/threads.json" else ''
selectors:
board: 'form[name="postcontrols"]'
diff --git a/src/site/SW.yotsuba.coffee b/src/site/SW.yotsuba.coffee
index ba51fb569..b1c3a0d15 100644
--- a/src/site/SW.yotsuba.coffee
+++ b/src/site/SW.yotsuba.coffee
@@ -4,6 +4,8 @@ SW.yotsuba =
urls:
thread: ({boardID, threadID}) -> "#{location.protocol}//#{BoardConfig.domain(boardID)}/#{boardID}/thread/#{threadID}"
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
+ threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
+ archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
selectors:
board: '.board'
From 67d5af480f2aa0aba710e4655f5ddec27f1be155 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 8 Mar 2019 23:44:38 -0800
Subject: [PATCH 006/131] Remove remaining places where list of boards without
archive is hardcoded. #525
---
src/General/Header.coffee | 2 +-
src/General/Index.coffee | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/General/Header.coffee b/src/General/Header.coffee
index 265f804bd..2cd3060f2 100644
--- a/src/General/Header.coffee
+++ b/src/General/Header.coffee
@@ -282,7 +282,7 @@ Header =
return a.firstChild # Its text node.
if /-expired/.test t
- if boardID not in ['b', 'f', 'trash', 'bant']
+ if BoardConfig.isArchived(boardID)
a.href = "//#{BoardConfig.domain(boardID)}/#{boardID}/archive"
else
return a.firstChild # Its text node.
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 0b5540c1d..dfd087347 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -84,7 +84,7 @@ Index =
@navLinks = $.el 'div', className: 'navLinks json-index'
$.extend @navLinks, <%= readHTML('NavLinks.html') %>
$('.cataloglink a', @navLinks).href = CatalogLinks.catalog()
- $('.archlistlink', @navLinks).hidden = true if g.BOARD.ID in ['b', 'trash', 'bant']
+ $('.archlistlink', @navLinks).hidden = true unless BoardConfig.isArchived(g.BOARD.ID)
$.on $('#index-last-refresh a', @navLinks), 'click', @cb.refreshFront
# Search field
From 496043342fc6898e3cada727992d17b514745a53 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 8 Mar 2019 23:49:18 -0800
Subject: [PATCH 007/131] No longer needed since
44feaf4eb700d13b218076bc11188cd5cdeab9f4
---
src/General/Index.coffee | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index dfd087347..b00080e34 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -609,7 +609,6 @@ Index =
delete Index.notice
if req.aborted
- req.onloadend = null
notice?.close()
return
From 4c329af6e0bfdc6610e25196c0b0cd5005d95f65 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sat, 9 Mar 2019 00:06:51 -0800
Subject: [PATCH 008/131] Remove event from $.cache interface.
---
src/classes/Fetcher.coffee | 10 ++++++----
src/platform/$.coffee | 7 +++----
2 files changed, 9 insertions(+), 8 deletions(-)
diff --git a/src/classes/Fetcher.coffee b/src/classes/Fetcher.coffee
index 1e144c68b..b130c36a3 100644
--- a/src/classes/Fetcher.coffee
+++ b/src/classes/Fetcher.coffee
@@ -15,8 +15,9 @@ class Fetcher
@root.textContent = "Loading post No.#{@postID}..."
if @threadID
- $.cache "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json", (e, isCached) =>
- @fetchedPost e.target, isCached
+ that = @
+ $.cache "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json", ({isCached}) ->
+ that.fetchedPost @, isCached
else
@archivedPost()
@@ -80,8 +81,9 @@ class Fetcher
if isCached
api = "#{location.protocol}//a.4cdn.org/#{@boardID}/thread/#{@threadID}.json"
$.cleanCache (url) -> url is api
- $.cache api, (e) =>
- @fetchedPost e.target, false
+ that = @
+ $.cache api, ->
+ that.fetchedPost @, false
return
# The post can be deleted by the time we check a quote.
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index abb6f28bc..9b95c49ae 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -87,7 +87,7 @@ do ->
$.cache = (url, cb, options) ->
if req = reqs[url]
if req.readyState is 4
- $.queueTask -> cb.call req, req.evt, true
+ $.queueTask -> cb.call req, {isCached: true}
else
req.callbacks.push cb
return req
@@ -96,10 +96,9 @@ do ->
return if not (req = $.ajax url, options)
catch err
return
- $.on req, 'load', (e) ->
- @evt = e
+ $.on req, 'load', ->
for cb in @callbacks
- do (cb) => $.queueTask => cb.call @, e, false
+ do (cb) => $.queueTask => cb.call @, {isCached: false}
delete @callbacks
$.on req, 'abort error', rm
req.callbacks = [cb]
From a7af355820a05bf8ff70b745dab28375d2febb73 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sat, 9 Mar 2019 00:47:16 -0800
Subject: [PATCH 009/131] Limit $.cache to subset of XMLHttpRequest API.
---
src/platform/$.coffee | 28 +++++++++++++---------------
1 file changed, 13 insertions(+), 15 deletions(-)
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 9b95c49ae..078248731 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -84,23 +84,21 @@ $.ajax = do ->
do ->
reqs = {}
- $.cache = (url, cb, options) ->
- if req = reqs[url]
- if req.readyState is 4
- $.queueTask -> cb.call req, {isCached: true}
- else
+ $.cache = (url, cb, options={}) ->
+ if (req = reqs[url])
+ if req.callbacks
req.callbacks.push cb
+ else
+ $.queueTask -> cb.call req, {isCached: true}
return req
- rm = -> delete reqs[url]
- try
- return if not (req = $.ajax url, options)
- catch err
- return
- $.on req, 'load', ->
- for cb in @callbacks
- do (cb) => $.queueTask => cb.call @, {isCached: false}
- delete @callbacks
- $.on req, 'abort error', rm
+ options.onloadend = ->
+ if @status
+ for cb in @callbacks
+ do (cb) => $.queueTask => cb.call @, {isCached: false}
+ delete @callbacks
+ else
+ delete reqs[url]
+ req = $.ajax url, options
req.callbacks = [cb]
reqs[url] = req
$.cleanCache = (testf) ->
From 78a79f1942a6344a0e984dc6fbb2d01805ca684a Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sat, 9 Mar 2019 02:13:19 -0800
Subject: [PATCH 010/131] Let $.cache report connection errors.
---
src/General/Build.Test.coffee | 1 +
src/Miscellaneous/ExpandComment.coffee | 2 +-
src/Miscellaneous/ExpandThread.coffee | 2 +-
src/classes/Fetcher.coffee | 4 +++-
src/platform/$.coffee | 9 ++++-----
src/platform/CrossOrigin.coffee | 7 ++-----
6 files changed, 12 insertions(+), 13 deletions(-)
diff --git a/src/General/Build.Test.coffee b/src/General/Build.Test.coffee
index cc015ac11..12945f106 100644
--- a/src/General/Build.Test.coffee
+++ b/src/General/Build.Test.coffee
@@ -66,6 +66,7 @@ Build.Test =
testOne: (post) ->
Build.Test.postsRemaining++
$.cache "#{location.protocol}//a.4cdn.org/#{post.board.ID}/thread/#{post.thread.ID}.json", ->
+ return unless @response
{posts} = @response
Build.spoilerRange[post.board.ID] = posts[0].custom_spoiler
for postData in posts
diff --git a/src/Miscellaneous/ExpandComment.coffee b/src/Miscellaneous/ExpandComment.coffee
index 09d596774..4adf5eda4 100644
--- a/src/Miscellaneous/ExpandComment.coffee
+++ b/src/Miscellaneous/ExpandComment.coffee
@@ -38,7 +38,7 @@ ExpandComment =
parse: (req, a, post) ->
{status} = req
unless status in [200, 304]
- a.textContent = "Error #{req.statusText} (#{status})"
+ a.textContent = if status then "Error #{req.statusText} (#{status})" else 'Connection Error'
return
posts = req.response.posts
diff --git a/src/Miscellaneous/ExpandThread.coffee b/src/Miscellaneous/ExpandThread.coffee
index d86451371..31bef3bfb 100644
--- a/src/Miscellaneous/ExpandThread.coffee
+++ b/src/Miscellaneous/ExpandThread.coffee
@@ -89,7 +89,7 @@ ExpandThread =
parse: (req, thread, a) ->
if req.status not in [200, 304]
- a.textContent = "Error #{req.statusText} (#{req.status})"
+ a.textContent = if req.status then "Error #{req.statusText} (#{req.status})" else 'Connection Error'
return
Build.spoilerRange[thread.board] = req.response.posts[0].custom_spoiler
diff --git a/src/classes/Fetcher.coffee b/src/classes/Fetcher.coffee
index b130c36a3..472d1f8de 100644
--- a/src/classes/Fetcher.coffee
+++ b/src/classes/Fetcher.coffee
@@ -61,12 +61,14 @@ class Fetcher
{status} = req
unless status is 200
# The thread can die by the time we check a quote.
- return if @archivedPost()
+ return if status and @archivedPost()
$.addClass @root, 'warning'
@root.textContent =
if status is 404
"Thread No.#{@threadID} 404'd."
+ else if !status
+ 'Connection Error'
else
"Error #{req.statusText} (#{req.status})."
return
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 078248731..dc19a2e7f 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -92,12 +92,11 @@ do ->
$.queueTask -> cb.call req, {isCached: true}
return req
options.onloadend = ->
- if @status
- for cb in @callbacks
- do (cb) => $.queueTask => cb.call @, {isCached: false}
- delete @callbacks
- else
+ unless @status
delete reqs[url]
+ for cb in @callbacks
+ do (cb) => $.queueTask => cb.call @, {isCached: false}
+ delete @callbacks
req = $.ajax url, options
req.callbacks = [cb]
reqs[url] = req
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index cb9bc899a..aa73888c5 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -70,7 +70,7 @@ CrossOrigin =
# Attempts to fetch `url` in JSON format using cross-origin privileges, if available.
# On success, calls `cb` with a `this` containing properties `status`, `statusText`, `response` and caches result.
- # On error/abort, calls `cb` with a `this` of `{}`.
+ # On error/abort/timeout, calls `cb` with a `this` of `{}` or in some cases an XMLHttpRequest reflecting the error.
# If `bypassCache` is true, ignores previously cached results.
json: do ->
callbacks = {}
@@ -90,10 +90,7 @@ CrossOrigin =
unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
if bypassCache
$.cleanCache (url2) -> url2 is url
- if (req = $.cache url, cb)
- $.on req, 'abort error', -> cb.call({})
- else
- cb.call {}
+ req = $.cache url, cb
return
<% } %>
From 697d16192f293f050c17f4c451c231efaf9a7d55 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sat, 9 Mar 2019 22:29:40 -0800
Subject: [PATCH 011/131] Index refresh should also fetch freshest copy.
Fixes bug from 52128775e1edb69f83c93526516e6531511725fd.
---
src/General/Index.coffee | 1 +
1 file changed, 1 insertion(+)
diff --git a/src/General/Index.coffee b/src/General/Index.coffee
index 8acf0e7bc..a047d13ab 100644
--- a/src/General/Index.coffee
+++ b/src/General/Index.coffee
@@ -598,6 +598,7 @@ Index =
onloadend: Index.load
,
whenModified: 'Index'
+ bypassCache: true
$.addClass Index.button, 'fa-spin'
load: (e) ->
From 188422f1aee69dfa45cf31ebd035c3029c3b32d1 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sun, 10 Mar 2019 21:11:43 -0700
Subject: [PATCH 012/131] Replace CrossOrigin.json with simpler
CrossOrigin.ajax and a CrossOrigin.cache making use of $.cache.
---
src/Archive/Redirect.coffee | 3 +-
src/Linkification/Embedding.coffee | 6 +-
src/Monitoring/ThreadWatcher.coffee | 9 +--
src/classes/Fetcher.coffee | 2 +-
src/platform/$.coffee | 4 +-
src/platform/CrossOrigin.coffee | 97 ++++++++++++-----------------
6 files changed, 54 insertions(+), 67 deletions(-)
diff --git a/src/Archive/Redirect.coffee b/src/Archive/Redirect.coffee
index 03d20cfab..c7ec71f3d 100644
--- a/src/Archive/Redirect.coffee
+++ b/src/Archive/Redirect.coffee
@@ -68,7 +68,8 @@ Redirect =
continue
load(i).call {status: 200, response}
else
- CrossOrigin.json url, load(i), true
+ CrossOrigin.ajax url,
+ onloadend: load(i)
else
Redirect.parse [], cb
return
diff --git a/src/Linkification/Embedding.coffee b/src/Linkification/Embedding.coffee
index 7de873103..98084a655 100644
--- a/src/Linkification/Embedding.coffee
+++ b/src/Linkification/Embedding.coffee
@@ -111,7 +111,7 @@ Embedding =
if service.queue.length >= service.batchSize
Embedding.flushTitles service
else
- CrossOrigin.json service.api(uid), (-> Embedding.cb.title @, data)
+ CrossOrigin.cache service.api(uid), (-> Embedding.cb.title @, data)
flushTitles: (service) ->
{queue} = service
@@ -120,7 +120,7 @@ Embedding =
cb = ->
Embedding.cb.title @, data for data in queue
return
- CrossOrigin.json service.api(data.uid for data in queue), cb
+ CrossOrigin.cache service.api(data.uid for data in queue), cb
preview: (data) ->
{key, uid, link} = data
@@ -275,7 +275,7 @@ Embedding =
el = $.el 'pre',
hidden: true
id: "gist-embed-#{counter++}"
- CrossOrigin.json "https://api.github.com/gists/#{a.dataset.uid}", ->
+ CrossOrigin.cache "https://api.github.com/gists/#{a.dataset.uid}", ->
el.textContent = Object.values(@response.files)[0].content
el.className = 'prettyprint'
$.global ->
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index d63002850..5e3409219 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -228,10 +228,11 @@ ThreadWatcher =
whenModified: if force then false else 'ThreadWatcher'
else
req = {abort: () -> req.aborted = true}
- CrossOrigin.json url, ->
- return if req.aborted
- ThreadWatcher.parseStatus.call @, thread
- , true, $.MINUTE
+ CrossOrigin.ajax url,
+ onloadend: ->
+ return if req.aborted
+ ThreadWatcher.parseStatus.call @, thread
+ timeout: $.MINUTE
ThreadWatcher.requests.push req
parseStatus: ({siteID, boardID, threadID, data}) ->
diff --git a/src/classes/Fetcher.coffee b/src/classes/Fetcher.coffee
index 472d1f8de..a396bbca1 100644
--- a/src/classes/Fetcher.coffee
+++ b/src/classes/Fetcher.coffee
@@ -111,7 +111,7 @@ class Fetcher
encryptionOK = /^https:\/\//.test(url) or location.protocol is 'http:'
if encryptionOK or Conf['Exempt Archives from Encryption']
that = @
- CrossOrigin.json url, ->
+ CrossOrigin.cache url, ->
if !encryptionOK and @response?.media
{media} = @response
for key of media when /_link$/.test key
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index dc19a2e7f..dd961b27a 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -84,7 +84,7 @@ $.ajax = do ->
do ->
reqs = {}
- $.cache = (url, cb, options={}) ->
+ $.cache = (url, cb, options={}, extra={}) ->
if (req = reqs[url])
if req.callbacks
req.callbacks.push cb
@@ -97,7 +97,7 @@ do ->
for cb in @callbacks
do (cb) => $.queueTask => cb.call @, {isCached: false}
delete @callbacks
- req = $.ajax url, options
+ req = (extra.ajax or $.ajax) url, options
req.callbacks = [cb]
reqs[url] = req
$.cleanCache = (testf) ->
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index aa73888c5..691105786 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -69,65 +69,50 @@ CrossOrigin =
cb blob
# Attempts to fetch `url` in JSON format using cross-origin privileges, if available.
- # On success, calls `cb` with a `this` containing properties `status`, `statusText`, `response` and caches result.
- # On error/abort/timeout, calls `cb` with a `this` of `{}` or in some cases an XMLHttpRequest reflecting the error.
- # If `bypassCache` is true, ignores previously cached results.
- json: do ->
- callbacks = {}
- results = {}
- success = (url, result) ->
- for cb in callbacks[url]
- $.queueTask -> cb.call result
- delete callbacks[url]
- results[url] = result
- failure = (url) ->
- for cb in callbacks[url]
- $.queueTask -> cb.call {}
- delete callbacks[url]
+ # Interface is a subset of that of $.ajax.
+ # Returns an object with `status`, `statusText`, `response` properties, all initially set falsy.
+ # On success, populates the properties.
+ # Both on success or error/abort/timeout, calls `options.onloadend` with the returned object as `this`.
+ ajax: (url, options={}) ->
+ {onloadend, timeout} = options
- (url, cb, bypassCache, timeout) ->
- <% if (type === 'userscript') { %>
- unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
- if bypassCache
- $.cleanCache (url2) -> url2 is url
- req = $.cache url, cb
- return
- <% } %>
+ <% if (type === 'userscript') { %>
+ unless GM?.xmlHttpRequest? or GM_xmlhttpRequest?
+ return $.ajax url, options
+ <% } %>
- if bypassCache
- delete results[url]
- else
- if results[url]
- cb.call results[url]
- return
- if callbacks[url]
- callbacks[url].push cb
- return
- callbacks[url] = [cb]
+ req =
+ status: 0
+ statusText: ''
+ response: null
- <% if (type === 'userscript') { %>
- (GM?.xmlHttpRequest or GM_xmlhttpRequest)
- method: "GET"
- url: url+''
- timeout: timeout
- onload: (xhr) ->
- {status, statusText} = xhr
- try
- response = JSON.parse(xhr.responseText)
- success url, {status, statusText, response}
- catch
- failure url
- onerror: -> failure(url)
- onabort: -> failure(url)
- ontimeout: -> failure(url)
- <% } %>
- <% if (type === 'crx') { %>
- eventPageRequest {type: 'ajax', url, responseType: 'json', timeout}, (result) ->
- if result.status
- success url, result
- else
- failure url
- <% } %>
+ <% if (type === 'userscript') { %>
+ (GM?.xmlHttpRequest or GM_xmlhttpRequest)
+ method: "GET"
+ url: url+''
+ timeout: timeout
+ onload: (xhr) ->
+ try
+ response = JSON.parse(xhr.responseText)
+ $.extend req, {response, status: xhr.status, statusText: xhr.statusText}
+ onloadend.call(req)
+ onerror: -> onloadend.call(req)
+ onabort: -> onloadend.call(req)
+ ontimeout: -> onloadend.call(req)
+ <% } %>
+
+ <% if (type === 'crx') { %>
+ eventPageRequest {type: 'ajax', url, responseType: 'json', timeout}, (result) ->
+ if result.status
+ $.extend req, result
+ onloadend.call(req)
+ <% } %>
+
+ req
+
+ cache: (url, cb, options={}, extra={}) ->
+ extra.ajax = CrossOrigin.ajax
+ $.cache url, cb, options, extra
permission: (cb) ->
<% if (type === 'crx') { %>
From 77c3efde20fb2ab06abdd071110c0653a9daba8e Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sun, 10 Mar 2019 20:44:43 -0700
Subject: [PATCH 013/131] This should be a warning, not an error.
---
src/platform/$.coffee | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index dd961b27a..456493113 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -72,7 +72,7 @@ $.ajax = do ->
$.extend r, options
$.extend r.upload, upCallbacks
# connection error or content blocker
- $.on r, 'error', -> (c.error "4chan X failed to load: #{url}" unless r.status)
+ $.on r, 'error', -> (c.warn "4chan X failed to load: #{url}" unless r.status)
r.send form
catch err
# XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
From f0558692869411678d2b5a4ec59523d14353a421 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sun, 10 Mar 2019 23:01:38 -0700
Subject: [PATCH 014/131] Remove unused parameter from $.cache
---
src/platform/$.coffee | 7 ++++---
src/platform/CrossOrigin.coffee | 6 +++---
2 files changed, 7 insertions(+), 6 deletions(-)
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 456493113..bd3e8e0ce 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -84,20 +84,21 @@ $.ajax = do ->
do ->
reqs = {}
- $.cache = (url, cb, options={}, extra={}) ->
+ $.cache = (url, cb, options={}) ->
+ {ajax} = options
if (req = reqs[url])
if req.callbacks
req.callbacks.push cb
else
$.queueTask -> cb.call req, {isCached: true}
return req
- options.onloadend = ->
+ onloadend = ->
unless @status
delete reqs[url]
for cb in @callbacks
do (cb) => $.queueTask => cb.call @, {isCached: false}
delete @callbacks
- req = (extra.ajax or $.ajax) url, options
+ req = (ajax or $.ajax) url, {onloadend}
req.callbacks = [cb]
reqs[url] = req
$.cleanCache = (testf) ->
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index 691105786..0998970ad 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -110,9 +110,9 @@ CrossOrigin =
req
- cache: (url, cb, options={}, extra={}) ->
- extra.ajax = CrossOrigin.ajax
- $.cache url, cb, options, extra
+ cache: (url, cb) ->
+ $.cache url, cb,
+ ajax: CrossOrigin.ajax
permission: (cb) ->
<% if (type === 'crx') { %>
From 74268f78d5165292c5064de833dafd302c14823d Mon Sep 17 00:00:00 2001
From: ccd0
Date: Mon, 11 Mar 2019 00:33:46 -0700
Subject: [PATCH 015/131] Thread Watcher: Remove fake abort for cross-site
requests.
---
src/Monitoring/ThreadWatcher.coffee | 8 +++-----
1 file changed, 3 insertions(+), 5 deletions(-)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 5e3409219..4886b37bb 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -178,8 +178,8 @@ ThreadWatcher =
abort: ->
for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
- req.abort()
- ThreadWatcher.clearRequests()
+ req.abort?()
+ return
fetchAuto: ->
clearTimeout ThreadWatcher.timeout
@@ -227,10 +227,8 @@ ThreadWatcher =
,
whenModified: if force then false else 'ThreadWatcher'
else
- req = {abort: () -> req.aborted = true}
- CrossOrigin.ajax url,
+ req = CrossOrigin.ajax url,
onloadend: ->
- return if req.aborted
ThreadWatcher.parseStatus.call @, thread
timeout: $.MINUTE
ThreadWatcher.requests.push req
From cafb7250c798b5bcfd45ca76b7a72a6b0eeff6a7 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Mon, 11 Mar 2019 00:49:21 -0700
Subject: [PATCH 016/131] Simplify duplicated code for thread watcher requests.
---
src/Monitoring/ThreadWatcher.coffee | 19 +++++++------------
1 file changed, 7 insertions(+), 12 deletions(-)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 4886b37bb..b5658e696 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -219,18 +219,13 @@ ThreadWatcher =
if ThreadWatcher.requests.length is 0
ThreadWatcher.status.textContent = '...'
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
- if Site.hasCORS?(url) or url.split('/')[...3].join('/') is location.origin
- req = $.ajax url,
- onloadend: ->
- ThreadWatcher.parseStatus.call @, thread
- timeout: $.MINUTE
- ,
- whenModified: if force then false else 'ThreadWatcher'
- else
- req = CrossOrigin.ajax url,
- onloadend: ->
- ThreadWatcher.parseStatus.call @, thread
- timeout: $.MINUTE
+ ajax = if (siteID is Site.hostname) then $.ajax else CrossOrigin.ajax
+ req = ajax url,
+ onloadend: ->
+ ThreadWatcher.parseStatus.call @, thread
+ timeout: $.MINUTE
+ ,
+ whenModified: if force then false else 'ThreadWatcher'
ThreadWatcher.requests.push req
parseStatus: ({siteID, boardID, threadID, data}) ->
From fafdc4fa4459d181d5db498a7ace624421164a8a Mon Sep 17 00:00:00 2001
From: ccd0
Date: Sun, 17 Mar 2019 05:42:50 -0700
Subject: [PATCH 017/131] Allow image hover previews to use full width of
screen.
---
src/General/UI.coffee | 12 +++++++-----
src/Images/ImageHover.coffee | 3 ++-
src/css/style.css | 3 +++
3 files changed, 12 insertions(+), 6 deletions(-)
diff --git a/src/General/UI.coffee b/src/General/UI.coffee
index c57bc0251..388aea7c4 100644
--- a/src/General/UI.coffee
+++ b/src/General/UI.coffee
@@ -308,7 +308,7 @@ dragend = ->
$.off d, 'mouseup', @up
$.set "#{@id}.position", @style.cssText
-hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
+hoverstart = ({root, el, latestEvent, endEvents, height, width, cb, noRemove}) ->
o = {
root
el
@@ -320,6 +320,7 @@ hoverstart = ({root, el, latestEvent, endEvents, height, cb, noRemove}) ->
clientHeight: doc.clientHeight
clientWidth: doc.clientWidth
height
+ width
noRemove
}
o.hover = hover.bind o
@@ -344,6 +345,7 @@ hoverstart.padding = 25
hover = (e) ->
@latestEvent = e
height = (@height or @el.offsetHeight) + hoverstart.padding
+ width = (@width or @el.offsetWidth)
{clientX, clientY} = e
top = if @isImage
@@ -353,10 +355,10 @@ hover = (e) ->
threshold = @clientWidth / 2
threshold = Math.max threshold, @clientWidth - 400 unless @isImage
- [left, right] = if clientX <= threshold
- [clientX + 45 + 'px', '']
- else
- ['', @clientWidth - clientX + 45 + 'px']
+ marginX = (if clientX <= threshold then clientX else @clientWidth - clientX) + 45
+ marginX = Math.min(marginX, @clientWidth - width) if @isImage
+ marginX += 'px'
+ [left, right] = if clientX <= threshold then [marginX, ''] else ['', marginX]
{style} = @
style.top = top + 'px'
diff --git a/src/Images/ImageHover.coffee b/src/Images/ImageHover.coffee
index ee47c120e..fdce1854e 100644
--- a/src/Images/ImageHover.coffee
+++ b/src/Images/ImageHover.coffee
@@ -48,7 +48,7 @@ ImageHover =
@currentTime = el.currentTime if @nodeName is 'VIDEO'
[width, height] = (+x for x in file.dimensions.split 'x')
{left, right} = @getBoundingClientRect()
- maxWidth = Math.max left, doc.clientWidth - right
+ maxWidth = doc.clientWidth
maxHeight = doc.clientHeight - UI.hover.padding
scale = Math.min 1, maxWidth / width, maxHeight / height
el.style.maxWidth = "#{scale * width}px"
@@ -59,6 +59,7 @@ ImageHover =
latestEvent: e
endEvents: 'mouseout click'
height: scale * height
+ width: scale * width
noRemove: true
cb: ->
$.off el, 'error', error
diff --git a/src/css/style.css b/src/css/style.css
index 935f73bdd..89d12663a 100644
--- a/src/css/style.css
+++ b/src/css/style.css
@@ -1344,6 +1344,9 @@ span.hide-announcement {
.fileThumb > .warning {
clear: both;
}
+#ihover {
+ pointer-events: none;
+}
/* WEBM Metadata */
.webm-title > a::before {
content: "title";
From 24f1458a73a7b95c8248783de91c68f274948711 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Mon, 18 Mar 2019 21:54:02 -0700
Subject: [PATCH 018/131] Fix issues from
https://github.com/ccd0/4chan-x/pull/2231#issuecomment-467247167 #2231
---
src/Filtering/Filter.coffee | 16 ++--------------
src/Monitoring/Unread.coffee | 4 ++--
2 files changed, 4 insertions(+), 16 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 28d463389..d77057462 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -162,20 +162,8 @@ Filter =
if hl
@highlights = hl
$.addClass @nodes.root, hl...
- if noti
- if Header.areNotificationsEnabled
- if not (Unread.posts is null)
- if (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
- notif = new Notification "#{@info.nameBlock} triggered a notification filter",
- body: @commentDisplay()
- icon: Favicon.logo
- notif.onclick = ->
- Header.scrollToIfNeeded @nodes.bottom, true
- window.focus()
- notif.onshow = ->
- setTimeout ->
- notif.close()
- , 7 * $.SECOND
+ if noti and Unread.posts and (@ID > Unread.lastReadPost) and not QuoteYou.isYou(@)
+ Unread.openNotification @, ' triggered a notification filter'
isHidden: (post) ->
!!Filter.test(post).hide
diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee
index 4714deff8..50a9a9c28 100644
--- a/src/Monitoring/Unread.coffee
+++ b/src/Monitoring/Unread.coffee
@@ -124,9 +124,9 @@ Unread =
Unread.openNotification post
return
- openNotification: (post) ->
+ openNotification: (post, predicate=' replied to you') ->
return unless Header.areNotificationsEnabled
- notif = new Notification "#{post.info.nameBlock} replied to you",
+ notif = new Notification "#{post.info.nameBlock}#{predicate}",
body: post.commentDisplay()
icon: Favicon.logo
notif.onclick = ->
From 28dc40b1d404ff4987a92941ed80424850bcc718 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Mon, 18 Mar 2019 21:58:22 -0700
Subject: [PATCH 019/131] Don't hide posts with notification filters. #2231
---
src/Filtering/Filter.coffee | 8 ++++----
1 file changed, 4 insertions(+), 4 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index d77057462..386c8f083 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -106,7 +106,7 @@ Filter =
(value) -> regexp.test value
settings =
- hide: !hl
+ hide: !(hl or noti)
stub: stub
class: hl
top: top
@@ -143,10 +143,10 @@ Filter =
unless hl and result.class in hl
(hl or= []).push result.class
top or= result.top
- if result.noti
- noti = true
+ if result.noti
+ noti = true
if hide
- {hide, stub, noti}
+ {hide, stub}
else
{hl, top, noti}
From ef89d9324927629cad6f4d0bff3cd6e845d7e8ca Mon Sep 17 00:00:00 2001
From: ccd0
Date: Mon, 18 Mar 2019 22:03:46 -0700
Subject: [PATCH 020/131] Document notification filters. #2231
---
src/General/Settings/Filter-guide.html | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/src/General/Settings/Filter-guide.html b/src/General/Settings/Filter-guide.html
index df0838609..911a9051f 100644
--- a/src/General/Settings/Filter-guide.html
+++ b/src/General/Settings/Filter-guide.html
@@ -30,6 +30,10 @@
Highlighted OPs will have their threads put on top of the board index by default.
For example: top:yes; or top:no;.
+
+ Show a desktop notification instead of hiding.
+ For example: notify;.
+
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.
From 569ae9b06be56418531943ed016b7be11a8e0c92 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Tue, 19 Mar 2019 14:20:03 -0700
Subject: [PATCH 021/131] Improve Filter performance.
---
src/Filtering/Filter.coffee | 79 ++++++++++++++++---------------------
1 file changed, 35 insertions(+), 44 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 386c8f083..e280f8d24 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -22,17 +22,26 @@ Filter =
# Comma-separated list of the boards this filter applies to.
# Defaults to global.
- boards = filter.match(/boards:([^;]+)/)?[1].toLowerCase() or 'global'
- boards = boards.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards)
- boards = if boards is 'global' then null else boards.split(',')
+ boardsRaw = filter.match(/boards:([^;]+)/)?[1].toLowerCase()
+ if boardsRaw
+ boards = {}
+ for board in boardsRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
+ boards[board] = true
+ else
+ boards = false
# boards to exclude from an otherwise global rule
# due to the sfw and nsfw keywords, also works on all filters
# replaces 'nsfw' and 'sfw' for consistency
- excludes = filter.match(/exclude:([^;]+)/)?[1].toLowerCase() or null
- excludes = if excludes is null then null else excludes.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
+ excludesRaw = filter.match(/exclude:([^;]+)/)?[1].toLowerCase()
+ if excludesRaw
+ excludes = {}
+ for board in excludesRaw.replace('nsfw', nsfwBoards).replace('sfw', sfwBoards).split(',')
+ excludes[board] = true
+ else
+ excludes = false
- if key in ['uniqueID', 'MD5']
+ if (isstring = (key in ['uniqueID', 'MD5']))
# MD5 filter will use strings instead of regular expressions.
regexp = regexp[1]
else
@@ -67,11 +76,10 @@ Filter =
# Desktop notification
noti = /notify/.test filter
- # Highlight the post, or hide it.
+ # Highlight the post.
# If not specified, the highlight class will be filter-highlight.
- # Defaults to post hiding.
- if hl = /highlight/.test filter
- hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight'
+ if (hl = /highlight/.test filter)
+ hl = filter.match(/highlight:([\w-]+)/)?[1] or 'filter-highlight'
# Put highlighted OP's thread on top of the board page or not.
# Defaults to on top.
top = filter.match(/top:(yes|no)/)?[1] or 'yes'
@@ -85,7 +93,10 @@ Filter =
else
types = ['subject', 'name', 'filename', 'comment']
- filter = @createFilter regexp, boards, excludes, op, stub, hl, top, noti
+ # Hide the post (default case).
+ hide = !(hl or noti)
+
+ filter = {isstring, regexp, boards, excludes, op, hide, stub, hl, top, noti}
if key is 'general'
for type in types
(@filters[type] or= []).push filter
@@ -97,32 +108,6 @@ Filter =
name: 'Filter'
cb: @node
- createFilter: (regexp, boards, excludes, op, stub, hl, top, noti) ->
- test =
- if typeof regexp is 'string'
- # MD5 checking
- (value) -> regexp is value
- else
- (value) -> regexp.test value
-
- settings =
- hide: !(hl or noti)
- stub: stub
- class: hl
- top: top
- noti: noti
-
- (value, boardID, isReply) ->
- if boards and boardID not in boards
- return false
- if excludes and boardID in excludes
- return false
- if isReply and op is 'only' or !isReply and op is 'no'
- return false
- unless test value
- return false
- settings
-
test: (post, hideable=true) ->
return post.filterResults if post.filterResults
hide = false
@@ -134,16 +119,22 @@ Filter =
hideable = false
for key of Filter.filters when ((value = Filter[key] post)?)
# Continue if there's nothing to filter (no tripcode for example).
- for filter in Filter.filters[key] when (result = filter value, post.boardID, post.isReply)
- if result.hide
+ for filter in Filter.filters[key]
+ continue if (
+ (filter.boards and !filter.boards[post.boardID]) or
+ (filter.excludes and filter.excludes[post.boardID]) or
+ filter.op is (if post.isReply then 'only' else 'no') or
+ (if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value))
+ )
+ if filter.hide
if hideable
hide = true
- stub and= result.stub
+ stub and= filter.stub
else
- unless hl and result.class in hl
- (hl or= []).push result.class
- top or= result.top
- if result.noti
+ unless hl and filter.hl in hl
+ (hl or= []).push filter.hl
+ top or= filter.top
+ if filter.noti
noti = true
if hide
{hide, stub}
From 725d7d458ece25e0aafc64cfadb5831173273c0d Mon Sep 17 00:00:00 2001
From: ccd0
Date: Wed, 20 Mar 2019 18:30:05 -0700
Subject: [PATCH 022/131] Use threads.json in thread watcher to reduce number
of thread JSON checks.
---
src/Monitoring/ThreadWatcher.coffee | 78 +++++++++++++++++++----------
1 file changed, 51 insertions(+), 27 deletions(-)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index b5658e696..fab969abe 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -170,11 +170,26 @@ ThreadWatcher =
requests: []
fetched: 0
- clearRequests: ->
- ThreadWatcher.requests = []
- ThreadWatcher.fetched = 0
- ThreadWatcher.status.textContent = ''
- $.rmClass ThreadWatcher.refreshButton, 'fa-spin'
+ fetch: (url, {siteID, force}, args, cb) ->
+ if ThreadWatcher.requests.length is 0
+ ThreadWatcher.status.textContent = '...'
+ $.addClass ThreadWatcher.refreshButton, 'fa-spin'
+ ajax = if (siteID is Site.hostname) then $.ajax else CrossOrigin.ajax
+ req = ajax url,
+ onloadend: ->
+ ThreadWatcher.fetched++
+ if ThreadWatcher.fetched is ThreadWatcher.requests.length
+ ThreadWatcher.requests = []
+ ThreadWatcher.fetched = 0
+ ThreadWatcher.status.textContent = ''
+ $.rmClass ThreadWatcher.refreshButton, 'fa-spin'
+ else
+ ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
+ cb.apply @, args
+ timeout: $.MINUTE
+ ,
+ whenModified: if force then false else 'ThreadWatcher'
+ ThreadWatcher.requests.push req
abort: ->
for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
@@ -204,11 +219,34 @@ ThreadWatcher =
for db in dbs
db.forceSync ->
if (++n) is dbs.length
- threads = ThreadWatcher.getAll()
- for thread in threads
- ThreadWatcher.fetchStatus thread
+ boards = ThreadWatcher.getAll(true)
+ for board in boards
+ ThreadWatcher.fetchBoard board
return
+ fetchBoard: (board) ->
+ return unless board.some (thread) -> !thread.data.isDead
+ {siteID, boardID} = board[0]
+ software = Conf['siteProperties'][siteID]?.software
+ url = SW[software]?.urls.threadsListJSON?({siteID, boardID})
+ return unless url
+ ThreadWatcher.fetch url, {siteID}, [board], ThreadWatcher.parseBoard
+
+ parseBoard: (board) ->
+ return unless @status is 200
+ modified = {}
+ try
+ for page in @response
+ for item in page.threads
+ modified[item.no] = item.last_modified
+ for thread in board
+ {siteID, boardID, threadID} = thread
+ if modified[threadID]
+ continue if thread.data.modified is modified[threadID]
+ ThreadWatcher.db.extend {siteID, boardID, threadID, val: {modified: modified[threadID]}}
+ ThreadWatcher.fetchStatus thread
+ return
+
fetchStatus: (thread, force) ->
{siteID, boardID, threadID, data} = thread
software = Conf['siteProperties'][siteID]?.software
@@ -216,25 +254,9 @@ ThreadWatcher =
return unless url
return if data.isDead and not force
return if data.last is -1 # 404 or no JSON API
- if ThreadWatcher.requests.length is 0
- ThreadWatcher.status.textContent = '...'
- $.addClass ThreadWatcher.refreshButton, 'fa-spin'
- ajax = if (siteID is Site.hostname) then $.ajax else CrossOrigin.ajax
- req = ajax url,
- onloadend: ->
- ThreadWatcher.parseStatus.call @, thread
- timeout: $.MINUTE
- ,
- whenModified: if force then false else 'ThreadWatcher'
- ThreadWatcher.requests.push req
+ ThreadWatcher.fetch url, {siteID, force}, [thread], ThreadWatcher.parseStatus
parseStatus: ({siteID, boardID, threadID, data}) ->
- ThreadWatcher.fetched++
- if ThreadWatcher.fetched is ThreadWatcher.requests.length
- ThreadWatcher.clearRequests()
- else
- ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
-
software = Conf['siteProperties'][siteID]?.software
if @status is 200 and @response
@@ -293,14 +315,16 @@ ThreadWatcher =
ThreadWatcher.refresh()
- getAll: ->
+ getAll: (groupByBoard) ->
all = []
for siteID, boards of ThreadWatcher.db.data
for boardID, threads of boards.boards
if Conf['Current Board'] and (siteID isnt Site.hostname or boardID isnt g.BOARD.ID)
continue
+ if groupByBoard
+ all.push (cont = [])
for threadID, data of threads when data and typeof data is 'object'
- all.push {siteID, boardID, threadID, data}
+ (if groupByBoard then cont else all).push {siteID, boardID, threadID, data}
all
makeLine: (siteID, boardID, threadID, data) ->
From e98fed9e5f9ec0a48e534840a542a340646d0933 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Wed, 20 Mar 2019 18:30:45 -0700
Subject: [PATCH 023/131] Add catalog JSON URL.
---
src/site/SW.tinyboard.coffee | 3 +++
src/site/SW.yotsuba.coffee | 1 +
2 files changed, 4 insertions(+)
diff --git a/src/site/SW.tinyboard.coffee b/src/site/SW.tinyboard.coffee
index 412724b39..6e9f6fecc 100644
--- a/src/site/SW.tinyboard.coffee
+++ b/src/site/SW.tinyboard.coffee
@@ -63,6 +63,9 @@ SW.tinyboard =
threadsListJSON: ({siteID, boardID}) ->
root = Conf['siteProperties'][siteID]?.root
if root then "#{root}#{boardID}/threads.json" else ''
+ catalogJSON: ({siteID, boardID}) ->
+ root = Conf['siteProperties'][siteID]?.root
+ if root then "#{root}#{boardID}/catalog.json" else ''
selectors:
board: 'form[name="postcontrols"]'
diff --git a/src/site/SW.yotsuba.coffee b/src/site/SW.yotsuba.coffee
index b1c3a0d15..2aa84bba3 100644
--- a/src/site/SW.yotsuba.coffee
+++ b/src/site/SW.yotsuba.coffee
@@ -6,6 +6,7 @@ SW.yotsuba =
threadJSON: ({boardID, threadID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/thread/#{threadID}.json"
threadsListJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/threads.json"
archiveListJSON: ({boardID}) -> if BoardConfig.isArchived(boardID) then "#{location.protocol}//a.4cdn.org/#{boardID}/archive.json" else ''
+ catalogJSON: ({boardID}) -> "#{location.protocol}//a.4cdn.org/#{boardID}/catalog.json"
selectors:
board: '.board'
From 6ea1d4ca131053af8926be87d8df699e70ee0002 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Wed, 20 Mar 2019 19:08:46 -0700
Subject: [PATCH 024/131] Offer cross-origin abort in userscripts if available.
---
src/Monitoring/ThreadWatcher.coffee | 3 ++-
src/platform/CrossOrigin.coffee | 3 ++-
2 files changed, 4 insertions(+), 2 deletions(-)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index fab969abe..227d9368b 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -177,6 +177,7 @@ ThreadWatcher =
ajax = if (siteID is Site.hostname) then $.ajax else CrossOrigin.ajax
req = ajax url,
onloadend: ->
+ @finished = true
ThreadWatcher.fetched++
if ThreadWatcher.fetched is ThreadWatcher.requests.length
ThreadWatcher.requests = []
@@ -192,7 +193,7 @@ ThreadWatcher =
ThreadWatcher.requests.push req
abort: ->
- for req in ThreadWatcher.requests when req.readyState isnt 4 # DONE
+ for req in ThreadWatcher.requests when !req.finished
req.abort?()
return
diff --git a/src/platform/CrossOrigin.coffee b/src/platform/CrossOrigin.coffee
index 0998970ad..b424b2517 100644
--- a/src/platform/CrossOrigin.coffee
+++ b/src/platform/CrossOrigin.coffee
@@ -87,7 +87,7 @@ CrossOrigin =
response: null
<% if (type === 'userscript') { %>
- (GM?.xmlHttpRequest or GM_xmlhttpRequest)
+ gmReq = (GM?.xmlHttpRequest or GM_xmlhttpRequest)
method: "GET"
url: url+''
timeout: timeout
@@ -99,6 +99,7 @@ CrossOrigin =
onerror: -> onloadend.call(req)
onabort: -> onloadend.call(req)
ontimeout: -> onloadend.call(req)
+ req.abort = gmReq.abort
<% } %>
<% if (type === 'crx') { %>
From b102e9561389fb7e278c5008125b15844ff73fd0 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Wed, 20 Mar 2019 19:41:34 -0700
Subject: [PATCH 025/131] Restore fake abort in thread watcher if real abort
not available or doesn't go through; needed to retry.
---
src/Monitoring/ThreadWatcher.coffee | 33 +++++++++++++++++------------
1 file changed, 20 insertions(+), 13 deletions(-)
diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee
index 227d9368b..513eaffcf 100644
--- a/src/Monitoring/ThreadWatcher.coffee
+++ b/src/Monitoring/ThreadWatcher.coffee
@@ -175,27 +175,34 @@ ThreadWatcher =
ThreadWatcher.status.textContent = '...'
$.addClass ThreadWatcher.refreshButton, 'fa-spin'
ajax = if (siteID is Site.hostname) then $.ajax else CrossOrigin.ajax
+ onloadend = ->
+ return if @finished
+ @finished = true
+ ThreadWatcher.fetched++
+ if ThreadWatcher.fetched is ThreadWatcher.requests.length
+ ThreadWatcher.clearRequests()
+ else
+ ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
+ cb.apply @, args
req = ajax url,
- onloadend: ->
- @finished = true
- ThreadWatcher.fetched++
- if ThreadWatcher.fetched is ThreadWatcher.requests.length
- ThreadWatcher.requests = []
- ThreadWatcher.fetched = 0
- ThreadWatcher.status.textContent = ''
- $.rmClass ThreadWatcher.refreshButton, 'fa-spin'
- else
- ThreadWatcher.status.textContent = "#{Math.round(ThreadWatcher.fetched / ThreadWatcher.requests.length * 100)}%"
- cb.apply @, args
+ onloadend: onloadend
timeout: $.MINUTE
,
whenModified: if force then false else 'ThreadWatcher'
ThreadWatcher.requests.push req
+ clearRequests: ->
+ ThreadWatcher.requests = []
+ ThreadWatcher.fetched = 0
+ ThreadWatcher.status.textContent = ''
+ $.rmClass ThreadWatcher.refreshButton, 'fa-spin'
+
abort: ->
for req in ThreadWatcher.requests when !req.finished
- req.abort?()
- return
+ req.finished = true
+ try
+ req.abort?()
+ ThreadWatcher.clearRequests()
fetchAuto: ->
clearTimeout ThreadWatcher.timeout
From 50ecb097b7e52c2dc06595ec2f728dc5896f7c47 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Thu, 21 Mar 2019 05:07:28 -0700
Subject: [PATCH 026/131] Make it possible to filter absent ID. #1578
---
src/Filtering/Filter.coffee | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index e280f8d24..232da3cf5 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -14,7 +14,7 @@ Filter =
for line in Conf[key].split '\n'
continue if line[0] is '#'
- if not (regexp = line.match /\/(.+)\/(\w*)/)
+ if not (regexp = line.match /\/(.*)\/(\w*)/)
continue
# Don't mix up filter flags with the regular expression.
@@ -161,7 +161,7 @@ Filter =
postID: (post) -> "#{post.ID}"
name: (post) -> post.info.name
- uniqueID: (post) -> post.info.uniqueID
+ uniqueID: (post) -> post.info.uniqueID or ''
tripcode: (post) -> post.info.tripcode
capcode: (post) -> post.info.capcode
pass: (post) -> post.info.pass
From 6dd3150d2bfd6303460b6ad4be3e01a2809d41c9 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Thu, 21 Mar 2019 05:09:03 -0700
Subject: [PATCH 027/131] Document that Unique ID filtering uses exact string
matching.
---
src/General/Settings/Filter-guide.html | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/General/Settings/Filter-guide.html b/src/General/Settings/Filter-guide.html
index 911a9051f..5dec5f533 100644
--- a/src/General/Settings/Filter-guide.html
+++ b/src/General/Settings/Filter-guide.html
@@ -3,7 +3,7 @@
Use regular expressions, one per line.
Lines starting with a # will be ignored.
For example, /weeaboo/i will filter posts containing the string `weeaboo`, case-insensitive.
- MD5 filtering uses exact string matching, not regular expressions.
+ MD5 and Unique ID filtering use exact string matching, not regular expressions.
You can use these settings with each regular expression, separate them with semicolons:
-
From f823b73a40da41d0ff400e7d769db5c1d9c81ce6 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 22 Mar 2019 02:26:57 -0700
Subject: [PATCH 028/131] Add option to filter only posts with files.
---
src/Filtering/Filter.coffee | 16 +++++++++++-----
src/General/Settings/Filter-guide.html | 8 ++++++--
2 files changed, 17 insertions(+), 7 deletions(-)
diff --git a/src/Filtering/Filter.coffee b/src/Filtering/Filter.coffee
index 232da3cf5..a0c6a6193 100644
--- a/src/Filtering/Filter.coffee
+++ b/src/Filtering/Filter.coffee
@@ -59,9 +59,13 @@ Filter =
], 60
continue
- # Filter OPs along with their threads, replies only, or both.
- # Defaults to both.
- op = filter.match(/[^t]op:(yes|no|only)/)?[1] or 'yes'
+ # Filter OPs along with their threads or replies only.
+ op = filter.match(/[^t]op:(no|only)/)?[1] or ''
+ mask = {'no': 1, 'only': 2}[op] or 0
+
+ # Filter only posts with/without files.
+ file = filter.match(/file:(no|only)/)?[1] or ''
+ mask = mask | ({'no': 4, 'only': 8}[file] or 0)
# Overrule the `Show Stubs` setting.
# Defaults to stub showing.
@@ -96,7 +100,7 @@ Filter =
# Hide the post (default case).
hide = !(hl or noti)
- filter = {isstring, regexp, boards, excludes, op, hide, stub, hl, top, noti}
+ filter = {isstring, regexp, boards, excludes, mask, hide, stub, hl, top, noti}
if key is 'general'
for type in types
(@filters[type] or= []).push filter
@@ -117,13 +121,15 @@ Filter =
noti = false
if QuoteYou.isYou(post)
hideable = false
+ mask = (if post.isReply then 2 else 1)
+ mask = (mask | (if post.file then 4 else 8))
for key of Filter.filters when ((value = Filter[key] post)?)
# Continue if there's nothing to filter (no tripcode for example).
for filter in Filter.filters[key]
continue if (
(filter.boards and !filter.boards[post.boardID]) or
(filter.excludes and filter.excludes[post.boardID]) or
- filter.op is (if post.isReply then 'only' else 'no') or
+ (filter.mask & mask) or
(if filter.isstring then (filter.regexp isnt value) else !filter.regexp.test(value))
)
if filter.hide
diff --git a/src/General/Settings/Filter-guide.html b/src/General/Settings/Filter-guide.html
index 5dec5f533..2e47c6b31 100644
--- a/src/General/Settings/Filter-guide.html
+++ b/src/General/Settings/Filter-guide.html
@@ -15,8 +15,12 @@
For example:
exclude:vg,v;.
-
- Filter OPs only along with their threads (`only`), replies only (`no`), or both (`yes`, this is default).
- For example: op:only;, op:no; or op:yes;.
+ Filter OPs only along with their threads (`only`) or replies only (`no`).
+ For example: op:only; or op:no;.
+
+ -
+ Filter only posts with files (`only`) or only posts without files (`no`).
+ For example: file:only; or file:no;.
-
Overrule the `Show Stubs` setting if specified: create a stub (`yes`) or not (`no`).
From 95841af60bc90e16230ae1e158290c8721027731 Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 22 Mar 2019 02:53:14 -0700
Subject: [PATCH 029/131] Add message alerting Chrome extension users to
disable chrome://flags/#network-service
---
src/platform/$.coffee | 6 ++++++
1 file changed, 6 insertions(+)
diff --git a/src/platform/$.coffee b/src/platform/$.coffee
index 78697a9db..d7116472f 100644
--- a/src/platform/$.coffee
+++ b/src/platform/$.coffee
@@ -73,6 +73,12 @@ $.ajax = do ->
$.extend r.upload, upCallbacks
# connection error or content blocker
$.on r, 'error', -> (c.error "4chan X failed to load: #{url}" unless r.status)
+ <% if (type === 'crx') { %>
+ $.on r, 'load', ->
+ return unless r.readyState is 4 and r.status is 200 and r.statusText is '' and r.response is null and !$.ajaxWarningShown
+ new Notice 'warning', "Error loading #{url}; try going to chrome://flags/#network-service and disabling the network service flag."
+ $.ajaxWarningShown = true
+ <% } %>
r.send form
catch err
# XXX Some content blockers in Firefox (e.g. Adblock Plus and NoScript) throw an exception instead of simulating a connection error.
From 01b078545eeecae0b870ea085838782b3deae0bd Mon Sep 17 00:00:00 2001
From: ccd0
Date: Fri, 22 Mar 2019 03:01:56 -0700
Subject: [PATCH 030/131] Release 4chan X v1.14.5.14.
---
CHANGELOG.md | 4 ++++
builds/4chan-X-beta.crx | Bin 304029 -> 304161 bytes
builds/4chan-X-beta.meta.js | 2 +-
builds/4chan-X-beta.user.js | 7 ++++---
builds/4chan-X-noupdate.crx | Bin 303971 -> 304102 bytes
builds/4chan-X-noupdate.user.js | 7 ++++---
builds/4chan-X.crx | Bin 304025 -> 304157 bytes
builds/4chan-X.meta.js | 2 +-
builds/4chan-X.user.js | 7 ++++---
builds/4chan-X.zip | Bin 303405 -> 303536 bytes
builds/updates-beta.json | 2 +-
builds/updates-beta.xml | 2 +-
builds/updates.json | 2 +-
builds/updates.xml | 2 +-
version.json | 4 ++--
15 files changed, 24 insertions(+), 17 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 407809490..e842d860f 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -4,6 +4,10 @@
### v1.14.5
+**v1.14.5.14** *(2019-03-22)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.14/builds/4chan-X-noupdate.crx)]
+- Add message alerting Chrome extension users to disable chrome://flags/#network-service
+- Minor bugfix in catalog/index loading.
+
**v1.14.5.13** *(2019-03-08)* - [[Userscript](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.user.js)] [[Chrome extension](https://raw.githubusercontent.com/ccd0/4chan-x/1.14.5.13/builds/4chan-X-noupdate.crx)]
- Fix bugs related to additional permissions requests. #2230
- Revert changes in thread watcher that caused performance decrease.
diff --git a/builds/4chan-X-beta.crx b/builds/4chan-X-beta.crx
index 55390a6e8f9108829712d5b80a2b41e4d76729ce..c07e86681d5cb95f241fc3c471d66d682dda9d69 100644
GIT binary patch
delta 274426
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