diff --git a/app/soapbox/actions/reports.ts b/app/soapbox/actions/reports.ts index dce162247..efb24bd1a 100644 --- a/app/soapbox/actions/reports.ts +++ b/app/soapbox/actions/reports.ts @@ -4,7 +4,7 @@ import { openModal } from './modals'; import type { AxiosError } from 'axios'; import type { AppDispatch, RootState } from 'soapbox/store'; -import type { Account, Status } from 'soapbox/types/entities'; +import type { Account, ChatMessage, Status } from 'soapbox/types/entities'; const REPORT_INIT = 'REPORT_INIT'; const REPORT_CANCEL = 'REPORT_CANCEL'; @@ -20,17 +20,25 @@ const REPORT_BLOCK_CHANGE = 'REPORT_BLOCK_CHANGE'; const REPORT_RULE_CHANGE = 'REPORT_RULE_CHANGE'; -const initReport = (account: Account, status?: Status) => - (dispatch: AppDispatch) => { - dispatch({ - type: REPORT_INIT, - account, - status, - }); +type ReportedEntity = { + status?: Status, + chatMessage?: ChatMessage +} - return dispatch(openModal('REPORT')); - }; +const initReport = (account: Account, entities?: ReportedEntity) => (dispatch: AppDispatch) => { + const { status, chatMessage } = entities || {}; + + dispatch({ + type: REPORT_INIT, + account, + status, + chatMessage, + }); + + return dispatch(openModal('REPORT')); +}; +// TODO: no longer used. Can be removed. const initReportById = (accountId: string) => (dispatch: AppDispatch, getState: () => RootState) => { dispatch({ @@ -59,6 +67,7 @@ const submitReport = () => return api(getState).post('/api/v1/reports', { account_id: reports.getIn(['new', 'account_id']), status_ids: reports.getIn(['new', 'status_ids']), + message_ids: [reports.getIn(['new', 'chat_message', 'id'])], rule_ids: reports.getIn(['new', 'rule_ids']), comment: reports.getIn(['new', 'comment']), forward: reports.getIn(['new', 'forward']), diff --git a/app/soapbox/components/status-action-bar.tsx b/app/soapbox/components/status-action-bar.tsx index 546d162a1..4e3bcba3b 100644 --- a/app/soapbox/components/status-action-bar.tsx +++ b/app/soapbox/components/status-action-bar.tsx @@ -251,7 +251,7 @@ const StatusActionBar: React.FC = ({ secondary: intl.formatMessage(messages.blockAndReport), onSecondary: () => { dispatch(blockAccount(account.id)); - dispatch(initReport(account, status)); + dispatch(initReport(account, { status })); }, })); }; @@ -270,7 +270,7 @@ const StatusActionBar: React.FC = ({ const handleReport: React.EventHandler = (e) => { e.stopPropagation(); - dispatch(initReport(status.account as Account, status)); + dispatch(initReport(status.account as Account, { status })); }; const handleConversationMuteClick: React.EventHandler = (e) => { diff --git a/app/soapbox/components/ui/divider/divider.tsx b/app/soapbox/components/ui/divider/divider.tsx index 343a1b948..ebe436fa9 100644 --- a/app/soapbox/components/ui/divider/divider.tsx +++ b/app/soapbox/components/ui/divider/divider.tsx @@ -18,7 +18,7 @@ const Divider = ({ text, textSize = 'md' }: IDivider) => ( {text && (
- + {text}
diff --git a/app/soapbox/features/chats/components/chat-message-list.tsx b/app/soapbox/features/chats/components/chat-message-list.tsx index 3f842bf35..42f323ef3 100644 --- a/app/soapbox/features/chats/components/chat-message-list.tsx +++ b/app/soapbox/features/chats/components/chat-message-list.tsx @@ -7,6 +7,7 @@ import React, { useState, useEffect, useRef } from 'react'; import { useIntl, defineMessages } from 'react-intl'; import { openModal } from 'soapbox/actions/modals'; +import { initReport } from 'soapbox/actions/reports'; import { Avatar, Button, Divider, HStack, Spinner, Stack, Text } from 'soapbox/components/ui'; import DropdownMenuContainer from 'soapbox/containers/dropdown_menu_container'; // import emojify from 'soapbox/features/emoji/emoji'; @@ -14,6 +15,7 @@ import PlaceholderChatMessage from 'soapbox/features/placeholder/components/plac import Bundle from 'soapbox/features/ui/components/bundle'; import { MediaGallery } from 'soapbox/features/ui/util/async-components'; import { useAppSelector, useAppDispatch, useOwnAccount } from 'soapbox/hooks'; +import { normalizeAccount } from 'soapbox/normalizers'; import { chatKeys, IChat, IChatMessage, useChat, useChatMessages } from 'soapbox/queries/chats'; import { queryClient } from 'soapbox/queries/client'; import { onlyEmoji } from 'soapbox/utils/rich_content'; @@ -246,7 +248,7 @@ const ChatMessageList: React.FC = ({ chat, autosize }) => { } else { menu.push({ text: intl.formatMessage(messages.report), - action: () => null, // TODO: implement once API is available + action: () => dispatch(initReport(normalizeAccount(chat.account) as any, { chatMessage })), icon: require('@tabler/icons/flag.svg'), }); menu.push({ diff --git a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx index a0f52af13..e889484a2 100644 --- a/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx +++ b/app/soapbox/features/ui/components/modals/report-modal/report-modal.tsx @@ -6,7 +6,7 @@ import { submitReport, submitReportSuccess, submitReportFail } from 'soapbox/act import { expandAccountTimeline } from 'soapbox/actions/timelines'; import AttachmentThumbs from 'soapbox/components/attachment-thumbs'; import StatusContent from 'soapbox/components/status_content'; -import { Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui'; +import { Avatar, HStack, Modal, ProgressBar, Stack, Text } from 'soapbox/components/ui'; import AccountContainer from 'soapbox/containers/account_container'; import { useAccount, useAppDispatch, useAppSelector } from 'soapbox/hooks'; @@ -74,6 +74,12 @@ interface IReportModal { onClose: () => void } +enum ReportedEntities { + Account = 'Account', + Status = 'Status', + ChatMessage = 'ChatMessage' +} + const ReportModal = ({ onClose }: IReportModal) => { const dispatch = useAppDispatch(); const intl = useIntl(); @@ -86,10 +92,23 @@ const ReportModal = ({ onClose }: IReportModal) => { const rules = useAppSelector((state) => state.rules.items); const ruleIds = useAppSelector((state) => state.reports.new.rule_ids); const selectedStatusIds = useAppSelector((state) => state.reports.new.status_ids); + const selectedChatMessage = useAppSelector((state) => state.reports.new.chat_message); - const isReportingAccount = useMemo(() => selectedStatusIds.size === 0, []); const shouldRequireRule = rules.length > 0; + const reportedEntity = useMemo(() => { + if (selectedStatusIds.size === 0 && !selectedChatMessage) { + return ReportedEntities.Account; + } 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.ONE); const handleSubmit = () => { @@ -132,6 +151,31 @@ const ReportModal = ({ onClose }: IReportModal) => { } }, [selectedStatusIds.size]); + const renderSelectedChatMessage = () => { + if (account) { + return ( + +
+ +
+ +
+ +
+
+ ); + } + }; + + const renderSelectedEntity = () => { + switch (reportedEntity) { + case ReportedEntities.Status: + return renderSelectedStatuses(); + case ReportedEntities.ChatMessage: + return renderSelectedChatMessage(); + } + }; + const confirmationText = useMemo(() => { switch (currentStep) { case Steps.TWO: @@ -148,8 +192,8 @@ const ReportModal = ({ onClose }: IReportModal) => { return false; } - return isSubmitting || (shouldRequireRule && ruleIds.isEmpty()) || (!isReportingAccount && selectedStatusIds.size === 0); - }, [currentStep, isSubmitting, shouldRequireRule, ruleIds, selectedStatusIds.size, isReportingAccount]); + return isSubmitting || (shouldRequireRule && ruleIds.isEmpty()) || (isReportingStatus && selectedStatusIds.size === 0); + }, [currentStep, isSubmitting, shouldRequireRule, ruleIds, selectedStatusIds.size, isReportingStatus]); const calculateProgress = useCallback(() => { switch (currentStep) { @@ -189,7 +233,7 @@ const ReportModal = ({ onClose }: IReportModal) => { - {(currentStep !== Steps.THREE && !isReportingAccount) && renderSelectedStatuses()} + {(currentStep !== Steps.THREE && !isReportingAccount) && renderSelectedEntity()} diff --git a/app/soapbox/reducers/reports.ts b/app/soapbox/reducers/reports.ts index 1ca8ead41..71d0325fc 100644 --- a/app/soapbox/reducers/reports.ts +++ b/app/soapbox/reducers/reports.ts @@ -1,5 +1,7 @@ import { Record as ImmutableRecord, Set as ImmutableSet } from 'immutable'; +import { ChatMessage } from 'soapbox/types/entities'; + import { REPORT_INIT, REPORT_SUBMIT_REQUEST, @@ -19,6 +21,7 @@ const NewReportRecord = ImmutableRecord({ isSubmitting: false, account_id: null as string | null, status_ids: ImmutableSet(), + chat_message: null as null | ChatMessage, comment: '', forward: false, block: false, @@ -38,6 +41,10 @@ export default function reports(state: State = ReducerRecord(), action: AnyActio map.setIn(['new', 'isSubmitting'], false); map.setIn(['new', 'account_id'], action.account.id); + if (action.chatMessage) { + map.setIn(['new', 'chat_message'], action.chatMessage); + } + 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', 'comment'], ''); @@ -76,6 +83,7 @@ export default function reports(state: State = ReducerRecord(), action: AnyActio return state.withMutations(map => { map.setIn(['new', 'account_id'], null); map.setIn(['new', 'status_ids'], ImmutableSet()); + map.setIn(['new', 'chat_message'], null); map.setIn(['new', 'comment'], ''); map.setIn(['new', 'isSubmitting'], false); map.setIn(['new', 'rule_ids'], ImmutableSet());