Fix rare race conditions with thread/reply hiding, unread, your posts. #968

Added DataBoards for simplifications and to reduce asyncings.
Made it possible to export/import those.
Made it possible for (You) to work with cross-board quotelinks.
Replaced Misc.clearThreads.

This is more v3-like.
This commit is contained in:
Nicolas Stepien 2013-04-01 01:42:57 +02:00
parent 435c411a36
commit 203bbf091d
5 changed files with 247 additions and 198 deletions

View File

@ -17,6 +17,7 @@ module.exports = function(grunt) {
'src/features.coffee', 'src/features.coffee',
'src/qr.coffee', 'src/qr.coffee',
'src/report.coffee', 'src/report.coffee',
'src/databoard.coffee',
'src/main.coffee' 'src/main.coffee'
], ],
dest: 'tmp/script.coffee' dest: 'tmp/script.coffee'

80
src/databoard.coffee Normal file
View File

@ -0,0 +1,80 @@
DataBoards = ['hiddenThreads', 'hiddenPosts', 'lastReadPosts', 'yourPosts']
class DataBoard
constructor: (@key, sync) ->
@data = Conf[key]
$.sync key, @onSync.bind @
@clean()
return unless sync
# Chrome also fires the onChanged callback on the current tab,
# so we only start syncing when we're ready.
$.on d, '4chanXInitFinished', => @sync = sync
delete: ({boardID, threadID, postID}) ->
if postID
delete @data.boards[boardID][threadID][postID]
@deleteIfEmpty {boardID, threadID}
else if threadID
delete @data.boards[boardID][threadID]
@deleteIfEmpty {boardID}
else
delete @data.boards[boardID]
$.set @key, @data
deleteIfEmpty: ({boardID, threadID}) ->
if threadID
unless Object.keys(@data.boards[boardID][threadID]).length
delete @data.boards[boardID][threadID]
@deleteIfEmpty {boardID}
else unless Object.keys(@data.boards[boardID]).length
delete @data.boards[boardID]
set: ({boardID, threadID, postID, val}) ->
if postID
((@data.boards[boardID] or= {})[threadID] or= {})[postID] = val
else if threadID
(@data.boards[boardID] or= {})[threadID] = val
else
@data.boards[boardID] = val
$.set @key, @data
get: ({boardID, threadID, postID, defaultValue}) ->
if board = @data.boards[boardID]
unless threadID
if postID
for ID, thread in board
if postID of thread
val = thread[postID]
break
else
val = board
else if thread = board[threadID]
val = if postID
thread[postID]
else
thread
val or defaultValue
clean: ->
for boardID of @data.boards
@deleteIfEmpty {boardID}
now = Date.now()
if @data.lastChecked < now - 12 * $.HOUR
@data.lastChecked = now
for boardID of @data.boards
@ajaxClean boardID
$.set @key, @data
ajaxClean: (boardID) ->
$.ajax "//api.4chan.org/#{boardID}/threads.json", onload: (e) =>
board = @data.boards[boardID]
threads = {}
for page in JSON.parse e.target.response
for thread in page.threads
if thread.no of board
threads[thread.no] = board[thread.no]
@data.boards[boardID] = threads
@deleteIfEmpty {boardID}
$.set @key, @data
onSync: (data) ->
@data = data
@sync?()

View File

