Implement ReportContent page

This commit is contained in:
infinite-persistence 2021-04-08 23:39:33 +08:00 committed by Sean Yesmunt
parent e5e4a5688f
commit cb2c33a35f
12 changed files with 881 additions and 0 deletions

View file

@ -10,6 +10,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
- Japanese, Afrikaans, Filipino, Thai and Vietnamese language support ([#5684](https://github.com/lbryio/lbry-desktop/issues/5684)) - Japanese, Afrikaans, Filipino, Thai and Vietnamese language support ([#5684](https://github.com/lbryio/lbry-desktop/issues/5684))
- Highlight comments made by content owner _community pr!_ ([#5744](https://github.com/lbryio/lbry-desktop/pull/5744)) - Highlight comments made by content owner _community pr!_ ([#5744](https://github.com/lbryio/lbry-desktop/pull/5744))
- Ability to report infringing content directly from the application ([#5808](https://github.com/lbryio/lbry-desktop/pull/5808))
### Changed ### Changed

View file

@ -1784,6 +1784,75 @@
"Credits sent": "Credits sent", "Credits sent": "Credits sent",
"Unresolved": "Unresolved", "Unresolved": "Unresolved",
"Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.": "Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.", "Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.": "Received amount did not match order code %chargeCode%. Contact hello@lbry.com to resolve the payment.",
"publish": "publish",
"Sexual content": "Sexual content",
"Violent or repulsive content": "Violent or repulsive content",
"Hateful or abusive content": "Hateful or abusive content",
"Harmful or dangerous acts": "Harmful or dangerous acts",
"Child abuse": "Child abuse",
"Promotes terrorism": "Promotes terrorism",
"Spam or misleading": "Spam or misleading",
"Infringes my rights": "Infringes my rights",
"Next": "Next",
"Graphic sexual activity": "Graphic sexual activity",
"Nudity": "Nudity",
"Suggestive, but without nudity": "Suggestive, but without nudity",
"Content involving minors": "Content involving minors",
"Sexual Abusive title or description": "Sexual Abusive title or description",
"Other sexual content": "Other sexual content",
"Timestamp": "Timestamp",
"Provide additional details": "Provide additional details",
"e.g. John Doe": "e.g. John Doe",
"e.g. john@example.com": "e.g. john@example.com",
"e.g. satoshi@example.com": "e.g. satoshi@example.com",
"Your channel": "Your channel",
"Set to \"Anonymous\" if you do not want to associate your channel in this report.": "Set to \"Anonymous\" if you do not want to associate your channel in this report.",
"Send report?": "Send report?",
"Send Report": "Send Report",
"Contact details": "Contact details",
"Report submitted": "Report submitted",
"We will review and respond shortly.": "We will review and respond shortly.",
"Adults fighting": "Adults fighting",
"Physical attack": "Physical attack",
"Youth violence": "Youth violence",
"Animal abuse": "Animal abuse",
"Promotes hatred or violence": "Promotes hatred or violence",
"Bullying": "Bullying",
"Hateful Abusive title or description": "Hateful Abusive title or description",
"Pharmaceutical or drug abuse": "Pharmaceutical or drug abuse",
"Abuse of fire or explosives": "Abuse of fire or explosives",
"Suicide or self injury": "Suicide or self injury",
"Other dangerous acts": "Other dangerous acts",
"Mass advertising": "Mass advertising",
"Pharmaceutical drugs for sale": "Pharmaceutical drugs for sale",
"Misleading text": "Misleading text",
"Misleading thumbnail": "Misleading thumbnail",
"Scams or fraud": "Scams or fraud",
"Copyright issue": "Copyright issue",
"Other legal issue": "Other legal issue",
"Affected party": "Affected party",
"Myself": "Myself",
"My company, organization, or client": "My company, organization, or client",
"Copyright owner name": "Copyright owner name",
"Relationship to copyrighted content": "Relationship to copyrighted content",
"(between 10 to 500 characters)": "(between 10 to 500 characters)",
"Remove now": "Remove now",
"Additional email (optional)": "Additional email (optional)",
"Phone number": "Phone number",
"Address": "Address",
"City": "City",
"State/province": "State/province",
"Zip code": "Zip code",
"Signature": "Signature",
"Your name": "Your name",
"Acting on behalf of": "Acting on behalf of",
"Self": "Self",
"Client": "Client",
"Title of specific law": "Title of specific law",
"Specific law": "Specific law",
"Law URL": "Law URL",
"Clarification": "Clarification",
"Client name": "Client name",
"We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.", "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.": "We apologize for this inconvenience, but have added this additional step to prevent abuse. Users on VPN or shared connections will continue to see this message and are not eligible for Rewards.",
"Help LBRY Save Crypto": "Help LBRY Save Crypto", "Help LBRY Save Crypto": "Help LBRY Save Crypto",
"The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?", "The US government is attempting to destroy the cryptocurrency industry. Can you help?": "The US government is attempting to destroy the cryptocurrency industry. Can you help?",

View file

@ -0,0 +1,29 @@
import { connect } from 'react-redux';
import { doReportContent } from 'redux/actions/reportContent';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { selectIsReportingContent, selectReportContentError } from 'redux/selectors/reportContent';
import { makeSelectClaimForClaimId, doClaimSearch } from 'lbry-redux';
import { withRouter } from 'react-router';
import ReportContent from './view';
const select = (state, props) => {
const { search } = props.location;
const urlParams = new URLSearchParams(search);
const claimId = urlParams.get('claimId');
return {
isReporting: selectIsReportingContent(state),
error: selectReportContentError(state),
activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state),
claimId: claimId,
claim: makeSelectClaimForClaimId(claimId)(state),
};
};
const perform = (dispatch) => ({
doClaimSearch: (options) => dispatch(doClaimSearch(options)),
doReportContent: (category, params) => dispatch(doReportContent(category, params)),
});
export default withRouter(connect(select, perform)(ReportContent));

View file

@ -0,0 +1,727 @@
// @flow
import React from 'react';
import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import Card from 'component/common/card';
import ClaimPreview from 'component/claimPreview';
import ChannelSelector from 'component/channelSelector';
import Spinner from 'component/spinner';
import ErrorText from 'component/common/error-text';
import Icon from 'component/common/icon';
import { COUNTRIES } from 'util/country';
import { EMAIL_REGEX } from 'constants/email';
import {
FF_MAX_CHARS_REPORT_CONTENT_DETAILS,
FF_MAX_CHARS_REPORT_CONTENT_SHORT,
FF_MAX_CHARS_REPORT_CONTENT_ADDRESS,
} from 'constants/form-field';
import * as REPORT_API from 'constants/report_content';
import * as ICONS from 'constants/icons';
import { useHistory } from 'react-router-dom';
const PAGE_TYPE = 'page--type';
const PAGE_CATEGORY = 'page--category';
const PAGE_INFRINGEMENT_DETAILS = 'page--infringement-details';
const PAGE_SUBMITTER_DETAILS = 'page--submitter-details';
const PAGE_SUBMITTER_DETAILS_ADDRESS = 'page--submitter-details-address';
const PAGE_CONFIRM = 'page--confirm';
const PAGE_SENT = 'page--sent';
const isDev = process.env.NODE_ENV !== 'production';
const DEFAULT_INPUT_DATA = {
// page: string,
type: '',
category: '',
timestamp: '',
additionalDetails: '',
email: '',
additional_email: '',
phone_number: '',
country: '',
street_address: '',
city: '',
state_or_province: '',
zip_code: '',
signature: '',
reporter_name: '',
acting_on_behalf_of: REPORT_API.BEHALF_SELF,
client_name: '',
specific_law: '',
law_url: '',
clarification: '',
affected_party: REPORT_API.PARTY_SELF,
copyright_owner_name: '',
relationship_to_copyrighted_content: '',
remove_now: true,
};
type Props = {
claimId: string,
claim: StreamClaim,
isReporting: boolean,
error: string,
activeChannelClaim: ?ChannelClaim,
incognito: boolean,
doClaimSearch: (any) => void,
doReportContent: (string, string) => void,
};
export default function ReportContent(props: Props) {
const { isReporting, error, activeChannelClaim, incognito, claimId, claim, doClaimSearch, doReportContent } = props;
const [input, setInput] = React.useState({ ...DEFAULT_INPUT_DATA });
const [page, setPage] = React.useState(PAGE_TYPE);
const [timestampInvalid, setTimestampInvalid] = React.useState(false);
const [isResolvingClaim, setIsResolvingClaim] = React.useState(false);
const { goBack } = useHistory();
// Resolve claim if URL is entered directly or if page is reloaded.
React.useEffect(() => {
if (!claim) {
setIsResolvingClaim(true);
doClaimSearch({
page_size: 20,
page: 1,
no_totals: true,
claim_ids: [claimId],
});
}
}, [claim, claimId]);
// On mount, pause player and get the timestamp, if applicable.
React.useEffect(() => {
if (window.player) {
window.player.pause();
const seconds = window.player.currentTime();
const h = Math.floor(seconds / 3600);
const m = Math.floor((seconds % 3600) / 60);
const s = Math.floor((seconds % 3600) % 60);
const str = (n) => n.toLocaleString('en-US', { minimumIntegerDigits: 2, useGrouping: false });
updateInput('timestamp', str(h) + ':' + str(m) + ':' + str(s));
}
}, []);
React.useEffect(() => {
let timer;
if (isResolvingClaim) {
timer = setTimeout(() => {
setIsResolvingClaim(false);
}, 3000);
}
return () => clearTimeout(timer);
}, [isResolvingClaim]);
function onSubmit() {
if (!claim) {
if (isDev) throw new Error('ReportContent::onSubmit -- null claim');
return;
}
const pushParam = (params, label, value, encode = true) => {
if (encode) {
params.push(`${label}=${encodeURIComponent(value)}`);
} else {
params.push(`${label}=${value}`);
}
};
const params = [];
pushParam(params, 'primary_email', input.email);
pushParam(params, 'claim_id', claim.claim_id, false);
pushParam(params, 'transaction_id', claim.txid, false);
pushParam(params, 'vout', claim.nout.toString(), false);
if (!incognito && activeChannelClaim) {
pushParam(params, 'channel_name', activeChannelClaim.name);
pushParam(params, 'channel_claim_id', activeChannelClaim.claim_id, false);
}
switch (input.type) {
case REPORT_API.INFRINGES_MY_RIGHTS:
pushParam(params, 'signature', input.signature);
switch (input.category) {
case REPORT_API.COPYRIGHT_ISSUES:
pushParam(params, 'additional_email', input.additional_email);
pushParam(params, 'phone_number', input.phone_number);
pushParam(params, 'country', input.country);
pushParam(params, 'street_address', input.street_address);
pushParam(params, 'city', input.city);
pushParam(params, 'state_or_province', input.state_or_province);
pushParam(params, 'zip_code', input.zip_code);
pushParam(params, 'affected_party', input.affected_party);
pushParam(params, 'copyright_owner_name', input.copyright_owner_name);
pushParam(params, 'relationship_to_copyrighted_content', input.relationship_to_copyrighted_content);
pushParam(params, 'remove_now', input.remove_now.toString(), false);
break;
case REPORT_API.OTHER_LEGAL_ISSUES:
pushParam(params, 'reporter_name', input.reporter_name);
pushParam(params, 'acting_on_behalf_of', input.acting_on_behalf_of);
pushParam(params, 'specific_law', input.specific_law);
pushParam(params, 'law_url', input.law_url);
pushParam(params, 'clarification', input.clarification);
if (input.acting_on_behalf_of === REPORT_API.BEHALF_CLIENT) {
pushParam(params, 'client_name', input.client_name);
}
break;
}
break;
default:
pushParam(params, 'type', input.type);
pushParam(params, 'category', input.category);
pushParam(params, 'additional_details', input.additionalDetails);
if (includeTimestamp(claim)) {
pushParam(params, 'timestamp', input.timestamp);
}
break;
}
doReportContent(input.category, params.join('&'));
}
function updateInput(field: string, value: any) {
let newInput = input;
if (isDev && newInput[field] === undefined) {
throw new Error('Unexpected field: ' + field);
}
newInput[field] = value;
if (field === 'type') {
newInput['category'] = '';
}
setInput({ ...newInput });
}
function isInfringementDetailsValid(type: string, category: string) {
switch (type) {
case REPORT_API.INFRINGES_MY_RIGHTS:
switch (category) {
case REPORT_API.COPYRIGHT_ISSUES:
return (
input.affected_party &&
input.copyright_owner_name &&
input.relationship_to_copyrighted_content &&
input.relationship_to_copyrighted_content.length > REPORT_API.RELATIONSHIP_FIELD_MIN_WIDTH
);
case REPORT_API.OTHER_LEGAL_ISSUES:
return (
input.reporter_name &&
input.specific_law &&
input.law_url &&
input.acting_on_behalf_of &&
(input.acting_on_behalf_of === REPORT_API.BEHALF_SELF || input.client_name) &&
input.clarification
);
default:
return false;
}
default:
return (!includeTimestamp(claim) || isTimestampValid(input.timestamp)) && input.additionalDetails;
}
}
function isSubmitterDetailsValid(type: string, category: string) {
if (category === REPORT_API.COPYRIGHT_ISSUES) {
return (
input.email.match(EMAIL_REGEX) &&
(!input.additional_email || input.additional_email.match(EMAIL_REGEX)) &&
input.phone_number
);
}
return input.email.match(EMAIL_REGEX);
}
function isSubmitterDetailsAddressValid() {
return input.street_address && input.city && input.state_or_province && input.zip_code && input.country;
}
function isTimestampValid(timestamp: string) {
if (timestamp === '0') {
return true;
}
const length = timestamp.length;
if (length <= 4) {
return timestamp.match(/\d:[0-5]\d/);
} else if (length <= 5) {
return timestamp.match(/[0-5]\d:[0-5]\d/);
} else if (length <= 8) {
return timestamp.match(/\d{1,2}:[0-5]\d:\d\d/);
} else {
return false;
}
}
function includeTimestamp(claim: StreamClaim) {
return (
claim.value_type === 'stream' && (claim.value.stream_type === 'video' || claim.value.stream_type === 'audio')
);
}
function getActionElem() {
let body;
switch (page) {
case PAGE_TYPE:
return (
<>
<div className="section section--vertical-compact">
<fieldset>
{Object.keys(REPORT_API.PARAMETERS['type']).map((x) => {
return (
<FormField
type="radio"
name={x}
key={x}
label={__(String(x))}
checked={x === input.type}
onChange={() => updateInput('type', x)}
/>
);
})}
</fieldset>
</div>
<div className="section__actions">
<Button
button="primary"
label={__('Next')}
disabled={input.type === ''}
onClick={() => setPage(PAGE_CATEGORY)}
/>
</div>
</>
);
case PAGE_CATEGORY:
if (!input.type) {
return null;
} else {
return (
<>
<div className="section">
<div>
<b>{__(input.type)}</b>
</div>
<div className="section section--vertical-compact">
<fieldset>
{REPORT_API.PARAMETERS['type'][input.type][REPORT_API.CATEGORIES].map((x) => {
return (
<FormField
type="radio"
name={x}
key={x}
label={__(String(x))}
checked={input.category === x}
onChange={() => updateInput('category', x)}
/>
);
})}
</fieldset>
</div>
</div>
<div className="section__actions">
<Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_TYPE)} />
<Button
button="primary"
label={__('Next')}
disabled={!input.category || input.category === ''}
onClick={() => setPage(PAGE_INFRINGEMENT_DETAILS)}
/>
</div>
</>
);
}
case PAGE_INFRINGEMENT_DETAILS:
switch (input.type) {
case REPORT_API.INFRINGES_MY_RIGHTS:
switch (input.category) {
case REPORT_API.COPYRIGHT_ISSUES:
body = (
<div className="section section--vertical-compact">
<FormField
type="select"
name="affected_party"
label={__('Affected party')}
value={input.affected_party}
onChange={(e) => updateInput('affected_party', e.target.value)}
blockWrap={false}
>
<option key={REPORT_API.PARTY_SELF} value={REPORT_API.PARTY_SELF}>
{__(REPORT_API.PARTY_SELF)}
</option>
<option key={REPORT_API.PARTY_GROUP} value={REPORT_API.PARTY_GROUP}>
{__(REPORT_API.PARTY_GROUP)}
</option>
</FormField>
<FormField
type="text"
name="copyright_owner_name"
label={__('Copyright owner name')}
value={input.copyright_owner_name}
maxlength={REPORT_API.COPYRIGHT_OWNER_MAX_LENGTH}
onChange={(e) => updateInput('copyright_owner_name', e.target.value)}
/>
<FormField
type="textarea"
name="relationship_to_copyrighted_content"
label={__('Relationship to copyrighted content')}
placeholder={__('(between 10 to 500 characters)')}
value={input.relationship_to_copyrighted_content}
onChange={(e) => updateInput('relationship_to_copyrighted_content', e.target.value)}
charCount={input.relationship_to_copyrighted_content.length}
textAreaMaxLength={FF_MAX_CHARS_REPORT_CONTENT_DETAILS}
/>
<FormField
type="checkbox"
name="remove_now"
label={__('Remove now')}
checked={input.remove_now}
onChange={() => updateInput('remove_now', !input.remove_now)}
/>
</div>
);
break;
case REPORT_API.OTHER_LEGAL_ISSUES:
body = (
<div className="section section--vertical-compact">
<FormField
type="text"
name="reporter_name"
label={__('Your name')}
placeholder={__('Your name')}
value={input.reporter_name}
onChange={(e) => updateInput('reporter_name', e.target.value)}
/>
<FormField
type="select"
name="acting_on_behalf_of"
label={__('Acting on behalf of')}
value={input.acting_on_behalf_of}
onChange={(e) => updateInput('acting_on_behalf_of', e.target.value)}
blockWrap={false}
>
<option key={REPORT_API.BEHALF_SELF} value={REPORT_API.BEHALF_SELF}>
{__('Self')}
</option>
<option key={REPORT_API.BEHALF_CLIENT} value={REPORT_API.BEHALF_CLIENT}>
{__('Client')}
</option>
</FormField>
{input.acting_on_behalf_of === REPORT_API.BEHALF_CLIENT && (
<FormField
type="text"
name="client_name"
label={__('Client name')}
placeholder={__('Client name')}
value={input.client_name}
onChange={(e) => updateInput('client_name', e.target.value)}
/>
)}
<FormField
type="text"
name="specific_law"
label={__('Title of specific law')}
placeholder={__('Specific law')}
value={input.specific_law}
onChange={(e) => updateInput('specific_law', e.target.value)}
/>
<FormField
type="text"
name="law_url"
label={__('Law URL')}
placeholder={'www.lawsrus.com/copyright-act-2030'}
value={input.law_url}
onChange={(e) => updateInput('law_url', e.target.value)}
/>
<FormField
type="textarea"
name="clarification"
label={__('Clarification')}
placeholder={__('Provide additional details')}
value={input.clarification}
textAreaMaxLength={FF_MAX_CHARS_REPORT_CONTENT_DETAILS}
onChange={(e) => updateInput('clarification', e.target.value)}
/>
</div>
);
break;
default:
if (isDev) throw new Error('Unhandled category for DMCA: ' + input.category);
body = null;
break;
}
break;
default:
body = (
<>
{includeTimestamp(claim) && (
<div className="section">
<FormField
type="tel"
name="timestamp"
label={__('Timestamp')}
placeholder={'00:23:59'}
pattern="[0-9]{3}"
value={input.timestamp}
maxlength={8}
onChange={(e) => {
updateInput('timestamp', e.target.value);
setTimestampInvalid(!isTimestampValid(e.target.value));
}}
error={timestampInvalid && input.timestamp ? 'Invalid timestamp (e.g. 05:23, 00:23:59)' : ''}
/>
</div>
)}
<div className="section">
<FormField
type="textarea"
name="additional_details"
value={input.additionalDetails}
onChange={(e) => updateInput('additionalDetails', e.target.value)}
charCount={input.additionalDetails.length}
textAreaMaxLength={FF_MAX_CHARS_REPORT_CONTENT_DETAILS}
placeholder={__('Provide additional details')}
/>
</div>
</>
);
}
return (
<>
{body}
<div className="section__actions">
<Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_CATEGORY)} />
<Button
button="primary"
label={__('Next')}
disabled={!isInfringementDetailsValid(input.type, input.category)}
onClick={() => setPage(PAGE_SUBMITTER_DETAILS)}
/>
</div>
</>
);
case PAGE_SUBMITTER_DETAILS:
return (
<>
<div className="section">
<div className="section">
<FormField
type="email"
name="primary_email"
label={__('Email')}
placeholder={__('e.g. john@example.com')}
value={input.email}
onChange={(e) => updateInput('email', e.target.value)}
/>
{input.category === REPORT_API.COPYRIGHT_ISSUES && (
<>
<FormField
type="email"
name="additional_email"
label={__('Additional email (optional)')}
placeholder={__('e.g. satoshi@example.com')}
value={input.additional_email}
onChange={(e) => updateInput('additional_email', e.target.value)}
/>
<FormField
type="text"
name="phone_number"
label={__('Phone number')}
placeholder={'e.g. +1 (xxx) xxx-xx-xx'}
value={input.phone_number}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('phone_number', e.target.value)}
/>
</>
)}
</div>
<div className="section">
<label>{__('Your channel')}</label>
<Icon
className="icon--help"
icon={ICONS.HELP}
tooltip
size={16}
customTooltipText={__(
'Set to "Anonymous" if you do not want to associate your channel in this report.'
)}
/>
<ChannelSelector />
</div>
</div>
<div className="section__actions">
<Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_INFRINGEMENT_DETAILS)} />
<Button
button="primary"
label={__('Next')}
disabled={!isSubmitterDetailsValid(input.type, input.category)}
onClick={() =>
setPage(
input.category === REPORT_API.COPYRIGHT_ISSUES ? PAGE_SUBMITTER_DETAILS_ADDRESS : PAGE_CONFIRM
)
}
/>
</div>
</>
);
case PAGE_SUBMITTER_DETAILS_ADDRESS:
return (
<>
<div className="section section--vertical-compact">
<FormField
type="text"
name="street_address"
label={__('Address')}
placeholder={'Street address'}
value={input.street_address}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_ADDRESS}
onChange={(e) => updateInput('street_address', e.target.value)}
/>
<FormField
type="text"
name="city"
placeholder={__('City')}
value={input.city}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('city', e.target.value)}
/>
<FormField
type="text"
name="state_or_province"
placeholder={__('State/province')}
value={input.state_or_province}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('state_or_province', e.target.value)}
/>
<FormField
type="text"
name="zip_code"
placeholder={__('Zip code')}
value={input.zip_code}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('zip_code', e.target.value)}
/>
<FormField
type="select"
name="country"
label={__('Country')}
value={input.country}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('country', e.target.value)}
>
<option value="" disabled defaultValue>
{__('Select your country')}
</option>
{COUNTRIES.map((country) => (
<option key={country} value={country}>
{country}
</option>
))}
</FormField>
</div>
<div className="section__actions">
<Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_SUBMITTER_DETAILS)} />
<Button
button="primary"
label={__('Next')}
disabled={!isSubmitterDetailsAddressValid()}
onClick={() => setPage(PAGE_CONFIRM)}
/>
</div>
</>
);
case PAGE_CONFIRM:
return (
<>
<div className="section section--padded card--inline confirm__wrapper">
<div className="confirm__label">{__('Contact details')}</div>
<div className="confirm__value">{input.email}</div>
{input.type === REPORT_API.INFRINGES_MY_RIGHTS && (
<FormField
type="text"
name="signature"
label={__('Signature')}
placeholder={__('e.g. John Doe')}
value={input.signature}
maxlength={FF_MAX_CHARS_REPORT_CONTENT_SHORT}
onChange={(e) => updateInput('signature', e.target.value)}
/>
)}
</div>
<div className="section">{__('Send report?')}</div>
<div className="section__actions">
<Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_SUBMITTER_DETAILS)} />
<Button
button="primary"
label={__('Send Report')}
disabled={input.type === REPORT_API.INFRINGES_MY_RIGHTS ? !input.signature : false}
onClick={() => {
onSubmit();
setPage(PAGE_SENT);
}}
/>
</div>
</>
);
case PAGE_SENT:
if (isReporting) {
body = <Spinner />;
} else if (error) {
body = (
<div className="error__wrapper--no-overflow">
<ErrorText>{error}</ErrorText>
</div>
);
} else {
body = (
<>
<div className="section__title">{__('Report submitted')}</div>
<div className="section">{__('We will review and respond shortly.')}</div>
</>
);
}
return (
<>
<div className="section">{body}</div>
<div className="section__actions">
{error && <Button button="alt" label={__('Back')} onClick={() => setPage(PAGE_CONFIRM)} />}
<Button button="primary" label={__('Close')} disabled={isReporting} onClick={() => goBack()} />
</div>
</>
);
}
}
function getClaimPreview(claim: StreamClaim) {
return (
<div className="section content__non-clickable">
<ClaimPreview uri={claim.permanent_url} hideMenu hideActions type="small" />
</div>
);
}
return (
<Form onSubmit={onSubmit}>
<Card
title={claim && claim.value_type === 'channel' ? __('Report channel') : __('Report content')}
subtitle={claim ? getClaimPreview(claim) : null}
actions={claim ? getActionElem() : isResolvingClaim ? <Spinner /> : __('Invalid claim ID')}
/>
</Form>
);
}

