FEATURE: Special call-out for new / returning posters. (#7115)

FEATURE: Special call-out for new / returning posters. (#7115)

diff --git a/app/assets/javascripts/discourse/lib/transform-post.js.es6 b/app/assets/javascripts/discourse/lib/transform-post.js.es6
index b63c224..e0d0866 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 fbbda45..956dafd 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 09247ad..8f5f630 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 00a4db5..73f52ec 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 42686fa..916e2cd 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 6141724..bb1eb39 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 f457a08..300697c 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 627f4d5..8bd2244 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 d8bee40..6f77319 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 7037c7a..f6946b2 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 c40f82f..4f8c0ac 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"]

[... diff too long, it was truncated ...]

GitHub sha: 35942f7c

This 3px is kind of bothering me, can’t wee something less “magic” ?

2 Likes

Please avoid styling directly on html attribute as much as possible.

1 Like

It’ll be better to move this string to a constant so that it can be accessed in multiple places. If we’re hard-coding the string in multiple places, we’ll have to look for and change the string in multiple spots if the content ever changes.

1 Like

Same as FEATURE: Special call-out for new / returning posters. (#7115) · discourse/discourse@35942f7 · GitHub

1 Like

DEV: Replace magic values (#8398)