ReportContent: add ?commentId support (#1826)

## Ticket
1822

## Changes
- Add `?commentId=` support in the URL.
- `claimId` remains a required parameter so that we can derive the comment URL.
    - The backend will know it's a comment report when both parameters are present.
- The comment URL is added to the top of `additional_details`.
- The backend rejects if `additional_details` is provided for DMCA. Grayed out DMCA for the case of reporting comments.
This commit is contained in:
infinite-persistence 2022-07-13 01:44:34 +08:00 committed by GitHub
parent 7b89ad8c14
commit 68a4697c7d
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 106 additions and 18 deletions

View file

@ -31,6 +31,7 @@
"Loop": "Loop", "Loop": "Loop",
"Report content": "Report content", "Report content": "Report content",
"Report Content": "Report Content", "Report Content": "Report Content",
"Report comment": "Report comment",
"Languages": "Languages", "Languages": "Languages",
"Media Type": "Media Type", "Media Type": "Media Type",
"License": "License", "License": "License",
@ -411,6 +412,8 @@
"Loading": "Loading", "Loading": "Loading",
"This file is in your library.": "This file is in your library.", "This file is in your library.": "This file is in your library.",
"Invalid claim ID": "Invalid claim ID", "Invalid claim ID": "Invalid claim ID",
"Invalid comment ID": "Invalid comment ID",
"Missing 'claimId' parameter.": "Missing 'claimId' parameter.",
"View Tag": "View Tag", "View Tag": "View Tag",
"Hide": "Hide", "Hide": "Hide",
"Close": "Close", "Close": "Close",

View file

@ -1,6 +1,8 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { doCommentById } from 'redux/actions/comments';
import { doReportContent } from 'redux/actions/reportContent'; import { doReportContent } from 'redux/actions/reportContent';
import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app'; import { selectActiveChannelClaim, selectIncognito } from 'redux/selectors/app';
import { selectCommentForCommentId } from 'redux/selectors/comments';
import { selectIsReportingContent, selectReportContentError } from 'redux/selectors/reportContent'; import { selectIsReportingContent, selectReportContentError } from 'redux/selectors/reportContent';
import { doClaimSearch } from 'redux/actions/claims'; import { doClaimSearch } from 'redux/actions/claims';
import { selectClaimForClaimId } from 'redux/selectors/claims'; import { selectClaimForClaimId } from 'redux/selectors/claims';
@ -11,19 +13,23 @@ const select = (state, props) => {
const { search } = props.location; const { search } = props.location;
const urlParams = new URLSearchParams(search); const urlParams = new URLSearchParams(search);
const claimId = urlParams.get('claimId'); const claimId = urlParams.get('claimId');
const commentId = urlParams.get('commentId');
return { return {
claimId,
commentId,
isReporting: selectIsReportingContent(state), isReporting: selectIsReportingContent(state),
error: selectReportContentError(state), error: selectReportContentError(state),
activeChannelClaim: selectActiveChannelClaim(state), activeChannelClaim: selectActiveChannelClaim(state),
incognito: selectIncognito(state), incognito: selectIncognito(state),
claimId: claimId,
claim: selectClaimForClaimId(state, claimId), claim: selectClaimForClaimId(state, claimId),
comment: selectCommentForCommentId(state, commentId),
}; };
}; };
const perform = { const perform = {
doClaimSearch, doClaimSearch,
doCommentById,
doReportContent, doReportContent,
}; };

View file

