From e002a24eca1a0c81d025f0ac5c33fa7b0ee13584 Mon Sep 17 00:00:00 2001 From: Ted Johansson Date: Thu, 20 Apr 2023 15:49:35 +0800 Subject: [PATCH] FEATURE: Add new don't feed the trolls feature (#21001) Responding to negative behaviour tends to solicit more of the same. Common wisdom states: "don't feed the trolls". This change codifies that advice by introducing a new nudge when hitting the reply button on a flagged post. It will be shown if either the current user, or two other users (configurable via a site setting) have flagged the post. --- app/models/post.rb | 6 +- config/locales/server.en.yml | 4 ++ config/site_settings.yml | 1 + lib/composer_messages_finder.rb | 27 ++++++++ spec/lib/composer_messages_finder_spec.rb | 69 +++++++++++++++++++ .../dont_feed_the_trolls_popup_spec.rb | 22 ++++++ .../page_objects/components/composer.rb | 8 +++ spec/system/page_objects/pages/topic.rb | 6 ++ 8 files changed, 142 insertions(+), 1 deletion(-) create mode 100644 spec/system/composer/dont_feed_the_trolls_popup_spec.rb diff --git a/app/models/post.rb b/app/models/post.rb index 4b1ba463a05..739954db9f8 100644 --- a/app/models/post.rb +++ b/app/models/post.rb @@ -550,10 +550,14 @@ class Post < ActiveRecord::Base end def is_flagged? + flags.count != 0 + end + + def flags post_actions.where( post_action_type_id: PostActionType.flag_types_without_custom.values, deleted_at: nil, - ).count != 0 + ) end def reviewable_flag diff --git a/config/locales/server.en.yml b/config/locales/server.en.yml index cebc0326da1..5cc052aee81 100644 --- a/config/locales/server.en.yml +++ b/config/locales/server.en.yml @@ -555,6 +555,8 @@ en: get_a_room: You’ve replied to @%{reply_username} %{count} times, did you know you could send them a personal message instead? + dont_feed_the_trolls: This post has already been flagged for moderator attention. Are you sure you wish to reply to it? Replies to negative content tend to encourage more negative behavior. + too_many_replies: | ### You have reached the reply limit for this topic @@ -2179,6 +2181,8 @@ en: get_a_room_threshold: "Number of posts a user has to make to the same person in the same topic before being warned." + dont_feed_the_trolls_threshold: "Number of flags from other users before being warned." + enable_mobile_theme: "Mobile devices use a mobile-friendly theme, with the ability to switch to the full site. Disable this if you want to use a custom stylesheet that is fully responsive." dominating_topic_minimum_percent: "What percentage of posts a user has to make in a topic before being reminded about overly dominating a topic." diff --git a/config/site_settings.yml b/config/site_settings.yml index f53ca8b592c..53babda1a81 100644 --- a/config/site_settings.yml +++ b/config/site_settings.yml @@ -2390,6 +2390,7 @@ uncategorized: educate_until_posts: 2 sequential_replies_threshold: 2 get_a_room_threshold: 3 + dont_feed_the_trolls_threshold: 2 dominating_topic_minimum_percent: 40 disable_avatar_education_message: false pm_warn_user_last_seen_months_ago: 24 diff --git a/lib/composer_messages_finder.rb b/lib/composer_messages_finder.rb index 3ba4dfa6b70..78841a91385 100644 --- a/lib/composer_messages_finder.rb +++ b/lib/composer_messages_finder.rb @@ -226,6 +226,33 @@ class ComposerMessagesFinder } end + def check_dont_feed_the_trolls + return if !replying? + + post = + if @details[:post_id] + Post.find_by(id: @details[:post_id]) + else + @topic.first_post + end + + return if post.blank? + + flags = post.flags.group(:user_id).count + flagged_by_replier = flags[@user.id].to_i > 0 + flagged_by_others = flags.values.sum >= SiteSetting.dont_feed_the_trolls_threshold + + return if !flagged_by_replier && !flagged_by_others + + { + id: "dont_feed_the_trolls", + templateName: "education", + wait_for_typing: false, + extraClass: "urgent", + body: PrettyText.cook(I18n.t("education.dont_feed_the_trolls")), + } + end + def check_reviving_old_topic return unless replying? if @topic.nil? || SiteSetting.warn_reviving_old_topic_age < 1 || @topic.last_posted_at.nil? || diff --git a/spec/lib/composer_messages_finder_spec.rb b/spec/lib/composer_messages_finder_spec.rb index 1d7ad381a81..a95370e86a2 100644 --- a/spec/lib/composer_messages_finder_spec.rb +++ b/spec/lib/composer_messages_finder_spec.rb @@ -328,6 +328,75 @@ RSpec.describe ComposerMessagesFinder do end end + describe "#dont_feed_the_trolls" do + fab!(:user) { Fabricate(:user) } + fab!(:author) { Fabricate(:user) } + fab!(:other_user) { Fabricate(:user) } + fab!(:third_user) { Fabricate(:user) } + fab!(:topic) { Fabricate(:topic, user: author) } + fab!(:original_post) { Fabricate(:post, topic: topic, user: author) } + fab!(:unflagged_post) { Fabricate(:post, topic: topic, user: author) } + fab!(:self_flagged_post) { Fabricate(:post, topic: topic, user: author) } + fab!(:under_flagged_post) { Fabricate(:post, topic: topic, user: author) } + fab!(:over_flagged_post) { Fabricate(:post, topic: topic, user: author) } + + before { SiteSetting.dont_feed_the_trolls_threshold = 2 } + + it "does not show a message for unflagged posts" do + finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: unflagged_post.id, + ) + expect(finder.check_dont_feed_the_trolls).to be_blank + end + + it "shows a message when the replier has already flagged the post" do + Fabricate(:flag, post: self_flagged_post, user: user) + finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: self_flagged_post.id, + ) + expect(finder.check_dont_feed_the_trolls).to be_present + end + + it "shows a message when replying to flagged topic (first post)" do + Fabricate(:flag, post: original_post, user: user) + finder = ComposerMessagesFinder.new(user, composer_action: "reply", topic_id: topic.id) + expect(finder.check_dont_feed_the_trolls).to be_present + end + + it "does not show a message when not enough others have flagged the post" do + Fabricate(:flag, post: under_flagged_post, user: other_user) + finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: under_flagged_post.id, + ) + expect(finder.check_dont_feed_the_trolls).to be_blank + end + + it "shows a message when enough others have already flagged the post" do + Fabricate(:flag, post: over_flagged_post, user: other_user) + Fabricate(:flag, post: over_flagged_post, user: third_user) + finder = + ComposerMessagesFinder.new( + user, + composer_action: "reply", + topic_id: topic.id, + post_id: over_flagged_post.id, + ) + expect(finder.check_dont_feed_the_trolls).to be_present + end + end + describe ".check_get_a_room" do fab!(:user) { Fabricate(:user) } fab!(:other_user) { Fabricate(:user) } diff --git a/spec/system/composer/dont_feed_the_trolls_popup_spec.rb b/spec/system/composer/dont_feed_the_trolls_popup_spec.rb new file mode 100644 index 00000000000..ece0cc65b6a --- /dev/null +++ b/spec/system/composer/dont_feed_the_trolls_popup_spec.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +describe "Composer don't feed the trolls popup", type: :system, js: true do + fab!(:user) { Fabricate(:user) } + fab!(:troll) { Fabricate(:user) } + fab!(:topic) { Fabricate(:topic, user: user) } + fab!(:post) { Fabricate(:post, user: user, topic: topic) } + fab!(:reply) { Fabricate(:post, user: troll, topic: topic) } + fab!(:flag) { Fabricate(:flag, post: reply, user: user) } + let(:topic_page) { PageObjects::Pages::Topic.new } + + before { sign_in user } + + it "shows a popup when about to reply to a troll" do + SiteSetting.educate_until_posts = 0 + + topic_page.visit_topic(topic) + topic_page.click_post_action_button(reply, :reply) + + expect(topic_page).to have_composer_popup_content(I18n.t("education.dont_feed_the_trolls")) + end +end diff --git a/spec/system/page_objects/components/composer.rb b/spec/system/page_objects/components/composer.rb index 2cd15633d2a..dd6f3df4b32 100644 --- a/spec/system/page_objects/components/composer.rb +++ b/spec/system/page_objects/components/composer.rb @@ -43,6 +43,10 @@ module PageObjects composer_input.value == content end + def has_popup_content?(content) + composer_popup.has_content?(content) + end + def select_action(action) find(action(action)).click self @@ -83,6 +87,10 @@ module PageObjects def composer_input find("#{COMPOSER_ID} .d-editor .d-editor-input") end + + def composer_popup + find("#{COMPOSER_ID} .composer-popup") + end end end end diff --git a/spec/system/page_objects/pages/topic.rb b/spec/system/page_objects/pages/topic.rb index 649f15c11b9..4a5e146cddf 100644 --- a/spec/system/page_objects/pages/topic.rb +++ b/spec/system/page_objects/pages/topic.rb @@ -71,6 +71,8 @@ module PageObjects case button when :bookmark post_by_number(post).find(".bookmark.with-reminder").click + when :reply + post_by_number(post).find(".post-controls .reply").click end end @@ -111,6 +113,10 @@ module PageObjects @composer_component.has_content?(content) end + def has_composer_popup_content?(content) + @composer_component.has_popup_content?(content) + end + def send_reply find("#reply-control .save-or-cancel .create").click end