diff --git a/Guardfile b/Guardfile index 147f523cc2b..a3e3ef8b9c2 100644 --- a/Guardfile +++ b/Guardfile @@ -58,19 +58,21 @@ unless ENV["USING_AUTOSPEC"] end end -def message_bus - MessageBus::Instance.new.tap do |bus| - bus.site_id_lookup do - # this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different - "default" - end - end -end module ::Guard class AutoReload < ::Guard::Guard require File.dirname(__FILE__) + '/config/environment' + + def self.message_bus + MessageBus::Instance.new.tap do |bus| + bus.site_id_lookup do + # this is going to be dev the majority of the time, if you have multisite configured in dev stuff may be different + "default" + end + end + end + def run_on_change(paths) paths.map! do |p| hash = nil @@ -82,7 +84,7 @@ module ::Guard p = p.sub /^app\/assets\/stylesheets/, "assets" {name: p, hash: hash} end - message_bus.publish "/file-change", paths + self.class.message_bus.publish "/file-change", paths end def run_all @@ -93,7 +95,7 @@ end Thread.new do Listen.to('tmp/') do |modified,added,removed| modified.each do |m| - message_bus.publish "/file-change", ["refresh"] if m =~ /refresh_browser/ + Guard::AutoReload.message_bus.publish "/file-change", ["refresh"] if m =~ /refresh_browser/ end end end diff --git a/app/assets/javascripts/discourse.js b/app/assets/javascripts/discourse.js index 228db475c01..c619a7dcb18 100644 --- a/app/assets/javascripts/discourse.js +++ b/app/assets/javascripts/discourse.js @@ -141,6 +141,10 @@ Discourse = Ember.Application.createWithMixins({ xhr.setRequestHeader('X-CSRF-Token', csrfToken); } }); + + setInterval(function(){ + Discourse.Formatter.updateRelativeAge($('.relative-date')); + },60 * 1000); }, /** diff --git a/app/assets/javascripts/discourse/components/formatter.js b/app/assets/javascripts/discourse/components/formatter.js new file mode 100644 index 00000000000..a7152f51d77 --- /dev/null +++ b/app/assets/javascripts/discourse/components/formatter.js @@ -0,0 +1,68 @@ +Discourse.Formatter = (function(){ + var updateRelativeAge, autoUpdatingRelativeAge, relativeAge; + + updateRelativeAge = function(elems) { + elems.each(function(){ + var $this = $(this); + $this.html(relativeAge(new Date($this.data('time')), $this.data('format'))); + }); + }; + + autoUpdatingRelativeAge = function(date,options) { + options = options || {}; + var format = options.format || "tiny"; + + return "" + relativeAge(date, options) + ""; + }; + + // mostly lifted from rails with a few amendments + relativeAge = function(date, options) { + options = options || {}; + var format = options.format || "tiny"; + + var distance = Math.round((new Date() - date) / 1000); + var distance_in_minutes = Math.round(distance / 60.0); + + var formatted; + var t = function(key,opts){ + return Ember.String.i18n("dates." + format + "." + key, opts); + }; + + switch(true){ + + case(distance_in_minutes < 1): + formatted = t("less_than_x_minutes", {count: 1}); + break; + case(distance_in_minutes >= 1 && distance_in_minutes <= 44): + formatted = t("x_minutes", {count: distance_in_minutes}); + break; + case(distance_in_minutes >= 45 && distance_in_minutes <= 89): + formatted = t("about_x_hours", {count: 1}); + break; + case(distance_in_minutes >= 90 && distance_in_minutes <= 1439): + formatted = t("about_x_hours", {count: Math.round(distance_in_minutes / 60.0)}); + break; + case(distance_in_minutes >= 1440 && distance_in_minutes <= 2519): + formatted = t("x_days", {count: 1}); + break; + case(distance_in_minutes >= 2520 && distance_in_minutes <= 129599): + formatted = t("x_days", {count: Math.round(distance_in_minutes / 1440.0)}); + break; + case(distance_in_minutes >= 129600 && distance_in_minutes <= 525599): + formatted = t("x_months", {count: Math.round(distance_in_minutes / 43200.0)}); + break; + default: + var months = Math.round(distance_in_minutes / 43200.0); + if (months < 24) { + formatted = t("x_months", {count: months}); + } else { + formatted = t("over_x_years", {count: Math.round(months / 12.0)}); + } + break; + } + + return formatted; + }; + + return {relativeAge: relativeAge, autoUpdatingRelativeAge: autoUpdatingRelativeAge, updateRelativeAge: updateRelativeAge}; +})(); diff --git a/app/assets/javascripts/discourse/helpers/application_helpers.js b/app/assets/javascripts/discourse/helpers/application_helpers.js index 40a4fdf5391..6091aba78fb 100644 --- a/app/assets/javascripts/discourse/helpers/application_helpers.js +++ b/app/assets/javascripts/discourse/helpers/application_helpers.js @@ -173,11 +173,21 @@ Handlebars.registerHelper('avatar', function(user, options) { @for Handlebars **/ Handlebars.registerHelper('unboundDate', function(property, options) { - var dt; - dt = new Date(Ember.Handlebars.get(this, property, options)); + var dt = new Date(Ember.Handlebars.get(this, property, options)); return dt.format("long"); }); +/** + Live refreshing age helper + + @method unboundDate + @for Handlebars +**/ +Handlebars.registerHelper('unboundAge', function(property, options) { + var dt = new Date(Ember.Handlebars.get(this, property, options)); + return new Handlebars.SafeString(Discourse.Formatter.autoUpdatingRelativeAge(dt)); +}); + /** Display a date related to an edit of a post @@ -285,7 +295,7 @@ Handlebars.registerHelper('date', function(property, options) { } displayDate = humanized; if (!leaveAgo) { - displayDate = (dt.millisecondsAgo()).duration(); + displayDate = (dt.millisecondsAgo()).duration(); } } return new Handlebars.SafeString("" + displayDate + ""); diff --git a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars index 5ab4f16640c..f03e659e238 100644 --- a/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars +++ b/app/assets/javascripts/discourse/templates/list/topic_list_item.js.handlebars @@ -63,14 +63,14 @@ {{#if bumped}} - {{{age}}} + {{unboundAge created_at}} - {{{bumped_age}}} + {{unboundAge bumped_at}} {{else}} - {{{age}}} + {{unboundAge created_at}} {{/if}} diff --git a/app/serializers/listable_topic_serializer.rb b/app/serializers/listable_topic_serializer.rb index 8b66469201d..b48448a24a4 100644 --- a/app/serializers/listable_topic_serializer.rb +++ b/app/serializers/listable_topic_serializer.rb @@ -1,4 +1,3 @@ -require_dependency 'age_words' require_dependency 'pinned_check' class ListableTopicSerializer < BasicTopicSerializer @@ -10,8 +9,6 @@ class ListableTopicSerializer < BasicTopicSerializer :last_posted_at, :bumped, :bumped_at, - :bumped_age, - :age, :unseen, :last_read_post_number, :unread, @@ -23,20 +20,10 @@ class ListableTopicSerializer < BasicTopicSerializer :closed, :archived - def age - AgeWords.age_words(Time.now - (object.created_at || Time.now)) - end - def bumped object.created_at < object.bumped_at end - def bumped_age - return nil if object.bumped_at.blank? - AgeWords.age_words(Time.now - object.bumped_at) - end - alias include_bumped_age? :bumped - def seen object.user_data.present? end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6011936edf4..ff7ec173bd7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -7,6 +7,42 @@ en: js: + dates: + tiny: + half_a_minute: "< 1m" + less_than_x_seconds: + one: "< 1s" + other: "< %{count}s" + x_seconds: + one: "1s" + other: "%{count}s" + less_than_x_minutes: + one: "< 1m" + other: "< %{count}m" + x_minutes: + one: "1m" + other: "%{count}m" + about_x_hours: + one: "1h" + other: "%{count}h" + x_days: + one: "1d" + other: "%{count}d" + about_x_months: + one: "1mon" + other: "%{count}mon" + x_months: + one: "1mon" + other: "%{count}mon" + about_x_years: + one: "1y" + other: "%{count}y" + over_x_years: + one: "> 1y" + other: "> %{count}y" + almost_x_years: + one: "1y" + other: "%{count}y" share: topic: 'share a link to this topic' post: 'share a link to this post' diff --git a/spec/javascripts/components/formatter_spec.js b/spec/javascripts/components/formatter_spec.js new file mode 100644 index 00000000000..8ea077199c9 --- /dev/null +++ b/spec/javascripts/components/formatter_spec.js @@ -0,0 +1,63 @@ +/*global expect:true describe:true it:true beforeEach:true afterEach:true spyOn:true */ + +describe("Discourse.Formatter", function() { + + describe("relativeTime", function() { + + it("can format dates", function() { + var mins_ago = function(mins){ + return new Date((new Date()) - mins * 60 * 1000); + }; + + var formatMins = function(mins) { + return Discourse.Formatter.relativeAge(mins_ago(mins)); + }; + + var formatHours = function(hours) { + return formatMins(hours * 60); + }; + + var formatDays = function(days) { + return formatHours(days * 24); + }; + + var formatMonths = function(months) { + return formatDays(months * 30); + }; + + expect(formatMins(0)).toBe("< 1m"); + expect(formatMins(2)).toBe("2m"); + expect(formatMins(60)).toBe("1h"); + expect(formatHours(4)).toBe("4h"); + expect(formatDays(1)).toBe("1d"); + expect(formatDays(20)).toBe("20d"); + expect(formatMonths(3)).toBe("3mon"); + expect(formatMonths(23)).toBe("23mon"); + expect(formatMonths(24)).toBe("> 2y"); + }); + }); + + describe("autoUpdatingRelativeAge", function(){ + it("can format dates", function(){ + var d = new Date(); + + var $elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d)); + + expect($elem.data('format')).toBe("tiny"); + expect($elem.data('time')).toBe(d.getTime()); + }); + }); + describe("updateRelativeAge", function(){ + it("can update relative dates", function(){ + + var d = new Date(); + var $elem = $(Discourse.Formatter.autoUpdatingRelativeAge(d)); + $elem.data('time', d.getTime() - 2 * 60 * 1000); + + Discourse.Formatter.updateRelativeAge($elem); + + expect($elem.html()).toBe("2m"); + + }); + }); +}); diff --git a/spec/javascripts/spec.js b/spec/javascripts/spec.js index e7d3fe6736f..3a589046230 100644 --- a/spec/javascripts/spec.js +++ b/spec/javascripts/spec.js @@ -22,6 +22,7 @@ //= require ../../app/assets/javascripts/locales/i18n //= require ../../app/assets/javascripts/discourse/helpers/i18n_helpers +//= require ../../app/assets/javascripts/locales/en //= require ../../app/assets/javascripts/discourse // Stuff we need to load first