From 86eb75cee77bc5a2fbc089c8b4a14a8ef4b61dfa Mon Sep 17 00:00:00 2001 From: Nick Taylor Date: Tue, 19 Jan 2021 12:07:47 -0500 Subject: [PATCH] Report a Message in Connect (#12229) * Frontend Ready for Connect Report Abuse * add feedback api * js defination fix * Added Hooks to the Component * add json response in feedback * Block popup added * fix render issue * Made changes in internal view * change error message * Added few design changes = * add test cases * Update app/javascript/chat/actions/requestActions.js Co-authored-by: Nick Taylor * add PR suggestions * add backend test cases * report abuse form close * Update app/javascript/chat/actions/requestActions.js Co-authored-by: Nick Taylor * Update app/javascript/chat/ReportAbuse/index.jsx Co-authored-by: Nick Taylor * add test cases * Update app/javascript/chat/ReportAbuse/index.jsx Co-authored-by: Nick Taylor * Update app/javascript/chat/ReportAbuse/index.jsx Co-authored-by: Marcy Sutton * Update app/javascript/chat/ReportAbuse/index.jsx Co-authored-by: Marcy Sutton * group the fieldset * fix report abuse api * fix test case * fix request test case * fix typo * cleaned up markup in report abuse component. * Fixed spacing between abuse options. * Fixed wording in report abuse confirmation. * Removed unnecessary data-testid and aria-label attributes. * Added some top margin to the report abuse form. * Update app/javascript/chat/message.jsx Co-authored-by: Suzanne Aitchison * Added a legend to the the fieldset. * Update app/javascript/chat/actions/requestActions.js Co-authored-by: Michael Kohl Co-authored-by: Sarthak <7lovesharma7@gmail.com> Co-authored-by: Narender Singh Co-authored-by: Marcy Sutton Co-authored-by: Suzanne Aitchison Co-authored-by: Michael Kohl --- app/assets/stylesheets/chat.scss | 15 ++ .../feedback_messages_controller.rb | 26 ++- app/javascript/chat/ReportAbuse/index.jsx | 149 ++++++++++++++++++ .../chat/__tests__/reportAbuse.test.jsx | 44 ++++++ app/javascript/chat/actions/requestActions.js | 49 ++++++ app/javascript/chat/chat.jsx | 21 ++- app/javascript/chat/content.jsx | 26 ++- app/javascript/chat/message.jsx | 20 ++- .../onboarding/components/FollowUsers.jsx | 6 +- app/javascript/packs/base.js | 2 +- app/javascript/packs/homePage.jsx | 7 +- .../_feedback_message.html.erb | 40 ++++- .../admin/feedback_messages/_style.html.erb | 17 ++ app/views/layouts/_nav_menu.html.erb | 2 +- docs/technical-overview/ab_testing.md | 28 ++-- spec/requests/feedback_messages_spec.rb | 22 +++ spec/system/feedback_message_spec.rb | 24 +++ 17 files changed, 461 insertions(+), 37 deletions(-) create mode 100644 app/javascript/chat/ReportAbuse/index.jsx create mode 100644 app/javascript/chat/__tests__/reportAbuse.test.jsx create mode 100644 spec/system/feedback_message_spec.rb diff --git a/app/assets/stylesheets/chat.scss b/app/assets/stylesheets/chat.scss index ebfc68bac..e20290263 100644 --- a/app/assets/stylesheets/chat.scss +++ b/app/assets/stylesheets/chat.scss @@ -1266,6 +1266,21 @@ } } +.report__abuse-options { + .crayons-field + .crayons-field { + margin-top: var(--su-2); + } +} + +.report__abuse__button { + width: 135px; +} +.reported__message { + border: 1px solid; + max-height: 300px; + overflow: scroll; +} + .membership-section { position: relative; diff --git a/app/controllers/feedback_messages_controller.rb b/app/controllers/feedback_messages_controller.rb index 13d3808ba..e9016e15a 100644 --- a/app/controllers/feedback_messages_controller.rb +++ b/app/controllers/feedback_messages_controller.rb @@ -10,7 +10,7 @@ class FeedbackMessagesController < ApplicationController @feedback_message = FeedbackMessage.new(params) recaptcha_enabled = ReCaptcha::CheckEnabled.call(current_user) - if (!recaptcha_enabled || recaptcha_verified?) && @feedback_message.save + if (!recaptcha_enabled || recaptcha_verified? || connect_feedback?) && @feedback_message.save Slack::Messengers::Feedback.call( user: current_user, type: feedback_message_params[:feedback_type], @@ -20,12 +20,24 @@ class FeedbackMessagesController < ApplicationController ) rate_limiter.track_limit_by_action(:feedback_message_creation) - redirect_to feedback_messages_path + respond_to do |format| + format.html { redirect_to feedback_messages_path } + format.json { render json: { success: true, message: "Your report is submitted" } } + end else @previous_message = feedback_message_params[:message] - flash[:notice] = "Make sure the forms are filled 🤖" - render "pages/report_abuse" + + respond_to do |format| + format.html { render "pages/report_abuse" } + format.json do + render json: { + success: false, + message: @feedback_message.errors_as_sentence, + status: :bad_request + } + end + end end end @@ -36,8 +48,12 @@ class FeedbackMessagesController < ApplicationController params["g-recaptcha-response"] && verify_recaptcha(recaptcha_params) end + def connect_feedback? + feedback_message_params[:feedback_type] == "connect" + end + def feedback_message_params - allowed_params = %i[message feedback_type category reported_url] + allowed_params = %i[message feedback_type category reported_url offender_id] params.require(:feedback_message).permit(allowed_params) end end diff --git a/app/javascript/chat/ReportAbuse/index.jsx b/app/javascript/chat/ReportAbuse/index.jsx new file mode 100644 index 000000000..16c9013f0 --- /dev/null +++ b/app/javascript/chat/ReportAbuse/index.jsx @@ -0,0 +1,149 @@ +import { h, Fragment } from 'preact'; +import PropTypes from 'prop-types'; +import { useState } from 'preact/hooks'; +import { reportAbuse, blockUser } from '../actions/requestActions'; +import { addSnackbarItem } from '../../Snackbar'; +import { Button, FormField, RadioButton } from '@crayons'; + +/** + * This component render the report abuse + * + * @param {object} props + * @param {object} props.data + * @param {function} props.closeReportAbuseForm + * + * @component + * + * @example + * + * + * + */ +function ReportAbuse({ data, closeReportAbuseForm }) { + const [category, setCategory] = useState(null); + + const handleChange = (e) => { + setCategory(e.target.value); + }; + + const handleSubmit = async () => { + const response = await reportAbuse( + data.message, + 'connect', + category, + data.user_id, + ); + const { success, message } = response; + if (success) { + const confirmBlock = window.confirm( + `The message will be reported.\n\nWould you like to block this person as well?\n\nThis will: + - prevent them from commenting on your posts + - block all notifications from them + - prevent them from messaging you via chat`, + ); + + if (confirmBlock) { + const response = await blockUser(data.user_id); + if (response.result === 'blocked') { + addSnackbarItem({ + message: + 'Your report has been submitted and the user has been blocked', + }); + } + } else { + addSnackbarItem({ message: 'Your report has been submitted.' }); + } + closeReportAbuseForm(); + } else { + addSnackbarItem({ message }); + } + }; + + return ( + +
+

Report Abuse

+

+ Thank you for reporting any abuse that violates our{' '} + code of conduct or{' '} + terms and conditions. We continue to try to make + this environment a great one for everybody. +

+
+ Why is this content inappropriate? + + + + + + + + + + + + + + + + +

Message to Report

+
+ +
+
+
+ ); +} + +ReportAbuse.propTypes = { + resource: PropTypes.shape({ + data: PropTypes.shape({ + user_id: PropTypes.number.isRequired, + message: PropTypes.element.isRequired, + }), + }).isRequired, +}; + +export default ReportAbuse; diff --git a/app/javascript/chat/__tests__/reportAbuse.test.jsx b/app/javascript/chat/__tests__/reportAbuse.test.jsx new file mode 100644 index 000000000..c5c56220f --- /dev/null +++ b/app/javascript/chat/__tests__/reportAbuse.test.jsx @@ -0,0 +1,44 @@ +import { h } from 'preact'; +import { render } from '@testing-library/preact'; +import { axe } from 'jest-axe'; +import ReportAbuse from '../ReportAbuse'; + +describe('', () => { + it('should have no a11y violations', async () => { + const { container } = render( + , + ); + const results = await axe(container); + + expect(results).toHaveNoViolations(); + }); + + it('should render the component', () => { + const { getByText, getByLabelText } = render( + , + ); + + expect(getByText('Report Abuse')).toBeDefined(); + + const vulgarInput = getByLabelText('Rude or vulgar'); + expect(vulgarInput.value).toEqual('rude or vulgar'); + + const harassmentInput = getByLabelText('Harassment or hate speech'); + expect(harassmentInput.value).toEqual('harassment'); + + const listingsInput = getByLabelText( + 'Inappropriate listings message/category', + ); + expect(listingsInput.value).toEqual('listings'); + + const spamInput = getByLabelText('Spam or copyright issue'); + expect(spamInput.value).toEqual('spam'); + + expect(getByText('Report Message')).toBeDefined(); + }); +}); diff --git a/app/javascript/chat/actions/requestActions.js b/app/javascript/chat/actions/requestActions.js index c96463306..72b652273 100644 --- a/app/javascript/chat/actions/requestActions.js +++ b/app/javascript/chat/actions/requestActions.js @@ -92,3 +92,52 @@ export async function updateMembership(membershipId, userAction) { return response.json(); } + +/** + * + * @param {string} feedback_message + * @param {string} type_of_feedback + * @param {string} category + * @param {string} reported_url + */ +export async function reportAbuse( + feedback_message, + feedback_type, + category, + offender_id, +) { + const response = await request('/feedback_messages', { + method: 'POST', + body: { + feedback_message: { + message: feedback_message, + feedback_type, + category, + offender_id, + }, + }, + }); + + return response.json(); +} + +/** + * Blocks a user with the given ID from using Connect + * + * @param {number} userId + * + * + */ + +export async function blockUser(userId) { + const response = await request('/user_blocks', { + method: 'POST', + body: { + user_block: { + blocked_id: userId, + }, + }, + }); + + return response.json(); +} diff --git a/app/javascript/chat/chat.jsx b/app/javascript/chat/chat.jsx index e82c4b91d..19e8d9b4c 100644 --- a/app/javascript/chat/chat.jsx +++ b/app/javascript/chat/chat.jsx @@ -1073,6 +1073,15 @@ export default class Chat extends Component { })); }; + closeReportAbuseForm = () => { + const { activeChannelId } = this.state; + this.setActiveContentState(activeChannelId, null); + this.setState({ + fullscreenContent: null, + expanded: window.innerWidth > NARROW_WIDTH_LIMIT, + }); + }; + setActiveContent = (response) => { const { activeChannelId } = this.state; this.setActiveContentState(activeChannelId, response); @@ -1184,7 +1193,6 @@ export default class Chat extends Component { ); } } - return messages[activeChannelId].map((message) => message.action ? ( ), ); }; + triggerReportMessage = (messageId) => { + const { activeChannelId, messages } = this.state; + this.setActiveContent({ + data: messages[activeChannelId].find( + (message) => message.id === messageId, + ), + type_of: 'message-report-abuse', + }); + }; triggerChannelFilter = (e) => { const { channelTypeFilter } = this.state; const filters = @@ -1594,6 +1612,7 @@ export default class Chat extends Component { resource={state.activeContent[state.activeChannelId]} activeChannel={state.activeChannel} fullscreen={state.fullscreenContent === 'sidecar'} + closeReportAbuseForm={this.closeReportAbuseForm} /> ( - + ); } } -const Display = ({ resource }) => { +function Display({ resource, closeReportAbuseForm }) { switch (resource.type_of) { case 'loading-user': return
; @@ -116,7 +127,14 @@ const Display = ({ resource }) => { handleLeavingChannel={resource.handleLeavingChannel} /> ); + case 'message-report-abuse': + return ( + + ); default: return null; } -}; +} diff --git a/app/javascript/chat/message.jsx b/app/javascript/chat/message.jsx index 268a56319..578dbb064 100644 --- a/app/javascript/chat/message.jsx +++ b/app/javascript/chat/message.jsx @@ -19,6 +19,7 @@ const Message = ({ profileImageUrl, onContentTrigger, onDeleteMessageTrigger, + onReportMessageTrigger, onEditMessageTrigger, }) => { const spanStyle = { color }; @@ -60,6 +61,22 @@ const Message = ({
); + const dropdownReport = ( +
+ + message actions + + +
+ +
+
+ ); return (
@@ -119,7 +136,7 @@ const Message = ({ ' ' )}
- {userID === currentUserId ? dropdown : ' '} + {userID === currentUserId ? dropdown : dropdownReport}
@@ -143,6 +160,7 @@ Message.propTypes = { onContentTrigger: PropTypes.func.isRequired, onDeleteMessageTrigger: PropTypes.func.isRequired, onEditMessageTrigger: PropTypes.func.isRequired, + onReportMessageTrigger: PropTypes.func.isRequired, }; Message.defaultProps = { diff --git a/app/javascript/onboarding/components/FollowUsers.jsx b/app/javascript/onboarding/components/FollowUsers.jsx index fdd00be15..fb6d3083f 100644 --- a/app/javascript/onboarding/components/FollowUsers.jsx +++ b/app/javascript/onboarding/components/FollowUsers.jsx @@ -128,7 +128,11 @@ class FollowUsers extends Component { } return ( - ); diff --git a/app/javascript/packs/base.js b/app/javascript/packs/base.js index c41b3c616..7ca85afcb 100644 --- a/app/javascript/packs/base.js +++ b/app/javascript/packs/base.js @@ -17,7 +17,7 @@ const menus = [...document.getElementsByClassName('js-hamburger-trigger')]; const moreMenus = [...document.getElementsByClassName('js-nav-more-trigger')]; getInstantClick().then((spa) => { - spa.on('change', function () { + spa.on('change', () => { const { currentPage } = document.getElementById('page-content').dataset; setCurrentPageIconLink(currentPage, getPageEntries()); diff --git a/app/javascript/packs/homePage.jsx b/app/javascript/packs/homePage.jsx index b44f97b2b..dbd964412 100644 --- a/app/javascript/packs/homePage.jsx +++ b/app/javascript/packs/homePage.jsx @@ -53,9 +53,7 @@ function renderTagsFollowed(user = userData()) { } function renderSidebar() { - const sidebarContainer = document.getElementById( - 'sidebar-wrapper-right', - ); + const sidebarContainer = document.getElementById('sidebar-wrapper-right'); // If the screen's width is less than 1024px we don't need this extra data. if (sidebarContainer && screen.width > 1023 && window.location.pathname === '/') { @@ -67,11 +65,10 @@ function renderSidebar() { } } - const feedTimeFrame = frontPageFeedPathNames.get(window.location.pathname); if (!document.getElementById('featured-story-marker')) { - const waitingForDataLoad = setInterval(function dataLoadedCheck() { + const waitingForDataLoad = setInterval(() => { const { user = null, userStatus } = document.body.dataset; if (userStatus === 'logged-out') { return; diff --git a/app/views/admin/feedback_messages/_feedback_message.html.erb b/app/views/admin/feedback_messages/_feedback_message.html.erb index abc45bf0a..6b09f64c4 100644 --- a/app/views/admin/feedback_messages/_feedback_message.html.erb +++ b/app/views/admin/feedback_messages/_feedback_message.html.erb @@ -19,7 +19,11 @@
+ <% if feedback_message.feedback_type == "connect" %> + Reporter and Affected: + <% else %> Reporter: + <% end %>
<% if feedback_message.reporter_id? %> @@ -29,19 +33,35 @@ Anonymous <% end %>
+ + <% if feedback_message.feedback_type == "connect" %> +
+ Offender: +
+
+ <%= feedback_message.offender.name %> + @<%= feedback_message.offender.username %> +
+ <% else %>
- Reported URL (new tab): -
-
- <%= feedback_message.reported_url %> -
+ Reported URL (new tab): + +
+ <%= feedback_message.reported_url %> +
+ <% end %>
+
-

+

<%= feedback_message.category.titleize %> + <% if feedback_message.feedback_type == "connect" %> + <%= feedback_message.feedback_type %> + <% end %>

+ <% if feedback_message.feedback_type != "connect" %>
Message:
@@ -52,6 +72,14 @@ <%= feedback_message.message %> <% end %>

+ <% else %> +
+ Message from Offender: +
+
+ <%= raw(feedback_message.message) %> +
+ <% end %>

diff --git a/app/views/admin/feedback_messages/_style.html.erb b/app/views/admin/feedback_messages/_style.html.erb index ee0f88794..873bde974 100644 --- a/app/views/admin/feedback_messages/_style.html.erb +++ b/app/views/admin/feedback_messages/_style.html.erb @@ -22,4 +22,21 @@ color: black; text-align: center; } + .badge-connect { + background-color: #26d9ca; + margin-top: 5px; + text-transform: capitalize; +} +.reported__message { + border: 1px solid; + margin: 20px auto; + padding: 20px; + max-height: 300px; + overflow: scroll; +} +.report__tags{ + display: grid; +justify-items: end; +} + diff --git a/app/views/layouts/_nav_menu.html.erb b/app/views/layouts/_nav_menu.html.erb index f0d2ea1c1..1cd97eb9e 100644 --- a/app/views/layouts/_nav_menu.html.erb +++ b/app/views/layouts/_nav_menu.html.erb @@ -15,7 +15,7 @@ Reading list Settings
- + diff --git a/docs/technical-overview/ab_testing.md b/docs/technical-overview/ab_testing.md index a81850713..85e0a5d1e 100644 --- a/docs/technical-overview/ab_testing.md +++ b/docs/technical-overview/ab_testing.md @@ -4,19 +4,22 @@ We use the [Field Test](https://github.com/ankane/field_test) gem for conducting simple A/B tests. If you want to propose an A/B test of a feature, you may make a pull request -which defines the hypotheses and what admins should look for to declare a winner. -As A/B tests are going to have results that may differ from Forem to Forem, the -process is relatively immature. In the future we may have more studies that can return anonymous ecosystem-wide results. +which defines the hypotheses and what admins should look for to declare a +winner. As A/B tests are going to have results that may differ from Forem to +Forem, the process is relatively immature. In the future we may have more +studies that can return anonymous ecosystem-wide results. A/B tests are inherently the most useful in _large_ Forems, where qualitative -feedback may be more useful on small Forems. As such, [DEV](https://dev.to) -is our largest Forem and therefore can provide us with the most feedback for -our existing A/B tests. However, we must keep in mind that DEV results may -not apply well to future large Forems. We should seek to re-run useful experiments within the ecosystem after time has passed. +feedback may be more useful on small Forems. As such, [DEV](https://dev.to) is +our largest Forem and therefore can provide us with the most feedback for our +existing A/B tests. However, we must keep in mind that DEV results may not apply +well to future large Forems. We should seek to re-run useful experiments within +the ecosystem after time has passed. ## Creating a new A/B test -Follow the guidelines of the field test gem and add the test info to [config/field_test.yml](https://github.com/forem/forem/blob/master/config/field_test.yml). +Follow the guidelines of the field test gem and add the test info to +[config/field_test.yml](https://github.com/forem/forem/blob/master/config/field_test.yml). Then where you want to trigger the variant, you'll add some code like this: @@ -38,9 +41,9 @@ Then where you want to trigger the variant, you'll add some code like this: end ``` -Which would find or create the test variant for that user in particular. If -this code is not called in the controller or view, you'll need to first -include the gem helpers at the top of the file... +Which would find or create the test variant for that user in particular. If this +code is not called in the controller or view, you'll need to first include the +gem helpers at the top of the file... ``` include FieldTest::Helpers @@ -53,4 +56,5 @@ To record a successful field test outcome, you should call something like this .perform_async(user_id, :follow_implicit_points, "user_creates_reaction") ``` -And modify that class as needed to determine whether to record the successful trial. +And modify that class as needed to determine whether to record the successful +trial. diff --git a/spec/requests/feedback_messages_spec.rb b/spec/requests/feedback_messages_spec.rb index ac59a6d8a..9d40d2a18 100644 --- a/spec/requests/feedback_messages_spec.rb +++ b/spec/requests/feedback_messages_spec.rb @@ -1,6 +1,8 @@ require "rails_helper" RSpec.describe "feedback_messages", type: :request do + let(:user) { create(:user) } + describe "POST /feedback_messages" do def mock_recaptcha_verification # rubocop:disable RSpec/AnyInstance @@ -49,6 +51,26 @@ RSpec.describe "feedback_messages", type: :request do end end + context "when feedback is created by chat" do + before do + sign_in user + post "/feedback_messages", params: { + feedback_message: { + message: "Test Message", + feedback_type: "connect", + category: "rude or vulgar", + offender_id: user.id + } + }, as: :json + end + + it "creates a feedback message" do + expect(response.status).to eq(200) + expect(response.parsed_body["success"]).to eq(true) + expect(FeedbackMessage.where(offender_id: user.id).count).to eq(1) + end + end + context "with valid params and recaptcha not configured" do before do allow(SiteConfig).to receive(:recaptcha_secret_key).and_return(nil) diff --git a/spec/system/feedback_message_spec.rb b/spec/system/feedback_message_spec.rb new file mode 100644 index 000000000..7caf32450 --- /dev/null +++ b/spec/system/feedback_message_spec.rb @@ -0,0 +1,24 @@ +require "rails_helper" + +RSpec.describe "Feedback report by chat channel messages", type: :system do + let(:user) { create(:user) } + + context "when user create a report abuse feedback message" do + before do + sign_in user + end + + it "feedback messahe should increase by one", js: true do + expect do + post "/feedback_messages", params: { + feedback_message: { + message: "Test Message", + feedback_type: "connect", + category: "rude or vulgar", + offender_id: user.id + } + }, as: :json + end.to change(FeedbackMessage, :count).by(1) + end + end +end