diff --git a/app/assets/javascripts/discourse/app/components/composer-messages.js b/app/assets/javascripts/discourse/app/components/composer-messages.js
index 3273da6b16c..e3fbca18589 100644
--- a/app/assets/javascripts/discourse/app/components/composer-messages.js
+++ b/app/assets/javascripts/discourse/app/components/composer-messages.js
@@ -4,6 +4,7 @@ import EmberObject from "@ember/object";
import { scheduleOnce } from "@ember/runloop";
import Component from "@ember/component";
import LinkLookup from "discourse/lib/link-lookup";
+import { durationTextFromSeconds } from "discourse/helpers/slow-mode";
let _messagesCache = {};
@@ -116,6 +117,21 @@ export default Component.extend({
}
}
+ const topic = composer.topic;
+ if (topic && topic.slow_mode_seconds) {
+ const msg = composer.store.createRecord("composer-message", {
+ id: "slow-mode-enabled",
+ extraClass: "custom-body",
+ templateName: "custom-body",
+ title: I18n.t("composer.slow_mode.title"),
+ body: I18n.t("composer.slow_mode.body", {
+ duration: durationTextFromSeconds(topic.slow_mode_seconds),
+ }),
+ });
+
+ this.send("popup", msg);
+ }
+
this.queuedForTyping.forEach((msg) => this.send("popup", msg));
},
diff --git a/app/assets/javascripts/discourse/app/components/slow-mode-info.js b/app/assets/javascripts/discourse/app/components/slow-mode-info.js
new file mode 100644
index 00000000000..bcb4278786a
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/components/slow-mode-info.js
@@ -0,0 +1,25 @@
+import { durationTextFromSeconds } from "discourse/helpers/slow-mode";
+import Component from "@ember/component";
+import discourseComputed from "discourse-common/utils/decorators";
+import Topic from "discourse/models/topic";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { action } from "@ember/object";
+
+export default Component.extend({
+ @discourseComputed("topic.slow_mode_seconds")
+ durationText(seconds) {
+ return durationTextFromSeconds(seconds);
+ },
+
+ @discourseComputed("topic.slow_mode_seconds", "topic.closed")
+ showSlowModeNotice(seconds, closed) {
+ return seconds > 0 && !closed;
+ },
+
+ @action
+ disableSlowMode() {
+ Topic.setSlowMode(this.topic.id, 0)
+ .catch(popupAjaxError)
+ .then(() => this.set("topic.slow_mode_seconds", 0));
+ },
+});
diff --git a/app/assets/javascripts/discourse/app/controllers/edit-slow-mode.js b/app/assets/javascripts/discourse/app/controllers/edit-slow-mode.js
new file mode 100644
index 00000000000..5460fd27ae1
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/controllers/edit-slow-mode.js
@@ -0,0 +1,112 @@
+import Controller from "@ember/controller";
+import ModalFunctionality from "discourse/mixins/modal-functionality";
+import discourseComputed from "discourse-common/utils/decorators";
+import I18n from "I18n";
+import Topic from "discourse/models/topic";
+import { fromSeconds, toSeconds } from "discourse/helpers/slow-mode";
+import { popupAjaxError } from "discourse/lib/ajax-error";
+import { equal } from "@ember/object/computed";
+import { action } from "@ember/object";
+
+export default Controller.extend(ModalFunctionality, {
+ selectedSlowMode: null,
+ hours: null,
+ minutes: null,
+ seconds: null,
+ saveDisabled: false,
+ showCustomSelect: equal("selectedSlowMode", "custom"),
+
+ init() {
+ this._super(...arguments);
+
+ this.set("slowModes", [
+ {
+ id: "900",
+ name: I18n.t("topic.slow_mode_update.durations.15_minutes"),
+ },
+ {
+ id: "3600",
+ name: I18n.t("topic.slow_mode_update.durations.1_hour"),
+ },
+ {
+ id: "14400",
+ name: I18n.t("topic.slow_mode_update.durations.4_hours"),
+ },
+ {
+ id: "86400",
+ name: I18n.t("topic.slow_mode_update.durations.1_day"),
+ },
+ {
+ id: "604800",
+ name: I18n.t("topic.slow_mode_update.durations.1_week"),
+ },
+ {
+ id: "custom",
+ name: I18n.t("topic.slow_mode_update.durations.custom"),
+ },
+ ]);
+ },
+
+ onShow() {
+ const currentDuration = parseInt(this.model.slow_mode_seconds, 10);
+
+ if (currentDuration) {
+ const selectedDuration = this.slowModes.find((mode) => {
+ return mode.id === currentDuration.toString();
+ });
+
+ if (selectedDuration) {
+ this.set("selectedSlowMode", currentDuration.toString());
+ } else {
+ this.set("selectedSlowMode", "custom");
+ }
+
+ this._setFromSeconds(currentDuration);
+ }
+ },
+
+ @discourseComputed("hours", "minutes", "seconds")
+ submitDisabled(hours, minutes, seconds) {
+ return this.saveDisabled || !(hours || minutes || seconds);
+ },
+
+ _setFromSeconds(seconds) {
+ this.setProperties(fromSeconds(seconds));
+ },
+
+ @action
+ setSlowModeDuration(duration) {
+ if (duration !== "custom") {
+ let seconds = parseInt(duration, 10);
+
+ this._setFromSeconds(seconds);
+ }
+
+ this.set("selectedSlowMode", duration);
+ },
+
+ @action
+ enableSlowMode() {
+ this.set("saveDisabled", true);
+ const seconds = toSeconds(this.hours, this.minutes, this.seconds);
+ Topic.setSlowMode(this.model.id, seconds)
+ .catch(popupAjaxError)
+ .then(() => {
+ this.set("model.slow_mode_seconds", seconds);
+ this.send("closeModal");
+ })
+ .finally(() => this.set("saveDisabled", false));
+ },
+
+ @action
+ disableSlowMode() {
+ this.set("saveDisabled", true);
+ Topic.setSlowMode(this.model.id, 0)
+ .catch(popupAjaxError)
+ .then(() => {
+ this.set("model.slow_mode_seconds", 0);
+ this.send("closeModal");
+ })
+ .finally(() => this.set("saveDisabled", false));
+ },
+});
diff --git a/app/assets/javascripts/discourse/app/helpers/slow-mode.js b/app/assets/javascripts/discourse/app/helpers/slow-mode.js
new file mode 100644
index 00000000000..fa37871788d
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/helpers/slow-mode.js
@@ -0,0 +1,30 @@
+export function fromSeconds(seconds) {
+ let initialSeconds = seconds;
+
+ let hours = initialSeconds / 3600;
+ if (hours >= 1) {
+ initialSeconds = initialSeconds - 3600 * hours;
+ } else {
+ hours = 0;
+ }
+
+ let minutes = initialSeconds / 60;
+ if (minutes >= 1) {
+ initialSeconds = initialSeconds - 60 * minutes;
+ } else {
+ minutes = 0;
+ }
+
+ return { hours, minutes, seconds: initialSeconds };
+}
+
+export function toSeconds(hours, minutes, seconds) {
+ const hoursAsSeconds = parseInt(hours, 10) * 60 * 60;
+ const minutesAsSeconds = parseInt(minutes, 10) * 60;
+
+ return parseInt(seconds, 10) + hoursAsSeconds + minutesAsSeconds;
+}
+
+export function durationTextFromSeconds(seconds) {
+ return moment.duration(seconds, "seconds").humanize();
+}
diff --git a/app/assets/javascripts/discourse/app/models/topic.js b/app/assets/javascripts/discourse/app/models/topic.js
index 13ab5fae0b0..ed9c2634e09 100644
--- a/app/assets/javascripts/discourse/app/models/topic.js
+++ b/app/assets/javascripts/discourse/app/models/topic.js
@@ -845,6 +845,11 @@ Topic.reopenClass({
idForSlug(slug) {
return ajax(`/t/id_for/${slug}`);
},
+
+ setSlowMode(topicId, seconds) {
+ const data = { seconds };
+ return ajax(`/t/${topicId}/slow_mode`, { type: "PUT", data });
+ },
});
function moveResult(result) {
diff --git a/app/assets/javascripts/discourse/app/routes/topic.js b/app/assets/javascripts/discourse/app/routes/topic.js
index 28d61612353..8e08d53c499 100644
--- a/app/assets/javascripts/discourse/app/routes/topic.js
+++ b/app/assets/javascripts/discourse/app/routes/topic.js
@@ -118,6 +118,12 @@ const TopicRoute = DiscourseRoute.extend({
this.controllerFor("modal").set("modalClass", "edit-topic-timer-modal");
},
+ showTopicSlowModeUpdate() {
+ const model = this.modelFor("topic");
+
+ showModal("edit-slow-mode", { model });
+ },
+
showChangeTimestamp() {
showModal("change-timestamp", {
model: this.modelFor("topic"),
diff --git a/app/assets/javascripts/discourse/app/templates/components/slow-mode-info.hbs b/app/assets/javascripts/discourse/app/templates/components/slow-mode-info.hbs
new file mode 100644
index 00000000000..88103992400
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/components/slow-mode-info.hbs
@@ -0,0 +1,14 @@
+{{#if showSlowModeNotice}}
+
+
+
+ {{d-icon "hourglass-end"}}
+ {{i18n "topic.slow_mode_notice.duration" duration=durationText}}
+
+
+ {{d-button class="slow-mode-remove"
+ action=(action "disableSlowMode")
+ icon="trash-alt"}}
+
+
+{{/if}}
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/templates/components/topic-footer-buttons.hbs b/app/assets/javascripts/discourse/app/templates/components/topic-footer-buttons.hbs
index aa5b44a7163..e92d28c8928 100644
--- a/app/assets/javascripts/discourse/app/templates/components/topic-footer-buttons.hbs
+++ b/app/assets/javascripts/discourse/app/templates/components/topic-footer-buttons.hbs
@@ -3,6 +3,7 @@
topic=topic
openUpwards="true"
toggleMultiSelect=toggleMultiSelect
+ showTopicSlowModeUpdate=showTopicSlowModeUpdate
deleteTopic=deleteTopic
recoverTopic=recoverTopic
toggleFeaturedOnProfile=toggleFeaturedOnProfile
diff --git a/app/assets/javascripts/discourse/app/templates/modal/edit-slow-mode.hbs b/app/assets/javascripts/discourse/app/templates/modal/edit-slow-mode.hbs
new file mode 100644
index 00000000000..17d751bd030
--- /dev/null
+++ b/app/assets/javascripts/discourse/app/templates/modal/edit-slow-mode.hbs
@@ -0,0 +1,48 @@
+{{#d-modal-body title="topic.slow_mode_update.title" autoFocus=false}}
+
+
+
+
+
+
+ {{combo-box
+ class="slow-mode-type"
+ content=slowModes
+ value=selectedSlowMode
+ onChange=(action "setSlowModeDuration")
+ }}
+
+
+ {{#if showCustomSelect}}
+
+ {{d-icon "hourglass-end"}}
+ {{input value=hours type="number" class="input-small" placeholder=(i18n "topic.slow_mode_update.hours")}}
+ {{input value=minutes type="number" class="input-small" placeholder=(i18n "topic.slow_mode_update.minutes")}}
+ {{input value=seconds type="number" class="input-small" placeholder=(i18n "topic.slow_mode_update.seconds")}}
+
+ {{/if}}
+
+ {{#if model.slow_mode_seconds}}
+
+
+ {{i18n "topic.slow_mode_update.current" hours=hours minutes=minutes seconds=seconds}}
+
+
+ {{/if}}
+{{/d-modal-body}}
+
+
\ No newline at end of file
diff --git a/app/assets/javascripts/discourse/app/templates/topic.hbs b/app/assets/javascripts/discourse/app/templates/topic.hbs
index af022006355..0490f39b162 100644
--- a/app/assets/javascripts/discourse/app/templates/topic.hbs
+++ b/app/assets/javascripts/discourse/app/templates/topic.hbs
@@ -145,6 +145,7 @@
jumpToIndex=(action "jumpToIndex")
replyToPost=(action "replyToPost")
toggleMultiSelect=(action "toggleMultiSelect")
+ showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
@@ -168,6 +169,7 @@
openUpwards="true"
rightSide="true"
toggleMultiSelect=(action "toggleMultiSelect")
+ showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
@@ -286,6 +288,8 @@
{{/if}}
+ {{slow-mode-info topic=model user=currentUser}}
+
{{topic-timer-info
topicClosed=model.closed
statusType=model.topic_timer.status_type
@@ -305,6 +309,7 @@
{{topic-footer-buttons
topic=model
toggleMultiSelect=(action "toggleMultiSelect")
+ showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
deleteTopic=(action "deleteTopic")
recoverTopic=(action "recoverTopic")
toggleClosed=(action "toggleClosed")
diff --git a/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js b/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
index 143c28cb37a..0c7e3c4fe46 100644
--- a/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
+++ b/app/assets/javascripts/discourse/app/widgets/topic-admin-menu.js
@@ -164,6 +164,14 @@ export default createWidget("topic-admin-menu", {
});
}
+ this.addActionButton({
+ className: "topic-admin-slow-mode",
+ buttonClass: "popup-menu-btn",
+ action: "showTopicSlowModeUpdate",
+ icon: "hourglass-end",
+ label: "actions.slow_mode",
+ });
+
if (topic.get("deleted") && details.get("can_recover")) {
this.addActionButton({
className: "topic-admin-recover",
diff --git a/app/assets/stylesheets/common/base/modal.scss b/app/assets/stylesheets/common/base/modal.scss
index fc09f8990d1..f2c51704031 100644
--- a/app/assets/stylesheets/common/base/modal.scss
+++ b/app/assets/stylesheets/common/base/modal.scss
@@ -811,3 +811,17 @@
}
}
}
+
+.modal.edit-slow-mode-modal {
+ .slow-mode-label {
+ display: inline-flex;
+ }
+
+ .alert.alert-info {
+ margin-bottom: 0;
+ }
+
+ .input-small {
+ width: 15%;
+ }
+}
diff --git a/app/assets/stylesheets/desktop/topic.scss b/app/assets/stylesheets/desktop/topic.scss
index 101bef0910f..3c175e4e2cc 100644
--- a/app/assets/stylesheets/desktop/topic.scss
+++ b/app/assets/stylesheets/desktop/topic.scss
@@ -63,12 +63,14 @@
border-top: 1px solid var(--primary-low);
padding: 10px 0;
max-width: 758px;
- .topic-timer-heading {
+ .topic-timer-heading,
+ .slow-mode-heading {
display: flex;
align-items: center;
margin: 0px;
}
- .topic-timer-remove {
+ .topic-timer-remove,
+ .slow-mode-remove {
font-size: $font-down-2;
background: transparent;
margin-left: auto;
diff --git a/app/controllers/topics_controller.rb b/app/controllers/topics_controller.rb
index 9b453855b79..628903ed8de 100644
--- a/app/controllers/topics_controller.rb
+++ b/app/controllers/topics_controller.rb
@@ -28,7 +28,8 @@ class TopicsController < ApplicationController
:convert_topic,
:bookmark,
:publish,
- :reset_bump_date
+ :reset_bump_date,
+ :set_slow_mode
]
before_action :consider_user_for_promotion, only: :show
@@ -932,6 +933,15 @@ class TopicsController < ApplicationController
render body: nil
end
+ def set_slow_mode
+ topic = Topic.find(params[:topic_id])
+
+ guardian.ensure_can_moderate!(topic)
+ topic.update!(slow_mode_seconds: params[:seconds])
+
+ head :ok
+ end
+
private
def topic_params
diff --git a/app/models/topic.rb b/app/models/topic.rb
index 558c15417e5..976c1996a24 100644
--- a/app/models/topic.rb
+++ b/app/models/topic.rb
@@ -1770,6 +1770,7 @@ end
# archetype :string default("regular"), not null
# featured_user4_id :integer
# notify_moderators_count :integer default(0), not null
+# slow_mode_seconds :integer default(0), not null
# spam_count :integer default(0), not null
# pinned_at :datetime
# score :float
diff --git a/app/models/topic_user.rb b/app/models/topic_user.rb
index 509db6ed386..ddd5226298f 100644
--- a/app/models/topic_user.rb
+++ b/app/models/topic_user.rb
@@ -495,6 +495,7 @@ end
# posted :boolean default(FALSE), not null
# last_read_post_number :integer
# highest_seen_post_number :integer
+# last_posted_at :datetime
# last_visited_at :datetime
# first_visited_at :datetime
# notification_level :integer default(1), not null
diff --git a/app/serializers/topic_view_serializer.rb b/app/serializers/topic_view_serializer.rb
index 7c6bd0bbef9..4587164fd24 100644
--- a/app/serializers/topic_view_serializer.rb
+++ b/app/serializers/topic_view_serializer.rb
@@ -40,7 +40,8 @@ class TopicViewSerializer < ApplicationSerializer
:pinned_globally,
:pinned_at,
:pinned_until,
- :image_url
+ :image_url,
+ :slow_mode_seconds
)
attributes(
@@ -72,7 +73,8 @@ class TopicViewSerializer < ApplicationSerializer
:queued_posts_count,
:show_read_indicator,
:requested_group_name,
- :thumbnails
+ :thumbnails,
+ :user_last_posted_at
)
has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects
@@ -280,4 +282,12 @@ class TopicViewSerializer < ApplicationSerializer
extra_sizes = ThemeModifierHelper.new(request: scope.request).topic_thumbnail_sizes
object.topic.thumbnail_info(enqueue_if_missing: true, extra_sizes: extra_sizes)
end
+
+ def user_last_posted_at
+ object.topic_user.last_posted_at
+ end
+
+ def include_user_last_posted_at?
+ object.topic.slow_mode_seconds.to_i > 0
+ end
end
diff --git a/app/serializers/web_hook_topic_view_serializer.rb b/app/serializers/web_hook_topic_view_serializer.rb
index 849250bc1ca..cadd50ebaf7 100644
--- a/app/serializers/web_hook_topic_view_serializer.rb
+++ b/app/serializers/web_hook_topic_view_serializer.rb
@@ -20,6 +20,7 @@ class WebHookTopicViewSerializer < TopicViewSerializer
topic_timer
details
image_url
+ slow_mode_seconds
}.each do |attr|
define_method("include_#{attr}?") do
false
diff --git a/config/locales/client.en.yml b/config/locales/client.en.yml
index d57c8f6ddce..96373937460 100644
--- a/config/locales/client.en.yml
+++ b/config/locales/client.en.yml
@@ -1922,6 +1922,9 @@ en:
yourself_confirm:
title: "Did you forget to add recipients?"
body: "Right now this message is only being sent to yourself!"
+ slow_mode:
+ title: "This topic is in slow mode."
+ body: "After submitting a post, you'll need to wait %{duration} before being able to post again."
admin_options_title: "Optional staff settings for this topic"
@@ -2313,6 +2316,25 @@ en:
jump_reply_down: jump to later reply
deleted: "The topic has been deleted"
+ slow_mode_update:
+ title: "Slow Mode"
+ select: "Duration:"
+ description: "Users will have to wait to be able to post again."
+ current: "Current duration is %{hours} hours, %{minutes} minutes, and %{seconds} seconds."
+ save: "Save"
+ remove: "Disable"
+ hours: "Hours"
+ minutes: "Minutes"
+ seconds: "Seconds"
+ durations:
+ 15_minutes: "15 Minutes"
+ 1_hour: "1 Hour"
+ 4_hours: "4 Hours"
+ 1_day: "1 Day"
+ 1_week: "1 Week"
+ custom: "Pick Duration"
+ slow_mode_notice:
+ duration: "You need to wait %{duration} between posts in this topic"
topic_status_update:
title: "Topic Timer"
save: "Set Timer"
@@ -2447,6 +2469,7 @@ en:
open: "Open Topic"
close: "Close Topic"
multi_select: "Select Posts…"
+ slow_mode: "Set Slow Mode"
timed_update: "Set Topic Timer..."
pin: "Pin Topic…"
unpin: "Un-Pin Topic…"
diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml
index 736749eb660..a15abb7c68a 100644
--- a/config/locales/server.en.yml
+++ b/config/locales/server.en.yml
@@ -340,6 +340,7 @@ en:
removed_direct_reply_full_quotes: "Automatically removed quote of whole previous post."
secure_upload_not_allowed_in_public_topic: "Sorry, the following secure upload(s) cannot be used in a public topic: %{upload_filenames}."
create_pm_on_existing_topic: "Sorry, you can't create a PM on an existing topic."
+ slow_mode_enabled: "You recently posted on this topic, which is in slow mode. Please wait so other users can have their chance to participate."
just_posted_that: "is too similar to what you recently posted"
invalid_characters: "contains invalid characters"
diff --git a/config/routes.rb b/config/routes.rb
index 0c5e28b4241..8da5504df0e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -804,6 +804,7 @@ Discourse::Application.routes.draw do
put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ }
put "t/:topic_id/remove_bookmarks" => "topics#remove_bookmarks", constraints: { topic_id: /\d+/ }
put "t/:topic_id/tags" => "topics#update_tags", constraints: { topic_id: /\d+/ }
+ put "t/:topic_id/slow_mode" => "topics#set_slow_mode", constraints: { topic_id: /\d+/ }
post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ }
diff --git a/db/migrate/20201005165544_add_topic_slow_mode_interval.rb b/db/migrate/20201005165544_add_topic_slow_mode_interval.rb
new file mode 100644
index 00000000000..643ba2535bc
--- /dev/null
+++ b/db/migrate/20201005165544_add_topic_slow_mode_interval.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddTopicSlowModeInterval < ActiveRecord::Migration[6.0]
+ def change
+ add_column :topics, :slow_mode_seconds, :integer, null: false, default: 0
+ end
+end
diff --git a/db/migrate/20201009190955_add_last_posted_at_to_topic_user.rb b/db/migrate/20201009190955_add_last_posted_at_to_topic_user.rb
new file mode 100644
index 00000000000..713473722a2
--- /dev/null
+++ b/db/migrate/20201009190955_add_last_posted_at_to_topic_user.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+class AddLastPostedAtToTopicUser < ActiveRecord::Migration[6.0]
+ def change
+ add_column :topic_users, :last_posted_at, :datetime
+ end
+end
diff --git a/lib/post_creator.rb b/lib/post_creator.rb
index 7c19f663ce4..3fb1324195e 100644
--- a/lib/post_creator.rb
+++ b/lib/post_creator.rb
@@ -159,6 +159,19 @@ class PostCreator
return false
end
+ if @topic&.slow_mode_seconds.to_i > 0
+ tu = TopicUser.find_by(user: @user, topic: @topic)
+
+ if tu&.last_posted_at
+ threshold = tu.last_posted_at + @topic.slow_mode_seconds.seconds
+
+ if DateTime.now < threshold
+ errors.add(:base, I18n.t(:slow_mode_enabled))
+ return false
+ end
+ end
+ end
+
unless @topic.present? && (@opts[:skip_guardian] || guardian.can_create?(Post, @topic))
errors.add(:base, I18n.t(:topic_not_found))
return false
@@ -622,7 +635,8 @@ class PostCreator
@topic.id,
posted: true,
last_read_post_number: @post.post_number,
- highest_seen_post_number: @post.post_number)
+ highest_seen_post_number: @post.post_number,
+ last_posted_at: Time.zone.now)
# assume it took us 5 seconds of reading time to make a post
PostTiming.record_timing(topic_id: @post.topic_id,
diff --git a/lib/svg_sprite/svg_sprite.rb b/lib/svg_sprite/svg_sprite.rb
index 531ef3e65d3..ffa83a3644f 100644
--- a/lib/svg_sprite/svg_sprite.rb
+++ b/lib/svg_sprite/svg_sprite.rb
@@ -130,6 +130,7 @@ module SvgSprite
"heading",
"heart",
"home",
+ "hourglass-end",
"id-card",
"info-circle",
"italic",
diff --git a/spec/components/post_creator_spec.rb b/spec/components/post_creator_spec.rb
index a624437b102..25115937717 100644
--- a/spec/components/post_creator_spec.rb
+++ b/spec/components/post_creator_spec.rb
@@ -722,6 +722,32 @@ describe PostCreator do
expect(topic.word_count).to eq(6)
end
end
+
+ context 'when the topic is in slow mode' do
+ before do
+ one_day = 86400
+ topic.update!(slow_mode_seconds: one_day)
+ end
+
+ it 'fails if the user recently posted in this topic' do
+ TopicUser.create!(user: user, topic: topic, last_posted_at: 10.minutes.ago)
+
+ post = creator.create
+
+ expect(post).to be_blank
+ expect(creator.errors.count).to eq 1
+ expect(creator.errors.messages[:base][0]).to match I18n.t(:slow_mode_enabled)
+ end
+
+ it 'creates the topic if the user last post is older than the slow mode interval' do
+ TopicUser.create!(user: user, topic: topic, last_posted_at: 5.days.ago)
+
+ post = creator.create
+
+ expect(post).to be_present
+ expect(creator.errors.count).to be_zero
+ end
+ end
end
context 'closed topic' do
@@ -1194,6 +1220,19 @@ describe PostCreator do
topic_user = TopicUser.find_by(user_id: user.id, topic_id: pm.id)
expect(topic_user.notification_level).to eq(3)
end
+
+ it 'sets the last_posted_at timestamp to track the last time the user posted' do
+ topic = Fabricate(:topic)
+
+ PostCreator.create(
+ user,
+ topic_id: topic.id,
+ raw: "this is a test reply 123 123 ;)"
+ )
+
+ topic_user = TopicUser.find_by(user_id: user.id, topic_id: topic.id)
+ expect(topic_user.last_posted_at).to be_present
+ end
end
describe '#create!' do
diff --git a/spec/requests/topics_controller_spec.rb b/spec/requests/topics_controller_spec.rb
index c9181814057..2ceeba01ad4 100644
--- a/spec/requests/topics_controller_spec.rb
+++ b/spec/requests/topics_controller_spec.rb
@@ -3047,6 +3047,58 @@ RSpec.describe TopicsController do
end
end
+ describe '#set_slow_mode' do
+ context 'when not logged in' do
+ it 'returns a forbidden response' do
+ put "/t/#{topic.id}/slow_mode.json", params: {
+ seconds: '3600'
+ }
+
+ expect(response.status).to eq(403)
+ end
+ end
+
+ context 'logged in as an admin' do
+ it 'allows admins to set the slow mode interval' do
+ sign_in(admin)
+
+ put "/t/#{topic.id}/slow_mode.json", params: {
+ seconds: '3600'
+ }
+
+ topic.reload
+ expect(response.status).to eq(200)
+ expect(topic.slow_mode_seconds).to eq(3600)
+ end
+ end
+
+ context 'logged in as a regular user' do
+ it 'does nothing if the user is not TL4' do
+ user.update!(trust_level: TrustLevel[3])
+ sign_in(user)
+
+ put "/t/#{topic.id}/slow_mode.json", params: {
+ seconds: '3600'
+ }
+
+ expect(response.status).to eq(403)
+ end
+
+ it 'allows TL4 users to set the slow mode interval' do
+ user.update!(trust_level: TrustLevel[4])
+ sign_in(user)
+
+ put "/t/#{topic.id}/slow_mode.json", params: {
+ seconds: '3600'
+ }
+
+ topic.reload
+ expect(response.status).to eq(200)
+ expect(topic.slow_mode_seconds).to eq(3600)
+ end
+ end
+ end
+
describe '#invite' do
describe 'when not logged in' do
it "should return the right response" do