Add ability to report a Group
This commit is contained in:
parent
948d66bcab
commit
30ef70440f
15 changed files with 206 additions and 63 deletions
|
@ -4,7 +4,7 @@ import { openModal } from './modals';
|
||||||
|
|
||||||
import type { AxiosError } from 'axios';
|
import type { AxiosError } from 'axios';
|
||||||
import type { AppDispatch, RootState } from 'soapbox/store';
|
import type { AppDispatch, RootState } from 'soapbox/store';
|
||||||
import type { Account, ChatMessage, Status } from 'soapbox/types/entities';
|
import type { Account, ChatMessage, Group, Status } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const REPORT_INIT = 'REPORT_INIT';
|
const REPORT_INIT = 'REPORT_INIT';
|
||||||
const REPORT_CANCEL = 'REPORT_CANCEL';
|
const REPORT_CANCEL = 'REPORT_CANCEL';
|
||||||
|
@ -20,19 +20,29 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE';
|
||||||
|
|
||||||
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE';
|
||||||
|
|
||||||
|
enum ReportableEntities {
|
||||||
|
ACCOUNT = 'ACCOUNT',
|
||||||
|
CHAT_MESSAGE = 'CHAT_MESSAGE',
|
||||||
|
GROUP = 'GROUP',
|
||||||
|
STATUS = 'STATUS'
|
||||||
|
}
|
||||||
|
|
||||||
type ReportedEntity = {
|
type ReportedEntity = {
|
||||||
status?: Status
|
status?: Status
|
||||||
chatMessage?: ChatMessage
|
chatMessage?: ChatMessage
|
||||||
|
group?: Group
|
||||||
}
|
}
|
||||||
|
|
||||||
const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
const initReport = (entityType: ReportableEntities, account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => {
|
||||||
const { status, chatMessage } = entities || {};
|
const { status, chatMessage, group } = entities || {};
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
type: REPORT_INIT,
|
type: REPORT_INIT,
|
||||||
|
entityType,
|
||||||
account,
|
account,
|
||||||
status,
|
status,
|
||||||
chatMessage,
|
chatMessage,
|
||||||
|
group,
|
||||||
});
|
});
|
||||||
|
|
||||||
return dispatch(openModal('REPORT'));
|
return dispatch(openModal('REPORT'));
|
||||||
|
@ -56,7 +66,8 @@ const submitReport = () =>
|
||||||
return api(getState).post('/api/v1/reports', {
|
return api(getState).post('/api/v1/reports', {
|
||||||
account_id: reports.getIn(['new', 'account_id']),
|
account_id: reports.getIn(['new', 'account_id']),
|
||||||
status_ids: reports.getIn(['new', 'status_ids']),
|
status_ids: reports.getIn(['new', 'status_ids']),
|
||||||
message_ids: [reports.getIn(['new', 'chat_message', 'id'])],
|
message_ids: [reports.getIn(['new', 'chat_message', 'id'])].filter(Boolean),
|
||||||
|
group_id: reports.getIn(['new', 'group', 'id']),
|
||||||
rule_ids: reports.getIn(['new', 'rule_ids']),
|
rule_ids: reports.getIn(['new', 'rule_ids']),
|
||||||
comment: reports.getIn(['new', 'comment']),
|
comment: reports.getIn(['new', 'comment']),
|
||||||
forward: reports.getIn(['new', 'forward']),
|
forward: reports.getIn(['new', 'forward']),
|
||||||
|
@ -97,6 +108,7 @@ const changeReportRule = (ruleId: string) => ({
|
||||||
});
|
});
|
||||||
|
|
||||||
export {
|
export {
|
||||||
|
ReportableEntities,
|
||||||
REPORT_INIT,
|
REPORT_INIT,
|
||||||
REPORT_CANCEL,
|
REPORT_CANCEL,
|
||||||
REPORT_SUBMIT_REQUEST,
|
REPORT_SUBMIT_REQUEST,
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { toggleBookmark, toggleFavourite, togglePin, toggleReblog } from 'soapbo
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus, editStatus, toggleMuteStatus } from 'soapbox/actions/statuses';
|
||||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import StatusActionButton from 'soapbox/components/status-action-button';
|
import StatusActionButton from 'soapbox/components/status-action-button';
|
||||||
|
@ -254,7 +254,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
secondary: intl.formatMessage(messages.blockAndReport),
|
secondary: intl.formatMessage(messages.blockAndReport),
|
||||||
onSecondary: () => {
|
onSecondary: () => {
|
||||||
dispatch(blockAccount(account.id));
|
dispatch(blockAccount(account.id));
|
||||||
dispatch(initReport(account, { status }));
|
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
@ -271,7 +271,7 @@ const StatusActionBar: React.FC<IStatusActionBar> = ({
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleReport: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
dispatch(initReport(status.account as Account, { status }));
|
dispatch(initReport(ReportableEntities.STATUS, status.account as Account, { status }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
const handleConversationMuteClick: React.EventHandler<React.MouseEvent> = (e) => {
|
||||||
|
|
|
@ -14,7 +14,7 @@ interface IIconButton extends React.ButtonHTMLAttributes<HTMLButtonElement> {
|
||||||
/** Don't render a background behind the icon. */
|
/** Don't render a background behind the icon. */
|
||||||
transparent?: boolean
|
transparent?: boolean
|
||||||
/** Predefined styles to display for the button. */
|
/** Predefined styles to display for the button. */
|
||||||
theme?: 'seamless' | 'outlined'
|
theme?: 'seamless' | 'outlined' | 'secondary'
|
||||||
/** Override the data-testid */
|
/** Override the data-testid */
|
||||||
'data-testid'?: string
|
'data-testid'?: string
|
||||||
}
|
}
|
||||||
|
@ -30,6 +30,7 @@ const IconButton = React.forwardRef((props: IIconButton, ref: React.ForwardedRef
|
||||||
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
|
className={clsx('flex items-center space-x-2 rounded-full p-1 focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 dark:ring-offset-0', {
|
||||||
'bg-white dark:bg-transparent': !transparent,
|
'bg-white dark:bg-transparent': !transparent,
|
||||||
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
'border border-solid bg-transparent border-gray-400 dark:border-gray-800 hover:border-primary-300 dark:hover:border-primary-700 focus:border-primary-500 text-gray-900 dark:text-gray-100 focus:ring-primary-500': theme === 'outlined',
|
||||||
|
'border-transparent bg-primary-100 dark:bg-primary-800 hover:bg-primary-50 dark:hover:bg-primary-700 focus:bg-primary-100 dark:focus:bg-primary-800 text-primary-500 dark:text-primary-200': theme === 'secondary',
|
||||||
'opacity-50': filteredProps.disabled,
|
'opacity-50': filteredProps.disabled,
|
||||||
}, className)}
|
}, className)}
|
||||||
{...filteredProps}
|
{...filteredProps}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import { mentionCompose, directCompose } from 'soapbox/actions/compose';
|
||||||
import { blockDomain, unblockDomain } from 'soapbox/actions/domain-blocks';
|
import { blockDomain, unblockDomain } from 'soapbox/actions/domain-blocks';
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { setSearchAccount } from 'soapbox/actions/search';
|
import { setSearchAccount } from 'soapbox/actions/search';
|
||||||
import { getSettings } from 'soapbox/actions/settings';
|
import { getSettings } from 'soapbox/actions/settings';
|
||||||
import Badge from 'soapbox/components/badge';
|
import Badge from 'soapbox/components/badge';
|
||||||
|
@ -136,7 +136,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
secondary: intl.formatMessage(messages.blockAndReport),
|
secondary: intl.formatMessage(messages.blockAndReport),
|
||||||
onSecondary: () => {
|
onSecondary: () => {
|
||||||
dispatch(blockAccount(account.id));
|
dispatch(blockAccount(account.id));
|
||||||
dispatch(initReport(account));
|
dispatch(initReport(ReportableEntities.ACCOUNT, account));
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
@ -171,7 +171,7 @@ const Header: React.FC<IHeader> = ({ account }) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const onReport = () => {
|
const onReport = () => {
|
||||||
dispatch(initReport(account));
|
dispatch(initReport(ReportableEntities.ACCOUNT, account));
|
||||||
};
|
};
|
||||||
|
|
||||||
const onMute = () => {
|
const onMute = () => {
|
||||||
|
|
|
@ -6,7 +6,7 @@ import React, { useMemo, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
import DropdownMenu from 'soapbox/components/dropdown-menu';
|
||||||
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
import { HStack, Icon, Stack, Text } from 'soapbox/components/ui';
|
||||||
import emojify from 'soapbox/features/emoji';
|
import emojify from 'soapbox/features/emoji';
|
||||||
|
@ -24,7 +24,7 @@ import ChatMessageReactionWrapper from './chat-message-reaction-wrapper/chat-mes
|
||||||
|
|
||||||
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
import type { Menu as IMenu } from 'soapbox/components/dropdown-menu';
|
||||||
import type { IMediaGallery } from 'soapbox/components/media-gallery';
|
import type { IMediaGallery } from 'soapbox/components/media-gallery';
|
||||||
import type { ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
import type { Account, ChatMessage as ChatMessageEntity } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
|
copy: { id: 'chats.actions.copy', defaultMessage: 'Copy' },
|
||||||
|
@ -178,7 +178,7 @@ const ChatMessage = (props: IChatMessage) => {
|
||||||
if (features.reportChats) {
|
if (features.reportChats) {
|
||||||
menu.push({
|
menu.push({
|
||||||
text: intl.formatMessage(messages.report),
|
text: intl.formatMessage(messages.report),
|
||||||
action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage } as any)),
|
action: () => dispatch(initReport(ReportableEntities.CHAT_MESSAGE, normalizeAccount(chat.account) as Account, { chatMessage })),
|
||||||
icon: require('@tabler/icons/flag.svg'),
|
icon: require('@tabler/icons/flag.svg'),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,7 +11,7 @@ import { toggleBookmark, togglePin, toggleReblog } from 'soapbox/actions/interac
|
||||||
import { openModal } from 'soapbox/actions/modals';
|
import { openModal } from 'soapbox/actions/modals';
|
||||||
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
import { deleteStatusModal, toggleStatusSensitivityModal } from 'soapbox/actions/moderation';
|
||||||
import { initMuteModal } from 'soapbox/actions/mutes';
|
import { initMuteModal } from 'soapbox/actions/mutes';
|
||||||
import { initReport } from 'soapbox/actions/reports';
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { deleteStatus } from 'soapbox/actions/statuses';
|
import { deleteStatus } from 'soapbox/actions/statuses';
|
||||||
import Icon from 'soapbox/components/icon';
|
import Icon from 'soapbox/components/icon';
|
||||||
import StillImage from 'soapbox/components/still-image';
|
import StillImage from 'soapbox/components/still-image';
|
||||||
|
@ -176,13 +176,13 @@ const EventHeader: React.FC<IEventHeader> = ({ status }) => {
|
||||||
secondary: intl.formatMessage(messages.blockAndReport),
|
secondary: intl.formatMessage(messages.blockAndReport),
|
||||||
onSecondary: () => {
|
onSecondary: () => {
|
||||||
dispatch(blockAccount(account.id));
|
dispatch(blockAccount(account.id));
|
||||||
dispatch(initReport(account, { status }));
|
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||||
},
|
},
|
||||||
}));
|
}));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleReport = () => {
|
const handleReport = () => {
|
||||||
dispatch(initReport(account, { status }));
|
dispatch(initReport(ReportableEntities.STATUS, account, { status }));
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleModerate = () => {
|
const handleModerate = () => {
|
||||||
|
|
|
@ -12,6 +12,7 @@ import { isDefaultHeader } from 'soapbox/utils/accounts';
|
||||||
|
|
||||||
import GroupActionButton from './group-action-button';
|
import GroupActionButton from './group-action-button';
|
||||||
import GroupMemberCount from './group-member-count';
|
import GroupMemberCount from './group-member-count';
|
||||||
|
import GroupOptionsButton from './group-options-button';
|
||||||
import GroupPrivacy from './group-privacy';
|
import GroupPrivacy from './group-privacy';
|
||||||
import GroupRelationship from './group-relationship';
|
import GroupRelationship from './group-relationship';
|
||||||
|
|
||||||
|
@ -140,7 +141,10 @@ const GroupHeader: React.FC<IGroupHeader> = ({ group }) => {
|
||||||
/>
|
/>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<GroupActionButton group={group} />
|
<HStack alignItems='center' space={2}>
|
||||||
|
<GroupOptionsButton group={group} />
|
||||||
|
<GroupActionButton group={group} />
|
||||||
|
</HStack>
|
||||||
</Stack>
|
</Stack>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -0,0 +1,46 @@
|
||||||
|
import React, { useMemo } from 'react';
|
||||||
|
|
||||||
|
import { initReport, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
|
import DropdownMenu, { Menu } from 'soapbox/components/dropdown-menu';
|
||||||
|
import { IconButton } from 'soapbox/components/ui';
|
||||||
|
import { useAppDispatch, useOwnAccount } from 'soapbox/hooks';
|
||||||
|
import { GroupRoles } from 'soapbox/schemas/group-member';
|
||||||
|
|
||||||
|
import type { Account, Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
|
interface IGroupActionButton {
|
||||||
|
group: Group
|
||||||
|
}
|
||||||
|
|
||||||
|
const GroupOptionsButton = ({ group }: IGroupActionButton) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const account = useOwnAccount();
|
||||||
|
|
||||||
|
const isMember = group.relationship?.role === GroupRoles.USER;
|
||||||
|
const isBlocked = group.relationship?.blocked_by;
|
||||||
|
|
||||||
|
const menu: Menu = useMemo(() => ([
|
||||||
|
{
|
||||||
|
text: 'Report',
|
||||||
|
icon: require('@tabler/icons/flag.svg'),
|
||||||
|
action: () => dispatch(initReport(ReportableEntities.GROUP, account as Account, { group })),
|
||||||
|
},
|
||||||
|
]), []);
|
||||||
|
|
||||||
|
if (isBlocked || !isMember || menu.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<DropdownMenu items={menu} placement='bottom'>
|
||||||
|
<IconButton
|
||||||
|
src={require('@tabler/icons/dots.svg')}
|
||||||
|
theme='secondary'
|
||||||
|
iconClassName='h-5 w-5'
|
||||||
|
className='self-stretch px-2.5'
|
||||||
|
/>
|
||||||
|
</DropdownMenu>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default GroupOptionsButton;
|
|
@ -2,6 +2,7 @@ import userEvent from '@testing-library/user-event';
|
||||||
import { Map as ImmutableMap, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
import { Map as ImmutableMap, Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
|
||||||
|
import { ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { __stub } from 'soapbox/api';
|
import { __stub } from 'soapbox/api';
|
||||||
|
|
||||||
import { render, screen, waitFor } from '../../../../../../jest/test-helpers';
|
import { render, screen, waitFor } from '../../../../../../jest/test-helpers';
|
||||||
|
@ -29,6 +30,7 @@ describe('<ReportModal />', () => {
|
||||||
account_id: '1',
|
account_id: '1',
|
||||||
status_ids: ImmutableSet(['1']),
|
status_ids: ImmutableSet(['1']),
|
||||||
rule_ids: ImmutableSet(),
|
rule_ids: ImmutableSet(),
|
||||||
|
entityType: ReportableEntities.STATUS,
|
||||||
})(),
|
})(),
|
||||||
})(),
|
})(),
|
||||||
statuses: ImmutableMap({
|
statuses: ImmutableMap({
|
||||||
|
|
|
@ -2,9 +2,10 @@ import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { blockAccount } from 'soapbox/actions/accounts';
|
import { blockAccount } from 'soapbox/actions/accounts';
|
||||||
import { submitReport, submitReportSuccess, submitReportFail } from 'soapbox/actions/reports';
|
import { submitReport, submitReportSuccess, submitReportFail, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
import { expandAccountTimeline } from 'soapbox/actions/timelines';
|
||||||
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
import AttachmentThumbs from 'soapbox/components/attachment-thumbs';
|
||||||
|
import GroupCard from 'soapbox/components/group-card';
|
||||||
import List, { ListItem } from 'soapbox/components/list';
|
import List, { ListItem } from 'soapbox/components/list';
|
||||||
import StatusContent from 'soapbox/components/status-content';
|
import StatusContent from 'soapbox/components/status-content';
|
||||||
import { Avatar, HStack, Icon, Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui';
|
import { Avatar, HStack, Icon, Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui';
|
||||||
|
@ -24,6 +25,7 @@ const messages = defineMessages({
|
||||||
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
submit: { id: 'report.submit', defaultMessage: 'Submit' },
|
||||||
reportContext: { id: 'report.chatMessage.context', defaultMessage: 'When reporting a user’s message, the five messages before and five messages after the one selected will be passed along to our moderation team for context.' },
|
reportContext: { id: 'report.chatMessage.context', defaultMessage: 'When reporting a user’s message, the five messages before and five messages after the one selected will be passed along to our moderation team for context.' },
|
||||||
reportMessage: { id: 'report.chatMessage.title', defaultMessage: 'Report message' },
|
reportMessage: { id: 'report.chatMessage.title', defaultMessage: 'Report message' },
|
||||||
|
reportGroup: { id: 'report.group.title', defaultMessage: 'Report Group' },
|
||||||
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
|
cancel: { id: 'common.cancel', defaultMessage: 'Cancel' },
|
||||||
previous: { id: 'report.previous', defaultMessage: 'Previous' },
|
previous: { id: 'report.previous', defaultMessage: 'Previous' },
|
||||||
});
|
});
|
||||||
|
@ -35,9 +37,26 @@ enum Steps {
|
||||||
}
|
}
|
||||||
|
|
||||||
const reportSteps = {
|
const reportSteps = {
|
||||||
ONE: ReasonStep,
|
[ReportableEntities.ACCOUNT]: {
|
||||||
TWO: OtherActionsStep,
|
ONE: ReasonStep,
|
||||||
THREE: ConfirmationStep,
|
TWO: OtherActionsStep,
|
||||||
|
THREE: ConfirmationStep,
|
||||||
|
},
|
||||||
|
[ReportableEntities.CHAT_MESSAGE]: {
|
||||||
|
ONE: ReasonStep,
|
||||||
|
TWO: OtherActionsStep,
|
||||||
|
THREE: ConfirmationStep,
|
||||||
|
},
|
||||||
|
[ReportableEntities.STATUS]: {
|
||||||
|
ONE: ReasonStep,
|
||||||
|
TWO: OtherActionsStep,
|
||||||
|
THREE: ConfirmationStep,
|
||||||
|
},
|
||||||
|
[ReportableEntities.GROUP]: {
|
||||||
|
ONE: ReasonStep,
|
||||||
|
TWO: ConfirmationStep,
|
||||||
|
THREE: null,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
const SelectedStatus = ({ statusId }: { statusId: string }) => {
|
||||||
|
@ -76,12 +95,6 @@ interface IReportModal {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
enum ReportedEntities {
|
|
||||||
Account = 'Account',
|
|
||||||
Status = 'Status',
|
|
||||||
ChatMessage = 'ChatMessage'
|
|
||||||
}
|
|
||||||
|
|
||||||
const ReportModal = ({ onClose }: IReportModal) => {
|
const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
@ -89,27 +102,20 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
const accountId = useAppSelector((state) => state.reports.new.account_id);
|
const accountId = useAppSelector((state) => state.reports.new.account_id);
|
||||||
const account = useAccount(accountId as string);
|
const account = useAccount(accountId as string);
|
||||||
|
|
||||||
|
const entityType = useAppSelector((state) => state.reports.new.entityType);
|
||||||
const isBlocked = useAppSelector((state) => state.reports.new.block);
|
const isBlocked = useAppSelector((state) => state.reports.new.block);
|
||||||
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
|
const isSubmitting = useAppSelector((state) => state.reports.new.isSubmitting);
|
||||||
const rules = useAppSelector((state) => state.rules.items);
|
const rules = useAppSelector((state) => state.rules.items);
|
||||||
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
||||||
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
|
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
|
||||||
const selectedChatMessage = useAppSelector((state) => state.reports.new.chat_message);
|
const selectedChatMessage = useAppSelector((state) => state.reports.new.chat_message);
|
||||||
|
const selectedGroup = useAppSelector((state) => state.reports.new.group);
|
||||||
|
|
||||||
const shouldRequireRule = rules.length > 0;
|
const shouldRequireRule = rules.length > 0;
|
||||||
|
|
||||||
const reportedEntity = useMemo(() => {
|
const isReportingAccount = entityType === ReportableEntities.ACCOUNT;
|
||||||
if (selectedStatusIds.size === 0 && !selectedChatMessage) {
|
const isReportingStatus = entityType === ReportableEntities.STATUS;
|
||||||
return ReportedEntities.Account;
|
const isReportingGroup = entityType === ReportableEntities.GROUP;
|
||||||
} else if (selectedChatMessage) {
|
|
||||||
return ReportedEntities.ChatMessage;
|
|
||||||
} else {
|
|
||||||
return ReportedEntities.Status;
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const isReportingAccount = reportedEntity === ReportedEntities.Account;
|
|
||||||
const isReportingStatus = reportedEntity === ReportedEntities.Status;
|
|
||||||
|
|
||||||
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
const [currentStep, setCurrentStep] = useState<Steps>(Steps.ONE);
|
||||||
|
|
||||||
|
@ -160,22 +166,41 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
|
|
||||||
const confirmationText = useMemo(() => {
|
const confirmationText = useMemo(() => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
|
case Steps.ONE:
|
||||||
|
if (isReportingGroup) {
|
||||||
|
return intl.formatMessage(messages.submit);
|
||||||
|
} else {
|
||||||
|
return intl.formatMessage(messages.next);
|
||||||
|
}
|
||||||
case Steps.TWO:
|
case Steps.TWO:
|
||||||
return intl.formatMessage(messages.submit);
|
if (isReportingGroup) {
|
||||||
|
return intl.formatMessage(messages.done);
|
||||||
|
} else {
|
||||||
|
return intl.formatMessage(messages.submit);
|
||||||
|
}
|
||||||
case Steps.THREE:
|
case Steps.THREE:
|
||||||
return intl.formatMessage(messages.done);
|
return intl.formatMessage(messages.done);
|
||||||
default:
|
default:
|
||||||
return intl.formatMessage(messages.next);
|
return intl.formatMessage(messages.next);
|
||||||
}
|
}
|
||||||
}, [currentStep]);
|
}, [currentStep, isReportingGroup]);
|
||||||
|
|
||||||
const handleNextStep = () => {
|
const handleNextStep = () => {
|
||||||
switch (currentStep) {
|
switch (currentStep) {
|
||||||
case Steps.ONE:
|
case Steps.ONE:
|
||||||
setCurrentStep(Steps.TWO);
|
if (isReportingGroup) {
|
||||||
|
handleSubmit();
|
||||||
|
} else {
|
||||||
|
setCurrentStep(Steps.TWO);
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Steps.TWO:
|
case Steps.TWO:
|
||||||
handleSubmit();
|
if (isReportingGroup) {
|
||||||
|
dispatch(submitReportSuccess());
|
||||||
|
onClose();
|
||||||
|
} else {
|
||||||
|
handleSubmit();
|
||||||
|
}
|
||||||
break;
|
break;
|
||||||
case Steps.THREE:
|
case Steps.THREE:
|
||||||
dispatch(submitReportSuccess());
|
dispatch(submitReportSuccess());
|
||||||
|
@ -212,19 +237,35 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const renderSelectedGroup = () => {
|
||||||
|
if (selectedGroup) {
|
||||||
|
return <GroupCard group={selectedGroup} />;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const renderSelectedEntity = () => {
|
const renderSelectedEntity = () => {
|
||||||
switch (reportedEntity) {
|
switch (entityType) {
|
||||||
case ReportedEntities.Status:
|
case ReportableEntities.STATUS:
|
||||||
return renderSelectedStatuses();
|
return renderSelectedStatuses();
|
||||||
case ReportedEntities.ChatMessage:
|
case ReportableEntities.CHAT_MESSAGE:
|
||||||
return renderSelectedChatMessage();
|
return renderSelectedChatMessage();
|
||||||
|
case ReportableEntities.GROUP:
|
||||||
|
if (currentStep === Steps.TWO) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderSelectedGroup();
|
||||||
|
default:
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const renderTitle = () => {
|
const renderTitle = () => {
|
||||||
switch (reportedEntity) {
|
switch (entityType) {
|
||||||
case ReportedEntities.ChatMessage:
|
case ReportableEntities.CHAT_MESSAGE:
|
||||||
return intl.formatMessage(messages.reportMessage);
|
return intl.formatMessage(messages.reportMessage);
|
||||||
|
case ReportableEntities.GROUP:
|
||||||
|
return intl.formatMessage(messages.reportGroup);
|
||||||
default:
|
default:
|
||||||
return <FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account?.acct}</strong> }} />;
|
return <FormattedMessage id='report.target' defaultMessage='Reporting {target}' values={{ target: <strong>@{account?.acct}</strong> }} />;
|
||||||
}
|
}
|
||||||
|
@ -252,16 +293,16 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
}, [currentStep]);
|
}, [currentStep]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (account) {
|
if (account?.id) {
|
||||||
dispatch(expandAccountTimeline(account.id, { withReplies: true, maxId: null }));
|
dispatch(expandAccountTimeline(account.id, { withReplies: true, maxId: null }));
|
||||||
}
|
}
|
||||||
}, [account]);
|
}, [account?.id]);
|
||||||
|
|
||||||
if (!account) {
|
if (!account) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const StepToRender = reportSteps[currentStep];
|
const StepToRender = reportSteps[entityType][currentStep];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<Modal
|
||||||
|
@ -279,7 +320,9 @@ const ReportModal = ({ onClose }: IReportModal) => {
|
||||||
|
|
||||||
{(currentStep !== Steps.THREE && !isReportingAccount) && renderSelectedEntity()}
|
{(currentStep !== Steps.THREE && !isReportingAccount) && renderSelectedEntity()}
|
||||||
|
|
||||||
<StepToRender account={account} />
|
{StepToRender && (
|
||||||
|
<StepToRender account={account} />
|
||||||
|
)}
|
||||||
</Stack>
|
</Stack>
|
||||||
</Modal>
|
</Modal>
|
||||||
);
|
);
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
import { defineMessages, FormattedMessage, useIntl } from 'react-intl';
|
||||||
|
|
||||||
|
import { ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
import { getSoapboxConfig } from 'soapbox/actions/soapbox';
|
||||||
import { Stack, Text } from 'soapbox/components/ui';
|
import { Stack, Text } from 'soapbox/components/ui';
|
||||||
import { useAppSelector } from 'soapbox/hooks';
|
import { useAppSelector } from 'soapbox/hooks';
|
||||||
|
@ -8,8 +9,10 @@ import { useAppSelector } from 'soapbox/hooks';
|
||||||
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
import type { ReducerAccount } from 'soapbox/reducers/accounts';
|
||||||
|
|
||||||
const messages = defineMessages({
|
const messages = defineMessages({
|
||||||
|
accountEntity: { id: 'report.confirmation.entity.account', defaultMessage: 'account' },
|
||||||
|
groupEntity: { id: 'report.confirmation.entity.group', defaultMessage: 'group' },
|
||||||
title: { id: 'report.confirmation.title', defaultMessage: 'Thanks for submitting your report.' },
|
title: { id: 'report.confirmation.title', defaultMessage: 'Thanks for submitting your report.' },
|
||||||
content: { id: 'report.confirmation.content', defaultMessage: 'If we find that this account is violating the {link} we will take further action on the matter.' },
|
content: { id: 'report.confirmation.content', defaultMessage: 'If we find that this {entity} is violating the {link} we will take further action on the matter.' },
|
||||||
});
|
});
|
||||||
|
|
||||||
interface IOtherActionsStep {
|
interface IOtherActionsStep {
|
||||||
|
@ -34,6 +37,11 @@ const renderTermsOfServiceLink = (href: string) => (
|
||||||
const ConfirmationStep = ({ account }: IOtherActionsStep) => {
|
const ConfirmationStep = ({ account }: IOtherActionsStep) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const links = useAppSelector((state) => getSoapboxConfig(state).get('links') as any);
|
const links = useAppSelector((state) => getSoapboxConfig(state).get('links') as any);
|
||||||
|
const entityType = useAppSelector((state) => state.reports.new.entityType);
|
||||||
|
|
||||||
|
const entity = entityType === ReportableEntities.GROUP
|
||||||
|
? intl.formatMessage(messages.groupEntity)
|
||||||
|
: intl.formatMessage(messages.accountEntity);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Stack space={1}>
|
<Stack space={1}>
|
||||||
|
@ -43,6 +51,7 @@ const ConfirmationStep = ({ account }: IOtherActionsStep) => {
|
||||||
|
|
||||||
<Text>
|
<Text>
|
||||||
{intl.formatMessage(messages.content, {
|
{intl.formatMessage(messages.content, {
|
||||||
|
entity,
|
||||||
link: links.get('termsOfService') ?
|
link: links.get('termsOfService') ?
|
||||||
renderTermsOfServiceLink(links.get('termsOfService')) :
|
renderTermsOfServiceLink(links.get('termsOfService')) :
|
||||||
termsOfServiceText,
|
termsOfServiceText,
|
||||||
|
|
|
@ -1,8 +1,8 @@
|
||||||
import clsx from 'clsx';
|
import clsx from 'clsx';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useRef, useState } from 'react';
|
||||||
import { defineMessages, useIntl } from 'react-intl';
|
import { defineMessages, useIntl } from 'react-intl';
|
||||||
|
|
||||||
import { changeReportComment, changeReportRule } from 'soapbox/actions/reports';
|
import { changeReportComment, changeReportRule, ReportableEntities } from 'soapbox/actions/reports';
|
||||||
import { fetchRules } from 'soapbox/actions/rules';
|
import { fetchRules } from 'soapbox/actions/rules';
|
||||||
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
|
import { FormGroup, Stack, Text, Textarea } from 'soapbox/components/ui';
|
||||||
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
import { useAppDispatch, useAppSelector } from 'soapbox/hooks';
|
||||||
|
@ -29,14 +29,12 @@ const ReasonStep = (_props: IReasonStep) => {
|
||||||
const [isNearBottom, setNearBottom] = useState<boolean>(false);
|
const [isNearBottom, setNearBottom] = useState<boolean>(false);
|
||||||
const [isNearTop, setNearTop] = useState<boolean>(true);
|
const [isNearTop, setNearTop] = useState<boolean>(true);
|
||||||
|
|
||||||
|
const entityType = useAppSelector((state) => state.reports.new.entityType);
|
||||||
const comment = useAppSelector((state) => state.reports.new.comment);
|
const comment = useAppSelector((state) => state.reports.new.comment);
|
||||||
const rules = useAppSelector((state) => state.rules.items);
|
const rules = useAppSelector((state) => state.rules.items);
|
||||||
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
const ruleIds = useAppSelector((state) => state.reports.new.rule_ids);
|
||||||
const shouldRequireRule = rules.length > 0;
|
const shouldRequireRule = rules.length > 0;
|
||||||
|
|
||||||
const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids);
|
|
||||||
const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []);
|
|
||||||
|
|
||||||
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
const handleCommentChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
|
||||||
dispatch(changeReportComment(event.target.value));
|
dispatch(changeReportComment(event.target.value));
|
||||||
};
|
};
|
||||||
|
@ -60,7 +58,23 @@ const ReasonStep = (_props: IReasonStep) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
const filterRuleType = (rule: any) => {
|
const filterRuleType = (rule: any) => {
|
||||||
const ruleTypeToFilter = isReportingAccount ? 'account' : 'content';
|
let ruleTypeToFilter = 'content';
|
||||||
|
|
||||||
|
switch (entityType) {
|
||||||
|
case ReportableEntities.ACCOUNT:
|
||||||
|
ruleTypeToFilter = 'account';
|
||||||
|
break;
|
||||||
|
case ReportableEntities.STATUS:
|
||||||
|
case ReportableEntities.CHAT_MESSAGE:
|
||||||
|
ruleTypeToFilter = 'content';
|
||||||
|
break;
|
||||||
|
case ReportableEntities.GROUP:
|
||||||
|
ruleTypeToFilter = 'group';
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
ruleTypeToFilter = 'content';
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
if (rule.rule_type) {
|
if (rule.rule_type) {
|
||||||
return rule.rule_type === ruleTypeToFilter;
|
return rule.rule_type === ruleTypeToFilter;
|
||||||
|
|
|
@ -1241,11 +1241,14 @@
|
||||||
"report.block_hint": "Do you also want to block this account?",
|
"report.block_hint": "Do you also want to block this account?",
|
||||||
"report.chatMessage.context": "When reporting a user’s message, the five messages before and five messages after the one selected will be passed along to our moderation team for context.",
|
"report.chatMessage.context": "When reporting a user’s message, the five messages before and five messages after the one selected will be passed along to our moderation team for context.",
|
||||||
"report.chatMessage.title": "Report message",
|
"report.chatMessage.title": "Report message",
|
||||||
"report.confirmation.content": "If we find that this account is violating the {link} we will take further action on the matter.",
|
"report.confirmation.content": "If we find that this {entity} is violating the {link} we will take further action on the matter.",
|
||||||
|
"report.confirmation.entity.account": "account",
|
||||||
|
"report.confirmation.entity.group": "group",
|
||||||
"report.confirmation.title": "Thanks for submitting your report.",
|
"report.confirmation.title": "Thanks for submitting your report.",
|
||||||
"report.done": "Done",
|
"report.done": "Done",
|
||||||
"report.forward": "Forward to {target}",
|
"report.forward": "Forward to {target}",
|
||||||
"report.forward_hint": "The account is from another server. Send a copy of the report there as well?",
|
"report.forward_hint": "The account is from another server. Send a copy of the report there as well?",
|
||||||
|
"report.group.title": "Report Group",
|
||||||
"report.next": "Next",
|
"report.next": "Next",
|
||||||
"report.otherActions.addAdditional": "Would you like to add additional statuses to this report?",
|
"report.otherActions.addAdditional": "Would you like to add additional statuses to this report?",
|
||||||
"report.otherActions.addMore": "Add more",
|
"report.otherActions.addMore": "Add more",
|
||||||
|
|
|
@ -8,6 +8,8 @@ describe('reports reducer', () => {
|
||||||
account_id: null,
|
account_id: null,
|
||||||
status_ids: [],
|
status_ids: [],
|
||||||
chat_message: null,
|
chat_message: null,
|
||||||
|
group: null,
|
||||||
|
entityType: '',
|
||||||
comment: '',
|
comment: '',
|
||||||
forward: false,
|
forward: false,
|
||||||
block: false,
|
block: false,
|
||||||
|
|
|
@ -1,7 +1,5 @@
|
||||||
import { Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
import { Record as ImmutableRecord, Set as ImmutableSet } from 'immutable';
|
||||||
|
|
||||||
import { ChatMessage } from 'soapbox/types/entities';
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
REPORT_INIT,
|
REPORT_INIT,
|
||||||
REPORT_SUBMIT_REQUEST,
|
REPORT_SUBMIT_REQUEST,
|
||||||
|
@ -13,15 +11,19 @@ import {
|
||||||
REPORT_FORWARD_CHANGE,
|
REPORT_FORWARD_CHANGE,
|
||||||
REPORT_BLOCK_CHANGE,
|
REPORT_BLOCK_CHANGE,
|
||||||
REPORT_RULE_CHANGE,
|
REPORT_RULE_CHANGE,
|
||||||
|
ReportableEntities,
|
||||||
} from '../actions/reports';
|
} from '../actions/reports';
|
||||||
|
|
||||||
import type { AnyAction } from 'redux';
|
import type { AnyAction } from 'redux';
|
||||||
|
import type { ChatMessage, Group } from 'soapbox/types/entities';
|
||||||
|
|
||||||
const NewReportRecord = ImmutableRecord({
|
const NewReportRecord = ImmutableRecord({
|
||||||
isSubmitting: false,
|
isSubmitting: false,
|
||||||
|
entityType: '' as ReportableEntities,
|
||||||
account_id: null as string | null,
|
account_id: null as string | null,
|
||||||
status_ids: ImmutableSet<string>(),
|
status_ids: ImmutableSet<string>(),
|
||||||
chat_message: null as null | ChatMessage,
|
chat_message: null as null | ChatMessage,
|
||||||
|
group: null as null | Group,
|
||||||
comment: '',
|
comment: '',
|
||||||
forward: false,
|
forward: false,
|
||||||
block: false,
|
block: false,
|
||||||
|
@ -40,11 +42,16 @@ export default function reports(state: State = ReducerRecord(), action: AnyActio
|
||||||
return state.withMutations(map => {
|
return state.withMutations(map => {
|
||||||
map.setIn(['new', 'isSubmitting'], false);
|
map.setIn(['new', 'isSubmitting'], false);
|
||||||
map.setIn(['new', 'account_id'], action.account.id);
|
map.setIn(['new', 'account_id'], action.account.id);
|
||||||
|
map.setIn(['new', 'entityType'], action.entityType);
|
||||||
|
|
||||||
if (action.chatMessage) {
|
if (action.chatMessage) {
|
||||||
map.setIn(['new', 'chat_message'], action.chatMessage);
|
map.setIn(['new', 'chat_message'], action.chatMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (action.group) {
|
||||||
|
map.setIn(['new', 'group'], action.group);
|
||||||
|
}
|
||||||
|
|
||||||
if (state.new.account_id !== action.account.id) {
|
if (state.new.account_id !== action.account.id) {
|
||||||
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.reblog?.id || action.status.id]) : ImmutableSet());
|
map.setIn(['new', 'status_ids'], action.status ? ImmutableSet([action.status.reblog?.id || action.status.id]) : ImmutableSet());
|
||||||
map.setIn(['new', 'comment'], '');
|
map.setIn(['new', 'comment'], '');
|
||||||
|
|
Loading…
Reference in a new issue