@ -4,11 +4,13 @@ import Button from 'component/button';
import { Form, FormField } from 'component/common/form'; import { Form, FormField } from 'component/common/form';
import Card from 'component/common/card'; import Card from 'component/common/card';
import ClaimPreview from 'component/claimPreview'; import ClaimPreview from 'component/claimPreview';
import Comment from 'component/comment';
import ChannelSelector from 'component/channelSelector'; import ChannelSelector from 'component/channelSelector';
import Spinner from 'component/spinner'; import Spinner from 'component/spinner';
import ErrorText from 'component/common/error-text'; import ErrorText from 'component/common/error-text';
import Icon from 'component/common/icon'; import Icon from 'component/common/icon';
import { COUNTRIES } from 'util/country'; import { COUNTRIES } from 'util/country';
import { URL } from 'config';
import { EMAIL_REGEX } from 'constants/email'; import { EMAIL_REGEX } from 'constants/email';
import { import {
FF_MAX_CHARS_REPORT_CONTENT_DETAILS, FF_MAX_CHARS_REPORT_CONTENT_DETAILS,
@ -59,23 +61,39 @@ const DEFAULT_INPUT_DATA = {
type Props = { type Props = {
// --- urlParams --- // --- urlParams ---
claimId: string, claimId: string,
commentId?: string,
// --- redux --- // --- redux ---
claim: StreamClaim, claim: StreamClaim,
comment?: Comment,
isReporting: boolean, isReporting: boolean,
error: string, error: string,
activeChannelClaim: ?ChannelClaim, activeChannelClaim: ?ChannelClaim,
incognito: boolean, incognito: boolean,
doClaimSearch: (any) => Promise<any>, doClaimSearch: (any) => Promise<any>,
doCommentById: (string, boolean) => Promise<any>,
doReportContent: (string, string) => void, doReportContent: (string, string) => void,
}; };
export default function ReportContent(props: Props) { export default function ReportContent(props: Props) {
const { isReporting, error, activeChannelClaim, incognito, claimId, claim, doClaimSearch, doReportContent } = props; const {
isReporting,
error,
activeChannelClaim,
incognito,
claimId,
commentId,
claim,
comment,
doClaimSearch,
doCommentById,
doReportContent,
} = props;
const [input, setInput] = React.useState({ ...DEFAULT_INPUT_DATA }); const [input, setInput] = React.useState({ ...DEFAULT_INPUT_DATA });
const [page, setPage] = React.useState(PAGE_TYPE); const [page, setPage] = React.useState(PAGE_TYPE);
const [timestampInvalid, setTimestampInvalid] = React.useState(false); const [timestampInvalid, setTimestampInvalid] = React.useState(false);
const [isResolvingClaim, setIsResolvingClaim] = React.useState(false); const [isResolvingClaim, setIsResolvingClaim] = React.useState(false);
const [isResolvingComment, setIsResolvingComment] = React.useState(false);
const { goBack } = useHistory(); const { goBack } = useHistory();
// Resolve claim if URL is entered directly or if page is reloaded. // Resolve claim if URL is entered directly or if page is reloaded.
@ -93,6 +111,16 @@ export default function ReportContent(props: Props) {
} }
}, [claim, claimId, doClaimSearch]); }, [claim, claimId, doClaimSearch]);
// Fetch comment if `commentId` is provided
React.useEffect(() => {
if (commentId) {
setIsResolvingComment(true);
doCommentById(commentId, false).finally(() => {
setIsResolvingComment(false);
});
}
}, [commentId, doCommentById]);
// On mount, pause player and get the timestamp, if applicable. // On mount, pause player and get the timestamp, if applicable.
React.useEffect(() => { React.useEffect(() => {
if (window.player) { if (window.player) {
@ -109,6 +137,20 @@ export default function ReportContent(props: Props) {
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
}, []); }, []);
function getCommentUrl(contentClaim, commentId) {
if (commentId) {
if (contentClaim && contentClaim.canonical_url) {
const canonical = contentClaim.canonical_url.replace(/#/g, ':');
const commentUrl = `${canonical.replace('lbry://', `${URL}/`)}?lc=${commentId}`;
return commentUrl + '\n\n';
} else {
return commentId + '\n\n';
}
} else {
return '';
}
}
function onSubmit() { function onSubmit() {
if (!claim) { if (!claim) {
if (isDev) throw new Error('ReportContent::onSubmit -- null claim'); if (isDev) throw new Error('ReportContent::onSubmit -- null claim');
@ -168,8 +210,8 @@ export default function ReportContent(props: Props) {
default: default:
pushParam(params, 'type', input.type); pushParam(params, 'type', input.type);
pushParam(params, 'category', input.category); pushParam(params, 'category', input.category);
pushParam(params, 'additional_details', input.additionalDetails); pushParam(params, 'additional_details', getCommentUrl(claim, commentId) + input.additionalDetails);
if (includeTimestamp(claim)) { if (includeTimestamp(claim, commentId)) {
pushParam(params, 'timestamp', input.timestamp); pushParam(params, 'timestamp', input.timestamp);
} }
break; break;
@ -216,7 +258,7 @@ export default function ReportContent(props: Props) {
return false; return false;
} }
default: default:
return (!includeTimestamp(claim) || isTimestampValid(input.timestamp)) && input.additionalDetails; return (!includeTimestamp(claim, commentId) || isTimestampValid(input.timestamp)) && input.additionalDetails;
} }
} }
@ -253,9 +295,11 @@ export default function ReportContent(props: Props) {
} }
} }
function includeTimestamp(claim: StreamClaim) { function includeTimestamp(claim: StreamClaim, commentId: ?string) {
return ( return (
claim.value_type === 'stream' && (claim.value.stream_type === 'video' || claim.value.stream_type === 'audio') !commentId &&
claim.value_type === 'stream' &&
(claim.value.stream_type === 'video' || claim.value.stream_type === 'audio')
); );
} }
@ -276,6 +320,7 @@ export default function ReportContent(props: Props) {
key={x} key={x}
label={__(String(x))} label={__(String(x))}
checked={x === input.type} checked={x === input.type}
disabled={x === REPORT_API.INFRINGES_MY_RIGHTS && commentId}
onChange={() => updateInput('type', x)} onChange={() => updateInput('type', x)}
/> />
); );
@ -461,7 +506,7 @@ export default function ReportContent(props: Props) {
default: default:
body = ( body = (
<> <>
{includeTimestamp(claim) && ( {includeTimestamp(claim, commentId) && (
<div className="section"> <div className="section">
<FormField <FormField
type="text" type="text"
@ -706,20 +751,54 @@ export default function ReportContent(props: Props) {
} }
function getClaimPreview(claim: StreamClaim) { function getClaimPreview(claim: StreamClaim) {
return ( return claim ? (
<div className="section"> <div className="section">
<ClaimPreview uri={claim.permanent_url} hideMenu hideActions nonClickable type="small" /> <ClaimPreview uri={claim.permanent_url} hideMenu hideActions nonClickable type="small" />
</div> </div>
) : null;
}
function getCommentPreviews(comment: ?Comment) {
return comment ? (
<div className="section non-clickable">
<Comment comment={comment} isTopLevel hideActions hideContextMenu />
</div>
) : null;
}
// **************************************************************************
// **************************************************************************
// --- Report comment ---
if (commentId) {
if (!claimId) {
return <Card title={__('Report comment')} subtitle={__("Missing 'claimId' parameter.")} />;
} else {
return (
<Form onSubmit={onSubmit}>
<Card
title={__('Report comment')}
subtitle={getCommentPreviews(comment)}
actions={comment ? getActionElem() : isResolvingComment ? <Spinner /> : __('Invalid comment ID')}
/>
</Form>
);
}
}
// --- Report content ---
if (claimId) {
return (
<Form onSubmit={onSubmit}>
<Card
title={claim && claim.value_type === 'channel' ? __('Report channel') : __('Report content')}
subtitle={getClaimPreview(claim)}
actions={claim ? getActionElem() : isResolvingClaim ? <Spinner /> : __('Invalid claim ID')}
/>
</Form>
); );
} }
return ( // --- Invalid ---
<Form onSubmit={onSubmit}> return <Card title={__('Report content')} subtitle={__('Invalid parameters.')} />;
<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>
);
} }