Add live-updating to timestamps.

This commit is contained in:
Jack Lawson
2014-04-16 12:11:54 -07:00
parent a8346c0b26
commit 16abccafe5
13 changed files with 241 additions and 161 deletions

View File

@@ -450,6 +450,8 @@ module["reddit"] = LocalizedModule("reddit.js",
"lib/jquery.url.js",
"lib/backbone-1.0.0.js",
"templates.js",
"scrollupdater.js",
"timetext.js",
"ui.js",
"login.js",
"flair.js",

View File

@@ -618,26 +618,26 @@ class Link(Thing, Printable):
if item.score_fmt == Score.points:
taglinetext = ("<span>" +
_("%(score)s submitted %(when)s "
"ago%(lastedited)s") +
"%(lastedited)s") +
"</span>")
taglinetext += author_text
elif item.different_sr:
taglinetext = _("submitted %(when)s ago%(lastedited)s "
taglinetext = _("submitted %(when)s %(lastedited)s "
"by %(author)s to %(reddit)s")
else:
taglinetext = _("submitted %(when)s ago%(lastedited)s "
taglinetext = _("submitted %(when)s %(lastedited)s "
"by %(author)s")
else:
if item.score_fmt == Score.points:
taglinetext = ("<span>" +
_("%(score)s submitted %(when)s ago") +
_("%(score)s submitted %(when)s") +
"</span>")
taglinetext += author_text
elif item.different_sr:
taglinetext = _("submitted %(when)s ago by %(author)s "
taglinetext = _("submitted %(when)s by %(author)s "
"to %(reddit)s")
else:
taglinetext = _("submitted %(when)s ago by %(author)s")
taglinetext = _("submitted %(when)s by %(author)s")
item.taglinetext = taglinetext
if item.is_author:
@@ -1518,13 +1518,13 @@ class Message(Thing, Printable):
item.body = _('[unblock user to see this message]')
taglinetext = ''
if item.hide_author:
taglinetext = _("subreddit message %(author)s sent %(when)s ago")
taglinetext = _("subreddit message %(author)s sent %(when)s")
elif item.author_id == c.user._id:
taglinetext = _("to %(dest)s sent %(when)s ago")
taglinetext = _("to %(dest)s sent %(when)s")
elif item.to_id == c.user._id or item.to_id is None:
taglinetext = _("from %(author)s sent %(when)s ago")
taglinetext = _("from %(author)s sent %(when)s")
else:
taglinetext = _("to %(dest)s from %(author)s sent %(when)s ago")
taglinetext = _("to %(dest)s from %(author)s sent %(when)s")
item.taglinetext = taglinetext
if item.to:
if item.to._deleted:

View File

@@ -454,8 +454,8 @@ $.fn.replace_things = function(things, keep_children, reveal, stubs) {
* case of a comment tree, flags whether or not the new thing has
* the thread present) while "reveal" determines whether or not to
* animate the transition from old to new. */
var self = this;
return $.map(things, function(thing) {
var self = this,
map = $.map(things, function(thing) {
var data = thing.data;
var existing = $(self).things(data.id);
if(stubs)
@@ -508,13 +508,15 @@ $.fn.replace_things = function(things, keep_children, reveal, stubs) {
$(document).trigger('new_thing', new_thing)
return new_thing;
});
$(document).trigger('new_things_inserted')
return map
};
$.insert_things = function(things, append) {
/* Insert new things into a listing.*/
return $.map(things, function(thing) {
var map = $.map(things, function(thing) {
var data = thing.data;
var s = $.listing(data.parent);
if(append)
@@ -525,7 +527,9 @@ $.insert_things = function(things, append) {
thing_init_func(s.hide().show());
$(document).trigger('new_thing', s)
return s;
});
})
$(document).trigger('new_things_inserted')
return map
};
$.fn.delete_table_row = function(callback) {

View File

@@ -1,115 +1,129 @@
r.ScrollUpdater = Backbone.View.extend({
selector: null,
update: function() {},
!function(r, $){
r.ScrollUpdater = Backbone.View.extend({
selector: null,
startUpdate: function () {},
update: function ($el) {},
endUpdate: function ($els) {},
start: function() {
this._resetScrollState()
this._listen()
return this
},
start: function() {
this._resetScrollState()
this._listen()
return this
},
restart: function() {
this._resetScrollState()
return this
},
restart: function() {
this._resetScrollState()
return this
},
_resetScrollState: function() {
this._elements = $(this.selector)
_.sortBy(this._elements, function(el) {
return $(el).offset().top
})
_resetScrollState: function() {
this._elements = this.$el.find(this.selector)
_.sortBy(this._elements, function(el) {
return $(el).offset().top
})
this._curIndex = 0
this._lastScroll = null
this._toUpdate = []
this._totalTime = 0
this._curIndex = 0
this._lastScroll = null
this._toUpdate = []
this._totalTime = 0
// Trigger once now to detect any elements currently in view.
_.defer($.proxy(this, '_updateThings'))
},
// Trigger once now to detect any elements currently in view.
_.defer($.proxy(this, '_updateThings'))
},
_listen: function() {
var throttledUpdate = _.throttle($.proxy(this, '_updateThings'), 20)
$(window).on("scroll", throttledUpdate)
},
_listen: function() {
var throttledUpdate = _.throttle($.proxy(this, '_updateThings'), 20)
$(window).on('scroll', throttledUpdate)
},
_updateThings: function(ev) {
if (!this._elements.length) {
return
}
_updateThings: function(ev) {
if (!this._elements.length) {
return
}
var startTime = new Date()
var startTime = new Date()
// update the current page of elements and half a page in the
// direction of motion
var $win = $(window),
winHeight = $win.height(),
scrollTop = $win.scrollTop(),
ceiling = scrollTop,
floor = scrollTop + winHeight
// update the current page of elements and half a page in the
// direction of motion
var $win = $(window),
winHeight = $win.height(),
scrollTop = $win.scrollTop(),
ceiling = scrollTop,
floor = scrollTop + winHeight
if (scrollTop < this._lastScroll) {
ceiling = Math.max(ceiling - Math.floor(winHeight / 2), 0)
} else {
floor += Math.ceil(winHeight / 2)
}
if (scrollTop < this._lastScroll) {
ceiling = Math.max(ceiling - Math.floor(winHeight / 2), 0)
} else {
floor += Math.ceil(winHeight / 2)
}
// scan to ceiling to set the cursor
var idx = this._curIndex,
$cur = $(this._elements[idx])
if ($cur.offset().top < ceiling) {
// forward
while (idx < this._elements.length-1 && $cur.offset().top < ceiling) {
// scan to ceiling to set the cursor
var idx = this._curIndex,
$cur = $(this._elements[idx])
if ($cur.offset().top < ceiling) {
// forward
while (idx < this._elements.length-1 && $cur.offset().top < ceiling) {
$cur = $(this._elements[idx])
idx++
}
} else {
// backward
while (idx > 0 && $cur.offset().top > ceiling) {
$cur = $(this._elements[idx])
idx--
}
}
// update forward to floor
var count = 0
do {
$cur = $(this._elements[idx])
this._toUpdate.push($cur)
idx++
count++
} while (idx <= this._elements.length-1 && $cur.offset().top <= floor)
this._curIndex = idx - 1
this._lastScroll = scrollTop
var endTime = new Date()
this._totalTime += endTime - startTime
r.debug('scrollupdater queued', count, 'in', endTime - startTime, 'ms')
this._doUpdates()
},
cutoff: 1000 / 60,
_doUpdates: function() {
this.startUpdate()
var startTime = new Date(),
endTime = startTime,
count = 0,
els = []
while (endTime - startTime < this.cutoff) {
if (!this._toUpdate.length) {
break
}
var $el = this._toUpdate.shift()
els.push($el)
this.update($el)
count++
endTime = new Date()
}
} else {
// backward
while (idx > 0 && $cur.offset().top > ceiling) {
$cur = $(this._elements[idx])
idx--
this._totalTime += endTime - startTime
r.debug('scrollupdater updated', count, 'in', endTime - startTime, 'ms')
r.debug('scrollupdater total', this._totalTime, 'ms')
if (this._toUpdate.length) {
_.defer($.proxy(this, '_doUpdates'))
}
this.endUpdate($(els))
}
// update forward to floor
var count = 0
do {
$cur = $(this._elements[idx])
this._toUpdate.push($cur)
idx++
count++
} while (idx <= this._elements.length-1 && $cur.offset().top <= floor)
this._curIndex = idx - 1
this._lastScroll = scrollTop
var endTime = new Date()
this._totalTime += endTime - startTime
r.debug('scrollupdater queued', count, 'in', endTime - startTime, 'ms')
this._doUpdates()
},
cutoff: 1000 / 60,
_doUpdates: function() {
var startTime = new Date(),
endTime = startTime,
count = 0
while (endTime - startTime < this.cutoff) {
if (!this._toUpdate.length) {
break
}
var $el = this._toUpdate.shift()
this.update($el)
count++
endTime = new Date()
}
this._totalTime += endTime - startTime
r.debug('scrollupdater updated', count, 'in', endTime - startTime, 'ms')
r.debug('scrollupdater total', this._totalTime, 'ms')
if (this._toUpdate.length) {
_.defer($.proxy(this, '_doUpdates'))
}
}
})
})
}(r, jQuery)

View File

@@ -1,58 +1,81 @@
// polyfill, see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now
if (!Date.now) {
Date.now = function now() {
return new Date().getTime()
!function(r, $){
if (!Date.now) {
Date.now = function now() {
return new Date().getTime()
}
}
}
r.timetext = {
_maxAge: 24 * 60 * 60,
_chunks: [
[60 * 60 * 24 * 365, r.NP_('a year ago', '%(num)s years ago')],
[60 * 60 * 24 * 30, r.NP_('a month ago', '%(num)s months ago')],
[60 * 60 * 24, r.NP_('a day ago', '%(num)s days ago')],
// Start with smallest chunk, since we're probably looking at a recent
// set of timestamps and probably don't need to check older dates.
var CHUNKS = [
[60, r.NP_('a minute ago', '%(num)s minutes ago')],
[60 * 60, r.NP_('an hour ago', '%(num)s hours ago')],
[60, r.NP_('a minute ago', '%(num)s minutes ago')]
],
[60 * 60 * 24, r.NP_('a day ago', '%(num)s days ago')],
[60 * 60 * 24 * 30, r.NP_('a month ago', '%(num)s months ago')],
[60 * 60 * 24 * 365, r.NP_('a year ago', '%(num)s years ago')]
]
init: function () {
var defaults = {
maxage: 24 * 60 * 60
}
function TimeText(selector, opts) {
this.opts = _.defaults(opts || {}, defaults)
this.elCache = selector ? $(selector) : $([])
this.refresh = _.throttle(this._refresh, 1000)
setInterval($.proxy(this.refresh, this), 20 * 1000)
this.refresh()
setInterval(this.refresh, 20 * 1000)
},
}
refresh: function () {
TimeText.prototype._refresh = function(){
var now = Date.now()
$('time.live').each(function () {
r.timetext.refreshOne(this, now)
})
},
this.elCache.each($.proxy(function (i, el) {
this.refreshOne(el, now)
}, this))
}
refreshOne: function (el, now) {
if (!now)
now = Date.now()
TimeText.prototype.updateCache = function(elCache) {
this.elCache = elCache
this.refresh()
}
TimeText.prototype.refreshOne = function (el, now) {
if (!now){
now = Date.now()
}
var $el = $(el)
var timestamp = $el.data('timestamp')
var isoTimestamp
var age
var count
var keys
var text
if (!timestamp) {
var isoTimestamp = $el.attr('datetime')
isoTimestamp = $el.attr('datetime')
timestamp = Date.parse(isoTimestamp)
$el.data('timestamp', timestamp)
}
var age = (now - timestamp) / 1000
if (age > this._maxAge) {
age = (now - timestamp) / 1000
if (age > this.opts.maxage) {
$el.removeClass('live-timestamp')
return
}
var chunks = r.timetext._chunks
var text = r._('just now')
text = r._('just now')
$.each(r.timetext._chunks, function (ix, chunk) {
var count = Math.floor(age / chunk[0])
if (count > 0) {
var keys = chunk[1]
$.each(CHUNKS, function (ix, chunk) {
count = Math.floor(age / chunk[0])
if (count < chunk[0] && count > 0) {
keys = chunk[1]
text = r.P_(keys[0], keys[1], count).format({num: count})
return false
}
@@ -60,8 +83,6 @@ r.timetext = {
$el.text(text)
}
}
$(function () {
r.timetext.init()
})
r.TimeText = TimeText
}(r, jQuery)

View File

@@ -42,6 +42,34 @@ r.ui.init = function() {
}
r.ui.PermissionEditor.init()
r.ui.initLiveTimestamps()
}
r.ui.TimeTextScrollListener = r.ScrollUpdater.extend({
initialize: function() {
this.timeText = new r.TimeText(this.selector)
},
selector: '.live-timestamp:visible',
endUpdate: function($els) {
this.timeText.updateCache($els)
}
})
r.ui.initLiveTimestamps = function() {
// We only want a global timestamp scroll listener to instantiate on
// pages with `thing`s. Since we don't have a router yet, we'll scope
// the element to `.sitetable`s, which will contain it. This is kind of a
// dirty hack and should be obsoleted by a router + view system.
if ($('.sitetable').length) {
var listener = new r.ui.TimeTextScrollListener({ el: '.sitetable' })
listener.start()
// Every time we add a new `thing`, we'll need to re-grab our element caches.
$(document).on('new_things_inserted', function() {
listener.restart()
})
}
}
r.ui.showWorkingDeferred = function(el, deferred) {

View File

@@ -120,7 +120,7 @@ ${parent.collapsed()}
${unsafe(self.score(thing, likes = thing.likes))}&#32;
%endif
%endif
${thing_timestamp(thing, thing.timesince)}&#32;${_("ago")}
${thing_timestamp(thing, thing.timesince, live=True, include_tense=True)}
${edited(thing, thing.lastedited)}
%endif

View File

@@ -209,7 +209,7 @@ ${parent.thing_data_attributes(what)} data-ups="${what.upvotes}" data-downs="${w
%>
${unsafe(taglinetext % dict(reddit=self.subreddit(),
score=capture(self.score, thing, thing.likes, tag='span'),
when=capture(thing_timestamp, thing, thing.timesince),
when=capture(thing_timestamp, thing, thing.timesince, live=True, include_tense=True),
author=WrappedUser(thing.author, thing.attribs, thing).render(),
lastedited=capture(edited, thing, thing.lastedited)
))}

View File

@@ -64,7 +64,7 @@ ${parent.thing_css_class(what)} ${"new" if thing.new else ""} ${"was-comment" if
subreddit=subreddit)
taglinetext = thing.taglinetext.replace(' ', '&#32;') % dict(
when=capture(thing_timestamp, thing, thing.timesince),
when=capture(thing_timestamp, thing, thing.timesince, include_tense=True),
author=u"<b>%s</b>" % author,
dest=u"<b>%s</b>" % thing.dest)
%>

View File

@@ -32,7 +32,7 @@
%>
<tr class="modactions" style="background-color: ${bgcolor}" data-fullname="${thing.fullname}">
<td class="timestamp whitespace:nowrap">${timestamp(thing.date)}&#32;ago</td>
<td class="timestamp whitespace:nowrap">${timestamp(thing.date, live=True, include_tense=True)}</td>
%if is_multi:
<td class="subreddit">${plain_link('/r/' + thing.sr_name, thing.sr_path + 'about/log', title=thing.sr_name)}</td>
%endif

View File

@@ -49,7 +49,7 @@
<tr>
<td>${ip}</td>
<td>${location.get('country_name', '')}</td>
<td>${timestamp(last_visit)}&#32;${_('ago')}</td>
<td>${timestamp(last_visit, live=True, include_tense=True)}</td>
</tr>
% endfor
</tbody>

View File

@@ -546,18 +546,29 @@ ${unsafe(txt)}
</label>
</%def>
<%def name="timestamp(date, since=None)">
<%def name="timestamp(date, since=None, live=False, include_tense=False)">
## todo: use pubdate attribute once things are <article> tags.
## note: comment and link templates will pass a CachedVariable stub as since.
<time title="${long_datetime(date)}" datetime="${html_datetime(date)}">
<% now = date.now(g.tz) %>
<time title="${long_datetime(date)}" datetime="${html_datetime(date)}"
%if live:
class="live-timestamp"
%endif
>
${unsafe(since or timesince(date))}
%if include_tense and date < now:
${_("ago")}
%elif date > now:
${_("from now")}
%endif
</time>
</%def>
<%def name="thing_timestamp(thing, since=None)">
<%def name="thing_timestamp(thing, since=None, live=False, include_tense=False)">
## todo: use pubdate attribute once things are <article> tags.
## note: comment and link templates will pass a CachedVariable stub as since.
${timestamp(thing._date, since=since)}
${timestamp(thing._date, since=since, live=live, include_tense=include_tense)}
</%def>
<%def name="percentage(slice, total)">

View File

@@ -44,7 +44,7 @@
%endif
<td style="white-space: nowrap;">
${timestamp(thing.date)}&nbsp;ago
${timestamp(thing.date, live=True, include_tense=True)}
</td>
%if not thing.show_extended: