diff --git a/4chan_x.user.js b/4chan_x.user.js index a705c9cdd..6a0db4081 100644 --- a/4chan_x.user.js +++ b/4chan_x.user.js @@ -43,7 +43,7 @@ */ (function() { - var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, + var $, $$, Anonymize, ArchiveLink, AutoGIF, Board, Build, Clone, Conf, Config, DeleteLink, DownloadLink, FileInfo, Filter, Get, Header, ImageExpand, ImageHover, Main, Menu, Notification, Polyfill, Post, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Recursive, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, Settings, Thread, ThreadHiding, ThreadUpdater, Time, UI, d, doc, g, __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }, __hasProp = {}.hasOwnProperty, __extends = function(child, parent) { for (var key in parent) { if (__hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; }; @@ -55,6 +55,7 @@ '404 Redirect': [true, 'Redirect dead threads and images.'], 'Keybinds': [true, 'Bind actions to keyboard shortcuts.'], 'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'], + 'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'], 'File Info Formatting': [true, 'Reformat the file information.'], 'Comment Expansion': [true, 'Can expand too long comments.'], 'Thread Expansion': [true, 'Can expand threads to view all replies.'], @@ -834,8 +835,19 @@ open: function(url) { return (GM_openInTab || window.open)(url, '_blank'); }, - hidden: function() { - return d.hidden || d.oHidden || d.mozHidden || d.webkitHidden; + debounce: function(wait, fn) { + var timeout; + timeout = null; + return function() { + if (timeout) { + clearTimeout(timeout); + } else { + fn.apply(this, arguments); + } + return timeout = setTimeout((function() { + return timeout = null; + }), wait); + }; }, queueTask: (function() { var execTask, taskChannel, taskQueue; @@ -940,6 +952,34 @@ } }); + Polyfill = { + init: function() { + return Polyfill.visibility(); + }, + visibility: function() { + var event, prefix, property; + if ('visibilityState' in document) { + return; + } + if ('webkitVisibilityState' in document) { + prefix = 'webkit'; + } else if ('mozVisibilityState' in document) { + prefix = 'moz'; + } else { + return; + } + property = prefix + 'VisibilityState'; + event = prefix + 'visibilitychange'; + d.visibilityState = d[property]; + d.hidden = d.visibilityState === 'hidden'; + return $.on(d, event, function() { + d.visibilityState = d[property]; + d.hidden = d.visibilityState === 'hidden'; + return $.event('visibilitychange'); + }); + } + }; + Header = { init: function() { var boardList, boardListButton, boardTitle, catalogToggler, headerBar, menuButton, toggleBar; @@ -2220,7 +2260,7 @@ case 'u': return "//nsfw.foolz.us/" + board + "/full_image/" + filename; case 'po': - return "http://archive.thedarkcave.org/" + board + "/full_image/" + filename; + return "//archive.thedarkcave.org/" + board + "/full_image/" + filename; case 'ck': case 'lit': return "//fuuka.warosu.org/" + board + "/full_image/" + filename; @@ -2263,8 +2303,10 @@ case 'u': case 'kuku': return "//nsfw.foolz.us/_/api/chan/post/?board=" + board + "&num=" + postID; + case 'c': + case 'int': case 'po': - return "http://archive.thedarkcave.org/_/api/chan/post/?board=" + board + "&num=" + postID; + return "//archive.thedarkcave.org/_/api/chan/post/?board=" + board + "&num=" + postID; } }, to: function(data) { @@ -2290,8 +2332,9 @@ case 'kuku': url = Redirect.path('//nsfw.foolz.us', 'foolfuuka', data); break; + case 'int': case 'po': - url = Redirect.path('http://archive.thedarkcave.org', 'foolfuuka', data); + url = Redirect.path('//archive.thedarkcave.org', 'foolfuuka', data); break; case 'ck': case 'lit': @@ -3284,6 +3327,73 @@ } }; + RelativeDates = { + INTERVAL: $.MINUTE, + init: function() { + if (g.VIEW === 'catalog' || !Conf['Relative Post Dates']) { + return; + } + $.on(d, 'visibilitychange ThreadUpdate', this.flush); + return Post.prototype.callbacks.push({ + name: 'Relative Post Dates', + cb: this.node + }); + }, + node: function() { + var dateEl, diff; + dateEl = this.nodes.date; + dateEl.title = dateEl.textContent; + diff = Date.now() - this.info.date; + dateEl.textContent = RelativeDates.relative(diff); + return RelativeDates.setUpdate(this, diff); + }, + relative: function(diff) { + var number, rounded, unit; + unit = (number = diff / $.DAY) > 1 ? 'day' : (number = diff / $.HOUR) > 1 ? 'hour' : (number = diff / $.MINUTE) > 1 ? 'minute' : (number = diff / $.SECOND, 'second'); + rounded = Math.round(number); + if (rounded !== 1) { + unit += 's'; + } + return "" + rounded + " " + unit + " ago"; + }, + stale: [], + flush: $.debounce($.SECOND, function() { + var now, update, _i, _len, _ref; + if (d.hidden) { + return; + } + now = Date.now(); + _ref = RelativeDates.stale; + for (_i = 0, _len = _ref.length; _i < _len; _i++) { + update = _ref[_i]; + update(now); + } + RelativeDates.stale = []; + clearTimeout(RelativeDates.timeout); + return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL); + }), + setUpdate: function(post, diff) { + var dateEl, markStale, setOwnTimeout, update; + setOwnTimeout = function(diff) { + var delay; + delay = diff > $.HOUR ? diff % $.HOUR : diff > $.MINUTE ? diff % $.MINUTE : diff % $.SECOND; + return setTimeout(markStale, delay); + }; + dateEl = post.nodes.date; + update = function(now) { + if (d.contains(dateEl)) { + diff = now - post.info.date; + dateEl.textContent = RelativeDates.relative(diff); + return setOwnTimeout(diff); + } + }; + markStale = function() { + return RelativeDates.stale.push(update); + }; + return setOwnTimeout(diff); + } + }; + FileInfo = { init: function() { if (g.VIEW === 'catalog' || !Conf['File Info Formatting']) { @@ -3839,7 +3949,7 @@ } $.on(window, 'online offline', ThreadUpdater.cb.online); $.on(d, 'QRPostSuccessful', ThreadUpdater.cb.post); - $.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility); + $.on(d, 'visibilitychange', ThreadUpdater.cb.visibility); ThreadUpdater.cb.online(); return $.add(d.body, ThreadUpdater.dialog); }, @@ -3874,7 +3984,7 @@ } }, visibility: function() { - if ($.hidden()) { + if (d.hidden) { return; } ThreadUpdater.outdateCount = 0; @@ -3886,7 +3996,7 @@ return ThreadUpdater.scrollBG = Conf['Scroll BG'] ? function() { return true; } : function() { - return !$.hidden(); + return !d.hidden; }; }, autoUpdate: function() { @@ -3942,7 +4052,7 @@ var i, j; i = ThreadUpdater.interval; j = Math.min(ThreadUpdater.outdateCount, 10); - if (!$.hidden()) { + if (!d.hidden) { j = Math.min(j, 7); } return ThreadUpdater.seconds = Math.max(i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]); @@ -4037,7 +4147,7 @@ } else { ThreadUpdater.set('status', "+" + count, 'new'); ThreadUpdater.outdateCount = 0; - if (Conf['Beep'] && $.hidden()) { + if (Conf['Beep'] && d.hidden) { if (!ThreadUpdater.audio) { ThreadUpdater.audio = $.el('audio', { src: ThreadUpdater.beep @@ -5381,6 +5491,7 @@ return console.timeEnd("" + name + " initialization"); }; console.time('All initializations'); + initFeature('Polyfill', Polyfill); initFeature('Header', Header); initFeature('Settings', Settings); initFeature('Resurrect Quotes', Quotify); @@ -5404,6 +5515,7 @@ initFeature('Mark Cross-thread Quotes', QuoteCT); initFeature('Anonymize', Anonymize); initFeature('Time Formatting', Time); + initFeature('Relative Post Dates', RelativeDates); initFeature('File Info Formatting', FileInfo); initFeature('Sauce', Sauce); initFeature('Image Expansion', ImageExpand); diff --git a/changelog b/changelog index cd0bd18ac..1d4b53337 100644 --- a/changelog +++ b/changelog @@ -26,8 +26,31 @@ alpha Fix unreadable inlined posts with the Tomorrow theme. master + +2.38.1 +- Mayhem + Fix a little regression introduced in 2.38.0 for webkit browsers. + +2.38.0 +- Queue + Add Relative Post Dates ("35 seconds ago"), disabled by default. +- Mayhem + Add /int/ archive redirection for threads, and post resurrection. + +2.37.6 +- Mayhem + Fix image expanding. + +2.37.5 +- Mayhem + Fix quoting inside inlined backlinks. + +2.37.4 +- James Campos + Don't expand pdfs - Mayhem Add /po/ archive redirection for threads, images and post resurrection. + Fix quoting. 2.37.3 - Mayhem diff --git a/grunt.js b/grunt.js index 03635bef3..13717ca8b 100644 --- a/grunt.js +++ b/grunt.js @@ -19,6 +19,7 @@ module.exports = function(grunt) { '', '', '', + '', '', '', '' diff --git a/latest.js b/latest.js index dda29cbcb..cfd9adaef 100644 --- a/latest.js +++ b/latest.js @@ -1 +1 @@ -postMessage({version:'2.37.3'},'*') \ No newline at end of file +postMessage({version:'2.38.1'},'*') \ No newline at end of file diff --git a/lib/$.coffee b/lib/$.coffee index d7db8ded0..082bf13f7 100644 --- a/lib/$.coffee +++ b/lib/$.coffee @@ -140,8 +140,17 @@ $.extend $, root.dispatchEvent new CustomEvent event, {bubbles: true, detail} open: (url) -> (GM_openInTab or window.open) url, '_blank' - hidden: -> - d.hidden or d.oHidden or d.mozHidden or d.webkitHidden + debounce: (wait, fn) -> + timeout = null + return -> + if timeout + # stop current reset + clearTimeout timeout + else + fn.apply this, arguments + + # after wait, let next invocation execute immediately + timeout = setTimeout (-> timeout = null), wait queueTask: (-> # inspired by https://www.w3.org/Bugs/Public/show_bug.cgi?id=15007 taskQueue = [] diff --git a/src/config.coffee b/src/config.coffee index 9eb0db11d..a97ed93bd 100644 --- a/src/config.coffee +++ b/src/config.coffee @@ -5,6 +5,7 @@ Config = '404 Redirect': [true, 'Redirect dead threads and images.'] 'Keybinds': [true, 'Bind actions to keyboard shortcuts.'] 'Time Formatting': [true, 'Localize and format timestamps arbitrarily.'] + 'Relative Post Dates': [false, 'Display dates like "3 minutes ago". Tooltip shows the timestamp.'] 'File Info Formatting': [true, 'Reformat the file information.'] 'Comment Expansion': [true, 'Can expand too long comments.'] 'Thread Expansion': [true, 'Can expand threads to view all replies.'] diff --git a/src/features.coffee b/src/features.coffee index 2695fbfdb..c9fa37e6b 100644 --- a/src/features.coffee +++ b/src/features.coffee @@ -1032,7 +1032,7 @@ Redirect = when 'u' "//nsfw.foolz.us/#{board}/full_image/#{filename}" when 'po' - "http://archive.thedarkcave.org/#{board}/full_image/#{filename}" + "//archive.thedarkcave.org/#{board}/full_image/#{filename}" when 'ck', 'lit' "//fuuka.warosu.org/#{board}/full_image/#{filename}" when 'diy', 'sci' @@ -1049,8 +1049,8 @@ Redirect = "//archive.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}" when 'u', 'kuku' "//nsfw.foolz.us/_/api/chan/post/?board=#{board}&num=#{postID}" - when 'po' - "http://archive.thedarkcave.org/_/api/chan/post/?board=#{board}&num=#{postID}" + when 'c', 'int', 'po' + "//archive.thedarkcave.org/_/api/chan/post/?board=#{board}&num=#{postID}" # for fuuka-based archives: # https://github.com/eksopl/fuuka/issues/27 to: (data) -> @@ -1060,8 +1060,8 @@ Redirect = url = Redirect.path '//archive.foolz.us', 'foolfuuka', data when 'u', 'kuku' url = Redirect.path '//nsfw.foolz.us', 'foolfuuka', data - when 'po' - url = Redirect.path 'http://archive.thedarkcave.org', 'foolfuuka', data + when 'int', 'po' + url = Redirect.path '//archive.thedarkcave.org', 'foolfuuka', data when 'ck', 'lit' url = Redirect.path '//fuuka.warosu.org', 'fuuka', data when 'diy', 'sci' @@ -1998,6 +1998,93 @@ Time = S: -> Time.zeroPad @getSeconds() y: -> @getFullYear() - 2000 +RelativeDates = + INTERVAL: $.MINUTE + init: -> + return if g.VIEW is 'catalog' or !Conf['Relative Post Dates'] + + # flush when page becomes visible again + $.on d, 'visibilitychange ThreadUpdate', @flush + + Post::callbacks.push + name: 'Relative Post Dates' + cb: @node + node: -> + dateEl = @nodes.date + + # Show original absolute time as tooltip so users can still know exact times + # Since "Time Formatting" runs `node` before us, the title tooltip will + # pick up the user-formatted time instead of 4chan time when enabled. + dateEl.title = dateEl.textContent + + diff = Date.now() - @info.date + + dateEl.textContent = RelativeDates.relative diff + RelativeDates.setUpdate @, diff + + # diff is milliseconds from now + relative: (diff) -> + unit = if (number = (diff / $.DAY)) > 1 + 'day' + else if (number = (diff / $.HOUR)) > 1 + 'hour' + else if (number = (diff / $.MINUTE)) > 1 + 'minute' + else + number = diff / $.SECOND + 'second' + + rounded = Math.round number + unit += 's' if rounded isnt 1 # pluralize + + "#{rounded} #{unit} ago" + + # changing all relative dates as soon as possible incurs many annoying + # redraws and scroll stuttering. Thus, sacrifice accuracy for UX/CPU economy, + # and perform redraws when the DOM is otherwise being manipulated (and scroll + # stuttering won't be noticed), falling back to INTERVAL while the page + # is visible. + # + # each individual dateTime element will add its update() function to the stale list + # when it is to be called. + stale: [] + flush: $.debounce($.SECOND, -> + # no point in changing the dates until the user sees them + return if d.hidden + + now = Date.now() + update now for update in RelativeDates.stale + RelativeDates.stale = [] + + # reset automatic flush + clearTimeout RelativeDates.timeout + RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL) + + # create function `update()`, closed over post and diff, that, when called + # from `flush()`, updates the element, and re-calls `setOwnTimeout()` to + # re-add `update()` to the stale list later. + setUpdate: (post, diff) -> + setOwnTimeout = (diff) -> + delay = if diff > $.HOUR + diff % $.HOUR + else if diff > $.MINUTE + diff % $.MINUTE + else + diff % $.SECOND + setTimeout markStale, delay + + dateEl = post.nodes.date + update = (now) -> + if d.contains dateEl # not removed from DOM + diff = now - post.info.date + dateEl.textContent = RelativeDates.relative diff + setOwnTimeout diff + + markStale = -> RelativeDates.stale.push update + + # kick off initial timeout with current diff + setOwnTimeout diff + FileInfo = init: -> return if g.VIEW is 'catalog' or !Conf['File Info Formatting'] @@ -2360,7 +2447,7 @@ ThreadUpdater = $.on window, 'online offline', ThreadUpdater.cb.online $.on d, 'QRPostSuccessful', ThreadUpdater.cb.post - $.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', ThreadUpdater.cb.visibility + $.on d, 'visibilitychange', ThreadUpdater.cb.visibility ThreadUpdater.cb.online() $.add d.body, ThreadUpdater.dialog @@ -2387,7 +2474,7 @@ ThreadUpdater = ThreadUpdater.outdateCount = 0 setTimeout ThreadUpdater.update, 1000 if ThreadUpdater.seconds > 2 visibility: -> - return if $.hidden() + return if d.hidden # Reset the counter when we focus this tab. ThreadUpdater.outdateCount = 0 if ThreadUpdater.seconds > ThreadUpdater.interval @@ -2396,7 +2483,7 @@ ThreadUpdater = ThreadUpdater.scrollBG = if Conf['Scroll BG'] -> true else - -> not $.hidden() + -> not d.hidden autoUpdate: -> if Conf['Auto Update This'] and ThreadUpdater.online ThreadUpdater.timeoutID = setTimeout ThreadUpdater.timeout, 1000 @@ -2447,7 +2534,7 @@ ThreadUpdater = getInterval: -> i = ThreadUpdater.interval j = Math.min ThreadUpdater.outdateCount, 10 - unless $.hidden() + unless d.hidden # Lower the max refresh rate limit on visible tabs. j = Math.min j, 7 ThreadUpdater.seconds = Math.max i, [0, 5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j] @@ -2523,7 +2610,7 @@ ThreadUpdater = else ThreadUpdater.set 'status', "+#{count}", 'new' ThreadUpdater.outdateCount = 0 - if Conf['Beep'] and $.hidden() # XXX and !Unread.replies.length + if Conf['Beep'] and d.hidden # XXX and !Unread.replies.length unless ThreadUpdater.audio ThreadUpdater.audio = $.el 'audio', src: ThreadUpdater.beep ThreadUpdater.audio.play() diff --git a/src/main.coffee b/src/main.coffee index 154b9a054..fda82dc75 100644 --- a/src/main.coffee +++ b/src/main.coffee @@ -295,6 +295,7 @@ Main = console.timeEnd "#{name} initialization" console.time 'All initializations' + initFeature 'Polyfill', Polyfill initFeature 'Header', Header initFeature 'Settings', Settings initFeature 'Resurrect Quotes', Quotify @@ -318,6 +319,7 @@ Main = initFeature 'Mark Cross-thread Quotes', QuoteCT initFeature 'Anonymize', Anonymize initFeature 'Time Formatting', Time + initFeature 'Relative Post Dates', RelativeDates initFeature 'File Info Formatting', FileInfo initFeature 'Sauce', Sauce initFeature 'Image Expansion', ImageExpand