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:
queue 2013-02-07 01:49:15 -07:00
parent b587719af8
commit b49664a11a
3 changed files with 216 additions and 18 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,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();
}

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,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()