diff --git a/src/General/Build.coffee b/src/General/Build.coffee index 371868821..43b734a47 100644 --- a/src/General/Build.coffee +++ b/src/General/Build.coffee @@ -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(//gi, '\n') .replace(/\n\nRolled [^<]*<\/b>/i, '') # Rolls (/tg/) - .replace(/]*>/g, '') Build.unescape html @@ -93,6 +92,9 @@ Build = unless Conf['Remove Spoilers'] or Conf['Reveal Spoilers'] while (html2 = html.replace /(?:(?!<\/?s>).)*<\/s>/g, '[spoiler]') isnt html html = html2 + html = html + .replace(/^Rolled [^<]*<\/b>/i, '') # Rolls (/tg/, /qst/) + .replace(/ (${file.size}, ${file.dimensions || "PDF"}) - + 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 /\/+/ diff --git a/src/General/Header.coffee b/src/General/Header.coffee index 0789fa93f..c651b4227 100644 --- a/src/General/Header.coffee +++ b/src/General/Header.coffee @@ -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 diff --git a/src/General/Index.coffee b/src/General/Index.coffee index a6d24ff75..12f81c64f 100644 --- a/src/General/Index.coffee +++ b/src/General/Index.coffee @@ -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 diff --git a/src/General/Index/NavLinks.html b/src/General/Index/NavLinks.html index 90d1794fc..e69e93af2 100644 --- a/src/General/Index/NavLinks.html +++ b/src/General/Index/NavLinks.html @@ -6,6 +6,7 @@ × — [Show] + Index Mode Paged diff --git a/src/General/Settings/Advanced.html b/src/General/Settings/Advanced.html index cb09ffc15..324e5e55c 100644 --- a/src/General/Settings/Advanced.html +++ b/src/General/Settings/Advanced.html @@ -40,7 +40,7 @@ Index-only link: g-index Catalog-only link: g-catalog Index mode: g-mode:"infinite scrolling" - Index sort: g-sort:"creation date" + Index sort: g-sort:"creation date rev" External link: external-text:"Google","http://www.google.com" Combinations are possible: g-index-text:"Technology Index" Full board list toggle: toggle-all diff --git a/src/General/UI.coffee b/src/General/UI.coffee index c1b34cd1e..165ff4eb7 100644 --- a/src/General/UI.coffee +++ b/src/General/UI.coffee @@ -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 diff --git a/src/Images/Gallery.coffee b/src/Images/Gallery.coffee index c9af0a390..5d3de63ff 100644 --- a/src/Images/Gallery.coffee +++ b/src/Images/Gallery.coffee @@ -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 diff --git a/src/Images/ImageCommon.coffee b/src/Images/ImageCommon.coffee index 4b8b7396f..b94835b90 100644 --- a/src/Images/ImageCommon.coffee +++ b/src/Images/ImageCommon.coffee @@ -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 diff --git a/src/Images/ImageExpand.coffee b/src/Images/ImageExpand.coffee index 0ced99216..97650cf8f 100644 --- a/src/Images/ImageExpand.coffee +++ b/src/Images/ImageExpand.coffee @@ -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 diff --git a/src/Images/ImageHost.coffee b/src/Images/ImageHost.coffee new file mode 100644 index 000000000..8d024aeb6 --- /dev/null +++ b/src/Images/ImageHost.coffee @@ -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 diff --git a/src/Linkification/Embedding.coffee b/src/Linkification/Embedding.coffee index 6fe79aeb0..ed6f22e3d 100644 --- a/src/Linkification/Embedding.coffee +++ b/src/Linkification/Embedding.coffee @@ -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 diff --git a/src/Miscellaneous/CatalogLinks.coffee b/src/Miscellaneous/CatalogLinks.coffee index a3db30253..4b7f6aec6 100644 --- a/src/Miscellaneous/CatalogLinks.coffee +++ b/src/Miscellaneous/CatalogLinks.coffee @@ -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'] diff --git a/src/Miscellaneous/IDColor.coffee b/src/Miscellaneous/IDColor.coffee index 9d956c3fe..bb4046c3c 100644 --- a/src/Miscellaneous/IDColor.coffee +++ b/src/Miscellaneous/IDColor.coffee @@ -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 diff --git a/src/Miscellaneous/IDHighlight.coffee b/src/Miscellaneous/IDHighlight.coffee index 759a8b607..bf8aaa933 100644 --- a/src/Miscellaneous/IDHighlight.coffee +++ b/src/Miscellaneous/IDHighlight.coffee @@ -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) -> diff --git a/src/Miscellaneous/IDPostCount.coffee b/src/Miscellaneous/IDPostCount.coffee index cca9d1abb..5e8896c70 100644 --- a/src/Miscellaneous/IDPostCount.coffee +++ b/src/Miscellaneous/IDPostCount.coffee @@ -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 diff --git a/src/Monitoring/ThreadStats.coffee b/src/Monitoring/ThreadStats.coffee index dae528d88..1ad67cb3c 100644 --- a/src/Monitoring/ThreadStats.coffee +++ b/src/Monitoring/ThreadStats.coffee @@ -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('&{statsHTML}') %> $.addClass doc, 'float' $.ready -> diff --git a/src/Monitoring/ThreadUpdater.coffee b/src/Monitoring/ThreadUpdater.coffee index 81518172f..ef1398c75 100644 --- a/src/Monitoring/ThreadUpdater.coffee +++ b/src/Monitoring/ThreadUpdater.coffee @@ -14,7 +14,7 @@ ThreadUpdater = $.extend sc, <%= html('') %> Header.addShortcut 'updater', sc, 100 else - @dialog = sc = UI.dialog 'updater', 'bottom: 0px; left: 0px;', + @dialog = sc = UI.dialog 'updater', <%= html('') %> $.addClass doc, 'float' $.ready -> diff --git a/src/Monitoring/ThreadWatcher.coffee b/src/Monitoring/ThreadWatcher.coffee index 87af3b195..7530c068b 100644 --- a/src/Monitoring/ThreadWatcher.coffee +++ b/src/Monitoring/ThreadWatcher.coffee @@ -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 diff --git a/src/Monitoring/Unread.coffee b/src/Monitoring/Unread.coffee index 230fb7a7a..6c14dee0f 100644 --- a/src/Monitoring/Unread.coffee +++ b/src/Monitoring/Unread.coffee @@ -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 diff --git a/src/Posting/QR.coffee b/src/Posting/QR.coffee index 56aa2d283..22f0c177e 100644 --- a/src/Posting/QR.coffee +++ b/src/Posting/QR.coffee @@ -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() diff --git a/src/Posting/QR.cooldown.coffee b/src/Posting/QR.cooldown.coffee index b6f050c76..1b18148b4 100644 --- a/src/Posting/QR.cooldown.coffee +++ b/src/Posting/QR.cooldown.coffee @@ -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 diff --git a/src/Posting/QR.oekaki.coffee b/src/Posting/QR.oekaki.coffee index f1daaf97d..bb152f774 100644 --- a/src/Posting/QR.oekaki.coffee +++ b/src/Posting/QR.oekaki.coffee @@ -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' diff --git a/src/Posting/QR.post.coffee b/src/Posting/QR.post.coffee index a719f92a4..8346355fc 100644 --- a/src/Posting/QR.post.coffee +++ b/src/Posting/QR.post.coffee @@ -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 diff --git a/src/classes/Board.coffee b/src/classes/Board.coffee index dbf456d2f..de2d91116 100644 --- a/src/classes/Board.coffee +++ b/src/classes/Board.coffee @@ -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 diff --git a/src/classes/Fetcher.coffee b/src/classes/Fetcher.coffee index 7f41b0534..05569b41a 100644 --- a/src/classes/Fetcher.coffee +++ b/src/classes/Fetcher.coffee @@ -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 diff --git a/src/classes/Post.coffee b/src/classes/Post.coffee index fb05354e6..531eb6046 100644 --- a/src/classes/Post.coffee +++ b/src/classes/Post.coffee @@ -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 diff --git a/src/config/Config.coffee b/src/config/Config.coffee index 28fc7512c..080b4c294 100644 --- a/src/config/Config.coffee +++ b/src/config/Config.coffee @@ -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;' diff --git a/src/css/style.css b/src/css/style.css index a4348ac8d..7f89002b5 100644 --- a/src/css/style.css +++ b/src/css/style.css @@ -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 { diff --git a/src/main/Main.coffee b/src/main/Main.coffee index 3921ff298..b938e3df4 100644 --- a/src/main/Main.coffee +++ b/src/main/Main.coffee @@ -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] diff --git a/src/platform/$.coffee b/src/platform/$.coffee index c3b873337..4ba6bf011 100644 --- a/src/platform/$.coffee +++ b/src/platform/$.coffee @@ -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?()
g-index
g-catalog
g-mode:"infinite scrolling"
g-sort:"creation date"
g-sort:"creation date rev"
external-text:"Google","http://www.google.com"
g-index-text:"Technology Index"
toggle-all