Merge branch 'next'

This commit is contained in:
ccd0 2016-11-06 22:17:11 -08:00
commit d17550b802
32 changed files with 229 additions and 157 deletions

View File

@ -66,7 +66,7 @@ Build =
url: if boardID is 'f'
"#{location.protocol}//i.4cdn.org/#{boardID}/#{encodeURIComponent data.filename}#{data.ext}"
else
"#{location.protocol}//i.4cdn.org/#{boardID}/#{data.tim}#{data.ext}"
"#{location.protocol}//#{if data.no % 3 then 'i.4cdn.org' else 'is.4chan.org'}/#{boardID}/#{data.tim}#{data.ext}"
height: data.h
width: data.w
MD5: data.md5
@ -76,6 +76,7 @@ Build =
twidth: data.tn_w
isSpoiler: !!data.spoiler
tag: data.tag
hasDownscale: !!data.m_img
o.file.dimensions = "#{o.file.width}x#{o.file.height}" unless /\.pdf$/.test o.file.url
o
@ -83,8 +84,6 @@ Build =
html = html
.replace(/<br\b[^<]*>/gi, '\n')
.replace(/\n\n<span\b[^<]* class="abbr"[^]*$/i, '') # EXIF data (/p/)
.replace(/^<b\b[^<]*>Rolled [^<]*<\/b>/i, '') # Rolls (/tg/)
.replace(/<span\b[^<]* class="fortune"[^]*$/i, '') # Fortunes (/s4s/)
.replace(/<[^>]*>/g, '')
Build.unescape html
@ -93,6 +92,9 @@ Build =
unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
while (html2 = html.replace /<s>(?:(?!<\/?s>).)*<\/s>/g, '[spoiler]') isnt html
html = html2
html = html
.replace(/^<b\b[^<]*>Rolled [^<]*<\/b>/i, '') # Rolls (/tg/, /qst/)
.replace(/<span\b[^<]* class="fortune"[^]*$/i, '') # Fortunes (/s4s/)
# Remove preceding and following new lines, trailing spaces.
Build.parseComment(html).trim().replace(/\s+$/gm, '')

View File

@ -14,7 +14,7 @@
</a>
(${file.size}, ${file.dimensions || "PDF"})
</div>
<a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${fileURL}" target="_blank">
<a class="fileThumb?{file.isSpoiler}{ imgspoiler}{}" href="${fileURL}" target="_blank"?{file.hasDownscale}{ data-m}>
<img
src="${fileThumb}"
alt="${file.size}"

View File

@ -3,9 +3,9 @@ Get =
{OP} = thread
excerpt = ("/#{thread.board}/ - ") + (
OP.info.subject?.trim() or
OP.info.commentDisplay.replace(/\n+/g, ' // ') or
OP.commentDisplay().replace(/\n+/g, ' // ') or
OP.file?.name or
OP.info.nameBlock)
"No.#{OP}")
return "#{excerpt[...70]}..." if excerpt.length > 73
excerpt
threadFromRoot: (root) ->
@ -18,7 +18,7 @@ Get =
index = root.dataset.clone
if index then post.clones[index] else post
postFromNode: (root) ->
Get.postFromRoot $.x '(ancestor-or-self::div[contains(@class,"postContainer")][1]|following::div[contains(@class,"postContainer")][1])', root
Get.postFromRoot $.x 'ancestor-or-self::div[contains(@class,"postContainer")][1]', root
postDataFromLink: (link) ->
if link.hostname is 'boards.4chan.org'
path = link.pathname.split /\/+/

View File

