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

View file

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

View file

@ -4,11 +4,13 @@ import Button from 'component/button';
import { Form, FormField } from 'component/common/form';
import Card from 'component/common/card';
import ClaimPreview from 'component/claimPreview';
import Comment from 'component/comment';
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 { URL } from 'config';
import { EMAIL_REGEX } from 'constants/email';
import {
FF_MAX_CHARS_REPORT_CONTENT_DETAILS,
@ -59,23 +61,39 @@ const DEFAULT_INPUT_DATA = {
type Props = {
// --- urlParams ---
claimId: string,
commentId?: string,
// --- redux ---
claim: StreamClaim,
comment?: Comment,
isReporting: boolean,
error: string,
activeChannelClaim: ?ChannelClaim,
incognito: boolean,
doClaimSearch: (any) => Promise<any>,
doCommentById: (string, boolean) => Promise<any>,
doReportContent: (string, string) => void,
};
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 [page, setPage] = React.useState(PAGE_TYPE);
const [timestampInvalid, setTimestampInvalid] = React.useState(false);
const [isResolvingClaim, setIsResolvingClaim] = React.useState(false);
const [isResolvingComment, setIsResolvingComment] = React.useState(false);
const { goBack } = useHistory();
// 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]);
// 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.
React.useEffect(() => {
if (window.player) {
@ -109,6 +137,20 @@ export default function ReportContent(props: Props) {
// 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() {
if (!claim) {
if (isDev) throw new Error('ReportContent::onSubmit -- null claim');
@ -168,8 +210,8 @@ export default function ReportContent(props: Props) {
default:
pushParam(params, 'type', input.type);
pushParam(params, 'category', input.category);
pushParam(params, 'additional_details', input.additionalDetails);
if (includeTimestamp(claim)) {
pushParam(params, 'additional_details', getCommentUrl(claim, commentId) + input.additionalDetails);
if (includeTimestamp(claim, commentId)) {
pushParam(params, 'timestamp', input.timestamp);
}
break;
@ -216,7 +258,7 @@ export default function ReportContent(props: Props) {
return false;
}
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 (
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}
label={__(String(x))}
checked={x === input.type}
disabled={x === REPORT_API.INFRINGES_MY_RIGHTS && commentId}
onChange={() => updateInput('type', x)}
/>
);
@ -461,7 +506,7 @@ export default function ReportContent(props: Props) {
default:
body = (
<>
{includeTimestamp(claim) && (
{includeTimestamp(claim, commentId) && (
<div className="section">
<FormField
type="text"
@ -706,20 +751,54 @@ export default function ReportContent(props: Props) {
}
function getClaimPreview(claim: StreamClaim) {
return (
return claim ? (
<div className="section">
<ClaimPreview uri={claim.permanent_url} hideMenu hideActions nonClickable type="small" />
</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 (
<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>
);
// --- Invalid ---
return <Card title={__('Report content')} subtitle={__('Invalid parameters.')} />;
}