diff --git a/app/assets/javascripts/discourse/app/components/modal/change-post-notice.gjs b/app/assets/javascripts/discourse/app/components/modal/change-post-notice.gjs
new file mode 100644
index 00000000000..97bef5aaede
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/modal/change-post-notice.gjs
@@ -0,0 +1,110 @@
+import Component from "@glimmer/component";
+import { tracked } from "@glimmer/tracking";
+import { fn } from "@ember/helper";
+import { on } from "@ember/modifier";
+import { action } from "@ember/object";
+import { service } from "@ember/service";
+import { isEmpty } from "@ember/utils";
+import DButton from "discourse/components/d-button";
+import DModal from "discourse/components/d-modal";
+import DModalCancel from "discourse/components/d-modal-cancel";
+import withEventValue from "discourse/helpers/with-event-value";
+import { i18n } from "discourse-i18n";
+
+export default class ChangePostNoticeModal extends Component {
+ @service currentUser;
+ @tracked post = this.args.model.post;
+ @tracked notice = this.args.model.post.notice?.raw ?? "";
+ @tracked saving = false;
+
+ resolve = this.args.model.resolve;
+ reject = this.args.model.reject;
+
+ get disabled() {
+ return (
+ this.saving ||
+ isEmpty(this.notice) ||
+ this.notice === this.post.notice?.raw
+ );
+ }
+
+ @action
+ saveNotice() {
+ this.setNotice(this.notice);
+ }
+
+ @action
+ deleteNotice() {
+ this.setNotice();
+ }
+
+ @action
+ setNotice(notice) {
+ const { resolve, reject } = this;
+
+ this.saving = true;
+ this.resolve = null;
+ this.reject = null;
+
+ this.post
+ .updatePostField("notice", notice)
+ .then((response) => {
+ if (notice) {
+ return response.cooked_notice;
+ }
+ })
+ .then((cooked) => {
+ this.post.set(
+ "notice",
+ cooked
+ ? {
+ type: "custom",
+ raw: notice,
+ cooked: cooked.toString(),
+ }
+ : null
+ );
+ this.post.set("noticeCreatedByUser", this.currentUser);
+ })
+ .then(resolve, reject)
+ .finally(() => this.args.closeModal());
+ }
+
+
+
+ <:body>
+
+
+ <:footer>
+
+ {{#if @model.post.notice}}
+
+ {{/if}}
+
+
+
+
+}
diff --git a/app/assets/javascripts/discourse/app/components/modal/change-post-notice.hbs b/app/assets/javascripts/discourse/app/components/modal/change-post-notice.hbs
deleted file mode 100644
index baee022d459..00000000000
--- a/app/assets/javascripts/discourse/app/components/modal/change-post-notice.hbs
+++ /dev/null
@@ -1,30 +0,0 @@
-
- <:body>
-
-
- <:footer>
-
- {{#if @model.post.notice}}
-
- {{/if}}
-
-
-
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/components/modal/change-post-notice.js b/app/assets/javascripts/discourse/app/components/modal/change-post-notice.js
deleted file mode 100644
index 53bbd2a59c4..00000000000
--- a/app/assets/javascripts/discourse/app/components/modal/change-post-notice.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Component from "@glimmer/component";
-import { tracked } from "@glimmer/tracking";
-import { action } from "@ember/object";
-import { isEmpty } from "@ember/utils";
-import { cook } from "discourse/lib/text";
-
-export default class ChangePostNoticeModal extends Component {
- @tracked post = this.args.model.post;
- @tracked notice = this.args.model.post.notice?.raw ?? "";
- @tracked saving = false;
-
- resolve = this.args.model.resolve;
- reject = this.args.model.reject;
-
- get disabled() {
- return (
- this.saving ||
- isEmpty(this.notice) ||
- this.notice === this.post.notice?.raw
- );
- }
-
- @action
- saveNotice() {
- this.setNotice(this.notice);
- }
-
- @action
- deleteNotice() {
- this.setNotice();
- }
-
- @action
- setNotice(notice) {
- const { resolve, reject } = this;
-
- this.saving = true;
- this.resolve = null;
- this.reject = null;
-
- this.post
- .updatePostField("notice", notice)
- .then(() => {
- if (notice) {
- return cook(notice, { features: { onebox: false } });
- }
- })
- .then((cooked) =>
- this.post.set(
- "notice",
- cooked
- ? {
- type: "custom",
- raw: notice,
- cooked: cooked.toString(),
- }
- : null
- )
- )
- .then(resolve, reject)
- .finally(() => this.args.closeModal());
- }
-}
diff --git a/app/assets/javascripts/discourse/app/lib/transform-post.js b/app/assets/javascripts/discourse/app/lib/transform-post.js
index 9fb135d7b6e..219082d05dc 100644
--- a/app/assets/javascripts/discourse/app/lib/transform-post.js
+++ b/app/assets/javascripts/discourse/app/lib/transform-post.js
@@ -169,6 +169,7 @@ export default function transformPost(
if (post.notice) {
postAtts.notice = post.notice;
+ postAtts.noticeCreatedByUser = post.notice_created_by_user;
if (postAtts.notice.type === "returning_user") {
postAtts.notice.lastPostedAt = new Date(post.notice.last_posted_at);
}
diff --git a/app/assets/javascripts/discourse/app/models/post.js b/app/assets/javascripts/discourse/app/models/post.js
index e8d39cd41e4..1fa1ff4e318 100644
--- a/app/assets/javascripts/discourse/app/models/post.js
+++ b/app/assets/javascripts/discourse/app/models/post.js
@@ -232,7 +232,10 @@ export default class Post extends RestModel {
data[field] = value;
return ajax(`/posts/${this.id}/${field}`, { type: "PUT", data })
- .then(() => this.set(field, value))
+ .then((response) => {
+ this.set(field, value);
+ return response;
+ })
.catch(popupAjaxError);
}
diff --git a/app/assets/javascripts/discourse/app/widgets/post.js b/app/assets/javascripts/discourse/app/widgets/post.js
index 4385fed4fe7..23813903507 100644
--- a/app/assets/javascripts/discourse/app/widgets/post.js
+++ b/app/assets/javascripts/discourse/app/widgets/post.js
@@ -20,7 +20,11 @@ import {
import { consolePrefix } from "discourse/lib/source-identifier";
import { transformBasicPost } from "discourse/lib/transform-post";
import DiscourseURL from "discourse/lib/url";
-import { clipboardCopy, formatUsername } from "discourse/lib/utilities";
+import {
+ clipboardCopy,
+ escapeExpression,
+ formatUsername,
+} from "discourse/lib/utilities";
import DecoratorHelper from "discourse/widgets/decorator-helper";
import widgetHbs from "discourse/widgets/hbs-compiler";
import PostCooked from "discourse/widgets/post-cooked";
@@ -808,9 +812,28 @@ createWidget("post-notice", {
html(attrs) {
if (attrs.notice.type === "custom") {
+ let createdByHTML = "";
+ if (attrs.noticeCreatedByUser) {
+ const createdByName = escapeExpression(
+ prioritizeNameInUx(attrs.noticeCreatedByUser.name)
+ ? attrs.noticeCreatedByUser.name
+ : attrs.noticeCreatedByUser.username
+ );
+ createdByHTML = i18n("post.notice.custom_created_by", {
+ userLinkHTML: `${createdByName}`,
+ });
+ }
return [
iconNode("user-shield"),
- new RawHtml({ html: `
${attrs.notice.cooked}
` }),
+ new RawHtml({
+ html: `${attrs.notice.cooked} ${createdByHTML}
`,
+ }),
];
}
diff --git a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js
index 820f42b090e..df649797a97 100644
--- a/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js
+++ b/app/assets/javascripts/discourse/tests/integration/components/widgets/post-test.js
@@ -1126,6 +1126,38 @@ module("Integration | Component | Widget | post", function (hooks) {
);
});
+ test("post notice - custom official notice with created by username", async function (assert) {
+ this.siteSettings.display_name_on_posts = false;
+ this.siteSettings.prioritize_username_in_ux = true;
+ this.set("args", {
+ notice: {
+ type: "custom",
+ cooked: "This is an official notice
",
+ },
+ noticeCreatedByUser: {
+ username: "codinghorror",
+ name: "Jeff",
+ id: 1,
+ },
+ });
+
+ await render(hbs`
+ `);
+
+ assert.dom(".post-notice.custom").hasText(
+ "This is an official notice " +
+ i18n("post.notice.custom_created_by", {
+ userLinkHTML: "codinghorror",
+ })
+ );
+
+ assert
+ .dom(
+ ".post-notice.custom .post-notice-message a.trigger-user-card[data-user-card='codinghorror']"
+ )
+ .exists();
+ });
+
test("post notice - with name", async function (assert) {
this.siteSettings.display_name_on_posts = true;
this.siteSettings.prioritize_username_in_ux = false;
diff --git a/app/assets/stylesheets/common/base/topic-post.scss b/app/assets/stylesheets/common/base/topic-post.scss
index 689c6c4e5b5..089d9cb1c58 100644
--- a/app/assets/stylesheets/common/base/topic-post.scss
+++ b/app/assets/stylesheets/common/base/topic-post.scss
@@ -1432,6 +1432,10 @@ span.mention {
}
}
+ .post-notice-message p {
+ display: inline;
+ }
+
p {
margin: 0;
}
diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb
index 9dc6c8434dc..f418065abfb 100644
--- a/app/controllers/posts_controller.rb
+++ b/app/controllers/posts_controller.rb
@@ -625,10 +625,12 @@ class PostsController < ApplicationController
old_notice = post.custom_fields[Post::NOTICE]
if params[:notice].present?
+ cooked_notice = PrettyText.cook(params[:notice], features: { onebox: false })
post.custom_fields[Post::NOTICE] = {
type: Post.notices[:custom],
raw: params[:notice],
- cooked: PrettyText.cook(params[:notice], features: { onebox: false }),
+ cooked: cooked_notice,
+ created_by_user_id: current_user.id,
}
else
post.custom_fields.delete(Post::NOTICE)
@@ -642,7 +644,7 @@ class PostsController < ApplicationController
new_value: params[:notice],
)
- render body: nil
+ render json: success_json.merge(cooked_notice:)
end
def destroy_bookmark
diff --git a/app/serializers/post_serializer.rb b/app/serializers/post_serializer.rb
index 8ae1f9e5b0e..e236d2a67d7 100644
--- a/app/serializers/post_serializer.rb
+++ b/app/serializers/post_serializer.rb
@@ -11,6 +11,7 @@ class PostSerializer < BasicPostSerializer
post_actions
all_post_actions
add_excerpt
+ notice_created_by_users
]
INSTANCE_VARS.each { |v| self.public_send(:attr_accessor, v) }
@@ -82,6 +83,7 @@ class PostSerializer < BasicPostSerializer
:action_code_who,
:action_code_path,
:notice,
+ :notice_created_by_user,
:last_wiki_edit,
:locked,
:excerpt,
@@ -518,6 +520,19 @@ class PostSerializer < BasicPostSerializer
include_action_code? && action_code_path.present?
end
+ def include_notice_created_by_user?
+ scope.is_staff? && notice.present? && notice_created_by_users.present?
+ end
+
+ def notice_created_by_user
+ return if notice.blank?
+ return if notice["type"] != Post.notices[:custom]
+ return if notice["created_by_user_id"].blank?
+ found_user = notice_created_by_users&.find { |user| user.id == notice["created_by_user_id"] }
+ return if !found_user
+ BasicUserSerializer.new(found_user, root: false).as_json
+ end
+
def notice
post_custom_fields[Post::NOTICE]
end
diff --git a/app/serializers/post_stream_serializer_mixin.rb b/app/serializers/post_stream_serializer_mixin.rb
index e3cb95554ee..f6ef79a25c0 100644
--- a/app/serializers/post_stream_serializer_mixin.rb
+++ b/app/serializers/post_stream_serializer_mixin.rb
@@ -64,11 +64,25 @@ module PostStreamSerializerMixin
serializer.add_raw = true if @options[:include_raw]
serializer.topic_view = object
+ serializer.notice_created_by_users = post_notice_created_by_users if scope.is_staff?
+
serializer.as_json
end
end
end
+ def post_notice_created_by_users
+ @post_notice_created_by_users ||=
+ begin
+ user_ids =
+ object
+ .post_custom_fields
+ .filter_map { |_, custom_fields| custom_fields.dig(Post::NOTICE, "created_by_user_id") }
+ .uniq
+ User.real.where(id: user_ids)
+ end
+ end
+
def theme_modifier_helper
@theme_modifier_helper ||= ThemeModifierHelper.new(request: scope.request)
end
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index b0af5e8391a..830fd98c5f5 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -3811,6 +3811,7 @@ en:
notice:
new_user: "This is the first time %{user} has posted — let’s welcome them to our community!"
returning_user: "It’s been a while since we’ve seen %{user} — their last post was %{time}."
+ custom_created_by: "(added by %{userLinkHTML})"
unread: "Post is unread"
has_replies:
@@ -3940,9 +3941,9 @@ en:
delete_topic_confirm_modal_no: "No, keep this topic"
delete_topic_error: "An error occurred while deleting this topic"
delete_topic: "delete topic"
- add_post_notice: "Add Staff Notice…"
- change_post_notice: "Change Staff Notice…"
- delete_post_notice: "Delete Staff Notice"
+ add_post_notice: "Add Official Notice…"
+ change_post_notice: "Change Official Notice…"
+ delete_post_notice: "Delete Official Notice"
remove_timer: "remove timer"
edit_timer: "edit timer"
diff --git a/spec/requests/api/posts_spec.rb b/spec/requests/api/posts_spec.rb
index f8f14029659..a43d1562b9b 100644
--- a/spec/requests/api/posts_spec.rb
+++ b/spec/requests/api/posts_spec.rb
@@ -595,6 +595,9 @@ RSpec.describe "posts" do
notice: {
type: :object,
},
+ notice_created_by_user: {
+ type: %i[object null],
+ },
reviewable_id: {
type: %i[integer null],
},
diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb
index 972454bd0c4..33fe5bb579b 100644
--- a/spec/requests/posts_controller_spec.rb
+++ b/spec/requests/posts_controller_spec.rb
@@ -2911,6 +2911,7 @@ RSpec.describe PostsController do
"type" => Post.notices[:custom],
"raw" => raw_notice,
"cooked" => PrettyText.cook(raw_notice, features: { onebox: false }),
+ "created_by_user_id" => moderator.id,
)
expect(UserHistory.where(action: UserHistory.actions[:post_staff_note_create]).count).to eq(1)
@@ -2944,6 +2945,7 @@ RSpec.describe PostsController do
"type" => Post.notices[:custom],
"raw" => raw_notice,
"cooked" => PrettyText.cook(raw_notice, features: { onebox: false }),
+ "created_by_user_id" => user.id,
)
put "/posts/#{public_post.id}/notice.json", params: { notice: nil }
diff --git a/spec/serializers/post_serializer_spec.rb b/spec/serializers/post_serializer_spec.rb
index 49693328c81..7d077050f5a 100644
--- a/spec/serializers/post_serializer_spec.rb
+++ b/spec/serializers/post_serializer_spec.rb
@@ -224,32 +224,102 @@ RSpec.describe PostSerializer do
fab!(:user) { Fabricate(:user, trust_level: 1) }
fab!(:user_tl1) { Fabricate(:user, trust_level: 1) }
fab!(:user_tl2) { Fabricate(:user, trust_level: 2) }
+ fab!(:post) { Fabricate(:post, user: user) }
- let(:post) do
- post = Fabricate(:post, user: user)
- post.custom_fields[Post::NOTICE] = {
- type: Post.notices[:returning_user],
- last_posted_at: 1.day.ago,
- }
- post.save_custom_fields
- post
+ def json_for_user(user, serializer_opts = {})
+ serializer = PostSerializer.new(post, scope: Guardian.new(user), root: false)
+
+ if serializer_opts[:notice_created_by_users]
+ serializer.notice_created_by_users = serializer_opts[:notice_created_by_users]
+ end
+
+ serializer.as_json(serializer_opts)
end
- def json_for_user(user)
- PostSerializer.new(post, scope: Guardian.new(user), root: false).as_json
+ describe "returning_user notice" do
+ before do
+ post.custom_fields[Post::NOTICE] = {
+ type: Post.notices[:returning_user],
+ last_posted_at: 1.day.ago,
+ }
+ post.save_custom_fields
+ end
+
+ it "is visible for TL2+ users (except poster)" do
+ expect(json_for_user(nil)[:notice]).to eq(nil)
+ expect(json_for_user(user)[:notice]).to eq(nil)
+
+ SiteSetting.returning_user_notice_tl = 2
+ expect(json_for_user(user_tl1)[:notice]).to eq(nil)
+ expect(json_for_user(user_tl2)[:notice][:type]).to eq(Post.notices[:returning_user])
+
+ SiteSetting.returning_user_notice_tl = 1
+ expect(json_for_user(user_tl1)[:notice][:type]).to eq(Post.notices[:returning_user])
+ expect(json_for_user(user_tl2)[:notice][:type]).to eq(Post.notices[:returning_user])
+ end
end
- it "is visible for TL2+ users (except poster)" do
- expect(json_for_user(nil)[:notice]).to eq(nil)
- expect(json_for_user(user)[:notice]).to eq(nil)
+ describe "custom notice" do
+ fab!(:moderator)
- SiteSetting.returning_user_notice_tl = 2
- expect(json_for_user(user_tl1)[:notice]).to eq(nil)
- expect(json_for_user(user_tl2)[:notice][:type]).to eq(Post.notices[:returning_user])
+ before do
+ post.custom_fields[Post::NOTICE] = {
+ type: Post.notices[:custom],
+ raw: "This is a notice",
+ cooked: "This is a notice
",
+ created_by_user_id: moderator.id,
+ }
+ post.save_custom_fields
+ end
- SiteSetting.returning_user_notice_tl = 1
- expect(json_for_user(user_tl1)[:notice][:type]).to eq(Post.notices[:returning_user])
- expect(json_for_user(user_tl2)[:notice][:type]).to eq(Post.notices[:returning_user])
+ it "displays for all trust levels" do
+ expect(json_for_user(user)[:notice]).to eq(
+ {
+ cooked: "This is a notice
",
+ created_by_user_id: moderator.id,
+ raw: "This is a notice",
+ type: Post.notices[:custom],
+ }.with_indifferent_access,
+ )
+ expect(json_for_user(user_tl1)[:notice]).to eq(
+ {
+ cooked: "This is a notice
",
+ created_by_user_id: moderator.id,
+ raw: "This is a notice",
+ type: Post.notices[:custom],
+ }.with_indifferent_access,
+ )
+ expect(json_for_user(user_tl2)[:notice]).to eq(
+ {
+ cooked: "This is a notice
",
+ created_by_user_id: moderator.id,
+ raw: "This is a notice",
+ type: Post.notices[:custom],
+ }.with_indifferent_access,
+ )
+ end
+
+ it "only displays the created_by_user for staff" do
+ expect(
+ json_for_user(user, notice_created_by_users: [moderator])[:notice_created_by_user],
+ ).to eq(nil)
+ expect(
+ json_for_user(user_tl1, notice_created_by_users: [moderator])[:notice_created_by_user],
+ ).to eq(nil)
+ expect(
+ json_for_user(user_tl2, notice_created_by_users: [moderator])[:notice_created_by_user],
+ ).to eq(nil)
+ expect(
+ json_for_user(moderator, notice_created_by_users: [moderator])[:notice_created_by_user],
+ ).to eq(
+ {
+ id: moderator.id,
+ username: moderator.username,
+ name: moderator.name,
+ avatar_template: moderator.avatar_template,
+ },
+ )
+ end
end
end
diff --git a/spec/system/post_menu_spec.rb b/spec/system/post_menu_spec.rb
index 879a6a70d6b..243dd4b20fd 100644
--- a/spec/system/post_menu_spec.rb
+++ b/spec/system/post_menu_spec.rb
@@ -108,7 +108,7 @@ describe "Post menu", type: :system do
expect(topic_page).to have_post_action_button(post2, :admin)
end
- it "displays the admin button when the user can wiki the post / edit staff notices" do
+ it "displays the admin button when the user can wiki the post / edit official notices" do
# display the admin button when the user can wiki
sign_in(Fabricate(:trust_level_4))