@ -347,18 +347,20 @@ Settings =
innerHTML: "<button></button><span class=description>: Clear manually hidden threads and posts on /#{g.BOARD}/." innerHTML: "<button></button><span class=description>: Clear manually hidden threads and posts on /#{g.BOARD}/."
button = $ 'button', div button = $ 'button', div
hiddenNum = 0 hiddenNum = 0
ThreadHiding.getHiddenThreads (hiddenThreads) -> $.get 'hiddenThreads', boards: {}, (item) ->
for ID, thread of hiddenThreads.threads for ID, board of item.hiddenThreads.boards
hiddenNum++ for ID, thread of board
button.textContent = "Hidden: #{hiddenNum}"
ReplyHiding.getHiddenPosts (hiddenPosts) ->
for ID, thread of hiddenPosts.threads
for ID, post of thread
hiddenNum++ hiddenNum++
button.textContent = "Hidden: #{hiddenNum}" button.textContent = "Hidden: #{hiddenNum}"
$.get 'hiddenPosts', boards: {}, (item) ->
for ID, board of item.hiddenPosts.boards
for ID, thread of board
for ID, post of thread
hiddenNum++
button.textContent = "Hidden: #{hiddenNum}"
$.on button, 'click', -> $.on button, 'click', ->
@textContent = 'Hidden: 0' @textContent = 'Hidden: 0'
$.delete ["hiddenThreads.#{g.BOARD}", "hiddenPosts.#{g.BOARD}"] $.delete ['hiddenThreads', 'hiddenPosts']
$.after $('input[name="Stubs"]', section).parentNode.parentNode, div $.after $('input[name="Stubs"]', section).parentNode.parentNode, div
export: (now, data) -> export: (now, data) ->
unless typeof now is 'number' unless typeof now is 'number'
@ -367,9 +369,14 @@ Settings =
version: g.VERSION version: g.VERSION
date: now date: now
Conf: Conf Conf: Conf
$.get 'WatchedThreads', {}, (item) -> items = WatchedThreads: {}
data.WatchedThreads = item.WatchedThreads for db in DataBoards
items[db] = boards: {}
$.get items (items) ->
for key, val in items
data[key] = val
Settings.export now, data Settings.export now, data
return
a = $.el 'a', a = $.el 'a',
className: 'warning' className: 'warning'
textContent: 'Save me!' textContent: 'Save me!'
@ -472,6 +479,8 @@ Settings =
"Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}" "Shift+#{s[0...-1]}#{s[-1..].toLowerCase()}"
for key, val of data.Conf for key, val of data.Conf
$.set key, val $.set key, val
for db in DataBoards
$.set db, data[db]
$.set 'WatchedThreads', data.WatchedThreads $.set 'WatchedThreads', data.WatchedThreads
convertSettings: (data, map) -> convertSettings: (data, map) ->
for prevKey, newKey of map for prevKey, newKey of map
@ -1060,43 +1069,38 @@ ThreadHiding =
init: -> init: ->
return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link'] return if g.VIEW isnt 'index' or !Conf['Thread Hiding'] and !Conf['Thread Hiding Link']
Misc.clearThreads "hiddenThreads.#{g.BOARD}" @db = new DataBoard 'hiddenThreads'
@syncFromCatalog() @syncFromCatalog()
Thread::callbacks.push Thread::callbacks.push
name: 'Thread Hiding' name: 'Thread Hiding'
cb: @node cb: @node
node: -> node: ->
if data = ThreadHiding.hiddenThreads.threads[@] if data = ThreadHiding.db.get {boardID: @board.ID, threadID: @ID}
ThreadHiding.hide @, data.makeStub ThreadHiding.hide @, data.makeStub
return unless Conf['Thread Hiding'] return unless Conf['Thread Hiding']
$.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide' $.prepend @OP.nodes.root, ThreadHiding.makeButton @, 'hide'
getHiddenThreads: (cb) ->
$.get "hiddenThreads.#{g.BOARD}", threads: {}, (item) ->
ThreadHiding.hiddenThreads = item["hiddenThreads.#{g.BOARD}"]
cb ThreadHiding.hiddenThreads if cb
syncFromCatalog: -> syncFromCatalog: ->
# Sync hidden threads from the catalog into the index. # Sync hidden threads from the catalog into the index.
ThreadHiding.getHiddenThreads (hiddenThreads) -> hiddenThreads = ThreadHiding.db.get
{threads} = hiddenThreads boardID: g.BOARD.ID
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} defaultValue: {}
hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
# Add threads that were hidden in the catalog. # Add threads that were hidden in the catalog.
for threadID of hiddenThreadsOnCatalog for threadID of hiddenThreadsOnCatalog
continue if threadID of threads unless threadID of hiddenThreads
threads[threadID] = {} hiddenThreads[threadID] = {}
# Remove threads that were un-hidden in the catalog. # Remove threads that were un-hidden in the catalog.
for threadID of threads for threadID of hiddenThreads
continue if threadID of threads unless threadID of hiddenThreadsOnCatalog
delete threads[threadID] delete hiddenThreads[threadID]
if Object.keys(threads).length ThreadHiding.db.set
$.set "hiddenThreads.#{g.BOARD}", ThreadHiding.hiddenThreads boardID: g.BOARD.ID
else val: hiddenThreads
$.delete "hiddenThreads.#{g.BOARD}"
menu: menu:
init: -> init: ->
@ -1141,17 +1145,19 @@ ThreadHiding =
a a
saveHiddenState: (thread, makeStub) -> saveHiddenState: (thread, makeStub) ->
# Get fresh hidden threads. hiddenThreadsOnCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {}
ThreadHiding.getHiddenThreads (hiddenThreads) -> if thread.isHidden
hiddenThreadsCatalog = JSON.parse(localStorage.getItem "4chan-hide-t-#{g.BOARD}") or {} ThreadHiding.db.set
if thread.isHidden boardID: thread.board.ID
hiddenThreads.threads[thread] = {makeStub} threadID: thread.ID
hiddenThreadsCatalog[thread] = true val: {makeStub}
else hiddenThreadsOnCatalog[thread] = true
delete hiddenThreads.threads[thread] else
delete hiddenThreadsCatalog[thread] ThreadHiding.db.delete
$.set "hiddenThreads.#{g.BOARD}", hiddenThreads boardID: thread.board.ID
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsCatalog threadID: thread.ID
delete hiddenThreadsOnCatalog[thread]
localStorage.setItem "4chan-hide-t-#{g.BOARD}", JSON.stringify hiddenThreadsOnCatalog
toggle: (thread) -> toggle: (thread) ->
unless thread instanceof Thread unless thread instanceof Thread
@ -1204,29 +1210,22 @@ ReplyHiding =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link'] return if g.VIEW is 'catalog' or !Conf['Reply Hiding'] and !Conf['Reply Hiding Link']
Misc.clearThreads "hiddenPosts.#{g.BOARD}" @db = new DataBoard 'hiddenPosts'
@getHiddenPosts()
Post::callbacks.push Post::callbacks.push
name: 'Reply Hiding' name: 'Reply Hiding'
cb: @node cb: @node
node: -> node: ->
return if !@isReply or @isClone return if !@isReply or @isClone
if thread = ReplyHiding.hiddenPosts.threads[@thread] if data = ReplyHiding.db.get {boardID: @board.ID, threadID: @thread.ID, postID: @ID}
if data = thread[@] if data.thisPost
if data.thisPost ReplyHiding.hide @, data.makeStub, data.hideRecursively
ReplyHiding.hide @, data.makeStub, data.hideRecursively else
else Recursive.apply ReplyHiding.hide, @, data.makeStub, true
Recursive.apply ReplyHiding.hide, @, data.makeStub, true Recursive.add ReplyHiding.hide, @, data.makeStub, true
Recursive.add ReplyHiding.hide, @, data.makeStub, true
return unless Conf['Reply Hiding'] return unless Conf['Reply Hiding']
$.replace $('.sideArrows', @nodes.root), ReplyHiding.makeButton @, 'hide' $.replace $('.sideArrows', @nodes.root), ReplyHiding.makeButton @, 'hide'
getHiddenPosts: (cb) ->
$.get "hiddenPosts.#{g.BOARD}", threads: {}, (item) ->
ReplyHiding.hiddenPosts = item["hiddenPosts.#{g.BOARD}"]
cb ReplyHiding.hiddenPosts if cb
menu: menu:
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link'] return if g.VIEW is 'catalog' or !Conf['Menu'] or !Conf['Reply Hiding Link']
@ -1279,10 +1278,9 @@ ReplyHiding =
el: div el: div
order: 20 order: 20
open: (post) -> open: (post) ->
if !post.isReply or post.isClone if !post.isReply or post.isClone or !post.isHidden
return false return false
thread = ReplyHiding.hiddenPosts.threads[post.thread] unless data = ReplyHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
unless post.isHidden or data = thread?[post]
return false return false
ReplyHiding.menu.post = post ReplyHiding.menu.post = post
thisPost.firstChild.checked = post.isHidden thisPost.firstChild.checked = post.isHidden
@ -1309,8 +1307,6 @@ ReplyHiding =
thisPost = $('input[name=thisPost]', parent).checked thisPost = $('input[name=thisPost]', parent).checked
replies = $('input[name=replies]', parent).checked replies = $('input[name=replies]', parent).checked
{post} = ReplyHiding.menu {post} = ReplyHiding.menu
thread = ReplyHiding.hiddenPosts.threads[post.thread]
data = thread?[post]
if thisPost if thisPost
ReplyHiding.show post, replies ReplyHiding.show post, replies
else if replies else if replies
@ -1318,7 +1314,7 @@ ReplyHiding =
Recursive.rm ReplyHiding.hide, post, true Recursive.rm ReplyHiding.hide, post, true
else else
return return
if data if data = ReplyHiding.db.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
ReplyHiding.saveHiddenState post, !(thisPost and replies), !thisPost, data.makeStub, !replies ReplyHiding.saveHiddenState post, !(thisPost and replies), !thisPost, data.makeStub, !replies
$.event 'CloseMenu' $.event 'CloseMenu'
@ -1331,21 +1327,18 @@ ReplyHiding =
a a
saveHiddenState: (post, isHiding, thisPost, makeStub, hideRecursively) -> saveHiddenState: (post, isHiding, thisPost, makeStub, hideRecursively) ->
# Get fresh hidden posts. data =
ReplyHiding.getHiddenPosts (hiddenPosts) -> boardID: post.board.ID
if isHiding threadID: post.thread.ID
unless thread = hiddenPosts.threads[post.thread] postID: post.ID
thread = hiddenPosts.threads[post.thread] = {} if isHiding
thread[post] = data.val =
thisPost: thisPost isnt false # undefined -> true thisPost: thisPost isnt false # undefined -> true
makeStub: makeStub makeStub: makeStub
hideRecursively: hideRecursively hideRecursively: hideRecursively
else ReplyHiding.db.set data
thread = hiddenPosts.threads[post.thread] else
delete thread[post] ReplyHiding.db.delete data
unless Object.keys(thread).length
delete hiddenPosts.threads[post.thread]
$.set "hiddenPosts.#{g.BOARD}", hiddenPosts
toggle: -> toggle: ->
post = Get.postFromNode @ post = Get.postFromNode @
@ -1448,8 +1441,8 @@ QuoteStrikeThrough =
node: -> node: ->
return if @isClone return if @isClone
for quotelink in @nodes.quotelinks for quotelink in @nodes.quotelinks
{board, postID} = Get.postDataFromLink quotelink {boardID, postID} = Get.postDataFromLink quotelink
if g.posts["#{board}.#{postID}"]?.isHidden if g.posts["#{boardID}.#{postID}"]?.isHidden
$.addClass quotelink, 'filtered' $.addClass quotelink, 'filtered'
return return
@ -1597,7 +1590,7 @@ DeleteLink =
cooldown: cooldown:
start: (post, node) -> start: (post, node) ->
unless (thread = QR.yourPosts?.threads?[post.thread]) and post.ID in thread unless QR.db?.get {boardID: post.board.ID, threadID: post.thread.ID, postID: post.ID}
# Only start counting on our posts. # Only start counting on our posts.
delete DeleteLink.cooldown.counting delete DeleteLink.cooldown.counting
return return
@ -2304,15 +2297,15 @@ Get =
postDataFromLink: (link) -> postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org' if link.hostname is 'boards.4chan.org'
path = link.pathname.split '/' path = link.pathname.split '/'
board = path[1] boardID = path[1]
threadID = path[3] threadID = path[3]
postID = link.hash[2..] postID = link.hash[2..]
else # resurrected quote else # resurrected quote
board = link.dataset.board boardID = link.dataset.board
threadID = link.dataset.threadid or 0 threadID = link.dataset.threadid or 0
postID = link.dataset.postid postID = link.dataset.postid
return { return {
board: board boardID: boardID
threadID: +threadID threadID: +threadID
postID: +postID postID: +postID
} }
@ -2340,8 +2333,8 @@ Get =
# Third: # Third:
# Filter out irrelevant quotelinks. # Filter out irrelevant quotelinks.
quotelinks.filter (quotelink) -> quotelinks.filter (quotelink) ->
{board, postID} = Get.postDataFromLink quotelink {boardID, postID} = Get.postDataFromLink quotelink
board is post.board.ID and postID is post.ID boardID is post.board.ID and postID is post.ID
postClone: (board, threadID, postID, root, context) -> postClone: (board, threadID, postID, root, context) ->
if post = g.posts["#{board}.#{postID}"] if post = g.posts["#{board}.#{postID}"]
Get.insert post, root, context Get.insert post, root, context
@ -2510,34 +2503,6 @@ Get =
Main.callbackNodes Post, [post] Main.callbackNodes Post, [post]
Get.insert post, root, context Get.insert post, root, context
Misc = # super semantic
clearThreads: (key, data) ->
unless data
$.get key, null, (item) ->
data = item[key]
return unless data
Misc.clearThreads key, data
return
unless Object.keys(data.threads).length
$.delete key
return
return if data.lastChecked > Date.now() - 12 * $.HOUR
$.ajax "//api.4chan.org/#{g.BOARD}/threads.json", onload: ->
threads = {}
for page in JSON.parse @response
for thread in page.threads
if thread.no of data.threads
threads[thread.no] = data.threads[thread.no]
unless Object.keys(threads).length
$.delete key
return
data.threads = threads
data.lastChecked = Date.now()
$.set key, data
Quotify = Quotify =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes'] return if g.VIEW is 'catalog' or !Conf['Resurrect Quotes']
@ -2623,13 +2588,13 @@ QuoteInline =
toggle: (e) -> toggle: (e) ->
return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0 return if e.shiftKey or e.altKey or e.ctrlKey or e.metaKey or e.button isnt 0
e.preventDefault() e.preventDefault()
{board, threadID, postID} = Get.postDataFromLink @ {boardID, threadID, postID} = Get.postDataFromLink @
context = Get.contextFromLink @ context = Get.contextFromLink @
if $.hasClass @, 'inlined' if $.hasClass @, 'inlined'
QuoteInline.rm @, board, threadID, postID, context QuoteInline.rm @, boardID, threadID, postID, context
else else
return if $.x "ancestor::div[@id='p#{postID}']", @ return if $.x "ancestor::div[@id='p#{postID}']", @
QuoteInline.add @, board, threadID, postID, context QuoteInline.add @, boardID, threadID, postID, context
@classList.toggle 'inlined' @classList.toggle 'inlined'
findRoot: (quotelink, isBacklink) -> findRoot: (quotelink, isBacklink) ->
@ -2637,15 +2602,15 @@ QuoteInline =
quotelink.parentNode.parentNode quotelink.parentNode.parentNode
else else
$.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink $.x 'ancestor-or-self::*[parent::blockquote][1]', quotelink
add: (quotelink, board, threadID, postID, context) -> add: (quotelink, boardID, threadID, postID, context) ->
isBacklink = $.hasClass quotelink, 'backlink' isBacklink = $.hasClass quotelink, 'backlink'
inline = $.el 'div', inline = $.el 'div',
id: "i#{postID}" id: "i#{postID}"
className: 'inline' className: 'inline'
$.after QuoteInline.findRoot(quotelink, isBacklink), inline $.after QuoteInline.findRoot(quotelink, isBacklink), inline
Get.postClone board, threadID, postID, inline, context Get.postClone boardID, threadID, postID, inline, context
return unless (post = g.posts["#{board}.#{postID}"]) and return unless (post = g.posts["#{boardID}.#{postID}"]) and
context.thread is post.thread context.thread is post.thread
# Hide forward post if it's a backlink of a post in this thread. # Hide forward post if it's a backlink of a post in this thread.
@ -2659,7 +2624,7 @@ QuoteInline =
Unread.posts.splice i, 1 Unread.posts.splice i, 1
Unread.update() Unread.update()
rm: (quotelink, board, threadID, postID, context) -> rm: (quotelink, boardID, threadID, postID, context) ->
isBacklink = $.hasClass quotelink, 'backlink' isBacklink = $.hasClass quotelink, 'backlink'
# Select the corresponding inlined quote, and remove it. # Select the corresponding inlined quote, and remove it.
root = QuoteInline.findRoot quotelink, isBacklink root = QuoteInline.findRoot quotelink, isBacklink
@ -2670,21 +2635,21 @@ QuoteInline =
return unless el = root.firstElementChild return unless el = root.firstElementChild
# Dereference clone. # Dereference clone.
post = g.posts["#{board}.#{postID}"] post = g.posts["#{boardID}.#{postID}"]
post.rmClone el.dataset.clone post.rmClone el.dataset.clone
# Decrease forward count and unhide. # Decrease forward count and unhide.
if Conf['Forward Hiding'] and if Conf['Forward Hiding'] and
isBacklink and isBacklink and
context.thread is g.threads["#{board}.#{threadID}"] and context.thread is g.threads["#{boardID}.#{threadID}"] and
not --post.forwarded not --post.forwarded
delete post.forwarded delete post.forwarded
$.rmClass post.nodes.root, 'forwarded' $.rmClass post.nodes.root, 'forwarded'
# Repeat. # Repeat.
while inlined = $ '.inlined', el while inlined = $ '.inlined', el
{board, threadID, postID} = Get.postDataFromLink inlined {boardID, threadID, postID} = Get.postDataFromLink inlined
QuoteInline.rm inlined, board, threadID, postID, context QuoteInline.rm inlined, boardID, threadID, postID, context
$.rmClass inlined, 'inlined' $.rmClass inlined, 'inlined'
return return
@ -2704,13 +2669,13 @@ QuotePreview =
mouseover: (e) -> mouseover: (e) ->
return if $.hasClass @, 'inlined' return if $.hasClass @, 'inlined'
{board, threadID, postID} = Get.postDataFromLink @ {boardID, threadID, postID} = Get.postDataFromLink @
qp = $.el 'div', qp = $.el 'div',
id: 'qp' id: 'qp'
className: 'dialog' className: 'dialog'
$.add d.body, qp $.add d.body, qp
Get.postClone board, threadID, postID, qp, Get.contextFromLink @ Get.postClone boardID, threadID, postID, qp, Get.contextFromLink @
UI.hover UI.hover
root: @ root: @
@ -2720,7 +2685,7 @@ QuotePreview =
cb: QuotePreview.mouseout cb: QuotePreview.mouseout
asapTest: -> qp.firstElementChild asapTest: -> qp.firstElementChild
return unless origin = g.posts["#{board}.#{postID}"] return unless origin = g.posts["#{boardID}.#{postID}"]
if Conf['Quote Highlighting'] if Conf['Quote Highlighting']
posts = [origin].concat origin.clones posts = [origin].concat origin.clones
@ -2823,11 +2788,12 @@ QuoteYou =
return if @isClone return if @isClone
# Stop there if there's no quotes in that post. # Stop there if there's no quotes in that post.
return unless (quotes = @quotes).length return unless (quotes = @quotes).length
{db} = QR
return unless db
{quotelinks} = @nodes {quotelinks} = @nodes
for quotelink in quotelinks for quotelink in quotelinks
{threadID, postID} = Get.postDataFromLink quotelink if db.get Get.postDataFromLink quotelink
if (thread = QR.yourPosts.threads[threadID]) and postID in thread
$.add quotelink, $.tn QuoteYou.text $.add quotelink, $.tn QuoteYou.text
return return
@ -2856,8 +2822,8 @@ QuoteOP =
# add (OP) to quotes quoting this context's OP. # add (OP) to quotes quoting this context's OP.
return unless op in quotes return unless op in quotes
for quotelink in quotelinks for quotelink in quotelinks
{board, postID} = Get.postDataFromLink quotelink {boardID, postID} = Get.postDataFromLink quotelink
if "#{board}.#{postID}" is op if "#{boardID}.#{postID}" is op
$.add quotelink, $.tn QuoteOP.text $.add quotelink, $.tn QuoteOP.text
return return
@ -2879,11 +2845,11 @@ QuoteCT =
{board, thread} = if @isClone then @context else @ {board, thread} = if @isClone then @context else @
for quotelink in quotelinks for quotelink in quotelinks
data = Get.postDataFromLink quotelink {boardID, threadID} = Get.postDataFromLink quotelink
continue unless data.threadID # deadlink continue unless threadID # deadlink
if @isClone if @isClone
quotelink.textContent = quotelink.textContent.replace QuoteCT.text, '' quotelink.textContent = quotelink.textContent.replace QuoteCT.text, ''
if data.board is @board.ID and data.threadID isnt thread.ID if boardID is @board.ID and threadID isnt thread.ID
$.add quotelink, $.tn QuoteCT.text $.add quotelink, $.tn QuoteCT.text
return return
@ -3612,13 +3578,11 @@ Unread =
init: -> init: ->
return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Tab Icon'] return if g.VIEW isnt 'thread' or !Conf['Unread Count'] and !Conf['Unread Tab Icon']
Unread.hr = $.el 'hr', @db = new DataBoard 'lastReadPosts', @sync
@hr = $.el 'hr',
id: 'unread-line' id: 'unread-line'
Misc.clearThreads "lastReadPosts.#{g.BOARD}" @posts = []
$.sync "lastReadPosts.#{g.BOARD}", @sync @postsQuotingYou = []
Unread.posts = []
Unread.postsQuotingYou = []
Thread::callbacks.push Thread::callbacks.push
name: 'Unread' name: 'Unread'
@ -3630,23 +3594,29 @@ Unread =
posts = [] posts = []
for ID, post of @posts for ID, post of @posts
posts.push post if post.isReply posts.push post if post.isReply
$.get "lastReadPosts.#{@board}", threads: {}, (item) => Unread.lastReadPost = Unread.db.get
Unread.lastReadPost = item["lastReadPosts.#{@board}"].threads[@] or 0 boardID: @board.ID
Unread.addPosts posts threadID: @ID
if (hash = location.hash.match /\d+/) and post = @posts[hash[0]] defaultValue: 0
post.nodes.root.scrollIntoView() Unread.addPosts posts
else if Unread.posts.length if (hash = location.hash.match /\d+/) and post = @posts[hash[0]]
# Scroll to before the first unread post. post.nodes.root.scrollIntoView()
$.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false else if Unread.posts.length
else if posts.length # Scroll to before the first unread post.
# Scroll to the last read post. $.x('preceding-sibling::div[contains(@class,"postContainer")][1]', Unread.posts[0].nodes.root).scrollIntoView false
posts[posts.length - 1].nodes.root.scrollIntoView() else if posts.length
# Scroll to the last read post.
posts[posts.length - 1].nodes.root.scrollIntoView()
$.on d, 'ThreadUpdate', Unread.onUpdate $.on d, 'ThreadUpdate', Unread.onUpdate
$.on d, 'scroll visibilitychange', Unread.read $.on d, 'scroll visibilitychange', Unread.read
$.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line'] $.on d, 'visibilitychange', Unread.setLine if Conf['Unread Line']
sync: (lastReadPosts) -> sync: ->
return unless (lastReadPost = lastReadPosts?.threads?[Unread.thread]) and Unread.lastReadPost < lastReadPost lastReadPost = Unread.db.get
boardID: Unread.thread.board.ID
threadID: Unread.thread.ID
defaultValue: 0
return unless Unread.lastReadPost < lastReadPost
Unread.lastReadPost = lastReadPost Unread.lastReadPost = lastReadPost
Unread.readArray Unread.posts Unread.readArray Unread.posts
Unread.readArray Unread.postsQuotingYou Unread.readArray Unread.postsQuotingYou
@ -3654,29 +3624,30 @@ Unread =
Unread.update() Unread.update()
addPosts: (newPosts) -> addPosts: (newPosts) ->
if Conf['Quick Reply']
{yourPosts} = QR
youInThisThread = yourPosts.threads[Unread.thread]
for post in newPosts for post in newPosts
{ID} = post {ID} = post
if ID <= Unread.lastReadPost or post.isHidden or youInThisThread and ID in youInThisThread if ID <= Unread.lastReadPost or post.isHidden
continue continue
if QR.db
data =
boardID: post.board.ID
threadID: post.thread.ID
postID: post.ID
continue if QR.db.get data
Unread.posts.push post Unread.posts.push post
Unread.addPostQuotingYou post, yourPosts if yourPosts Unread.addPostQuotingYou post
if Conf['Unread Line'] if Conf['Unread Line']
# Force line on visible threads if there were no unread posts previously. # Force line on visible threads if there were no unread posts previously.
Unread.setLine Unread.posts[0] in newPosts Unread.setLine Unread.posts[0] in newPosts
Unread.read() Unread.read()
Unread.update() Unread.update()
addPostQuotingYou: (post, yourPosts) -> addPostQuotingYou: (post) ->
for quote in post.quotes return unless QR.db
[board, quoteID] = quote.split '.' for quotelink in post.nodes.quotelinks
continue unless board is Unread.thread.board.ID if QR.db.get Get.postDataFromLink quotelink
for thread, postIDs of yourPosts.threads Unread.postsQuotingYou.push post
if +quoteID in postIDs return
Unread.postsQuotingYou.push post
return
onUpdate: (e) -> onUpdate: (e) ->
if e.detail[404] if e.detail[404]
@ -3704,10 +3675,10 @@ Unread =
Unread.update() if e Unread.update() if e
saveLastReadPost: $.debounce 2 * $.SECOND, -> saveLastReadPost: $.debounce 2 * $.SECOND, ->
$.get "lastReadPosts.#{Unread.thread.board}", threads: {}, (item) -> Unread.db.set
lastReadPosts = item["lastReadPosts.#{Unread.thread.board}"] boardID: Unread.thread.board.ID
lastReadPosts.threads[Unread.thread] = Unread.lastReadPost threadID: Unread.thread.ID
$.set "lastReadPosts.#{Unread.thread.board}", lastReadPosts val: Unread.lastReadPost
setLine: (force) -> setLine: (force) ->
return unless d.hidden or force is true return unless d.hidden or force is true

