Implement relative dates, close #758
Squash of 21 commits: -- Document "%R" relative time formatter Sounds reasonable, don't you think? -- Implement %R time formatter original code do not steal -- Add absolute time as title on date elements -- Update relative post times if necessary Coupled code, I know. Would be better to have a 'threadupdate' event to subscribe to, oh well. -- Update changelog -- Rewrite relative date formatter function Aeosynth-approved, or at least acknoledged -- Reimplement relative post times in namespace Less quick-and-dirty, more right. Still need to polyfill visibility -- Polyfill and correct usage of page visibility Reselecting the tab always flushes (since ui thread block will be percieved as part of the tab switch), but background flushes don't queue when tab is not visible. This is actually a better implementation than html5chan has right now, will need to backport it later. -- Remove %R reference in documentation -- Clean up RelativeDates code Aeosynth approved (TM) (C) -- Move page visibility polyfill to Main -- Remove unnecessary ? -- Replace $.hidden() fn with d.hidden -- Use visibility polyfill for QR -- Remove all future relative dates Looks like it's all over after this. -- Compact pluralize condition -- Fix alignment -- Change RelativeDates.flush -> @flush -- Remove initial setTimeout Is unnecessary, as node is called, which will call flush at least once and set the timeout again. -- Improve absolute date display and cache diff Title tooltip now uses either time formatting rice or the original 4chan timestamp instead of toString(). the diff between a date and now is now shared between `relative` and `setUpdate` to avoid recalculation. -- Remove re-calling of Time formatting Relying on the ordering of `node` callbacks, so textContext will already have the formatted time.
This commit is contained in:
parent
b587719af8
commit
b49664a11a
103
4chan_x.user.js
103
4chan_x.user.js
@ -81,7 +81,7 @@
|
||||
*/
|
||||
|
||||
(function() {
|
||||
var $, $$, Anonymize, ArchiveLink, AutoGif, Build, CatalogLinks, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, ReplyHiding, ReportLink, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base;
|
||||
var $, $$, Anonymize, ArchiveLink, AutoGif, Build, CatalogLinks, Conf, Config, DeleteLink, DownloadLink, ExpandComment, ExpandThread, Favicon, FileInfo, Filter, Get, ImageExpand, ImageHover, Keybinds, Main, Menu, Nav, Options, QR, QuoteBacklink, QuoteCT, QuoteInline, QuoteOP, QuotePreview, Quotify, Redirect, RelativeDates, ReplyHiding, ReportLink, RevealSpoilers, Sauce, StrikethroughQuotes, ThreadHiding, ThreadStats, Time, TitlePost, UI, Unread, Updater, Watcher, d, g, _base;
|
||||
|
||||
Config = {
|
||||
main: {
|
||||
@ -91,6 +91,7 @@
|
||||
'404 Redirect': [true, 'Redirect dead threads and images'],
|
||||
'Keybinds': [true, 'Binds actions to keys'],
|
||||
'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time'],
|
||||
'Relative Post Dates': [false, 'Display post times as "3 minutes ago" or similar. Hover tooltip will display the original or formatted timestamp'],
|
||||
'File Info Formatting': [true, 'Reformats the file information'],
|
||||
'Comment Expansion': [true, 'Expand too long comments'],
|
||||
'Thread Expansion': [true, 'View all replies'],
|
||||
@ -516,8 +517,19 @@
|
||||
size = unit > 1 ? Math.round(size * 100) / 100 : Math.round(size);
|
||||
return "" + size + " " + ['B', 'KB', 'MB', 'GB'][unit];
|
||||
},
|
||||
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);
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
@ -1841,7 +1853,7 @@
|
||||
if (QR.captcha.isEnabled && /captcha|verification/i.test(el.textContent)) {
|
||||
$('[autocomplete]', QR.el).focus();
|
||||
}
|
||||
if ($.hidden()) {
|
||||
if (d.hidden) {
|
||||
return alert(el.textContent);
|
||||
}
|
||||
},
|
||||
@ -3033,7 +3045,7 @@
|
||||
}
|
||||
$.add(d.body, dialog);
|
||||
$.on(d, 'QRPostSuccessful', this.cb.post);
|
||||
return $.on(d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', this.cb.visibility);
|
||||
return $.on(d, 'visibilitychange', this.cb.visibility);
|
||||
},
|
||||
/*
|
||||
http://freesound.org/people/pierrecartoons1979/sounds/90112/
|
||||
@ -3052,7 +3064,7 @@
|
||||
return setTimeout(Updater.update, 500);
|
||||
},
|
||||
visibility: function() {
|
||||
if ($.hidden()) {
|
||||
if (d.hidden) {
|
||||
return;
|
||||
}
|
||||
Updater.unsuccessfulFetchCount = 0;
|
||||
@ -3088,7 +3100,7 @@
|
||||
return Updater.scrollBG = this.checked ? function() {
|
||||
return true;
|
||||
} : function() {
|
||||
return !$.hidden();
|
||||
return !d.hidden;
|
||||
};
|
||||
},
|
||||
load: function() {
|
||||
@ -3159,7 +3171,7 @@
|
||||
Updater.count.className = count ? 'new' : null;
|
||||
}
|
||||
if (count) {
|
||||
if (Conf['Beep'] && $.hidden() && (Unread.replies.length === 0)) {
|
||||
if (Conf['Beep'] && d.hidden && (Unread.replies.length === 0)) {
|
||||
Updater.audio.play();
|
||||
}
|
||||
Updater.unsuccessfulFetchCount = 0;
|
||||
@ -3187,7 +3199,7 @@
|
||||
var i, j;
|
||||
i = +Conf['Interval'];
|
||||
j = Math.min(this.unsuccessfulFetchCount, 9);
|
||||
if (!$.hidden()) {
|
||||
if (!d.hidden) {
|
||||
j = Math.min(j, 6);
|
||||
}
|
||||
return Math.max(i, [5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]);
|
||||
@ -3537,6 +3549,59 @@
|
||||
}
|
||||
};
|
||||
|
||||
RelativeDates = {
|
||||
INTERVAL: $.MINUTE,
|
||||
init: function() {
|
||||
Main.callbacks.push(this.node);
|
||||
return $.on(d, 'visibilitychange', this.flush);
|
||||
},
|
||||
node: function(post) {
|
||||
var dateEl, diff, utc;
|
||||
dateEl = $('.postInfo > .dateTime', post.el);
|
||||
utc = dateEl.dataset.utc * 1000;
|
||||
dateEl.title = dateEl.textContent;
|
||||
diff = Date.now() - utc;
|
||||
dateEl.textContent = RelativeDates.relative(diff);
|
||||
RelativeDates.setUpdate(dateEl, diff);
|
||||
return RelativeDates.flush();
|
||||
},
|
||||
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 dateEl, diff, _i, _len, _ref;
|
||||
if (d.hidden) {
|
||||
return;
|
||||
}
|
||||
_ref = RelativeDates.stale;
|
||||
for (_i = 0, _len = _ref.length; _i < _len; _i++) {
|
||||
dateEl = _ref[_i];
|
||||
if (d.contains(dateEl)) {
|
||||
diff = Date.now() - dateEl.dataset.utc * 1000;
|
||||
dateEl.textContent = RelativeDates.relative(diff);
|
||||
RelativeDates.setUpdate(dateEl, diff);
|
||||
}
|
||||
}
|
||||
RelativeDates.stale = [];
|
||||
clearTimeout(RelativeDates.timeout);
|
||||
return RelativeDates.timeout = setTimeout(RelativeDates.flush, RelativeDates.INTERVAL);
|
||||
}),
|
||||
setUpdate: function(dateEl, diff) {
|
||||
var delay;
|
||||
delay = diff > $.HOUR ? diff % $.HOUR : diff > $.MINUTE ? diff % $.MINUTE : diff % $.SECOND;
|
||||
return setTimeout((function() {
|
||||
return RelativeDates.stale.push(dateEl);
|
||||
}), delay);
|
||||
}
|
||||
};
|
||||
|
||||
FileInfo = {
|
||||
init: function() {
|
||||
if (g.BOARD === 'f') {
|
||||
@ -4782,7 +4847,6 @@
|
||||
return "//archive.foolz.us/" + board + "/full_image/" + filename;
|
||||
case 'u':
|
||||
return "//nsfw.foolz.us/" + board + "/full_image/" + filename;
|
||||
case 'int':
|
||||
case 'po':
|
||||
return "http://archive.thedarkcave.org/" + board + "/full_image/" + filename;
|
||||
case 'ck':
|
||||
@ -5308,12 +5372,28 @@
|
||||
settings.disableAll = true;
|
||||
localStorage.setItem('4chan-settings', JSON.stringify(settings));
|
||||
}
|
||||
Main.polyfill();
|
||||
if (g.CATALOG) {
|
||||
return $.ready(Main.catalog);
|
||||
} else {
|
||||
return Main.features();
|
||||
}
|
||||
},
|
||||
polyfill: function() {
|
||||
var event, prefix, property;
|
||||
if (!('visibilityState' in document)) {
|
||||
prefix = 'mozVisibilityState' in document ? 'moz' : 'webkitvisibilityState' in document ? 'webkit' : 'o';
|
||||
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(d, new CustomEvent('visibilitychange'));
|
||||
});
|
||||
}
|
||||
},
|
||||
catalog: function() {
|
||||
if (Conf['Catalog Links']) {
|
||||
CatalogLinks.init();
|
||||
@ -5375,6 +5455,9 @@
|
||||
if (Conf['Time Formatting']) {
|
||||
Time.init();
|
||||
}
|
||||
if (Conf['Relative Post Dates']) {
|
||||
RelativeDates.init();
|
||||
}
|
||||
if (Conf['File Info Formatting']) {
|
||||
FileInfo.init();
|
||||
}
|
||||
|
||||
@ -1,6 +1,8 @@
|
||||
master
|
||||
- Mayhem
|
||||
Add /int/ archive redirection for threads, and post resurrection.
|
||||
- Queue
|
||||
Add relative time ("35 seconds ago") date formatting option.
|
||||
|
||||
2.37.6
|
||||
- Mayhem
|
||||
|
||||
129
script.coffee
129
script.coffee
@ -6,6 +6,7 @@ Config =
|
||||
'404 Redirect': [true, 'Redirect dead threads and images']
|
||||
'Keybinds': [true, 'Binds actions to keys']
|
||||
'Time Formatting': [true, 'Arbitrarily formatted timestamps, using your local time']
|
||||
'Relative Post Dates': [false, 'Display post times as "3 minutes ago" or similar. Hover tooltip will display the original or formatted timestamp']
|
||||
'File Info Formatting': [true, 'Reformats the file information']
|
||||
'Comment Expansion': [true, 'Expand too long comments']
|
||||
'Thread Expansion': [true, 'View all replies']
|
||||
@ -397,8 +398,19 @@ $.extend $,
|
||||
# Round to an integer otherwise.
|
||||
Math.round size
|
||||
"#{size} #{['B', 'KB', 'MB', 'GB'][unit]}"
|
||||
hidden: ->
|
||||
d.hidden or d.oHidden or d.mozHidden or d.webkitHidden
|
||||
# a function that will execute at most every 'wait' ms. executes immediately
|
||||
# if possible, else discards invocation
|
||||
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
|
||||
|
||||
$.cache.requests = {}
|
||||
|
||||
@ -1422,7 +1434,7 @@ QR =
|
||||
if QR.captcha.isEnabled and /captcha|verification/i.test el.textContent
|
||||
# Focus the captcha input on captcha error.
|
||||
$('[autocomplete]', QR.el).focus()
|
||||
alert el.textContent if $.hidden()
|
||||
alert el.textContent if d.hidden
|
||||
cleanError: ->
|
||||
$('.warning', QR.el).textContent = null
|
||||
|
||||
@ -2458,7 +2470,7 @@ Updater =
|
||||
$.add d.body, dialog
|
||||
|
||||
$.on d, 'QRPostSuccessful', @cb.post
|
||||
$.on d, 'visibilitychange ovisibilitychange mozvisibilitychange webkitvisibilitychange', @cb.visibility
|
||||
$.on d, 'visibilitychange', @cb.visibility
|
||||
|
||||
###
|
||||
http://freesound.org/people/pierrecartoons1979/sounds/90112/
|
||||
@ -2472,7 +2484,7 @@ Updater =
|
||||
Updater.unsuccessfulFetchCount = 0
|
||||
setTimeout Updater.update, 500
|
||||
visibility: ->
|
||||
return if $.hidden()
|
||||
return if d.hidden
|
||||
# Reset the counter when we focus this tab.
|
||||
Updater.unsuccessfulFetchCount = 0
|
||||
if Updater.timer.textContent < -Conf['Interval']
|
||||
@ -2500,7 +2512,7 @@ Updater =
|
||||
if @checked
|
||||
-> true
|
||||
else
|
||||
-> ! $.hidden()
|
||||
-> ! d.hidden
|
||||
load: ->
|
||||
switch @status
|
||||
when 404
|
||||
@ -2555,7 +2567,7 @@ Updater =
|
||||
Updater.count.className = if count then 'new' else null
|
||||
|
||||
if count
|
||||
if Conf['Beep'] and $.hidden() and (Unread.replies.length is 0)
|
||||
if Conf['Beep'] and d.hidden and (Unread.replies.length is 0)
|
||||
Updater.audio.play()
|
||||
Updater.unsuccessfulFetchCount = 0
|
||||
else
|
||||
@ -2580,7 +2592,7 @@ Updater =
|
||||
getInterval: ->
|
||||
i = +Conf['Interval']
|
||||
j = Math.min @unsuccessfulFetchCount, 9
|
||||
unless $.hidden()
|
||||
unless d.hidden
|
||||
# Don't increase the refresh rate too much on visible tabs.
|
||||
j = Math.min j, 6
|
||||
Math.max i, [5, 10, 15, 20, 30, 60, 90, 120, 240, 300][j]
|
||||
@ -2827,6 +2839,82 @@ Time =
|
||||
S: -> Time.zeroPad Time.date.getSeconds()
|
||||
y: -> Time.date.getFullYear() - 2000
|
||||
|
||||
RelativeDates =
|
||||
INTERVAL: $.MINUTE
|
||||
init: ->
|
||||
Main.callbacks.push @node
|
||||
|
||||
# flush when page becomes visible again
|
||||
$.on d, 'visibilitychange', @flush
|
||||
node: (post) ->
|
||||
dateEl = $ '.postInfo > .dateTime', post.el
|
||||
|
||||
utc = dateEl.dataset.utc * 1000 # convert data-utc to milliseconds
|
||||
|
||||
# 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() - utc
|
||||
|
||||
dateEl.textContent = RelativeDates.relative diff
|
||||
RelativeDates.setUpdate dateEl, diff
|
||||
|
||||
# Main calls @node whenever a DOM node is added (update, inline post,
|
||||
# whatever), so use also this reflow opportunity to flush any other dates
|
||||
# flush is debounced, so this won't burn too much cpu
|
||||
RelativeDates.flush()
|
||||
|
||||
# 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 be added to the stale list when it
|
||||
# needs to change.
|
||||
stale: []
|
||||
flush: $.debounce($.SECOND, ->
|
||||
# no point in changing the dates until the user sees them
|
||||
return if d.hidden
|
||||
for dateEl in RelativeDates.stale
|
||||
if d.contains dateEl # not removed from DOM
|
||||
diff = Date.now() - dateEl.dataset.utc * 1000
|
||||
dateEl.textContent = RelativeDates.relative diff
|
||||
RelativeDates.setUpdate dateEl, diff
|
||||
RelativeDates.stale = []
|
||||
clearTimeout RelativeDates.timeout
|
||||
RelativeDates.timeout = setTimeout RelativeDates.flush, RelativeDates.INTERVAL)
|
||||
|
||||
# Add element to stale list when the relative date string would change
|
||||
# diff is in milliseconds between dateEl and now
|
||||
setUpdate: (dateEl, diff) ->
|
||||
delay = if diff > $.HOUR
|
||||
diff % $.HOUR
|
||||
else if diff > $.MINUTE
|
||||
diff % $.MINUTE
|
||||
else
|
||||
diff % $.SECOND
|
||||
setTimeout (-> RelativeDates.stale.push dateEl), delay
|
||||
|
||||
FileInfo =
|
||||
init: ->
|
||||
return if g.BOARD is 'f'
|
||||
@ -4274,11 +4362,33 @@ Main =
|
||||
settings.disableAll = true
|
||||
localStorage.setItem '4chan-settings', JSON.stringify settings
|
||||
|
||||
Main.polyfill()
|
||||
|
||||
if g.CATALOG
|
||||
$.ready Main.catalog
|
||||
else
|
||||
Main.features()
|
||||
|
||||
polyfill: ->
|
||||
# page visibility API
|
||||
unless 'visibilityState' of document
|
||||
prefix = if 'mozVisibilityState' of document
|
||||
'moz'
|
||||
else if 'webkitvisibilityState' of document
|
||||
'webkit'
|
||||
else
|
||||
'o'
|
||||
|
||||
property = prefix + 'VisibilityState'
|
||||
event = prefix + 'visibilitychange'
|
||||
|
||||
d.visibilityState = d[property]
|
||||
d.hidden = d.visibilityState is 'hidden'
|
||||
$.on d, event, ->
|
||||
d.visibilityState = d[property]
|
||||
d.hidden = d.visibilityState is 'hidden'
|
||||
$.event d, new CustomEvent 'visibilitychange'
|
||||
|
||||
catalog: ->
|
||||
if Conf['Catalog Links']
|
||||
CatalogLinks.init()
|
||||
@ -4337,6 +4447,9 @@ Main =
|
||||
if Conf['Time Formatting']
|
||||
Time.init()
|
||||
|
||||
if Conf['Relative Post Dates']
|
||||
RelativeDates.init()
|
||||
|
||||
if Conf['File Info Formatting']
|
||||
FileInfo.init()
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user