Improve reporting modal
This commit is contained in:
parent
0fb4c9bd40
commit
924b042c84
|
@ -25,6 +25,7 @@ module.exports = {
|
||||||
'import',
|
'import',
|
||||||
'promise',
|
'promise',
|
||||||
'react-hooks',
|
'react-hooks',
|
||||||
|
'@typescript-eslint',
|
||||||
],
|
],
|
||||||
|
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
|
@ -104,7 +105,8 @@ module.exports = {
|
||||||
'no-undef': 'error',
|
'no-undef': 'error',
|
||||||
'no-unreachable': 'error',
|
'no-unreachable': 'error',
|
||||||
'no-unused-expressions': 'error',
|
'no-unused-expressions': 'error',
|
||||||
'no-unused-vars': [
|
'no-unused-vars': 'off',
|
||||||
|
'@typescript-eslint/no-unused-vars': [
|
||||||
'error',
|
'error',
|
||||||
{
|
{
|
||||||
vars: 'all',
|
vars: 'all',
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import api from '../api';
|
import api from '../api';
|
||||||
|
|
||||||
import { openModal, closeModal } from './modals';
|
import { openModal } from './modals';
|
||||||
|
|
||||||
export const REPORT_INIT = 'REPORT_INIT';
|
export const REPORT_INIT = 'REPORT_INIT';
|
||||||
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
export const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
|
@ -14,6 +14,8 @@ export const REPORT_COMMENT_CHANGE = 'REPORT_COMMENT_CHANGE';
|
||||||
export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
|
export const REPORT_FORWARD_CHANGE = 'REPORT_FORWARD_CHANGE';
|
||||||
export const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
export const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
||||||
|
|
||||||
|
export const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
||||||
|
|
||||||
export function initReport(account, status) {
|
export function initReport(account, status) {
|
||||||
return dispatch => {
|
return dispatch => {
|
||||||
dispatch({
|
dispatch({
|
||||||
|
@ -55,15 +57,12 @@ export function submitReport() {
|
||||||
return (dispatch, getState) => {
|
return (dispatch, getState) => {
|
||||||
dispatch(submitReportRequest());
|
dispatch(submitReportRequest());
|
||||||
|
|
||||||
api(getState).post('/api/v1/reports', {
|
return api(getState).post('/api/v1/reports', {
|
||||||
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
account_id: getState().getIn(['reports', 'new', 'account_id']),
|
||||||
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
status_ids: getState().getIn(['reports', 'new', 'status_ids']),
|
||||||
comment: getState().getIn(['reports', 'new', 'comment']),
|
comment: getState().getIn(['reports', 'new', 'comment']),
|
||||||
forward: getState().getIn(['reports', 'new', 'forward']),
|
forward: getState().getIn(['reports', 'new', 'forward']),
|
||||||
}).then(response => {
|
});
|
||||||
dispatch(closeModal());
|
|
||||||
dispatch(submitReportSuccess(response.data));
|
|
||||||
}).catch(error => dispatch(submitReportFail(error)));
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,10 +72,9 @@ export function submitReportRequest() {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function submitReportSuccess(report) {
|
export function submitReportSuccess() {
|
||||||
return {
|
return {
|
||||||
type: REPORT_SUBMIT_SUCCESS,
|
type: REPORT_SUBMIT_SUCCESS,
|
||||||
report,
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -107,3 +105,10 @@ export function changeReportBlock(block) {
|
||||||
block,
|
block,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function changeReportRule(ruleId) {
|
||||||
|
return {
|
||||||
|
type: REPORT_RULE_CHANGE,
|
||||||
|
rule_id: ruleId,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import api from '../api';
|
||||||
|
|
||||||
|
import type { Rule } from 'soapbox/reducers/rules';
|
||||||
|
|
||||||
|
const RULES_FETCH_REQUEST = 'RULES_FETCH_REQUEST';
|
||||||
|
const RULES_FETCH_SUCCESS = 'RULES_FETCH_SUCCESS';
|
||||||
|
|
||||||
|
type RulesFetchRequestAction = {
|
||||||
|
type: typeof RULES_FETCH_REQUEST
|
||||||
|
}
|
||||||
|
|
||||||
|
type RulesFetchRequestSuccessAction = {
|
||||||
|
type: typeof RULES_FETCH_SUCCESS
|
||||||
|
payload: Rule[]
|
||||||
|
}
|
||||||
|
|
||||||
|
export type RulesActions = RulesFetchRequestAction | RulesFetchRequestSuccessAction
|
||||||
|
|
||||||
|
const fetchRules = () => (dispatch: React.Dispatch<RulesActions>, getState: any) => {
|
||||||
|
dispatch({ type: RULES_FETCH_REQUEST });
|
||||||
|
|
||||||
|
return api(getState)
|
||||||
|
.get('/api/v1/instance/rules')
|
||||||
|
.then((response) => dispatch({ type: RULES_FETCH_SUCCESS, payload: response.data }));
|
||||||
|
};
|
||||||
|
|
||||||
|
export {
|
||||||
|
fetchRules,
|
||||||
|
RULES_FETCH_REQUEST,
|
||||||
|
RULES_FETCH_SUCCESS,
|
||||||
|
};
|
|
@ -3,9 +3,9 @@ import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
interface IFormGroup {
|
interface IFormGroup {
|
||||||
/** Input label message. */
|
/** Input label message. */
|
||||||
hintText?: React.ReactNode,
|
hintText?: string | React.ReactNode,
|
||||||
/** Input hint message. */
|
/** Input hint message. */
|
||||||
labelText: React.ReactNode,
|
labelText: string | React.ReactNode,
|
||||||
/** Input errors. */
|
/** Input errors. */
|
||||||
errors?: string[]
|
errors?: string[]
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,8 @@ interface IModal {
|
||||||
secondaryAction?: () => void,
|
secondaryAction?: () => void,
|
||||||
/** Secondary button text. */
|
/** Secondary button text. */
|
||||||
secondaryText?: string,
|
secondaryText?: string,
|
||||||
|
/** Don't focus the "confirm" button on mount. */
|
||||||
|
skipFocus?: boolean,
|
||||||
/** Title text for the modal. */
|
/** Title text for the modal. */
|
||||||
title: string | React.ReactNode,
|
title: string | React.ReactNode,
|
||||||
}
|
}
|
||||||
|
@ -45,16 +47,17 @@ const Modal: React.FC<IModal> = ({
|
||||||
onClose,
|
onClose,
|
||||||
secondaryAction,
|
secondaryAction,
|
||||||
secondaryText,
|
secondaryText,
|
||||||
|
skipFocus = false,
|
||||||
title,
|
title,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
const buttonRef = React.useRef<HTMLButtonElement>(null);
|
||||||
|
|
||||||
React.useEffect(() => {
|
React.useEffect(() => {
|
||||||
if (buttonRef?.current) {
|
if (buttonRef?.current && !skipFocus) {
|
||||||
buttonRef.current.focus();
|
buttonRef.current.focus();
|
||||||
}
|
}
|
||||||
}, [buttonRef]);
|
}, [skipFocus, buttonRef]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div data-testid='modal' className='block w-full max-w-xl p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto'>
|
<div data-testid='modal' className='block w-full max-w-xl p-6 mx-auto overflow-hidden text-left align-middle transition-all transform bg-white dark:bg-slate-800 text-black dark:text-white shadow-xl rounded-2xl pointer-events-auto'>
|
||||||
|
@ -89,7 +92,7 @@ const Modal: React.FC<IModal> = ({
|
||||||
theme='ghost'
|
theme='ghost'
|
||||||
onClick={cancelAction}
|
onClick={cancelAction}
|
||||||
>
|
>
|
||||||
{cancelText}
|
{cancelText || 'Cancel'}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -8,7 +8,7 @@ type Alignments = 'left' | 'center' | 'right'
|
||||||
type TrackingSizes = 'normal' | 'wide'
|
type TrackingSizes = 'normal' | 'wide'
|
||||||
type TransformProperties = 'uppercase' | 'normal'
|
type TransformProperties = 'uppercase' | 'normal'
|
||||||
type Families = 'sans' | 'mono'
|
type Families = 'sans' | 'mono'
|
||||||
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6'
|
type Tags = 'abbr' | 'p' | 'span' | 'pre' | 'time' | 'h1' | 'h2' | 'h3' | 'h4' | 'h5' | 'h6' | 'label'
|
||||||
|
|
||||||
const themes = {
|
const themes = {
|
||||||
default: 'text-gray-900 dark:text-gray-100',
|
default: 'text-gray-900 dark:text-gray-100',
|
||||||
|
@ -66,6 +66,8 @@ interface IText extends Pick<React.HTMLAttributes<HTMLParagraphElement>, 'danger
|
||||||
className?: string,
|
className?: string,
|
||||||
/** Typeface of the text. */
|
/** Typeface of the text. */
|
||||||
family?: Families,
|
family?: Families,
|
||||||
|
/** The "for" attribute specifies which form element a label is bound to. */
|
||||||
|
htmlFor?: string,
|
||||||
/** Font size of the text. */
|
/** Font size of the text. */
|
||||||
size?: Sizes,
|
size?: Sizes,
|
||||||
/** HTML element name of the outer element. */
|
/** HTML element name of the outer element. */
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'required'> {
|
interface ITextarea extends Pick<React.TextareaHTMLAttributes<HTMLTextAreaElement>, 'maxLength' | 'onChange' | 'required' | 'disabled'> {
|
||||||
/** Put the cursor into the input on mount. */
|
/** Put the cursor into the input on mount. */
|
||||||
autoFocus?: boolean,
|
autoFocus?: boolean,
|
||||||
/** The initial text in the input. */
|
/** The initial text in the input. */
|
||||||
|
|
|
@ -82,7 +82,7 @@ export default class StatusCheckBox extends React.PureComponent {
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className='status-check-box-toggle'>
|
<div className='status-check-box-toggle'>
|
||||||
<Toggle checked={checked} onChange={onToggle} disabled={disabled} />
|
<Toggle checked={checked} onChange={onToggle} disabled={disabled} icons={false} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,133 @@
|
||||||
|
import { AxiosError } from 'axios';
|
||||||
|
import { Set as ImmutableSet } from 'immutable';
|
||||||
|
import React, { useEffect, useMemo, useState } from 'react';
|
||||||
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { blockAccount } from 'soapbox/actions/accounts';
|
||||||
|
import { submitReport, cancelReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
|
||||||
|
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
||||||
|
import { Modal } from 'soapbox/components/ui';
|
||||||
|
import { useAccount, useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import ConfirmationStep from './steps/confirmation-step';
|
||||||
|
import OtherActionsStep from './steps/other-actions-step';
|
||||||
|
import ReasonStep from './steps/reason-step';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
||||||
|
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||||
|
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||||
|
});
|
||||||
|
|
||||||
|
enum Steps {
|
||||||
|
ONE = 'ONE',
|
||||||
|
TWO = 'TWO',
|
||||||
|
THREE = 'THREE',
|
||||||
|
}
|
||||||
|
|
||||||
|
const reportSteps = {
|
||||||
|
ONE: ReasonStep,
|
||||||
|
TWO: OtherActionsStep,
|
||||||
|
THREE: ConfirmationStep,
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IReportModal {
|
||||||
|
onClose: () => void
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const accountId = useAppSelector((state) => state.reports.getIn(['new', 'account_id']) as string);
|
||||||
|
const account = useAccount(accountId);
|
||||||
|
|
||||||
|
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
|
||||||
|
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
|
||||||
|
const rules = useAppSelector((state) => state.rules.items);
|
||||||
|
const ruleId = useAppSelector((state) => state.reports.getIn(['new', 'rule_id']) as string);
|
||||||
|
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
|
||||||
|
|
||||||
|
const shouldRequireRule = rules.length > 0;
|
||||||
|
|
||||||
|
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||||
|
|
||||||
|
const handleSubmit = () => {
|
||||||
|
dispatch(submitReport())
|
||||||
|
.then(() => setCurrentStep(Steps.THREE))
|
||||||
|
.catch((error: AxiosError) => dispatch(submitReportFail(error)));
|
||||||
|
|
||||||
|
if (isBlocked && account) {
|
||||||
|
dispatch(blockAccount(account.id));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
dispatch(cancelReport());
|
||||||
|
onClose();
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleNextStep = () => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case Steps.ONE:
|
||||||
|
setCurrentStep(Steps.TWO);
|
||||||
|
break;
|
||||||
|
case Steps.TWO:
|
||||||
|
handleSubmit();
|
||||||
|
break;
|
||||||
|
case Steps.THREE:
|
||||||
|
dispatch(submitReportSuccess());
|
||||||
|
onClose();
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const confirmationText = useMemo(() => {
|
||||||
|
switch (currentStep) {
|
||||||
|
case Steps.TWO:
|
||||||
|
return intl.formatMessage(messages.submit);
|
||||||
|
case Steps.THREE:
|
||||||
|
return 'Done';
|
||||||
|
default:
|
||||||
|
return 'Next';
|
||||||
|
}
|
||||||
|
}, [currentStep]);
|
||||||
|
|
||||||
|
const isConfirmationButtonDisabled = useMemo(() => {
|
||||||
|
if (currentStep === Steps.THREE) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return isSubmitting || (shouldRequireRule && !ruleId) || selectedStatusIds.size === 0;
|
||||||
|
}, [currentStep, isSubmitting, shouldRequireRule, ruleId, selectedStatusIds.size]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (account) {
|
||||||
|
dispatch(expandAccountTimeline(account.id, { withReplies: true, maxId: null }));
|
||||||
|
}
|
||||||
|
}, [account]);
|
||||||
|
|
||||||
|
if (!account) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const StepToRender = reportSteps[currentStep];
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
title={<FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account.acct}</strong> }} />}
|
||||||
|
onClose={handleClose}
|
||||||
|
cancelAction={onClose}
|
||||||
|
confirmationAction={handleNextStep}
|
||||||
|
confirmationText={confirmationText}
|
||||||
|
confirmationDisabled={isConfirmationButtonDisabled}
|
||||||
|
skipFocus
|
||||||
|
>
|
||||||
|
<StepToRender account={account} />
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReportModal;
|
|
@ -0,0 +1,26 @@
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { Stack, Text } from 'soapbox/components/ui';
|
||||||
|
|
||||||
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
|
||||||
|
interface IOtherActionsStep {
|
||||||
|
account: ReducerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
const ConfirmationStep = ({ account }: IOtherActionsStep) => {
|
||||||
|
return (
|
||||||
|
<Stack space={1}>
|
||||||
|
<Text weight='medium'>
|
||||||
|
Thanks for submitting your report.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<Text>
|
||||||
|
If we find that this account is violating the TRUTH Terms of Service we
|
||||||
|
will take further action on the matter.
|
||||||
|
</Text>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ConfirmationStep;
|
|
@ -0,0 +1,180 @@
|
||||||
|
import { OrderedSet, Set as ImmutableSet } from 'immutable';
|
||||||
|
import React, { useCallback, useEffect, useState } from 'react';
|
||||||
|
import { FormattedMessage } from 'react-intl';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
import Toggle from 'react-toggle';
|
||||||
|
|
||||||
|
import { changeReportBlock, changeReportForward } from 'soapbox/actions/reports';
|
||||||
|
import { fetchRules } from 'soapbox/actions/rules';
|
||||||
|
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||||
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
|
import { Button, FormGroup, HStack, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
|
import StatusCheckBox from 'soapbox/features/report/containers/status_check_box_container';
|
||||||
|
import { useAppSelector, useFeatures } from 'soapbox/hooks';
|
||||||
|
import { isRemote, getDomain } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
|
||||||
|
const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
||||||
|
const status = useAppSelector((state) => state.statuses.get(statusId));
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={2} className='p-4 rounded-lg bg-gray-100 dark:bg-slate-700'>
|
||||||
|
<AccountContainer
|
||||||
|
id={status.get('account') as any}
|
||||||
|
showProfileHoverCard={false}
|
||||||
|
timestamp={status.get('created_at')}
|
||||||
|
hideActions
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
expanded
|
||||||
|
collapsable
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status.get('media_attachments').size > 0 && (
|
||||||
|
<AttachmentThumbs
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IOtherActionsStep {
|
||||||
|
account: ReducerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
const OtherActionsStep = ({ account }: IOtherActionsStep) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const features = useFeatures();
|
||||||
|
|
||||||
|
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
|
||||||
|
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${account.id}:with_replies`, 'items'])).union(state.reports.getIn(['new', 'status_ids']) as Iterable<unknown>) as OrderedSet<string>);
|
||||||
|
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
|
||||||
|
const isForward = useAppSelector((state) => state.reports.getIn(['reports', 'forward']) as boolean);
|
||||||
|
const canForward = isRemote(account as any) && features.federating;
|
||||||
|
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
|
||||||
|
|
||||||
|
const [showAdditionalStatuses, setShowAdditionalStatuses] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const renderSelectedStatuses = useCallback(() => {
|
||||||
|
switch (selectedStatusIds.size) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<div className='bg-gray-100 dark:bg-slate-700 p-4 rounded-lg flex items-center justify-center w-full'>
|
||||||
|
<Text theme='muted'>You have removed all statuses from being selected.</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <SelectedStatus statusId={selectedStatusIds.first()} />;
|
||||||
|
}
|
||||||
|
}, [selectedStatusIds.size]);
|
||||||
|
|
||||||
|
const handleBlockChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(changeReportBlock(event.target.checked));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleForwardChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
dispatch(changeReportForward(event.target.checked));
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchRules());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={4}>
|
||||||
|
{renderSelectedStatuses()}
|
||||||
|
|
||||||
|
{!features.reportMultipleStatuses && (
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text size='xl' weight='semibold'>Include other statuses?</Text>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
labelText='Would you like to add additional statuses to this report?'
|
||||||
|
>
|
||||||
|
{showAdditionalStatuses ? (
|
||||||
|
<Stack space={2}>
|
||||||
|
<div className='bg-gray-100 rounded-lg p-4'>
|
||||||
|
{statusIds.map((statusId) => <StatusCheckBox id={statusId} key={statusId} />)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
icon={require('@tabler/icons/icons/arrows-minimize.svg')}
|
||||||
|
theme='secondary'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setShowAdditionalStatuses(false)}
|
||||||
|
>
|
||||||
|
Hide additional statuses
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
icon={require('@tabler/icons/icons/plus.svg')}
|
||||||
|
theme='secondary'
|
||||||
|
size='sm'
|
||||||
|
onClick={() => setShowAdditionalStatuses(true)}
|
||||||
|
>
|
||||||
|
Add more
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</FormGroup>
|
||||||
|
</Stack>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text size='xl' weight='semibold'>Further actions:</Text>
|
||||||
|
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='report.block_hint' defaultMessage='Do you also want to block this account?' />}
|
||||||
|
>
|
||||||
|
<HStack space={2} alignItems='center'>
|
||||||
|
<Toggle
|
||||||
|
checked={isBlocked}
|
||||||
|
onChange={handleBlockChange}
|
||||||
|
icons={false}
|
||||||
|
id='report-block'
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text theme='muted' tag='label' size='sm' htmlFor='report-block'>
|
||||||
|
<FormattedMessage id='report.block' defaultMessage='Block {target}' values={{ target: `@${account.get('acct')}` }} />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</FormGroup>
|
||||||
|
|
||||||
|
{canForward && (
|
||||||
|
<FormGroup
|
||||||
|
labelText={<FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send a copy of the report there as well?' />}
|
||||||
|
>
|
||||||
|
<HStack space={2} alignItems='center'>
|
||||||
|
<Toggle
|
||||||
|
checked={isForward}
|
||||||
|
onChange={handleForwardChange}
|
||||||
|
icons={false}
|
||||||
|
id='report-forward'
|
||||||
|
disabled={isSubmitting}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Text theme='muted' tag='label' size='sm' htmlFor='report-forward'>
|
||||||
|
<FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: getDomain(account) }} />
|
||||||
|
</Text>
|
||||||
|
</HStack>
|
||||||
|
</FormGroup>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default OtherActionsStep;
|
|
@ -0,0 +1,189 @@
|
||||||
|
import classNames from 'classnames';
|
||||||
|
import { Set as ImmutableSet } from 'immutable';
|
||||||
|
import React, { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
import { useDispatch } from 'react-redux';
|
||||||
|
|
||||||
|
import { changeReportComment, changeReportRule } from 'soapbox/actions/reports';
|
||||||
|
import { fetchRules } from 'soapbox/actions/rules';
|
||||||
|
import AttachmentThumbs from 'soapbox/components/attachment_thumbs';
|
||||||
|
import StatusContent from 'soapbox/components/status_content';
|
||||||
|
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||||
|
import AccountContainer from 'soapbox/containers/account_container';
|
||||||
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
|
||||||
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
|
||||||
|
const messages = defineMessages({
|
||||||
|
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
||||||
|
});
|
||||||
|
|
||||||
|
const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
||||||
|
const status = useAppSelector((state) => state.statuses.get(statusId));
|
||||||
|
|
||||||
|
if (!status) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={2} className='p-4 rounded-lg bg-gray-100 dark:bg-slate-700'>
|
||||||
|
<AccountContainer
|
||||||
|
id={status.get('account') as any}
|
||||||
|
showProfileHoverCard={false}
|
||||||
|
timestamp={status.get('created_at')}
|
||||||
|
hideActions
|
||||||
|
/>
|
||||||
|
|
||||||
|
<StatusContent
|
||||||
|
status={status}
|
||||||
|
expanded
|
||||||
|
collapsable
|
||||||
|
/>
|
||||||
|
|
||||||
|
{status.get('media_attachments').size > 0 && (
|
||||||
|
<AttachmentThumbs
|
||||||
|
compact
|
||||||
|
media={status.get('media_attachments')}
|
||||||
|
sensitive={status.get('sensitive')}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface IReasonStep {
|
||||||
|
account: ReducerAccount
|
||||||
|
}
|
||||||
|
|
||||||
|
const ReasonStep = (_props: IReasonStep) => {
|
||||||
|
const dispatch = useDispatch();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
|
const rulesListRef = useRef(null);
|
||||||
|
|
||||||
|
const [isNearBottom, setNearBottom] = useState<boolean>(false);
|
||||||
|
const [isNearTop, setNearTop] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const selectedStatusIds = useAppSelector((state) => state.reports.getIn(['new', 'status_ids']) as ImmutableSet<string>);
|
||||||
|
const comment = useAppSelector((state) => state.reports.getIn(['new', 'comment']) as string);
|
||||||
|
const rules = useAppSelector((state) => state.rules.items);
|
||||||
|
const ruleId = useAppSelector((state) => state.reports.getIn(['new', 'rule_id']) as boolean);
|
||||||
|
|
||||||
|
const renderSelectedStatuses = useCallback(() => {
|
||||||
|
switch (selectedStatusIds.size) {
|
||||||
|
case 0:
|
||||||
|
return (
|
||||||
|
<div className='bg-gray-100 dark:bg-slate-700 p-4 rounded-lg flex items-center justify-center w-full'>
|
||||||
|
<Text theme='muted'>You have removed all statuses from being selected.</Text>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <SelectedStatus statusId={selectedStatusIds.first()} />;
|
||||||
|
}
|
||||||
|
}, [selectedStatusIds.size]);
|
||||||
|
|
||||||
|
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
|
dispatch(changeReportComment(event.target.value));
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleRulesScrolling = () => {
|
||||||
|
if (rulesListRef.current) {
|
||||||
|
const { scrollTop, scrollHeight, clientHeight } = rulesListRef.current;
|
||||||
|
|
||||||
|
if (scrollTop + clientHeight > scrollHeight - 24) {
|
||||||
|
setNearBottom(true);
|
||||||
|
} else {
|
||||||
|
setNearBottom(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scrollTop < 24) {
|
||||||
|
setNearTop(true);
|
||||||
|
} else {
|
||||||
|
setNearTop(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
dispatch(fetchRules());
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Stack space={4}>
|
||||||
|
{renderSelectedStatuses()}
|
||||||
|
|
||||||
|
<Stack space={2}>
|
||||||
|
<Text size='xl' weight='semibold'>Reason for reporting</Text>
|
||||||
|
|
||||||
|
<div className='relative'>
|
||||||
|
<div
|
||||||
|
className='bg-white rounded-lg -space-y-px max-h-96 overflow-y-auto'
|
||||||
|
onScroll={handleRulesScrolling}
|
||||||
|
ref={rulesListRef}
|
||||||
|
>
|
||||||
|
{rules.map((rule, idx) => {
|
||||||
|
const isSelected = String(ruleId) === rule.id;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<button
|
||||||
|
key={idx}
|
||||||
|
onClick={() => dispatch(changeReportRule(rule.id))}
|
||||||
|
className={classNames({
|
||||||
|
'relative border border-solid border-gray-200 hover:bg-gray-50 text-left w-full p-4 flex justify-between items-center cursor-pointer': true,
|
||||||
|
'rounded-tl-lg rounded-tr-lg': idx === 0,
|
||||||
|
'rounded-bl-lg rounded-br-lg': idx === rules.length - 1,
|
||||||
|
'bg-gray-50': isSelected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
<div className='mr-3 flex flex-col'>
|
||||||
|
<span
|
||||||
|
className={classNames('block text-sm font-medium', {
|
||||||
|
'text-primary-800': isSelected,
|
||||||
|
'text-gray-800': !isSelected,
|
||||||
|
})}
|
||||||
|
>
|
||||||
|
{rule.text}
|
||||||
|
</span>
|
||||||
|
<Text theme='muted' size='sm'>{rule.subtext}</Text>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<input
|
||||||
|
name='reason'
|
||||||
|
type='radio'
|
||||||
|
value={rule.id}
|
||||||
|
checked={isSelected}
|
||||||
|
readOnly
|
||||||
|
className='h-4 w-4 mt-0.5 cursor-pointer text-indigo-600 border-gray-300 focus:ring-indigo-500'
|
||||||
|
/>
|
||||||
|
</button>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div
|
||||||
|
className={classNames('inset-x-0 top-0 flex justify-center bg-gradient-to-b from-white pb-12 pt-8 pointer-events-none dark:from-slate-900 absolute transition-opacity duration-500', {
|
||||||
|
'opacity-0': isNearTop,
|
||||||
|
'opacity-100': !isNearTop,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
className={classNames('inset-x-0 bottom-0 flex justify-center bg-gradient-to-t from-white pt-12 pb-8 pointer-events-none dark:from-slate-900 absolute transition-opacity duration-500', {
|
||||||
|
'opacity-0': isNearBottom,
|
||||||
|
'opacity-100': !isNearBottom,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
<FormGroup labelText={intl.formatMessage(messages.placeholder)}>
|
||||||
|
<Textarea
|
||||||
|
placeholder={intl.formatMessage(messages.placeholder)}
|
||||||
|
value={comment}
|
||||||
|
onChange={handleCommentChange}
|
||||||
|
/>
|
||||||
|
</FormGroup>
|
||||||
|
</Stack>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ReasonStep;
|
|
@ -1,129 +0,0 @@
|
||||||
import { OrderedSet } from 'immutable';
|
|
||||||
import React, { useEffect } from 'react';
|
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
|
||||||
import { useDispatch } from 'react-redux';
|
|
||||||
import Toggle from 'react-toggle';
|
|
||||||
|
|
||||||
import { blockAccount } from 'soapbox/actions/accounts';
|
|
||||||
import { changeReportComment, changeReportForward, changeReportBlock, submitReport } from 'soapbox/actions/reports';
|
|
||||||
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
|
||||||
import { Button, Modal } from 'soapbox/components/ui';
|
|
||||||
import { useAccount, useAppSelector, useFeatures } from 'soapbox/hooks';
|
|
||||||
import { isRemote, getDomain } from 'soapbox/utils/accounts';
|
|
||||||
|
|
||||||
import StatusCheckBox from '../../report/containers/status_check_box_container';
|
|
||||||
|
|
||||||
const messages = defineMessages({
|
|
||||||
close: { id: 'lightbox.close', defaultMessage: 'Close' },
|
|
||||||
placeholder: { id: 'report.placeholder', defaultMessage: 'Additional comments' },
|
|
||||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
|
||||||
});
|
|
||||||
|
|
||||||
interface IReportModal {
|
|
||||||
onClose: () => void
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportModal = ({ onClose }: IReportModal) => {
|
|
||||||
const dispatch = useDispatch();
|
|
||||||
const features = useFeatures();
|
|
||||||
const intl = useIntl();
|
|
||||||
|
|
||||||
const accountId = useAppSelector((state) => state.reports.getIn(['new', 'account_id']) as string);
|
|
||||||
const account = useAccount(accountId);
|
|
||||||
|
|
||||||
const isBlocked = useAppSelector((state) => state.reports.getIn(['new', 'block']) as boolean);
|
|
||||||
const isSubmitting = useAppSelector((state) => state.reports.getIn(['new', 'isSubmitting']) as boolean);
|
|
||||||
const isForward = useAppSelector((state) => state.reports.getIn(['reports', 'forward']) as boolean);
|
|
||||||
const comment = useAppSelector((state) => state.reports.getIn(['new', 'comment']) as string);
|
|
||||||
const statusIds = useAppSelector((state) => OrderedSet(state.timelines.getIn([`account:${accountId}:with_replies`, 'items'])).union(state.reports.getIn(['new', 'status_ids']) as Iterable<unknown>) as OrderedSet<string>);
|
|
||||||
const canForward = isRemote(account as any) && features.federating;
|
|
||||||
|
|
||||||
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
|
||||||
dispatch(changeReportComment(event.target.value));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleForwardChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
dispatch(changeReportForward(event.target.checked));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBlockChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
dispatch(changeReportBlock(event.target.checked));
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleSubmit = () => {
|
|
||||||
dispatch(submitReport());
|
|
||||||
|
|
||||||
if (isBlocked && account) {
|
|
||||||
dispatch(blockAccount(account.id));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
|
|
||||||
if (event.keyCode === 13 && (event.ctrlKey || event.metaKey)) {
|
|
||||||
handleSubmit();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (account) {
|
|
||||||
dispatch(expandAccountTimeline(account.id, { withReplies: true, maxId: null }));
|
|
||||||
}
|
|
||||||
}, [account]);
|
|
||||||
|
|
||||||
if (!account) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
title={<FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account.acct}</strong> }} />}
|
|
||||||
onClose={onClose}
|
|
||||||
>
|
|
||||||
<div className='report-modal__container'>
|
|
||||||
<div className='report-modal__comment'>
|
|
||||||
<p><FormattedMessage id='report.hint' defaultMessage='The report will be sent to your server moderators. You can provide an explanation of why you are reporting this account below:' /></p>
|
|
||||||
|
|
||||||
<textarea
|
|
||||||
className='setting-text light'
|
|
||||||
placeholder={intl.formatMessage(messages.placeholder)}
|
|
||||||
value={comment}
|
|
||||||
onChange={handleCommentChange}
|
|
||||||
onKeyDown={handleKeyDown}
|
|
||||||
disabled={isSubmitting}
|
|
||||||
autoFocus
|
|
||||||
/>
|
|
||||||
|
|
||||||
{canForward && (
|
|
||||||
<div>
|
|
||||||
<p><FormattedMessage id='report.forward_hint' defaultMessage='The account is from another server. Send a copy of the report there as well?' /></p>
|
|
||||||
|
|
||||||
<div className='setting-toggle'>
|
|
||||||
<Toggle id='report-forward' checked={isForward} disabled={isSubmitting} onChange={handleForwardChange} />
|
|
||||||
<label htmlFor='report-forward' className='setting-toggle__label'><FormattedMessage id='report.forward' defaultMessage='Forward to {target}' values={{ target: getDomain(account) }} /></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<div>
|
|
||||||
<p><FormattedMessage id='report.block_hint' defaultMessage='Do you also want to block this account?' /></p>
|
|
||||||
|
|
||||||
<div className='setting-toggle'>
|
|
||||||
<Toggle id='report-block' checked={isBlocked} disabled={isSubmitting} onChange={handleBlockChange} />
|
|
||||||
<label htmlFor='report-block' className='setting-toggle__label'><FormattedMessage id='report.block' defaultMessage='Block {target}' values={{ target: account.get('acct') }} /></label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<Button disabled={isSubmitting} onClick={handleSubmit}>{intl.formatMessage(messages.submit)}</Button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className='report-modal__statuses'>
|
|
||||||
<div>
|
|
||||||
{statusIds.map((statusId) => <StatusCheckBox id={statusId} key={statusId} disabled={isSubmitting} />)}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ReportModal;
|
|
|
@ -127,7 +127,7 @@ export function Filters() {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function ReportModal() {
|
export function ReportModal() {
|
||||||
return import(/* webpackChunkName: "modals/report_modal" */'../components/report_modal');
|
return import(/* webpackChunkName: "modals/report-modal/report-modal" */'../components/modals/report-modal/report-modal');
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MediaGallery() {
|
export function MediaGallery() {
|
||||||
|
|
|
@ -10,11 +10,10 @@ import AgeVerification from './steps/age-verification';
|
||||||
import EmailVerification from './steps/email-verification';
|
import EmailVerification from './steps/email-verification';
|
||||||
import SmsVerification from './steps/sms-verification';
|
import SmsVerification from './steps/sms-verification';
|
||||||
|
|
||||||
// eslint-disable-next-line no-unused-vars
|
|
||||||
enum ChallengeTypes {
|
enum ChallengeTypes {
|
||||||
EMAIL = 'email', // eslint-disable-line no-unused-vars
|
EMAIL = 'email',
|
||||||
SMS = 'sms', // eslint-disable-line no-unused-vars
|
SMS = 'sms',
|
||||||
AGE = 'age', // eslint-disable-line no-unused-vars
|
AGE = 'age',
|
||||||
}
|
}
|
||||||
|
|
||||||
const verificationSteps = {
|
const verificationSteps = {
|
||||||
|
|
|
@ -45,6 +45,7 @@ import profile_hover_card from './profile_hover_card';
|
||||||
import push_notifications from './push_notifications';
|
import push_notifications from './push_notifications';
|
||||||
import relationships from './relationships';
|
import relationships from './relationships';
|
||||||
import reports from './reports';
|
import reports from './reports';
|
||||||
|
import rules from './rules';
|
||||||
import scheduled_statuses from './scheduled_statuses';
|
import scheduled_statuses from './scheduled_statuses';
|
||||||
import search from './search';
|
import search from './search';
|
||||||
import security from './security';
|
import security from './security';
|
||||||
|
@ -116,6 +117,7 @@ const reducers = {
|
||||||
accounts_meta,
|
accounts_meta,
|
||||||
trending_statuses,
|
trending_statuses,
|
||||||
verification,
|
verification,
|
||||||
|
rules,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Build a default state from all reducers: it has the key and `undefined`
|
// Build a default state from all reducers: it has the key and `undefined`
|
||||||
|
|
|
@ -10,6 +10,7 @@ import {
|
||||||
REPORT_COMMENT_CHANGE,
|
REPORT_COMMENT_CHANGE,
|
||||||
REPORT_FORWARD_CHANGE,
|
REPORT_FORWARD_CHANGE,
|
||||||
REPORT_BLOCK_CHANGE,
|
REPORT_BLOCK_CHANGE,
|
||||||
|
REPORT_RULE_CHANGE,
|
||||||
} from '../actions/reports';
|
} from '../actions/reports';
|
||||||
|
|
||||||
const initialState = ImmutableMap({
|
const initialState = ImmutableMap({
|
||||||
|
@ -20,6 +21,7 @@ const initialState = ImmutableMap({
|
||||||
comment: '',
|
comment: '',
|
||||||
forward: false,
|
forward: false,
|
||||||
block: false,
|
block: false,
|
||||||
|
rule_id: null,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -51,6 +53,8 @@ export default function reports(state = initialState, action) {
|
||||||
return state.setIn(['new', 'forward'], action.forward);
|
return state.setIn(['new', 'forward'], action.forward);
|
||||||
case REPORT_BLOCK_CHANGE:
|
case REPORT_BLOCK_CHANGE:
|
||||||
return state.setIn(['new', 'block'], action.block);
|
return state.setIn(['new', 'block'], action.block);
|
||||||
|
case REPORT_RULE_CHANGE:
|
||||||
|
return state.setIn(['new', 'rule_id'], action.rule_id);
|
||||||
case REPORT_SUBMIT_REQUEST:
|
case REPORT_SUBMIT_REQUEST:
|
||||||
return state.setIn(['new', 'isSubmitting'], true);
|
return state.setIn(['new', 'isSubmitting'], true);
|
||||||
case REPORT_SUBMIT_FAIL:
|
case REPORT_SUBMIT_FAIL:
|
||||||
|
|
|
@ -0,0 +1,31 @@
|
||||||
|
import { RULES_FETCH_REQUEST, RULES_FETCH_SUCCESS } from '../actions/rules';
|
||||||
|
|
||||||
|
import type { RulesActions } from '../actions/rules';
|
||||||
|
|
||||||
|
export type Rule = {
|
||||||
|
id: string
|
||||||
|
text: string
|
||||||
|
subtext: string
|
||||||
|
rule_type: 'content' | 'account'
|
||||||
|
}
|
||||||
|
|
||||||
|
type RulesState = {
|
||||||
|
items: Rule[],
|
||||||
|
isLoading: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: RulesState = {
|
||||||
|
items: [],
|
||||||
|
isLoading: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function rules(state: RulesState = initialState, action: RulesActions): RulesState {
|
||||||
|
switch (action.type) {
|
||||||
|
case RULES_FETCH_REQUEST:
|
||||||
|
return { ...state, isLoading: true };
|
||||||
|
case RULES_FETCH_SUCCESS:
|
||||||
|
return { ...state, isLoading: false, items: action.payload };
|
||||||
|
default:
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
|
@ -395,6 +395,11 @@ const getInstanceFeatures = (instance: Instance) => {
|
||||||
*/
|
*/
|
||||||
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
remoteInteractionsAPI: v.software === PLEROMA && gte(v.version, '2.4.50'),
|
||||||
|
|
||||||
|
reportMultipleStatuses: any([
|
||||||
|
v.software === MASTODON,
|
||||||
|
v.software === PLEROMA,
|
||||||
|
]),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Can request a password reset email through the API.
|
* Can request a password reset email through the API.
|
||||||
* @see POST /auth/password
|
* @see POST /auth/password
|
||||||
|
|
Loading…
Reference in New Issue