Merge pull request #897 from qqueue/relativedate

Relative Dates, close #758
This commit is contained in:
Mayhem 2013-02-07 10:49:03 -08:00
commit 0a9c2ff75c
3 changed files with 239 additions and 17 deletions

View File

@ -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,68 @@
}
};
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);
dateEl.title = dateEl.textContent;
utc = dateEl.dataset.utc * 1000;
diff = Date.now() - utc;
dateEl.textContent = RelativeDates.relative(diff);
RelativeDates.setUpdate(dateEl, utc, 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 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(dateEl, utc, diff) {
var markStale, setOwnTimeout, update;
setOwnTimeout = function(diff) {
var delay;
delay = diff > $.HOUR ? diff % $.HOUR : diff > $.MINUTE ? diff % $.MINUTE : diff % $.SECOND;
return setTimeout(markStale, delay);
};
update = function(now) {
if (d.contains(dateEl)) {
diff = now - utc;
dateEl.textContent = RelativeDates.relative(diff);
return setOwnTimeout(diff);
}
};
markStale = function() {
return RelativeDates.stale.push(update);
};
return setOwnTimeout(diff);
}
};
FileInfo = {
init: function() {
if (g.BOARD === 'f') {
@ -5307,12 +5381,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();
@ -5374,6 +5464,9 @@
if (Conf['Time Formatting']) {
Time.init();
}
if (Conf['Relative Post Dates']) {
RelativeDates.init();
}
if (Conf['File Info Formatting']) {
FileInfo.init();
}

View File

@ -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

View File

@ -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,96 @@ 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
# 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
# convert data-utc to milliseconds
utc = dateEl.dataset.utc * 1000
diff = Date.now() - utc
dateEl.textContent = RelativeDates.relative diff
RelativeDates.setUpdate dateEl, utc, 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 add its update() function to the stale list
# when it 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 dateEl and diff, that, when called
# from `flush()`, updates the element, and re-calls `setOwnTimeout()` to
# re-add `update()` to the stale list later.
setUpdate: (dateEl, utc, diff) ->
setOwnTimeout = (diff) ->
delay = if diff > $.HOUR
diff % $.HOUR
else if diff > $.MINUTE
diff % $.MINUTE
else
diff % $.SECOND
setTimeout markStale, delay
update = (now) ->
if d.contains dateEl # not removed from DOM
diff = now - utc
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.BOARD is 'f'
@ -4274,11 +4376,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 +4461,9 @@ Main =
if Conf['Time Formatting']
Time.init()
if Conf['Relative Post Dates']
RelativeDates.init()
if Conf['File Info Formatting']
FileInfo.init()