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"
+ );
+ });
+});