@ -84,42 +84,39 @@ Header =
$.on window, 'load popstate', Header.hashScroll
$.on d, 'CreateNotification', @createNotification
$.asap (-> d.body), =>
$.onExists doc, 'body', =>
return unless Main.isThisPageLegit()
# Wait for #boardNavMobile instead of #boardNavDesktop,
# it might be incomplete otherwise.
$.asap (-> $.id('boardNavMobile') or d.readyState isnt 'loading'), ->
$.prepend d.body, @bar
$.add d.body, Header.hover
@setBarPosition Conf['Bottom Header']
$.onExists doc, '#boardNavDesktop > *', Header.setBoardList
Main.ready ->
if not (footer = $.id 'boardNavDesktopFoot')
return unless (absbot = $.id 'absbot')
footer = $.id('boardNavDesktop').cloneNode true
footer.id = 'boardNavDesktopFoot'
$('#navtopright', footer).id = 'navbotright'
$('#settingsWindowLink', footer).id = 'settingsWindowLinkBot'
Header.bottomBoardList = $ '.boardList', footer
if a = $ "a[href*='/#{g.BOARD}/']", footer
a.className = 'current'
Main.ready ->
if (oldFooter = $.id 'boardNavDesktopFoot')
$.replace $('.boardList', oldFooter), Header.bottomBoardList
else if (absbot = $.id 'absbot')
$.before absbot, footer
$.globalEval 'window.cloneTopNav = function() {};'
Header.setBoardList()
$.prepend d.body, @bar
$.add d.body, Header.hover
@setBarPosition Conf['Bottom Header']
@
$.before absbot, footer
$.globalEval 'window.cloneTopNav = function() {};'
if (a = $ "a[href*='/#{g.BOARD}/']", footer)
a.className = 'current'
Header.bottomBoardList = $ '.boardList', footer
CatalogLinks.setLinks Header.bottomBoardList
Main.ready =>
if g.VIEW is 'catalog' or !Conf['Disable Native Extension']
cs = $.el 'a', href: 'javascript:;'
if g.VIEW is 'catalog'
cs.title = cs.textContent = 'Catalog Settings'
cs.className = 'fa fa-book'
else
cs.title = cs.textContent = '4chan Settings'
cs.className = 'native-settings'
$.on cs, 'click', () ->
$.id('settingsWindowLink').click()
@addShortcut 'native', cs, 810
if g.VIEW is 'catalog' or !Conf['Disable Native Extension']
cs = $.el 'a', href: 'javascript:;'
if g.VIEW is 'catalog'
cs.title = cs.textContent = 'Catalog Settings'
cs.className = 'fa fa-book'
else
cs.title = cs.textContent = '4chan Settings'
cs.className = 'native-settings'
$.on cs, 'click', () ->
$.id('settingsWindowLink').click()
@addShortcut 'native', cs, 810
@enableDesktopNotifications()
@ -170,7 +167,9 @@ Header =
a = node.cloneNode true
a.className = 'current' if a.pathname.split('/')[1] is g.BOARD.ID
nodes.push a
$.add $('.boardList', boardList), nodes
fullBoardList = $ '.boardList', boardList
$.add fullBoardList, nodes
CatalogLinks.setLinks fullBoardList
$.add Header.bar, [Header.boardList, Header.shortcuts, Header.noticesRoot, Header.toggle]
@ -188,9 +187,8 @@ Header =
as = $$ '#full-board-list a[title]', Header.boardList
re = /[\w@]+(-(all|title|replace|full|index|catalog|archive|expired|(mode|sort|text):"[^"]+"(,"[^"]+")?))*|[^\w@]+/g
nodes = (Header.mapCustomNavigation t, as for t in boardnav.match re)
$.add list, nodes
$.ready CatalogLinks.initBoardList
CatalogLinks.setLinks list
mapCustomNavigation: (t, as) ->
if /^[^\w@]/.test t

View File

@ -95,17 +95,20 @@ Index =
@hideLabel = $ '#hidden-label', @navLinks
$.on $('#hidden-toggle a', @navLinks), 'click', @cb.toggleHiddenThreads
# Drop-down menus
# Drop-down menus and reverse sort toggle
@selectRev = $ '#index-rev', @navLinks
@selectMode = $ '#index-mode', @navLinks
@selectSort = $ '#index-sort', @navLinks
@selectSize = $ '#index-size', @navLinks
$.on @selectRev, 'change', @cb.sort
$.on @selectMode, 'change', @cb.mode
$.on @selectSort, 'change', @cb.sort
$.on @selectSize, 'change', $.cb.value
$.on @selectSize, 'change', @cb.size
for select in [@selectMode, @selectSize]
select.value = Conf[select.name]
@selectSort.value = Index.currentSort
@selectRev.checked = /-rev$/.test Index.currentSort
@selectSort.value = Index.currentSort.replace /-rev$/, ''
# Thread container
@root = $.el 'div', className: 'board json-index'
@ -252,7 +255,8 @@ Index =
Index.pageLoad false
sort: ->
Index.pushState {sort: @value}
value = if Index.selectRev.checked then Index.selectSort.value + "-rev" else Index.selectSort.value
Index.pushState {sort: value}
Index.pageLoad false
resort: (e) ->
@ -357,12 +361,12 @@ Index =
'all-pages': 'all pages'
'catalog': 'catalog'
sort:
'bump-order': 'bump'
'last-reply': 'lastreply'
'last-long-reply': 'lastlong'
'creation-date': 'birth'
'reply-count': 'replycount'
'file-count': 'filecount'
'bump-order': 'bump'
'last-reply': 'lastreply'
'last-long-reply': 'lastlong'
'creation-date': 'birth'
'reply-count': 'replycount'
'file-count': 'filecount'
processHash: ->
# XXX https://bugzilla.mozilla.org/show_bug.cgi?id=483304
@ -377,8 +381,9 @@ Index =
else if command is 'index'
state.mode = Conf['Previous Index Mode']
state.page = 1
else if (sort = Index.hashCommands.sort[command])
else if (sort = Index.hashCommands.sort[command.replace(/-rev$/, '')])
state.sort = sort
state.sort += '-rev' if /-rev$/.test(command)
else if /^s=/.test command
state.search = decodeURIComponent(command[2..]).replace(/\+/g, ' ').trim()
else
@ -461,7 +466,8 @@ Index =
$('#hidden-toggle a', Index.navLinks).textContent = 'Show'
setupSort: ->
Index.selectSort.value = Index.currentSort
Index.selectRev.checked = /-rev$/.test Index.currentSort
Index.selectSort.value = Index.currentSort.replace /-rev$/, ''
getPagesNum: ->
if Index.search
@ -759,7 +765,7 @@ Index =
sort: ->
{liveThreadIDs, liveThreadData} = Index
return unless liveThreadData
Index.sortedThreadIDs = switch Index.currentSort
Index.sortedThreadIDs = switch Index.currentSort.replace(/-rev$/, '')
when 'lastreply'
[liveThreadData...].sort((a, b) ->
a = num[num.length - 1] if (num = a.last_replies)
@ -779,6 +785,8 @@ Index =
when 'replycount' then [liveThreadData...].sort((a, b) -> b.replies - a.replies).map (post) -> post.no
when 'filecount' then [liveThreadData...].sort((a, b) -> b.images - a.images ).map (post) -> post.no
else liveThreadIDs
if /-rev$/.test(Index.currentSort)
Index.sortedThreadIDs = [Index.sortedThreadIDs...].reverse()
if Index.search and (threadIDs = Index.querySearch Index.search)
Index.sortedThreadIDs = threadIDs
# Sticky threads

View File

@ -6,6 +6,7 @@
<input type="search" id="index-search" class="field" placeholder="Search">
<a id="index-search-clear" href="javascript:;" title="Clear search">×</a>
<span id="hidden-label" hidden> &mdash; <span id="hidden-count"></span> <span id="hidden-toggle">[<a href="javascript:;">Show</a>]</span></span>
<input type="checkbox" id="index-rev" name="Reverse Sort" title="Reverse sort order">
<select id="index-mode" name="Index Mode">
<option disabled>Index Mode</option>
<option value="paged">Paged</option>

View File

@ -40,7 +40,7 @@
<div>Index-only link: <code>g-index</code></div>
<div>Catalog-only link: <code>g-catalog</code></div>
<div>Index mode: <code>g-mode:&quot;infinite scrolling&quot;</code></div>
<div>Index sort: <code>g-sort:&quot;creation date&quot;</code></div>
<div>Index sort: <code>g-sort:&quot;creation date rev&quot;</code></div>
<div>External link: <code>external-text:&quot;Google&quot;,&quot;http://www.google.com&quot;</code></div>
<div>Combinations are possible: <code>g-index-text:&quot;Technology Index&quot;</code></div>
<div>Full board list toggle: <code>toggle-all</code></div>

View File

@ -1,11 +1,9 @@
dialog = (id, position, properties) ->
dialog = (id, properties) ->
el = $.el 'div',
className: 'dialog'
id: id
$.extend el, properties
el.style.cssText = position
$.get "#{id}.position", position, (item) ->
(el.style.cssText = item["#{id}.position"])
el.style.cssText = Conf["#{id}.position"]
move = $ '.move', el
$.on move, 'touchstart mousedown', dragstart

View File

@ -194,7 +194,7 @@ Gallery =
error: ->
if @error?.code is MediaError.MEDIA_ERR_DECODE
return new Notice 'error', 'Corrupt or unplayable video', 30
return unless @src.split('/')[2] is 'i.4cdn.org'
return if ImageCommon.isFromArchive @
ImageCommon.error @, g.posts[@dataset.post], null, (url) =>
return unless url
Gallery.images[@dataset.id].href = url

View File

@ -33,6 +33,9 @@ ImageCommon =
message.textContent = 'Error: Corrupt or unplayable video'
return true
isFromArchive: (file) ->
file.src.split('/')[2] not in ['i.4cdn.org', 'is.4chan.org']
error: (file, post, delay, cb) ->
src = post.file.url.split '/'
URL = Redirect.to 'file', {
@ -42,12 +45,12 @@ ImageCommon =
unless Conf['404 Redirect'] and URL and Redirect.securityCheck URL
URL = null
return cb URL if (post.isDead or post.file.isDead) and file.src.split('/')[2] is 'i.4cdn.org'
return cb URL if (post.isDead or post.file.isDead) and not ImageCommon.isFromArchive file
timeoutID = setTimeout (-> cb URL), delay if delay?
return if post.isDead or post.file.isDead
redirect = ->
if file.src.split('/')[2] is 'i.4cdn.org'
unless ImageCommon.isFromArchive file
clearTimeout timeoutID if delay?
cb URL

View File

@ -268,7 +268,7 @@ ImageExpand =
if ImageCommon.decodeError @, post
return ImageExpand.contract post
# Don't autoretry images from the archive.
unless @src.split('/')[2] is 'i.4cdn.org'
if ImageCommon.isFromArchive @
return ImageExpand.contract post
ImageCommon.error @, post, 10 * $.SECOND, (URL) ->
if post.file.isExpanding or post.file.isExpanded

View File

@ -0,0 +1,12 @@
ImageHost =
init: ->
return unless Conf['Use Faster Image Host'] and g.VIEW in ['index', 'thread']
Callbacks.Post.push
name: 'Image Host Rewriting'
cb: @node
node: ->
return unless @file and not @isClone and (m = @file.url.match /^https?:\/\/is\.4chan\.org\/(.*)$/)
@file.link.hostname = 'i.4cdn.org'
@file.thumbLink.hostname = 'i.4cdn.org' if @file.thumbLink
@file.url = @file.link.href

View File

@ -5,7 +5,7 @@ Embedding =
@types[type.key] = type for type in @ordered_types
if Conf['Embedding']
@dialog = UI.dialog 'embedding', 'top: 50px; right: 0px;',
@dialog = UI.dialog 'embedding',
<%= readHTML('Embed.html') %>
@media = $ '#media-embed', @dialog
$.one d, '4chanXInitFinished', @ready

View File

@ -41,34 +41,38 @@ CatalogLinks =
a.href = "//boards.4chan.org/#{m[1]}/#{m[2] or '#catalog'}"
return
# Set links on load or custom board list change.
# Called by Header when both board lists (header and footer) are ready.
initBoardList: ->
return unless CatalogLinks.el
CatalogLinks.set Conf['Header catalog links']
toggle: ->
$.event 'CloseMenu'
$.set 'Header catalog links', @checked
CatalogLinks.set @checked
set: (useCatalog) ->
for a in $$('a:not([data-only])', Header.boardList).concat $$('a', Header.bottomBoardList)
continue if a.hostname not in ['boards.4chan.org', 'catalog.neet.tv'] or
!(board = a.pathname.split('/')[1]) or
board in ['f', 'status', '4chan'] or
a.pathname.split('/')[2] is 'archive' or
$.hasClass a, 'external'
Conf['Header catalog links'] = useCatalog
CatalogLinks.setLinks Header.boardList
CatalogLinks.setLinks Header.bottomBoardList
CatalogLinks.el.title = "Turn catalog links #{if useCatalog then 'off' else 'on'}."
$('input', CatalogLinks.el).checked = useCatalog
# Also called by Header when board lists are loaded / generated.
setLinks: (list) ->
return unless CatalogLinks.el and list
for a in $$('a:not([data-only])', list)
continue if (
a.hostname not in ['boards.4chan.org', 'catalog.neet.tv'] or
!(board = a.pathname.split('/')[1]) or
board in ['f', 'status', '4chan'] or
a.pathname.split('/')[2] is 'archive' or
$.hasClass a, 'external'
)
# Href is easier than pathname because then we don't have
# conditions where External Catalog has been disabled between switches.
a.href = if useCatalog then CatalogLinks.catalog(board) else "/#{board}/"
a.href = if Conf['Header catalog links'] then CatalogLinks.catalog(board) else "/#{board}/"
if a.dataset.indexOptions and a.hostname is 'boards.4chan.org' and a.pathname.split('/')[2] is ''
a.href += (if a.hash then '/' else '#') + a.dataset.indexOptions
CatalogLinks.el.title = "Turn catalog links #{if useCatalog then 'off' else 'on'}."
$('input', CatalogLinks.el).checked = useCatalog
return
catalog: (board=g.BOARD.ID) ->
if Conf['External Catalog'] and board in ['a', 'c', 'g', 'biz', 'k', 'm', 'o', 'p', 'v', 'vg', 'vr', 'w', 'wg', 'cm', '3', 'adv', 'an', 'asp', 'cgl', 'ck', 'co', 'diy', 'fa', 'fit', 'gd', 'int', 'jp', 'lit', 'mlp', 'mu', 'n', 'out', 'po', 'sci', 'sp', 'tg', 'toy', 'trv', 'tv', 'vp', 'wsg', 'x', 'f', 'pol', 's4s', 'lgbt']

View File

@ -10,7 +10,7 @@ IDColor =
cb: @node
node: ->
return if @isClone or !((uid = @info.uniqueID) and (span = $ 'span.hand', @nodes.uniqueID))
return if @isClone or !((uid = @info.uniqueID) and (span = @nodes.uniqueID))
rgb = IDColor.ids[uid] or IDColor.compute uid

View File

@ -9,8 +9,8 @@ IDHighlight =
uniqueID: null
node: ->
$.on @nodes.uniqueID, 'click', IDHighlight.click @ if @nodes.uniqueID
$.on @nodes.capcode, 'click', IDHighlight.click @ if @nodes.capcode
$.on @nodes.uniqueIDRoot, 'click', IDHighlight.click @ if @nodes.uniqueIDRoot
$.on @nodes.capcode, 'click', IDHighlight.click @ if @nodes.capcode
IDHighlight.set @ unless @isClone
set: (post) ->

View File

@ -10,7 +10,7 @@ IDPostCount =
node: ->
if @nodes.uniqueID and @thread is IDPostCount.thread
$.on $('span.hand', @nodes.uniqueID), 'mouseover', IDPostCount.count
$.on @nodes.uniqueID, 'mouseover', IDPostCount.count
count: ->
{uniqueID} = Get.postFromNode(@).info

View File

@ -19,7 +19,7 @@ ThreadStats =
Header.addShortcut 'stats', sc, 200
else
@dialog = sc = UI.dialog 'thread-stats', 'bottom: 0px; right: 0px;',
@dialog = sc = UI.dialog 'thread-stats',
<%= html('<div class="move" title="${statsTitle}">&{statsHTML}</div>') %>
$.addClass doc, 'float'
$.ready ->

View File

@ -14,7 +14,7 @@ ThreadUpdater =
$.extend sc, <%= html('<span id="update-status" class="empty"></span><span id="update-timer" class="empty" title="Update now"></span>') %>
Header.addShortcut 'updater', sc, 100
else
@dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;',
@dialog = sc = UI.dialog 'updater',
<%= html('<div class="move"></div><span id="update-status"></span><span id="update-timer" title="Update now"></span>') %>
$.addClass doc, 'float'
$.ready ->

View File

@ -10,7 +10,7 @@ ThreadWatcher =
className: 'fa fa-eye'
@db = new DataBoard 'watchedThreads', @refresh, true
@dialog = UI.dialog 'thread-watcher', 'top: 50px; left: 0px;', <%= readHTML('ThreadWatcher.html') %>
@dialog = UI.dialog 'thread-watcher', <%= readHTML('ThreadWatcher.html') %>
@status = $ '#watcher-status', @dialog
@list = @dialog.lastElementChild

View File

@ -132,7 +132,7 @@ Unread =
openNotification: (post) ->
return unless Header.areNotificationsEnabled
notif = new Notification "#{post.info.nameBlock} replied to you",
body: post.info.commentDisplay
body: post.commentDisplay()
icon: Favicon.logo
notif.onclick = ->
Header.scrollToIfNeeded post.nodes.root, true

View File

@ -398,6 +398,7 @@ QR =
handleUrl: (urlDefault) ->
QR.open()
QR.selected.preventAutoPost()
url = prompt 'Enter a URL:', urlDefault
return if url is null
QR.nodes.fileButton.focus()
@ -456,7 +457,7 @@ QR =
dialog: ->
QR.nodes = nodes =
el: dialog = UI.dialog 'qr', 'top: 50px; right: 0px;',
el: dialog = UI.dialog 'qr',
<%= readHTML('QuickReply.html') %>
setNode = (name, query) ->
@ -579,6 +580,7 @@ QR =
QR.abort()
return
$.forceSync 'cooldowns'
if QR.cooldown.seconds
QR.cooldown.auto = !QR.cooldown.auto
QR.status()

View File

@ -1,11 +1,5 @@
QR.cooldown =
seconds: 0
delays:
thread: 0
reply: 0
image: 0
deletion: 60 # cooldown for deleting posts/files
thread_global: 300 # inter-board thread cooldown
# Called from Main
init: ->
@ -16,13 +10,7 @@ QR.cooldown =
# Called from QR
setup: ->
# Read cooldown times
if m = Get.scriptData().match /\bcooldowns *= *({[^}]+})/
$.extend QR.cooldown.delays, JSON.parse m[1]
# Pass users have reduced cooldowns.
if d.cookie.indexOf('pass_enabled=1') >= 0
for key in ['reply', 'image']
QR.cooldown.delays[key] = Math.ceil(QR.cooldown.delays[key] / 2)
QR.cooldown.delays = g.BOARD.cooldowns()
# The longest reply cooldown, for use in pruning old reply data
QR.cooldown.maxDelay = 0
@ -104,7 +92,9 @@ QR.cooldown =
delete data[scope]
$.set 'cooldowns', data
count: ->
update: ->
return unless QR.cooldown.isCounting
$.forceSync 'cooldowns'
save = []
nCooldowns = 0
@ -175,4 +165,7 @@ QR.cooldown =
update = seconds isnt QR.cooldown.seconds
QR.cooldown.seconds = seconds
QR.status() if update
QR.submit() if seconds is 0 and QR.cooldown.auto and !QR.req
count: ->
QR.cooldown.update()
QR.submit() if QR.cooldown.seconds is 0 and QR.cooldown.auto and !QR.req

View File

@ -97,6 +97,7 @@ QR.oekaki =
QR.oekaki.toggle()
edit: ->
QR.cooldown.auto = false
QR.oekaki.load -> $.global ->
{Tegaki, FCX} = window
name = document.getElementById('qr-filename').value.replace(/\.\w+$/, '') + '.png'

View File

@ -16,7 +16,8 @@ QR.post = class
$.on @nodes.rm, 'click', (e) => e.stopPropagation(); @rm()
$.on @nodes.spoiler, 'change', (e) =>
@spoiler = e.target.checked
(QR.nodes.spoiler.checked = @spoiler if @ is QR.selected)
QR.nodes.spoiler.checked = @spoiler if @ is QR.selected
@preventAutoPost()
for label in $$ 'label', el
$.on label, 'click', (e) -> e.stopPropagation()
$.add QR.nodes.dumpList, el
@ -112,7 +113,7 @@ QR.post = class
@showFileData()
QR.characterCount()
save: (input) ->
save: (input, forced) ->
if input.type is 'checkbox'
@spoiler = input.checked
return
@ -125,10 +126,6 @@ QR.post = class
QR.status()
when 'com'
@updateComment()
# Disable auto-posting if you're typing in the first post
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0] and 0 < QR.cooldown.seconds <= 5
QR.cooldown.auto = false
when 'filename'
return unless @file
@saveFilename()
@ -136,6 +133,7 @@ QR.post = class
when 'name'
if @name isnt prev # only save manual changes, not values filled in by persona settings
QR.persona.set @
@preventAutoPost() unless forced
forceSave: ->
return unless @ is QR.selected
@ -143,9 +141,16 @@ QR.post = class
# that do not trigger the `input` event.
for name in ['thread', 'name', 'email', 'sub', 'com', 'filename', 'spoiler']
continue if not (node = QR.nodes[name])
@save node
@save node, true
return
preventAutoPost: ->
# Disable auto-posting if you're editing the first post
# during the last 5 seconds of the cooldown.
if QR.cooldown.auto and @ is QR.posts[0]
QR.cooldown.update() # adding/removing file can change cooldown
QR.cooldown.auto = false if QR.cooldown.seconds <= 5
setComment: (com) ->
@com = com or null
if @ is QR.selected
@ -210,6 +215,7 @@ QR.post = class
@fileError 'Unsupported file type.'
else if /^(image|video)\//.test @file.type
@readFile()
@preventAutoPost()
checkSize: ->
max = QR.max_size
@ -306,6 +312,7 @@ QR.post = class
@showFileData()
URL.revokeObjectURL @URL
@dismissErrors (error) -> $.hasClass error, 'file-error'
@preventAutoPost()
saveFilename: ->
@file.newName = (@filename or '').replace /[/\\]/g, '-'
@ -336,6 +343,7 @@ QR.post = class
pasteText: (file) ->
@pasting = true
@preventAutoPost()
reader = new FileReader()
reader.onload = (e) =>
{result} = e.target
@ -362,6 +370,7 @@ QR.post = class
index = (el) -> [el.parentNode.children...].indexOf el
oldIndex = index el
newIndex = index @
return if QR.posts[oldIndex].isLocked or QR.posts[newIndex].isLocked
(if oldIndex < newIndex then $.after else $.before) @, el
post = QR.posts.splice(oldIndex, 1)[0]
QR.posts.splice newIndex, 0, post