View File

@ -282,22 +282,25 @@ class Clone extends Post
Main = Main =
init: (items) -> init: (items) ->
unless items # flatten Config into Conf
# flatten Config into Conf # and get saved or default values
# and get saved or default values flatten = (parent, obj) ->
flatten = (parent, obj) -> if obj instanceof Array
if obj instanceof Array Conf[parent] = obj[0]
Conf[parent] = obj[0] else if typeof obj is 'object'
else if typeof obj is 'object' for key, val of obj
for key, val of obj flatten key, val
flatten key, val else # string or number
else # string or number Conf[parent] = obj
Conf[parent] = obj
return
flatten null, Config
$.get Conf, Main.init
return return
flatten null, Config
for db in DataBoards
Conf[db] = boards: {}
$.get Conf, Main.initFeatures
$.on d, '4chanMainInit', Main.initStyle
initFeatures: (items) ->
Conf = items Conf = items
pathname = location.pathname.split '/' pathname = location.pathname.split '/'
@ -385,10 +388,10 @@ Main =
# c.timeEnd 'All initializations' # c.timeEnd 'All initializations'
$.on d, 'AddCallback', Main.addCallback $.on d, 'AddCallback', Main.addCallback
$.on d, '4chanMainInit', Main.initStyle
$.ready Main.initReady $.ready Main.initReady
initStyle: -> initStyle: ->
$.off d, '4chanMainInit', Main.initStyle
return unless Main.isThisPageLegit() return unless Main.isThisPageLegit()
# disable the mobile layout # disable the mobile layout
$('link[href*=mobile]', d.head)?.disabled = true $('link[href*=mobile]', d.head)?.disabled = true