View file

@ -13,6 +13,7 @@ import BackupPage from 'page/backup';
import Code2257Page from 'web/page/code2257'; import Code2257Page from 'web/page/code2257';
// @endif // @endif
import ReportPage from 'page/report'; import ReportPage from 'page/report';
import ReportContentPage from 'page/reportContent';
import ShowPage from 'page/show'; import ShowPage from 'page/show';
import PublishPage from 'page/publish'; import PublishPage from 'page/publish';
import DiscoverPage from 'page/discover'; import DiscoverPage from 'page/discover';
@ -252,6 +253,7 @@ function AppRouter(props: Props) {
<Route path={`/$/${PAGES.SETTINGS_ADVANCED}`} exact component={SettingsAdvancedPage} /> <Route path={`/$/${PAGES.SETTINGS_ADVANCED}`} exact component={SettingsAdvancedPage} />
<Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} /> <Route path={`/$/${PAGES.INVITE}/:referrer`} exact component={InvitedPage} />
<Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} /> <Route path={`/$/${PAGES.CHECKOUT}`} exact component={CheckoutPage} />
<Route path={`/$/${PAGES.REPORT_CONTENT}`} exact component={ReportContentPage} />
<PrivateRoute {...props} exact path={`/$/${PAGES.YOUTUBE_SYNC}`} component={YoutubeSyncPage} /> <PrivateRoute {...props} exact path={`/$/${PAGES.YOUTUBE_SYNC}`} component={YoutubeSyncPage} />
<PrivateRoute {...props} exact path={`/$/${PAGES.TAGS_FOLLOWING}`} component={TagsFollowingPage} /> <PrivateRoute {...props} exact path={`/$/${PAGES.TAGS_FOLLOWING}`} component={TagsFollowingPage} />

