Merge branch 'next'
This commit is contained in:
commit
d17550b802
@ -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, '')
|
||||
|
||||
|
||||
@ -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}"
|
||||
|
||||
@ -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 /\/+/
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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> — <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>
|
||||
|
||||
@ -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:"infinite scrolling"</code></div>
|
||||
<div>Index sort: <code>g-sort:"creation date"</code></div>
|
||||
<div>Index sort: <code>g-sort:"creation date rev"</code></div>
|
||||
<div>External link: <code>external-text:"Google","http://www.google.com"</code></div>
|
||||
<div>Combinations are possible: <code>g-index-text:"Technology Index"</code></div>
|
||||
<div>Full board list toggle: <code>toggle-all</code></div>
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
12
src/Images/ImageHost.coffee
Normal file
12
src/Images/ImageHost.coffee
Normal 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
|
||||
@ -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
|
||||
|
||||
@ -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']
|
||||
|
||||
@ -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
|
||||
|
||||
|
||||
@ -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) ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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 ->
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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'
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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;'
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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]
|
||||
|
||||
@ -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?()
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user