View File

@ -7,3 +7,17 @@ class Board
@config = BoardConfig.boards?[@ID] or {}
g.boards[@] = @
cooldowns: ->
c2 = (@config or {}).cooldowns or {}
c =
thread: c2.threads or 0
reply: c2.replies or 0
image: c2.images or 0
deletion: 60 # cooldown for deleting posts/files
thread_global: 300 # inter-board thread cooldown
# Pass users have reduced cooldowns.
if d.cookie.indexOf('pass_enabled=1') >= 0
for key in ['reply', 'image']
c[key] = Math.ceil(c[key] / 2)
c

View File

@ -175,7 +175,10 @@ class Fetcher
o.file =
name: data.media.media_filename
url: data.media.media_link or data.media.remote_media_link or
"#{location.protocol}//i.4cdn.org/#{@boardID}/#{encodeURIComponent data.media[if @boardID is 'f' then 'media_filename' else 'media_orig']}"
if @boardID is 'f'
"#{location.protocol}//i.4cdn.org/#{@boardID}/#{encodeURIComponent data.media.media_filename}"
else
"#{location.protocol}//#{if data.no % 3 then 'i.4cdn.org' else 'is.4chan.org'}/#{@boardID}/#{encodeURIComponent data.media.media_orig}"
height: data.media.media_h
width: data.media.media_w
MD5: data.media.media_hash

