Skip to content

Commit

Permalink
Add Report button to comments and for now just a basic display on adm…
Browse files Browse the repository at this point in the history
…in page
  • Loading branch information
mdirolf committed Feb 19, 2024
1 parent 8f93e5a commit 483b2dd
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 5 deletions.
35 changes: 30 additions & 5 deletions app/components/Comments.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,9 @@ import { Timestamp } from '../lib/timestamp';
import { getCollection, getDocRef } from '../lib/firebaseWrapper';
import { addDoc, updateDoc } from 'firebase/firestore';
import type { Root } from 'hast';
import { ReportOverlay } from './ReportOverlay';

const COMMENT_LENGTH_LIMIT = 2048;
export const COMMENT_LENGTH_LIMIT = 2048;

interface LocalComment extends Omit<Comment, 'replies'> {
isLocal: true;
Expand Down Expand Up @@ -85,6 +86,7 @@ const CommentWithReplies = (
}
) => {
const [showingForm, setShowingForm] = useState(false);
const [showingReportOverlay, setShowingReportOverlay] = useState(false);
const commentId = isComment(props.comment) ? props.comment.id : null;
const replies = isComment(props.comment) ? props.comment.replies : undefined;
return (
Expand All @@ -95,9 +97,18 @@ const CommentWithReplies = (
puzzleAuthorId={props.puzzleAuthorId}
comment={props.comment}
>
{!props.user || props.user.isAnonymous || !commentId ? (
{showingReportOverlay ? (
<ReportOverlay
puzzleId={props.puzzleId}
comment={props.comment}
closeOverlay={() => {
setShowingReportOverlay(false);
}}
/>
) : (
''
) : showingForm ? (
)}
{showingForm && props.user && !props.user.isAnonymous && commentId ? (
<div css={{ marginLeft: '2em' }}>
<CommentForm
{...props}
Expand All @@ -111,14 +122,28 @@ const CommentWithReplies = (
</div>
) : (
<div>
{!props.user || props.user.isAnonymous || !commentId ? (
''
) : (
<>
<ButtonAsLink
onClick={() => {
setShowingForm(true);
}}
text={t`Reply`}
/>{' '}
&middot;{' '}
</>
)}
<ButtonAsLink
onClick={() => {
setShowingForm(true);
setShowingReportOverlay(true);
}}
text={t`Reply`}
text={t`Report`}
/>
</div>
)}

{replies ? (
<ul
css={{
Expand Down
159 changes: 159 additions & 0 deletions app/components/ReportOverlay.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
import { Markdown } from './Markdown';
import { Overlay } from './Overlay';
import { LengthLimitedTextarea, LengthView } from './Inputs';
import { COMMENT_LENGTH_LIMIT } from './Comments';
import { FormEvent, useContext, useState } from 'react';
import { logAsyncErrors } from '../lib/utils';
import * as t from 'io-ts';
import { timestamp, Timestamp } from '../lib/timestamp';
import { Comment } from '../lib/types';
import { AuthContext } from './AuthContext';
import { setDoc } from 'firebase/firestore';
import { getDocRef } from '../lib/firebaseWrapper';
import { useSnackbar } from './Snackbar';
import { Button } from './Buttons';
import { GoogleLinkButton, GoogleSignInButton } from './GoogleButtons';

export const CommentReportV = t.type({
/** comment id */
cid: t.string,
/** comment author display name */
cn: t.string,
/** comment text */
ct: t.string,
/** puzzle id */
pid: t.string,
/** reporter user id */
u: t.string,
/** reporter notes */
n: t.string,
/** report timestamp */
t: timestamp,
/** handled? */
h: t.boolean,
});
type CommentReportT = t.TypeOf<typeof CommentReportV>;

export const ReportOverlay = (props: {
closeOverlay: () => void;
comment: Omit<Comment, 'replies'>;
puzzleId: string;
}) => {
const authContext = useContext(AuthContext);
const { showSnackbar } = useSnackbar();

const [notes, setNotes] = useState('');
const [submitting, setSubmitting] = useState(false);

async function submitReport(event: FormEvent) {
event.preventDefault();
setSubmitting(true);

const uid = authContext.user?.uid;
if (authContext.user?.isAnonymous || !uid) {
console.error('cannot submit report w/o user!');
return;
}
const report: CommentReportT = {
cid: props.comment.id,
cn: props.comment.authorDisplayName,
ct: props.comment.commentText,
pid: props.puzzleId,
u: uid,
n: notes,
t: Timestamp.now(),
h: false,
};

await setDoc(getDocRef('cr', `${props.comment.id}-${uid}`), report).then(
() => {
setSubmitting(false);
showSnackbar('Comment reported - thank you!');
props.closeOverlay();
}
);
}

return (
<Overlay closeCallback={props.closeOverlay}>
<h2>Report Comment for Moderation</h2>
<p>
Crosshare&apos;s goal is to be a respectful and inclusive place for
people to share, solve, and discuss puzzles. You can help to keep it
that way by reporting any comments that you feel break Crosshare&apos;s
only rule: &ldquo;please be nice&rdquo;.
</p>
{!authContext.user || authContext.user.isAnonymous ? (
<>
<p>Please sign in with google to report a comment.</p>
{authContext.user ? (
<GoogleLinkButton user={authContext.user} />
) : (
<GoogleSignInButton />
)}
</>
) : (
<>
<h3>Comment you&apos;re reporting</h3>
<div
css={{
borderRadius: '0.5em',
backgroundColor: 'var(--secondary)',
padding: '1em',
margin: '1em 0 2em',
}}
>
<Markdown hast={props.comment.commentHast} />
<div>
<i>- {props.comment.authorDisplayName}</i>
</div>
</div>
<h3>Notes</h3>
<form onSubmit={logAsyncErrors(submitReport)}>
<div css={{ marginBottom: '1em' }}>
<label css={{ width: '100%', margin: 0 }}>
<p>
(Optional) Add any notes that you think might be helpful to
our moderation team:
</p>
<LengthLimitedTextarea
css={{ width: '100%', display: 'block' }}
maxLength={COMMENT_LENGTH_LIMIT}
value={notes}
updateValue={setNotes}
/>
</label>
<div css={{ textAlign: 'right' }}>
<LengthView
maxLength={COMMENT_LENGTH_LIMIT}
value={notes}
hideUntilWithin={200}
/>
</div>
</div>
<Button
type="submit"
css={{ marginRight: '0.5em' }}
disabled={submitting}
text={'Report comment'}
/>
<Button
boring={true}
disabled={submitting}
css={{ marginRight: '0.5em' }}
onClick={props.closeOverlay}
text={'Cancel'}
/>
</form>

<p css={{ marginTop: '2em' }}>
<i>
False reports waste our moderators&apos;s time and will eventually
lead to your account being banned.
</i>
</p>
</>
)}
</Overlay>
);
};
42 changes: 42 additions & 0 deletions app/pages/admin.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ import { markdownToHast } from '../lib/markdown/markdown';
import { css } from '@emotion/react';
import { withStaticTranslation } from '../lib/translation';
import { logAsyncErrors } from '../lib/utils';
import { CommentReportV } from '../components/ReportOverlay';

export const getStaticProps = withStaticTranslation(() => {
return { props: {} };
Expand Down Expand Up @@ -240,6 +241,13 @@ export default requiresAdmin(() => {
);
const [automoderated] = useCollectionData(automoderatedCollection.current);

const reportedCommentsCollection = useRef(
query(getValidatedCollection('cr', CommentReportV), where('h', '==', false))
);
const [reportedComments] = useCollectionData(
reportedCommentsCollection.current
);

const donationsCollection = useRef(
doc(getValidatedCollection('donations', DonationsListV), 'donations')
);
Expand Down Expand Up @@ -322,6 +330,40 @@ export default requiresAdmin(() => {
) : (
''
)}
{reportedComments?.length ? (
<>
<h4 css={{ borderBottom: '1px solid var(--black)' }}>
Reported Comments:
</h4>
<ul>
{reportedComments.map((rc) => (
<li key={`${rc.cid}-${rc.u}`}>
<div>{rc.ct}</div>
<div>
<i>- {rc.cn}</i>
</div>
<div>
<Link href={`/crosswords/${rc.pid}`}>puzzle</Link> -{' '}
{rc.pid}
</div>
<button
onClick={logAsyncErrors(async () => {
await updateDoc(getDocRef('cr', `${rc.cid}-${rc.u}`), {
h: true,
}).then(() => {
console.log('marked as handled');
});
})}
>
Mark as Handled
</button>
</li>
))}
</ul>
</>
) : (
''
)}
<h4 css={{ borderBottom: '1px solid var(--black)' }}>
Comment Moderation
</h4>
Expand Down

0 comments on commit 483b2dd

Please sign in to comment.