diff --git a/r2/r2/lib/js.py b/r2/r2/lib/js.py index 59223ebfc..d964e2163 100755 --- a/r2/r2/lib/js.py +++ b/r2/r2/lib/js.py @@ -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", diff --git a/r2/r2/models/link.py b/r2/r2/models/link.py index a8378f0a8..3dc42c24d 100755 --- a/r2/r2/models/link.py +++ b/r2/r2/models/link.py @@ -618,26 +618,26 @@ class Link(Thing, Printable): if item.score_fmt == Score.points: taglinetext = ("" + _("%(score)s submitted %(when)s " - "ago%(lastedited)s") + + "%(lastedited)s") + "") 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 = ("" + - _("%(score)s submitted %(when)s ago") + + _("%(score)s submitted %(when)s") + "") 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: diff --git a/r2/r2/public/static/js/jquery.reddit.js b/r2/r2/public/static/js/jquery.reddit.js index 8a7423e0f..b24a95c96 100644 --- a/r2/r2/public/static/js/jquery.reddit.js +++ b/r2/r2/public/static/js/jquery.reddit.js @@ -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) { diff --git a/r2/r2/public/static/js/scrollupdater.js b/r2/r2/public/static/js/scrollupdater.js index b8640d8ef..cb618a45f 100644 --- a/r2/r2/public/static/js/scrollupdater.js +++ b/r2/r2/public/static/js/scrollupdater.js @@ -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) diff --git a/r2/r2/public/static/js/timetext.js b/r2/r2/public/static/js/timetext.js index 7c659de9e..b36fc2414 100644 --- a/r2/r2/public/static/js/timetext.js +++ b/r2/r2/public/static/js/timetext.js @@ -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) diff --git a/r2/r2/public/static/js/ui.js b/r2/r2/public/static/js/ui.js index 44b85d2c2..92bf0c41b 100644 --- a/r2/r2/public/static/js/ui.js +++ b/r2/r2/public/static/js/ui.js @@ -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) { diff --git a/r2/r2/templates/comment.html b/r2/r2/templates/comment.html index b1898602a..4a7265f87 100755 --- a/r2/r2/templates/comment.html +++ b/r2/r2/templates/comment.html @@ -120,7 +120,7 @@ ${parent.collapsed()} ${unsafe(self.score(thing, likes = thing.likes))} %endif %endif - ${thing_timestamp(thing, thing.timesince)} ${_("ago")} + ${thing_timestamp(thing, thing.timesince, live=True, include_tense=True)} ${edited(thing, thing.lastedited)} %endif diff --git a/r2/r2/templates/link.html b/r2/r2/templates/link.html index d5e99084f..fa07ee89f 100755 --- a/r2/r2/templates/link.html +++ b/r2/r2/templates/link.html @@ -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) ))} diff --git a/r2/r2/templates/message.html b/r2/r2/templates/message.html index f4fdcd2ce..8b1527308 100644 --- a/r2/r2/templates/message.html +++ b/r2/r2/templates/message.html @@ -64,7 +64,7 @@ ${parent.thing_css_class(what)} ${"new" if thing.new else ""} ${"was-comment" if subreddit=subreddit) taglinetext = thing.taglinetext.replace(' ', ' ') % dict( - when=capture(thing_timestamp, thing, thing.timesince), + when=capture(thing_timestamp, thing, thing.timesince, include_tense=True), author=u"%s" % author, dest=u"%s" % thing.dest) %> diff --git a/r2/r2/templates/modaction.html b/r2/r2/templates/modaction.html index 047856bb8..9bd5760d4 100755 --- a/r2/r2/templates/modaction.html +++ b/r2/r2/templates/modaction.html @@ -32,7 +32,7 @@ %> - ${timestamp(thing.date)} ago + ${timestamp(thing.date, live=True, include_tense=True)} %if is_multi: ${plain_link('/r/' + thing.sr_name, thing.sr_path + 'about/log', title=thing.sr_name)} %endif diff --git a/r2/r2/templates/useriphistory.html b/r2/r2/templates/useriphistory.html index 258a3c9d4..ae4fd45fd 100644 --- a/r2/r2/templates/useriphistory.html +++ b/r2/r2/templates/useriphistory.html @@ -49,7 +49,7 @@ ${ip} ${location.get('country_name', '')} - ${timestamp(last_visit)} ${_('ago')} + ${timestamp(last_visit, live=True, include_tense=True)} % endfor diff --git a/r2/r2/templates/utils.html b/r2/r2/templates/utils.html index 6683392d0..46a3866ce 100755 --- a/r2/r2/templates/utils.html +++ b/r2/r2/templates/utils.html @@ -546,18 +546,29 @@ ${unsafe(txt)} -<%def name="timestamp(date, since=None)"> +<%def name="timestamp(date, since=None, live=False, include_tense=False)"> ## todo: use pubdate attribute once things are
tags. ## note: comment and link templates will pass a CachedVariable stub as since. -