View File

@ -2,8 +2,7 @@ QR =
init: -> init: ->
return if g.VIEW is 'catalog' or !Conf['Quick Reply'] return if g.VIEW is 'catalog' or !Conf['Quick Reply']
Misc.clearThreads "yourPosts.#{g.BOARD}" @db = new DataBoard 'yourPosts'
@syncYourPosts()
if Conf['Hide Original Post Form'] if Conf['Hide Original Post Form']
$.addClass doc, 'hide-original-post-form' $.addClass doc, 'hide-original-post-form'
@ -88,14 +87,6 @@ QR =
else else
QR.unhide() QR.unhide()
syncYourPosts: (yourPosts) ->
if yourPosts
QR.yourPosts = yourPosts
return
$.get "yourPosts.#{g.BOARD}", threads: {}, (item) ->
QR.syncYourPosts item["yourPosts.#{g.BOARD}"]
$.sync "yourPosts.#{g.BOARD}", QR.syncYourPosts
error: (err) -> error: (err) ->
QR.open() QR.open()
if typeof err is 'string' if typeof err is 'string'
@ -984,8 +975,11 @@ QR =
threadID = +threadID or postID threadID = +threadID or postID
isReply = threadID isnt postID isReply = threadID isnt postID
(QR.yourPosts.threads[threadID] or= []).push postID QR.db.set
$.set "yourPosts.#{g.BOARD}", QR.yourPosts boardID: g.BOARD.ID
threadID: threadID
postID: postID
val: true
# Post/upload confirmed as successful. # Post/upload confirmed as successful.
$.event 'QRPostSuccessful', { $.event 'QRPostSuccessful', {