mirror of
https://github.com/discourse/discourse.git
synced 2024-11-29 00:55:06 +08:00
FEATURE: Topic slow mode. (#10904)
Adds a new slow mode for topics that are heating up. Users will have to wait for a period of time before being able to post again. We store this interval inside the topics table and track the last time a user posted using the last_posted_at datetime in the TopicUser relation.
This commit is contained in:
parent
4669e60ce5
commit
21c53ed249
|
@ -4,6 +4,7 @@ import EmberObject from "@ember/object";
|
||||||
import { scheduleOnce } from "@ember/runloop";
|
import { scheduleOnce } from "@ember/runloop";
|
||||||
import Component from "@ember/component";
|
import Component from "@ember/component";
|
||||||
import LinkLookup from "discourse/lib/link-lookup";
|
import LinkLookup from "discourse/lib/link-lookup";
|
||||||
|
import { durationTextFromSeconds } from "discourse/helpers/slow-mode";
|
||||||
|
|
||||||
let _messagesCache = {};
|
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));
|
this.queuedForTyping.forEach((msg) => this.send("popup", msg));
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|
|
@ -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));
|
||||||
|
},
|
||||||
|
});
|
|
@ -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));
|
||||||
|
},
|
||||||
|
});
|
30
app/assets/javascripts/discourse/app/helpers/slow-mode.js
Normal file
30
app/assets/javascripts/discourse/app/helpers/slow-mode.js
Normal file
|
@ -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();
|
||||||
|
}
|
|
@ -845,6 +845,11 @@ Topic.reopenClass({
|
||||||
idForSlug(slug) {
|
idForSlug(slug) {
|
||||||
return ajax(`/t/id_for/${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) {
|
function moveResult(result) {
|
||||||
|
|
|
@ -118,6 +118,12 @@ const TopicRoute = DiscourseRoute.extend({
|
||||||
this.controllerFor("modal").set("modalClass", "edit-topic-timer-modal");
|
this.controllerFor("modal").set("modalClass", "edit-topic-timer-modal");
|
||||||
},
|
},
|
||||||
|
|
||||||
|
showTopicSlowModeUpdate() {
|
||||||
|
const model = this.modelFor("topic");
|
||||||
|
|
||||||
|
showModal("edit-slow-mode", { model });
|
||||||
|
},
|
||||||
|
|
||||||
showChangeTimestamp() {
|
showChangeTimestamp() {
|
||||||
showModal("change-timestamp", {
|
showModal("change-timestamp", {
|
||||||
model: this.modelFor("topic"),
|
model: this.modelFor("topic"),
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
{{#if showSlowModeNotice}}
|
||||||
|
<div class="topic-status-info">
|
||||||
|
<h3 class="slow-mode-heading">
|
||||||
|
<span>
|
||||||
|
{{d-icon "hourglass-end"}}
|
||||||
|
{{i18n "topic.slow_mode_notice.duration" duration=durationText}}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
{{d-button class="slow-mode-remove"
|
||||||
|
action=(action "disableSlowMode")
|
||||||
|
icon="trash-alt"}}
|
||||||
|
</h3>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
|
@ -3,6 +3,7 @@
|
||||||
topic=topic
|
topic=topic
|
||||||
openUpwards="true"
|
openUpwards="true"
|
||||||
toggleMultiSelect=toggleMultiSelect
|
toggleMultiSelect=toggleMultiSelect
|
||||||
|
showTopicSlowModeUpdate=showTopicSlowModeUpdate
|
||||||
deleteTopic=deleteTopic
|
deleteTopic=deleteTopic
|
||||||
recoverTopic=recoverTopic
|
recoverTopic=recoverTopic
|
||||||
toggleFeaturedOnProfile=toggleFeaturedOnProfile
|
toggleFeaturedOnProfile=toggleFeaturedOnProfile
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
{{#d-modal-body title="topic.slow_mode_update.title" autoFocus=false}}
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="slow-mode-label">{{i18n "topic.slow_mode_update.description"}}</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="control-group">
|
||||||
|
<label class="slow-mode-label">{{i18n "topic.slow_mode_update.select"}}</label>
|
||||||
|
{{combo-box
|
||||||
|
class="slow-mode-type"
|
||||||
|
content=slowModes
|
||||||
|
value=selectedSlowMode
|
||||||
|
onChange=(action "setSlowModeDuration")
|
||||||
|
}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{{#if showCustomSelect}}
|
||||||
|
<div class="control-group">
|
||||||
|
{{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")}}
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
{{#if model.slow_mode_seconds}}
|
||||||
|
<div class="alert alert-info">
|
||||||
|
<b>
|
||||||
|
{{i18n "topic.slow_mode_update.current" hours=hours minutes=minutes seconds=seconds}}
|
||||||
|
</b>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
{{/d-modal-body}}
|
||||||
|
|
||||||
|
<div class="modal-footer">
|
||||||
|
{{d-button class="btn-primary"
|
||||||
|
disabled=submitDisabled
|
||||||
|
label="topic.slow_mode_update.save"
|
||||||
|
action=(action "enableSlowMode")}}
|
||||||
|
|
||||||
|
{{conditional-loading-spinner size="small" condition=loading}}
|
||||||
|
|
||||||
|
{{#if model.slow_mode_seconds}}
|
||||||
|
{{d-button class="btn-danger"
|
||||||
|
action=(action "disableSlowMode")
|
||||||
|
disabled=submitDisabled
|
||||||
|
label="topic.slow_mode_update.remove"}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
|
@ -145,6 +145,7 @@
|
||||||
jumpToIndex=(action "jumpToIndex")
|
jumpToIndex=(action "jumpToIndex")
|
||||||
replyToPost=(action "replyToPost")
|
replyToPost=(action "replyToPost")
|
||||||
toggleMultiSelect=(action "toggleMultiSelect")
|
toggleMultiSelect=(action "toggleMultiSelect")
|
||||||
|
showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
|
||||||
deleteTopic=(action "deleteTopic")
|
deleteTopic=(action "deleteTopic")
|
||||||
recoverTopic=(action "recoverTopic")
|
recoverTopic=(action "recoverTopic")
|
||||||
toggleClosed=(action "toggleClosed")
|
toggleClosed=(action "toggleClosed")
|
||||||
|
@ -168,6 +169,7 @@
|
||||||
openUpwards="true"
|
openUpwards="true"
|
||||||
rightSide="true"
|
rightSide="true"
|
||||||
toggleMultiSelect=(action "toggleMultiSelect")
|
toggleMultiSelect=(action "toggleMultiSelect")
|
||||||
|
showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
|
||||||
deleteTopic=(action "deleteTopic")
|
deleteTopic=(action "deleteTopic")
|
||||||
recoverTopic=(action "recoverTopic")
|
recoverTopic=(action "recoverTopic")
|
||||||
toggleClosed=(action "toggleClosed")
|
toggleClosed=(action "toggleClosed")
|
||||||
|
@ -286,6 +288,8 @@
|
||||||
</div>
|
</div>
|
||||||
{{/if}}
|
{{/if}}
|
||||||
|
|
||||||
|
{{slow-mode-info topic=model user=currentUser}}
|
||||||
|
|
||||||
{{topic-timer-info
|
{{topic-timer-info
|
||||||
topicClosed=model.closed
|
topicClosed=model.closed
|
||||||
statusType=model.topic_timer.status_type
|
statusType=model.topic_timer.status_type
|
||||||
|
@ -305,6 +309,7 @@
|
||||||
{{topic-footer-buttons
|
{{topic-footer-buttons
|
||||||
topic=model
|
topic=model
|
||||||
toggleMultiSelect=(action "toggleMultiSelect")
|
toggleMultiSelect=(action "toggleMultiSelect")
|
||||||
|
showTopicSlowModeUpdate=(route-action "showTopicSlowModeUpdate")
|
||||||
deleteTopic=(action "deleteTopic")
|
deleteTopic=(action "deleteTopic")
|
||||||
recoverTopic=(action "recoverTopic")
|
recoverTopic=(action "recoverTopic")
|
||||||
toggleClosed=(action "toggleClosed")
|
toggleClosed=(action "toggleClosed")
|
||||||
|
|
|
@ -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")) {
|
if (topic.get("deleted") && details.get("can_recover")) {
|
||||||
this.addActionButton({
|
this.addActionButton({
|
||||||
className: "topic-admin-recover",
|
className: "topic-admin-recover",
|
||||||
|
|
|
@ -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%;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -63,12 +63,14 @@
|
||||||
border-top: 1px solid var(--primary-low);
|
border-top: 1px solid var(--primary-low);
|
||||||
padding: 10px 0;
|
padding: 10px 0;
|
||||||
max-width: 758px;
|
max-width: 758px;
|
||||||
.topic-timer-heading {
|
.topic-timer-heading,
|
||||||
|
.slow-mode-heading {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
margin: 0px;
|
margin: 0px;
|
||||||
}
|
}
|
||||||
.topic-timer-remove {
|
.topic-timer-remove,
|
||||||
|
.slow-mode-remove {
|
||||||
font-size: $font-down-2;
|
font-size: $font-down-2;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
margin-left: auto;
|
margin-left: auto;
|
||||||
|
|
|
@ -28,7 +28,8 @@ class TopicsController < ApplicationController
|
||||||
:convert_topic,
|
:convert_topic,
|
||||||
:bookmark,
|
:bookmark,
|
||||||
:publish,
|
:publish,
|
||||||
:reset_bump_date
|
:reset_bump_date,
|
||||||
|
:set_slow_mode
|
||||||
]
|
]
|
||||||
|
|
||||||
before_action :consider_user_for_promotion, only: :show
|
before_action :consider_user_for_promotion, only: :show
|
||||||
|
@ -932,6 +933,15 @@ class TopicsController < ApplicationController
|
||||||
render body: nil
|
render body: nil
|
||||||
end
|
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
|
private
|
||||||
|
|
||||||
def topic_params
|
def topic_params
|
||||||
|
|
|
@ -1770,6 +1770,7 @@ end
|
||||||
# archetype :string default("regular"), not null
|
# archetype :string default("regular"), not null
|
||||||
# featured_user4_id :integer
|
# featured_user4_id :integer
|
||||||
# notify_moderators_count :integer default(0), not null
|
# notify_moderators_count :integer default(0), not null
|
||||||
|
# slow_mode_seconds :integer default(0), not null
|
||||||
# spam_count :integer default(0), not null
|
# spam_count :integer default(0), not null
|
||||||
# pinned_at :datetime
|
# pinned_at :datetime
|
||||||
# score :float
|
# score :float
|
||||||
|
|
|
@ -495,6 +495,7 @@ end
|
||||||
# posted :boolean default(FALSE), not null
|
# posted :boolean default(FALSE), not null
|
||||||
# last_read_post_number :integer
|
# last_read_post_number :integer
|
||||||
# highest_seen_post_number :integer
|
# highest_seen_post_number :integer
|
||||||
|
# last_posted_at :datetime
|
||||||
# last_visited_at :datetime
|
# last_visited_at :datetime
|
||||||
# first_visited_at :datetime
|
# first_visited_at :datetime
|
||||||
# notification_level :integer default(1), not null
|
# notification_level :integer default(1), not null
|
||||||
|
|
|
@ -40,7 +40,8 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:pinned_globally,
|
:pinned_globally,
|
||||||
:pinned_at,
|
:pinned_at,
|
||||||
:pinned_until,
|
:pinned_until,
|
||||||
:image_url
|
:image_url,
|
||||||
|
:slow_mode_seconds
|
||||||
)
|
)
|
||||||
|
|
||||||
attributes(
|
attributes(
|
||||||
|
@ -72,7 +73,8 @@ class TopicViewSerializer < ApplicationSerializer
|
||||||
:queued_posts_count,
|
:queued_posts_count,
|
||||||
:show_read_indicator,
|
:show_read_indicator,
|
||||||
:requested_group_name,
|
:requested_group_name,
|
||||||
:thumbnails
|
:thumbnails,
|
||||||
|
:user_last_posted_at
|
||||||
)
|
)
|
||||||
|
|
||||||
has_one :details, serializer: TopicViewDetailsSerializer, root: false, embed: :objects
|
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
|
extra_sizes = ThemeModifierHelper.new(request: scope.request).topic_thumbnail_sizes
|
||||||
object.topic.thumbnail_info(enqueue_if_missing: true, extra_sizes: extra_sizes)
|
object.topic.thumbnail_info(enqueue_if_missing: true, extra_sizes: extra_sizes)
|
||||||
end
|
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
|
end
|
||||||
|
|
|
@ -20,6 +20,7 @@ class WebHookTopicViewSerializer < TopicViewSerializer
|
||||||
topic_timer
|
topic_timer
|
||||||
details
|
details
|
||||||
image_url
|
image_url
|
||||||
|
slow_mode_seconds
|
||||||
}.each do |attr|
|
}.each do |attr|
|
||||||
define_method("include_#{attr}?") do
|
define_method("include_#{attr}?") do
|
||||||
false
|
false
|
||||||
|
|
|
@ -1922,6 +1922,9 @@ en:
|
||||||
yourself_confirm:
|
yourself_confirm:
|
||||||
title: "Did you forget to add recipients?"
|
title: "Did you forget to add recipients?"
|
||||||
body: "Right now this message is only being sent to yourself!"
|
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"
|
admin_options_title: "Optional staff settings for this topic"
|
||||||
|
|
||||||
|
@ -2313,6 +2316,25 @@ en:
|
||||||
jump_reply_down: jump to later reply
|
jump_reply_down: jump to later reply
|
||||||
deleted: "The topic has been deleted"
|
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:
|
topic_status_update:
|
||||||
title: "Topic Timer"
|
title: "Topic Timer"
|
||||||
save: "Set Timer"
|
save: "Set Timer"
|
||||||
|
@ -2447,6 +2469,7 @@ en:
|
||||||
open: "Open Topic"
|
open: "Open Topic"
|
||||||
close: "Close Topic"
|
close: "Close Topic"
|
||||||
multi_select: "Select Posts…"
|
multi_select: "Select Posts…"
|
||||||
|
slow_mode: "Set Slow Mode"
|
||||||
timed_update: "Set Topic Timer..."
|
timed_update: "Set Topic Timer..."
|
||||||
pin: "Pin Topic…"
|
pin: "Pin Topic…"
|
||||||
unpin: "Un-Pin Topic…"
|
unpin: "Un-Pin Topic…"
|
||||||
|
|
|
@ -340,6 +340,7 @@ en:
|
||||||
removed_direct_reply_full_quotes: "Automatically removed quote of whole previous post."
|
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}."
|
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."
|
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"
|
just_posted_that: "is too similar to what you recently posted"
|
||||||
invalid_characters: "contains invalid characters"
|
invalid_characters: "contains invalid characters"
|
||||||
|
|
|
@ -804,6 +804,7 @@ Discourse::Application.routes.draw do
|
||||||
put "t/:topic_id/bookmark" => "topics#bookmark", constraints: { topic_id: /\d+/ }
|
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/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/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+/ }
|
post "t/:topic_id/notifications" => "topics#set_notifications" , constraints: { topic_id: /\d+/ }
|
||||||
|
|
||||||
|
|
|
@ -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
|
|
@ -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
|
|
@ -159,6 +159,19 @@ class PostCreator
|
||||||
return false
|
return false
|
||||||
end
|
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))
|
unless @topic.present? && (@opts[:skip_guardian] || guardian.can_create?(Post, @topic))
|
||||||
errors.add(:base, I18n.t(:topic_not_found))
|
errors.add(:base, I18n.t(:topic_not_found))
|
||||||
return false
|
return false
|
||||||
|
@ -622,7 +635,8 @@ class PostCreator
|
||||||
@topic.id,
|
@topic.id,
|
||||||
posted: true,
|
posted: true,
|
||||||
last_read_post_number: @post.post_number,
|
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
|
# assume it took us 5 seconds of reading time to make a post
|
||||||
PostTiming.record_timing(topic_id: @post.topic_id,
|
PostTiming.record_timing(topic_id: @post.topic_id,
|
||||||
|
|
|
@ -130,6 +130,7 @@ module SvgSprite
|
||||||
"heading",
|
"heading",
|
||||||
"heart",
|
"heart",
|
||||||
"home",
|
"home",
|
||||||
|
"hourglass-end",
|
||||||
"id-card",
|
"id-card",
|
||||||
"info-circle",
|
"info-circle",
|
||||||
"italic",
|
"italic",
|
||||||
|
|
|
@ -722,6 +722,32 @@ describe PostCreator do
|
||||||
expect(topic.word_count).to eq(6)
|
expect(topic.word_count).to eq(6)
|
||||||
end
|
end
|
||||||
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
|
end
|
||||||
|
|
||||||
context 'closed topic' do
|
context 'closed topic' do
|
||||||
|
@ -1194,6 +1220,19 @@ describe PostCreator do
|
||||||
topic_user = TopicUser.find_by(user_id: user.id, topic_id: pm.id)
|
topic_user = TopicUser.find_by(user_id: user.id, topic_id: pm.id)
|
||||||
expect(topic_user.notification_level).to eq(3)
|
expect(topic_user.notification_level).to eq(3)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
describe '#create!' do
|
describe '#create!' do
|
||||||
|
|
|
@ -3047,6 +3047,58 @@ RSpec.describe TopicsController do
|
||||||
end
|
end
|
||||||
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 '#invite' do
|
||||||
describe 'when not logged in' do
|
describe 'when not logged in' do
|
||||||
it "should return the right response" do
|
it "should return the right response" do
|
||||||
|
|
Loading…
Reference in New Issue
Block a user