refactor(app): remove the report modal path selection step (#2548)

This commit is contained in:
Hampus authored and GitHub committed 2026-09-06 18:49:35 +02:00
1 parent a2d6477b42
commit 798e64b224
38 files changed
+3893 -5055

No files matched your search

@@ -7,7 +7,6 @@ import type {
} from '@fluxer/schema/src/domains/report/ReportSchemas';
export type IARReportContextType = 'message' | 'user' | 'guild';
export type IARPrimaryPath = 'platform' | 'community' | 'preference';
export type IARRuleReasonId =
| 'harassment'
| 'hate'
@@ -22,7 +22,6 @@ import {BanMemberModal} from '@app/features/moderation/components/modals/BanMemb
import {IARActionCards} from '@app/features/moderation/components/report_modal/IARActionCards';
import {
getReportCategoryForReason,
type IARPrimaryPath,
type IARRuleCategoryId,
type IARRuleReasonId,
} from '@app/features/moderation/components/report_modal/IARFlowUtils';
@@ -33,8 +32,6 @@ import {
getIARCategoryTitle,
getIARChildSafetyRoutingNote,
getIARModalDescription,
getIARPathTitle,
getIARPrimaryOptions,
getIARReasonTitle,
getIARReportEligibilityCopy,
getIARRuleCategoryOptions,
@@ -104,18 +101,16 @@ interface IARModalProps {
}
const logger = new Logger('IARModal');
const STEP_ORDER: ReadonlyArray<IARStep> = ['path', 'category', 'reason', 'guidance', 'success'];
const STEP_ORDER: ReadonlyArray<IARStep> = ['category', 'reason', 'success'];
export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
const {i18n} = useLingui();
const leaveGuild = useLeaveGuild();
const [step, setStep] = useState<IARStep>('path');
const [selectedPath, setSelectedPath] = useState<IARPrimaryPath | null>(null);
const [step, setStep] = useState<IARStep>('category');
const [selectedCategory, setSelectedCategory] = useState<IARRuleCategoryId | null>(null);
const [selectedReason, setSelectedReason] = useState<IARRuleReasonId | null>(null);
const [submitting, setSubmitting] = useState(false);
const canSubmitPlatformReport = canSubmitReport();
const resolvedContext = resolveIARModalContext(i18n, context);
const primaryOptions = getIARPrimaryOptions(i18n, context, resolvedContext);
const ruleCategoryOptions = getIARRuleCategoryOptions(i18n);
const ruleReasonOptions = getIARRuleReasonOptions(i18n, context.type, selectedCategory);
const childSafetyRoutingNote = getIARChildSafetyRoutingNote(i18n, context.type, selectedReason);
@@ -241,67 +236,38 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
onDeleteMessage: handleDeleteMessage,
onBanUser: handleBanUser,
};
const guidanceActionCards = getIARActionCards(i18n, context, resolvedContext, actionHandlers, {
includeModerationActions: false,
});
const successActionCards = getIARActionCards(i18n, context, resolvedContext, actionHandlers, {
isMessageDeleted,
isUserBanned: isUserKnownBanned,
includeModerationActions: true,
});
const handleBack = useCallback(() => {
switch (step) {
case 'path':
closeModal();
return;
case 'category':
case 'guidance':
setStep('path');
case 'success':
closeModal();
return;
case 'reason':
setSelectedReason(null);
setStep('category');
return;
case 'success':
closeModal();
return;
}
}, [closeModal, step]);
const handleContinue = useCallback(() => {
if (step === 'path') {
if (selectedPath === null) {
showModerationErrorModal(
i18n,
() => i18n._(PICK_AN_OPTION_TO_CONTINUE_DESCRIPTOR),
'moderation.iar-modal.path-required-error-modal',
);
return;
}
if (selectedPath === 'platform') {
if (!canSubmitPlatformReport) {
showReportRestrictionDialog();
return;
}
setStep('category');
return;
}
setStep('guidance');
if (selectedCategory === null) {
showModerationErrorModal(
i18n,
() => i18n._(PICK_AN_OPTION_TO_CONTINUE_DESCRIPTOR),
'moderation.iar-modal.category-required-error-modal',
);
return;
}
if (step === 'category') {
if (selectedCategory === null) {
showModerationErrorModal(
i18n,
() => i18n._(PICK_AN_OPTION_TO_CONTINUE_DESCRIPTOR),
'moderation.iar-modal.category-required-error-modal',
);
return;
}
setSelectedReason(null);
setStep('reason');
if (!canSubmitPlatformReport) {
showReportRestrictionDialog();
return;
}
}, [canSubmitPlatformReport, i18n, selectedCategory, selectedPath, step]);
setSelectedReason(null);
setStep('reason');
}, [canSubmitPlatformReport, i18n, selectedCategory]);
const handleSubmit = useCallback(async () => {
if (!canSubmitPlatformReport) {
showReportRestrictionDialog();
@@ -336,49 +302,6 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
setSubmitting(false);
}
}, [canSubmitPlatformReport, context, i18n, reportCategory, selectedReason]);
const renderPathStep = () => (
<div className={styles.step} data-flx="moderation.iar-modal.render-path-step.step">
<IARModalPreview
context={context}
currentChannel={resolvedContext.currentChannel}
data-flx="moderation.iar-modal.render-path-step.iar-modal-preview"
/>
{!canSubmitPlatformReport && (
<div className={styles.notice} data-flx="moderation.iar-modal.render-path-step.notice">
<div
className={styles.noticeTitle}
data-flx="moderation.report-modal.iar-modal.render-path-step.notice-title"
>
{reportEligibilityCopy.title}
</div>
<div className={styles.noticeText} data-flx="moderation.report-modal.iar-modal.render-path-step.notice-text">
{reportEligibilityCopy.body}
</div>
<div
className={styles.noticeActions}
data-flx="moderation.report-modal.iar-modal.render-path-step.notice-actions"
>
<Button
variant="secondary"
small
fitContent
onClick={handleOpenReportEligibility}
data-flx="moderation.iar-modal.render-path-step.button.open-report-eligibility"
>
{i18n._(FINISH_ACCOUNT_SETUP_DESCRIPTOR)}
</Button>
</div>
</div>
)}
<RadioGroup<IARPrimaryPath>
options={primaryOptions}
value={selectedPath}
onChange={setSelectedPath}
aria-label={getIARPathTitle(i18n)}
data-flx="moderation.iar-modal.render-path-step.radio-group.set-selected-path"
/>
</div>
);
const renderCategoryStep = () => (
<div className={styles.step} data-flx="moderation.iar-modal.render-category-step.step">
<h2 className={styles.stepTitle} data-flx="moderation.iar-modal.render-category-step.step-title">
@@ -389,6 +312,36 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
currentChannel={resolvedContext.currentChannel}
data-flx="moderation.iar-modal.render-category-step.iar-modal-preview"
/>
{!canSubmitPlatformReport && (
<div className={styles.notice} data-flx="moderation.iar-modal.render-category-step.notice">
<div
className={styles.noticeTitle}
data-flx="moderation.report-modal.iar-modal.render-category-step.notice-title"
>
{reportEligibilityCopy.title}
</div>
<div
className={styles.noticeText}
data-flx="moderation.report-modal.iar-modal.render-category-step.notice-text"
>
{reportEligibilityCopy.body}
</div>
<div
className={styles.noticeActions}
data-flx="moderation.report-modal.iar-modal.render-category-step.notice-actions"
>
<Button
variant="secondary"
small
fitContent
onClick={handleOpenReportEligibility}
data-flx="moderation.iar-modal.render-category-step.button.open-report-eligibility"
>
{i18n._(FINISH_ACCOUNT_SETUP_DESCRIPTOR)}
</Button>
</div>
</div>
)}
<RadioGroup<IARRuleCategoryId>
options={ruleCategoryOptions}
value={selectedCategory}
@@ -427,17 +380,6 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
)}
</div>
);
const renderGuidanceStep = () => {
if (selectedPath === null || selectedPath === 'platform') return null;
return (
<div className={styles.step} data-flx="moderation.iar-modal.render-guidance-step.step">
<IARActionCards
cards={guidanceActionCards}
data-flx="moderation.iar-modal.render-guidance-step.iar-action-cards"
/>
</div>
);
};
const renderSuccessStep = () => {
const successCopy = getIARSuccessCopy(i18n);
return (
@@ -457,40 +399,26 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
};
const renderStep = (): React.ReactNode => {
switch (step) {
case 'path':
return renderPathStep();
case 'category':
return renderCategoryStep();
case 'reason':
return renderReasonStep();
case 'guidance':
return renderGuidanceStep();
case 'success':
return renderSuccessStep();
}
};
const getSecondaryFooterLabel = (): string => {
return step === 'path' ? i18n._(CANCEL_DESCRIPTOR) : i18n._(BACK_DESCRIPTOR);
return step === 'category' ? i18n._(CANCEL_DESCRIPTOR) : i18n._(BACK_DESCRIPTOR);
};
const showSecondaryFooterButton = step !== 'success';
const renderPrimaryFooterButton = () => {
switch (step) {
case 'path':
return (
<Button
onClick={handleContinue}
disabled={selectedPath === null || submitting}
data-flx="moderation.iar-modal.render-primary-footer-button.button.continue"
>
{i18n._(CONTINUE_DESCRIPTOR)}
</Button>
);
case 'category':
return (
<Button
onClick={handleContinue}
disabled={selectedCategory === null || submitting}
data-flx="moderation.iar-modal.render-primary-footer-button.button.continue-category"
data-flx="moderation.iar-modal.render-primary-footer-button.button.continue"
>
{i18n._(CONTINUE_DESCRIPTOR)}
</Button>
@@ -506,7 +434,6 @@ export const IARModal: React.FC<IARModalProps> = observer(({context}) => {
{i18n._(SEND_REPORT_DESCRIPTOR)}
</Button>
);
case 'guidance':
case 'success':
return (
<Button
@@ -3,7 +3,6 @@
import {PRODUCT_NAME} from '@app/features/app/config/I18nDisplayConstants';
import {CLOSE_DM_DESCRIPTOR} from '@app/features/channel/utils/ChannelMessageDescriptors';
import {
type IARPrimaryPath,
type IARReportContextType,
type IARRuleCategoryId,
type IARRuleReasonId,
@@ -21,23 +20,6 @@ import {BLOCK_DESCRIPTOR} from '@app/features/moderation/utils/ModerationMessage
import type {I18n} from '@lingui/core';
import {msg} from '@lingui/core/macro';
const I_DON_T_LIKE_THIS_CONTENT_DESCRIPTOR = msg({
message: "I don't like this content",
comment: 'IAR modal: preference path option label for a message report.',
});
const I_DON_T_LIKE_THIS_USER_DESCRIPTOR = msg({
message: "I don't like this user",
comment: 'IAR modal: preference path option label for a user report.',
});
const I_DON_T_LIKE_THIS_COMMUNITY_DESCRIPTOR = msg({
message: "I don't like this community",
comment: 'IAR modal: preference path option label for a community report.',
});
const WHAT_DO_YOU_NEED_DESCRIPTOR = msg({
message: 'What do you need?',
comment:
'IAR modal: accessible label (aria-label only — not shown visually) for the primary-path radio group on step 1.',
});
const FINISH_ACCOUNT_SETUP_FIRST_DESCRIPTOR = msg({
message: 'Finish account setup first',
comment: 'IAR modal: title of the account-not-ready notice.',
@@ -107,14 +89,6 @@ const USE_CHILD_SAFETY_INSTEAD_DESCRIPTOR = msg({
message: 'If a minor is involved, use "{childSafetyReasonName}" instead.',
comment: 'IAR modal: inline routing hint shown under reasons that overlap with child safety.',
});
const REPORT_A_PLATFORM_RULE_VIOLATION_DESCRIPTOR = msg({
message: 'Report a platform rule violation',
comment: 'IAR modal: primary path label.',
});
const REPORT_TO_COMMUNITY_MODS_PATH_DESCRIPTOR = msg({
message: 'Report to the moderators of this community',
comment: 'IAR modal: community-mod path label.',
});
const REPORT_SENT_TITLE_DESCRIPTOR = msg({
message: 'Report sent',
comment: 'IAR modal: title on the success step shown after a platform report is submitted.',
@@ -473,25 +447,10 @@ function shouldShowContactSettings(context: IARContext): boolean {
return context.type !== 'guild';
}
function getPreferenceOptionName(i18n: I18n, context: IARContext): string {
switch (context.type) {
case 'message':
return i18n._(I_DON_T_LIKE_THIS_CONTENT_DESCRIPTOR);
case 'user':
return i18n._(I_DON_T_LIKE_THIS_USER_DESCRIPTOR);
case 'guild':
return i18n._(I_DON_T_LIKE_THIS_COMMUNITY_DESCRIPTOR);
}
}
export function getIARModalDescription(i18n: I18n): string {
return i18n._(MODAL_DESCRIPTION_DESCRIPTOR);
}
export function getIARPathTitle(i18n: I18n): string {
return i18n._(WHAT_DO_YOU_NEED_DESCRIPTOR);
}
export function getIARReportEligibilityCopy(i18n: I18n): IARCopyBlock {
return {
title: i18n._(FINISH_ACCOUNT_SETUP_FIRST_DESCRIPTOR),
@@ -579,30 +538,6 @@ export function getIARChildSafetyRoutingNote(
}
}
export function getIARPrimaryOptions(
i18n: I18n,
context: IARContext,
resolvedContext: IARResolvedContext,
): Array<IARRadioOption<IARPrimaryPath>> {
const options: Array<IARRadioOption<IARPrimaryPath>> = [
{
value: 'platform',
name: i18n._(REPORT_A_PLATFORM_RULE_VIOLATION_DESCRIPTOR),
},
];
if (resolvedContext.hasCommunityContext) {
options.push({
value: 'community',
name: i18n._(REPORT_TO_COMMUNITY_MODS_PATH_DESCRIPTOR),
});
}
options.push({
value: 'preference',
name: getPreferenceOptionName(i18n, context),
});
return options;
}
function getMessageRuleReasonOptions(i18n: I18n): Array<IARRadioOption<IARRuleReasonId>> {
return [
{
@@ -781,7 +716,6 @@ export function getIARSpecialSafetyNote(i18n: I18n, selectedReason: IARRuleReaso
export interface IARActionCardsOptions {
isMessageDeleted?: boolean;
isUserBanned?: boolean;
includeModerationActions?: boolean;
}
export function getIARActionCards(
@@ -792,7 +726,7 @@ export function getIARActionCards(
options: IARActionCardsOptions = {},
): Array<IARActionCardConfig> {
const cards: Array<IARActionCardConfig> = [];
const {isMessageDeleted = false, isUserBanned = false, includeModerationActions = true} = options;
const {isMessageDeleted = false, isUserBanned = false} = options;
if (!resolvedContext.isReportedUserBlocked && resolvedContext.reportedUser !== null) {
cards.push({
id: 'block-user',
@@ -833,7 +767,7 @@ export function getIARActionCards(
onClick: handlers.onLeaveCommunity,
});
}
if (includeModerationActions && resolvedContext.canDeleteReportedMessage) {
if (resolvedContext.canDeleteReportedMessage) {
const deleteDisabled = isMessageDeleted;
cards.push({
id: 'delete-message',
@@ -846,7 +780,7 @@ export function getIARActionCards(
disabledTooltip: deleteDisabled ? i18n._(ALREADY_DELETED_TOOLTIP_DESCRIPTOR) : undefined,
});
}
if (includeModerationActions && resolvedContext.canBanReportedUser) {
if (resolvedContext.canBanReportedUser) {
const banDisabled = isUserBanned;
cards.push({
id: 'ban-user',
@@ -37,7 +37,7 @@ export type IARRadioOption<T> = RadioOption<T> & {
name: string;
desc?: string;
};
export type IARStep = 'path' | 'category' | 'reason' | 'guidance' | 'success';
export type IARStep = 'category' | 'reason' | 'success';
export type IARContext =
| {
type: 'message';