feat: Introduce User Report Section on Moderation Console (#30554)
Co-authored-by: Douglas Fabris <27704687+dougfabris@users.noreply.github.com>pull/31619/head
parent
da410efa10
commit
2260c04ec6
@ -0,0 +1,8 @@ |
||||
--- |
||||
'@rocket.chat/model-typings': minor |
||||
'@rocket.chat/rest-typings': minor |
||||
'@rocket.chat/i18n': minor |
||||
'@rocket.chat/meteor': minor |
||||
--- |
||||
|
||||
**Added ‘Reported Users’ Tab to Moderation Console:** Enhances user monitoring by displaying reported users. |
||||
@ -0,0 +1,43 @@ |
||||
import type { IUser } from '@rocket.chat/core-typings'; |
||||
import { Tabs, TabsItem, ContextualbarHeader, ContextualbarTitle } from '@rocket.chat/fuselage'; |
||||
import { useTranslation, useRouter, useRouteParameter } from '@rocket.chat/ui-contexts'; |
||||
import React, { useState } from 'react'; |
||||
|
||||
import { Contextualbar, ContextualbarClose } from '../../../components/Contextualbar'; |
||||
import UserMessages from './UserMessages'; |
||||
import UserReportInfo from './UserReports/UserReportInfo'; |
||||
|
||||
type ModConsoleReportDetailsProps = { |
||||
userId: IUser['_id']; |
||||
default: string; |
||||
onRedirect: (mid: string) => void; |
||||
}; |
||||
|
||||
const ModConsoleReportDetails = ({ userId, default: defaultTab, onRedirect }: ModConsoleReportDetailsProps) => { |
||||
const t = useTranslation(); |
||||
const [tab, setTab] = useState<string>(defaultTab); |
||||
const moderationRoute = useRouter(); |
||||
|
||||
const activeTab = useRouteParameter('tab'); |
||||
|
||||
return ( |
||||
<Contextualbar> |
||||
<ContextualbarHeader> |
||||
<ContextualbarTitle>{t('Reports')}</ContextualbarTitle> |
||||
<ContextualbarClose onClick={() => moderationRoute.navigate(`/admin/moderation/${activeTab}`, { replace: true })} /> |
||||
</ContextualbarHeader> |
||||
<Tabs paddingBlockStart={8}> |
||||
<TabsItem selected={tab === 'messages'} onClick={() => setTab('messages')}> |
||||
{t('Messages')} |
||||
</TabsItem> |
||||
<TabsItem selected={tab === 'users'} onClick={() => setTab('users')}> |
||||
{t('User')} |
||||
</TabsItem> |
||||
</Tabs> |
||||
{tab === 'messages' && <UserMessages userId={userId} onRedirect={onRedirect} />} |
||||
{tab === 'users' && <UserReportInfo userId={userId} />} |
||||
</Contextualbar> |
||||
); |
||||
}; |
||||
|
||||
export default ModConsoleReportDetails; |
||||
@ -1,16 +1,45 @@ |
||||
import { usePermission } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
import { usePermission, useRouteParameter, useRouter } from '@rocket.chat/ui-contexts'; |
||||
import React, { useEffect } from 'react'; |
||||
|
||||
import NotAuthorizedPage from '../../notAuthorized/NotAuthorizedPage'; |
||||
import ModerationConsolePage from './ModerationConsolePage'; |
||||
|
||||
const MODERATION_VALID_TABS = ['users', 'messages'] as const; |
||||
|
||||
const isValidTab = (tab: string | undefined): tab is (typeof MODERATION_VALID_TABS)[number] => MODERATION_VALID_TABS.includes(tab as any); |
||||
|
||||
const ModerationRoute = () => { |
||||
const canViewModerationConsole = usePermission('view-moderation-console'); |
||||
const router = useRouter(); |
||||
const tab = useRouteParameter('tab'); |
||||
|
||||
useEffect(() => { |
||||
if (!isValidTab(tab)) { |
||||
router.navigate( |
||||
{ |
||||
pattern: '/admin/moderation/:tab?/:context?/:id?', |
||||
params: { tab: 'messages' }, |
||||
}, |
||||
{ replace: true }, |
||||
); |
||||
} |
||||
}, [tab, router]); |
||||
|
||||
if (!canViewModerationConsole) { |
||||
return <NotAuthorizedPage />; |
||||
} |
||||
|
||||
return <ModerationConsolePage />; |
||||
const onSelectTab = (tab: (typeof MODERATION_VALID_TABS)[number]) => { |
||||
router.navigate( |
||||
{ |
||||
pattern: '/admin/moderation/:tab?/:context?/:id?', |
||||
params: { tab }, |
||||
}, |
||||
{ replace: true }, |
||||
); |
||||
}; |
||||
|
||||
return <ModerationConsolePage tab={tab as (typeof MODERATION_VALID_TABS)[number]} onSelectTab={onSelectTab} />; |
||||
}; |
||||
|
||||
export default ModerationRoute; |
||||
|
||||
@ -0,0 +1,41 @@ |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
|
||||
import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; |
||||
import useDeactivateUserAction from '../hooks/useDeactivateUserAction'; |
||||
import useDismissUserAction from '../hooks/useDismissUserAction'; |
||||
import useResetAvatarAction from '../hooks/useResetAvatarAction'; |
||||
import type { ModConsoleUserRowProps } from './ModConsoleUserTableRow'; |
||||
|
||||
const ModConsoleUserActions = ({ report, onClick }: Omit<ModConsoleUserRowProps, 'isDesktopOrLarger'>) => { |
||||
const t = useTranslation(); |
||||
const { |
||||
reportedUser: { _id: uid }, |
||||
} = report; |
||||
|
||||
return ( |
||||
<> |
||||
<GenericMenu |
||||
title={t('Options')} |
||||
sections={[ |
||||
{ |
||||
items: [ |
||||
{ |
||||
id: 'seeReports', |
||||
content: t('Moderation_See_reports'), |
||||
icon: 'document-eye', |
||||
onClick: () => onClick(uid), |
||||
}, |
||||
], |
||||
}, |
||||
{ |
||||
items: [useDismissUserAction(uid, true), useDeactivateUserAction(uid, true), useResetAvatarAction(uid)], |
||||
}, |
||||
]} |
||||
placement='bottom-end' |
||||
/> |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ModConsoleUserActions; |
||||
@ -0,0 +1,37 @@ |
||||
import type { IUser, UserReport, Serialized } from '@rocket.chat/core-typings'; |
||||
import React from 'react'; |
||||
|
||||
import { GenericTableCell, GenericTableRow } from '../../../../components/GenericTable'; |
||||
import { useFormatDateAndTime } from '../../../../hooks/useFormatDateAndTime'; |
||||
import UserColumn from '../helpers/UserColumn'; |
||||
import ModConsoleUserActions from './ModConsoleUserActions'; |
||||
|
||||
export type ModConsoleUserRowProps = { |
||||
report: Serialized<Pick<UserReport, '_id' | 'reportedUser' | 'ts'> & { count: number }>; |
||||
onClick: (id: IUser['_id']) => void; |
||||
isDesktopOrLarger: boolean; |
||||
}; |
||||
|
||||
const ModConsoleUserTableRow = ({ report, onClick, isDesktopOrLarger }: ModConsoleUserRowProps): JSX.Element => { |
||||
const { reportedUser, count, ts } = report; |
||||
const { _id, username, name, createdAt, emails } = reportedUser; |
||||
|
||||
const formatDateAndTime = useFormatDateAndTime(); |
||||
|
||||
return ( |
||||
<GenericTableRow key={_id} onClick={(): void => onClick(_id)} tabIndex={0} role='link' action> |
||||
<GenericTableCell withTruncatedText> |
||||
<UserColumn name={name} username={username} fontSize='micro' size={isDesktopOrLarger ? 'x20' : 'x40'} /> |
||||
</GenericTableCell> |
||||
<GenericTableCell withTruncatedText>{formatDateAndTime(createdAt)}</GenericTableCell> |
||||
<GenericTableCell withTruncatedText>{emails?.[0].address}</GenericTableCell> |
||||
<GenericTableCell withTruncatedText>{formatDateAndTime(ts)}</GenericTableCell> |
||||
<GenericTableCell withTruncatedText>{count}</GenericTableCell> |
||||
<GenericTableCell onClick={(e): void => e.stopPropagation()}> |
||||
<ModConsoleUserActions report={report} onClick={onClick} /> |
||||
</GenericTableCell> |
||||
</GenericTableRow> |
||||
); |
||||
}; |
||||
|
||||
export default ModConsoleUserTableRow; |
||||
@ -0,0 +1,150 @@ |
||||
import { Pagination, States, StatesAction, StatesActions, StatesIcon, StatesTitle } from '@rocket.chat/fuselage'; |
||||
import { useDebouncedValue, useMediaQuery, useMutableCallback } from '@rocket.chat/fuselage-hooks'; |
||||
import { useEndpoint, useRouter } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import type { FC } from 'react'; |
||||
import React, { useState } from 'react'; |
||||
import { useTranslation } from 'react-i18next'; |
||||
|
||||
import GenericNoResults from '../../../../components/GenericNoResults'; |
||||
import { |
||||
GenericTable, |
||||
GenericTableLoadingTable, |
||||
GenericTableHeaderCell, |
||||
GenericTableBody, |
||||
GenericTableHeader, |
||||
} from '../../../../components/GenericTable'; |
||||
import { usePagination } from '../../../../components/GenericTable/hooks/usePagination'; |
||||
import { useSort } from '../../../../components/GenericTable/hooks/useSort'; |
||||
import ModerationFilter from '../helpers/ModerationFilter'; |
||||
import ModConsoleUserTableRow from './ModConsoleUserTableRow'; |
||||
|
||||
const ModConsoleUsersTable: FC = () => { |
||||
const [text, setText] = useState(''); |
||||
const router = useRouter(); |
||||
const { t } = useTranslation(); |
||||
const isDesktopOrLarger = useMediaQuery('(min-width: 1024px)'); |
||||
|
||||
const { sortBy, sortDirection, setSort } = useSort< |
||||
'reports.ts' | 'reports.reportedUser.username' | 'reports.reportedUser.createdAt' | 'count' |
||||
>('reports.ts'); |
||||
const { current, itemsPerPage, setItemsPerPage: onSetItemsPerPage, setCurrent: onSetCurrent, ...paginationProps } = usePagination(); |
||||
|
||||
const [dateRange, setDateRange] = useState<{ start: string | null; end: string | null }>({ |
||||
start: '', |
||||
end: '', |
||||
}); |
||||
const { start, end } = dateRange; |
||||
|
||||
const debouncedText = useDebouncedValue(text, 500); |
||||
|
||||
const query = { |
||||
selector: debouncedText, |
||||
sort: JSON.stringify({ [sortBy]: sortDirection === 'asc' ? 1 : -1 }), |
||||
count: itemsPerPage, |
||||
offset: current, |
||||
latest: end ? `${new Date(end).toISOString().slice(0, 10)}T23:59:59.999Z` : undefined, |
||||
oldest: start ? `${new Date(start).toISOString().slice(0, 10)}T00:00:00.000Z` : undefined, |
||||
}; |
||||
|
||||
const getReports = useEndpoint('GET', '/v1/moderation.userReports'); |
||||
|
||||
const { data, isLoading, isSuccess, isError, refetch } = useQuery( |
||||
['moderation', 'userReports', 'fetchAll', query], |
||||
() => getReports(query), |
||||
{ |
||||
keepPreviousData: true, |
||||
}, |
||||
); |
||||
|
||||
const handleClick = useMutableCallback((id): void => { |
||||
router.navigate({ |
||||
pattern: '/admin/moderation/:tab?/:context?/:id?', |
||||
params: { tab: 'users', context: 'info', id }, |
||||
}); |
||||
}); |
||||
|
||||
const headers = ( |
||||
<> |
||||
<GenericTableHeaderCell |
||||
key='name' |
||||
direction={sortDirection} |
||||
active={sortBy === 'reports.reportedUser.username'} |
||||
onClick={setSort} |
||||
sort='reports.reportedUser.username' |
||||
> |
||||
{t('User')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell |
||||
active={sortBy === 'reports.reportedUser.createdAt'} |
||||
onClick={setSort} |
||||
sort='reports.reportedUser.createdAt' |
||||
key='created' |
||||
direction={sortDirection} |
||||
> |
||||
{t('Created_at')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='email' direction={sortDirection}> |
||||
{t('Email')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='postdate' direction={sortDirection} active={sortBy === 'reports.ts'} onClick={setSort} sort='reports.ts'> |
||||
{t('Moderation_Report_date')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='reports' direction={sortDirection} active={sortBy === 'count'} onClick={setSort} sort='count'> |
||||
{t('Moderation_Report_plural')} |
||||
</GenericTableHeaderCell> |
||||
<GenericTableHeaderCell key='actions' width='x48' /> |
||||
</> |
||||
); |
||||
|
||||
return ( |
||||
<> |
||||
<ModerationFilter setText={setText} setDateRange={setDateRange} /> |
||||
|
||||
{isLoading && ( |
||||
<GenericTable> |
||||
<GenericTableHeader>{headers}</GenericTableHeader> |
||||
<GenericTableBody>{isLoading && <GenericTableLoadingTable headerCells={6} />}</GenericTableBody> |
||||
</GenericTable> |
||||
)} |
||||
{isSuccess && data.reports.length > 0 && ( |
||||
<> |
||||
<GenericTable> |
||||
<GenericTableHeader>{headers}</GenericTableHeader> |
||||
<GenericTableBody> |
||||
{data.reports.map((report) => ( |
||||
<ModConsoleUserTableRow |
||||
key={report.reportedUser?._id} |
||||
report={report} |
||||
onClick={handleClick} |
||||
isDesktopOrLarger={isDesktopOrLarger} |
||||
/> |
||||
))} |
||||
</GenericTableBody> |
||||
</GenericTable> |
||||
<Pagination |
||||
current={current} |
||||
divider |
||||
itemsPerPage={itemsPerPage} |
||||
count={data?.total || 0} |
||||
onSetItemsPerPage={onSetItemsPerPage} |
||||
onSetCurrent={onSetCurrent} |
||||
{...paginationProps} |
||||
/> |
||||
</> |
||||
)} |
||||
{isSuccess && data.reports.length === 0 && <GenericNoResults />} |
||||
{isError && ( |
||||
<States> |
||||
<StatesIcon name='warning' variation='danger' /> |
||||
<StatesTitle>{t('Something_went_wrong')}</StatesTitle> |
||||
<StatesActions> |
||||
<StatesAction onClick={() => refetch()}>{t('Reload_page')}</StatesAction> |
||||
</StatesActions> |
||||
</States> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default ModConsoleUsersTable; |
||||
@ -0,0 +1,30 @@ |
||||
import { Button, ButtonGroup, Box } from '@rocket.chat/fuselage'; |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React from 'react'; |
||||
import type { FC } from 'react'; |
||||
|
||||
import GenericMenu from '../../../../components/GenericMenu/GenericMenu'; |
||||
import useDeactivateUserAction from '../hooks/useDeactivateUserAction'; |
||||
import useDismissUserAction from '../hooks/useDismissUserAction'; |
||||
import useResetAvatarAction from '../hooks/useResetAvatarAction'; |
||||
|
||||
const UserContextFooter: FC<{ userId: string; deleted: boolean }> = ({ userId, deleted }) => { |
||||
const t = useTranslation(); |
||||
|
||||
const dismissUserAction = useDismissUserAction(userId, true); |
||||
const deactivateUserAction = useDeactivateUserAction(userId, true); |
||||
|
||||
return ( |
||||
<ButtonGroup stretch> |
||||
<Button onClick={dismissUserAction.onClick}>{t('Moderation_Dismiss_all_reports')}</Button> |
||||
<Button disabled={deleted} onClick={deactivateUserAction.onClick} secondary danger> |
||||
{t('Moderation_Deactivate_User')} |
||||
</Button> |
||||
<Box display='flex' flexGrow={0} marginInlineStart={8}> |
||||
<GenericMenu large title={t('More')} items={[{ ...useResetAvatarAction(userId), disabled: deleted }]} placement='top-end' /> |
||||
</Box> |
||||
</ButtonGroup> |
||||
); |
||||
}; |
||||
|
||||
export default UserContextFooter; |
||||
@ -0,0 +1,116 @@ |
||||
import { |
||||
Box, |
||||
Callout, |
||||
StatesAction, |
||||
StatesActions, |
||||
StatesIcon, |
||||
StatesTitle, |
||||
ContextualbarFooter, |
||||
FieldGroup, |
||||
Field, |
||||
FieldLabel, |
||||
FieldRow, |
||||
ContextualbarSkeleton, |
||||
} from '@rocket.chat/fuselage'; |
||||
import { useEndpoint, useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import { useQuery } from '@tanstack/react-query'; |
||||
import React, { useMemo } from 'react'; |
||||
|
||||
import { ContextualbarScrollableContent } from '../../../../components/Contextualbar'; |
||||
import GenericNoResults from '../../../../components/GenericNoResults'; |
||||
import UserCard from '../../../../components/UserCard'; |
||||
import { useFormatDate } from '../../../../hooks/useFormatDate'; |
||||
import ReportReason from '../helpers/ReportReason'; |
||||
import UserColumn from '../helpers/UserColumn'; |
||||
import UserContextFooter from './UserContextFooter'; |
||||
|
||||
const UserReportInfo = ({ userId }: { userId: string }) => { |
||||
const t = useTranslation(); |
||||
const getUserReports = useEndpoint('GET', '/v1/moderation.user.reportsByUserId'); |
||||
const formatDateAndTime = useFormatDate(); |
||||
|
||||
const { |
||||
data: report, |
||||
refetch: reloadUsersReports, |
||||
isLoading, |
||||
isSuccess, |
||||
isError, |
||||
dataUpdatedAt, |
||||
} = useQuery(['moderation', 'userReports', 'fetchDetails', userId], async () => getUserReports({ userId })); |
||||
|
||||
const userProfile = useMemo(() => { |
||||
if (!report?.user) { |
||||
return null; |
||||
} |
||||
|
||||
const { username, name } = report.user; |
||||
return <UserColumn key={dataUpdatedAt} username={username} name={name} fontSize='p2' size='x48' />; |
||||
}, [report?.user, dataUpdatedAt]); |
||||
|
||||
const userEmails = useMemo(() => { |
||||
if (!report?.user?.emails) { |
||||
return []; |
||||
} |
||||
return report.user.emails.map((email) => email.address); |
||||
}, [report]); |
||||
|
||||
if (isError) { |
||||
return ( |
||||
<Box display='flex' flexDirection='column' alignItems='center' pb={20} color='default'> |
||||
<StatesIcon name='warning' variation='danger' /> |
||||
<StatesTitle>{t('Something_went_wrong')}</StatesTitle> |
||||
<StatesActions> |
||||
<StatesAction onClick={() => reloadUsersReports()}>{t('Reload_page')}</StatesAction> |
||||
</StatesActions> |
||||
</Box> |
||||
); |
||||
} |
||||
|
||||
return ( |
||||
<> |
||||
<ContextualbarScrollableContent> |
||||
{isLoading && <ContextualbarSkeleton />} |
||||
{isSuccess && report.reports.length > 0 && ( |
||||
<> |
||||
{report.user ? ( |
||||
<FieldGroup> |
||||
<Field>{userProfile}</Field> |
||||
<Field> |
||||
<FieldLabel>{t('Roles')}</FieldLabel> |
||||
<FieldRow justifyContent='flex-start' spacing={1}> |
||||
{report.user.roles.map((role, index) => ( |
||||
<UserCard.Role key={index}>{role}</UserCard.Role> |
||||
))} |
||||
</FieldRow> |
||||
</Field> |
||||
<Field> |
||||
<FieldLabel>{t('Email')}</FieldLabel> |
||||
<FieldRow>{userEmails.join(', ')}</FieldRow> |
||||
</Field> |
||||
<Field> |
||||
<FieldLabel>{t('Created_at')}</FieldLabel> |
||||
<FieldRow>{formatDateAndTime(report.user.createdAt)}</FieldRow> |
||||
</Field> |
||||
</FieldGroup> |
||||
) : ( |
||||
<Callout mbs={8} type='warning' icon='warning'> |
||||
{t('Moderation_User_deleted_warning')} |
||||
</Callout> |
||||
)} |
||||
{report.reports.map((report, ind) => ( |
||||
<ReportReason key={ind} ind={ind + 1} uinfo={report.reportedBy?.username} msg={report.description} ts={new Date(report.ts)} /> |
||||
))} |
||||
</> |
||||
)} |
||||
{isSuccess && report.reports.length === 0 && <GenericNoResults title={t('No_user_reports')} icon='user' />} |
||||
</ContextualbarScrollableContent> |
||||
{isSuccess && report.reports.length > 0 && ( |
||||
<ContextualbarFooter> |
||||
<UserContextFooter userId={userId} deleted={!report.user} /> |
||||
</ContextualbarFooter> |
||||
)} |
||||
</> |
||||
); |
||||
}; |
||||
|
||||
export default UserReportInfo; |
||||
@ -0,0 +1,24 @@ |
||||
import { useTranslation } from '@rocket.chat/ui-contexts'; |
||||
import React, { useCallback } from 'react'; |
||||
|
||||
import FilterByText from '../../../../components/FilterByText'; |
||||
import DateRangePicker from './DateRangePicker'; |
||||
|
||||
type ModerationFilterProps = { |
||||
setText: (text: string) => void; |
||||
setDateRange: (dateRange: { start: string; end: string }) => void; |
||||
}; |
||||
|
||||
const ModerationFilter = ({ setText, setDateRange }: ModerationFilterProps) => { |
||||
const t = useTranslation(); |
||||
|
||||
const handleChange = useCallback(({ text }): void => setText(text), [setText]); |
||||
|
||||
return ( |
||||
<FilterByText autoFocus placeholder={t('Search')} onChange={handleChange}> |
||||
<DateRangePicker onChange={setDateRange} /> |
||||
</FilterByText> |
||||
); |
||||
}; |
||||
|
||||
export default ModerationFilter; |
||||
@ -0,0 +1,43 @@ |
||||
import { Box } from '@rocket.chat/fuselage'; |
||||
import React from 'react'; |
||||
|
||||
import UserAvatar from '../../../../components/avatar/UserAvatar'; |
||||
|
||||
type UserColumnProps = { |
||||
name?: string; |
||||
username?: string; |
||||
isDesktopOrLarger?: boolean; |
||||
isProfile?: boolean; |
||||
size?: string; |
||||
fontSize?: string; |
||||
}; |
||||
|
||||
const UserColumn = ({ name, username, fontSize, size }: UserColumnProps) => { |
||||
return ( |
||||
<Box display='flex' alignItems='center'> |
||||
{username && ( |
||||
<Box> |
||||
<UserAvatar sizes={size} username={username} /> |
||||
</Box> |
||||
)} |
||||
<Box display='flex' mi={8} withTruncatedText> |
||||
<Box display='flex' flexDirection='column' alignSelf='center' withTruncatedText> |
||||
<Box fontScale='p2m' color='default' withTruncatedText> |
||||
{name && username ? ( |
||||
<> |
||||
{name}{' '} |
||||
<Box display='inline-flex' fontWeight={300} fontSize={fontSize}> |
||||
(@{username}) |
||||
</Box> |
||||
</> |
||||
) : ( |
||||
name || username |
||||
)}{' '} |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
</Box> |
||||
); |
||||
}; |
||||
|
||||
export default UserColumn; |
||||
@ -0,0 +1,17 @@ |
||||
import { api, credentials, request } from './api-data'; |
||||
|
||||
export const makeModerationApiRequest = async (url: string, method: 'get' | 'post', data?: any) => { |
||||
let res: any; |
||||
|
||||
if (method === 'get') { |
||||
res = await request.get(api(url)).set(credentials).query(data); |
||||
} else if (method === 'post') { |
||||
res = await request.post(api(url)).set(credentials).send(data); |
||||
} |
||||
|
||||
return res.body; |
||||
}; |
||||
|
||||
export const reportUser = (userId: string, reason: string) => makeModerationApiRequest('moderation.reportUser', 'post', { userId, reason }); |
||||
|
||||
export const getUsersReports = (userId: string) => makeModerationApiRequest('moderation.user.reportsByUserId', 'get', { userId }); |
||||
Loading…
Reference in new issue