View File

@ -24,17 +24,23 @@ class Post
@thread.kill() if @thread.isArchived
@info =
nameBlock: if Conf['Anonymize'] then 'Anonymous' else @nodes.nameBlock.textContent.trim()
subject: @nodes.subject?.textContent or undefined
name: @nodes.name?.textContent
tripcode: @nodes.tripcode?.textContent
uniqueID: @nodes.uniqueID?.firstElementChild.textContent
uniqueID: @nodes.uniqueID?.textContent
capcode: @nodes.capcode?.textContent.replace '## ', ''
pass: @nodes.pass?.title.match(/\d*$/)[0]
flagCode: @nodes.flag?.className.match(/flag-(\w+)/)?[1].toUpperCase()
flag: @nodes.flag?.title
date: if @nodes.date then new Date(@nodes.date.dataset.utc * 1000)
if Conf['Anonymize']
@info.nameBlock = 'Anonymous'
else
@info.nameBlock = "#{@info.name or ''} #{@info.tripcode or ''}".trim()
@info.nameBlock += " ## #{@info.capcode}" if @info.capcode
@info.nameBlock += " (ID: #{@info.uniqueID})" if @info.uniqueID
@parseComment()
@parseQuotes()
@parseFile()
@ -59,25 +65,25 @@ class Post
post = $ '.post', root
info = $ '.postInfo', post
nodes =
root: root
post: post
info: info
subject: $ '.subject', info
name: $ '.name', info
email: $ '.useremail', info
tripcode: $ '.postertrip', info
uniqueID: $ '.posteruid', info
capcode: $ '.capcode.hand', info
pass: $ '.n-pu', info
flag: $ '.flag, .countryFlag', info
date: $ '.dateTime', info
nameBlock: $ '.nameBlock', info
quote: $ '.postNum > a:nth-of-type(2)', info
reply: $ '.replylink', info
fileRoot: $ '.file', post
comment: $ '.postMessage', post
links: []
quotelinks: []
root: root
post: post
info: info
subject: $ '.subject', info
name: $ '.name', info
email: $ '.useremail', info
tripcode: $ '.postertrip', info
uniqueIDRoot: $ '.posteruid', info
uniqueID: $ '.posteruid > .hand', info
capcode: $ '.capcode.hand', info
pass: $ '.n-pu', info
flag: $ '.flag, .countryFlag', info
date: $ '.dateTime', info
nameBlock: $ '.nameBlock', info
quote: $ '.postNum > a:nth-of-type(2)', info
reply: $ '.replylink', info
fileRoot: $ '.file', post
comment: $ '.postMessage', post
quotelinks: []
archivelinks: []
# XXX Edge invalidates HTMLCollections when an ancestor node is inserted into another node.
@ -101,29 +107,22 @@ class Post
# Remove:
# 'Comment too long'...
# EXIF data. (/p/)
# Rolls. (/tg/)
# Fortunes. (/s4s/)
bq = @nodes.comment.cloneNode true
for node in $$ '.abbr + br, .exif, b, .fortune', bq
$.rm node
if abbr = $ '.abbr', bq
$.rm abbr
@nodes.commentClean = bq = @nodes.comment.cloneNode true
@cleanComment bq
@info.comment = @nodesToText bq
if abbr
@info.comment = @info.comment.replace /\n\n$/, ''
# Hide spoilers.
# Remove:
commentDisplay: ->
# Get the comment's text for display purposes (e.g. notifications, excerpts).
# In addition to what's done in generating `@info.comment`, remove:
# Spoilers. (filter to '[spoiler]')
# Rolls. (/tg/, /qst/)
# Fortunes. (/s4s/)
# Preceding and following new lines.
# Trailing spaces.
commentDisplay = @info.comment
unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
spoilers = $$ 's', bq
if spoilers.length
for node in spoilers
$.replace node, $.tn '[spoiler]'
commentDisplay = @nodesToText bq
@info.commentDisplay = commentDisplay.trim().replace /\s+$/gm, ''
bq = @nodes.commentClean.cloneNode true
@cleanSpoilers bq unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers']
@cleanCommentDisplay bq
@nodesToText(bq).trim().replace(/\s+$/gm, '')
nodesToText: (bq) ->
text = ""
@ -133,6 +132,24 @@ class Post
text += node.data or '\n'
text
cleanComment: (bq) ->
if (abbr = $ '.abbr', bq) # 'Comment too long' or 'EXIF data available'
for node in $$ '.abbr + br, .exif', bq
$.rm node
for i in [0...2]
$.rm br if (br = abbr.previousSibling) and br.nodeName is 'BR'
$.rm abbr
cleanSpoilers: (bq) ->
spoilers = $$ 's', bq
for node in spoilers
$.replace node, $.tn '[spoiler]'
return
cleanCommentDisplay: (bq) ->
$.rm b if (b = $ 'b', bq) and /^Rolled /.test(b.textContent)
$.rm $('.fortune', bq)
parseQuotes: ->
@quotes = []
# XXX https://github.com/4chan/4chan-JS/issues/77
@ -171,8 +188,6 @@ class Post
return if not (link = $ '.fileText > a, .fileText-original > a', fileRoot)
return if not (info = link.nextSibling?.textContent.match /\(([\d.]+ [KMG]?B).*\)/)
fileText = fileRoot.firstElementChild
# XXX full images on https://is.4chan.org don't load
link.hostname = 'i.4cdn.org' if link.hostname is 'is.4chan.org'
@file =
text: fileText
link: link
@ -188,8 +203,6 @@ class Post
size *= 1024 while unit-- > 0
@file.sizeInBytes = size
if (thumb = $ 'a.fileThumb > [data-md5]', fileRoot)
# XXX full images on https://is.4chan.org don't load
thumb.parentNode.hostname = 'i.4cdn.org' if thumb.parentNode.hostname is 'is.4chan.org'
$.extend @file,
thumb: thumb
thumbLink: thumb.parentNode

