From 42bd9b61998743a38cd4bd299bb0716ebccd3a02 Mon Sep 17 00:00:00 2001 From: Robin Ward Date: Thu, 18 Jun 2015 17:06:25 -0400 Subject: [PATCH] FEATURE: Show time gap between posts if more than a few days --- .../discourse/components/time-gap.js.es6 | 20 ++++++++++ .../discourse/models/post-stream.js.es6 | 30 +++++++++++++-- .../javascripts/discourse/models/post.js.es6 | 4 ++ .../templates/components/time-gap.hbs | 5 +++ .../javascripts/discourse/templates/post.hbs | 4 ++ .../stylesheets/desktop/topic-post.scss | 15 ++++++++ config/locales/client.en.yml | 10 +++++ config/locales/server.en.yml | 1 + config/site_settings.yml | 3 ++ .../models/post-stream-test.js.es6 | 38 ++++++++++++++++++- 10 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 app/assets/javascripts/discourse/components/time-gap.js.es6 create mode 100644 app/assets/javascripts/discourse/templates/components/time-gap.hbs diff --git a/app/assets/javascripts/discourse/components/time-gap.js.es6 b/app/assets/javascripts/discourse/components/time-gap.js.es6 new file mode 100644 index 000000000..fb42dc324 --- /dev/null +++ b/app/assets/javascripts/discourse/components/time-gap.js.es6 @@ -0,0 +1,20 @@ +export default Ember.Component.extend({ + classNameBindings: [':time-gap'], + + render(buffer) { + const gapDays = this.get('gapDays'); + + let timeGapWords; + if (gapDays < 30) { + timeGapWords = I18n.t('dates.later.x_days', {count: gapDays}); + } else if (gapDays < 365) { + const gapMonths = Math.floor(gapDays / 30); + timeGapWords = I18n.t('dates.later.x_months', {count: gapMonths}); + } else { + const gapYears = Math.floor(gapDays / 365); + timeGapWords = I18n.t('dates.later.x_years', {count: gapYears}); + } + + buffer.push("
" + timeGapWords + "
"); + } +}); diff --git a/app/assets/javascripts/discourse/models/post-stream.js.es6 b/app/assets/javascripts/discourse/models/post-stream.js.es6 index 61a70f5d2..e92f92f41 100644 --- a/app/assets/javascripts/discourse/models/post-stream.js.es6 +++ b/app/assets/javascripts/discourse/models/post-stream.js.es6 @@ -1,5 +1,21 @@ import RestModel from 'discourse/models/rest'; +function calcDayDiff(p1, p2) { + if (!p1) { return; } + + const date = p1.get('created_at'); + if (date) { + if (p2) { + const lastDate = p2.get('created_at'); + if (lastDate) { + const delta = new Date(date).getTime() - new Date(lastDate).getTime(); + const days = Math.round(delta / (1000 * 60 * 60 * 24)); + + p1.set('daysSincePrevious', days); + } + } + } +} const PostStream = RestModel.extend({ loading: Em.computed.or('loadingAbove', 'loadingBelow', 'loadingFilter', 'stagingPost'), notLoading: Em.computed.not('loading'), @@ -367,14 +383,22 @@ const PostStream = RestModel.extend({ }, prependPost(post) { - this.get('posts').unshiftObject(this.storePost(post)); + const stored = this.storePost(post); + if (stored) { + const posts = this.get('posts'); + calcDayDiff(posts.get('firstObject'), stored); + posts.unshiftObject(stored); + } + return post; }, appendPost(post) { const stored = this.storePost(post); if (stored) { - this.get('posts').addObject(stored); + const posts = this.get('posts'); + calcDayDiff(stored, posts.get('lastObject')); + posts.addObject(stored); } return post; }, @@ -627,7 +651,7 @@ const PostStream = RestModel.extend({ const postId = Em.get(post, 'id'); if (postId) { const postIdentityMap = this.get('postIdentityMap'), - existing = postIdentityMap.get(post.get('id')); + existing = postIdentityMap.get(post.get('id')); if (existing) { // If the post is in the identity map, update it and return the old reference. diff --git a/app/assets/javascripts/discourse/models/post.js.es6 b/app/assets/javascripts/discourse/models/post.js.es6 index ba3744ec0..9e720b36d 100644 --- a/app/assets/javascripts/discourse/models/post.js.es6 +++ b/app/assets/javascripts/discourse/models/post.js.es6 @@ -27,6 +27,10 @@ const Post = RestModel.extend({ notDeleted: Em.computed.not('deleted'), userDeleted: Em.computed.empty('user_id'), + hasTimeGap: function() { + return (this.get('daysSincePrevious') || 0) > Discourse.SiteSettings.show_time_gap_days; + }.property('daysSincePrevious'), + showName: function() { const name = this.get('name'); return name && (name !== this.get('username')) && Discourse.SiteSettings.display_name_on_posts; diff --git a/app/assets/javascripts/discourse/templates/components/time-gap.hbs b/app/assets/javascripts/discourse/templates/components/time-gap.hbs new file mode 100644 index 000000000..1667ed07c --- /dev/null +++ b/app/assets/javascripts/discourse/templates/components/time-gap.hbs @@ -0,0 +1,5 @@ +
+
+ {{gapInWords}} +
+
diff --git a/app/assets/javascripts/discourse/templates/post.hbs b/app/assets/javascripts/discourse/templates/post.hbs index bbcd12ace..60750cc29 100644 --- a/app/assets/javascripts/discourse/templates/post.hbs +++ b/app/assets/javascripts/discourse/templates/post.hbs @@ -1,5 +1,9 @@ {{post-gap post=this postStream=controller.model.postStream before="true"}} +{{#if hasTimeGap}} + {{time-gap gapDays=daysSincePrevious}} +{{/if}} +
{{view 'reply-history' content=replyHistory}}
diff --git a/app/assets/stylesheets/desktop/topic-post.scss b/app/assets/stylesheets/desktop/topic-post.scss index dedd9735c..6b6eb254e 100644 --- a/app/assets/stylesheets/desktop/topic-post.scss +++ b/app/assets/stylesheets/desktop/topic-post.scss @@ -717,6 +717,21 @@ $topic-avatar-width: 45px; width: calc(#{$topic-avatar-width} + #{$topic-body-width} + 2 * #{$topic-body-width-padding}); } +.time-gap { + width: 755px; + border-top: 1px solid dark-light-diff($primary, $secondary, 90%, -60%); +} +.time-gap-words { + display: inline-block; + padding: 0.5em 1em; + margin: 0.5em 0 0.5em 56px; + text-transform: uppercase; + font-weight: bold; + font-size: 0.8em; + background-color: dark-light-diff($primary, $secondary, 90%, -60%); + color: lighten($primary, 30%); +} + .posts-wrapper { position: relative; -webkit-font-smoothing: subpixel-antialiased; diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 501c5e589..fa4c6fbcf 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -92,6 +92,16 @@ en: x_days: one: "1 day ago" other: "%{count} days ago" + later: + x_days: + one: "1 day layer" + other: "%{count} days later" + x_months: + one: "1 month layer" + other: "%{count} months later" + x_years: + one: "1 year layer" + other: "%{count} years later" share: topic: 'share a link to this topic' post: 'post #%{postNumber}' diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index 27ac0af5e..a90c20be9 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1120,6 +1120,7 @@ en: full_name_required: "Full name is a required field of a user's profile." enable_names: "Show the user's full name on their profile, user card, and emails. Disable to hide full name everywhere." display_name_on_posts: "Show a user's full name on their posts in addition to their @username." + show_time_gap_days: "If two posts are made this many days apart, display the time gap in the topic." invites_per_page: "Default invites shown on the user page." short_progress_text_threshold: "After the number of posts in a topic goes above this number, the progress bar will only show the current post number. If you change the progress bar's width, you may need to change this value." default_code_lang: "Default programming language syntax highlighting applied to GitHub code blocks (lang-auto, ruby, python etc.)" diff --git a/config/site_settings.yml b/config/site_settings.yml index 44c4fe249..792b824ed 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -415,6 +415,9 @@ posting: display_name_on_posts: client: true default: false + show_time_gap_days: + default: 4 + client: true short_progress_text_threshold: client: true default: 10000 diff --git a/test/javascripts/models/post-stream-test.js.es6 b/test/javascripts/models/post-stream-test.js.es6 index a510c4839..74569a036 100644 --- a/test/javascripts/models/post-stream-test.js.es6 +++ b/test/javascripts/models/post-stream-test.js.es6 @@ -26,6 +26,40 @@ test('defaults', function() { present(postStream.get('topic')); }); +test('daysSincePrevious when appending', function(assert) { + const postStream = buildStream(10000001, [1,2,3]); + const store = postStream.store; + + const p1 = store.createRecord('post', {id: 1, post_number: 1, created_at: "2015-05-29T18:17:35.868Z"}), + p2 = store.createRecord('post', {id: 2, post_number: 2, created_at: "2015-06-01T01:07:25.761Z"}), + p3 = store.createRecord('post', {id: 3, post_number: 3, created_at: "2015-06-02T01:07:25.761Z"}); + + postStream.appendPost(p1); + postStream.appendPost(p2); + postStream.appendPost(p3); + + assert.ok(!p1.get('daysSincePrevious')); + assert.equal(p2.get('daysSincePrevious'), 2); + assert.equal(p3.get('daysSincePrevious'), 1); +}); + +test('daysSincePrevious when prepending', function(assert) { + const postStream = buildStream(10000001, [1,2,3]); + const store = postStream.store; + + const p1 = store.createRecord('post', {id: 1, post_number: 1, created_at: "2015-05-29T18:17:35.868Z"}), + p2 = store.createRecord('post', {id: 2, post_number: 2, created_at: "2015-06-01T01:07:25.761Z"}), + p3 = store.createRecord('post', {id: 3, post_number: 3, created_at: "2015-06-02T01:07:25.761Z"}); + + postStream.prependPost(p3); + postStream.prependPost(p2); + postStream.prependPost(p1); + + assert.ok(!p1.get('daysSincePrevious')); + assert.equal(p2.get('daysSincePrevious'), 2); + assert.equal(p3.get('daysSincePrevious'), 1); +}); + test('appending posts', function() { const postStream = buildStream(4567, [1, 3, 4]); const store = postStream.store; @@ -96,8 +130,8 @@ test("removePosts", function() { const store = postStream.store; const p1 = store.createRecord('post', {id: 1, post_number: 2}), - p2 = store.createRecord('post', {id: 2, post_number: 3}), - p3 = store.createRecord('post', {id: 3, post_number: 4}); + p2 = store.createRecord('post', {id: 2, post_number: 3}), + p3 = store.createRecord('post', {id: 3, post_number: 4}); postStream.appendPost(p1); postStream.appendPost(p2);