From 12eaf6654b0913bfbc5e4f5c34d10effc655b6b7 Mon Sep 17 00:00:00 2001 From: "Thomas F. K. Jorna" Date: Wed, 4 Mar 2026 18:49:35 +0100 Subject: [PATCH 01/10] feat: allow community admins to flag users --- .../FlagUserDialog/FlagUserDialog.tsx | 110 ++++++++++++++++ client/components/index.ts | 1 + .../Discussion/ThreadComment.tsx | 29 ++++- server/apiRoutes.ts | 2 + server/discussion/api.ts | 14 +++ server/models.ts | 3 + server/pub/api.ts | 2 +- server/pub/permissions.ts | 6 +- server/spamTag/notifications.ts | 75 ++++++++++- server/threadComment/api.ts | 14 +++ server/userCommunityFlag/api.ts | 99 +++++++++++++++ server/userCommunityFlag/model.ts | 86 +++++++++++++ server/userCommunityFlag/queries.ts | 119 ++++++++++++++++++ .../utils/queryHelpers/discussionsSanitize.ts | 33 +++-- server/utils/queryHelpers/pubGet.ts | 8 +- server/utils/queryHelpers/pubSanitize.ts | 6 + server/utils/slack.ts | 79 +++++++++++- .../2026_03_04_createUserCommunityFlags.js | 62 +++++++++ types/spam.ts | 25 ++++ 19 files changed, 752 insertions(+), 21 deletions(-) create mode 100644 client/components/FlagUserDialog/FlagUserDialog.tsx create mode 100644 server/userCommunityFlag/api.ts create mode 100644 server/userCommunityFlag/model.ts create mode 100644 server/userCommunityFlag/queries.ts create mode 100644 tools/migrations/2026_03_04_createUserCommunityFlags.js diff --git a/client/components/FlagUserDialog/FlagUserDialog.tsx b/client/components/FlagUserDialog/FlagUserDialog.tsx new file mode 100644 index 0000000000..113e02674a --- /dev/null +++ b/client/components/FlagUserDialog/FlagUserDialog.tsx @@ -0,0 +1,110 @@ +import type { UserCommunityFlagReason } from 'types'; + +import React, { useCallback, useState } from 'react'; + +import { Button, Classes, Dialog, HTMLSelect, Intent, TextArea } from '@blueprintjs/core'; + +import { apiFetch } from 'client/utils/apiFetch'; + +const reasons: { value: UserCommunityFlagReason; label: string }[] = [ + { value: 'spam-content', label: 'Spam content' }, + { value: 'hateful-language', label: 'Hateful language' }, + { value: 'harassment', label: 'Harassment' }, + { value: 'impersonation', label: 'Impersonation' }, + { value: 'other', label: 'Other' }, +]; + +type Props = { + isOpen: boolean; + onClose: () => void; + userId: string; + communityId: string; + discussionId?: string | null; + userName?: string; + onFlagged?: () => void; +}; + +const FlagUserDialog = (props: Props) => { + const { isOpen, onClose, userId, communityId, discussionId, userName, onFlagged } = props; + const [reason, setReason] = useState('spam-content'); + const [reasonText, setReasonText] = useState(''); + const [isLoading, setIsLoading] = useState(false); + const [error, setError] = useState(null); + + const handleSubmit = useCallback(async () => { + setIsLoading(true); + setError(null); + try { + await apiFetch.post('/api/userCommunityFlags', { + userId, + communityId, + reason, + reasonText: reasonText.trim() || null, + sourceDiscussionId: discussionId ?? null, + }); + onFlagged?.(); + onClose(); + } catch (err: any) { + setError(err?.message ?? 'Failed to flag user'); + } finally { + setIsLoading(false); + } + }, [userId, communityId, reason, reasonText, discussionId, onFlagged, onClose]); + + return ( + +
+ +
+