View File

@ -184,6 +184,10 @@ Config =
]
'Images and Videos':
'Use Faster Image Host': [
true
'Change is.4chan.org links to point to the faster i.4cdn.org host.'
]
'Image Expansion': [
true
'Expand images / videos.'
@ -1049,3 +1053,10 @@ Config =
'Max Replies': 1000
'Autohiding Scrollbar': false
position:
'embedding.position': 'top: 50px; right: 0px;'
'thread-stats.position': 'bottom: 0px; right: 0px;'
'updater.position': 'bottom: 0px; left: 0px;'
'thread-watcher.position': 'top: 50px; left: 0px;'
'qr.position': 'top: 50px; right: 0px;'

View File

@ -706,7 +706,7 @@ div[data-checked="false"] > .suboption-list {
#index-search:not([data-searching]) + #index-search-clear {
display: none;
}
#index-mode, #index-sort, #index-size {
#index-rev, #index-mode, #index-sort, #index-size {
float: right;
}
.summary {

View File

@ -130,7 +130,7 @@ Main =
PostSuccessful.init()
return
when 'i.4cdn.org', 'is.4chan.org'
return unless pathname[2] and not /s\.jpg$/.test(pathname[2])
return unless pathname[2] and not /[sm]\.jpg$/.test(pathname[2])
$.asap (-> d.readyState isnt 'loading'), ->
if Conf['404 Redirect'] and d.title in ['4chan - Temporarily Offline', '4chan - 404 Not Found']
Redirect.navigate 'file', {
@ -425,6 +425,7 @@ Main =
['Board Configuration', BoardConfig]
['Normalize URL', NormalizeURL]
['Captcha Configuration', Captcha.replace]
['Image Host Rewriting', ImageHost]
['Redirect', Redirect]
['Header', Header]
['Catalog Links', CatalogLinks]

View File

@ -605,7 +605,6 @@ $.clear = (cb) ->
# Also support case where GM_listValues is not defined.
$.delete Object.keys(Conf)
$.delete ['previousversion', 'QR Size', 'captchas', 'QR.persona', 'hiddenPSA']
$.delete ("#{id}.position" for id in ['embedding', 'updater', 'thread-stats', 'thread-watcher', 'qr'])
try
$.delete $.listValues().map (key) -> key.replace g.NAMESPACE, ''
cb?()