View file

@ -1,3 +1,6 @@
export const FF_MAX_CHARS_DEFAULT = 2000; export const FF_MAX_CHARS_DEFAULT = 2000;
export const FF_MAX_CHARS_IN_COMMENT = 2000; export const FF_MAX_CHARS_IN_COMMENT = 2000;
export const FF_MAX_CHARS_IN_DESCRIPTION = 5000; export const FF_MAX_CHARS_IN_DESCRIPTION = 5000;
export const FF_MAX_CHARS_REPORT_CONTENT_DETAILS = 500;
export const FF_MAX_CHARS_REPORT_CONTENT_ADDRESS = 255;
export const FF_MAX_CHARS_REPORT_CONTENT_SHORT = 100;

View file

@ -29,6 +29,7 @@ exports.UPLOAD = 'upload';
exports.UPLOADS = 'uploads'; exports.UPLOADS = 'uploads';
exports.GET_CREDITS = 'getcredits'; exports.GET_CREDITS = 'getcredits';
exports.REPORT = 'report'; exports.REPORT = 'report';
exports.REPORT_CONTENT = 'report_content';
exports.REWARDS = 'rewards'; exports.REWARDS = 'rewards';
exports.REWARDS_VERIFY = 'rewards/verify'; exports.REWARDS_VERIFY = 'rewards/verify';
exports.REPOST_NEW = 'repost'; exports.REPOST_NEW = 'repost';

