Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Report add-on abuse UI #3202

Merged
merged 2 commits into from
Sep 26, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,7 @@
"react-redux-loading-bar": "2.9.2",
"react-router": "2.8.1",
"react-router-scroll": "0.4.2",
"react-textarea-autosize": "^5.1.0",
"redux": "3.7.2",
"redux-connect": "4.0.2",
"redux-logger": "3.0.6",
Expand Down
5 changes: 4 additions & 1 deletion src/amo/components/AddonMoreInfo/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import PropTypes from 'prop-types';
import { compose } from 'redux';

import Link from 'amo/components/Link';
import ReportAbuseButton from 'amo/components/ReportAbuseButton';
import translate from 'core/i18n/translate';
import { trimAndAddProtocolToUrl } from 'core/utils';
import Card from 'ui/components/Card';
Expand Down Expand Up @@ -151,14 +152,16 @@ export class AddonMoreInfoBase extends React.Component {
}

render() {
const { i18n } = this.props;
const { addon, i18n } = this.props;

return (
<Card
className="AddonMoreInfo"
header={i18n.gettext('More information')}
>
{this.listContent()}

<ReportAbuseButton addon={addon} />
</Card>
);
}
Expand Down
221 changes: 221 additions & 0 deletions src/amo/components/ReportAbuseButton/index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
import classNames from 'classnames';
import { oneLine } from 'common-tags';
import React from 'react';
import { connect } from 'react-redux';
import Textarea from 'react-textarea-autosize';
import { compose } from 'redux';

import { withErrorHandler } from 'core/errorHandler';
import type { ErrorHandlerType } from 'core/errorHandler';
import translate from 'core/i18n/translate';
import log from 'core/logger';
import {
disableAbuseButtonUI,
enableAbuseButtonUI,
hideAddonAbuseReportUI,
sendAddonAbuseReport,
showAddonAbuseReportUI,
} from 'core/reducers/abuse';
import { sanitizeHTML } from 'core/utils';
import Button from 'ui/components/Button';

import './styles.scss';


type PropTypes = {
abuseReport: {|
message: string,
reporter: Object | null,
|},
addon: Object | null,
dispatch: Function,
errorHandler: ErrorHandlerType,
loading: bool,
i18n: Object,
};

export class ReportAbuseButtonBase extends React.Component {
dismissReportUI = (event) => {
event.preventDefault();

const { addon, dispatch, loading } = this.props;

if (loading) {
log.debug(
"Ignoring dismiss click because we're submitting the abuse report");
return;
}

dispatch(hideAddonAbuseReportUI({ addon }));
}

sendReport = (event) => {
event.preventDefault();

// The button isn't clickable if there is no content, but just in case:
// we verify there's a message to send.
if (!this.textarea.value.length) {
log.debug(oneLine`User managed to click submit button while textarea
was empty. Ignoring this onClick/sendReport event.`);
return;
}

const { addon, dispatch, errorHandler } = this.props;

dispatch(sendAddonAbuseReport({
addonSlug: addon.slug,
errorHandlerId: errorHandler.id,
message: this.textarea.value,
}));
}

showReportUI = (event) => {
event.preventDefault();

const { addon, dispatch } = this.props;

dispatch(showAddonAbuseReportUI({ addon }));
this.textarea.focus();
}

textareaChange = () => {
const { abuseReport, addon, dispatch } = this.props;

// Don't dispatch the UI update if the button is already visible.
// We also test for `value.trim()` so the user can't submit an
// empty report full of spaces.
if (this.textarea.value.trim().length && !abuseReport.buttonEnabled) {
dispatch(enableAbuseButtonUI({ addon }));
} else if (!this.textarea.value.trim().length) {
dispatch(disableAbuseButtonUI({ addon }));
}
}

props: PropTypes;

render() {
const { abuseReport, addon, i18n, loading } = this.props;

if (!addon) {
return null;
}

if (abuseReport && abuseReport.message) {
return (
<div className="ReportAbuseButton ReportAbuseButton--report-sent">
<h3 className="ReportAbuseButton-header">
{i18n.gettext('You reported this add-on for abuse')}
</h3>

<p className="ReportAbuseButton-first-paragraph">
{i18n.gettext(
`We have received your report. Thanks for letting us know about
your concerns with this add-on.`
)}
</p>

<p>
{i18n.gettext(
`We can't respond to every abuse report but we'll look into
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no need for oneLine tag here? (same comment applies 7 lines above)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, I think I've mentioned it but it's easy to forget and very confusing: for i18n calls specifically our i18n tools deal with multiline template strings and automatically oneLine them. If we do it manually there are weird bugs (see #2059).

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah snap. Forgot it, again.

this issue.`
)}
</p>
</div>
);
}

const sendButtonIsDisabled = loading || !abuseReport.buttonEnabled;

const prefaceText = i18n.sprintf(i18n.gettext(
`If you think this add-on violates
%(linkTagStart)sMozilla's add-on policies%(linkTagEnd)s or has
security or privacy issues, please report these issues to Mozilla using
this form.`
), {
linkTagStart: '<a href="https://developer.mozilla.org/en-US/Add-ons/AMO/Policy">',
linkTagEnd: '</a>',
});

/* eslint-disable react/no-danger */
return (
<div
className={classNames('ReportAbuseButton', {
'ReportAbuseButton--is-expanded': abuseReport.uiVisible,
})}
>
<div className="ReportAbuseButton--preview">
<Button
className="ReportAbuseButton-show-more Button--report"
onClick={this.showReportUI}
>
{i18n.gettext('Report this add-on for abuse')}
</Button>
</div>

<div className="ReportAbuseButton--expanded">
<h3 className="ReportAbuseButton-header">
{i18n.gettext('Report this add-on for abuse')}
</h3>

<p
className="ReportAbuseButton-first-paragraph"
dangerouslySetInnerHTML={sanitizeHTML(prefaceText, ['a'])}
/>

<p>{i18n.gettext(
`Please don't use this form to report bugs or request add-on
features; this report will be sent to Mozilla and not to the
add-on developer.`
)}</p>
<Textarea
className="ReportAbuseButton-textarea"
disabled={loading}
inputRef={(ref) => { this.textarea = ref; }}
onChange={this.textareaChange}
placeholder={i18n.gettext(
'Explain how this add-on is violating our policies.'
)}
/>

<div className="ReportAbuseButton-buttons">
<a
className={classNames('ReportAbuseButton-dismiss-report', {
'ReportAbuseButton-dismiss-report--disabled': loading,
})}
href="#cancel"
onClick={this.dismissReportUI}
>
{i18n.gettext('Dismiss')}
</a>
<Button
className="ReportAbuseButton-send-report Button--report Button--small"
disabled={sendButtonIsDisabled}
onClick={this.sendReport}
>
{loading ?
i18n.gettext('Sending abuse report') :
i18n.gettext('Send abuse report')}
</Button>
</div>
</div>
</div>
);
/* eslint-enable react/no-danger */
}
}

