diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6 index b63c2249147..e0d08665ef4 100644 --- a/app/assets/javascripts/discourse/lib/transform-post.js.es6 +++ b/app/assets/javascripts/discourse/lib/transform-post.js.es6 @@ -134,6 +134,13 @@ export default function transformPost( postAtts.topicUrl = topic.get("url"); postAtts.isSaving = post.isSaving; + if (post.post_notice_type) { + postAtts.postNoticeType = post.post_notice_type; + if (postAtts.postNoticeType === "returning") { + postAtts.postNoticeTime = new Date(post.post_notice_time); + } + } + const showPMMap = topic.archetype === "private_message" && post.post_number === 1; if (showPMMap) { diff --git a/app/assets/javascripts/discourse/widgets/post.js.es6 b/app/assets/javascripts/discourse/widgets/post.js.es6 index fbbda456aa5..956dafd3583 100644 --- a/app/assets/javascripts/discourse/widgets/post.js.es6 +++ b/app/assets/javascripts/discourse/widgets/post.js.es6 @@ -13,6 +13,7 @@ import { formatUsername } from "discourse/lib/utilities"; import hbs from "discourse/widgets/hbs-compiler"; +import { relativeAge } from "discourse/lib/formatter"; function transformWithCallbacks(post) { let transformed = transformBasicPost(post); @@ -427,6 +428,29 @@ createWidget("post-contents", { } }); +createWidget("post-notice", { + tagName: "div.post-notice", + + html(attrs) { + let text, icon; + if (attrs.postNoticeType === "first") { + icon = "hands-helping"; + text = I18n.t("post.notice.first", { user: attrs.username }); + } else if (attrs.postNoticeType === "returning") { + icon = "far-smile"; + text = I18n.t("post.notice.return", { + user: attrs.username, + time: relativeAge(attrs.postNoticeTime, { + format: "tiny", + addAgo: true + }) + }); + } + + return h("p", [iconNode(icon), text]); + } +}); + createWidget("post-body", { tagName: "div.topic-body.clearfix", @@ -505,6 +529,10 @@ createWidget("post-article", { ); } + if (attrs.postNoticeType) { + rows.push(h("div.row", [this.attach("post-notice", attrs)])); + } + rows.push( h("div.row", [ this.attach("post-avatar", attrs), diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss index 09247ad12b4..8f5f630cce6 100644 --- a/app/assets/stylesheets/common/base/topic-post.scss +++ b/app/assets/stylesheets/common/base/topic-post.scss @@ -864,3 +864,22 @@ a.mention-group { margin-bottom: 1em; } } + +.post-notice { + background-color: $tertiary-low; + border-top: 1px solid $primary-low; + color: $primary; + padding: 1em; + width: calc( + #{$topic-body-width} + #{$topic-avatar-width} - #{$topic-body-width-padding} + + 3px + ); + + p { + margin: 0; + } + + .d-icon { + margin-right: 1em; + } +} diff --git a/app/models/post.rb b/app/models/post.rb index 00a4db52fa0..73f52ecb6eb 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -194,6 +194,7 @@ class Post < ActiveRecord::Base def recover! super update_flagged_posts_count + delete_post_notices recover_public_post_actions TopicLink.extract_from(self) QuotedPost.extract_from(self) @@ -381,6 +382,11 @@ class Post < ActiveRecord::Base PostAction.update_flagged_posts_count end + def delete_post_notices + self.custom_fields.delete("post_notice_type") + self.custom_fields.delete("post_notice_time") + end + def recover_public_post_actions PostAction.publics .with_deleted diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb index 42686fae37a..916e2cdb6ea 100644 --- a/app/serializers/post_serializer.rb +++ b/app/serializers/post_serializer.rb @@ -70,6 +70,8 @@ class PostSerializer < BasicPostSerializer :is_auto_generated, :action_code, :action_code_who, + :post_notice_type, + :post_notice_time, :last_wiki_edit, :locked, :excerpt @@ -363,6 +365,22 @@ class PostSerializer < BasicPostSerializer include_action_code? && action_code_who.present? end + def post_notice_type + post_custom_fields["post_notice_type"] + end + + def include_post_notice_type? + post_notice_type.present? + end + + def post_notice_time + post_custom_fields["post_notice_time"] + end + + def include_post_notice_time? + post_notice_time.present? + end + def locked true end diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 6141724c631..bb1eb3917a7 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -2149,6 +2149,10 @@ en: one: "view 1 hidden reply" other: "view {{count}} hidden replies" + notice: + first: "This is the first time {{user}} has posted — let's welcome them to our community!" + return: "It's been a while since we've seen {{user}} — their last post was in {{time}}." + unread: "Post is unread" has_replies: one: "{{count}} Reply" diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index f457a08e08a..300697c5952 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -1901,6 +1901,8 @@ en: max_allowed_message_recipients: "Maximum recipients allowed in a message." watched_words_regular_expressions: "Watched words are regular expressions." + returning_users_days: "How many days should pass before a user is considered to be returning." + default_email_digest_frequency: "How often users receive summary emails by default." default_include_tl0_in_digests: "Include posts from new users in summary emails by default. Users can change this in their preferences." default_email_personal_messages: "Send an email when someone messages the user by default." diff --git a/config/site_settings.yml b/config/site_settings.yml index 627f4d5b07d..8bd22447f95 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -807,6 +807,8 @@ posting: default: false client: true shadowed_by_global: true + returning_users_days: + default: 60 email: email_time_window_mins: diff --git a/lib/post_creator.rb b/lib/post_creator.rb index d8bee4047ca..6f773199bbf 100644 --- a/lib/post_creator.rb +++ b/lib/post_creator.rb @@ -165,6 +165,7 @@ class PostCreator transaction do build_post_stats create_topic + create_post_notice save_post extract_links track_topic @@ -508,6 +509,21 @@ class PostCreator @user.update_attributes(last_posted_at: @post.created_at) end + def create_post_notice + last_post_time = Post.where(user_id: @user.id) + .order(created_at: :desc) + .limit(1) + .pluck(:created_at) + .first + + if !last_post_time + @post.custom_fields["post_notice_type"] = "first" + elsif SiteSetting.returning_users_days > 0 && last_post_time < SiteSetting.returning_users_days.days.ago + @post.custom_fields["post_notice_type"] = "returning" + @post.custom_fields["post_notice_time"] = last_post_time + end + end + def publish return if @opts[:import_mode] || @post.post_number == 1 @post.publish_change_to_clients! :created diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb index 7037c7a0e54..f6946b2e325 100644 --- a/lib/svg_sprite/svg_sprite.rb +++ b/lib/svg_sprite/svg_sprite.rb @@ -118,6 +118,7 @@ module SvgSprite "globe", "globe-americas", "hand-point-right", + "hands-helping", "heading", "heart", "home", diff --git a/lib/topic_view.rb b/lib/topic_view.rb index c40f82ff0b5..4f8c0ac3668 100644 --- a/lib/topic_view.rb +++ b/lib/topic_view.rb @@ -18,7 +18,7 @@ class TopicView end def self.default_post_custom_fields - @default_post_custom_fields ||= ["action_code_who"] + @default_post_custom_fields ||= ["action_code_who", "post_notice_type", "post_notice_time"] end def self.post_custom_fields_whitelisters diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb index 8774c6402e9..97a4960f319 100644 --- a/spec/components/post_creator_spec.rb +++ b/spec/components/post_creator_spec.rb @@ -1238,4 +1238,32 @@ describe PostCreator do end end end + + context "#create_post_notice" do + let(:user) { Fabricate(:user) } + let(:new_user) { Fabricate(:user) } + let(:returning_user) { Fabricate(:user) } + + it "generates post notices" do + # new users + post = PostCreator.create(new_user, title: "one of my first topics", raw: "one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq("first") + post = PostCreator.create(new_user, title: "another one of my first topics", raw: "another one of my first posts") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + + # returning users + SiteSetting.returning_users_days = 30 + old_post = Fabricate(:post, user: returning_user, created_at: 31.days.ago) + post = PostCreator.create(returning_user, title: "this is a returning topic", raw: "this is a post") + expect(post.custom_fields["post_notice_type"]).to eq("returning") + expect(post.custom_fields["post_notice_time"]).to eq(old_post.created_at.to_s) + end + + it "does not generate post notices" do + Fabricate(:post, user: user, created_at: 3.days.ago) + post = PostCreator.create(user, title: "this is another topic", raw: "this is my another post") + expect(post.custom_fields["post_notice_type"]).to eq(nil) + expect(post.custom_fields["post_notice_time"]).to eq(nil) + end + end end diff --git a/spec/models/post_spec.rb b/spec/models/post_spec.rb index 9ddfccfb3fd..de5c367d19b 100644 --- a/spec/models/post_spec.rb +++ b/spec/models/post_spec.rb @@ -134,6 +134,29 @@ describe Post do end end + context 'a post with notices' do + let(:post) { + post = Fabricate(:post, post_args) + post.custom_fields["post_notice_type"] = "returning" + post.custom_fields["post_notice_time"] = 1.day.ago + post + } + + before do + post.trash! + post.reload + end + + describe 'recovery' do + it 'deletes notices' do + post.recover! + + expect(post.custom_fields).not_to have_key("post_notice_type") + expect(post.custom_fields).not_to have_key("post_notice_time") + end + end + end + end describe 'flagging helpers' do diff --git a/test/javascripts/widgets/post-test.js.es6 b/test/javascripts/widgets/post-test.js.es6 index 12b5726ff14..5dc1b8ad50d 100644 --- a/test/javascripts/widgets/post-test.js.es6 +++ b/test/javascripts/widgets/post-test.js.es6 @@ -852,3 +852,22 @@ widgetTest("pm map", { assert.equal(find(".private-message-map .user").length, 1); } }); + +widgetTest("post notice", { + template: '{{mount-widget widget="post" args=args}}', + beforeEach() { + this.set("args", { + postNoticeType: "returning", + postNoticeTime: new Date("2010-01-01 12:00:00 UTC"), + username: "codinghorror" + }); + }, + test(assert) { + assert.equal( + find(".post-notice") + .text() + .trim(), + I18n.t("post.notice.return", { user: "codinghorror", time: "Jan '10" }) + ); + } +});