View file

@ -0,0 +1,8 @@
import { connect } from 'react-redux';
import ReportContentPage from './view';
const select = (state) => ({});
const perform = (dispatch) => ({});
export default connect(select, perform)(ReportContentPage);

View file

@ -0,0 +1,19 @@
// @flow
import React from 'react';
import Page from 'component/page';
import ReportContent from 'component/reportContent';
export default function ReportContentPage(props: any) {
return (
<Page
noSideNavigation
className="main--report-content"
backout={{
backoutLabel: __('Done'),
title: __('Report content'),
}}
>
<ReportContent />
</Page>
);
}

View file

@ -190,3 +190,7 @@
.content__loading-text { .content__loading-text {
color: var(--color-white); color: var(--color-white);
} }
.content__non-clickable {
pointer-events: none;
}

View file

@ -266,6 +266,11 @@
max-width: 34rem; max-width: 34rem;
} }
.main--report-content {
@extend .main--auth-page;
max-width: 40rem;
}
.main--empty { .main--empty {
align-self: center; align-self: center;
display: flex; display: flex;

View file

@ -14,6 +14,19 @@
max-width: 40rem; max-width: 40rem;
} }
.section--vertical-compact {
fieldset-section,
fieldset-section.radio,
div.checkbox,
div.section {
margin-top: var(--spacing-s);
}
.channel__selector {
margin-bottom: var(--spacing-s);
}
}
.section--help { .section--help {
border-radius: var(--border-radius); border-radius: var(--border-radius);
background-color: var(--color-primary-alt); background-color: var(--color-primary-alt);