export const mapStateToProps = (state, ownProps) => {
const addon = ownProps.addon;

return {
abuseReport: addon && state.abuse.bySlug[addon.slug] ?
state.abuse.bySlug[addon.slug] : {},
loading: state.abuse.loading,
};
};

export default compose(
connect(mapStateToProps),
translate(),
withErrorHandler({ name: 'ReportAbuseButton' }),
)(ReportAbuseButtonBase);
62 changes: 62 additions & 0 deletions src/amo/components/ReportAbuseButton/styles.scss
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
@import "~ui/css/vars";

.ReportAbuseButton {
margin: 10px auto;
}

.ReportAbuseButton-header {
margin-bottom: 0;
}

.ReportAbuseButton-first-paragraph {
margin-top: 6px;
}

.ReportAbuseButton--preview {
display: block;

.ReportAbuseButton--is-expanded & {
display: none;
}
}

.ReportAbuseButton--expanded {
display: none;

.ReportAbuseButton--is-expanded & {
display: block;
}
}

.ReportAbuseButton-show-more {
width: 100%;
}

.ReportAbuseButton-textarea {
font-size: $font-size-m-smaller;
line-height: 1.4;
margin: 5px auto;
// This is the height of two lines of text. Often the placeholder text
// will span two lines–because this element will grow/shrink based on
// the contents of the text inside it, we set a minimum height so it
// won't shrink when the two-line placeholder is replaced with a single
// character when the user starts typing.
min-height: 51px;
padding: 5px;
resize: none;
width: 100%;
}

.ReportAbuseButton-buttons {
display: flex;
justify-content: space-between;
}

.ReportAbuseButton-dismiss-report {
align-self: center;
}

.ReportAbuseButton-dismiss-report--disabled:link {
color: $neutral-base-color;
cursor: not-allowed;
}
2 changes: 1 addition & 1 deletion src/core/api/abuse.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export function reportAddon(
auth: true,
endpoint: 'abuse/report/addon',
method: 'POST',
params: { addon: addonSlug, message },
body: { addon: addonSlug, message },
state: api,
});
}
1 change: 1 addition & 0 deletions src/core/css/inc/mixins.scss
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ $default-arrow-margin: 3px;
background: $background;
border: 1px solid $border-color;
border-radius: $border-radius;
cursor: pointer;
display: inline-block;
margin: 0;
min-height: 39px;
Expand Down