diff --git a/app/assets/javascripts/discourse/controllers/composer.js.es6 b/app/assets/javascripts/discourse/controllers/composer.js.es6 index bc1764c4420..3f52b692ecd 100644 --- a/app/assets/javascripts/discourse/controllers/composer.js.es6 +++ b/app/assets/javascripts/discourse/controllers/composer.js.es6 @@ -53,7 +53,8 @@ function loadDraft(store, opts) { composerTime: draft.composerTime, typingTime: draft.typingTime, whisper: draft.whisper, - tags: draft.tags + tags: draft.tags, + noBump: draft.noBump }); return composer; } @@ -194,6 +195,13 @@ export default Ember.Controller.extend({ } }, + @computed("model.noBump") + topicBumpText(noBump) { + if (noBump) { + return I18n.t("composer.no_topic_bump"); + } + }, + @computed isStaffUser() { const currentUser = this.currentUser; diff --git a/app/assets/javascripts/discourse/models/composer.js.es6 b/app/assets/javascripts/discourse/models/composer.js.es6 index 24f4d8fb794..483bb66eef7 100644 --- a/app/assets/javascripts/discourse/models/composer.js.es6 +++ b/app/assets/javascripts/discourse/models/composer.js.es6 @@ -41,7 +41,8 @@ const CLOSED = "closed", composer_open_duration_msecs: "composerTime", tags: "tags", featured_link: "featuredLink", - shared_draft: "sharedDraft" + shared_draft: "sharedDraft", + no_bump: "noBump" }, _edit_topic_serializer = { title: "topic.title", @@ -71,6 +72,7 @@ const SAVE_ICONS = { const Composer = RestModel.extend({ _categoryId: null, unlistTopic: false, + noBump: false, archetypes: function() { return this.site.get("archetypes"); @@ -608,7 +610,8 @@ const Composer = RestModel.extend({ composerTotalOpened: opts.composerTime, typingTime: opts.typingTime, whisper: opts.whisper, - tags: opts.tags + tags: opts.tags, + noBump: opts.noBump }); if (opts.post) { @@ -714,7 +717,8 @@ const Composer = RestModel.extend({ typingTime: 0, composerOpened: null, composerTotalOpened: 0, - featuredLink: null + featuredLink: null, + noBump: false }); }, @@ -964,7 +968,8 @@ const Composer = RestModel.extend({ usernames: this.get("targetUsernames"), composerTime: this.get("composerTime"), typingTime: this.get("typingTime"), - tags: this.get("tags") + tags: this.get("tags"), + noBump: this.get("noBump") }; this.set("draftStatus", I18n.t("composer.saving_draft_tip")); diff --git a/app/assets/javascripts/discourse/templates/composer.hbs b/app/assets/javascripts/discourse/templates/composer.hbs index e5e9033e7bd..712dd870188 100644 --- a/app/assets/javascripts/discourse/templates/composer.hbs +++ b/app/assets/javascripts/discourse/templates/composer.hbs @@ -21,6 +21,9 @@ {{#if whisperOrUnlistTopicText}} ({{whisperOrUnlistTopicText}}) {{/if}} + {{#if topicBumpText}} + {{topicBumpText}} + {{/if}} {{/unless}} {{#if canEdit}} @@ -120,6 +123,11 @@ {{d-icon "eye-slash"}} {{/if}} + {{#if topicBumpText}} + + {{d-icon "anchor"}} + + {{/if}} {{/if}} diff --git a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 index 9072e38e926..866a9750edc 100644 --- a/app/assets/javascripts/select-kit/components/composer-actions.js.es6 +++ b/app/assets/javascripts/select-kit/components/composer-actions.js.es6 @@ -192,6 +192,17 @@ export default DropdownSelectBoxComponent.extend({ }); } + const currentUser = Discourse.User.current(); + + if (action === REPLY && currentUser && currentUser.get("staff")) { + items.push({ + name: I18n.t("composer.composer_actions.toggle_topic_bump.label"), + description: I18n.t("composer.composer_actions.toggle_topic_bump.desc"), + icon: "anchor", + id: "toggle_topic_bump" + }); + } + return items; }, @@ -234,6 +245,10 @@ export default DropdownSelectBoxComponent.extend({ model.toggleProperty("whisper"); }, + toggleTopicBumpSelected(options, model) { + model.toggleProperty("noBump"); + }, + replyToTopicSelected(options) { options.action = REPLY; options.topic = _topicSnapshot; diff --git a/app/assets/stylesheets/common/base/compose.scss b/app/assets/stylesheets/common/base/compose.scss index 1d355ac9e8a..fde1550de0a 100644 --- a/app/assets/stylesheets/common/base/compose.scss +++ b/app/assets/stylesheets/common/base/compose.scss @@ -153,6 +153,7 @@ } .whisper, + .no-bump, .display-edit-reason { font-style: italic; } diff --git a/app/controllers/posts_controller.rb b/app/controllers/posts_controller.rb index b4d1eec1f50..9719d769386 100644 --- a/app/controllers/posts_controller.rb +++ b/app/controllers/posts_controller.rb @@ -653,6 +653,11 @@ class PostsController < ApplicationController result[:is_warning] = false end + if params[:no_bump] == "true" + raise Discourse::InvalidParameters.new(:no_bump) unless guardian.can_skip_bump? + result[:no_bump] = true + end + if params[:shared_draft] == 'true' raise Discourse::InvalidParameters.new(:shared_draft) unless guardian.can_create_shared_draft? diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml index 10c55376670..89ea74359ea 100644 --- a/config/locales/client.en.yml +++ b/config/locales/client.en.yml @@ -1318,6 +1318,7 @@ en: whisper: "whisper" unlist: "unlisted" blockquote_text: "Blockquote" + no_topic_bump: "(no bump)" add_warning: "This is an official warning." toggle_whisper: "Toggle Whisper" @@ -1437,6 +1438,9 @@ en: shared_draft: label: "Shared Draft" desc: "Draft a topic that will only be visible to staff" + toggle_topic_bump: + label: "Toggle topic bump" + desc: "Reply without changing the topic's bump date" notifications: tooltip: diff --git a/lib/guardian/post_guardian.rb b/lib/guardian/post_guardian.rb index 49a9688c9f8..af56156c023 100644 --- a/lib/guardian/post_guardian.rb +++ b/lib/guardian/post_guardian.rb @@ -255,4 +255,8 @@ module PostGuardian def can_unhide?(post) post.try(:hidden) && is_staff? end + + def can_skip_bump? + is_staff? + end end diff --git a/spec/requests/posts_controller_spec.rb b/spec/requests/posts_controller_spec.rb index 201b94bcfde..2e0264726f4 100644 --- a/spec/requests/posts_controller_spec.rb +++ b/spec/requests/posts_controller_spec.rb @@ -1055,6 +1055,67 @@ describe PostsController do end end end + + context "topic bump" do + shared_examples "it works" do + let(:original_bumped_at) { 1.day.ago } + let!(:topic) { Fabricate(:topic, bumped_at: original_bumped_at) } + + it "should be able to skip topic bumping" do + post "/posts.json", params: { + raw: 'this is the test content', + topic_id: topic.id, + no_bump: true + } + + expect(response.status).to eq(200) + expect(topic.reload.bumped_at).to be_within_one_second_of(original_bumped_at) + end + + it "should be able to post with topic bumping" do + post "/posts.json", params: { + raw: 'this is the test content', + topic_id: topic.id + } + + expect(response.status).to eq(200) + expect(topic.reload.bumped_at).to eq(topic.posts.last.created_at) + end + end + + context "admins" do + before do + sign_in(Fabricate(:admin)) + end + + include_examples "it works" + end + + context "moderators" do + before do + sign_in(Fabricate(:moderator)) + end + + include_examples "it works" + end + + context "users" do + let(:topic) { Fabricate(:topic) } + + [:user, :trust_level_4].each do |user| + it "will raise an error for #{user}" do + sign_in(Fabricate(user)) + post "/posts.json", params: { + raw: 'this is the test content', + topic_id: topic.id, + no_bump: true + } + expect(response.status).to eq(400) + end + end + end + end + end describe '#revisions' do @@ -1524,5 +1585,4 @@ describe PostsController do expect(public_post).not_to be_locked end end - end diff --git a/test/javascripts/acceptance/composer-actions-test.js.es6 b/test/javascripts/acceptance/composer-actions-test.js.es6 index d6b2bfd1272..7a104fb0a3c 100644 --- a/test/javascripts/acceptance/composer-actions-test.js.es6 +++ b/test/javascripts/acceptance/composer-actions-test.js.es6 @@ -1,4 +1,4 @@ -import { acceptance } from "helpers/qunit-helpers"; +import { acceptance, replaceCurrentUser } from "helpers/qunit-helpers"; import { _clearSnapshots } from "select-kit/components/composer-actions"; acceptance("Composer Actions", { @@ -25,7 +25,8 @@ QUnit.test("replying to post", async assert => { ); assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); - assert.equal(composerActions.rowByIndex(4).value(), undefined); + assert.equal(composerActions.rowByIndex(4).value(), "toggle_topic_bump"); + assert.equal(composerActions.rowByIndex(5).value(), undefined); }); QUnit.test("replying to post - reply_as_private_message", async assert => { @@ -179,7 +180,8 @@ QUnit.test("interactions", async assert => { "reply_as_private_message" ); assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); - assert.equal(composerActions.rows().length, 4); + assert.equal(composerActions.rowByIndex(4).value(), "toggle_topic_bump"); + assert.equal(composerActions.rows().length, 5); await composerActions.selectRowByValue("reply_to_post"); await composerActions.expand(); @@ -199,7 +201,8 @@ QUnit.test("interactions", async assert => { ); assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); assert.equal(composerActions.rowByIndex(3).value(), "toggle_whisper"); - assert.equal(composerActions.rows().length, 4); + assert.equal(composerActions.rowByIndex(4).value(), "toggle_topic_bump"); + assert.equal(composerActions.rows().length, 5); await composerActions.selectRowByValue("reply_as_new_topic"); await composerActions.expand(); @@ -243,3 +246,62 @@ QUnit.test("interactions", async assert => { assert.equal(composerActions.rowByIndex(2).value(), "reply_to_topic"); assert.equal(composerActions.rows().length, 3); }); + +QUnit.test("replying to post - toggle_topic_bump", async assert => { + const composerActions = selectKit(".composer-actions"); + + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + + assert.ok( + find(".composer-fields .no-bump").length === 0, + "no-bump text is not visible" + ); + + await composerActions.expand(); + await composerActions.selectRowByValue("toggle_topic_bump"); + + assert.equal( + find(".composer-fields .no-bump").text(), + I18n.t("composer.no_topic_bump"), + "no-bump text is visible" + ); + + await composerActions.expand(); + await composerActions.selectRowByValue("toggle_topic_bump"); + + assert.ok( + find(".composer-fields .no-bump").length === 0, + "no-bump text is not visible" + ); +}); + +QUnit.test("replying to post as staff", async assert => { + const composerActions = selectKit(".composer-actions"); + + replaceCurrentUser({ staff: true, admin: false }); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + await composerActions.expand(); + + assert.equal(composerActions.rows().length, 5); + assert.equal(composerActions.rowByIndex(4).value(), "toggle_topic_bump"); +}); + +QUnit.test("replying to post as regular user", async assert => { + const composerActions = selectKit(".composer-actions"); + + replaceCurrentUser({ staff: false, admin: false }); + await visit("/t/internationalization-localization/280"); + await click("article#post_3 button.reply"); + await composerActions.expand(); + + assert.equal(composerActions.rows().length, 3); + Array.from(composerActions.rows()).forEach(row => { + assert.notEqual( + row.value, + "toggle_topic_bump", + "toggle button is not visible" + ); + }); +});